diff --git "a/blog-site/content/posts/annex/xmind/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.xmind" "b/blog-site/content/posts/annex/xmind/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.xmind" new file mode 100644 index 00000000..190d9a9e Binary files /dev/null and "b/blog-site/content/posts/annex/xmind/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.xmind" differ diff --git a/blog-site/content/posts/books/JavaBooks.md b/blog-site/content/posts/books/JavaBooks.md index 93687508..20d64723 100644 --- a/blog-site/content/posts/books/JavaBooks.md +++ b/blog-site/content/posts/books/JavaBooks.md @@ -6,15 +6,17 @@ tags: ["书籍"] slug: "java-books" --- -- [HeadFirst设计模式](/iblog/posts/annex/pdf/books/HeadFirst设计模式.pdf) -- [Java数据结构和算法](/iblog/posts/annex/pdf/books/Java数据结构和算法.pdf) -- [Java核心技术卷I基础知识](/iblog/posts/annex/pdf/books/Java核心技术卷I基础知识.pdf) -- [Java编程思想](/iblog/posts/annex/pdf/books/Java编程思想.pdf) -- [代码整洁之道](/iblog/posts/annex/pdf/books/代码整洁之道.pdf) -- [大型网站技术架构](/iblog/posts/annex/pdf/books/大型网站技术架构.pdf) -- [大话数据结构](/iblog/posts/annex/pdf/books/大话数据结构.pdf) -- [深入分析JavaWeb技术内幕](/iblog/posts/annex/pdf/books/深入分析JavaWeb技术内幕.pdf) -- [疯狂Java讲义](/iblog/posts/annex/pdf/books/疯狂Java讲义.pdf) -- [重构:改善既有代码的设计](/iblog/posts/annex/pdf/books/重构:改善既有代码的设计.pdf) -- [领域驱动设计](/iblog/posts/annex/pdf/books/领域驱动设计.pdf) - +- [HeadFirst设计模式](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [Java数据结构和算法](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [Java核心技术卷I基础知识](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [Java编程思想](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [代码整洁之道](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [大型网站技术架构](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [大话数据结构](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [深入分析JavaWeb技术内幕](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [疯狂Java讲义](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [重构:改善既有代码的设计](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [领域驱动设计](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [深入理解计算机系统](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [Java并发编程的艺术](https://pan.baidu.com/s/1mXGLnTJqGtGeGlrePzx3fA?pwd=8888) +- [...](https://www.jiumodiary.com) \ No newline at end of file diff --git "a/blog-site/content/posts/essays/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.md" "b/blog-site/content/posts/essays/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.md" new file mode 100644 index 00000000..4513a8ea --- /dev/null +++ "b/blog-site/content/posts/essays/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.md" @@ -0,0 +1,95 @@ +--- +title: "前端学习路线" +date: 2024-02-29 +draft: false +tags: ["学习路线"] +slug: "front-learning-route" +--- + + +## 基础知识 +### 网络知识 +#### HTTP +#### DNS +#### 域名 +#### 云服务 +#### 网络安全 +- HTTPS +- CORS +- 网络渗透 +- OWASP +### HTML +### CSS +### JavaScript +### JQuery +### Ajax +### ES6-ES11 +### 综合应用 + +## 工程化体系 +### 代码规范 +### CSS预处理器 +- Less +- Sass +- PostCSS +### Node +### Promise +### Axios +### 工具 +#### 包管理工具 +- Npm +- Yarn +#### 打包工具 +- Webpack +- Parcel +#### 代码格式化工具 +- ESLint +- Prettier +#### 调试工具 +- Chrome +- IETest +- Postman +#### 版本管理工具 +- Git +- GitLab +- GitHub +#### 部署发布工具 +- Jenkins +- CICD +### 主流技术 +- TypeScript +- Vue +- React +- Angular +- 综合应用 +## 静态站点生成器 +- Next +- GatsbyJS +- Nuxt +- Vuepress +- Hugo +## 性能优化和监控 +### 性能优化概览 +### 浏览器及工作方式 +### SEO +### 资源管理 +- 延迟加载 +- 按需加载 +- 缓存复用 +- CDN部署 +- 请求合并 +- 异步同步 +## 移动端 +### Native App +- 安卓原生 +- IOS原生 +- 鸿蒙原生 +### Web App +- Uni-App +- Taro +- React Native +- Flutter + 1. 基础 + 2. 实战 +### 微信小程序 + diff --git "a/blog-site/content/posts/exam/\345\244\207\350\200\203\346\210\220\344\272\272\346\234\254\347\247\221\345\255\246\344\275\215\350\213\261\350\257\255.md" "b/blog-site/content/posts/exam/\345\244\207\350\200\203\346\210\220\344\272\272\346\234\254\347\247\221\345\255\246\344\275\215\350\213\261\350\257\255.md" index 57a43d63..0b177d39 100644 --- "a/blog-site/content/posts/exam/\345\244\207\350\200\203\346\210\220\344\272\272\346\234\254\347\247\221\345\255\246\344\275\215\350\213\261\350\257\255.md" +++ "b/blog-site/content/posts/exam/\345\244\207\350\200\203\346\210\220\344\272\272\346\234\254\347\247\221\345\255\246\344\275\215\350\213\261\350\257\255.md" @@ -1,5 +1,6 @@ --- title: "备考成人本科学位英语" +password: 2022englishexam date: 2022-03-24 hidden: true draft: true diff --git "a/blog-site/content/posts/resume/20201124\347\256\200\345\216\206.md" "b/blog-site/content/posts/resume/20201124\347\256\200\345\216\206.md" index 24860038..34e717de 100644 --- "a/blog-site/content/posts/resume/20201124\347\256\200\345\216\206.md" +++ "b/blog-site/content/posts/resume/20201124\347\256\200\345\216\206.md" @@ -1,8 +1,9 @@ --- title: "20201124简历" +password: 20201124resume date: 2020-11-24 -draft: true -tags: ["简历"] +draft: false +tags: ["简历","求职"] slug: "interview-resume-20201124" --- diff --git "a/blog-site/content/posts/resume/20220422\347\256\200\345\216\206.md" "b/blog-site/content/posts/resume/20220422\347\256\200\345\216\206.md" index 4eca3af1..b02c2e34 100644 --- "a/blog-site/content/posts/resume/20220422\347\256\200\345\216\206.md" +++ "b/blog-site/content/posts/resume/20220422\347\256\200\345\216\206.md" @@ -1,8 +1,9 @@ --- title: "20220422简历" +password: 20220422resume date: 2022-04-22 -draft: true -tags: ["简历"] +draft: false +tags: ["简历","求职"] slug: "interview-resume-20220422" --- diff --git "a/blog-site/content/posts/resume/20230915\347\256\200\345\216\206.md" "b/blog-site/content/posts/resume/20230915\347\256\200\345\216\206.md" index f1ce8179..3ea569b4 100644 --- "a/blog-site/content/posts/resume/20230915\347\256\200\345\216\206.md" +++ "b/blog-site/content/posts/resume/20230915\347\256\200\345\216\206.md" @@ -1,8 +1,9 @@ --- title: "20230915简历" +password: 20230915resume date: 2023-09-15 -draft: true -tags: ["简历"] +draft: false +tags: ["简历","求职"] slug: "interview-resume-20230915" --- diff --git "a/blog-site/content/posts/resume/\351\235\242\350\257\225Java\345\217\257\350\203\275\344\274\232\350\242\253\351\227\256\345\210\260\347\232\204\351\227\256\351\242\230.md" "b/blog-site/content/posts/resume/\351\235\242\350\257\225Java\345\217\257\350\203\275\344\274\232\350\242\253\351\227\256\345\210\260\347\232\204\351\227\256\351\242\230.md" index 8b5194fd..f88ccd95 100644 --- "a/blog-site/content/posts/resume/\351\235\242\350\257\225Java\345\217\257\350\203\275\344\274\232\350\242\253\351\227\256\345\210\260\347\232\204\351\227\256\351\242\230.md" +++ "b/blog-site/content/posts/resume/\351\235\242\350\257\225Java\345\217\257\350\203\275\344\274\232\350\242\253\351\227\256\345\210\260\347\232\204\351\227\256\351\242\230.md" @@ -2,7 +2,7 @@ title: "面试Java可能会被问到的问题" date: 2021-05-11 draft: false -tags: ["Java", "求职"] +tags: ["求职"] slug: "interview-junior-javaer" --- diff --git "a/blog-site/content/posts/resume/\351\235\242\350\257\225\344\270\255\345\270\270\350\247\201\347\232\204\351\227\256\351\242\230.md" "b/blog-site/content/posts/resume/\351\235\242\350\257\225\344\270\255\345\270\270\350\247\201\347\232\204\351\227\256\351\242\230.md" index b2b7827d..f6acf883 100644 --- "a/blog-site/content/posts/resume/\351\235\242\350\257\225\344\270\255\345\270\270\350\247\201\347\232\204\351\227\256\351\242\230.md" +++ "b/blog-site/content/posts/resume/\351\235\242\350\257\225\344\270\255\345\270\270\350\247\201\347\232\204\351\227\256\351\242\230.md" @@ -2,7 +2,7 @@ title: "面试中常见的问题" date: 2021-04-23 draft: false -tags: ["Java","求职"] +tags: ["求职"] slug: "Interview-questions-and-answers" --- diff --git "a/blog-site/content/posts/worksummary/2019\345\267\245\344\275\234\346\200\273\347\273\223.md" "b/blog-site/content/posts/worksummary/2019\345\267\245\344\275\234\346\200\273\347\273\223.md" index 8ec3af55..43012f99 100644 --- "a/blog-site/content/posts/worksummary/2019\345\267\245\344\275\234\346\200\273\347\273\223.md" +++ "b/blog-site/content/posts/worksummary/2019\345\267\245\344\275\234\346\200\273\347\273\223.md" @@ -1,22 +1,19 @@ --- title: "2019工作总结" +password: 2019summary date: 2019-12-01 -draft: true +draft: false tags: ["工作总结"] slug: "work-summary-2019" --- 本人在进入公司起,期间一直对自己要求严谨,遵守公司的相应制度. 在过去的一个月时间里,我参与了贵州银行的电子验印系统的开发,一直努力完成和完善分配给我的任务,在这一个月发现了自身还有很多的不足,所以抱着虚心学习的态度,学习公司的开发流程,了解公司的产品架构,主要技术,主动和同事沟通,学习经验,希望能快速融入公司,能够全心的投入工作. - 试用期完成的工作有限,主要负责验印系统的统一门开发,学习了一些新技术,因为自己在经验上不足,对于技术的学习和掌握还不够深入,发现问题的能力还不够,所以拖慢了自己的开发进度,简单列一些. 通过开发的过程中,学习并掌握了vue框架的使用,学习到了Oracle数据库的使用..... 使我认识到了一个称职的开发人员应当具备良好的语言表达能力,较强的逻辑能力,灵活的处理应变能力,有效的对外联系能力. 在参与项目的开发过程中,发现很多看似简单的工作,其实里面有很多技巧 - 今后,我会多注意在这些方面的学习和积累,努力做好开发人员的本职工作,注重工作态度,把自己的工作做好做扎实,为项目的开发及公司的发展贡献自己的一份力量.在工作的这段时间里.我得到了同事的帮助,经常与我交流,指出技术上的问题,传授了很多开发经验,在生活上也给与快了我很大的帮助,使得我很快就适应了这里的生活. - 整个工作学习过程中,我认为自己工作比较认真负责.具有较强的责任心和进取心,能完成领导交付的工作,但也存在着许多缺点与不足,对工作的专业性还不够,业务经验不够丰富,对于发现问题的处理还不是很全面,我会在以后的工作中不断实践和总结,并积极学习新知识,弥补自身不足,来提高自己的综合素质. 总之,认真的回顾了这段时间的工作,发现了一些不足之处,这都是我在接下的工作中需要完善的.同时,也会尽最大努力的学习和积累经验,逐步发展成一个全面的技术开发人员,更好的完成工作. - 以上是我对2019年的工作总结及2020年工作计划,可能还不是很成熟,希望领导指正.展望2020年,我会更加努力,认真负责的去对待工作,相信自己会完成新的任务,能迎接新的挑战。为公司的发展做出自己的贡献. diff --git "a/blog-site/content/posts/worksummary/2023\345\267\245\344\275\234\346\200\273\347\273\223.md" "b/blog-site/content/posts/worksummary/2023\345\267\245\344\275\234\346\200\273\347\273\223.md" index 14b4cb45..6aa007f1 100644 --- "a/blog-site/content/posts/worksummary/2023\345\267\245\344\275\234\346\200\273\347\273\223.md" +++ "b/blog-site/content/posts/worksummary/2023\345\267\245\344\275\234\346\200\273\347\273\223.md" @@ -1,7 +1,8 @@ --- title: "2023工作总结" +password: 2023summary date: 2023-12-01 -draft: true +draft: false tags: ["工作总结"] slug: "work-summary-2023" --- diff --git a/blog-site/public/404.html b/blog-site/public/404.html new file mode 100644 index 00000000..0d747144 --- /dev/null +++ b/blog-site/public/404.html @@ -0,0 +1,166 @@ + + + + + + + + + + + 404 Page not found | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+

○| ̄|_ =3

+

404 Page Not Found

+

首页

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/about/index.html b/blog-site/public/about/index.html new file mode 100644 index 00000000..4453cf61 --- /dev/null +++ b/blog-site/public/about/index.html @@ -0,0 +1,491 @@ + + + + + + + + + + + 关于 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

关于

+ 2021.02.20 +
+

搭建博客

+

本博客使用 hugo + GitHubPage 进行搭建,使用的主题为 zozo Designed by VarKai

+

如果你想要使用 hugo 搭建博客,可以参考以下相关资料:

+ +

面试资料

+

目前只有面试Java开发的相关资料,包括从网上收集和自己整理的一些,希望可以帮助到一些人吧。

+ +

学习参考资料

+

因为本人目前主要是做Java开发相关的工作,所以这里的博客文章、参考资料大部分都和Java相关,如果您不想看到Java可以直接关闭本页面。

+

大厂技术博客

+ +

资源网站

+ +
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + +
+ + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/categories/index.html b/blog-site/public/categories/index.html new file mode 100644 index 00000000..a0dcbdbb --- /dev/null +++ b/blog-site/public/categories/index.html @@ -0,0 +1,187 @@ + + + + + + + + + + + Categories | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
    + + +
+
+ +
+
+
+ + + + +
+ + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/categories/index.xml b/blog-site/public/categories/index.xml new file mode 100644 index 00000000..47689248 --- /dev/null +++ b/blog-site/public/categories/index.xml @@ -0,0 +1,11 @@ + + + + Categories on 唯手熟尔 + http://localhost:1313/iblog/categories/ + Recent content in Categories on 唯手熟尔 + Hugo -- gohugo.io + zh + + + diff --git a/blog-site/public/css/animate.min.css b/blog-site/public/css/animate.min.css new file mode 100644 index 00000000..333d7402 --- /dev/null +++ b/blog-site/public/css/animate.min.css @@ -0,0 +1,9 @@ + +@charset "UTF-8"; +/*! + * animate.css - https://animate.style/ + * Version - 4.1.0 + * Licensed under the MIT license - http://opensource.org/licenses/MIT + * + * Copyright (c) 2020 Animate.css + */:root{--animate-duration:1s;--animate-delay:1s;--animate-repeat:1}.animate__animated{-webkit-animation-duration:1s;animation-duration:1s;-webkit-animation-duration:var(--animate-duration);animation-duration:var(--animate-duration);-webkit-animation-fill-mode:both;animation-fill-mode:both}.animate__animated.animate__infinite{-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite}.animate__animated.animate__repeat-1{-webkit-animation-iteration-count:1;animation-iteration-count:1;-webkit-animation-iteration-count:var(--animate-repeat);animation-iteration-count:var(--animate-repeat)}.animate__animated.animate__repeat-2{-webkit-animation-iteration-count:2;animation-iteration-count:2;-webkit-animation-iteration-count:calc(var(--animate-repeat)*2);animation-iteration-count:calc(var(--animate-repeat)*2)}.animate__animated.animate__repeat-3{-webkit-animation-iteration-count:3;animation-iteration-count:3;-webkit-animation-iteration-count:calc(var(--animate-repeat)*3);animation-iteration-count:calc(var(--animate-repeat)*3)}.animate__animated.animate__delay-1s{-webkit-animation-delay:1s;animation-delay:1s;-webkit-animation-delay:var(--animate-delay);animation-delay:var(--animate-delay)}.animate__animated.animate__delay-2s{-webkit-animation-delay:2s;animation-delay:2s;-webkit-animation-delay:calc(var(--animate-delay)*2);animation-delay:calc(var(--animate-delay)*2)}.animate__animated.animate__delay-3s{-webkit-animation-delay:3s;animation-delay:3s;-webkit-animation-delay:calc(var(--animate-delay)*3);animation-delay:calc(var(--animate-delay)*3)}.animate__animated.animate__delay-4s{-webkit-animation-delay:4s;animation-delay:4s;-webkit-animation-delay:calc(var(--animate-delay)*4);animation-delay:calc(var(--animate-delay)*4)}.animate__animated.animate__delay-5s{-webkit-animation-delay:5s;animation-delay:5s;-webkit-animation-delay:calc(var(--animate-delay)*5);animation-delay:calc(var(--animate-delay)*5)}.animate__animated.animate__faster{-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-animation-duration:calc(var(--animate-duration)/2);animation-duration:calc(var(--animate-duration)/2)}.animate__animated.animate__fast{-webkit-animation-duration:.8s;animation-duration:.8s;-webkit-animation-duration:calc(var(--animate-duration)*0.8);animation-duration:calc(var(--animate-duration)*0.8)}.animate__animated.animate__slow{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-duration:calc(var(--animate-duration)*2);animation-duration:calc(var(--animate-duration)*2)}.animate__animated.animate__slower{-webkit-animation-duration:3s;animation-duration:3s;-webkit-animation-duration:calc(var(--animate-duration)*3);animation-duration:calc(var(--animate-duration)*3)}@media (prefers-reduced-motion:reduce),print{.animate__animated{-webkit-animation-duration:1ms!important;animation-duration:1ms!important;-webkit-transition-duration:1ms!important;transition-duration:1ms!important;-webkit-animation-iteration-count:1!important;animation-iteration-count:1!important}.animate__animated[class*=Out]{opacity:0}}@-webkit-keyframes bounce{0%,20%,53%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-30px,0) scaleY(1.1);transform:translate3d(0,-30px,0) scaleY(1.1)}70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-15px,0) scaleY(1.05);transform:translate3d(0,-15px,0) scaleY(1.05)}80%{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1);transition-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0) scaleY(.95);transform:translateZ(0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-4px,0) scaleY(1.02);transform:translate3d(0,-4px,0) scaleY(1.02)}}@keyframes bounce{0%,20%,53%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0);transform:translateZ(0)}40%,43%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-30px,0) scaleY(1.1);transform:translate3d(0,-30px,0) scaleY(1.1)}70%{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06);-webkit-transform:translate3d(0,-15px,0) scaleY(1.05);transform:translate3d(0,-15px,0) scaleY(1.05)}80%{-webkit-transition-timing-function:cubic-bezier(.215,.61,.355,1);transition-timing-function:cubic-bezier(.215,.61,.355,1);-webkit-transform:translateZ(0) scaleY(.95);transform:translateZ(0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-4px,0) scaleY(1.02);transform:translate3d(0,-4px,0) scaleY(1.02)}}.animate__bounce{-webkit-animation-name:bounce;animation-name:bounce;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}@keyframes flash{0%,50%,to{opacity:1}25%,75%{opacity:0}}.animate__flash{-webkit-animation-name:flash;animation-name:flash}@-webkit-keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes pulse{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}50%{-webkit-transform:scale3d(1.05,1.05,1.05);transform:scale3d(1.05,1.05,1.05)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__pulse{-webkit-animation-name:pulse;animation-name:pulse;-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}@-webkit-keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes rubberBand{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}30%{-webkit-transform:scale3d(1.25,.75,1);transform:scale3d(1.25,.75,1)}40%{-webkit-transform:scale3d(.75,1.25,1);transform:scale3d(.75,1.25,1)}50%{-webkit-transform:scale3d(1.15,.85,1);transform:scale3d(1.15,.85,1)}65%{-webkit-transform:scale3d(.95,1.05,1);transform:scale3d(.95,1.05,1)}75%{-webkit-transform:scale3d(1.05,.95,1);transform:scale3d(1.05,.95,1)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__rubberBand{-webkit-animation-name:rubberBand;animation-name:rubberBand}@-webkit-keyframes shakeX{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}@keyframes shakeX{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(-10px,0,0);transform:translate3d(-10px,0,0)}20%,40%,60%,80%{-webkit-transform:translate3d(10px,0,0);transform:translate3d(10px,0,0)}}.animate__shakeX{-webkit-animation-name:shakeX;animation-name:shakeX}@-webkit-keyframes shakeY{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}20%,40%,60%,80%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}}@keyframes shakeY{0%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}10%,30%,50%,70%,90%{-webkit-transform:translate3d(0,-10px,0);transform:translate3d(0,-10px,0)}20%,40%,60%,80%{-webkit-transform:translate3d(0,10px,0);transform:translate3d(0,10px,0)}}.animate__shakeY{-webkit-animation-name:shakeY;animation-name:shakeY}@-webkit-keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}@keyframes headShake{0%{-webkit-transform:translateX(0);transform:translateX(0)}6.5%{-webkit-transform:translateX(-6px) rotateY(-9deg);transform:translateX(-6px) rotateY(-9deg)}18.5%{-webkit-transform:translateX(5px) rotateY(7deg);transform:translateX(5px) rotateY(7deg)}31.5%{-webkit-transform:translateX(-3px) rotateY(-5deg);transform:translateX(-3px) rotateY(-5deg)}43.5%{-webkit-transform:translateX(2px) rotateY(3deg);transform:translateX(2px) rotateY(3deg)}50%{-webkit-transform:translateX(0);transform:translateX(0)}}.animate__headShake{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;-webkit-animation-name:headShake;animation-name:headShake}@-webkit-keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}@keyframes swing{20%{-webkit-transform:rotate(15deg);transform:rotate(15deg)}40%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}60%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}80%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(0deg);transform:rotate(0deg)}}.animate__swing{-webkit-transform-origin:top center;transform-origin:top center;-webkit-animation-name:swing;animation-name:swing}@-webkit-keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes tada{0%{-webkit-transform:scaleX(1);transform:scaleX(1)}10%,20%{-webkit-transform:scale3d(.9,.9,.9) rotate(-3deg);transform:scale3d(.9,.9,.9) rotate(-3deg)}30%,50%,70%,90%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(3deg);transform:scale3d(1.1,1.1,1.1) rotate(3deg)}40%,60%,80%{-webkit-transform:scale3d(1.1,1.1,1.1) rotate(-3deg);transform:scale3d(1.1,1.1,1.1) rotate(-3deg)}to{-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__tada{-webkit-animation-name:tada;animation-name:tada}@-webkit-keyframes wobble{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes wobble{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}15%{-webkit-transform:translate3d(-25%,0,0) rotate(-5deg);transform:translate3d(-25%,0,0) rotate(-5deg)}30%{-webkit-transform:translate3d(20%,0,0) rotate(3deg);transform:translate3d(20%,0,0) rotate(3deg)}45%{-webkit-transform:translate3d(-15%,0,0) rotate(-3deg);transform:translate3d(-15%,0,0) rotate(-3deg)}60%{-webkit-transform:translate3d(10%,0,0) rotate(2deg);transform:translate3d(10%,0,0) rotate(2deg)}75%{-webkit-transform:translate3d(-5%,0,0) rotate(-1deg);transform:translate3d(-5%,0,0) rotate(-1deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__wobble{-webkit-animation-name:wobble;animation-name:wobble}@-webkit-keyframes jello{0%,11.1%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}@keyframes jello{0%,11.1%,to{-webkit-transform:translateZ(0);transform:translateZ(0)}22.2%{-webkit-transform:skewX(-12.5deg) skewY(-12.5deg);transform:skewX(-12.5deg) skewY(-12.5deg)}33.3%{-webkit-transform:skewX(6.25deg) skewY(6.25deg);transform:skewX(6.25deg) skewY(6.25deg)}44.4%{-webkit-transform:skewX(-3.125deg) skewY(-3.125deg);transform:skewX(-3.125deg) skewY(-3.125deg)}55.5%{-webkit-transform:skewX(1.5625deg) skewY(1.5625deg);transform:skewX(1.5625deg) skewY(1.5625deg)}66.6%{-webkit-transform:skewX(-.78125deg) skewY(-.78125deg);transform:skewX(-.78125deg) skewY(-.78125deg)}77.7%{-webkit-transform:skewX(.390625deg) skewY(.390625deg);transform:skewX(.390625deg) skewY(.390625deg)}88.8%{-webkit-transform:skewX(-.1953125deg) skewY(-.1953125deg);transform:skewX(-.1953125deg) skewY(-.1953125deg)}}.animate__jello{-webkit-animation-name:jello;animation-name:jello;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes heartBeat{0%{-webkit-transform:scale(1);transform:scale(1)}14%{-webkit-transform:scale(1.3);transform:scale(1.3)}28%{-webkit-transform:scale(1);transform:scale(1)}42%{-webkit-transform:scale(1.3);transform:scale(1.3)}70%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes heartBeat{0%{-webkit-transform:scale(1);transform:scale(1)}14%{-webkit-transform:scale(1.3);transform:scale(1.3)}28%{-webkit-transform:scale(1);transform:scale(1)}42%{-webkit-transform:scale(1.3);transform:scale(1.3)}70%{-webkit-transform:scale(1);transform:scale(1)}}.animate__heartBeat{-webkit-animation-name:heartBeat;animation-name:heartBeat;-webkit-animation-duration:1.3s;animation-duration:1.3s;-webkit-animation-duration:calc(var(--animate-duration)*1.3);animation-duration:calc(var(--animate-duration)*1.3);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}@-webkit-keyframes backInDown{0%{-webkit-transform:translateY(-1200px) scale(.7);transform:translateY(-1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInDown{0%{-webkit-transform:translateY(-1200px) scale(.7);transform:translateY(-1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInDown{-webkit-animation-name:backInDown;animation-name:backInDown}@-webkit-keyframes backInLeft{0%{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInLeft{0%{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInLeft{-webkit-animation-name:backInLeft;animation-name:backInLeft}@-webkit-keyframes backInRight{0%{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInRight{0%{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}80%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInRight{-webkit-animation-name:backInRight;animation-name:backInRight}@-webkit-keyframes backInUp{0%{-webkit-transform:translateY(1200px) scale(.7);transform:translateY(1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes backInUp{0%{-webkit-transform:translateY(1200px) scale(.7);transform:translateY(1200px) scale(.7);opacity:.7}80%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}.animate__backInUp{-webkit-animation-name:backInUp;animation-name:backInUp}@-webkit-keyframes backOutDown{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(700px) scale(.7);transform:translateY(700px) scale(.7);opacity:.7}}@keyframes backOutDown{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(700px) scale(.7);transform:translateY(700px) scale(.7);opacity:.7}}.animate__backOutDown{-webkit-animation-name:backOutDown;animation-name:backOutDown}@-webkit-keyframes backOutLeft{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}}@keyframes backOutLeft{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(-2000px) scale(.7);transform:translateX(-2000px) scale(.7);opacity:.7}}.animate__backOutLeft{-webkit-animation-name:backOutLeft;animation-name:backOutLeft}@-webkit-keyframes backOutRight{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}}@keyframes backOutRight{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateX(0) scale(.7);transform:translateX(0) scale(.7);opacity:.7}to{-webkit-transform:translateX(2000px) scale(.7);transform:translateX(2000px) scale(.7);opacity:.7}}.animate__backOutRight{-webkit-animation-name:backOutRight;animation-name:backOutRight}@-webkit-keyframes backOutUp{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(-700px) scale(.7);transform:translateY(-700px) scale(.7);opacity:.7}}@keyframes backOutUp{0%{-webkit-transform:scale(1);transform:scale(1);opacity:1}20%{-webkit-transform:translateY(0) scale(.7);transform:translateY(0) scale(.7);opacity:.7}to{-webkit-transform:translateY(-700px) scale(.7);transform:translateY(-700px) scale(.7);opacity:.7}}.animate__backOutUp{-webkit-animation-name:backOutUp;animation-name:backOutUp}@-webkit-keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}@keyframes bounceIn{0%,20%,40%,60%,80%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}20%{-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}40%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}60%{opacity:1;-webkit-transform:scale3d(1.03,1.03,1.03);transform:scale3d(1.03,1.03,1.03)}80%{-webkit-transform:scale3d(.97,.97,.97);transform:scale3d(.97,.97,.97)}to{opacity:1;-webkit-transform:scaleX(1);transform:scaleX(1)}}.animate__bounceIn{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:bounceIn;animation-name:bounceIn}@-webkit-keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0) scaleY(3);transform:translate3d(0,-3000px,0) scaleY(3)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0) scaleY(.9);transform:translate3d(0,25px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,-10px,0) scaleY(.95);transform:translate3d(0,-10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,5px,0) scaleY(.985);transform:translate3d(0,5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInDown{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,-3000px,0) scaleY(3);transform:translate3d(0,-3000px,0) scaleY(3)}60%{opacity:1;-webkit-transform:translate3d(0,25px,0) scaleY(.9);transform:translate3d(0,25px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,-10px,0) scaleY(.95);transform:translate3d(0,-10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,5px,0) scaleY(.985);transform:translate3d(0,5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInDown{-webkit-animation-name:bounceInDown;animation-name:bounceInDown}@-webkit-keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0) scaleX(3);transform:translate3d(-3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0) scaleX(1);transform:translate3d(25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(-10px,0,0) scaleX(.98);transform:translate3d(-10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(5px,0,0) scaleX(.995);transform:translate3d(5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInLeft{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(-3000px,0,0) scaleX(3);transform:translate3d(-3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(25px,0,0) scaleX(1);transform:translate3d(25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(-10px,0,0) scaleX(.98);transform:translate3d(-10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(5px,0,0) scaleX(.995);transform:translate3d(5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInLeft{-webkit-animation-name:bounceInLeft;animation-name:bounceInLeft}@-webkit-keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0) scaleX(3);transform:translate3d(3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0) scaleX(1);transform:translate3d(-25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(10px,0,0) scaleX(.98);transform:translate3d(10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(-5px,0,0) scaleX(.995);transform:translate3d(-5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInRight{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(3000px,0,0) scaleX(3);transform:translate3d(3000px,0,0) scaleX(3)}60%{opacity:1;-webkit-transform:translate3d(-25px,0,0) scaleX(1);transform:translate3d(-25px,0,0) scaleX(1)}75%{-webkit-transform:translate3d(10px,0,0) scaleX(.98);transform:translate3d(10px,0,0) scaleX(.98)}90%{-webkit-transform:translate3d(-5px,0,0) scaleX(.995);transform:translate3d(-5px,0,0) scaleX(.995)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInRight{-webkit-animation-name:bounceInRight;animation-name:bounceInRight}@-webkit-keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0) scaleY(5);transform:translate3d(0,3000px,0) scaleY(5)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,10px,0) scaleY(.95);transform:translate3d(0,10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-5px,0) scaleY(.985);transform:translate3d(0,-5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes bounceInUp{0%,60%,75%,90%,to{-webkit-animation-timing-function:cubic-bezier(.215,.61,.355,1);animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;-webkit-transform:translate3d(0,3000px,0) scaleY(5);transform:translate3d(0,3000px,0) scaleY(5)}60%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}75%{-webkit-transform:translate3d(0,10px,0) scaleY(.95);transform:translate3d(0,10px,0) scaleY(.95)}90%{-webkit-transform:translate3d(0,-5px,0) scaleY(.985);transform:translate3d(0,-5px,0) scaleY(.985)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__bounceInUp{-webkit-animation-name:bounceInUp;animation-name:bounceInUp}@-webkit-keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}@keyframes bounceOut{20%{-webkit-transform:scale3d(.9,.9,.9);transform:scale3d(.9,.9,.9)}50%,55%{opacity:1;-webkit-transform:scale3d(1.1,1.1,1.1);transform:scale3d(1.1,1.1,1.1)}to{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}}.animate__bounceOut{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:bounceOut;animation-name:bounceOut}@-webkit-keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0) scaleY(.985);transform:translate3d(0,10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0) scaleY(3);transform:translate3d(0,2000px,0) scaleY(3)}}@keyframes bounceOutDown{20%{-webkit-transform:translate3d(0,10px,0) scaleY(.985);transform:translate3d(0,10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,-20px,0) scaleY(.9);transform:translate3d(0,-20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,2000px,0) scaleY(3);transform:translate3d(0,2000px,0) scaleY(3)}}.animate__bounceOutDown{-webkit-animation-name:bounceOutDown;animation-name:bounceOutDown}@-webkit-keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0) scaleX(.9);transform:translate3d(20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0) scaleX(2);transform:translate3d(-2000px,0,0) scaleX(2)}}@keyframes bounceOutLeft{20%{opacity:1;-webkit-transform:translate3d(20px,0,0) scaleX(.9);transform:translate3d(20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0) scaleX(2);transform:translate3d(-2000px,0,0) scaleX(2)}}.animate__bounceOutLeft{-webkit-animation-name:bounceOutLeft;animation-name:bounceOutLeft}@-webkit-keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0) scaleX(.9);transform:translate3d(-20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0) scaleX(2);transform:translate3d(2000px,0,0) scaleX(2)}}@keyframes bounceOutRight{20%{opacity:1;-webkit-transform:translate3d(-20px,0,0) scaleX(.9);transform:translate3d(-20px,0,0) scaleX(.9)}to{opacity:0;-webkit-transform:translate3d(2000px,0,0) scaleX(2);transform:translate3d(2000px,0,0) scaleX(2)}}.animate__bounceOutRight{-webkit-animation-name:bounceOutRight;animation-name:bounceOutRight}@-webkit-keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0) scaleY(.985);transform:translate3d(0,-10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0) scaleY(.9);transform:translate3d(0,20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0) scaleY(3);transform:translate3d(0,-2000px,0) scaleY(3)}}@keyframes bounceOutUp{20%{-webkit-transform:translate3d(0,-10px,0) scaleY(.985);transform:translate3d(0,-10px,0) scaleY(.985)}40%,45%{opacity:1;-webkit-transform:translate3d(0,20px,0) scaleY(.9);transform:translate3d(0,20px,0) scaleY(.9)}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0) scaleY(3);transform:translate3d(0,-2000px,0) scaleY(3)}}.animate__bounceOutUp{-webkit-animation-name:bounceOutUp;animation-name:bounceOutUp}@-webkit-keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}.animate__fadeIn{-webkit-animation-name:fadeIn;animation-name:fadeIn}@-webkit-keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInDown{0%{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInDown{-webkit-animation-name:fadeInDown;animation-name:fadeInDown}@-webkit-keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInDownBig{0%{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInDownBig{-webkit-animation-name:fadeInDownBig;animation-name:fadeInDownBig}@-webkit-keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInLeft{-webkit-animation-name:fadeInLeft;animation-name:fadeInLeft}@-webkit-keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInLeftBig{0%{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInLeftBig{-webkit-animation-name:fadeInLeftBig;animation-name:fadeInLeftBig}@-webkit-keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInRight{0%{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInRight{-webkit-animation-name:fadeInRight;animation-name:fadeInRight}@-webkit-keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInRightBig{0%{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInRightBig{-webkit-animation-name:fadeInRightBig;animation-name:fadeInRightBig}@-webkit-keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInUp{0%{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInUp{-webkit-animation-name:fadeInUp;animation-name:fadeInUp}@-webkit-keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInUpBig{0%{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInUpBig{-webkit-animation-name:fadeInUpBig;animation-name:fadeInUpBig}@-webkit-keyframes fadeInTopLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInTopLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInTopLeft{-webkit-animation-name:fadeInTopLeft;animation-name:fadeInTopLeft}@-webkit-keyframes fadeInTopRight{0%{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInTopRight{0%{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInTopRight{-webkit-animation-name:fadeInTopRight;animation-name:fadeInTopRight}@-webkit-keyframes fadeInBottomLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInBottomLeft{0%{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInBottomLeft{-webkit-animation-name:fadeInBottomLeft;animation-name:fadeInBottomLeft}@-webkit-keyframes fadeInBottomRight{0%{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes fadeInBottomRight{0%{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__fadeInBottomRight{-webkit-animation-name:fadeInBottomRight;animation-name:fadeInBottomRight}@-webkit-keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}.animate__fadeOut{-webkit-animation-name:fadeOut;animation-name:fadeOut}@-webkit-keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes fadeOutDown{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.animate__fadeOutDown{-webkit-animation-name:fadeOutDown;animation-name:fadeOutDown}@-webkit-keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}@keyframes fadeOutDownBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,2000px,0);transform:translate3d(0,2000px,0)}}.animate__fadeOutDownBig{-webkit-animation-name:fadeOutDownBig;animation-name:fadeOutDownBig}@-webkit-keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes fadeOutLeft{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.animate__fadeOutLeft{-webkit-animation-name:fadeOutLeft;animation-name:fadeOutLeft}@-webkit-keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}@keyframes fadeOutLeftBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(-2000px,0,0);transform:translate3d(-2000px,0,0)}}.animate__fadeOutLeftBig{-webkit-animation-name:fadeOutLeftBig;animation-name:fadeOutLeftBig}@-webkit-keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes fadeOutRight{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.animate__fadeOutRight{-webkit-animation-name:fadeOutRight;animation-name:fadeOutRight}@-webkit-keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}@keyframes fadeOutRightBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(2000px,0,0);transform:translate3d(2000px,0,0)}}.animate__fadeOutRightBig{-webkit-animation-name:fadeOutRightBig;animation-name:fadeOutRightBig}@-webkit-keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes fadeOutUp{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.animate__fadeOutUp{-webkit-animation-name:fadeOutUp;animation-name:fadeOutUp}@-webkit-keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}@keyframes fadeOutUpBig{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(0,-2000px,0);transform:translate3d(0,-2000px,0)}}.animate__fadeOutUpBig{-webkit-animation-name:fadeOutUpBig;animation-name:fadeOutUpBig}@-webkit-keyframes fadeOutTopLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}}@keyframes fadeOutTopLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,-100%,0);transform:translate3d(-100%,-100%,0)}}.animate__fadeOutTopLeft{-webkit-animation-name:fadeOutTopLeft;animation-name:fadeOutTopLeft}@-webkit-keyframes fadeOutTopRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}}@keyframes fadeOutTopRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,-100%,0);transform:translate3d(100%,-100%,0)}}.animate__fadeOutTopRight{-webkit-animation-name:fadeOutTopRight;animation-name:fadeOutTopRight}@-webkit-keyframes fadeOutBottomRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}}@keyframes fadeOutBottomRight{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(100%,100%,0);transform:translate3d(100%,100%,0)}}.animate__fadeOutBottomRight{-webkit-animation-name:fadeOutBottomRight;animation-name:fadeOutBottomRight}@-webkit-keyframes fadeOutBottomLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}}@keyframes fadeOutBottomLeft{0%{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}to{opacity:0;-webkit-transform:translate3d(-100%,100%,0);transform:translate3d(-100%,100%,0)}}.animate__fadeOutBottomLeft{-webkit-animation-name:fadeOutBottomLeft;animation-name:fadeOutBottomLeft}@-webkit-keyframes flip{0%{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}to{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}@keyframes flip{0%{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(-1turn);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}40%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-190deg);-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}50%{-webkit-transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);transform:perspective(400px) scaleX(1) translateZ(150px) rotateY(-170deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}80%{-webkit-transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);transform:perspective(400px) scale3d(.95,.95,.95) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}to{-webkit-transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);transform:perspective(400px) scaleX(1) translateZ(0) rotateY(0deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}}.animate__animated.animate__flip{-webkit-backface-visibility:visible;backface-visibility:visible;-webkit-animation-name:flip;animation-name:flip}@-webkit-keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInX{0%{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateX(10deg);transform:perspective(400px) rotateX(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateX(-5deg);transform:perspective(400px) rotateX(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.animate__flipInX{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInX;animation-name:flipInX}@-webkit-keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}@keyframes flipInY{0%{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in;opacity:0}40%{-webkit-transform:perspective(400px) rotateY(-20deg);transform:perspective(400px) rotateY(-20deg);-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}60%{-webkit-transform:perspective(400px) rotateY(10deg);transform:perspective(400px) rotateY(10deg);opacity:1}80%{-webkit-transform:perspective(400px) rotateY(-5deg);transform:perspective(400px) rotateY(-5deg)}to{-webkit-transform:perspective(400px);transform:perspective(400px)}}.animate__flipInY{-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipInY;animation-name:flipInY}@-webkit-keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}@keyframes flipOutX{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateX(-20deg);transform:perspective(400px) rotateX(-20deg);opacity:1}to{-webkit-transform:perspective(400px) rotateX(90deg);transform:perspective(400px) rotateX(90deg);opacity:0}}.animate__flipOutX{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-animation-name:flipOutX;animation-name:flipOutX;-webkit-backface-visibility:visible!important;backface-visibility:visible!important}@-webkit-keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}@keyframes flipOutY{0%{-webkit-transform:perspective(400px);transform:perspective(400px)}30%{-webkit-transform:perspective(400px) rotateY(-15deg);transform:perspective(400px) rotateY(-15deg);opacity:1}to{-webkit-transform:perspective(400px) rotateY(90deg);transform:perspective(400px) rotateY(90deg);opacity:0}}.animate__flipOutY{-webkit-animation-duration:.75s;animation-duration:.75s;-webkit-animation-duration:calc(var(--animate-duration)*0.75);animation-duration:calc(var(--animate-duration)*0.75);-webkit-backface-visibility:visible!important;backface-visibility:visible!important;-webkit-animation-name:flipOutY;animation-name:flipOutY}@-webkit-keyframes lightSpeedInRight{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg);opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes lightSpeedInRight{0%{-webkit-transform:translate3d(100%,0,0) skewX(-30deg);transform:translate3d(100%,0,0) skewX(-30deg);opacity:0}60%{-webkit-transform:skewX(20deg);transform:skewX(20deg);opacity:1}80%{-webkit-transform:skewX(-5deg);transform:skewX(-5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__lightSpeedInRight{-webkit-animation-name:lightSpeedInRight;animation-name:lightSpeedInRight;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedInLeft{0%{-webkit-transform:translate3d(-100%,0,0) skewX(30deg);transform:translate3d(-100%,0,0) skewX(30deg);opacity:0}60%{-webkit-transform:skewX(-20deg);transform:skewX(-20deg);opacity:1}80%{-webkit-transform:skewX(5deg);transform:skewX(5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes lightSpeedInLeft{0%{-webkit-transform:translate3d(-100%,0,0) skewX(30deg);transform:translate3d(-100%,0,0) skewX(30deg);opacity:0}60%{-webkit-transform:skewX(-20deg);transform:skewX(-20deg);opacity:1}80%{-webkit-transform:skewX(5deg);transform:skewX(5deg)}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__lightSpeedInLeft{-webkit-animation-name:lightSpeedInLeft;animation-name:lightSpeedInLeft;-webkit-animation-timing-function:ease-out;animation-timing-function:ease-out}@-webkit-keyframes lightSpeedOutRight{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}@keyframes lightSpeedOutRight{0%{opacity:1}to{-webkit-transform:translate3d(100%,0,0) skewX(30deg);transform:translate3d(100%,0,0) skewX(30deg);opacity:0}}.animate__lightSpeedOutRight{-webkit-animation-name:lightSpeedOutRight;animation-name:lightSpeedOutRight;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes lightSpeedOutLeft{0%{opacity:1}to{-webkit-transform:translate3d(-100%,0,0) skewX(-30deg);transform:translate3d(-100%,0,0) skewX(-30deg);opacity:0}}@keyframes lightSpeedOutLeft{0%{opacity:1}to{-webkit-transform:translate3d(-100%,0,0) skewX(-30deg);transform:translate3d(-100%,0,0) skewX(-30deg);opacity:0}}.animate__lightSpeedOutLeft{-webkit-animation-name:lightSpeedOutLeft;animation-name:lightSpeedOutLeft;-webkit-animation-timing-function:ease-in;animation-timing-function:ease-in}@-webkit-keyframes rotateIn{0%{-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateIn{0%{-webkit-transform:rotate(-200deg);transform:rotate(-200deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateIn{-webkit-animation-name:rotateIn;animation-name:rotateIn;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes rotateInDownLeft{0%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInDownLeft{0%{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInDownLeft{-webkit-animation-name:rotateInDownLeft;animation-name:rotateInDownLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateInDownRight{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInDownRight{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInDownRight{-webkit-animation-name:rotateInDownRight;animation-name:rotateInDownRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateInUpLeft{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInUpLeft{0%{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInUpLeft{-webkit-animation-name:rotateInUpLeft;animation-name:rotateInUpLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateInUpRight{0%{-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}@keyframes rotateInUpRight{0%{-webkit-transform:rotate(-90deg);transform:rotate(-90deg);opacity:0}to{-webkit-transform:translateZ(0);transform:translateZ(0);opacity:1}}.animate__rotateInUpRight{-webkit-animation-name:rotateInUpRight;animation-name:rotateInUpRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateOut{0%{opacity:1}to{-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}@keyframes rotateOut{0%{opacity:1}to{-webkit-transform:rotate(200deg);transform:rotate(200deg);opacity:0}}.animate__rotateOut{-webkit-animation-name:rotateOut;animation-name:rotateOut;-webkit-transform-origin:center;transform-origin:center}@-webkit-keyframes rotateOutDownLeft{0%{opacity:1}to{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}@keyframes rotateOutDownLeft{0%{opacity:1}to{-webkit-transform:rotate(45deg);transform:rotate(45deg);opacity:0}}.animate__rotateOutDownLeft{-webkit-animation-name:rotateOutDownLeft;animation-name:rotateOutDownLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateOutDownRight{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutDownRight{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.animate__rotateOutDownRight{-webkit-animation-name:rotateOutDownRight;animation-name:rotateOutDownRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes rotateOutUpLeft{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}@keyframes rotateOutUpLeft{0%{opacity:1}to{-webkit-transform:rotate(-45deg);transform:rotate(-45deg);opacity:0}}.animate__rotateOutUpLeft{-webkit-animation-name:rotateOutUpLeft;animation-name:rotateOutUpLeft;-webkit-transform-origin:left bottom;transform-origin:left bottom}@-webkit-keyframes rotateOutUpRight{0%{opacity:1}to{-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}@keyframes rotateOutUpRight{0%{opacity:1}to{-webkit-transform:rotate(90deg);transform:rotate(90deg);opacity:0}}.animate__rotateOutUpRight{-webkit-animation-name:rotateOutUpRight;animation-name:rotateOutUpRight;-webkit-transform-origin:right bottom;transform-origin:right bottom}@-webkit-keyframes hinge{0%{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}@keyframes hinge{0%{-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}20%,60%{-webkit-transform:rotate(80deg);transform:rotate(80deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out}40%,80%{-webkit-transform:rotate(60deg);transform:rotate(60deg);-webkit-animation-timing-function:ease-in-out;animation-timing-function:ease-in-out;opacity:1}to{-webkit-transform:translate3d(0,700px,0);transform:translate3d(0,700px,0);opacity:0}}.animate__hinge{-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-duration:calc(var(--animate-duration)*2);animation-duration:calc(var(--animate-duration)*2);-webkit-animation-name:hinge;animation-name:hinge;-webkit-transform-origin:top left;transform-origin:top left}@-webkit-keyframes jackInTheBox{0%{opacity:0;-webkit-transform:scale(.1) rotate(30deg);transform:scale(.1) rotate(30deg);-webkit-transform-origin:center bottom;transform-origin:center bottom}50%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}70%{-webkit-transform:rotate(3deg);transform:rotate(3deg)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}@keyframes jackInTheBox{0%{opacity:0;-webkit-transform:scale(.1) rotate(30deg);transform:scale(.1) rotate(30deg);-webkit-transform-origin:center bottom;transform-origin:center bottom}50%{-webkit-transform:rotate(-10deg);transform:rotate(-10deg)}70%{-webkit-transform:rotate(3deg);transform:rotate(3deg)}to{opacity:1;-webkit-transform:scale(1);transform:scale(1)}}.animate__jackInTheBox{-webkit-animation-name:jackInTheBox;animation-name:jackInTheBox}@-webkit-keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes rollIn{0%{opacity:0;-webkit-transform:translate3d(-100%,0,0) rotate(-120deg);transform:translate3d(-100%,0,0) rotate(-120deg)}to{opacity:1;-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__rollIn{-webkit-animation-name:rollIn;animation-name:rollIn}@-webkit-keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}@keyframes rollOut{0%{opacity:1}to{opacity:0;-webkit-transform:translate3d(100%,0,0) rotate(120deg);transform:translate3d(100%,0,0) rotate(120deg)}}.animate__rollOut{-webkit-animation-name:rollOut;animation-name:rollOut}@-webkit-keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes zoomIn{0%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}50%{opacity:1}}.animate__zoomIn{-webkit-animation-name:zoomIn;animation-name:zoomIn}@-webkit-keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInDown{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInDown{-webkit-animation-name:zoomInDown;animation-name:zoomInDown}@-webkit-keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInLeft{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(-1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(10px,0,0);transform:scale3d(.475,.475,.475) translate3d(10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInLeft{-webkit-animation-name:zoomInLeft;animation-name:zoomInLeft}@-webkit-keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInRight{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);transform:scale3d(.1,.1,.1) translate3d(1000px,0,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);transform:scale3d(.475,.475,.475) translate3d(-10px,0,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInRight{-webkit-animation-name:zoomInRight;animation-name:zoomInRight}@-webkit-keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomInUp{0%{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);transform:scale3d(.1,.1,.1) translate3d(0,1000px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}60%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomInUp{-webkit-animation-name:zoomInUp;animation-name:zoomInUp}@-webkit-keyframes zoomOut{0%{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}to{opacity:0}}@keyframes zoomOut{0%{opacity:1}50%{opacity:0;-webkit-transform:scale3d(.3,.3,.3);transform:scale3d(.3,.3,.3)}to{opacity:0}}.animate__zoomOut{-webkit-animation-name:zoomOut;animation-name:zoomOut}@-webkit-keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutDown{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);transform:scale3d(.475,.475,.475) translate3d(0,-60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomOutDown{-webkit-animation-name:zoomOutDown;animation-name:zoomOutDown;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0)}}@keyframes zoomOutLeft{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(42px,0,0);transform:scale3d(.475,.475,.475) translate3d(42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(-2000px,0,0);transform:scale(.1) translate3d(-2000px,0,0)}}.animate__zoomOutLeft{-webkit-animation-name:zoomOutLeft;animation-name:zoomOutLeft;-webkit-transform-origin:left center;transform-origin:left center}@-webkit-keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0)}}@keyframes zoomOutRight{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(-42px,0,0);transform:scale3d(.475,.475,.475) translate3d(-42px,0,0)}to{opacity:0;-webkit-transform:scale(.1) translate3d(2000px,0,0);transform:scale(.1) translate3d(2000px,0,0)}}.animate__zoomOutRight{-webkit-animation-name:zoomOutRight;animation-name:zoomOutRight;-webkit-transform-origin:right center;transform-origin:right center}@-webkit-keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}@keyframes zoomOutUp{40%{opacity:1;-webkit-transform:scale3d(.475,.475,.475) translate3d(0,60px,0);transform:scale3d(.475,.475,.475) translate3d(0,60px,0);-webkit-animation-timing-function:cubic-bezier(.55,.055,.675,.19);animation-timing-function:cubic-bezier(.55,.055,.675,.19)}to{opacity:0;-webkit-transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);transform:scale3d(.1,.1,.1) translate3d(0,-2000px,0);-webkit-animation-timing-function:cubic-bezier(.175,.885,.32,1);animation-timing-function:cubic-bezier(.175,.885,.32,1)}}.animate__zoomOutUp{-webkit-animation-name:zoomOutUp;animation-name:zoomOutUp;-webkit-transform-origin:center bottom;transform-origin:center bottom}@-webkit-keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInDown{0%{-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInDown{-webkit-animation-name:slideInDown;animation-name:slideInDown}@-webkit-keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInLeft{0%{-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInLeft{-webkit-animation-name:slideInLeft;animation-name:slideInLeft}@-webkit-keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInRight{0%{-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInRight{-webkit-animation-name:slideInRight;animation-name:slideInRight}@-webkit-keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}@keyframes slideInUp{0%{-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0);visibility:visible}to{-webkit-transform:translateZ(0);transform:translateZ(0)}}.animate__slideInUp{-webkit-animation-name:slideInUp;animation-name:slideInUp}@-webkit-keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}@keyframes slideOutDown{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,100%,0);transform:translate3d(0,100%,0)}}.animate__slideOutDown{-webkit-animation-name:slideOutDown;animation-name:slideOutDown}@-webkit-keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}@keyframes slideOutLeft{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(-100%,0,0);transform:translate3d(-100%,0,0)}}.animate__slideOutLeft{-webkit-animation-name:slideOutLeft;animation-name:slideOutLeft}@-webkit-keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}@keyframes slideOutRight{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(100%,0,0);transform:translate3d(100%,0,0)}}.animate__slideOutRight{-webkit-animation-name:slideOutRight;animation-name:slideOutRight}@-webkit-keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}@keyframes slideOutUp{0%{-webkit-transform:translateZ(0);transform:translateZ(0)}to{visibility:hidden;-webkit-transform:translate3d(0,-100%,0);transform:translate3d(0,-100%,0)}}.animate__slideOutUp{-webkit-animation-name:slideOutUp;animation-name:slideOutUp} \ No newline at end of file diff --git a/blog-site/public/css/comments.css b/blog-site/public/css/comments.css new file mode 100644 index 00000000..2993e1c6 --- /dev/null +++ b/blog-site/public/css/comments.css @@ -0,0 +1,25 @@ +.v .vwrap { + border-radius: unset; + margin-top: 15px; +} + +.v .veditor { + min-height: 4.75rem; +} + +.v .vwrap .vheader .vinput { + border: none; +} + +.v .vbtn { + color: #FFFFFF; + background-color: #000000; + border: none; +} + +.v .vbtn:active, +.v .vbtn:hover { + color: #FFFFFF; + border-color: #000000; + background-color: #303538; +} diff --git a/blog-site/public/css/fancybox.min.css b/blog-site/public/css/fancybox.min.css new file mode 100644 index 00000000..b374f8d5 --- /dev/null +++ b/blog-site/public/css/fancybox.min.css @@ -0,0 +1,819 @@ +body.compensate-for-scrollbar { + overflow: hidden +} + +.fancybox-active { + height: auto +} + +.fancybox-is-hidden { + left: -9999px; + margin: 0; + position: absolute !important; + top: -9999px; + visibility: hidden +} + +.fancybox-container { + -webkit-backface-visibility: hidden; + height: 100%; + left: 0; + outline: none; + position: fixed; + -webkit-tap-highlight-color: transparent; + top: 0; + -ms-touch-action: manipulation; + touch-action: manipulation; + transform: translateZ(0); + width: 100%; + z-index: 99992; + background: rgba(255, 255, 255, 0.7); + backdrop-filter: saturate(180%) blur(20px); + -webkit-backdrop-filter: saturate(180%) blur(20px); +} + +.fancybox-container * { + box-sizing: border-box +} + +.fancybox-bg, .fancybox-inner, .fancybox-outer, .fancybox-stage { + bottom: 0; + left: 0; + position: absolute; + right: 0; + top: 0 +} + +.fancybox-outer { + -webkit-overflow-scrolling: touch; + overflow-y: auto +} + +.fancybox-bg { + opacity: 0; + transition-duration: inherit; + transition-property: opacity; + transition-timing-function: cubic-bezier(.47, 0, .74, .71) +} + +.fancybox-is-open .fancybox-bg { + opacity: 1; + transition-timing-function: cubic-bezier(.22, .61, .36, 1) +} + +.fancybox-caption, .fancybox-infobar, .fancybox-navigation .fancybox-button, .fancybox-toolbar { + direction: ltr; + opacity: 0; + position: absolute; + transition: opacity .25s ease, visibility 0s ease .25s; + visibility: hidden; + z-index: 99997 +} + +.fancybox-show-caption .fancybox-caption, .fancybox-show-infobar .fancybox-infobar, .fancybox-show-nav .fancybox-navigation .fancybox-button, .fancybox-show-toolbar .fancybox-toolbar { + opacity: 1; + transition: opacity .25s ease 0s, visibility 0s ease 0s; + visibility: visible +} + +.fancybox-infobar { + color: #ccc; + font-size: 13px; + -webkit-font-smoothing: subpixel-antialiased; + height: 44px; + left: 0; + line-height: 44px; + min-width: 44px; + mix-blend-mode: difference; + padding: 0 10px; + pointer-events: none; + top: 0; + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none +} + +.fancybox-toolbar { + right: 0; + top: 0 +} + +.fancybox-stage { + direction: ltr; + overflow: visible; + transform: translateZ(0); + z-index: 99994 +} + +.fancybox-is-open .fancybox-stage { + overflow: hidden +} + +.fancybox-slide { + -webkit-backface-visibility: hidden; + display: none; + height: 100%; + left: 0; + outline: none; + overflow: auto; + -webkit-overflow-scrolling: touch; + padding: 44px; + position: absolute; + text-align: center; + top: 0; + transition-property: transform, opacity; + white-space: normal; + width: 100%; + z-index: 99994 +} + +.fancybox-slide:before { + content: ""; + display: inline-block; + font-size: 0; + height: 100%; + vertical-align: middle; + width: 0 +} + +.fancybox-is-sliding .fancybox-slide, .fancybox-slide--current, .fancybox-slide--next, .fancybox-slide--previous { + display: block +} + +.fancybox-slide--image { + overflow: hidden; + padding: 44px 0 +} + +.fancybox-slide--image:before { + display: none +} + +.fancybox-slide--html { + padding: 6px +} + +.fancybox-content { + background: #fff; + display: inline-block; + margin: 0; + max-width: 100%; + overflow: auto; + -webkit-overflow-scrolling: touch; + padding: 44px; + position: relative; + text-align: left; + vertical-align: middle +} + +.fancybox-slide--image .fancybox-content { + animation-timing-function: cubic-bezier(.5, 0, .14, 1); + -webkit-backface-visibility: hidden; + background: transparent; + background-repeat: no-repeat; + background-size: 100% 100%; + left: 0; + max-width: none; + overflow: visible; + padding: 0; + position: absolute; + top: 0; + transform-origin: top left; + transition-property: transform, opacity; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + z-index: 99995 +} + +.fancybox-can-zoomOut .fancybox-content { + cursor: zoom-out +} + +.fancybox-can-zoomIn .fancybox-content { + cursor: zoom-in +} + +.fancybox-can-pan .fancybox-content, .fancybox-can-swipe .fancybox-content { + cursor: grab +} + +.fancybox-is-grabbing .fancybox-content { + cursor: grabbing +} + +.fancybox-container [data-selectable=true] { + cursor: text +} + +.fancybox-image, .fancybox-spaceball { + background: transparent; + border: 0; + height: 100%; + left: 0; + margin: 0; + max-height: none; + max-width: none; + padding: 0; + position: absolute; + top: 0; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: 100% +} + +.fancybox-spaceball { + z-index: 1 +} + +.fancybox-slide--iframe .fancybox-content, .fancybox-slide--map .fancybox-content, .fancybox-slide--pdf .fancybox-content, .fancybox-slide--video .fancybox-content { + height: 100%; + overflow: visible; + padding: 0; + width: 100% +} + +.fancybox-slide--video .fancybox-content { + background: #000 +} + +.fancybox-slide--map .fancybox-content { + background: #e5e3df +} + +.fancybox-slide--iframe .fancybox-content { + background: #fff +} + +.fancybox-iframe, .fancybox-video { + background: transparent; + border: 0; + display: block; + height: 100%; + margin: 0; + overflow: hidden; + padding: 0; + width: 100% +} + +.fancybox-iframe { + left: 0; + position: absolute; + top: 0 +} + +.fancybox-error { + background: #fff; + cursor: default; + max-width: 400px; + padding: 40px; + width: 100% +} + +.fancybox-error p { + color: #444; + font-size: 16px; + line-height: 20px; + margin: 0; + padding: 0 +} + +.fancybox-button { + background: rgba(30, 30, 30, .6); + border: 0; + border-radius: 0; + box-shadow: none; + cursor: pointer; + display: inline-block; + height: 44px; + margin: 0; + padding: 10px; + position: relative; + transition: color .2s; + vertical-align: top; + visibility: inherit; + width: 44px +} + +.fancybox-button, .fancybox-button:link, .fancybox-button:visited { + color: #ccc +} + +.fancybox-button:hover { + color: #fff +} + +.fancybox-button:focus { + outline: none +} + +.fancybox-button.fancybox-focus { + outline: 1px dotted +} + +.fancybox-button[disabled], .fancybox-button[disabled]:hover { + color: #888; + cursor: default; + outline: none +} + +.fancybox-button div { + height: 100% +} + +.fancybox-button svg { + display: block; + height: 100%; + overflow: visible; + position: relative; + width: 100% +} + +.fancybox-button svg path { + fill: currentColor; + stroke-width: 0 +} + +.fancybox-button--fsenter svg:nth-child(2), .fancybox-button--fsexit svg:first-child, .fancybox-button--pause svg:first-child, .fancybox-button--play svg:nth-child(2) { + display: none +} + +.fancybox-progress { + background: #ff5268; + height: 2px; + left: 0; + position: absolute; + right: 0; + top: 0; + transform: scaleX(0); + transform-origin: 0; + transition-property: transform; + transition-timing-function: linear; + z-index: 99998 +} + +.fancybox-close-small { + background: transparent; + border: 0; + border-radius: 0; + color: #ccc; + cursor: pointer; + opacity: .8; + padding: 8px; + position: absolute; + right: -12px; + top: -44px; + z-index: 401 +} + +.fancybox-close-small:hover { + color: #fff; + opacity: 1 +} + +.fancybox-slide--html .fancybox-close-small { + color: currentColor; + padding: 10px; + right: 0; + top: 0 +} + +.fancybox-slide--image.fancybox-is-scaling .fancybox-content { + overflow: hidden +} + +.fancybox-is-scaling .fancybox-close-small, .fancybox-is-zoomable.fancybox-can-pan .fancybox-close-small { + display: none +} + +.fancybox-navigation .fancybox-button { + background-clip: content-box; + height: 100px; + opacity: 0; + position: absolute; + top: calc(50% - 50px); + width: 70px +} + +.fancybox-navigation .fancybox-button div { + padding: 7px +} + +.fancybox-navigation .fancybox-button--arrow_left { + left: 0; + left: env(safe-area-inset-left); + padding: 31px 26px 31px 6px +} + +.fancybox-navigation .fancybox-button--arrow_right { + padding: 31px 6px 31px 26px; + right: 0; + right: env(safe-area-inset-right) +} + +.fancybox-caption { + background: linear-gradient(0deg, rgba(0, 0, 0, .85) 0, rgba(0, 0, 0, .3) 50%, rgba(0, 0, 0, .15) 65%, rgba(0, 0, 0, .075) 75.5%, rgba(0, 0, 0, .037) 82.85%, rgba(0, 0, 0, .019) 88%, transparent); + bottom: 0; + color: #eee; + font-size: 14px; + font-weight: 400; + left: 0; + line-height: 1.5; + padding: 75px 44px 25px; + pointer-events: none; + right: 0; + text-align: center; + z-index: 99996 +} + +@supports (padding:max(0px)) { + .fancybox-caption { + padding: 75px max(44px, env(safe-area-inset-right)) max(25px, env(safe-area-inset-bottom)) max(44px, env(safe-area-inset-left)) + } +} + +.fancybox-caption--separate { + margin-top: -50px +} + +.fancybox-caption__body { + max-height: 50vh; + overflow: auto; + pointer-events: all +} + +.fancybox-caption a, .fancybox-caption a:link, .fancybox-caption a:visited { + color: #ccc; + text-decoration: none +} + +.fancybox-caption a:hover { + color: #fff; + text-decoration: underline +} + +.fancybox-loading { + animation: a 1s linear infinite; + background: transparent; + border: 4px solid #888; + border-bottom-color: #fff; + border-radius: 50%; + height: 50px; + left: 50%; + margin: -25px 0 0 -25px; + opacity: .7; + padding: 0; + position: absolute; + top: 50%; + width: 50px; + z-index: 99999 +} + +@keyframes a { + to { + transform: rotate(1turn) + } +} + +.fancybox-animated { + transition-timing-function: cubic-bezier(0, 0, .25, 1) +} + +.fancybox-fx-slide.fancybox-slide--previous { + opacity: 0; + transform: translate3d(-100%, 0, 0) +} + +.fancybox-fx-slide.fancybox-slide--next { + opacity: 0; + transform: translate3d(100%, 0, 0) +} + +.fancybox-fx-slide.fancybox-slide--current { + opacity: 1; + transform: translateZ(0) +} + +.fancybox-fx-fade.fancybox-slide--next, .fancybox-fx-fade.fancybox-slide--previous { + opacity: 0; + transition-timing-function: cubic-bezier(.19, 1, .22, 1) +} + +.fancybox-fx-fade.fancybox-slide--current { + opacity: 1 +} + +.fancybox-fx-zoom-in-out.fancybox-slide--previous { + opacity: 0; + transform: scale3d(1.5, 1.5, 1.5) +} + +.fancybox-fx-zoom-in-out.fancybox-slide--next { + opacity: 0; + transform: scale3d(.5, .5, .5) +} + +.fancybox-fx-zoom-in-out.fancybox-slide--current { + opacity: 1; + transform: scaleX(1) +} + +.fancybox-fx-rotate.fancybox-slide--previous { + opacity: 0; + transform: rotate(-1turn) +} + +.fancybox-fx-rotate.fancybox-slide--next { + opacity: 0; + transform: rotate(1turn) +} + +.fancybox-fx-rotate.fancybox-slide--current { + opacity: 1; + transform: rotate(0deg) +} + +.fancybox-fx-circular.fancybox-slide--previous { + opacity: 0; + transform: scale3d(0, 0, 0) translate3d(-100%, 0, 0) +} + +.fancybox-fx-circular.fancybox-slide--next { + opacity: 0; + transform: scale3d(0, 0, 0) translate3d(100%, 0, 0) +} + +.fancybox-fx-circular.fancybox-slide--current { + opacity: 1; + transform: scaleX(1) translateZ(0) +} + +.fancybox-fx-tube.fancybox-slide--previous { + transform: translate3d(-100%, 0, 0) scale(.1) skew(-10deg) +} + +.fancybox-fx-tube.fancybox-slide--next { + transform: translate3d(100%, 0, 0) scale(.1) skew(10deg) +} + +.fancybox-fx-tube.fancybox-slide--current { + transform: translateZ(0) scale(1) +} + +@media (max-height: 576px) { + .fancybox-slide { + padding-left: 6px; + padding-right: 6px + } + + .fancybox-slide--image { + padding: 6px 0 + } + + .fancybox-close-small { + right: -6px + } + + .fancybox-slide--image .fancybox-close-small { + background: #4e4e4e; + color: #f2f4f6; + height: 36px; + opacity: 1; + padding: 6px; + right: 0; + top: 0; + width: 36px + } + + .fancybox-caption { + padding-left: 12px; + padding-right: 12px + } + + @supports (padding:max(0px)) { + .fancybox-caption { + padding-left: max(12px, env(safe-area-inset-left)); + padding-right: max(12px, env(safe-area-inset-right)) + } + } +} + +.fancybox-share { + background: #f4f4f4; + border-radius: 3px; + max-width: 90%; + padding: 30px; + text-align: center +} + +.fancybox-share h1 { + color: #222; + font-size: 35px; + font-weight: 700; + margin: 0 0 20px +} + +.fancybox-share p { + margin: 0; + padding: 0 +} + +.fancybox-share__button { + border: 0; + border-radius: 3px; + display: inline-block; + font-size: 14px; + font-weight: 700; + line-height: 40px; + margin: 0 5px 10px; + min-width: 130px; + padding: 0 15px; + text-decoration: none; + transition: all .2s; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + white-space: nowrap +} + +.fancybox-share__button:link, .fancybox-share__button:visited { + color: #fff +} + +.fancybox-share__button:hover { + text-decoration: none +} + +.fancybox-share__button--fb { + background: #3b5998 +} + +.fancybox-share__button--fb:hover { + background: #344e86 +} + +.fancybox-share__button--pt { + background: #bd081d +} + +.fancybox-share__button--pt:hover { + background: #aa0719 +} + +.fancybox-share__button--tw { + background: #1da1f2 +} + +.fancybox-share__button--tw:hover { + background: #0d95e8 +} + +.fancybox-share__button svg { + height: 25px; + margin-right: 7px; + position: relative; + top: -1px; + vertical-align: middle; + width: 25px +} + +.fancybox-share__button svg path { + fill: #fff +} + +.fancybox-share__input { + background: transparent; + border: 0; + border-bottom: 1px solid #d7d7d7; + border-radius: 0; + color: #5d5b5b; + font-size: 14px; + margin: 10px 0 0; + outline: none; + padding: 10px 15px; + width: 100% +} + +.fancybox-thumbs { + background: #ddd; + bottom: 0; + display: none; + margin: 0; + -webkit-overflow-scrolling: touch; + -ms-overflow-style: -ms-autohiding-scrollbar; + padding: 2px 2px 4px; + position: absolute; + right: 0; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + top: 0; + width: 212px; + z-index: 99995 +} + +.fancybox-thumbs-x { + overflow-x: auto; + overflow-y: hidden +} + +.fancybox-show-thumbs .fancybox-thumbs { + display: block +} + +.fancybox-show-thumbs .fancybox-inner { + right: 212px +} + +.fancybox-thumbs__list { + font-size: 0; + height: 100%; + list-style: none; + margin: 0; + overflow-x: hidden; + overflow-y: auto; + padding: 0; + position: absolute; + position: relative; + white-space: nowrap; + width: 100% +} + +.fancybox-thumbs-x .fancybox-thumbs__list { + overflow: hidden +} + +.fancybox-thumbs-y .fancybox-thumbs__list::-webkit-scrollbar { + width: 7px +} + +.fancybox-thumbs-y .fancybox-thumbs__list::-webkit-scrollbar-track { + background: #fff; + border-radius: 10px; + box-shadow: inset 0 0 6px rgba(0, 0, 0, .3) +} + +.fancybox-thumbs-y .fancybox-thumbs__list::-webkit-scrollbar-thumb { + background: #2a2a2a; + border-radius: 10px +} + +.fancybox-thumbs__list a { + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + background-color: rgba(0, 0, 0, .1); + background-position: 50%; + background-repeat: no-repeat; + background-size: cover; + cursor: pointer; + float: left; + height: 75px; + margin: 2px; + max-height: calc(100% - 8px); + max-width: calc(50% - 4px); + outline: none; + overflow: hidden; + padding: 0; + position: relative; + -webkit-tap-highlight-color: transparent; + width: 100px +} + +.fancybox-thumbs__list a:before { + border: 6px solid #ff5268; + bottom: 0; + content: ""; + left: 0; + opacity: 0; + position: absolute; + right: 0; + top: 0; + transition: all .2s cubic-bezier(.25, .46, .45, .94); + z-index: 99991 +} + +.fancybox-thumbs__list a:focus:before { + opacity: .5 +} + +.fancybox-thumbs__list a.fancybox-thumbs-active:before { + opacity: 1 +} + +@media (max-width: 576px) { + .fancybox-thumbs { + width: 110px + } + + .fancybox-show-thumbs .fancybox-inner { + right: 110px + } + + .fancybox-thumbs__list a { + max-width: calc(100% - 10px) + } +} \ No newline at end of file diff --git a/blog-site/public/css/highlight.css b/blog-site/public/css/highlight.css new file mode 100644 index 00000000..2cd35eaf --- /dev/null +++ b/blog-site/public/css/highlight.css @@ -0,0 +1,82 @@ +/* Background */ .chroma { background-color: #ffffff } +/* Other */ .chroma .x { } +/* Error */ .chroma .err { color: #a61717; background-color: #e3d2d2 } +/* LineTableTD */ .chroma .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; } +/* LineTable */ .chroma .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; width: auto; overflow: auto; display: block; } +/* LineHighlight */ .chroma .hl { display: block; width: 100%;background-color: #ffffcc } +/* LineNumbersTable */ .chroma .lnt { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } +/* LineNumbers */ .chroma .ln { margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f } +/* Keyword */ .chroma .k { color: #000000; font-weight: bold } +/* KeywordConstant */ .chroma .kc { color: #000000; font-weight: bold } +/* KeywordDeclaration */ .chroma .kd { color: #000000; font-weight: bold } +/* KeywordNamespace */ .chroma .kn { color: #000000; font-weight: bold } +/* KeywordPseudo */ .chroma .kp { color: #000000; font-weight: bold } +/* KeywordReserved */ .chroma .kr { color: #000000; font-weight: bold } +/* KeywordType */ .chroma .kt { color: #445588; font-weight: bold } +/* Name */ .chroma .n { } +/* NameAttribute */ .chroma .na { color: #008080 } +/* NameBuiltin */ .chroma .nb { color: #0086b3 } +/* NameBuiltinPseudo */ .chroma .bp { color: #999999 } +/* NameClass */ .chroma .nc { color: #445588; font-weight: bold } +/* NameConstant */ .chroma .no { color: #008080 } +/* NameDecorator */ .chroma .nd { color: #3c5d5d; font-weight: bold } +/* NameEntity */ .chroma .ni { color: #800080 } +/* NameException */ .chroma .ne { color: #990000; font-weight: bold } +/* NameFunction */ .chroma .nf { color: #990000; font-weight: bold } +/* NameFunctionMagic */ .chroma .fm { } +/* NameLabel */ .chroma .nl { color: #990000; font-weight: bold } +/* NameNamespace */ .chroma .nn { color: #555555 } +/* NameOther */ .chroma .nx { } +/* NameProperty */ .chroma .py { } +/* NameTag */ .chroma .nt { color: #000080 } +/* NameVariable */ .chroma .nv { color: #008080 } +/* NameVariableClass */ .chroma .vc { color: #008080 } +/* NameVariableGlobal */ .chroma .vg { color: #008080 } +/* NameVariableInstance */ .chroma .vi { color: #008080 } +/* NameVariableMagic */ .chroma .vm { } +/* Literal */ .chroma .l { } +/* LiteralDate */ .chroma .ld { } +/* LiteralString */ .chroma .s { color: #dd1144 } +/* LiteralStringAffix */ .chroma .sa { color: #dd1144 } +/* LiteralStringBacktick */ .chroma .sb { color: #dd1144 } +/* LiteralStringChar */ .chroma .sc { color: #dd1144 } +/* LiteralStringDelimiter */ .chroma .dl { color: #dd1144 } +/* LiteralStringDoc */ .chroma .sd { color: #dd1144 } +/* LiteralStringDouble */ .chroma .s2 { color: #dd1144 } +/* LiteralStringEscape */ .chroma .se { color: #dd1144 } +/* LiteralStringHeredoc */ .chroma .sh { color: #dd1144 } +/* LiteralStringInterpol */ .chroma .si { color: #dd1144 } +/* LiteralStringOther */ .chroma .sx { color: #dd1144 } +/* LiteralStringRegex */ .chroma .sr { color: #009926 } +/* LiteralStringSingle */ .chroma .s1 { color: #dd1144 } +/* LiteralStringSymbol */ .chroma .ss { color: #990073 } +/* LiteralNumber */ .chroma .m { color: #009999 } +/* LiteralNumberBin */ .chroma .mb { color: #009999 } +/* LiteralNumberFloat */ .chroma .mf { color: #009999 } +/* LiteralNumberHex */ .chroma .mh { color: #009999 } +/* LiteralNumberInteger */ .chroma .mi { color: #009999 } +/* LiteralNumberIntegerLong */ .chroma .il { color: #009999 } +/* LiteralNumberOct */ .chroma .mo { color: #009999 } +/* Operator */ .chroma .o { color: #000000; font-weight: bold } +/* OperatorWord */ .chroma .ow { color: #000000; font-weight: bold } +/* Punctuation */ .chroma .p { } +/* Comment */ .chroma .c { color: #999988; font-style: italic } +/* CommentHashbang */ .chroma .ch { color: #999988; font-style: italic } +/* CommentMultiline */ .chroma .cm { color: #999988; font-style: italic } +/* CommentSingle */ .chroma .c1 { color: #6a737d; font-style: italic } +/* CommentSpecial */ .chroma .cs { color: #999999; font-weight: bold; font-style: italic } +/* CommentPreproc */ .chroma .cp { color: #999999; font-weight: bold; font-style: italic } +/* CommentPreprocFile */ .chroma .cpf { color: #999999; font-weight: bold; font-style: italic } +/* Generic */ .chroma .g { } +/* GenericDeleted */ .chroma .gd { color: #000000; background-color: #ffdddd } +/* GenericEmph */ .chroma .ge { color: #000000; font-style: italic } +/* GenericError */ .chroma .gr { color: #aa0000 } +/* GenericHeading */ .chroma .gh { color: #999999 } +/* GenericInserted */ .chroma .gi { color: #000000; background-color: #ddffdd } +/* GenericOutput */ .chroma .go { color: #888888 } +/* GenericPrompt */ .chroma .gp { color: #555555 } +/* GenericStrong */ .chroma .gs { font-weight: bold } +/* GenericSubheading */ .chroma .gu { color: #aaaaaa } +/* GenericTraceback */ .chroma .gt { color: #aa0000 } +/* GenericUnderline */ .chroma .gl { text-decoration: underline } +/* TextWhitespace */ .chroma .w { color: #bbbbbb } diff --git a/blog-site/public/css/normalize.css b/blog-site/public/css/normalize.css new file mode 100644 index 00000000..c45a85f8 --- /dev/null +++ b/blog-site/public/css/normalize.css @@ -0,0 +1,349 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + + html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + } + + /* Sections + ========================================================================== */ + + /** + * Remove the margin in all browsers. + */ + + body { + margin: 0; + } + + /** + * Render the `main` element consistently in IE. + */ + + main { + display: block; + } + + /** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + + h1 { + font-size: 2em; + margin: 0.67em 0; + } + + /* Grouping content + ========================================================================== */ + + /** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + + hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ + } + + /** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + + pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ + } + + /* Text-level semantics + ========================================================================== */ + + /** + * Remove the gray background on active links in IE 10. + */ + + a { + background-color: transparent; + } + + /** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + + abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ + } + + /** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + + b, + strong { + font-weight: bolder; + } + + /** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + + code, + kbd, + samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ + } + + /** + * Add the correct font size in all browsers. + */ + + small { + font-size: 80%; + } + + /** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + + sub, + sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + /* Embedded content + ========================================================================== */ + + /** + * Remove the border on images inside links in IE 10. + */ + + img { + border-style: none; + } + + /* Forms + ========================================================================== */ + + /** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + + button, + input, + optgroup, + select, + textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ + } + + /** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + + button, + input { /* 1 */ + overflow: visible; + } + + /** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + + button, + select { /* 1 */ + text-transform: none; + } + + /** + * Correct the inability to style clickable types in iOS and Safari. + */ + + button, + [type="button"], + [type="reset"], + [type="submit"] { + -webkit-appearance: button; + } + + /** + * Remove the inner border and padding in Firefox. + */ + + button::-moz-focus-inner, + [type="button"]::-moz-focus-inner, + [type="reset"]::-moz-focus-inner, + [type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; + } + + /** + * Restore the focus styles unset by the previous rule. + */ + + button:-moz-focusring, + [type="button"]:-moz-focusring, + [type="reset"]:-moz-focusring, + [type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; + } + + /** + * Correct the padding in Firefox. + */ + + fieldset { + padding: 0.35em 0.75em 0.625em; + } + + /** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + + legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ + } + + /** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + + progress { + vertical-align: baseline; + } + + /** + * Remove the default vertical scrollbar in IE 10+. + */ + + textarea { + overflow: auto; + } + + /** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + + [type="checkbox"], + [type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ + } + + /** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + + [type="number"]::-webkit-inner-spin-button, + [type="number"]::-webkit-outer-spin-button { + height: auto; + } + + /** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + + [type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ + } + + /** + * Remove the inner padding in Chrome and Safari on macOS. + */ + + [type="search"]::-webkit-search-decoration { + -webkit-appearance: none; + } + + /** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + + ::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ + } + + /* Interactive + ========================================================================== */ + + /* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + + details { + display: block; + } + + /* + * Add the correct display in all browsers. + */ + + summary { + display: list-item; + } + + /* Misc + ========================================================================== */ + + /** + * Add the correct display in IE 10+. + */ + + template { + display: none; + } + + /** + * Add the correct display in IE 10. + */ + + [hidden] { + display: none; + } \ No newline at end of file diff --git a/blog-site/public/css/toc.css b/blog-site/public/css/toc.css new file mode 100644 index 00000000..8fb0b9df --- /dev/null +++ b/blog-site/public/css/toc.css @@ -0,0 +1,70 @@ +/* toc style */ +.toc { + height: 80%; + position: fixed; + top: 50%; + left: 3%; + width: 15%; + transform: translateY(-50%); + background-color: #f6f6f6; + border-radius: 5px; + padding-bottom: 1rem; + font-size: 12px; + overflow: scroll !important; +} +.toc::-webkit-scrollbar { + /*滚动条整体样式*/ + width : 7px; /*高宽分别对应横竖滚动条的尺寸*/ + height: 1px; +} +.toc::-webkit-scrollbar-thumb { + /*滚动条里面小方块*/ + border-radius: 5px; + background-color: rgba(153, 153, 153); +} +.toc::-webkit-scrollbar-track { +/* !*滚动条里面轨道*!*/ + border-radius: 7px; + background : #f7f7f7; +} + +.toc .page-header { + margin: 1rem 0 1rem 25px; +} + +.toc-nav ul { + overflow:hidden; + white-space:nowrap; + line-height: 1rem; +} +.toc-nav{ + margin-left: -25px; +} + +/* ignore h1 header */ +.toc-nav ul ul ul { + margin-left: -30px; +} + +.toc-nav .nav-link { + text-overflow:ellipsis; + overflow:hidden; + color: #333; +} + + +.toc-nav li.active .nav-link { + background-color: #f6f6f6; + color: var(--accent); + border-left: solid 2px var(--accent); +} + +/* Media Queries */ +@media (max-width: 1080px) { + main { + max-width: 100%; + } + .toc { + display: none; + } +} \ No newline at end of file diff --git a/blog-site/public/css/zozo.css b/blog-site/public/css/zozo.css new file mode 100644 index 00000000..e38d8f08 --- /dev/null +++ b/blog-site/public/css/zozo.css @@ -0,0 +1,774 @@ +@charset "UTF-8"; +/*basic styles starts*/ +/*黑白色主题*/ +.darkmode-toggle { + z-index: 100; +} +.darkmode-layer{ + z-index: 1; +} + +html { + background-color: #f7f7f7; + -webkit-font-smoothing: antialiased; +} + +html::-webkit-scrollbar { + width: 13px; +} + +html::-webkit-scrollbar-button { + display: none; +} + +html::-webkit-scrollbar-thumb { + min-height: 16px; + background-color: #999999; + background-clip: padding-box; + border: 3px solid #fdfdfd; + border-radius: 5px; +} + +body { + color: #333333; + font-family: "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + font-size: 16px; + width: 100%; + background-color: #f7f7f7; + margin: 0; + padding: 0; +} + +p { + line-height: 1.9em; + font-weight: 400; + margin: 0; +} + +a { + text-decoration: none; +} + +a:link, a:visited { + opacity: 1; + -webkit-transition: all 0.15s linear; + transition: all 0.15s linear; + color: #424242; +} + +a:hover, a:active { + color: #555555; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + text-rendering: optimizeLegibility; +} + +hr { + border: 1px solid #f3f3f3; +} + +/*basic styles ends*/ +.main { + background: #ffffff; + -webkit-box-shadow: 0 10px 20px 0 rgba(236, 236, 236, 0.86); + box-shadow: 0 10px 20px 0 rgba(236, 236, 236, 0.86); + width: 820px; + margin: 60px auto 0; +} + +.content { + width: 720px; + height: auto; + margin: 0 auto; +} + +.list_with_title .container { + width: 720px; + margin: 0 auto; +} + +.nav_container { + width: 740px; + height: 3rem; + margin: 0 auto; + padding: 20px 0; +} + +.nav_container .menu_icon { + padding: 10px; + float: right; + display: none; +} + +.nav_container .menu_icon a { + cursor: pointer; +} + +.nav_container .site_nav { + float: right; +} + +.nav_container .site_nav ul { + list-style: none; + margin: 0; + padding: 0; + float: right; +} + +.nav_container .site_nav li, +.nav_container .site_nav a { + position: relative; +} + +.nav_container .site_nav li { + float: left; +} + +.nav_container .site_nav a { + display: block; + white-space: nowrap; + padding: 10px; + font-size: 0.9rem; +} + +.header { + width: 720px; + padding: 50px 0 120px 0; + margin: 0 auto; + background-size: cover; +} + +.header .site_title_container { + padding: 0 0; +} + +.header .site_title_container h1 { + line-height: 1; + margin: 0; +} + +.header .site_title_container a { + text-decoration: none; + font-weight: bold; + float: left; +} + +.header .site_title_container .site_title a { + font-size: 28px; + width: 65px; + letter-spacing: 2px; + line-height: 1.2; + color: #000000; +} + +.header .site_title_container .site_title a span { + display: block; +} + +.header .site_title_container .site_title a:hover { + color: #666666; +} + +.header .site_title_container .description { + font-size: 0.9rem; + color: #565654; + float: right; + text-align: right; +} + +.header .site_title_container .description .sub_title { + font-weight: normal !important; + float: none !important; + text-align: right; +} + +.header .my_socials { + list-style: none; + font-size: 14px; + float: right; + margin: 5px 0; +} + +.header .my_socials i { + margin-left: 10px; +} + +.header .my_socials a { + color: #5f5f5f; + font-size: 16px; + float: none; + cursor: pointer; +} + +.header .my_socials a:hover { + color: #000000; +} + +.header .sys_function { + list-style: none; + font-size: 14px; +} + + +.post { + background-color: #ffffff; + margin-top: 50px; +} + +.post .post_title h2 { + letter-spacing: 1px; + font-size: 1.4rem; + line-height: 1; + font-weight: 600; + color: #1f1f1f; + margin: 0 0 6px 0; +} + +.post .post_title a { + text-decoration: none; + letter-spacing: 1px; + color: #1f1f1f; + font-size: 1.5rem; + line-height: 28px; +} + +.post .post_title span { + color: rgba(0, 0, 0, 0.44); + font-size: 14px; +} + +.post .post_title span.date { + font-size: 14px; +} + +.post .post_detail_title h2 { + font-size: 1.5rem; + font-weight: bold; +} + +.post .list p { + padding-bottom: 0 !important; +} + +.post .post_content { + word-break: normal; + margin-top: 20px; +} + +.post .post_content p { + line-height: 2em; + letter-spacing: 0.2px; + margin-bottom: 20px; +} + +.post .post_content a { + text-decoration: none; + letter-spacing: 1px; + color: #e42b2b; +} + +.post .post_content a:hover { + color: #8e0000; +} + +.post .post_footer { + padding: 20px 0; + /*border-bottom: 1px solid #f3f3f3;*/ +} + +.post .post_footer .meta { + max-width: 100%; + height: 25px; + color: #bbbbbb; +} + +.post .post_footer .meta .info { + float: left; + font-size: 11px; +} + +.post .post_footer .meta .info .date { + margin-right: 10px; +} + +.post .post_footer .meta .field { + margin-right: 10px; +} + +.post .post_footer .meta .tags a { + text-decoration: none; + color: #bbbbbb; + padding-right: 6px; +} + +.post .post_footer .meta .tags a:hover { + color: #1f1f1f; +} + +.page_tags { + text-align: center; + margin-top: 50px; +} + +.page_tags ul li { + margin: 10px 15px; + display: inline-block; + font-size: 1em; +} + +.page_tags .terms_count { + display: inline-block; + position: relative; + top: -8px; + right: -2px; + color: #c5c5c5; + font-size: 12px; +} + +.page_tags ul { + margin: 0; + padding: 0; +} + +.doc_comments { + font-size: 14px; + color: #383838; + margin-top: 30px; + padding: 35px 0 45px; +} + +.doc_comments .comments_block_title { + color: #000000; + padding: .25em; + /*margin-bottom: 15px;*/ + font-size: 20px; +} + + +.footer { + clear: both; + max-width: 780px; + text-align: center; + font-size: 12px; + padding: 40px 0; + margin: 0 auto; +} + +.footer a { + color: #a6a6a6; + margin: 0 12px; +} + +.footer a:hover { + color: #1f1f1f; +} + +.footer .powered_by { + margin: 0; + font-size: 11px; +} + +.footer .powered_by a { + color: #cccccc; + margin: 0 2px; +} + +.footer .powered_by a:hover { + color: #1f1f1f; +} + +.footer .footer_slogan { + padding-top: 25px; + padding-bottom: 10px; + color: #000000; + font-size: 16px; + letter-spacing: 1px; +} + +/*for archive*/ +.list_with_title { + font-size: 14px; + margin: 0; + padding: 0; +} + +.list_with_title li { + list-style-type: none; + padding: 0; +} + +.list_with_title .listing_title { + font-size: 1.4rem; + color: #1f1f1f; + font-weight: bold; + padding-top: 10px; + line-height: 2.2em; +} + +.list_with_title .listing { + margin: 0 0 50px 0; + padding: 0; + line-height: 2.1; +} + +.list_with_title .listing .listing_post { + padding-bottom: 5px; +} + +.list_with_title .listing .listing_post a { + display: inline-block; + width: 85%; +} + +.list_with_title .listing .listing_post .post_time { + float: right; + color: #c5c5c5; + font-size: 14px; +} + +.list_with_title .listing .listing_post a:hover { + color: #1f1f1f; +} + +.pagination { + padding: 30px 0 60px 0; + border-bottom: 1px solid #f2f2f2; + color: #666666; + font-size: 14px; +} + +.pagination a { + color: #888888; + text-decoration: none; +} + +.pagination a:hover { + color: #333333; +} + +.pagination .pre { + float: left; +} + +.pagination .next { + float: right; +} + +.markdown { + line-height: 1.8em; + word-wrap: break-word; + word-break: normal; + overflow-wrap: break-word; +} + +.markdown ul, +.markdown ol, +.markdown dl { + margin: 0.8em 0; +} + +.markdown h1, +.markdown h2, +.markdown h3, +.markdown h4, +.markdown h5, +.markdown h6 { + margin: 20px 0 20px 0; + color: #000000; +} + +.markdown h1 { + font-size: 1.35em; +} + +.markdown h2 { + font-size: 1.25em; +} + +.markdown h3 { + font-size: 1.15em; +} + +.markdown h4 { + font-size: 1.1em; +} + +.markdown pre, +.markdown code { + font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + font-size: 0.9em; + background: #f8fafc; + border: none; + white-space: pre; +} + +.markdown pre { + padding: 16px; + display: block; + overflow: auto; + white-space: pre; + word-wrap: break-word; +} + +.markdown code { + color: #666666; + padding: 4px 5px; + border-radius: 2px; + white-space: nowrap; +} + +.markdown pre code { + padding: 0; + color: #555555; + white-space: pre; +} + +.markdown blockquote p { + white-space: pre-line; +} + +.markdown pre::-webkit-scrollbar { + height: 10px; +} + +.markdown pre::-webkit-scrollbar-button { + display: none; +} + +.markdown pre::-webkit-scrollbar-track { + background: white; +} + +.markdown pre::-webkit-scrollbar-thumb { + min-height: 1rem; + background-color: #cccccc; + background-clip: padding-box; + border: 3px solid #fdfdfd; + border-radius: 5px; +} + +.markdown pre::-webkit-scrollbar-thumb:active { + background-color: #999999; + border-width: 2px; +} + +.markdown blockquote { + -webkit-box-sizing: border-box; + box-sizing: border-box; + margin: 2.5em 0; + padding: 0 0 0 50px; + font-style: italic; + color: #555555; + border-left: none; +} + +.markdown blockquote:before { + content: '“'; + display: block; + font-family: times, sans-serif; + font-style: normal; + font-size: 48px; + color: #444444; + font-weight: bold; + line-height: 30px; + margin-left: -50px; + position: absolute; +} + +.markdown strong, +.markdown b, +.markdown em { + padding: 1px 2px; + background-color: #fcfcf0; + font-weight: normal; +} + +.markdown .fancybox { + text-align: center; +} + +.markdown img { + max-width: 90%; + height: auto; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +table th { + background-color: #f6f8fa; +} + +table th, table td { + padding: 10px 20px; + border: 1px solid #dfe2e5; +} + + +a.back_to_top,a.content_hidden,a.content_display,a.top_to_back { + text-decoration: none; + position: fixed; + bottom: 40px; + right: 30px; + background: #f0f0f0; + height: 40px; + width: 40px; + border-radius: 50%; + line-height: 36px; + font-size: 18px; + text-align: center; + -webkit-transition-duration: 0.5s; + transition-duration: 0.5s; + -webkit-transition-property: background-color; + transition-property: background-color; + display: none; +} + +.content_hidden{ + bottom: 100px !important; + line-height: 44px !important; + font-size: 20px !important; +} + +.content_display{ + bottom: 100px !important; + line-height: 44px !important; +} + +.top_to_back{ + bottom: 160px !important; + line-height: 50px !important; +} + +a.back_to_top span { + color: #888888; +} + +a.back_to_top:hover,a.content_hidden:hover,a.content_display:hover,a.top_to_back:hover { + cursor: pointer; + background: #dfdfdf; +} + +a.back_to_top:hover span { + color: #555555; +} + +@media print, screen and (max-width: 680px) { + .nav_container { + position: relative; + z-index: 999; + } + .nav_container .site_nav { + display: none; + position: fixed; + right: 0; + top: 60px; + padding: 10px 0; + width: 100%; + background-color: #ffffff; + -webkit-box-shadow: 0 10px 20px 0 rgba(236, 236, 236, 0.86); + box-shadow: 0 10px 20px 0 rgba(236, 236, 236, 0.86); + } + .nav_container .site_nav ul { + float: none; + text-align: center; + } + .nav_container .site_nav li { + float: none; + } + .nav_container .menu_icon { + position: fixed; + right: 0; + display: block; + } + .back_to_top { + display: none !important; + } +} + +@media screen and (min-width: 1600px) { + .main { + width: 1000px; + } + .nav_container { + width: 840px; + } + .header, + .content { + width: 820px; + } +} + +@media screen and (max-width: 900px) { + .main { + width: 95%; + margin-top: 20px; + } + .nav_container { + width: 95%; + } + .header, + .content { + width: 90%; + } + .post_page { + padding-top: 0; + } + .footer a { + margin: 0 6px; + } + .post { + margin-top: 40px; + } + .post .post_title a { + font-size: 1.4rem; + } + .post_footer { + padding-bottom: 30px; + } +} + +/*animation starts*/ +@-webkit-keyframes fadeInDown { + 0% { + opacity: 0; + -webkit-transform: translateY(-3px); + } + 100% { + opacity: 1; + -webkit-transform: translateY(0); + } +} + +@keyframes fadeInDown { + 0% { + opacity: 0; + -webkit-transform: translateY(-3px); + transform: translateY(-3px); + } + 100% { + opacity: 1; + -webkit-transform: translateY(0); + transform: translateY(0); + } +} + +/*animation ends*/ + +.display_none{ + display: none !important; +} + +.display_block{ + display: block !important; +} + + diff --git a/blog-site/public/en/404.html b/blog-site/public/en/404.html new file mode 100644 index 00000000..944fce8f --- /dev/null +++ b/blog-site/public/en/404.html @@ -0,0 +1,166 @@ + + + + + + + + + + + 404 Page not found | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+

○| ̄|_ =3

+

404 Page Not Found

+

Home

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/en/categories/index.html b/blog-site/public/en/categories/index.html new file mode 100644 index 00000000..e4efa8b1 --- /dev/null +++ b/blog-site/public/en/categories/index.html @@ -0,0 +1,187 @@ + + + + + + + + + + + Categories | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
    + + +
+
+ +
+
+
+ + + + +
+ + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/en/categories/index.xml b/blog-site/public/en/categories/index.xml new file mode 100644 index 00000000..5babaed2 --- /dev/null +++ b/blog-site/public/en/categories/index.xml @@ -0,0 +1,11 @@ + + + + Categories on 唯手熟尔 + http://localhost:1313/iblog/en/categories/ + Recent content in Categories on 唯手熟尔 + Hugo -- gohugo.io + en + + + diff --git a/blog-site/public/en/index.html b/blog-site/public/en/index.html new file mode 100644 index 00000000..d01c7bc9 --- /dev/null +++ b/blog-site/public/en/index.html @@ -0,0 +1,187 @@ + + + + + + + + + + + + 唯手熟尔 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+ + + + +
+
+
+ + + + +
+ + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/en/index.xml b/blog-site/public/en/index.xml new file mode 100644 index 00000000..ffcd36e7 --- /dev/null +++ b/blog-site/public/en/index.xml @@ -0,0 +1,11 @@ + + + + 唯手熟尔 + http://localhost:1313/iblog/en/ + Recent content on 唯手熟尔 + Hugo -- gohugo.io + en + + + diff --git a/blog-site/public/en/page/1/index.html b/blog-site/public/en/page/1/index.html new file mode 100644 index 00000000..4c5d0535 --- /dev/null +++ b/blog-site/public/en/page/1/index.html @@ -0,0 +1,10 @@ + + + + http://localhost:1313/iblog/en/ + + + + + + diff --git a/blog-site/public/en/sitemap.xml b/blog-site/public/en/sitemap.xml new file mode 100644 index 00000000..eec4a99e --- /dev/null +++ b/blog-site/public/en/sitemap.xml @@ -0,0 +1,41 @@ + + + + http://localhost:1313/iblog/en/categories/ + + + + http://localhost:1313/iblog/en/tags/ + + + + http://localhost:1313/iblog/en/ + + + + diff --git a/blog-site/public/en/tags/index.html b/blog-site/public/en/tags/index.html new file mode 100644 index 00000000..24642f55 --- /dev/null +++ b/blog-site/public/en/tags/index.html @@ -0,0 +1,187 @@ + + + + + + + + + + + Tags | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
    + + +
+
+ +
+
+
+ + + + +
+ + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/en/tags/index.xml b/blog-site/public/en/tags/index.xml new file mode 100644 index 00000000..1f982280 --- /dev/null +++ b/blog-site/public/en/tags/index.xml @@ -0,0 +1,11 @@ + + + + Tags on 唯手熟尔 + http://localhost:1313/iblog/en/tags/ + Recent content in Tags on 唯手熟尔 + Hugo -- gohugo.io + en + + + diff --git a/blog-site/public/images/favicon.ico b/blog-site/public/images/favicon.ico new file mode 100644 index 00000000..a59308e2 Binary files /dev/null and b/blog-site/public/images/favicon.ico differ diff --git a/blog-site/public/index.html b/blog-site/public/index.html new file mode 100644 index 00000000..36d92a63 --- /dev/null +++ b/blog-site/public/index.html @@ -0,0 +1,832 @@ + + + + + + + + + + + + 唯手熟尔 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+ + +
+
+

2023工作总结

+
+ +
+
+

在职期间,我主要负责耐材项目的开发与维护,共迭代171个版本。通过与团队成员的紧密合作,我们按时完成了项目中的需求。在这段时间里,我不断提升自己的专业技能和知识,增强了自己的专业能力。我始终认为团队合作是成功的关键。在工作中,我积极与同事沟......

+
+
+ + +
+ +
+
+

20230915简历

+
+ +
+
+

自我介绍 1998 · 李济芝 河北唐山 15176733539  m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和四年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对Jav......

+
+
+ + +
+ +
+
+

定时任务可视化管理

+
+ +
+
+

代码实现 代码结构 pom <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> 库表结构 -- ---------------------------- -- 定时任务调度表 -- ---------------------------- drop table if exists sys_job; create table sys_job ( job_id bigint(20) not null auto_increment comment '任务ID', job_name varchar(64) default '' comment '任务名称', job_group varchar(64) default 'DEFAULT' comment '任务组名', invoke_target varchar(500) not null comment......

+
+
+ + +
+ +
+
+

SpringBoot整合nacos

+
+ +
+
+

nacos nacos下载 下载地址 一键傻瓜试安装即可,官网写的很清楚这里不在赘述 http://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html nacos启动 将模式改为单机模式 启动成功 nacos相关配置 demo-dev.yaml server: port: 8001 config: info: "config info for dev from nacos config center" demo-test.yaml server: port: 3333 config: info: "config info for test from nacos config center" user.yaml user: name: zs1112222 age: 10 address: 测试地址 代码 整合nacos配置中心,注册......

+
+
+ + +
+ +
+
+

整合文件上传功能

+
+ +
+
+

结构 pom.xml fastdfs-client-java-1.27.jar:点击下载 <dependencies> <!-- fastdfs --> <dependency> <groupId>org.csource</groupId> <artifactId>fastdfs-client-java</artifactId> <version>1.27</version> <systemPath>${project.basedir}/lib/fastdfs-client-java-1.27.jar</systemPath> <scope>system</scope> </dependency> <!--aliyun oss 依赖--> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.11</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency> </dependencies> application.yml server: port: 80 公共部分 FileManagement public interface FileManagement { /** * 设置下一个bean的对象 * * @param nextFileManagement 下一个......

+
+
+ + +
+ +
+
+

整合支付功能

+
+ +
+
+

结构 pom.xml <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.11</version> </dependency> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.9.9</version> </dependency> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-pay</artifactId> <version>4.5.0</version> </dependency> </dependencies> application.yml server: port: 8080 pay: wechat: #微信公众号或者小程序等的appid appId: "" #微信支付商户号 mchId: "" #微信支付商户密钥 mchKey: "" #服务商模式下的子商户公众账号ID subAppId: #服务商模式下的子商户号 subMchId: # p12证书的位......

+
+
+ + +
+ +
+
+

Validator参数校验

+
+ +
+
+

常见参数校验 在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,最简单就是用if条件语句来判断,但是随着参数越来越多,业务越来越复杂,判断参数代码语句显得尤为冗长. 或者有些程序会将if封装起来,例如spring中......

+
+
+ + +
+ +
+
+

管道流设计模式结合业务

+
+ +
+
+

流程图 代码实现 pom <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.plugin</groupId> <artifactId>spring-plugin-core</artifactId> <version>${spring.plugin.core.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> </dependencies> context EventContext public interface EventContext { /** * 是否继续调用链 */ boolean continueChain(); /** * 获取当前过滤器选择器 */ FilterSelector getFilterSelector(); } BizType public interface BizType { /** * 获取业务类型码值 */ Integer getCode(); /** * 业务类型名称 * */ String getName(); } AbstractEventContext public abstract class AbstractEventContext implements EventContext{ private final BizType businessType; private final FilterSelector filterSelector; protected AbstractEventContext(BizType businessType, FilterSelector filterSelector) {......

+
+
+ + +
+ +
+
+

重构一个程序

+
+ +
+
+

什么是重构 摘自《重构:改善既有代码的设计》 重构(名词形式): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。 重构(动词形式): 使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构。 重......

+
+
+ + +
+ +
+
+

SpringMVC与SpringWebFlux

+
+ +
+
+

Spring MVC Spring Web MVC是建立在Servlet API上的原始Web框架,从一开始就包含在Spring框架中。正式名称 “Spring Web MVC “来自其源模块的名称(spring-webmvc),但它更常被称为 “Spring MVC”。 SpringMVC是基于S......

+
+
+ + +
+ +
+
+

MySQL详解

+
+ +
+
+

逻辑架构 主要分为:连接层,服务层,引擎层,存储层。 客户端执行一条select命令的流程如下: 连接层:最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcplip的通信。主要完成一些类似于连接处理、......

+
+
+ + +
+ +
+
+

如何减少及解决bug

+
+ +
+
+

bug的起源: 1945年,一只小飞蛾钻进了计算机电路里,导致系统无法工作,一位名叫格蕾丝·赫柏的人把飞蛾拍死在工作日志上,写道:就是这个 bug(虫子),害我们今天的工作无法完成——于是,bug一词成了电脑系统程序的专业术语,形容那些系统中的......

+
+
+ + +
+ +
+
+

Elasticsearch详解

+
+ +
+
+

概览 Elasticsearch,简称为 ES, ES是一个开源的高扩展的分布式全文搜索引擎, 是整个 ElasticStack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。 Elastic Stack, 包括 Elasticsearch、 Ki......

+
+
+ + +
+ +
+
+

编程常用词汇汇总

+
+ +
+
+

QPS 即 Queries Per Second的缩写,每秒能处理查询数目。是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。 TPS 即 Transactions Per Second的缩写,每秒处理的事务数目。一个事务是指一个客户机向服务器发送请求然后服务......

+
+
+ + +
+ +
+
+

接口优化

+
+ +
+
+

接口优化 线上接口很慢,线上生产问题,我们绝对不能马虎放过抱着侥幸心理,必须要找到根本原因及时处理,防止下次留下更大的坑.大致思路要定位接口问题,然后具体问题具体分析,讨论不同解决方案. 定位问题 要快速定位接口哪一个环节比较慢,性能瓶颈在哪里,......

+
+
+ + +
+ +
+
+

孙子兵法

+
+ +
+
+

始计 兵者,国之大事,死生之地,存亡之道,不可不察也。 故经之以五事,校之以计,而索其情:一曰道,二曰天,三曰地,四曰将,五曰法。道者,令民于上同意,可与之死,可与之生,而不危也;天者,阴阳、寒暑、时制也;地者,远近、险易、广狭、死生也;将者,......

+
+
+ + +
+ +
+
+

IDEA常用配置及使用技巧

+
+ +
+
+

下载 工欲善其事必先利其器,一个好的开发工具,能极大提高开发效率. 新UI很漂亮。IDEA 2022.2.3 官方下载地址: https://www.jetbrains.com/zh-cn/idea/download/other.html 激活工具 百度云下载. 链接:https://pan.baidu.com/s/19sCUTCBXvwXgEQc8vX-SYQ?pwd=gwu......

+
+
+ + +
+ +
+
+

整洁的代码

+
+ +
+
+

为什么要写整洁的代码 为什么要写整洁的代码,回答这个问题之前,也许应该想想写糟糕的代码的原因 是想快点完成吗?还是要赶时间吗?有可能.或许你觉得自己要干好所需要的时间不够;假使花时间清理代码,老板就会大发雷霆.或许你只是不耐烦再搞这套程序,期望......

+
+
+ + +
+ +
+
+

关于编程的书籍

+
+ +
+
+

HeadFirst设计模式 Java数据结构和算法 Java核心技术卷I基础知识 Java编程思想 代码整洁之道 大型网站技术架构 大话数据结构 深入分析JavaWeb技术内幕 疯狂Java讲义 重构:改善既有代码的设计 领域驱动设计......

+
+
+ + +
+ +
+
+

如何做好程序设计功能

+
+ +
+
+

产品需求澄清、PN排期及任务分解 开发设计评审 功能设计流程图 与外部系统交互、本系统模块之间流程,比较好用的画圈软件draw .io或在线的process on 数据库设计 从DDD角度界限上下文、ER图、评审表结构设计是否合理,表的关联关系是否合理、是......

+
+
+ + +
+ + + +
+
+
+ + + + +
+ + + + + + +
+ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/index.xml b/blog-site/public/index.xml new file mode 100644 index 00000000..eaa6679a --- /dev/null +++ b/blog-site/public/index.xml @@ -0,0 +1,586 @@ + + + + 唯手熟尔 + http://localhost:1313/iblog/ + Recent content on 唯手熟尔 + Hugo -- gohugo.io + zh + Fri, 01 Dec 2023 00:00:00 +0000 + + + 2023工作总结 + http://localhost:1313/iblog/posts/worksummary/work-summary-2023/ + Fri, 01 Dec 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/worksummary/work-summary-2023/ + 在职期间,我主要负责耐材项目的开发与维护,共迭代171个版本。通过与团队成员的紧密合作,我们按时完成了项目中的需求。在这段时间里,我不断提升自己的专业技能和知识,增强了自己的专业能力。我始终认为团队合作是成功的关键。在工作中,我积极与同事沟 + + + 20230915简历 + http://localhost:1313/iblog/posts/resume/interview-resume-20230915/ + Fri, 15 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-resume-20230915/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和四年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对Jav + + + 定时任务可视化管理 + http://localhost:1313/iblog/posts/essays/scheduled-job/ + Sat, 09 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/scheduled-job/ + 代码实现 代码结构 pom &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-quartz&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;/dependency&gt; 库表结构 -- ---------------------------- -- 定时任务调度表 -- ---------------------------- drop table if exists sys_job; create table sys_job ( job_id bigint(20) not null auto_increment comment &#39;任务ID&#39;, job_name varchar(64) default &#39;&#39; comment &#39;任务名称&#39;, job_group varchar(64) default &#39;DEFAULT&#39; comment &#39;任务组名&#39;, invoke_target varchar(500) not null comment + + + SpringBoot整合nacos + http://localhost:1313/iblog/posts/spring/springboot-nacos/ + Mon, 04 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-nacos/ + nacos nacos下载 下载地址 一键傻瓜试安装即可,官网写的很清楚这里不在赘述 http://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html nacos启动 将模式改为单机模式 启动成功 nacos相关配置 demo-dev.yaml server: port: 8001 config: info: &#34;config info for dev from nacos config center&#34; demo-test.yaml server: port: 3333 config: info: &#34;config info for test from nacos config center&#34; user.yaml user: name: zs1112222 age: 10 address: 测试地址 代码 整合nacos配置中心,注册 + + + 整合文件上传功能 + http://localhost:1313/iblog/posts/essays/uploadfile-code/ + Fri, 11 Aug 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/uploadfile-code/ + 结构 pom.xml fastdfs-client-java-1.27.jar:点击下载 &lt;dependencies&gt; &lt;!-- fastdfs --&gt; &lt;dependency&gt; &lt;groupId&gt;org.csource&lt;/groupId&gt; &lt;artifactId&gt;fastdfs-client-java&lt;/artifactId&gt; &lt;version&gt;1.27&lt;/version&gt; &lt;systemPath&gt;${project.basedir}/lib/fastdfs-client-java-1.27.jar&lt;/systemPath&gt; &lt;scope&gt;system&lt;/scope&gt; &lt;/dependency&gt; &lt;!--aliyun oss 依赖--&gt; &lt;dependency&gt; &lt;groupId&gt;com.aliyun.oss&lt;/groupId&gt; &lt;artifactId&gt;aliyun-sdk-oss&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;5.8.11&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;commons-io&lt;/groupId&gt; &lt;artifactId&gt;commons-io&lt;/artifactId&gt; &lt;version&gt;2.11.0&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; application.yml server: port: 80 公共部分 FileManagement public interface FileManagement { /** * 设置下一个bean的对象 * * @param nextFileManagement 下一个 + + + 整合支付功能 + http://localhost:1313/iblog/posts/essays/pay-code/ + Thu, 10 Aug 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/pay-code/ + 结构 pom.xml &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;5.8.11&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.alipay.sdk&lt;/groupId&gt; &lt;artifactId&gt;alipay-sdk-java&lt;/artifactId&gt; &lt;version&gt;4.9.9&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.github.binarywang&lt;/groupId&gt; &lt;artifactId&gt;weixin-java-pay&lt;/artifactId&gt; &lt;version&gt;4.5.0&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; application.yml server: port: 8080 pay: wechat: #微信公众号或者小程序等的appid appId: &#34;&#34; #微信支付商户号 mchId: &#34;&#34; #微信支付商户密钥 mchKey: &#34;&#34; #服务商模式下的子商户公众账号ID subAppId: #服务商模式下的子商户号 subMchId: # p12证书的位 + + + Validator参数校验 + http://localhost:1313/iblog/posts/essays/springboot-validator/ + Sat, 01 Jul 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/springboot-validator/ + 常见参数校验 在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,最简单就是用if条件语句来判断,但是随着参数越来越多,业务越来越复杂,判断参数代码语句显得尤为冗长. 或者有些程序会将if封装起来,例如spring中 + + + 管道流设计模式结合业务 + http://localhost:1313/iblog/posts/essays/pipeline-business/ + Thu, 15 Jun 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/pipeline-business/ + 流程图 代码实现 pom &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.plugin&lt;/groupId&gt; &lt;artifactId&gt;spring-plugin-core&lt;/artifactId&gt; &lt;version&gt;${spring.plugin.core.version}&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;${hutool.version}&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; context EventContext public interface EventContext { /** * 是否继续调用链 */ boolean continueChain(); /** * 获取当前过滤器选择器 */ FilterSelector getFilterSelector(); } BizType public interface BizType { /** * 获取业务类型码值 */ Integer getCode(); /** * 业务类型名称 * */ String getName(); } AbstractEventContext public abstract class AbstractEventContext implements EventContext{ private final BizType businessType; private final FilterSelector filterSelector; protected AbstractEventContext(BizType businessType, FilterSelector filterSelector) { + + + 重构一个程序 + http://localhost:1313/iblog/posts/essays/java-project-reconstitution/ + Thu, 20 Apr 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-project-reconstitution/ + 什么是重构 摘自《重构:改善既有代码的设计》 重构(名词形式): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。 重构(动词形式): 使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构。 重 + + + SpringMVC与SpringWebFlux + http://localhost:1313/iblog/posts/spring/java-spring-mvc-webflux/ + Fri, 14 Apr 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/java-spring-mvc-webflux/ + Spring MVC Spring Web MVC是建立在Servlet API上的原始Web框架,从一开始就包含在Spring框架中。正式名称 &ldquo;Spring Web MVC &ldquo;来自其源模块的名称(spring-webmvc),但它更常被称为 &ldquo;Spring MVC&rdquo;。 SpringMVC是基于S + + + MySQL详解 + http://localhost:1313/iblog/posts/essays/sql-select-fast/ + Mon, 13 Mar 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/sql-select-fast/ + 逻辑架构 主要分为:连接层,服务层,引擎层,存储层。 客户端执行一条select命令的流程如下: 连接层:最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcplip的通信。主要完成一些类似于连接处理、 + + + 如何减少及解决bug + http://localhost:1313/iblog/posts/essays/java-bugs/ + Fri, 10 Mar 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-bugs/ + bug的起源: 1945年,一只小飞蛾钻进了计算机电路里,导致系统无法工作,一位名叫格蕾丝·赫柏的人把飞蛾拍死在工作日志上,写道:就是这个 bug(虫子),害我们今天的工作无法完成——于是,bug一词成了电脑系统程序的专业术语,形容那些系统中的 + + + Elasticsearch详解 + http://localhost:1313/iblog/posts/essays/elasticsearch/ + Tue, 14 Feb 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/elasticsearch/ + 概览 Elasticsearch,简称为 ES, ES是一个开源的高扩展的分布式全文搜索引擎, 是整个 ElasticStack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。 Elastic Stack, 包括 Elasticsearch、 Ki + + + 编程常用词汇汇总 + http://localhost:1313/iblog/posts/essays/java-dict/ + Mon, 13 Feb 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-dict/ + QPS 即 Queries Per Second的缩写,每秒能处理查询数目。是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。 TPS 即 Transactions Per Second的缩写,每秒处理的事务数目。一个事务是指一个客户机向服务器发送请求然后服务 + + + 接口优化 + http://localhost:1313/iblog/posts/essays/java-improve/ + Tue, 20 Dec 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-improve/ + 接口优化 线上接口很慢,线上生产问题,我们绝对不能马虎放过抱着侥幸心理,必须要找到根本原因及时处理,防止下次留下更大的坑.大致思路要定位接口问题,然后具体问题具体分析,讨论不同解决方案. 定位问题 要快速定位接口哪一个环节比较慢,性能瓶颈在哪里, + + + 孙子兵法 + http://localhost:1313/iblog/posts/books/books-sunzibingfa/ + Mon, 19 Dec 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/books/books-sunzibingfa/ + 始计 兵者,国之大事,死生之地,存亡之道,不可不察也。 故经之以五事,校之以计,而索其情:一曰道,二曰天,三曰地,四曰将,五曰法。道者,令民于上同意,可与之死,可与之生,而不危也;天者,阴阳、寒暑、时制也;地者,远近、险易、广狭、死生也;将者, + + + IDEA常用配置及使用技巧 + http://localhost:1313/iblog/posts/essays/dev-idea/ + Fri, 16 Dec 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/dev-idea/ + 下载 工欲善其事必先利其器,一个好的开发工具,能极大提高开发效率. 新UI很漂亮。IDEA 2022.2.3 官方下载地址: https://www.jetbrains.com/zh-cn/idea/download/other.html 激活工具 百度云下载. 链接:https://pan.baidu.com/s/19sCUTCBXvwXgEQc8vX-SYQ?pwd=gwu + + + 整洁的代码 + http://localhost:1313/iblog/posts/essays/clean-code/ + Thu, 01 Sep 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/clean-code/ + 为什么要写整洁的代码 为什么要写整洁的代码,回答这个问题之前,也许应该想想写糟糕的代码的原因 是想快点完成吗?还是要赶时间吗?有可能.或许你觉得自己要干好所需要的时间不够;假使花时间清理代码,老板就会大发雷霆.或许你只是不耐烦再搞这套程序,期望 + + + 关于编程的书籍 + http://localhost:1313/iblog/posts/books/java-books/ + Mon, 08 Aug 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/books/java-books/ + HeadFirst设计模式 Java数据结构和算法 Java核心技术卷I基础知识 Java编程思想 代码整洁之道 大型网站技术架构 大话数据结构 深入分析JavaWeb技术内幕 疯狂Java讲义 重构:改善既有代码的设计 领域驱动设计 + + + 如何做好程序设计功能 + http://localhost:1313/iblog/posts/essays/java-design/ + Tue, 02 Aug 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-design/ + 产品需求澄清、PN排期及任务分解 开发设计评审 功能设计流程图 与外部系统交互、本系统模块之间流程,比较好用的画圈软件draw .io或在线的process on 数据库设计 从DDD角度界限上下文、ER图、评审表结构设计是否合理,表的关联关系是否合理、是 + + + 20220422简历 + http://localhost:1313/iblog/posts/resume/interview-resume-20220422/ + Fri, 22 Apr 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-resume-20220422/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和三年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对SQL + + + Java小程序集合 + http://localhost:1313/iblog/posts/toy/java-multi-gadget/ + Sat, 09 Apr 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/java-multi-gadget/ + 写在前面 本文中所涉及的程序均为Java开发,如果您想要直接使用这些工具需要提前配置Java环境。所涉及到的程序均提供完整代码,如果您有兴趣可以尝试运行。 使用java -jar命令启动 某些程序功能并不是很完善,但是也可以凑合着用,写这些程序的主 + + + 数据结构与算法 + http://localhost:1313/iblog/posts/essays/data-structures-algorithms/ + Fri, 10 Dec 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/data-structures-algorithms/ + 数据结构 数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好数据结构可以编写出更有效率的代码。数据结构是算法的基础,想要学好算法,就必须把数据结构学到位。 数据结构包括:线性结构、非线性结构。 线性结构作为最常用的数据结构, + + + 规范编写Java代码总结 + http://localhost:1313/iblog/posts/essays/java-code-rule/ + Thu, 25 Nov 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-code-rule/ + 编码规范 我们为什么要遵守规范来编码? 是因为通常在编码过程中我们不只自己进行开发,通常需要一个团队来进行,开发好之后还需要维护,所以编码规范就显的尤为重要。 代码维护时间比较长,那么保证代码可读性就显得很重要。作为一个程序员,咱们得有点追求和信 + + + 网络编程 + http://localhost:1313/iblog/posts/essays/net-program-java/ + Fri, 19 Nov 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/net-program-java/ + 网络协议 以下内容摘自百度百科: https://baike.baidu.com/item/网络协议/328636 https://baike.baidu.com/item/网络七层协议/6056879 网络协议指的是计算机网络中互相通信的对等实体 + + + MQ详解 + http://localhost:1313/iblog/posts/essays/java-mq/ + Tue, 19 Oct 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-mq/ + 概念 MQ 即 messagequeue 消息队列,是分布式系统的重要组件,主要解决异步消息,应用解耦,消峰等问题。从而实现高可用,高性能,可伸缩和最终一致性的架构。使用较多的MQ有:activeMQ,rabbitMQ,kafka,metaMQ。 MQ优点 异步消息处理:可以 + + + Java集合 + http://localhost:1313/iblog/posts/java/rookie-java-container/ + Mon, 04 Oct 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-java-container/ + 概述 Java中的集合主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 如果你看过ArrayList类源码,就知道ArrayList底层是通过数组来存储元素的,所以如果严格来说,数组也算集合的一种 + + + Java反射 + http://localhost:1313/iblog/posts/java/rookie-reflect/ + Sat, 02 Oct 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-reflect/ + 概述 什么是反射 在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。 反射是Java语言的一个特性,它允许程序 + + + 常见故障排查及程序配置 + http://localhost:1313/iblog/posts/essays/eye-beam/ + Wed, 08 Sep 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/eye-beam/ + 故障排查基础 收录Linux常用命令,以下命令来自https://www.bilibili.com/video/BV14A411378a 关机/重启/注销 常用命令 作用 shutdown -h now 即刻关机 shutdown -h 10 10分钟后关机 shutdown -h 11:00 11:00关机 shutdown -h +10 预定时间关机(10 + + + 分布式事务详解 + http://localhost:1313/iblog/posts/essays/java-transaction/ + Mon, 02 Aug 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-transaction/ + 基础概念 什么是事务 什么是事务?举个例子:你去超市买东西,&ldquo;一手交钱,一手交货&quot;就是一个事务的例子,交钱和交货必须同时成功,事务才算成功,其中有一个环节失败,事务将会撤销所有已成功的活动。 所以事务可以看作是一次重大的活动 + + + Object类方法 + http://localhost:1313/iblog/posts/java/rookie-objectclass-methods/ + Sat, 10 Jul 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-objectclass-methods/ + 概览 Object 类位于 java.lang 包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承Object,成为Object的子类。 Object类可以显示继承,也可以隐式继承,效果都是一样的。 class A extends Object{ // to do } class A { // to do } Java Objec + + + 微服务治理 + http://localhost:1313/iblog/posts/essays/java-small-service/ + Mon, 21 Jun 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-small-service/ + 什么是微服务架构 In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API。 These services are built around business capabilities and independently deployable by fully automated deployment machinery。 There is a bare minimum of centralized management of these services, which may be written in different programming + + + Redis详解 + http://localhost:1313/iblog/posts/essays/java-redis/ + Thu, 17 Jun 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-redis/ + Redis概述 参考文章: https://www.runoob.com/redis/redis-intro.html https://www.redis.com.cn/redis-interview-questions.html 什么是Redis Redis(Remote Dictionary Server) Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API 的非关系型数据库。 简而言之,Redis是一个可基于内存亦可持久 + + + Spring详解 + http://localhost:1313/iblog/posts/spring/java-spring/ + Thu, 13 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/java-spring/ + 概览 Spring是一个轻量级的Java开源框架,为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IOC)和面向切面(AOP)。 简单来说,Spring是一个分层的JavaSE/EE 一站式轻量级开源框架。在每一层都提供支持。 + + + 面试Java可能会被问到的问题 + http://localhost:1313/iblog/posts/resume/interview-junior-javaer/ + Tue, 11 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-junior-javaer/ + 面试必问 自我介绍一下 你有什么职业规划 你为什么要离职 说一下你的优缺点 你的期望薪资是多少 你为什么要选择我们公司 你能否接受加班 你有对象了吗 你还有什么问题要问的吗 基础 说一下UDP、TCP及http与https 如何保证线程安全 线程池工作原理 如何避免死 + + + JVM-垃圾回收器 + http://localhost:1313/iblog/posts/jvm/java-garbage-collector/ + Thu, 06 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/java-garbage-collector/ + 垃圾回收器分类 垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。 由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。 Java不同版本新特性学习思路: 语法层面:Lambda表达式、s + + + Java多线程 + http://localhost:1313/iblog/posts/java/rookie-multi-thread/ + Wed, 05 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-multi-thread/ + 相关概念 线程与进程 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。例如,一个正在运行的程序的实例就是一个进程。 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 一条线程指的是进程中一个单一 + + + HashMap详解 + http://localhost:1313/iblog/posts/essays/java-hashmap/ + Mon, 03 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-hashmap/ + 相关概念 capacity: 容量,默认16; loadFactor: 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容; threshold: 阈值;阈值 + + + JVM-相关概念 + http://localhost:1313/iblog/posts/jvm/jvm-about/ + Tue, 27 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-about/ + 内存溢出 内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。 官方文档中对内存溢出的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。 由于GC一 + + + 面试中常见的问题 + http://localhost:1313/iblog/posts/resume/interview-questions-and-answers/ + Fri, 23 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-questions-and-answers/ + 面试常见问题 自我介绍 个人经历可以进行适当包装,但是不能造假,一方面如果一旦被人拆穿,后果就不用我说了吧,另一方面如果你说谎,说了一些你自己不感兴趣的项目,在入职之后可能会被分配到该项目上; 在介绍的时候要说明你对面试的公司有什么用,根据不同类 + + + JVM-垃圾回收 + http://localhost:1313/iblog/posts/jvm/java-garbage-collection/ + Wed, 21 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/java-garbage-collection/ + 垃圾回收 垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。 垃圾收集机制是Java的招牌能力,极大地提高了开发效率。 如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展 + + + JVM-执行引擎 + http://localhost:1313/iblog/posts/jvm/jvm-execute-engine/ + Thu, 15 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-execute-engine/ + 概述 执行引擎是Java虚拟机核心的组成部分之一,属于JVM的下层,里面包括 解释器、及时编译器、垃圾回收器。 “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力, 其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统 + + + JVM-直接内存 + http://localhost:1313/iblog/posts/jvm/jvm-direct-memory/ + Wed, 14 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-direct-memory/ + 直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。 直接内存是在Java堆外的、直接向系统申请的内存区间。 操作直接内存演示代码: public class MainTest { public static void main(String[] args) { ByteBuffer allocate = ByteBuffer.allocate(1024 * 1024 * 1024); System.out.println(&# + + + JVM-Java对象 + http://localhost:1313/iblog/posts/jvm/java-object/ + Mon, 12 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/java-object/ + 对象实例化 对象的创建方式 使用new关键字创建:最常见的方式、单例类中调用getInstance的静态类方法,XXXFactory的静态方法; 使用反射方式创建: 使用Class的newInstance方法:在JDK9里面被标记为过时的方法,因为 + + + Java语法糖 + http://localhost:1313/iblog/posts/essays/java-syntax-sugar/ + Sat, 10 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-syntax-sugar/ + 原文地址:https://www.jianshu.com/p/0f967298a5d7 语法糖 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法, 这种语法对语言的功能并没有 + + + JavaIO + http://localhost:1313/iblog/posts/java/rookie-io/ + Fri, 09 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-io/ + 概念 Java IO通过数据流、序列化和文件系统提供系统输入和输出。 IO,即 in 和 out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。 传统的 IO 是通过流技术来处理的。 流(Stream),是一个抽象的概念, + + + JVM-方法区 + http://localhost:1313/iblog/posts/jvm/jvm-method-area/ + Thu, 08 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-method-area/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-堆 + http://localhost:1313/iblog/posts/jvm/jvm-heap/ + Sat, 03 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-heap/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-本地方法接口 + http://localhost:1313/iblog/posts/jvm/jvm-native-interface/ + Fri, 02 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-native-interface/ + 概念 简单地讲,一个Native Methodt是一个Java调用非Java代码的接囗。 一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。 这个特征并非Java所特有,很多其它的编程语言都有这一机制,比 + + + JVM-本地方法栈 + http://localhost:1313/iblog/posts/jvm/jvm-native-stack/ + Fri, 02 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-native-stack/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-虚拟机栈 + http://localhost:1313/iblog/posts/jvm/jvm-stack/ + Sun, 28 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-stack/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-程序计数寄存器 + http://localhost:1313/iblog/posts/jvm/jvm-pc-register/ + Sat, 27 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-pc-register/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-JVM介绍 + http://localhost:1313/iblog/posts/jvm/jvm-start/ + Fri, 05 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-start/ + 为什么要学习JVM 大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。 一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要, + + + Nginx介绍 + http://localhost:1313/iblog/posts/essays/nginx-start/ + Thu, 04 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/nginx-start/ + Nginx介绍 Nginx (&ldquo;engine x&rdquo;)是一个高性能的HTTP和反向代理服务器,特点是占有内存少,并发能力强,事实上Nginx的并发能力确实在同类型的网页服务器中表现较好. Nginx专为性能优化而开发,性能是其最重要的考量,实现上非常注重效率, + + + 道德经 + http://localhost:1313/iblog/posts/books/books-daodejing/ + Wed, 03 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/books/books-daodejing/ + 第一章 道可道,非常道。名可名,非常名。 无名天地之始﹔有名万物之母。 故常无,欲以观其妙﹔常有,欲以观其徼。 此两者,同出而异名,同谓之玄。 玄之又玄,众妙之门。 第二章 天下皆知美之为美,斯恶已。 皆知善之为善,斯不善已。 有无相生,难易相成,长短相形, + + + 关于 + http://localhost:1313/iblog/about/ + Sat, 20 Feb 2021 00:00:00 +0000 + http://localhost:1313/iblog/about/ + 搭建博客 本博客使用 hugo + GitHubPage 进行搭建,使用的主题为 zozo Designed by VarKai。 如果你想要使用 hugo 搭建博客,可以参考以下相关资料: hugo中文帮助文档: https://hugo.aiaide.com hugo中文文档: https://www.gohugo.org hugo给文章添加目录: https://www.ariesme.com/posts/2019/add_toc_for_hugo 使用hugo搭建个人博客站点: https://blog.coderzh.com/2015/08/29/hugo 不蒜子计数统计: https://busuanzi.ibruce.info 暗黑主 + + + 面向对象 + http://localhost:1313/iblog/posts/java/rookie-object-oriented/ + Mon, 15 Feb 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-object-oriented/ + 面向对象是一种编程思想,包括三大特性和六大原则,其中,三大特性指的是封装、继承和多态;六大原则指的是单一职责原则、开放封闭原则、迪米特原则、里氏替换原则、依赖倒置原则以及接口隔离原则,其中,单一职责原则是指一个类应该是一组相关性很高的函数和 + + + JVM-Java类加载机制 + http://localhost:1313/iblog/posts/jvm/jvm-classloader/ + Fri, 05 Feb 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-classloader/ + 类加载过程 在Java中,类加载器把一个类装入JVM中,要经过以下步骤: 加载、验证、准备、解析和初始化。其中验证,准备,解析统称为连接。 这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。 类加载器只负责class文 + + + Java运算 + http://localhost:1313/iblog/posts/java/rookie-operation/ + Sat, 30 Jan 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-operation/ + 运算符与表达式 运算符 运算符指明对操作数的运算方式。组成表达式的Java操作符有很多种。运算符按照其要求的操作数数目来分,可以有单目运算符、双目运算符和三目运算符,它们分别对应于1个、2个、3个操作数。 种类 运算符按其功能来分:有算术运算符、赋 + + + Java数据类型 + http://localhost:1313/iblog/posts/java/rookie-datatype/ + Wed, 20 Jan 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-datatype/ + 基本类型 Java语言提供了八种基本类型。六种数值类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. 俗称4类8种 这里只介绍称4类8种.实际上,JAVA中还存在另外一种基本类型 void,它也有对应的包装类java.lang.Void + + + Java异常 + http://localhost:1313/iblog/posts/java/rookie-exception/ + Wed, 13 Jan 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-exception/ + 异常类型 Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:Error 和 Exception。 其中 Error 用来表示Java程序无法处理的错误;这类错误一般与硬件有关,与程序本身无关,通常由系统进行处理,程序本身无法捕获和处理。是不可控制的。 Exception 分为两种 + + + 20201124简历 + http://localhost:1313/iblog/posts/resume/interview-resume-20201124/ + Tue, 24 Nov 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-resume-20201124/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 专业技能 熟练使用 SSM,SpringBoot等框架技术; 熟练使用HTML,CSS等相关技术; 有Redis,VUE相关使用经验; 有对接第三方系统,调用外系统相关经验; 熟悉 MySQL,ORACLE.基本操作,熟练使 + + + SpringBoot整合docker + http://localhost:1313/iblog/posts/spring/springboot-docker/ + Sun, 30 Aug 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-docker/ + MacOS上安装docker 下载 国内下载网站: http://get.daocloud.io 不推荐下载docker版本太旧了 官网下载: https://docs.docker.com/get-started/#download-and-install-docker 或用homebrew进行下载安装 brew install --cask --appdir=/Applications docker 配置镜像 由于网速原因,可以配置一下国内的镜像加速器 中科大镜像: https://docker.mirrors.ustc.edu.cn 网易: https://hub-mirror.c.163.com 阿里云: https://&lt;你 + + + SpringBoot整合kafka + http://localhost:1313/iblog/posts/spring/springboot-kafka/ + Thu, 20 Aug 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-kafka/ + kafka介绍 kafka官网: http://kafka.apache.org kafka中文官网: https://kafka.apachecn.org Kafka是一种分布式的,基于发布/订阅的消息系统。主要特点如下: 以时间复杂度为O(1)的方式提供消息持久化能力,并保证即使对TB级以上数据也能保证常数时间的访问性能 高吞吐率。即使在非常 + + + 线程状态及创建方式 + http://localhost:1313/iblog/posts/essays/thread-state-and-created/ + Mon, 20 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/thread-state-and-created/ + 线程状态及转换 线程状态共包含6种,6中状态又可以互相的转换。 新建状态(New): 创建了线程后尚未启动; 可运行状态(Runnable): 可能正在运行,也可能正在等待 CPU 时间片。包含了运行中(Running)和 就绪(Ready)状态; 就绪(Rea + + + Docker介绍 + http://localhost:1313/iblog/posts/essays/docker-start/ + Tue, 07 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/docker-start/ + docker是什么 Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前流行的 Linux 容器解决方案。 Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样 + + + Java中常用到的锁 + http://localhost:1313/iblog/posts/essays/java-lock/ + Tue, 07 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-lock/ + 公平锁 指多个线程按照申请锁的顺序来获取锁类似排队打饭 先来后到 优点: 所有的线程都能得到资源,不会饿死在队列中。 缺点: 吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。 非公平锁 指在多线程获取锁的顺序并 + + + Java中集合的线程不安全问题 + http://localhost:1313/iblog/posts/essays/java-thread-collection/ + Sun, 05 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-thread-collection/ + ArrayList ArrayList线程不安全示例: public static void main(String[] args) { ArrayList&lt;String&gt; arrayList = new ArrayList&lt;&gt;(); for(int i=0; i&lt; 3; i++) { new Thread(() -&gt; { arrayList.add(UUID.randomUUID().toString()); System.out.println(arrayList); },String.valueOf(i)).start(); } } // ConcurrentModificationException 同步修改异常 Exception in thread &#34;8&#34; java.util.ConcurrentModificationException [null, 2041b613-8068-4ddd-9d01-305f5680d377] [null, 2041b613-8068-4ddd-9d01-305f5680d377, b3e0296d-e263-4632-a023-4267cdec5e25] [null, 2041b613-8068-4ddd-9d01-305f5680d377] 原因分析: 当某个线程正在执行 add()方法时,被某个线程打断,添加到一半被打断,没有被添加完 解决方案: 使用Vec + + + CAS原理 + http://localhost:1313/iblog/posts/essays/cas-principle/ + Sat, 04 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/cas-principle/ + CAS CAS全称为Compare and Swap被译为比较并交换。是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。 java.util.concurrent.atomic 并发包下的所有原子类都是基于 CAS 来实现的。 以 AtomicInteger 原子整型类为例。 public class MainTest { public static void main(String[] args) { new AtomicInteger().compareAndSet(1,2); } } 以上面的代码为例 + + + SpringBoot整合redis + http://localhost:1313/iblog/posts/spring/springboot-redis/ + Sun, 01 Mar 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-redis/ + Redis介绍 redis是开源的一个高性能的 key-value 数据库。 主要特点 Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用 Redis支持数据的备份,即master-slave模式的数据备份 Redis 可以存储键与5种不同 + + + SpringBoot整合elasticsearch + http://localhost:1313/iblog/posts/spring/springboot-elasticsearch/ + Sun, 09 Feb 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-elasticsearch/ + 安装elasticsearch 要注意导入依赖的版本和安装elasticsearch的版本与springboot的兼容问题 用 docker 安装 elasticsearch 本例用elasticsearch-6.5.3和springboot-2.1.0.RELEASE版本 下载镜像: docker + + + 2019工作总结 + http://localhost:1313/iblog/posts/worksummary/work-summary-2019/ + Sun, 01 Dec 2019 00:00:00 +0000 + http://localhost:1313/iblog/posts/worksummary/work-summary-2019/ + 本人在进入公司起,期间一直对自己要求严谨,遵守公司的相应制度. 在过去的一个月时间里,我参与了贵州银行的电子验印系统的开发,一直努力完成和完善分配给我的任务,在这一个月发现了自身还有很多的不足,所以抱着虚心学习的态度,学习公司的开发流程,了解 + + + Vue2.0学习笔记 + http://localhost:1313/iblog/posts/essays/vue2-note/ + Thu, 23 May 2019 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/vue2-note/ + 参考资料 vue官方文档: https://cn.vuejs.org/v2/guide vue参考视频资料: https://www.bilibili.com/video/av50680998 vue菜鸟教程文档: https://www.runoob.com/vue2/vue-tutorial.html vue-组件 参考资料: https://cn.vuejs.org/v2/guide/components.html#ad 组件是可复用的 Vue 实例,且带有一个名字. 组件的出现是为了拆分vue实例的代码量,能够让我们以不同的组件,来划分不同的功能模块,将来我们需要什么样的功 + + + Js雪花飘落 + http://localhost:1313/iblog/posts/toy/js-snow/ + Tue, 25 Dec 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-snow/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;snow&lt;/title&gt; &lt;/head&gt; &lt;style&gt; html { width: 100%; } body { margin: 0; padding: 0; overflow-y: hidden; width: 100%; } .header { width: 100%; height: 315px; background: url(&#34;images/header-bg.png&#34;) repeat; } .snow { position: relative; height: inherit; width: 960px; background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) no-repeat 0 0;; margin: 0 auto; animation: auto 10s linear infinite; } /* 下雪动画 插入两个背景图片*/ @keyframes auto { from { background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) repeat 0 0; } to { background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) repeat 0 1000px; } } tree, snow { position: absolute; } tree { width: 112px; height: 137px; background: url(&#34;images/tree.png&#34;); + + + Js下雨特效 + http://localhost:1313/iblog/posts/toy/js-rain/ + Mon, 10 Dec 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-rain/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt; rain &lt;/title&gt; &lt;style&gt; html { width: 100%; } body { width: 100%; margin: 0; padding: 0; background-color: #000; } .rain { display: block; } embed { display: block; } &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;!-- 2、使用hidden=&#34;true&#34;表示隐藏音乐播放按钮,相反使用hidden=&#34;false&#34;表示开启音乐播放按钮。 3、使用a + + + Js换肤特效 + http://localhost:1313/iblog/posts/toy/js-trans-skin/ + Wed, 14 Nov 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-trans-skin/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;换肤特效&lt;/title&gt; &lt;style type=&#34;text/css&#34;&gt; body { margin: 0; background-image: url(&#34;images/1.jpg&#34;); background-size: cover; } ul { margin: 0; padding: 0; list-style-type: none; } .bg-list { display: none; margin: 0; width: 100%; height: 200px; background: rgba(0, 0, 0, 0.5); } .img-wrap { height: 200px; display: flex; justify-content: space-around; align-items: center; } .tab-btn { background-image: url(&#34;images/upseek.png&#34;); height: 50px; width: 50px; position: fixed; top: 0; right: 0; } .tab-btn:hover { background-position-y: -63.6px; } &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;div class=&#34;bg-list&#34;&gt; &lt;ul class=&#34;img-wrap&#34;&gt; &lt;li class=&#34;img-item&#34; data-src=&#34;images/1.jpg&#34;&gt; &lt;img src=&#34;images/1-1.jpg&#34; width=&#34;160px&#34;/&gt; &lt;/li&gt; + + + Js折纸导航栏 + http://localhost:1313/iblog/posts/toy/js-paper-folding/ + Thu, 25 Oct 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-paper-folding/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;折纸导航栏&lt;/title&gt; &lt;/head&gt; &lt;style&gt; *{ margin: 0; padding: 0; } .content{ position: relative; width: 400px; height: 30px; margin: 50px auto; /*-webkit-perspective: 1000px; -moz-perspective: 1000px; -ms-perspective: 1000px;*/ perspective: 1000px;/*景深相当于眼睛距离元素的位置距离*/ } .content .open{ transform: rotateX(0); animation: open 1s linear; } @keyframes open { 0%{ transform: rotateX(-90deg); } 20%{ transform:rotateX(30deg); } 40%{ transform:rotateX(-60deg); } 60%{ transform:rotateX(60deg); } 80%{ transform:rotateX(-30deg); + + + Js表白神器 + http://localhost:1313/iblog/posts/toy/js-love-heart/ + Sun, 14 Oct 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-love-heart/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;love&lt;/title&gt; &lt;style&gt; *{ margin: 0; padding: 0; } body{ background-color: #000; background-size: cover; overflow-y: hidden; } .love{ width: 400px; height: 400px; /*background-color: #7c7c7c;*/ margin: 130px auto; animation: move 1s infinite alternate; } @keyframes move { 100%{ transform: scale(1.5); } } .left{ float: left; width: 150px; height: 250px; background-color: #FF0000; border-radius: 75px 75px 0 5px; -webkit-transform: rotate(-45deg); -moz-transform: rotate(-45deg); -ms-transform: rotate(-45deg); -o-transform: rotate(-45deg); transform: rotate(-45deg); margin-left: 85px; box-shadow: 0 0 20px #FF0000; animation: shadow 1s infinite alternate; } @keyframes shadow { 100%{ box-shadow: 0 0 100px #FF0000; } } .right{ float: left; width: 150px; height: 250px; background-color: #FF0000; border-radius: 75px 75px 5px 0; -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -ms-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg); + + + Js懒加载 + http://localhost:1313/iblog/posts/toy/js-loadding-lazy/ + Fri, 21 Sep 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-loadding-lazy/ + index.html &lt;!doctype html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;meta name=&#34;Generator&#34; content=&#34;EditPlus®&#34;&gt; &lt;meta name=&#34;Author&#34; content=&#34;&#34;&gt; &lt;meta name=&#34;Keywords&#34; content=&#34;&#34;&gt; &lt;meta name=&#34;Description&#34; content=&#34;&#34;&gt; &lt;title&gt;懒加载技术&lt;/title&gt; &lt;style&gt; *{ margin: 0; padding:0; } body{ background: rgb(0,0,0); } .box{ overflow: hidden; width: 948px; background-color: #7c7c7c; margin: 50px auto; -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; } .box img{ float: left; display: block; width: 300px; height: 150px; margin: + + + Js五子棋 + http://localhost:1313/iblog/posts/toy/js-gomoku/ + Mon, 10 Sep 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-gomoku/ + index.html &lt;!DOCTYPE html PUBLIC &#34;-//W3C//DTD XHTML 1.0 Transitional//EN&#34; &#34;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&#34;&gt; &lt;html xmlns=&#34;http://www.w3.org/1999/xhtml&#34;&gt; &lt;head&gt; &lt;meta http-equiv=&#34;Content-Type&#34; content=&#34;text/html; charset=UTF-8&#34; /&gt; &lt;title&gt;五子棋&lt;/title&gt; &lt;meta name=&#34;viewport&#34; content=&#34;device-width; initial-scale=1.0;&#34; /&gt; &lt;style&gt; #c1 { display: block; margin: 60px auto; box-shadow: 1px 1px 5px #000000; } &lt;/style&gt; &lt;script src=&#34;js/index.js&#34;&gt;&lt;/script&gt; &lt;/head&gt; &lt;body&gt; &lt;canvas id=&#34;c1&#34; width=&#34;450px&#34; height=&#34;450px&#34;&gt;&lt;/canvas&gt; &lt;/body&gt; &lt;/html&gt; index.js window.onload = function(){ var oC = document.getElementById(&#39;c1&#39;); var oGc = oC.getContext(&#39;2d&#39;); var over = false; oGc.strokeStyle = &#34;#bfbfbf&#34;; //绘制棋盘 for(var i=0;i&lt;15;i++){ oGc.moveTo(15+i*30,15); oGc.lineTo(15+i*30,435); oGc.stroke(); oGc.moveTo(15,15+i*30); oGc.lineTo(435,15+i*30); oGc.stroke(); } /* AI难点解析 赢法 + + + Js滑块拖拽 + http://localhost:1313/iblog/posts/toy/js-box-drag/ + Sat, 08 Sep 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-box-drag/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;滑块拖拽&lt;/title&gt; &lt;/head&gt; &lt;style&gt; body { margin: 0; padding: 0; user-select: none; } .content { position: relative; width: 300px; height: 40px; margin: 50px auto; background-color: #E8E8EB; text-align: center; line-height: 40px; } .rect { position: absolute; width: 100%; height: 100%; } .rect .bg { position: absolute; left: 0; top: 0; z-index: 1; width: 0; height: 100%; background: rgba(122,194,60,.4); } .rect .move { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; width: 45px; height: 40px; position: absolute; top: 0; left: 0; background-color: #fff; border: 1px solid #cccccc; + + + Js生日礼物 + http://localhost:1313/iblog/posts/toy/js-birthday-gift/ + Fri, 24 Aug 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-birthday-gift/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;card&lt;/title&gt; &lt;style&gt; body,html{ width: 100%; height: 100%; } body{ display: flex;/*弹性盒模型*/ justify-content: center;/*水平对齐 盒子位于中心*/ align-items: center;/*竖直对齐 居中对齐*/ background-color: yellow; perspective: 1000px;/*景深:眼到屏幕的距离*/ } body,h1,p{ margin: 0; } .card{ width: 520px; height: 350px; border-radius: 15px; background: linear-gradient(#020333 70%,#fff 75%);/* + + + diff --git a/blog-site/public/js/autodarkmode.js b/blog-site/public/js/autodarkmode.js new file mode 100644 index 00000000..60cb706f --- /dev/null +++ b/blog-site/public/js/autodarkmode.js @@ -0,0 +1,24 @@ + +// 夜间模式使用 +(function (){ + function darkModelOptions(){ + return { + right: '32px', // default: '32px' + bottom: 'unset', // default: '32px' + // left: '32px', // default: 'unset' + time: '0.3s', // default: '0.3s' + mixColor: '#f7f7f7', // default: '#fff' + backgroundColor: '#f7f7f7', // default: '#fff' + buttonColorDark: '#212121', // default: '#100f2c' + buttonColorLight: '#f7f7f7', // default: '#fff' + saveInCookies: false, // default: true, + autoMatchOsTheme: true // default: true + } + } + + // 自动调用夜间模式 + const hours = new Date().getHours(); + if (hours >= 21 && hours <= 5){ + new Darkmode(darkModelOptions()).toggle(); + } +})() diff --git a/blog-site/public/js/busuanzi_2.3_busuanzi.pure.mini.js b/blog-site/public/js/busuanzi_2.3_busuanzi.pure.mini.js new file mode 100644 index 00000000..ad5e0ac4 --- /dev/null +++ b/blog-site/public/js/busuanzi_2.3_busuanzi.pure.mini.js @@ -0,0 +1 @@ +var bszCaller,bszTag;!function(){var c,d,e,a=!1,b=[];ready=function(c){return a||"interactive"===document.readyState||"complete"===document.readyState?c.call(document):b.push(function(){return c.call(this)}),this},d=function(){for(var a=0,c=b.length;c>a;a++)b[a].apply(document);b=[]},e=function(){a||(a=!0,d.call(window),document.removeEventListener?document.removeEventListener("DOMContentLoaded",e,!1):document.attachEvent&&(document.detachEvent("onreadystatechange",e),window==window.top&&(clearInterval(c),c=null)))},document.addEventListener?document.addEventListener("DOMContentLoaded",e,!1):document.attachEvent&&(document.attachEvent("onreadystatechange",function(){/loaded|complete/.test(document.readyState)&&e()}),window==window.top&&(c=setInterval(function(){try{a||document.documentElement.doScroll("left")}catch(b){return}e()},5)))}(),bszCaller={fetch:function(a,b){var c="BusuanziCallback_"+Math.floor(1099511627776*Math.random());window[c]=this.evalCall(b),a=a.replace("=BusuanziCallback","="+c),scriptTag=document.createElement("SCRIPT"),scriptTag.type="text/javascript",scriptTag.defer=!0,scriptTag.src=a,scriptTag.referrerPolicy="no-referrer-when-downgrade",document.getElementsByTagName("HEAD")[0].appendChild(scriptTag)},evalCall:function(a){return function(b){ready(function(){try{a(b),scriptTag.parentElement.removeChild(scriptTag)}catch(c){bszTag.hides()}})}}},bszCaller.fetch("//busuanzi.ibruce.info/busuanzi?jsonpCallback=BusuanziCallback",function(a){bszTag.texts(a),bszTag.shows()}),bszTag={bszs:["site_pv","page_pv","site_uv"],texts:function(a){this.bszs.map(function(b){var c=document.getElementById("busuanzi_value_"+b);c&&(c.innerHTML=a[b])})},hides:function(){this.bszs.map(function(a){var b=document.getElementById("busuanzi_container_"+a);b&&(b.style.display="none")})},shows:function(){this.bszs.map(function(a){var b=document.getElementById("busuanzi_container_"+a);b&&(b.style.display="inline")})}}; diff --git a/blog-site/public/js/darkmode.js b/blog-site/public/js/darkmode.js new file mode 100644 index 00000000..37ac2e55 --- /dev/null +++ b/blog-site/public/js/darkmode.js @@ -0,0 +1,357 @@ +(function webpackUniversalModuleDefinition(root, factory) { + if (typeof exports === 'object' && typeof module === 'object') + module.exports = factory(); + else if (typeof define === 'function' && define.amd) + define("darkmode-js", [], factory); + else if (typeof exports === 'object') + exports["darkmode-js"] = factory(); + else + root["darkmode-js"] = factory(); +})(typeof self !== 'undefined' ? self : this, function () { + return /******/ (function (modules) { // webpackBootstrap + /******/ // The module cache + /******/ + var installedModules = {}; + /******/ + /******/ // The require function + /******/ + function __webpack_require__(moduleId) { + /******/ + /******/ // Check if module is in cache + /******/ + if (installedModules[moduleId]) { + /******/ + return installedModules[moduleId].exports; + /******/ + } + /******/ // Create a new module (and put it into the cache) + /******/ + var module = installedModules[moduleId] = { + /******/ i: moduleId, + /******/ l: false, + /******/ exports: {} + /******/ + }; + /******/ + /******/ // Execute the module function + /******/ + modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); + /******/ + /******/ // Flag the module as loaded + /******/ + module.l = true; + /******/ + /******/ // Return the exports of the module + /******/ + return module.exports; + /******/ + } + + /******/ + /******/ + /******/ // expose the modules object (__webpack_modules__) + /******/ + __webpack_require__.m = modules; + /******/ + /******/ // expose the module cache + /******/ + __webpack_require__.c = installedModules; + /******/ + /******/ // define getter function for harmony exports + /******/ + __webpack_require__.d = function (exports, name, getter) { + /******/ + if (!__webpack_require__.o(exports, name)) { + /******/ + Object.defineProperty(exports, name, {enumerable: true, get: getter}); + /******/ + } + /******/ + }; + /******/ + /******/ // define __esModule on exports + /******/ + __webpack_require__.r = function (exports) { + /******/ + if (typeof Symbol !== 'undefined' && Symbol.toStringTag) { + /******/ + Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'}); + /******/ + } + /******/ + Object.defineProperty(exports, '__esModule', {value: true}); + /******/ + }; + /******/ + /******/ // create a fake namespace object + /******/ // mode & 1: value is a module id, require it + /******/ // mode & 2: merge all properties of value into the ns + /******/ // mode & 4: return value when already ns object + /******/ // mode & 8|1: behave like require + /******/ + __webpack_require__.t = function (value, mode) { + /******/ + if (mode & 1) value = __webpack_require__(value); + /******/ + if (mode & 8) return value; + /******/ + if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; + /******/ + var ns = Object.create(null); + /******/ + __webpack_require__.r(ns); + /******/ + Object.defineProperty(ns, 'default', {enumerable: true, value: value}); + /******/ + if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns, key, function (key) { + return value[key]; + }.bind(null, key)); + /******/ + return ns; + /******/ + }; + /******/ + /******/ // getDefaultExport function for compatibility with non-harmony modules + /******/ + __webpack_require__.n = function (module) { + /******/ + var getter = module && module.__esModule ? + /******/ function getDefault() { + return module['default']; + } : + /******/ function getModuleExports() { + return module; + }; + /******/ + __webpack_require__.d(getter, 'a', getter); + /******/ + return getter; + /******/ + }; + /******/ + /******/ // Object.prototype.hasOwnProperty.call + /******/ + __webpack_require__.o = function (object, property) { + return Object.prototype.hasOwnProperty.call(object, property); + }; + /******/ + /******/ // __webpack_public_path__ + /******/ + __webpack_require__.p = ""; + /******/ + /******/ + /******/ // Load entry module and return exports + /******/ + return __webpack_require__(__webpack_require__.s = "./src/index.js"); + /******/ + }) + /************************************************************************/ + /******/ ({ + + /***/ "./src/darkmode.js": + /*!*************************!*\ + !*** ./src/darkmode.js ***! + \*************************/ + /*! no static exports found */ + /***/ (function (module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.default = void 0; + + function _classCallCheck(instance, Constructor) { + if (!(instance instanceof Constructor)) { + throw new TypeError("Cannot call a class as a function"); + } + } + + function _defineProperties(target, props) { + for (var i = 0; i < props.length; i++) { + var descriptor = props[i]; + descriptor.enumerable = descriptor.enumerable || false; + descriptor.configurable = true; + if ("value" in descriptor) descriptor.writable = true; + Object.defineProperty(target, descriptor.key, descriptor); + } + } + + function _createClass(Constructor, protoProps, staticProps) { + if (protoProps) _defineProperties(Constructor.prototype, protoProps); + if (staticProps) _defineProperties(Constructor, staticProps); + return Constructor; + } + + var Darkmode = + /*#__PURE__*/ + function () { + function Darkmode(options) { + _classCallCheck(this, Darkmode); + + var bottom = options && options.bottom || '32px'; + var right = options && options.right || '32px'; + var left = options && options.left || 'unset'; + var time = options && options.time || '0.3s'; + var mixColor = options && options.mixColor || '#fff'; + var backgroundColor = options && options.backgroundColor || '#fff'; + var buttonColorDark = options && options.buttonColorDark || '#100f2c'; + var buttonColorLight = options && options.buttonColorLight || '#fff'; + var label = options && options.label || ''; + var saveInCookies = options && options.saveInCookies; + /* eslint-disable */ + + var autoMatchOsTheme = options && options.autoMatchOsTheme === false ? false : true; + /* eslint-enable */ + + var css = "\n .darkmode-layer {\n position: fixed;\n pointer-events: none;\n background: ".concat(mixColor, ";\n transition: all ").concat(time, " ease;\n mix-blend-mode: difference;\n }\n\n" + + " .darkmode-layer--button {\n width: 2.9rem;\n height: 2.9rem;\n border-radius: 50%;\n right: ").concat(right, ";\n bottom: ").concat(bottom, ";\n left: ").concat(left, ";\n }\n\n " + + " .darkmode-layer--simple {\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n transform: scale(1) !important;\n }\n\n" + + " .darkmode-layer--expanded {\n transform: scale(100);\n border-radius: 0;\n }\n\n" + + " .darkmode-layer--no-transition {\n transition: none;\n }\n\n " + + " .darkmode-toggle {\n background: ").concat(buttonColorDark, ";\n width: 3rem;\n height: 3rem;\n position: fixed;\n border-radius: 50%;\n right: ").concat(right, ";\n bottom: ").concat(bottom, ";\n left: ").concat(left, ";\n cursor: pointer;\n transition: all 0.5s ease;\n display: flex;\n justify-content: center;\n align-items: center;\n z-index: 1;\n }\n\n" + + " .darkmode-toggle--white {\n background: ").concat(buttonColorLight, ";\n }\n\n .darkmode-background {\n background: ").concat(backgroundColor, ";\n position: fixed;\n pointer-events: none;\n z-index: -10;\n width: 100%;\n height: 100%;\n top: 0;\n left: 0;\n }\n\n " + + " img, .darkmode-ignore {\n isolation: isolate;\n display: inline-block;\n }\n\n " + + " @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {\n .darkmode-toggle {display: none !important}\n }\n\n " + + " @supports (-ms-ime-align:auto), (-ms-accelerator:true) {\n .darkmode-toggle {display: none !important}\n }\n "); + var layer = document.createElement('div'); + var button = document.createElement('div'); + var background = document.createElement('div'); + button.innerHTML = label; + layer.classList.add('darkmode-layer'); + background.classList.add('darkmode-background'); + background.setAttribute('id','darkmode-background') + + var darkmodeActivated = window.localStorage.getItem('darkmode') === 'true'; + var preferedThemeOs = autoMatchOsTheme && window.matchMedia('(prefers-color-scheme: dark)').matches; + var darkmodeNeverActivatedByAction = window.localStorage.getItem('darkmode') === null; + + if (darkmodeActivated === true && saveInCookies || darkmodeNeverActivatedByAction && preferedThemeOs) { + layer.classList.add('darkmode-layer--expanded', 'darkmode-layer--simple', 'darkmode-layer--no-transition'); + button.classList.add('darkmode-toggle--white'); + document.body.classList.add('darkmode--activated'); + document.getElementById("wrapper").style.color = '#ccc'; + } + + document.body.insertBefore(button, document.body.firstChild); + document.body.insertBefore(layer, document.body.firstChild); + document.body.insertBefore(background, document.body.firstChild); + this.addStyle(css); + this.button = button; + this.layer = layer; + this.saveInCookies = saveInCookies; + this.time = time; + } + + _createClass(Darkmode, [{ + key: "addStyle", + value: function addStyle(css) { + var linkElement = document.createElement('link'); + linkElement.setAttribute('rel', 'stylesheet'); + linkElement.setAttribute('type', 'text/css'); + linkElement.setAttribute('href', 'data:text/css;charset=UTF-8,' + encodeURIComponent(css)); + document.head.appendChild(linkElement); + } + }, { + key: "showWidget", + value: function showWidget() { + var _this = this; + + var button = this.button; + var layer = this.layer; + var time = parseFloat(this.time) * 1000; + button.classList.add('darkmode-toggle'); + layer.classList.add('darkmode-layer--button'); + button.setAttribute('id','darkmode-toggle') + layer.setAttribute('id','darkmode-layer') + + button.addEventListener('click', function () { + var isDarkmode = _this.isActivated(); + + if (!isDarkmode) { + layer.classList.add('darkmode-layer--expanded'); + setTimeout(function () { + layer.classList.add('darkmode-layer--no-transition'); + layer.classList.add('darkmode-layer--simple'); + }, time); + } else { + layer.classList.remove('darkmode-layer--simple'); + setTimeout(function () { + layer.classList.remove('darkmode-layer--no-transition'); + layer.classList.remove('darkmode-layer--expanded'); + }, 1); + } + + button.classList.toggle('darkmode-toggle--white'); + document.body.classList.toggle('darkmode--activated'); + window.localStorage.setItem('darkmode', !isDarkmode); + }); + } + }, { + key: "toggle", + value: function toggle() { + var layer = this.layer; + var isDarkmode = this.isActivated(); + layer.classList.toggle('darkmode-layer--simple'); + document.body.classList.toggle('darkmode--activated'); + window.localStorage.setItem('darkmode', !isDarkmode); + } + }, { + key: "isActivated", + value: function isActivated() { + return document.body.classList.contains('darkmode--activated'); + } + }]); + + return Darkmode; + }(); + + exports.default = Darkmode; + module.exports = exports["default"]; + + /***/ + }), + + /***/ "./src/index.js": + /*!**********************!*\ + !*** ./src/index.js ***! + \**********************/ + /*! no static exports found */ + /***/ (function (module, exports, __webpack_require__) { + + "use strict"; + + + Object.defineProperty(exports, "__esModule", { + value: true + }); + exports.default = void 0; + + var _darkmode = _interopRequireDefault(__webpack_require__(/*! ./darkmode */ "./src/darkmode.js")); + + function _interopRequireDefault(obj) { + return obj && obj.__esModule ? obj : {default: obj}; + } + + var _default = _darkmode.default; + /* eslint-disable */ + + exports.default = _default; + + (function (window) { + window.Darkmode = _darkmode.default; + })(window); + /* eslint-enable */ + + + module.exports = exports["default"]; + + /***/ + }) + + /******/ + }); +}); +//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly9kYXJrbW9kZS1qcy93ZWJwYWNrL3VuaXZlcnNhbE1vZHVsZURlZmluaXRpb24iLCJ3ZWJwYWNrOi8vZGFya21vZGUtanMvd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vZGFya21vZGUtanMvLi9zcmMvZGFya21vZGUuanMiLCJ3ZWJwYWNrOi8vZGFya21vZGUtanMvLi9zcmMvaW5kZXguanMiXSwibmFtZXMiOlsiRGFya21vZGUiLCJvcHRpb25zIiwiYm90dG9tIiwicmlnaHQiLCJsZWZ0IiwidGltZSIsIm1peENvbG9yIiwiYmFja2dyb3VuZENvbG9yIiwiYnV0dG9uQ29sb3JEYXJrIiwiYnV0dG9uQ29sb3JMaWdodCIsImxhYmVsIiwic2F2ZUluQ29va2llcyIsImF1dG9NYXRjaE9zVGhlbWUiLCJjc3MiLCJsYXllciIsImRvY3VtZW50IiwiY3JlYXRlRWxlbWVudCIsImJ1dHRvbiIsImJhY2tncm91bmQiLCJpbm5lckhUTUwiLCJjbGFzc0xpc3QiLCJhZGQiLCJkYXJrbW9kZUFjdGl2YXRlZCIsIndpbmRvdyIsImxvY2FsU3RvcmFnZSIsImdldEl0ZW0iLCJwcmVmZXJlZFRoZW1lT3MiLCJtYXRjaE1lZGlhIiwibWF0Y2hlcyIsImRhcmttb2RlTmV2ZXJBY3RpdmF0ZWRCeUFjdGlvbiIsImJvZHkiLCJpbnNlcnRCZWZvcmUiLCJmaXJzdENoaWxkIiwiYWRkU3R5bGUiLCJsaW5rRWxlbWVudCIsInNldEF0dHJpYnV0ZSIsImVuY29kZVVSSUNvbXBvbmVudCIsImhlYWQiLCJhcHBlbmRDaGlsZCIsInBhcnNlRmxvYXQiLCJhZGRFdmVudExpc3RlbmVyIiwiaXNEYXJrbW9kZSIsImlzQWN0aXZhdGVkIiwic2V0VGltZW91dCIsInJlbW92ZSIsInRvZ2dsZSIsInNldEl0ZW0iLCJjb250YWlucyJdLCJtYXBwaW5ncyI6IkFBQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0EsQ0FBQztBQUNELE87QUNWQTtBQUNBOztBQUVBO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FBRUE7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTs7O0FBR0E7QUFDQTs7QUFFQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBLGtEQUEwQyxnQ0FBZ0M7QUFDMUU7QUFDQTs7QUFFQTtBQUNBO0FBQ0E7QUFDQSxnRUFBd0Qsa0JBQWtCO0FBQzFFO0FBQ0EseURBQWlELGNBQWM7QUFDL0Q7O0FBRUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBLGlEQUF5QyxpQ0FBaUM7QUFDMUUsd0hBQWdILG1CQUFtQixFQUFFO0FBQ3JJO0FBQ0E7O0FBRUE7QUFDQTtBQUNBO0FBQ0EsbUNBQTJCLDBCQUEwQixFQUFFO0FBQ3ZELHlDQUFpQyxlQUFlO0FBQ2hEO0FBQ0E7QUFDQTs7QUFFQTtBQUNBLDhEQUFzRCwrREFBK0Q7O0FBRXJIO0FBQ0E7OztBQUdBO0FBQ0E7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0lDbEZxQkEsUTs7O0FBQ25CLG9CQUFZQyxPQUFaLEVBQXFCO0FBQUE7O0FBQ25CLFFBQU1DLE1BQU0sR0FBR0QsT0FBTyxJQUFJQSxPQUFPLENBQUNDLE1BQW5CLElBQTZCLE1BQTVDO0FBQ0EsUUFBTUMsS0FBSyxHQUFHRixPQUFPLElBQUlBLE9BQU8sQ0FBQ0UsS0FBbkIsSUFBNEIsTUFBMUM7QUFDQSxRQUFNQyxJQUFJLEdBQUdILE9BQU8sSUFBSUEsT0FBTyxDQUFDRyxJQUFuQixJQUEyQixPQUF4QztBQUNBLFFBQU1DLElBQUksR0FBR0osT0FBTyxJQUFJQSxPQUFPLENBQUNJLElBQW5CLElBQTJCLE1BQXhDO0FBQ0EsUUFBTUMsUUFBUSxHQUFHTCxPQUFPLElBQUlBLE9BQU8sQ0FBQ0ssUUFBbkIsSUFBK0IsTUFBaEQ7QUFDQSxRQUFNQyxlQUFlLEdBQUdOLE9BQU8sSUFBSUEsT0FBTyxDQUFDTSxlQUFuQixJQUFzQyxNQUE5RDtBQUNBLFFBQU1DLGVBQWUsR0FBR1AsT0FBTyxJQUFJQSxPQUFPLENBQUNPLGVBQW5CLElBQXNDLFNBQTlEO0FBQ0EsUUFBTUMsZ0JBQWdCLEdBQUdSLE9BQU8sSUFBSUEsT0FBTyxDQUFDUSxnQkFBbkIsSUFBdUMsTUFBaEU7QUFDQSxRQUFNQyxLQUFLLEdBQUdULE9BQU8sSUFBSUEsT0FBTyxDQUFDUyxLQUFuQixJQUE0QixFQUExQztBQUNBLFFBQU1DLGFBQWEsR0FBR1YsT0FBTyxJQUFJQSxPQUFPLENBQUNVLGFBQXpDO0FBQ0E7O0FBQ0EsUUFBTUMsZ0JBQWdCLEdBQUdYLE9BQU8sSUFBSUEsT0FBTyxDQUFDVyxnQkFBUixLQUE2QixLQUF4QyxHQUFnRCxLQUFoRCxHQUF3RCxJQUFqRjtBQUNBOztBQUVBLFFBQU1DLEdBQUcscUhBSVNQLFFBSlQsd0NBS2FELElBTGIsbU1BYUlGLEtBYkosZ0NBY0tELE1BZEwsOEJBZUdFLElBZkgscWFBb0NTSSxlQXBDVCxtSUF5Q0lMLEtBekNKLGdDQTBDS0QsTUExQ0wsOEJBMkNHRSxJQTNDSCxzT0FvRFNLLGdCQXBEVCw2RUF3RFNGLGVBeERULDhpQkFBVDtBQWdGQSxRQUFNTyxLQUFLLEdBQUdDLFFBQVEsQ0FBQ0MsYUFBVCxDQUF1QixLQUF2QixDQUFkO0FBQ0EsUUFBTUMsTUFBTSxHQUFHRixRQUFRLENBQUNDLGFBQVQsQ0FBdUIsS0FBdkIsQ0FBZjtBQUNBLFFBQU1FLFVBQVUsR0FBR0gsUUFBUSxDQUFDQyxhQUFULENBQXVCLEtBQXZCLENBQW5CO0FBRUFDLFVBQU0sQ0FBQ0UsU0FBUCxHQUFtQlQsS0FBbkI7QUFDQUksU0FBSyxDQUFDTSxTQUFOLENBQWdCQyxHQUFoQixDQUFvQixnQkFBcEI7QUFDQUgsY0FBVSxDQUFDRSxTQUFYLENBQXFCQyxHQUFyQixDQUF5QixxQkFBekI7QUFFQSxRQUFNQyxpQkFBaUIsR0FBR0MsTUFBTSxDQUFDQyxZQUFQLENBQW9CQyxPQUFwQixDQUE0QixVQUE1QixNQUE0QyxNQUF0RTtBQUNBLFFBQU1DLGVBQWUsR0FBR2QsZ0JBQWdCLElBQUlXLE1BQU0sQ0FBQ0ksVUFBUCxDQUFrQiw4QkFBbEIsRUFBa0RDLE9BQTlGO0FBQ0EsUUFBTUMsOEJBQThCLEdBQUdOLE1BQU0sQ0FBQ0MsWUFBUCxDQUFvQkMsT0FBcEIsQ0FBNEIsVUFBNUIsTUFBNEMsSUFBbkY7O0FBRUEsUUFBS0gsaUJBQWlCLEtBQUssSUFBdEIsSUFBOEJYLGFBQS9CLElBQWtEa0IsOEJBQThCLElBQUlILGVBQXhGLEVBQTBHO0FBQ3hHWixXQUFLLENBQUNNLFNBQU4sQ0FBZ0JDLEdBQWhCLENBQW9CLDBCQUFwQixFQUFnRCx3QkFBaEQsRUFBMEUsK0JBQTFFO0FBQ0FKLFlBQU0sQ0FBQ0csU0FBUCxDQUFpQkMsR0FBakIsQ0FBcUIsd0JBQXJCO0FBQ0FOLGNBQVEsQ0FBQ2UsSUFBVCxDQUFjVixTQUFkLENBQXdCQyxHQUF4QixDQUE0QixxQkFBNUI7QUFDRDs7QUFFRE4sWUFBUSxDQUFDZSxJQUFULENBQWNDLFlBQWQsQ0FBMkJkLE1BQTNCLEVBQW1DRixRQUFRLENBQUNlLElBQVQsQ0FBY0UsVUFBakQ7QUFDQWpCLFlBQVEsQ0FBQ2UsSUFBVCxDQUFjQyxZQUFkLENBQTJCakIsS0FBM0IsRUFBa0NDLFFBQVEsQ0FBQ2UsSUFBVCxDQUFjRSxVQUFoRDtBQUNBakIsWUFBUSxDQUFDZSxJQUFULENBQWNDLFlBQWQsQ0FBMkJiLFVBQTNCLEVBQXVDSCxRQUFRLENBQUNlLElBQVQsQ0FBY0UsVUFBckQ7QUFFQSxTQUFLQyxRQUFMLENBQWNwQixHQUFkO0FBRUEsU0FBS0ksTUFBTCxHQUFjQSxNQUFkO0FBQ0EsU0FBS0gsS0FBTCxHQUFhQSxLQUFiO0FBQ0EsU0FBS0gsYUFBTCxHQUFxQkEsYUFBckI7QUFDQSxTQUFLTixJQUFMLEdBQVlBLElBQVo7QUFDRDs7Ozs2QkFFUVEsRyxFQUFLO0FBQ1osVUFBTXFCLFdBQVcsR0FBR25CLFFBQVEsQ0FBQ0MsYUFBVCxDQUF1QixNQUF2QixDQUFwQjtBQUVBa0IsaUJBQVcsQ0FBQ0MsWUFBWixDQUF5QixLQUF6QixFQUFnQyxZQUFoQztBQUNBRCxpQkFBVyxDQUFDQyxZQUFaLENBQXlCLE1BQXpCLEVBQWlDLFVBQWpDO0FBQ0FELGlCQUFXLENBQUNDLFlBQVosQ0FBeUIsTUFBekIsRUFBaUMsaUNBQWlDQyxrQkFBa0IsQ0FBQ3ZCLEdBQUQsQ0FBcEY7QUFDQUUsY0FBUSxDQUFDc0IsSUFBVCxDQUFjQyxXQUFkLENBQTBCSixXQUExQjtBQUNEOzs7aUNBRVk7QUFBQTs7QUFDWCxVQUFNakIsTUFBTSxHQUFHLEtBQUtBLE1BQXBCO0FBQ0EsVUFBTUgsS0FBSyxHQUFHLEtBQUtBLEtBQW5CO0FBQ0EsVUFBTVQsSUFBSSxHQUFHa0MsVUFBVSxDQUFDLEtBQUtsQyxJQUFOLENBQVYsR0FBd0IsSUFBckM7QUFFQVksWUFBTSxDQUFDRyxTQUFQLENBQWlCQyxHQUFqQixDQUFxQixpQkFBckI7QUFDQVAsV0FBSyxDQUFDTSxTQUFOLENBQWdCQyxHQUFoQixDQUFvQix3QkFBcEI7QUFFQUosWUFBTSxDQUFDdUIsZ0JBQVAsQ0FBd0IsT0FBeEIsRUFBaUMsWUFBTTtBQUNyQyxZQUFNQyxVQUFVLEdBQUcsS0FBSSxDQUFDQyxXQUFMLEVBQW5COztBQUVBLFlBQUksQ0FBQ0QsVUFBTCxFQUFpQjtBQUNmM0IsZUFBSyxDQUFDTSxTQUFOLENBQWdCQyxHQUFoQixDQUFvQiwwQkFBcEI7QUFDQXNCLG9CQUFVLENBQUMsWUFBTTtBQUNmN0IsaUJBQUssQ0FBQ00sU0FBTixDQUFnQkMsR0FBaEIsQ0FBb0IsK0JBQXBCO0FBQ0FQLGlCQUFLLENBQUNNLFNBQU4sQ0FBZ0JDLEdBQWhCLENBQW9CLHdCQUFwQjtBQUNELFdBSFMsRUFHUGhCLElBSE8sQ0FBVjtBQUlELFNBTkQsTUFNTztBQUNMUyxlQUFLLENBQUNNLFNBQU4sQ0FBZ0J3QixNQUFoQixDQUF1Qix3QkFBdkI7QUFDQUQsb0JBQVUsQ0FBQyxZQUFNO0FBQ2Y3QixpQkFBSyxDQUFDTSxTQUFOLENBQWdCd0IsTUFBaEIsQ0FBdUIsK0JBQXZCO0FBQ0E5QixpQkFBSyxDQUFDTSxTQUFOLENBQWdCd0IsTUFBaEIsQ0FBdUIsMEJBQXZCO0FBQ0QsV0FIUyxFQUdQLENBSE8sQ0FBVjtBQUlEOztBQUVEM0IsY0FBTSxDQUFDRyxTQUFQLENBQWlCeUIsTUFBakIsQ0FBd0Isd0JBQXhCO0FBQ0E5QixnQkFBUSxDQUFDZSxJQUFULENBQWNWLFNBQWQsQ0FBd0J5QixNQUF4QixDQUErQixxQkFBL0I7QUFDQXRCLGNBQU0sQ0FBQ0MsWUFBUCxDQUFvQnNCLE9BQXBCLENBQTRCLFVBQTVCLEVBQXdDLENBQUNMLFVBQXpDO0FBQ0QsT0FwQkQ7QUFxQkQ7Ozs2QkFFUTtBQUNQLFVBQU0zQixLQUFLLEdBQUcsS0FBS0EsS0FBbkI7QUFDQSxVQUFNMkIsVUFBVSxHQUFHLEtBQUtDLFdBQUwsRUFBbkI7QUFFQTVCLFdBQUssQ0FBQ00sU0FBTixDQUFnQnlCLE1BQWhCLENBQXVCLHdCQUF2QjtBQUNBOUIsY0FBUSxDQUFDZSxJQUFULENBQWNWLFNBQWQsQ0FBd0J5QixNQUF4QixDQUErQixxQkFBL0I7QUFDQXRCLFlBQU0sQ0FBQ0MsWUFBUCxDQUFvQnNCLE9BQXBCLENBQTRCLFVBQTVCLEVBQXdDLENBQUNMLFVBQXpDO0FBQ0Q7OztrQ0FFYTtBQUNaLGFBQU8xQixRQUFRLENBQUNlLElBQVQsQ0FBY1YsU0FBZCxDQUF3QjJCLFFBQXhCLENBQWlDLHFCQUFqQyxDQUFQO0FBQ0Q7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FDakxIOzs7OztBQUdBOzs7O0FBQ0EsQ0FBQyxVQUFTeEIsTUFBVCxFQUFnQjtBQUNmQSxRQUFNLENBQUN2QixRQUFQO0FBQ0QsQ0FGRCxFQUVHdUIsTUFGSDtBQUdBIiwiZmlsZSI6ImRhcmttb2RlLWpzLmpzIiwic291cmNlc0NvbnRlbnQiOlsiKGZ1bmN0aW9uIHdlYnBhY2tVbml2ZXJzYWxNb2R1bGVEZWZpbml0aW9uKHJvb3QsIGZhY3RvcnkpIHtcblx0aWYodHlwZW9mIGV4cG9ydHMgPT09ICdvYmplY3QnICYmIHR5cGVvZiBtb2R1bGUgPT09ICdvYmplY3QnKVxuXHRcdG1vZHVsZS5leHBvcnRzID0gZmFjdG9yeSgpO1xuXHRlbHNlIGlmKHR5cGVvZiBkZWZpbmUgPT09ICdmdW5jdGlvbicgJiYgZGVmaW5lLmFtZClcblx0XHRkZWZpbmUoXCJkYXJrbW9kZS1qc1wiLCBbXSwgZmFjdG9yeSk7XG5cdGVsc2UgaWYodHlwZW9mIGV4cG9ydHMgPT09ICdvYmplY3QnKVxuXHRcdGV4cG9ydHNbXCJkYXJrbW9kZS1qc1wiXSA9IGZhY3RvcnkoKTtcblx0ZWxzZVxuXHRcdHJvb3RbXCJkYXJrbW9kZS1qc1wiXSA9IGZhY3RvcnkoKTtcbn0pKHR5cGVvZiBzZWxmICE9PSAndW5kZWZpbmVkJyA/IHNlbGYgOiB0aGlzLCBmdW5jdGlvbigpIHtcbnJldHVybiAiLCIgXHQvLyBUaGUgbW9kdWxlIGNhY2hlXG4gXHR2YXIgaW5zdGFsbGVkTW9kdWxlcyA9IHt9O1xuXG4gXHQvLyBUaGUgcmVxdWlyZSBmdW5jdGlvblxuIFx0ZnVuY3Rpb24gX193ZWJwYWNrX3JlcXVpcmVfXyhtb2R1bGVJZCkge1xuXG4gXHRcdC8vIENoZWNrIGlmIG1vZHVsZSBpcyBpbiBjYWNoZVxuIFx0XHRpZihpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXSkge1xuIFx0XHRcdHJldHVybiBpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXS5leHBvcnRzO1xuIFx0XHR9XG4gXHRcdC8vIENyZWF0ZSBhIG5ldyBtb2R1bGUgKGFuZCBwdXQgaXQgaW50byB0aGUgY2FjaGUpXG4gXHRcdHZhciBtb2R1bGUgPSBpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXSA9IHtcbiBcdFx0XHRpOiBtb2R1bGVJZCxcbiBcdFx0XHRsOiBmYWxzZSxcbiBcdFx0XHRleHBvcnRzOiB7fVxuIFx0XHR9O1xuXG4gXHRcdC8vIEV4ZWN1dGUgdGhlIG1vZHVsZSBmdW5jdGlvblxuIFx0XHRtb2R1bGVzW21vZHVsZUlkXS5jYWxsKG1vZHVsZS5leHBvcnRzLCBtb2R1bGUsIG1vZHVsZS5leHBvcnRzLCBfX3dlYnBhY2tfcmVxdWlyZV9fKTtcblxuIFx0XHQvLyBGbGFnIHRoZSBtb2R1bGUgYXMgbG9hZGVkXG4gXHRcdG1vZHVsZS5sID0gdHJ1ZTtcblxuIFx0XHQvLyBSZXR1cm4gdGhlIGV4cG9ydHMgb2YgdGhlIG1vZHVsZVxuIFx0XHRyZXR1cm4gbW9kdWxlLmV4cG9ydHM7XG4gXHR9XG5cblxuIFx0Ly8gZXhwb3NlIHRoZSBtb2R1bGVzIG9iamVjdCAoX193ZWJwYWNrX21vZHVsZXNfXylcbiBcdF9fd2VicGFja19yZXF1aXJlX18ubSA9IG1vZHVsZXM7XG5cbiBcdC8vIGV4cG9zZSB0aGUgbW9kdWxlIGNhY2hlXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLmMgPSBpbnN0YWxsZWRNb2R1bGVzO1xuXG4gXHQvLyBkZWZpbmUgZ2V0dGVyIGZ1bmN0aW9uIGZvciBoYXJtb255IGV4cG9ydHNcbiBcdF9fd2VicGFja19yZXF1aXJlX18uZCA9IGZ1bmN0aW9uKGV4cG9ydHMsIG5hbWUsIGdldHRlcikge1xuIFx0XHRpZighX193ZWJwYWNrX3JlcXVpcmVfXy5vKGV4cG9ydHMsIG5hbWUpKSB7XG4gXHRcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIG5hbWUsIHsgZW51bWVyYWJsZTogdHJ1ZSwgZ2V0OiBnZXR0ZXIgfSk7XG4gXHRcdH1cbiBcdH07XG5cbiBcdC8vIGRlZmluZSBfX2VzTW9kdWxlIG9uIGV4cG9ydHNcbiBcdF9fd2VicGFja19yZXF1aXJlX18uciA9IGZ1bmN0aW9uKGV4cG9ydHMpIHtcbiBcdFx0aWYodHlwZW9mIFN5bWJvbCAhPT0gJ3VuZGVmaW5lZCcgJiYgU3ltYm9sLnRvU3RyaW5nVGFnKSB7XG4gXHRcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIFN5bWJvbC50b1N0cmluZ1RhZywgeyB2YWx1ZTogJ01vZHVsZScgfSk7XG4gXHRcdH1cbiBcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsICdfX2VzTW9kdWxlJywgeyB2YWx1ZTogdHJ1ZSB9KTtcbiBcdH07XG5cbiBcdC8vIGNyZWF0ZSBhIGZha2UgbmFtZXNwYWNlIG9iamVjdFxuIFx0Ly8gbW9kZSAmIDE6IHZhbHVlIGlzIGEgbW9kdWxlIGlkLCByZXF1aXJlIGl0XG4gXHQvLyBtb2RlICYgMjogbWVyZ2UgYWxsIHByb3BlcnRpZXMgb2YgdmFsdWUgaW50byB0aGUgbnNcbiBcdC8vIG1vZGUgJiA0OiByZXR1cm4gdmFsdWUgd2hlbiBhbHJlYWR5IG5zIG9iamVjdFxuIFx0Ly8gbW9kZSAmIDh8MTogYmVoYXZlIGxpa2UgcmVxdWlyZVxuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy50ID0gZnVuY3Rpb24odmFsdWUsIG1vZGUpIHtcbiBcdFx0aWYobW9kZSAmIDEpIHZhbHVlID0gX193ZWJwYWNrX3JlcXVpcmVfXyh2YWx1ZSk7XG4gXHRcdGlmKG1vZGUgJiA4KSByZXR1cm4gdmFsdWU7XG4gXHRcdGlmKChtb2RlICYgNCkgJiYgdHlwZW9mIHZhbHVlID09PSAnb2JqZWN0JyAmJiB2YWx1ZSAmJiB2YWx1ZS5fX2VzTW9kdWxlKSByZXR1cm4gdmFsdWU7XG4gXHRcdHZhciBucyA9IE9iamVjdC5jcmVhdGUobnVsbCk7XG4gXHRcdF9fd2VicGFja19yZXF1aXJlX18ucihucyk7XG4gXHRcdE9iamVjdC5kZWZpbmVQcm9wZXJ0eShucywgJ2RlZmF1bHQnLCB7IGVudW1lcmFibGU6IHRydWUsIHZhbHVlOiB2YWx1ZSB9KTtcbiBcdFx0aWYobW9kZSAmIDIgJiYgdHlwZW9mIHZhbHVlICE9ICdzdHJpbmcnKSBmb3IodmFyIGtleSBpbiB2YWx1ZSkgX193ZWJwYWNrX3JlcXVpcmVfXy5kKG5zLCBrZXksIGZ1bmN0aW9uKGtleSkgeyByZXR1cm4gdmFsdWVba2V5XTsgfS5iaW5kKG51bGwsIGtleSkpO1xuIFx0XHRyZXR1cm4gbnM7XG4gXHR9O1xuXG4gXHQvLyBnZXREZWZhdWx0RXhwb3J0IGZ1bmN0aW9uIGZvciBjb21wYXRpYmlsaXR5IHdpdGggbm9uLWhhcm1vbnkgbW9kdWxlc1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5uID0gZnVuY3Rpb24obW9kdWxlKSB7XG4gXHRcdHZhciBnZXR0ZXIgPSBtb2R1bGUgJiYgbW9kdWxlLl9fZXNNb2R1bGUgP1xuIFx0XHRcdGZ1bmN0aW9uIGdldERlZmF1bHQoKSB7IHJldHVybiBtb2R1bGVbJ2RlZmF1bHQnXTsgfSA6XG4gXHRcdFx0ZnVuY3Rpb24gZ2V0TW9kdWxlRXhwb3J0cygpIHsgcmV0dXJuIG1vZHVsZTsgfTtcbiBcdFx0X193ZWJwYWNrX3JlcXVpcmVfXy5kKGdldHRlciwgJ2EnLCBnZXR0ZXIpO1xuIFx0XHRyZXR1cm4gZ2V0dGVyO1xuIFx0fTtcblxuIFx0Ly8gT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLm8gPSBmdW5jdGlvbihvYmplY3QsIHByb3BlcnR5KSB7IHJldHVybiBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwob2JqZWN0LCBwcm9wZXJ0eSk7IH07XG5cbiBcdC8vIF9fd2VicGFja19wdWJsaWNfcGF0aF9fXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLnAgPSBcIlwiO1xuXG5cbiBcdC8vIExvYWQgZW50cnkgbW9kdWxlIGFuZCByZXR1cm4gZXhwb3J0c1xuIFx0cmV0dXJuIF9fd2VicGFja19yZXF1aXJlX18oX193ZWJwYWNrX3JlcXVpcmVfXy5zID0gXCIuL3NyYy9pbmRleC5qc1wiKTtcbiIsImV4cG9ydCBkZWZhdWx0IGNsYXNzIERhcmttb2RlIHtcclxuICBjb25zdHJ1Y3RvcihvcHRpb25zKSB7XHJcbiAgICBjb25zdCBib3R0b20gPSBvcHRpb25zICYmIG9wdGlvbnMuYm90dG9tIHx8ICczMnB4JztcclxuICAgIGNvbnN0IHJpZ2h0ID0gb3B0aW9ucyAmJiBvcHRpb25zLnJpZ2h0IHx8ICczMnB4JztcclxuICAgIGNvbnN0IGxlZnQgPSBvcHRpb25zICYmIG9wdGlvbnMubGVmdCB8fCAndW5zZXQnO1xyXG4gICAgY29uc3QgdGltZSA9IG9wdGlvbnMgJiYgb3B0aW9ucy50aW1lIHx8ICcwLjNzJztcclxuICAgIGNvbnN0IG1peENvbG9yID0gb3B0aW9ucyAmJiBvcHRpb25zLm1peENvbG9yIHx8ICcjZmZmJztcclxuICAgIGNvbnN0IGJhY2tncm91bmRDb2xvciA9IG9wdGlvbnMgJiYgb3B0aW9ucy5iYWNrZ3JvdW5kQ29sb3IgfHwgJyNmZmYnO1xyXG4gICAgY29uc3QgYnV0dG9uQ29sb3JEYXJrID0gb3B0aW9ucyAmJiBvcHRpb25zLmJ1dHRvbkNvbG9yRGFyayB8fCAnIzEwMGYyYyc7XHJcbiAgICBjb25zdCBidXR0b25Db2xvckxpZ2h0ID0gb3B0aW9ucyAmJiBvcHRpb25zLmJ1dHRvbkNvbG9yTGlnaHQgfHwgJyNmZmYnO1xyXG4gICAgY29uc3QgbGFiZWwgPSBvcHRpb25zICYmIG9wdGlvbnMubGFiZWwgfHwgJyc7XHJcbiAgICBjb25zdCBzYXZlSW5Db29raWVzID0gb3B0aW9ucyAmJiBvcHRpb25zLnNhdmVJbkNvb2tpZXM7XHJcbiAgICAvKiBlc2xpbnQtZGlzYWJsZSAqL1xyXG4gICAgY29uc3QgYXV0b01hdGNoT3NUaGVtZSA9IG9wdGlvbnMgJiYgb3B0aW9ucy5hdXRvTWF0Y2hPc1RoZW1lID09PSBmYWxzZSA/IGZhbHNlIDogdHJ1ZTtcclxuICAgIC8qIGVzbGludC1lbmFibGUgKi9cclxuXHJcbiAgICBjb25zdCBjc3MgPSBgXHJcbiAgICAgIC5kYXJrbW9kZS1sYXllciB7XHJcbiAgICAgICAgcG9zaXRpb246IGZpeGVkO1xyXG4gICAgICAgIHBvaW50ZXItZXZlbnRzOiBub25lO1xyXG4gICAgICAgIGJhY2tncm91bmQ6ICR7bWl4Q29sb3J9O1xyXG4gICAgICAgIHRyYW5zaXRpb246IGFsbCAke3RpbWV9IGVhc2U7XHJcbiAgICAgICAgbWl4LWJsZW5kLW1vZGU6IGRpZmZlcmVuY2U7XHJcbiAgICAgIH1cclxuXHJcbiAgICAgIC5kYXJrbW9kZS1sYXllci0tYnV0dG9uIHtcclxuICAgICAgICB3aWR0aDogMi45cmVtO1xyXG4gICAgICAgIGhlaWdodDogMi45cmVtO1xyXG4gICAgICAgIGJvcmRlci1yYWRpdXM6IDUwJTtcclxuICAgICAgICByaWdodDogJHtyaWdodH07XHJcbiAgICAgICAgYm90dG9tOiAke2JvdHRvbX07XHJcbiAgICAgICAgbGVmdDogJHtsZWZ0fTtcclxuICAgICAgfVxyXG5cclxuICAgICAgLmRhcmttb2RlLWxheWVyLS1zaW1wbGUge1xyXG4gICAgICAgIHdpZHRoOiAxMDAlO1xyXG4gICAgICAgIGhlaWdodDogMTAwJTtcclxuICAgICAgICB0b3A6IDA7XHJcbiAgICAgICAgbGVmdDogMDtcclxuICAgICAgICB0cmFuc2Zvcm06IHNjYWxlKDEpICFpbXBvcnRhbnQ7XHJcbiAgICAgIH1cclxuXHJcbiAgICAgIC5kYXJrbW9kZS1sYXllci0tZXhwYW5kZWQge1xyXG4gICAgICAgIHRyYW5zZm9ybTogc2NhbGUoMTAwKTtcclxuICAgICAgICBib3JkZXItcmFkaXVzOiAwO1xyXG4gICAgICB9XHJcblxyXG4gICAgICAuZGFya21vZGUtbGF5ZXItLW5vLXRyYW5zaXRpb24ge1xyXG4gICAgICAgIHRyYW5zaXRpb246IG5vbmU7XHJcbiAgICAgIH1cclxuXHJcbiAgICAgIC5kYXJrbW9kZS10b2dnbGUge1xyXG4gICAgICAgIGJhY2tncm91bmQ6ICR7YnV0dG9uQ29sb3JEYXJrfTtcclxuICAgICAgICB3aWR0aDogM3JlbTtcclxuICAgICAgICBoZWlnaHQ6IDNyZW07XHJcbiAgICAgICAgcG9zaXRpb246IGZpeGVkO1xyXG4gICAgICAgIGJvcmRlci1yYWRpdXM6IDUwJTtcclxuICAgICAgICByaWdodDogJHtyaWdodH07XHJcbiAgICAgICAgYm90dG9tOiAke2JvdHRvbX07XHJcbiAgICAgICAgbGVmdDogJHtsZWZ0fTtcclxuICAgICAgICBjdXJzb3I6IHBvaW50ZXI7XHJcbiAgICAgICAgdHJhbnNpdGlvbjogYWxsIDAuNXMgZWFzZTtcclxuICAgICAgICBkaXNwbGF5OiBmbGV4O1xyXG4gICAgICAgIGp1c3RpZnktY29udGVudDogY2VudGVyO1xyXG4gICAgICAgIGFsaWduLWl0ZW1zOiBjZW50ZXI7XHJcbiAgICAgIH1cclxuXHJcbiAgICAgIC5kYXJrbW9kZS10b2dnbGUtLXdoaXRlIHtcclxuICAgICAgICBiYWNrZ3JvdW5kOiAke2J1dHRvbkNvbG9yTGlnaHR9O1xyXG4gICAgICB9XHJcblxyXG4gICAgICAuZGFya21vZGUtYmFja2dyb3VuZCB7XHJcbiAgICAgICAgYmFja2dyb3VuZDogJHtiYWNrZ3JvdW5kQ29sb3J9O1xyXG4gICAgICAgIHBvc2l0aW9uOiBmaXhlZDtcclxuICAgICAgICBwb2ludGVyLWV2ZW50czogbm9uZTtcclxuICAgICAgICB6LWluZGV4OiAtMTA7XHJcbiAgICAgICAgd2lkdGg6IDEwMCU7XHJcbiAgICAgICAgaGVpZ2h0OiAxMDAlO1xyXG4gICAgICAgIHRvcDogMDtcclxuICAgICAgICBsZWZ0OiAwO1xyXG4gICAgICB9XHJcblxyXG4gICAgICBpbWcsIC5kYXJrbW9kZS1pZ25vcmUge1xyXG4gICAgICAgIGlzb2xhdGlvbjogaXNvbGF0ZTtcclxuICAgICAgICBkaXNwbGF5OiBpbmxpbmUtYmxvY2s7XHJcbiAgICAgIH1cclxuXHJcbiAgICAgIEBtZWRpYSBzY3JlZW4gYW5kICgtbXMtaGlnaC1jb250cmFzdDogYWN0aXZlKSwgKC1tcy1oaWdoLWNvbnRyYXN0OiBub25lKSB7XHJcbiAgICAgICAgLmRhcmttb2RlLXRvZ2dsZSB7ZGlzcGxheTogbm9uZSAhaW1wb3J0YW50fVxyXG4gICAgICB9XHJcblxyXG4gICAgICBAc3VwcG9ydHMgKC1tcy1pbWUtYWxpZ246YXV0byksICgtbXMtYWNjZWxlcmF0b3I6dHJ1ZSkge1xyXG4gICAgICAgIC5kYXJrbW9kZS10b2dnbGUge2Rpc3BsYXk6IG5vbmUgIWltcG9ydGFudH1cclxuICAgICAgfVxyXG4gICAgYDtcclxuXHJcbiAgICBjb25zdCBsYXllciA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpO1xyXG4gICAgY29uc3QgYnV0dG9uID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2Jyk7XHJcbiAgICBjb25zdCBiYWNrZ3JvdW5kID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnZGl2Jyk7XHJcblxyXG4gICAgYnV0dG9uLmlubmVySFRNTCA9IGxhYmVsO1xyXG4gICAgbGF5ZXIuY2xhc3NMaXN0LmFkZCgnZGFya21vZGUtbGF5ZXInKTtcclxuICAgIGJhY2tncm91bmQuY2xhc3NMaXN0LmFkZCgnZGFya21vZGUtYmFja2dyb3VuZCcpO1xyXG5cclxuICAgIGNvbnN0IGRhcmttb2RlQWN0aXZhdGVkID0gd2luZG93LmxvY2FsU3RvcmFnZS5nZXRJdGVtKCdkYXJrbW9kZScpID09PSAndHJ1ZSc7XHJcbiAgICBjb25zdCBwcmVmZXJlZFRoZW1lT3MgPSBhdXRvTWF0Y2hPc1RoZW1lICYmIHdpbmRvdy5tYXRjaE1lZGlhKCcocHJlZmVycy1jb2xvci1zY2hlbWU6IGRhcmspJykubWF0Y2hlcztcclxuICAgIGNvbnN0IGRhcmttb2RlTmV2ZXJBY3RpdmF0ZWRCeUFjdGlvbiA9IHdpbmRvdy5sb2NhbFN0b3JhZ2UuZ2V0SXRlbSgnZGFya21vZGUnKSA9PT0gbnVsbDtcclxuXHJcbiAgICBpZiAoKGRhcmttb2RlQWN0aXZhdGVkID09PSB0cnVlICYmIHNhdmVJbkNvb2tpZXMpIHx8IChkYXJrbW9kZU5ldmVyQWN0aXZhdGVkQnlBY3Rpb24gJiYgcHJlZmVyZWRUaGVtZU9zKSkge1xyXG4gICAgICBsYXllci5jbGFzc0xpc3QuYWRkKCdkYXJrbW9kZS1sYXllci0tZXhwYW5kZWQnLCAnZGFya21vZGUtbGF5ZXItLXNpbXBsZScsICdkYXJrbW9kZS1sYXllci0tbm8tdHJhbnNpdGlvbicpO1xyXG4gICAgICBidXR0b24uY2xhc3NMaXN0LmFkZCgnZGFya21vZGUtdG9nZ2xlLS13aGl0ZScpO1xyXG4gICAgICBkb2N1bWVudC5ib2R5LmNsYXNzTGlzdC5hZGQoJ2Rhcmttb2RlLS1hY3RpdmF0ZWQnKTtcclxuICAgIH1cclxuXHJcbiAgICBkb2N1bWVudC5ib2R5Lmluc2VydEJlZm9yZShidXR0b24sIGRvY3VtZW50LmJvZHkuZmlyc3RDaGlsZCk7XHJcbiAgICBkb2N1bWVudC5ib2R5Lmluc2VydEJlZm9yZShsYXllciwgZG9jdW1lbnQuYm9keS5maXJzdENoaWxkKTtcclxuICAgIGRvY3VtZW50LmJvZHkuaW5zZXJ0QmVmb3JlKGJhY2tncm91bmQsIGRvY3VtZW50LmJvZHkuZmlyc3RDaGlsZCk7XHJcblxyXG4gICAgdGhpcy5hZGRTdHlsZShjc3MpO1xyXG5cclxuICAgIHRoaXMuYnV0dG9uID0gYnV0dG9uO1xyXG4gICAgdGhpcy5sYXllciA9IGxheWVyO1xyXG4gICAgdGhpcy5zYXZlSW5Db29raWVzID0gc2F2ZUluQ29va2llcztcclxuICAgIHRoaXMudGltZSA9IHRpbWU7XHJcbiAgfVxyXG5cclxuICBhZGRTdHlsZShjc3MpIHtcclxuICAgIGNvbnN0IGxpbmtFbGVtZW50ID0gZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgnbGluaycpO1xyXG5cclxuICAgIGxpbmtFbGVtZW50LnNldEF0dHJpYnV0ZSgncmVsJywgJ3N0eWxlc2hlZXQnKTtcclxuICAgIGxpbmtFbGVtZW50LnNldEF0dHJpYnV0ZSgndHlwZScsICd0ZXh0L2NzcycpO1xyXG4gICAgbGlua0VsZW1lbnQuc2V0QXR0cmlidXRlKCdocmVmJywgJ2RhdGE6dGV4dC9jc3M7Y2hhcnNldD1VVEYtOCwnICsgZW5jb2RlVVJJQ29tcG9uZW50KGNzcykpO1xyXG4gICAgZG9jdW1lbnQuaGVhZC5hcHBlbmRDaGlsZChsaW5rRWxlbWVudCk7XHJcbiAgfVxyXG5cclxuICBzaG93V2lkZ2V0KCkge1xyXG4gICAgY29uc3QgYnV0dG9uID0gdGhpcy5idXR0b247XHJcbiAgICBjb25zdCBsYXllciA9IHRoaXMubGF5ZXI7XHJcbiAgICBjb25zdCB0aW1lID0gcGFyc2VGbG9hdCh0aGlzLnRpbWUpICogMTAwMDtcclxuXHJcbiAgICBidXR0b24uY2xhc3NMaXN0LmFkZCgnZGFya21vZGUtdG9nZ2xlJyk7XHJcbiAgICBsYXllci5jbGFzc0xpc3QuYWRkKCdkYXJrbW9kZS1sYXllci0tYnV0dG9uJyk7XHJcblxyXG4gICAgYnV0dG9uLmFkZEV2ZW50TGlzdGVuZXIoJ2NsaWNrJywgKCkgPT4ge1xyXG4gICAgICBjb25zdCBpc0Rhcmttb2RlID0gdGhpcy5pc0FjdGl2YXRlZCgpO1xyXG5cclxuICAgICAgaWYgKCFpc0Rhcmttb2RlKSB7XHJcbiAgICAgICAgbGF5ZXIuY2xhc3NMaXN0LmFkZCgnZGFya21vZGUtbGF5ZXItLWV4cGFuZGVkJyk7XHJcbiAgICAgICAgc2V0VGltZW91dCgoKSA9PiB7XHJcbiAgICAgICAgICBsYXllci5jbGFzc0xpc3QuYWRkKCdkYXJrbW9kZS1sYXllci0tbm8tdHJhbnNpdGlvbicpO1xyXG4gICAgICAgICAgbGF5ZXIuY2xhc3NMaXN0LmFkZCgnZGFya21vZGUtbGF5ZXItLXNpbXBsZScpO1xyXG4gICAgICAgIH0sIHRpbWUpO1xyXG4gICAgICB9IGVsc2Uge1xyXG4gICAgICAgIGxheWVyLmNsYXNzTGlzdC5yZW1vdmUoJ2Rhcmttb2RlLWxheWVyLS1zaW1wbGUnKTtcclxuICAgICAgICBzZXRUaW1lb3V0KCgpID0+IHtcclxuICAgICAgICAgIGxheWVyLmNsYXNzTGlzdC5yZW1vdmUoJ2Rhcmttb2RlLWxheWVyLS1uby10cmFuc2l0aW9uJyk7XHJcbiAgICAgICAgICBsYXllci5jbGFzc0xpc3QucmVtb3ZlKCdkYXJrbW9kZS1sYXllci0tZXhwYW5kZWQnKTtcclxuICAgICAgICB9LCAxKTtcclxuICAgICAgfVxyXG5cclxuICAgICAgYnV0dG9uLmNsYXNzTGlzdC50b2dnbGUoJ2Rhcmttb2RlLXRvZ2dsZS0td2hpdGUnKTtcclxuICAgICAgZG9jdW1lbnQuYm9keS5jbGFzc0xpc3QudG9nZ2xlKCdkYXJrbW9kZS0tYWN0aXZhdGVkJyk7XHJcbiAgICAgIHdpbmRvdy5sb2NhbFN0b3JhZ2Uuc2V0SXRlbSgnZGFya21vZGUnLCAhaXNEYXJrbW9kZSk7XHJcbiAgICB9KTtcclxuICB9XHJcblxyXG4gIHRvZ2dsZSgpIHtcclxuICAgIGNvbnN0IGxheWVyID0gdGhpcy5sYXllcjtcclxuICAgIGNvbnN0IGlzRGFya21vZGUgPSB0aGlzLmlzQWN0aXZhdGVkKCk7XHJcblxyXG4gICAgbGF5ZXIuY2xhc3NMaXN0LnRvZ2dsZSgnZGFya21vZGUtbGF5ZXItLXNpbXBsZScpO1xyXG4gICAgZG9jdW1lbnQuYm9keS5jbGFzc0xpc3QudG9nZ2xlKCdkYXJrbW9kZS0tYWN0aXZhdGVkJyk7XHJcbiAgICB3aW5kb3cubG9jYWxTdG9yYWdlLnNldEl0ZW0oJ2Rhcmttb2RlJywgIWlzRGFya21vZGUpO1xyXG4gIH1cclxuXHJcbiAgaXNBY3RpdmF0ZWQoKSB7XHJcbiAgICByZXR1cm4gZG9jdW1lbnQuYm9keS5jbGFzc0xpc3QuY29udGFpbnMoJ2Rhcmttb2RlLS1hY3RpdmF0ZWQnKTtcclxuICB9XHJcbn1cclxuIiwiaW1wb3J0IERhcmttb2RlIGZyb20gJy4vZGFya21vZGUnO1xyXG5leHBvcnQgZGVmYXVsdCBEYXJrbW9kZTtcclxuXHJcbi8qIGVzbGludC1kaXNhYmxlICovXHJcbihmdW5jdGlvbih3aW5kb3cpe1xyXG4gIHdpbmRvdy5EYXJrbW9kZSA9IERhcmttb2RlO1xyXG59KSh3aW5kb3cpXHJcbi8qIGVzbGludC1lbmFibGUgKi9cclxuIl0sInNvdXJjZVJvb3QiOiIifQ== \ No newline at end of file diff --git a/blog-site/public/js/fancybox.min.js b/blog-site/public/js/fancybox.min.js new file mode 100644 index 00000000..e8b1b242 --- /dev/null +++ b/blog-site/public/js/fancybox.min.js @@ -0,0 +1,13 @@ +// ================================================== +// fancyBox v3.5.7 +// +// Licensed GPLv3 for open source use +// or fancyBox Commercial License for commercial use +// +// http://fancyapps.com/fancybox/ +// Copyright 2019 fancyApps +// +// ================================================== +!function(t,e,n,o){"use strict";function i(t,e){var o,i,a,s=[],r=0;t&&t.isDefaultPrevented()||(t.preventDefault(),e=e||{},t&&t.data&&(e=h(t.data.options,e)),o=e.$target||n(t.currentTarget).trigger("blur"),(a=n.fancybox.getInstance())&&a.$trigger&&a.$trigger.is(o)||(e.selector?s=n(e.selector):(i=o.attr("data-fancybox")||"",i?(s=t.data?t.data.items:[],s=s.length?s.filter('[data-fancybox="'+i+'"]'):n('[data-fancybox="'+i+'"]')):s=[o]),r=n(s).index(o),r<0&&(r=0),a=n.fancybox.open(s,e,r),a.$trigger=o))}if(t.console=t.console||{info:function(t){}},n){if(n.fn.fancybox)return void console.info("fancyBox already initialized");var a={closeExisting:!1,loop:!1,gutter:50,keyboard:!0,preventCaptionOverlap:!0,arrows:!0,infobar:!0,smallBtn:"auto",toolbar:"auto",buttons:["zoom","slideShow","thumbs","close"],idleTime:3,protect:!1,modal:!1,image:{preload:!1},ajax:{settings:{data:{fancybox:!0}}},iframe:{tpl:'',preload:!0,css:{},attr:{scrolling:"auto"}},video:{tpl:'',format:"",autoStart:!0},defaultType:"image",animationEffect:"zoom",animationDuration:366,zoomOpacity:"auto",transitionEffect:"fade",transitionDuration:366,slideClass:"",baseClass:"",baseTpl:'',spinnerTpl:'
',errorTpl:'

{{ERROR}}

',btnTpl:{download:'',zoom:'',close:'',arrowLeft:'',arrowRight:'',smallBtn:''},parentEl:"body",hideScrollbar:!0,autoFocus:!0,backFocus:!0,trapFocus:!0,fullScreen:{autoStart:!1},touch:{vertical:!0,momentum:!0},hash:null,media:{},slideShow:{autoStart:!1,speed:3e3},thumbs:{autoStart:!1,hideOnClose:!0,parentEl:".fancybox-container",axis:"y"},wheel:"auto",onInit:n.noop,beforeLoad:n.noop,afterLoad:n.noop,beforeShow:n.noop,afterShow:n.noop,beforeClose:n.noop,afterClose:n.noop,onActivate:n.noop,onDeactivate:n.noop,clickContent:function(t,e){return"image"===t.type&&"zoom"},clickSlide:"close",clickOutside:"close",dblclickContent:!1,dblclickSlide:!1,dblclickOutside:!1,mobile:{preventCaptionOverlap:!1,idleTime:!1,clickContent:function(t,e){return"image"===t.type&&"toggleControls"},clickSlide:function(t,e){return"image"===t.type?"toggleControls":"close"},dblclickContent:function(t,e){return"image"===t.type&&"zoom"},dblclickSlide:function(t,e){return"image"===t.type&&"zoom"}},lang:"en",i18n:{en:{CLOSE:"Close",NEXT:"Next",PREV:"Previous",ERROR:"The requested content cannot be loaded.
Please try again later.",PLAY_START:"Start slideshow",PLAY_STOP:"Pause slideshow",FULL_SCREEN:"Full screen",THUMBS:"Thumbnails",DOWNLOAD:"Download",SHARE:"Share",ZOOM:"Zoom"},de:{CLOSE:"Schließen",NEXT:"Weiter",PREV:"Zurück",ERROR:"Die angeforderten Daten konnten nicht geladen werden.
Bitte versuchen Sie es später nochmal.",PLAY_START:"Diaschau starten",PLAY_STOP:"Diaschau beenden",FULL_SCREEN:"Vollbild",THUMBS:"Vorschaubilder",DOWNLOAD:"Herunterladen",SHARE:"Teilen",ZOOM:"Vergrößern"}}},s=n(t),r=n(e),c=0,l=function(t){return t&&t.hasOwnProperty&&t instanceof n},d=function(){return t.requestAnimationFrame||t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||t.oRequestAnimationFrame||function(e){return t.setTimeout(e,1e3/60)}}(),u=function(){return t.cancelAnimationFrame||t.webkitCancelAnimationFrame||t.mozCancelAnimationFrame||t.oCancelAnimationFrame||function(e){t.clearTimeout(e)}}(),f=function(){var t,n=e.createElement("fakeelement"),o={transition:"transitionend",OTransition:"oTransitionEnd",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"};for(t in o)if(void 0!==n.style[t])return o[t];return"transitionend"}(),p=function(t){return t&&t.length&&t[0].offsetHeight},h=function(t,e){var o=n.extend(!0,{},t,e);return n.each(e,function(t,e){n.isArray(e)&&(o[t]=e)}),o},g=function(t){var o,i;return!(!t||t.ownerDocument!==e)&&(n(".fancybox-container").css("pointer-events","none"),o={x:t.getBoundingClientRect().left+t.offsetWidth/2,y:t.getBoundingClientRect().top+t.offsetHeight/2},i=e.elementFromPoint(o.x,o.y)===t,n(".fancybox-container").css("pointer-events",""),i)},b=function(t,e,o){var i=this;i.opts=h({index:o},n.fancybox.defaults),n.isPlainObject(e)&&(i.opts=h(i.opts,e)),n.fancybox.isMobile&&(i.opts=h(i.opts,i.opts.mobile)),i.id=i.opts.id||++c,i.currIndex=parseInt(i.opts.index,10)||0,i.prevIndex=null,i.prevPos=null,i.currPos=0,i.firstRun=!0,i.group=[],i.slides={},i.addContent(t),i.group.length&&i.init()};n.extend(b.prototype,{init:function(){var o,i,a=this,s=a.group[a.currIndex],r=s.opts;r.closeExisting&&n.fancybox.close(!0),n("body").addClass("fancybox-active"),!n.fancybox.getInstance()&&!1!==r.hideScrollbar&&!n.fancybox.isMobile&&e.body.scrollHeight>t.innerHeight&&(n("head").append('"),n("body").addClass("compensate-for-scrollbar")),i="",n.each(r.buttons,function(t,e){i+=r.btnTpl[e]||""}),o=n(a.translate(a,r.baseTpl.replace("{{buttons}}",i).replace("{{arrows}}",r.btnTpl.arrowLeft+r.btnTpl.arrowRight))).attr("id","fancybox-container-"+a.id).addClass(r.baseClass).data("FancyBox",a).appendTo(r.parentEl),a.$refs={container:o},["bg","inner","infobar","toolbar","stage","caption","navigation"].forEach(function(t){a.$refs[t]=o.find(".fancybox-"+t)}),a.trigger("onInit"),a.activate(),a.jumpTo(a.currIndex)},translate:function(t,e){var n=t.opts.i18n[t.opts.lang]||t.opts.i18n.en;return e.replace(/\{\{(\w+)\}\}/g,function(t,e){return void 0===n[e]?t:n[e]})},addContent:function(t){var e,o=this,i=n.makeArray(t);n.each(i,function(t,e){var i,a,s,r,c,l={},d={};n.isPlainObject(e)?(l=e,d=e.opts||e):"object"===n.type(e)&&n(e).length?(i=n(e),d=i.data()||{},d=n.extend(!0,{},d,d.options),d.$orig=i,l.src=o.opts.src||d.src||i.attr("href"),l.type||l.src||(l.type="inline",l.src=e)):l={type:"html",src:e+""},l.opts=n.extend(!0,{},o.opts,d),n.isArray(d.buttons)&&(l.opts.buttons=d.buttons),n.fancybox.isMobile&&l.opts.mobile&&(l.opts=h(l.opts,l.opts.mobile)),a=l.type||l.opts.type,r=l.src||"",!a&&r&&((s=r.match(/\.(mp4|mov|ogv|webm)((\?|#).*)?$/i))?(a="video",l.opts.video.format||(l.opts.video.format="video/"+("ogv"===s[1]?"ogg":s[1]))):r.match(/(^data:image\/[a-z0-9+\/=]*,)|(\.(jp(e|g|eg)|gif|png|bmp|webp|svg|ico)((\?|#).*)?$)/i)?a="image":r.match(/\.(pdf)((\?|#).*)?$/i)?(a="iframe",l=n.extend(!0,l,{contentType:"pdf",opts:{iframe:{preload:!1}}})):"#"===r.charAt(0)&&(a="inline")),a?l.type=a:o.trigger("objectNeedsType",l),l.contentType||(l.contentType=n.inArray(l.type,["html","inline","ajax"])>-1?"html":l.type),l.index=o.group.length,"auto"==l.opts.smallBtn&&(l.opts.smallBtn=n.inArray(l.type,["html","inline","ajax"])>-1),"auto"===l.opts.toolbar&&(l.opts.toolbar=!l.opts.smallBtn),l.$thumb=l.opts.$thumb||null,l.opts.$trigger&&l.index===o.opts.index&&(l.$thumb=l.opts.$trigger.find("img:first"),l.$thumb.length&&(l.opts.$orig=l.opts.$trigger)),l.$thumb&&l.$thumb.length||!l.opts.$orig||(l.$thumb=l.opts.$orig.find("img:first")),l.$thumb&&!l.$thumb.length&&(l.$thumb=null),l.thumb=l.opts.thumb||(l.$thumb?l.$thumb[0].src:null),"function"===n.type(l.opts.caption)&&(l.opts.caption=l.opts.caption.apply(e,[o,l])),"function"===n.type(o.opts.caption)&&(l.opts.caption=o.opts.caption.apply(e,[o,l])),l.opts.caption instanceof n||(l.opts.caption=void 0===l.opts.caption?"":l.opts.caption+""),"ajax"===l.type&&(c=r.split(/\s+/,2),c.length>1&&(l.src=c.shift(),l.opts.filter=c.shift())),l.opts.modal&&(l.opts=n.extend(!0,l.opts,{trapFocus:!0,infobar:0,toolbar:0,smallBtn:0,keyboard:0,slideShow:0,fullScreen:0,thumbs:0,touch:0,clickContent:!1,clickSlide:!1,clickOutside:!1,dblclickContent:!1,dblclickSlide:!1,dblclickOutside:!1})),o.group.push(l)}),Object.keys(o.slides).length&&(o.updateControls(),(e=o.Thumbs)&&e.isActive&&(e.create(),e.focus()))},addEvents:function(){var e=this;e.removeEvents(),e.$refs.container.on("click.fb-close","[data-fancybox-close]",function(t){t.stopPropagation(),t.preventDefault(),e.close(t)}).on("touchstart.fb-prev click.fb-prev","[data-fancybox-prev]",function(t){t.stopPropagation(),t.preventDefault(),e.previous()}).on("touchstart.fb-next click.fb-next","[data-fancybox-next]",function(t){t.stopPropagation(),t.preventDefault(),e.next()}).on("click.fb","[data-fancybox-zoom]",function(t){e[e.isScaledDown()?"scaleToActual":"scaleToFit"]()}),s.on("orientationchange.fb resize.fb",function(t){t&&t.originalEvent&&"resize"===t.originalEvent.type?(e.requestId&&u(e.requestId),e.requestId=d(function(){e.update(t)})):(e.current&&"iframe"===e.current.type&&e.$refs.stage.hide(),setTimeout(function(){e.$refs.stage.show(),e.update(t)},n.fancybox.isMobile?600:250))}),r.on("keydown.fb",function(t){var o=n.fancybox?n.fancybox.getInstance():null,i=o.current,a=t.keyCode||t.which;if(9==a)return void(i.opts.trapFocus&&e.focus(t));if(!(!i.opts.keyboard||t.ctrlKey||t.altKey||t.shiftKey||n(t.target).is("input,textarea,video,audio,select")))return 8===a||27===a?(t.preventDefault(),void e.close(t)):37===a||38===a?(t.preventDefault(),void e.previous()):39===a||40===a?(t.preventDefault(),void e.next()):void e.trigger("afterKeydown",t,a)}),e.group[e.currIndex].opts.idleTime&&(e.idleSecondsCounter=0,r.on("mousemove.fb-idle mouseleave.fb-idle mousedown.fb-idle touchstart.fb-idle touchmove.fb-idle scroll.fb-idle keydown.fb-idle",function(t){e.idleSecondsCounter=0,e.isIdle&&e.showControls(),e.isIdle=!1}),e.idleInterval=t.setInterval(function(){++e.idleSecondsCounter>=e.group[e.currIndex].opts.idleTime&&!e.isDragging&&(e.isIdle=!0,e.idleSecondsCounter=0,e.hideControls())},1e3))},removeEvents:function(){var e=this;s.off("orientationchange.fb resize.fb"),r.off("keydown.fb .fb-idle"),this.$refs.container.off(".fb-close .fb-prev .fb-next"),e.idleInterval&&(t.clearInterval(e.idleInterval),e.idleInterval=null)},previous:function(t){return this.jumpTo(this.currPos-1,t)},next:function(t){return this.jumpTo(this.currPos+1,t)},jumpTo:function(t,e){var o,i,a,s,r,c,l,d,u,f=this,h=f.group.length;if(!(f.isDragging||f.isClosing||f.isAnimating&&f.firstRun)){if(t=parseInt(t,10),!(a=f.current?f.current.opts.loop:f.opts.loop)&&(t<0||t>=h))return!1;if(o=f.firstRun=!Object.keys(f.slides).length,r=f.current,f.prevIndex=f.currIndex,f.prevPos=f.currPos,s=f.createSlide(t),h>1&&((a||s.index0)&&f.createSlide(t-1)),f.current=s,f.currIndex=s.index,f.currPos=s.pos,f.trigger("beforeShow",o),f.updateControls(),s.forcedDuration=void 0,n.isNumeric(e)?s.forcedDuration=e:e=s.opts[o?"animationDuration":"transitionDuration"],e=parseInt(e,10),i=f.isMoved(s),s.$slide.addClass("fancybox-slide--current"),o)return s.opts.animationEffect&&e&&f.$refs.container.css("transition-duration",e+"ms"),f.$refs.container.addClass("fancybox-is-open").trigger("focus"),f.loadSlide(s),void f.preload("image");c=n.fancybox.getTranslate(r.$slide),l=n.fancybox.getTranslate(f.$refs.stage),n.each(f.slides,function(t,e){n.fancybox.stop(e.$slide,!0)}),r.pos!==s.pos&&(r.isComplete=!1),r.$slide.removeClass("fancybox-slide--complete fancybox-slide--current"),i?(u=c.left-(r.pos*c.width+r.pos*r.opts.gutter),n.each(f.slides,function(t,o){o.$slide.removeClass("fancybox-animated").removeClass(function(t,e){return(e.match(/(^|\s)fancybox-fx-\S+/g)||[]).join(" ")});var i=o.pos*c.width+o.pos*o.opts.gutter;n.fancybox.setTranslate(o.$slide,{top:0,left:i-l.left+u}),o.pos!==s.pos&&o.$slide.addClass("fancybox-slide--"+(o.pos>s.pos?"next":"previous")),p(o.$slide),n.fancybox.animate(o.$slide,{top:0,left:(o.pos-s.pos)*c.width+(o.pos-s.pos)*o.opts.gutter},e,function(){o.$slide.css({transform:"",opacity:""}).removeClass("fancybox-slide--next fancybox-slide--previous"),o.pos===f.currPos&&f.complete()})})):e&&s.opts.transitionEffect&&(d="fancybox-animated fancybox-fx-"+s.opts.transitionEffect,r.$slide.addClass("fancybox-slide--"+(r.pos>s.pos?"next":"previous")),n.fancybox.animate(r.$slide,d,e,function(){r.$slide.removeClass(d).removeClass("fancybox-slide--next fancybox-slide--previous")},!1)),s.isLoaded?f.revealContent(s):f.loadSlide(s),f.preload("image")}},createSlide:function(t){var e,o,i=this;return o=t%i.group.length,o=o<0?i.group.length+o:o,!i.slides[t]&&i.group[o]&&(e=n('
').appendTo(i.$refs.stage),i.slides[t]=n.extend(!0,{},i.group[o],{pos:t,$slide:e,isLoaded:!1}),i.updateSlide(i.slides[t])),i.slides[t]},scaleToActual:function(t,e,o){var i,a,s,r,c,l=this,d=l.current,u=d.$content,f=n.fancybox.getTranslate(d.$slide).width,p=n.fancybox.getTranslate(d.$slide).height,h=d.width,g=d.height;l.isAnimating||l.isMoved()||!u||"image"!=d.type||!d.isLoaded||d.hasError||(l.isAnimating=!0,n.fancybox.stop(u),t=void 0===t?.5*f:t,e=void 0===e?.5*p:e,i=n.fancybox.getTranslate(u),i.top-=n.fancybox.getTranslate(d.$slide).top,i.left-=n.fancybox.getTranslate(d.$slide).left,r=h/i.width,c=g/i.height,a=.5*f-.5*h,s=.5*p-.5*g,h>f&&(a=i.left*r-(t*r-t),a>0&&(a=0),ap&&(s=i.top*c-(e*c-e),s>0&&(s=0),se-.5&&(l=e),d>o-.5&&(d=o),"image"===t.type?(u.top=Math.floor(.5*(o-d))+parseFloat(c.css("paddingTop")),u.left=Math.floor(.5*(e-l))+parseFloat(c.css("paddingLeft"))):"video"===t.contentType&&(a=t.opts.width&&t.opts.height?l/d:t.opts.ratio||16/9,d>l/a?d=l/a:l>d*a&&(l=d*a)),u.width=l,u.height=d,u)},update:function(t){var e=this;n.each(e.slides,function(n,o){e.updateSlide(o,t)})},updateSlide:function(t,e){var o=this,i=t&&t.$content,a=t.width||t.opts.width,s=t.height||t.opts.height,r=t.$slide;o.adjustCaption(t),i&&(a||s||"video"===t.contentType)&&!t.hasError&&(n.fancybox.stop(i),n.fancybox.setTranslate(i,o.getFitPos(t)),t.pos===o.currPos&&(o.isAnimating=!1,o.updateCursor())),o.adjustLayout(t),r.length&&(r.trigger("refresh"),t.pos===o.currPos&&o.$refs.toolbar.add(o.$refs.navigation.find(".fancybox-button--arrow_right")).toggleClass("compensate-for-scrollbar",r.get(0).scrollHeight>r.get(0).clientHeight)),o.trigger("onUpdate",t,e)},centerSlide:function(t){var e=this,o=e.current,i=o.$slide;!e.isClosing&&o&&(i.siblings().css({transform:"",opacity:""}),i.parent().children().removeClass("fancybox-slide--previous fancybox-slide--next"),n.fancybox.animate(i,{top:0,left:0,opacity:1},void 0===t?0:t,function(){i.css({transform:"",opacity:""}),o.isComplete||e.complete()},!1))},isMoved:function(t){var e,o,i=t||this.current;return!!i&&(o=n.fancybox.getTranslate(this.$refs.stage),e=n.fancybox.getTranslate(i.$slide),!i.$slide.hasClass("fancybox-animated")&&(Math.abs(e.top-o.top)>.5||Math.abs(e.left-o.left)>.5))},updateCursor:function(t,e){var o,i,a=this,s=a.current,r=a.$refs.container;s&&!a.isClosing&&a.Guestures&&(r.removeClass("fancybox-is-zoomable fancybox-can-zoomIn fancybox-can-zoomOut fancybox-can-swipe fancybox-can-pan"),o=a.canPan(t,e),i=!!o||a.isZoomable(),r.toggleClass("fancybox-is-zoomable",i),n("[data-fancybox-zoom]").prop("disabled",!i),o?r.addClass("fancybox-can-pan"):i&&("zoom"===s.opts.clickContent||n.isFunction(s.opts.clickContent)&&"zoom"==s.opts.clickContent(s))?r.addClass("fancybox-can-zoomIn"):s.opts.touch&&(s.opts.touch.vertical||a.group.length>1)&&"video"!==s.contentType&&r.addClass("fancybox-can-swipe"))},isZoomable:function(){var t,e=this,n=e.current;if(n&&!e.isClosing&&"image"===n.type&&!n.hasError){if(!n.isLoaded)return!0;if((t=e.getFitPos(n))&&(n.width>t.width||n.height>t.height))return!0}return!1},isScaledDown:function(t,e){var o=this,i=!1,a=o.current,s=a.$content;return void 0!==t&&void 0!==e?i=t1.5||Math.abs(a.height-s.height)>1.5)),s},loadSlide:function(t){var e,o,i,a=this;if(!t.isLoading&&!t.isLoaded){if(t.isLoading=!0,!1===a.trigger("beforeLoad",t))return t.isLoading=!1,!1;switch(e=t.type,o=t.$slide,o.off("refresh").trigger("onReset").addClass(t.opts.slideClass),e){case"image":a.setImage(t);break;case"iframe":a.setIframe(t);break;case"html":a.setContent(t,t.src||t.content);break;case"video":a.setContent(t,t.opts.video.tpl.replace(/\{\{src\}\}/gi,t.src).replace("{{format}}",t.opts.videoFormat||t.opts.video.format||"").replace("{{poster}}",t.thumb||""));break;case"inline":n(t.src).length?a.setContent(t,n(t.src)):a.setError(t);break;case"ajax":a.showLoading(t),i=n.ajax(n.extend({},t.opts.ajax.settings,{url:t.src,success:function(e,n){"success"===n&&a.setContent(t,e)},error:function(e,n){e&&"abort"!==n&&a.setError(t)}})),o.one("onReset",function(){i.abort()});break;default:a.setError(t)}return!0}},setImage:function(t){var o,i=this;setTimeout(function(){var e=t.$image;i.isClosing||!t.isLoading||e&&e.length&&e[0].complete||t.hasError||i.showLoading(t)},50),i.checkSrcset(t),t.$content=n('
').addClass("fancybox-is-hidden").appendTo(t.$slide.addClass("fancybox-slide--image")),!1!==t.opts.preload&&t.opts.width&&t.opts.height&&t.thumb&&(t.width=t.opts.width,t.height=t.opts.height,o=e.createElement("img"),o.onerror=function(){n(this).remove(),t.$ghost=null},o.onload=function(){i.afterLoad(t)},t.$ghost=n(o).addClass("fancybox-image").appendTo(t.$content).attr("src",t.thumb)),i.setBigImage(t)},checkSrcset:function(e){var n,o,i,a,s=e.opts.srcset||e.opts.image.srcset;if(s){i=t.devicePixelRatio||1,a=t.innerWidth*i,o=s.split(",").map(function(t){var e={};return t.trim().split(/\s+/).forEach(function(t,n){var o=parseInt(t.substring(0,t.length-1),10);if(0===n)return e.url=t;o&&(e.value=o,e.postfix=t[t.length-1])}),e}),o.sort(function(t,e){return t.value-e.value});for(var r=0;r=a||"x"===c.postfix&&c.value>=i){n=c;break}}!n&&o.length&&(n=o[o.length-1]),n&&(e.src=n.url,e.width&&e.height&&"w"==n.postfix&&(e.height=e.width/e.height*n.value,e.width=n.value),e.opts.srcset=s)}},setBigImage:function(t){var o=this,i=e.createElement("img"),a=n(i);t.$image=a.one("error",function(){o.setError(t)}).one("load",function(){var e;t.$ghost||(o.resolveImageSlideSize(t,this.naturalWidth,this.naturalHeight),o.afterLoad(t)),o.isClosing||(t.opts.srcset&&(e=t.opts.sizes,e&&"auto"!==e||(e=(t.width/t.height>1&&s.width()/s.height()>1?"100":Math.round(t.width/t.height*100))+"vw"),a.attr("sizes",e).attr("srcset",t.opts.srcset)),t.$ghost&&setTimeout(function(){t.$ghost&&!o.isClosing&&t.$ghost.hide()},Math.min(300,Math.max(1e3,t.height/1600))),o.hideLoading(t))}).addClass("fancybox-image").attr("src",t.src).appendTo(t.$content),(i.complete||"complete"==i.readyState)&&a.naturalWidth&&a.naturalHeight?a.trigger("load"):i.error&&a.trigger("error")},resolveImageSlideSize:function(t,e,n){var o=parseInt(t.opts.width,10),i=parseInt(t.opts.height,10);t.width=e,t.height=n,o>0&&(t.width=o,t.height=Math.floor(o*n/e)),i>0&&(t.width=Math.floor(i*e/n),t.height=i)},setIframe:function(t){var e,o=this,i=t.opts.iframe,a=t.$slide;t.$content=n('
').css(i.css).appendTo(a),a.addClass("fancybox-slide--"+t.contentType),t.$iframe=e=n(i.tpl.replace(/\{rnd\}/g,(new Date).getTime())).attr(i.attr).appendTo(t.$content),i.preload?(o.showLoading(t),e.on("load.fb error.fb",function(e){this.isReady=1,t.$slide.trigger("refresh"),o.afterLoad(t)}),a.on("refresh.fb",function(){var n,o,s=t.$content,r=i.css.width,c=i.css.height;if(1===e[0].isReady){try{n=e.contents(),o=n.find("body")}catch(t){}o&&o.length&&o.children().length&&(a.css("overflow","visible"),s.css({width:"100%","max-width":"100%",height:"9999px"}),void 0===r&&(r=Math.ceil(Math.max(o[0].clientWidth,o.outerWidth(!0)))),s.css("width",r||"").css("max-width",""),void 0===c&&(c=Math.ceil(Math.max(o[0].clientHeight,o.outerHeight(!0)))),s.css("height",c||""),a.css("overflow","auto")),s.removeClass("fancybox-is-hidden")}})):o.afterLoad(t),e.attr("src",t.src),a.one("onReset",function(){try{n(this).find("iframe").hide().unbind().attr("src","//about:blank")}catch(t){}n(this).off("refresh.fb").empty(),t.isLoaded=!1,t.isRevealed=!1})},setContent:function(t,e){var o=this;o.isClosing||(o.hideLoading(t),t.$content&&n.fancybox.stop(t.$content),t.$slide.empty(),l(e)&&e.parent().length?((e.hasClass("fancybox-content")||e.parent().hasClass("fancybox-content"))&&e.parents(".fancybox-slide").trigger("onReset"),t.$placeholder=n("
").hide().insertAfter(e),e.css("display","inline-block")):t.hasError||("string"===n.type(e)&&(e=n("
").append(n.trim(e)).contents()),t.opts.filter&&(e=n("
").html(e).find(t.opts.filter))),t.$slide.one("onReset",function(){n(this).find("video,audio").trigger("pause"),t.$placeholder&&(t.$placeholder.after(e.removeClass("fancybox-content").hide()).remove(),t.$placeholder=null),t.$smallBtn&&(t.$smallBtn.remove(),t.$smallBtn=null),t.hasError||(n(this).empty(),t.isLoaded=!1,t.isRevealed=!1)}),n(e).appendTo(t.$slide),n(e).is("video,audio")&&(n(e).addClass("fancybox-video"),n(e).wrap("
"),t.contentType="video",t.opts.width=t.opts.width||n(e).attr("width"),t.opts.height=t.opts.height||n(e).attr("height")),t.$content=t.$slide.children().filter("div,form,main,video,audio,article,.fancybox-content").first(),t.$content.siblings().hide(),t.$content.length||(t.$content=t.$slide.wrapInner("
").children().first()),t.$content.addClass("fancybox-content"),t.$slide.addClass("fancybox-slide--"+t.contentType),o.afterLoad(t))},setError:function(t){t.hasError=!0,t.$slide.trigger("onReset").removeClass("fancybox-slide--"+t.contentType).addClass("fancybox-slide--error"),t.contentType="html",this.setContent(t,this.translate(t,t.opts.errorTpl)),t.pos===this.currPos&&(this.isAnimating=!1)},showLoading:function(t){var e=this;(t=t||e.current)&&!t.$spinner&&(t.$spinner=n(e.translate(e,e.opts.spinnerTpl)).appendTo(t.$slide).hide().fadeIn("fast"))},hideLoading:function(t){var e=this;(t=t||e.current)&&t.$spinner&&(t.$spinner.stop().remove(),delete t.$spinner)},afterLoad:function(t){var e=this;e.isClosing||(t.isLoading=!1,t.isLoaded=!0,e.trigger("afterLoad",t),e.hideLoading(t),!t.opts.smallBtn||t.$smallBtn&&t.$smallBtn.length||(t.$smallBtn=n(e.translate(t,t.opts.btnTpl.smallBtn)).appendTo(t.$content)),t.opts.protect&&t.$content&&!t.hasError&&(t.$content.on("contextmenu.fb",function(t){return 2==t.button&&t.preventDefault(),!0}),"image"===t.type&&n('
').appendTo(t.$content)),e.adjustCaption(t),e.adjustLayout(t),t.pos===e.currPos&&e.updateCursor(),e.revealContent(t))},adjustCaption:function(t){var e,n=this,o=t||n.current,i=o.opts.caption,a=o.opts.preventCaptionOverlap,s=n.$refs.caption,r=!1;s.toggleClass("fancybox-caption--separate",a),a&&i&&i.length&&(o.pos!==n.currPos?(e=s.clone().appendTo(s.parent()),e.children().eq(0).empty().html(i),r=e.outerHeight(!0),e.empty().remove()):n.$caption&&(r=n.$caption.outerHeight(!0)),o.$slide.css("padding-bottom",r||""))},adjustLayout:function(t){var e,n,o,i,a=this,s=t||a.current;s.isLoaded&&!0!==s.opts.disableLayoutFix&&(s.$content.css("margin-bottom",""),s.$content.outerHeight()>s.$slide.height()+.5&&(o=s.$slide[0].style["padding-bottom"],i=s.$slide.css("padding-bottom"),parseFloat(i)>0&&(e=s.$slide[0].scrollHeight,s.$slide.css("padding-bottom",0),Math.abs(e-s.$slide[0].scrollHeight)<1&&(n=i),s.$slide.css("padding-bottom",o))),s.$content.css("margin-bottom",n))},revealContent:function(t){var e,o,i,a,s=this,r=t.$slide,c=!1,l=!1,d=s.isMoved(t),u=t.isRevealed;return t.isRevealed=!0,e=t.opts[s.firstRun?"animationEffect":"transitionEffect"],i=t.opts[s.firstRun?"animationDuration":"transitionDuration"],i=parseInt(void 0===t.forcedDuration?i:t.forcedDuration,10),!d&&t.pos===s.currPos&&i||(e=!1),"zoom"===e&&(t.pos===s.currPos&&i&&"image"===t.type&&!t.hasError&&(l=s.getThumbPos(t))?c=s.getFitPos(t):e="fade"),"zoom"===e?(s.isAnimating=!0,c.scaleX=c.width/l.width,c.scaleY=c.height/l.height,a=t.opts.zoomOpacity,"auto"==a&&(a=Math.abs(t.width/t.height-l.width/l.height)>.1),a&&(l.opacity=.1,c.opacity=1),n.fancybox.setTranslate(t.$content.removeClass("fancybox-is-hidden"),l),p(t.$content),void n.fancybox.animate(t.$content,c,i,function(){s.isAnimating=!1,s.complete()})):(s.updateSlide(t),e?(n.fancybox.stop(r),o="fancybox-slide--"+(t.pos>=s.prevPos?"next":"previous")+" fancybox-animated fancybox-fx-"+e,r.addClass(o).removeClass("fancybox-slide--current"),t.$content.removeClass("fancybox-is-hidden"),p(r),"image"!==t.type&&t.$content.hide().show(0),void n.fancybox.animate(r,"fancybox-slide--current",i,function(){r.removeClass(o).css({transform:"",opacity:""}),t.pos===s.currPos&&s.complete()},!0)):(t.$content.removeClass("fancybox-is-hidden"),u||!d||"image"!==t.type||t.hasError||t.$content.hide().fadeIn("fast"),void(t.pos===s.currPos&&s.complete())))},getThumbPos:function(t){var e,o,i,a,s,r=!1,c=t.$thumb;return!(!c||!g(c[0]))&&(e=n.fancybox.getTranslate(c),o=parseFloat(c.css("border-top-width")||0),i=parseFloat(c.css("border-right-width")||0),a=parseFloat(c.css("border-bottom-width")||0),s=parseFloat(c.css("border-left-width")||0),r={top:e.top+o,left:e.left+s,width:e.width-i-s,height:e.height-o-a,scaleX:1,scaleY:1},e.width>0&&e.height>0&&r)},complete:function(){var t,e=this,o=e.current,i={};!e.isMoved()&&o.isLoaded&&(o.isComplete||(o.isComplete=!0,o.$slide.siblings().trigger("onReset"),e.preload("inline"),p(o.$slide),o.$slide.addClass("fancybox-slide--complete"),n.each(e.slides,function(t,o){o.pos>=e.currPos-1&&o.pos<=e.currPos+1?i[o.pos]=o:o&&(n.fancybox.stop(o.$slide),o.$slide.off().remove())}),e.slides=i),e.isAnimating=!1,e.updateCursor(),e.trigger("afterShow"),o.opts.video.autoStart&&o.$slide.find("video,audio").filter(":visible:first").trigger("play").one("ended",function(){Document.exitFullscreen?Document.exitFullscreen():this.webkitExitFullscreen&&this.webkitExitFullscreen(),e.next()}),o.opts.autoFocus&&"html"===o.contentType&&(t=o.$content.find("input[autofocus]:enabled:visible:first"),t.length?t.trigger("focus"):e.focus(null,!0)),o.$slide.scrollTop(0).scrollLeft(0))},preload:function(t){var e,n,o=this;o.group.length<2||(n=o.slides[o.currPos+1],e=o.slides[o.currPos-1],e&&e.type===t&&o.loadSlide(e),n&&n.type===t&&o.loadSlide(n))},focus:function(t,o){var i,a,s=this,r=["a[href]","area[href]",'input:not([disabled]):not([type="hidden"]):not([aria-hidden])',"select:not([disabled]):not([aria-hidden])","textarea:not([disabled]):not([aria-hidden])","button:not([disabled]):not([aria-hidden])","iframe","object","embed","video","audio","[contenteditable]",'[tabindex]:not([tabindex^="-"])'].join(",");s.isClosing||(i=!t&&s.current&&s.current.isComplete?s.current.$slide.find("*:visible"+(o?":not(.fancybox-close-small)":"")):s.$refs.container.find("*:visible"),i=i.filter(r).filter(function(){return"hidden"!==n(this).css("visibility")&&!n(this).hasClass("disabled")}),i.length?(a=i.index(e.activeElement),t&&t.shiftKey?(a<0||0==a)&&(t.preventDefault(),i.eq(i.length-1).trigger("focus")):(a<0||a==i.length-1)&&(t&&t.preventDefault(),i.eq(0).trigger("focus"))):s.$refs.container.trigger("focus"))},activate:function(){var t=this;n(".fancybox-container").each(function(){var e=n(this).data("FancyBox");e&&e.id!==t.id&&!e.isClosing&&(e.trigger("onDeactivate"),e.removeEvents(),e.isVisible=!1)}),t.isVisible=!0,(t.current||t.isIdle)&&(t.update(),t.updateControls()),t.trigger("onActivate"),t.addEvents()},close:function(t,e){var o,i,a,s,r,c,l,u=this,f=u.current,h=function(){u.cleanUp(t)};return!u.isClosing&&(u.isClosing=!0,!1===u.trigger("beforeClose",t)?(u.isClosing=!1,d(function(){u.update()}),!1):(u.removeEvents(),a=f.$content,o=f.opts.animationEffect,i=n.isNumeric(e)?e:o?f.opts.animationDuration:0,f.$slide.removeClass("fancybox-slide--complete fancybox-slide--next fancybox-slide--previous fancybox-animated"),!0!==t?n.fancybox.stop(f.$slide):o=!1,f.$slide.siblings().trigger("onReset").remove(),i&&u.$refs.container.removeClass("fancybox-is-open").addClass("fancybox-is-closing").css("transition-duration",i+"ms"),u.hideLoading(f),u.hideControls(!0),u.updateCursor(),"zoom"!==o||a&&i&&"image"===f.type&&!u.isMoved()&&!f.hasError&&(l=u.getThumbPos(f))||(o="fade"),"zoom"===o?(n.fancybox.stop(a),s=n.fancybox.getTranslate(a),c={top:s.top,left:s.left,scaleX:s.width/l.width,scaleY:s.height/l.height,width:l.width,height:l.height},r=f.opts.zoomOpacity, + "auto"==r&&(r=Math.abs(f.width/f.height-l.width/l.height)>.1),r&&(l.opacity=0),n.fancybox.setTranslate(a,c),p(a),n.fancybox.animate(a,l,i,h),!0):(o&&i?n.fancybox.animate(f.$slide.addClass("fancybox-slide--previous").removeClass("fancybox-slide--current"),"fancybox-animated fancybox-fx-"+o,i,h):!0===t?setTimeout(h,i):h(),!0)))},cleanUp:function(e){var o,i,a,s=this,r=s.current.opts.$orig;s.current.$slide.trigger("onReset"),s.$refs.container.empty().remove(),s.trigger("afterClose",e),s.current.opts.backFocus&&(r&&r.length&&r.is(":visible")||(r=s.$trigger),r&&r.length&&(i=t.scrollX,a=t.scrollY,r.trigger("focus"),n("html, body").scrollTop(a).scrollLeft(i))),s.current=null,o=n.fancybox.getInstance(),o?o.activate():(n("body").removeClass("fancybox-active compensate-for-scrollbar"),n("#fancybox-style-noscroll").remove())},trigger:function(t,e){var o,i=Array.prototype.slice.call(arguments,1),a=this,s=e&&e.opts?e:a.current;if(s?i.unshift(s):s=a,i.unshift(a),n.isFunction(s.opts[t])&&(o=s.opts[t].apply(s,i)),!1===o)return o;"afterClose"!==t&&a.$refs?a.$refs.container.trigger(t+".fb",i):r.trigger(t+".fb",i)},updateControls:function(){var t=this,o=t.current,i=o.index,a=t.$refs.container,s=t.$refs.caption,r=o.opts.caption;o.$slide.trigger("refresh"),r&&r.length?(t.$caption=s,s.children().eq(0).html(r)):t.$caption=null,t.hasHiddenControls||t.isIdle||t.showControls(),a.find("[data-fancybox-count]").html(t.group.length),a.find("[data-fancybox-index]").html(i+1),a.find("[data-fancybox-prev]").prop("disabled",!o.opts.loop&&i<=0),a.find("[data-fancybox-next]").prop("disabled",!o.opts.loop&&i>=t.group.length-1),"image"===o.type?a.find("[data-fancybox-zoom]").show().end().find("[data-fancybox-download]").attr("href",o.opts.image.src||o.src).show():o.opts.toolbar&&a.find("[data-fancybox-download],[data-fancybox-zoom]").hide(),n(e.activeElement).is(":hidden,[disabled]")&&t.$refs.container.trigger("focus")},hideControls:function(t){var e=this,n=["infobar","toolbar","nav"];!t&&e.current.opts.preventCaptionOverlap||n.push("caption"),this.$refs.container.removeClass(n.map(function(t){return"fancybox-show-"+t}).join(" ")),this.hasHiddenControls=!0},showControls:function(){var t=this,e=t.current?t.current.opts:t.opts,n=t.$refs.container;t.hasHiddenControls=!1,t.idleSecondsCounter=0,n.toggleClass("fancybox-show-toolbar",!(!e.toolbar||!e.buttons)).toggleClass("fancybox-show-infobar",!!(e.infobar&&t.group.length>1)).toggleClass("fancybox-show-caption",!!t.$caption).toggleClass("fancybox-show-nav",!!(e.arrows&&t.group.length>1)).toggleClass("fancybox-is-modal",!!e.modal)},toggleControls:function(){this.hasHiddenControls?this.showControls():this.hideControls()}}),n.fancybox={version:"3.5.7",defaults:a,getInstance:function(t){var e=n('.fancybox-container:not(".fancybox-is-closing"):last').data("FancyBox"),o=Array.prototype.slice.call(arguments,1);return e instanceof b&&("string"===n.type(t)?e[t].apply(e,o):"function"===n.type(t)&&t.apply(e,o),e)},open:function(t,e,n){return new b(t,e,n)},close:function(t){var e=this.getInstance();e&&(e.close(),!0===t&&this.close(t))},destroy:function(){this.close(!0),r.add("body").off("click.fb-start","**")},isMobile:/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),use3d:function(){var n=e.createElement("div");return t.getComputedStyle&&t.getComputedStyle(n)&&t.getComputedStyle(n).getPropertyValue("transform")&&!(e.documentMode&&e.documentMode<11)}(),getTranslate:function(t){var e;return!(!t||!t.length)&&(e=t[0].getBoundingClientRect(),{top:e.top||0,left:e.left||0,width:e.width,height:e.height,opacity:parseFloat(t.css("opacity"))})},setTranslate:function(t,e){var n="",o={};if(t&&e)return void 0===e.left&&void 0===e.top||(n=(void 0===e.left?t.position().left:e.left)+"px, "+(void 0===e.top?t.position().top:e.top)+"px",n=this.use3d?"translate3d("+n+", 0px)":"translate("+n+")"),void 0!==e.scaleX&&void 0!==e.scaleY?n+=" scale("+e.scaleX+", "+e.scaleY+")":void 0!==e.scaleX&&(n+=" scaleX("+e.scaleX+")"),n.length&&(o.transform=n),void 0!==e.opacity&&(o.opacity=e.opacity),void 0!==e.width&&(o.width=e.width),void 0!==e.height&&(o.height=e.height),t.css(o)},animate:function(t,e,o,i,a){var s,r=this;n.isFunction(o)&&(i=o,o=null),r.stop(t),s=r.getTranslate(t),t.on(f,function(c){(!c||!c.originalEvent||t.is(c.originalEvent.target)&&"z-index"!=c.originalEvent.propertyName)&&(r.stop(t),n.isNumeric(o)&&t.css("transition-duration",""),n.isPlainObject(e)?void 0!==e.scaleX&&void 0!==e.scaleY&&r.setTranslate(t,{top:e.top,left:e.left,width:s.width*e.scaleX,height:s.height*e.scaleY,scaleX:1,scaleY:1}):!0!==a&&t.removeClass(e),n.isFunction(i)&&i(c))}),n.isNumeric(o)&&t.css("transition-duration",o+"ms"),n.isPlainObject(e)?(void 0!==e.scaleX&&void 0!==e.scaleY&&(delete e.width,delete e.height,t.parent().hasClass("fancybox-slide--image")&&t.parent().addClass("fancybox-is-scaling")),n.fancybox.setTranslate(t,e)):t.addClass(e),t.data("timer",setTimeout(function(){t.trigger(f)},o+33))},stop:function(t,e){t&&t.length&&(clearTimeout(t.data("timer")),e&&t.trigger(f),t.off(f).css("transition-duration",""),t.parent().removeClass("fancybox-is-scaling"))}},n.fn.fancybox=function(t){var e;return t=t||{},e=t.selector||!1,e?n("body").off("click.fb-start",e).on("click.fb-start",e,{options:t},i):this.off("click.fb-start").on("click.fb-start",{items:this,options:t},i),this},r.on("click.fb-start","[data-fancybox]",i),r.on("click.fb-start","[data-fancybox-trigger]",function(t){n('[data-fancybox="'+n(this).attr("data-fancybox-trigger")+'"]').eq(n(this).attr("data-fancybox-index")||0).trigger("click.fb-start",{$trigger:n(this)})}),function(){var t=null;r.on("mousedown mouseup focus blur",".fancybox-button",function(e){switch(e.type){case"mousedown":t=n(this);break;case"mouseup":t=null;break;case"focusin":n(".fancybox-button").removeClass("fancybox-focus"),n(this).is(t)||n(this).is("[disabled]")||n(this).addClass("fancybox-focus");break;case"focusout":n(".fancybox-button").removeClass("fancybox-focus")}})}()}}(window,document,jQuery),function(t){"use strict";var e={youtube:{matcher:/(youtube\.com|youtu\.be|youtube\-nocookie\.com)\/(watch\?(.*&)?v=|v\/|u\/|embed\/?)?(videoseries\?list=(.*)|[\w-]{11}|\?listType=(.*)&list=(.*))(.*)/i,params:{autoplay:1,autohide:1,fs:1,rel:0,hd:1,wmode:"transparent",enablejsapi:1,html5:1},paramPlace:8,type:"iframe",url:"https://www.youtube-nocookie.com/embed/$4",thumb:"https://img.youtube.com/vi/$4/hqdefault.jpg"},vimeo:{matcher:/^.+vimeo.com\/(.*\/)?([\d]+)(.*)?/,params:{autoplay:1,hd:1,show_title:1,show_byline:1,show_portrait:0,fullscreen:1},paramPlace:3,type:"iframe",url:"//player.vimeo.com/video/$2"},instagram:{matcher:/(instagr\.am|instagram\.com)\/p\/([a-zA-Z0-9_\-]+)\/?/i,type:"image",url:"//$1/p/$2/media/?size=l"},gmap_place:{matcher:/(maps\.)?google\.([a-z]{2,3}(\.[a-z]{2})?)\/(((maps\/(place\/(.*)\/)?\@(.*),(\d+.?\d+?)z))|(\?ll=))(.*)?/i,type:"iframe",url:function(t){return"//maps.google."+t[2]+"/?ll="+(t[9]?t[9]+"&z="+Math.floor(t[10])+(t[12]?t[12].replace(/^\//,"&"):""):t[12]+"").replace(/\?/,"&")+"&output="+(t[12]&&t[12].indexOf("layer=c")>0?"svembed":"embed")}},gmap_search:{matcher:/(maps\.)?google\.([a-z]{2,3}(\.[a-z]{2})?)\/(maps\/search\/)(.*)/i,type:"iframe",url:function(t){return"//maps.google."+t[2]+"/maps?q="+t[5].replace("query=","q=").replace("api=1","")+"&output=embed"}}},n=function(e,n,o){if(e)return o=o||"","object"===t.type(o)&&(o=t.param(o,!0)),t.each(n,function(t,n){e=e.replace("$"+t,n||"")}),o.length&&(e+=(e.indexOf("?")>0?"&":"?")+o),e};t(document).on("objectNeedsType.fb",function(o,i,a){var s,r,c,l,d,u,f,p=a.src||"",h=!1;s=t.extend(!0,{},e,a.opts.media),t.each(s,function(e,o){if(c=p.match(o.matcher)){if(h=o.type,f=e,u={},o.paramPlace&&c[o.paramPlace]){d=c[o.paramPlace],"?"==d[0]&&(d=d.substring(1)),d=d.split("&");for(var i=0;i1&&("youtube"===n.contentSource||"vimeo"===n.contentSource)&&o.load(n.contentSource)}})}(jQuery),function(t,e,n){"use strict";var o=function(){return t.requestAnimationFrame||t.webkitRequestAnimationFrame||t.mozRequestAnimationFrame||t.oRequestAnimationFrame||function(e){return t.setTimeout(e,1e3/60)}}(),i=function(){return t.cancelAnimationFrame||t.webkitCancelAnimationFrame||t.mozCancelAnimationFrame||t.oCancelAnimationFrame||function(e){t.clearTimeout(e)}}(),a=function(e){var n=[];e=e.originalEvent||e||t.e,e=e.touches&&e.touches.length?e.touches:e.changedTouches&&e.changedTouches.length?e.changedTouches:[e];for(var o in e)e[o].pageX?n.push({x:e[o].pageX,y:e[o].pageY}):e[o].clientX&&n.push({x:e[o].clientX,y:e[o].clientY});return n},s=function(t,e,n){return e&&t?"x"===n?t.x-e.x:"y"===n?t.y-e.y:Math.sqrt(Math.pow(t.x-e.x,2)+Math.pow(t.y-e.y,2)):0},r=function(t){if(t.is('a,area,button,[role="button"],input,label,select,summary,textarea,video,audio,iframe')||n.isFunction(t.get(0).onclick)||t.data("selectable"))return!0;for(var e=0,o=t[0].attributes,i=o.length;ee.clientHeight,a=("scroll"===o||"auto"===o)&&e.scrollWidth>e.clientWidth;return i||a},l=function(t){for(var e=!1;;){if(e=c(t.get(0)))break;if(t=t.parent(),!t.length||t.hasClass("fancybox-stage")||t.is("body"))break}return e},d=function(t){var e=this;e.instance=t,e.$bg=t.$refs.bg,e.$stage=t.$refs.stage,e.$container=t.$refs.container,e.destroy(),e.$container.on("touchstart.fb.touch mousedown.fb.touch",n.proxy(e,"ontouchstart"))};d.prototype.destroy=function(){var t=this;t.$container.off(".fb.touch"),n(e).off(".fb.touch"),t.requestId&&(i(t.requestId),t.requestId=null),t.tapped&&(clearTimeout(t.tapped),t.tapped=null)},d.prototype.ontouchstart=function(o){var i=this,c=n(o.target),d=i.instance,u=d.current,f=u.$slide,p=u.$content,h="touchstart"==o.type;if(h&&i.$container.off("mousedown.fb.touch"),(!o.originalEvent||2!=o.originalEvent.button)&&f.length&&c.length&&!r(c)&&!r(c.parent())&&(c.is("img")||!(o.originalEvent.clientX>c[0].clientWidth+c.offset().left))){if(!u||d.isAnimating||u.$slide.hasClass("fancybox-animated"))return o.stopPropagation(),void o.preventDefault();i.realPoints=i.startPoints=a(o),i.startPoints.length&&(u.touch&&o.stopPropagation(),i.startEvent=o,i.canTap=!0,i.$target=c,i.$content=p,i.opts=u.opts.touch,i.isPanning=!1,i.isSwiping=!1,i.isZooming=!1,i.isScrolling=!1,i.canPan=d.canPan(),i.startTime=(new Date).getTime(),i.distanceX=i.distanceY=i.distance=0,i.canvasWidth=Math.round(f[0].clientWidth),i.canvasHeight=Math.round(f[0].clientHeight),i.contentLastPos=null,i.contentStartPos=n.fancybox.getTranslate(i.$content)||{top:0,left:0},i.sliderStartPos=n.fancybox.getTranslate(f),i.stagePos=n.fancybox.getTranslate(d.$refs.stage),i.sliderStartPos.top-=i.stagePos.top,i.sliderStartPos.left-=i.stagePos.left,i.contentStartPos.top-=i.stagePos.top,i.contentStartPos.left-=i.stagePos.left,n(e).off(".fb.touch").on(h?"touchend.fb.touch touchcancel.fb.touch":"mouseup.fb.touch mouseleave.fb.touch",n.proxy(i,"ontouchend")).on(h?"touchmove.fb.touch":"mousemove.fb.touch",n.proxy(i,"ontouchmove")),n.fancybox.isMobile&&e.addEventListener("scroll",i.onscroll,!0),((i.opts||i.canPan)&&(c.is(i.$stage)||i.$stage.find(c).length)||(c.is(".fancybox-image")&&o.preventDefault(),n.fancybox.isMobile&&c.parents(".fancybox-caption").length))&&(i.isScrollable=l(c)||l(c.parent()),n.fancybox.isMobile&&i.isScrollable||o.preventDefault(),(1===i.startPoints.length||u.hasError)&&(i.canPan?(n.fancybox.stop(i.$content),i.isPanning=!0):i.isSwiping=!0,i.$container.addClass("fancybox-is-grabbing")),2===i.startPoints.length&&"image"===u.type&&(u.isLoaded||u.$ghost)&&(i.canTap=!1,i.isSwiping=!1,i.isPanning=!1,i.isZooming=!0,n.fancybox.stop(i.$content),i.centerPointStartX=.5*(i.startPoints[0].x+i.startPoints[1].x)-n(t).scrollLeft(),i.centerPointStartY=.5*(i.startPoints[0].y+i.startPoints[1].y)-n(t).scrollTop(),i.percentageOfImageAtPinchPointX=(i.centerPointStartX-i.contentStartPos.left)/i.contentStartPos.width,i.percentageOfImageAtPinchPointY=(i.centerPointStartY-i.contentStartPos.top)/i.contentStartPos.height,i.startDistanceBetweenFingers=s(i.startPoints[0],i.startPoints[1]))))}},d.prototype.onscroll=function(t){var n=this;n.isScrolling=!0,e.removeEventListener("scroll",n.onscroll,!0)},d.prototype.ontouchmove=function(t){var e=this;return void 0!==t.originalEvent.buttons&&0===t.originalEvent.buttons?void e.ontouchend(t):e.isScrolling?void(e.canTap=!1):(e.newPoints=a(t),void((e.opts||e.canPan)&&e.newPoints.length&&e.newPoints.length&&(e.isSwiping&&!0===e.isSwiping||t.preventDefault(),e.distanceX=s(e.newPoints[0],e.startPoints[0],"x"),e.distanceY=s(e.newPoints[0],e.startPoints[0],"y"),e.distance=s(e.newPoints[0],e.startPoints[0]),e.distance>0&&(e.isSwiping?e.onSwipe(t):e.isPanning?e.onPan():e.isZooming&&e.onZoom()))))},d.prototype.onSwipe=function(e){var a,s=this,r=s.instance,c=s.isSwiping,l=s.sliderStartPos.left||0;if(!0!==c)"x"==c&&(s.distanceX>0&&(s.instance.group.length<2||0===s.instance.current.index&&!s.instance.current.opts.loop)?l+=Math.pow(s.distanceX,.8):s.distanceX<0&&(s.instance.group.length<2||s.instance.current.index===s.instance.group.length-1&&!s.instance.current.opts.loop)?l-=Math.pow(-s.distanceX,.8):l+=s.distanceX),s.sliderLastPos={top:"x"==c?0:s.sliderStartPos.top+s.distanceY,left:l},s.requestId&&(i(s.requestId),s.requestId=null),s.requestId=o(function(){s.sliderLastPos&&(n.each(s.instance.slides,function(t,e){var o=e.pos-s.instance.currPos;n.fancybox.setTranslate(e.$slide,{top:s.sliderLastPos.top,left:s.sliderLastPos.left+o*s.canvasWidth+o*e.opts.gutter})}),s.$container.addClass("fancybox-is-sliding"))});else if(Math.abs(s.distance)>10){if(s.canTap=!1,r.group.length<2&&s.opts.vertical?s.isSwiping="y":r.isDragging||!1===s.opts.vertical||"auto"===s.opts.vertical&&n(t).width()>800?s.isSwiping="x":(a=Math.abs(180*Math.atan2(s.distanceY,s.distanceX)/Math.PI),s.isSwiping=a>45&&a<135?"y":"x"),"y"===s.isSwiping&&n.fancybox.isMobile&&s.isScrollable)return void(s.isScrolling=!0);r.isDragging=s.isSwiping,s.startPoints=s.newPoints,n.each(r.slides,function(t,e){var o,i;n.fancybox.stop(e.$slide),o=n.fancybox.getTranslate(e.$slide),i=n.fancybox.getTranslate(r.$refs.stage),e.$slide.css({transform:"",opacity:"","transition-duration":""}).removeClass("fancybox-animated").removeClass(function(t,e){return(e.match(/(^|\s)fancybox-fx-\S+/g)||[]).join(" ")}),e.pos===r.current.pos&&(s.sliderStartPos.top=o.top-i.top,s.sliderStartPos.left=o.left-i.left),n.fancybox.setTranslate(e.$slide,{top:o.top-i.top,left:o.left-i.left})}),r.SlideShow&&r.SlideShow.isActive&&r.SlideShow.stop()}},d.prototype.onPan=function(){var t=this;if(s(t.newPoints[0],t.realPoints[0])<(n.fancybox.isMobile?10:5))return void(t.startPoints=t.newPoints);t.canTap=!1,t.contentLastPos=t.limitMovement(),t.requestId&&i(t.requestId),t.requestId=o(function(){n.fancybox.setTranslate(t.$content,t.contentLastPos)})},d.prototype.limitMovement=function(){var t,e,n,o,i,a,s=this,r=s.canvasWidth,c=s.canvasHeight,l=s.distanceX,d=s.distanceY,u=s.contentStartPos,f=u.left,p=u.top,h=u.width,g=u.height;return i=h>r?f+l:f,a=p+d,t=Math.max(0,.5*r-.5*h),e=Math.max(0,.5*c-.5*g),n=Math.min(r-h,.5*r-.5*h),o=Math.min(c-g,.5*c-.5*g),l>0&&i>t&&(i=t-1+Math.pow(-t+f+l,.8)||0),l<0&&i0&&a>e&&(a=e-1+Math.pow(-e+p+d,.8)||0),d<0&&aa?(t=t>0?0:t,t=ts?(e=e>0?0:e,e=e1&&(o.dMs>130&&s>10||s>50);o.sliderLastPos=null,"y"==t&&!e&&Math.abs(o.distanceY)>50?(n.fancybox.animate(o.instance.current.$slide,{top:o.sliderStartPos.top+o.distanceY+150*o.velocityY,opacity:0},200),i=o.instance.close(!0,250)):r&&o.distanceX>0?i=o.instance.previous(300):r&&o.distanceX<0&&(i=o.instance.next(300)),!1!==i||"x"!=t&&"y"!=t||o.instance.centerSlide(200),o.$container.removeClass("fancybox-is-sliding")},d.prototype.endPanning=function(){var t,e,o,i=this;i.contentLastPos&&(!1===i.opts.momentum||i.dMs>350?(t=i.contentLastPos.left,e=i.contentLastPos.top):(t=i.contentLastPos.left+500*i.velocityX,e=i.contentLastPos.top+500*i.velocityY),o=i.limitPosition(t,e,i.contentStartPos.width,i.contentStartPos.height),o.width=i.contentStartPos.width,o.height=i.contentStartPos.height,n.fancybox.animate(i.$content,o,366))},d.prototype.endZooming=function(){var t,e,o,i,a=this,s=a.instance.current,r=a.newWidth,c=a.newHeight;a.contentLastPos&&(t=a.contentLastPos.left,e=a.contentLastPos.top,i={top:e,left:t,width:r,height:c,scaleX:1,scaleY:1},n.fancybox.setTranslate(a.$content,i),rs.width||c>s.height?a.instance.scaleToActual(a.centerPointStartX,a.centerPointStartY,150):(o=a.limitPosition(t,e,r,c),n.fancybox.animate(a.$content,o,150)))},d.prototype.onTap=function(e){var o,i=this,s=n(e.target),r=i.instance,c=r.current,l=e&&a(e)||i.startPoints,d=l[0]?l[0].x-n(t).scrollLeft()-i.stagePos.left:0,u=l[0]?l[0].y-n(t).scrollTop()-i.stagePos.top:0,f=function(t){var o=c.opts[t];if(n.isFunction(o)&&(o=o.apply(r,[c,e])),o)switch(o){case"close":r.close(i.startEvent);break;case"toggleControls":r.toggleControls();break;case"next":r.next();break;case"nextOrClose":r.group.length>1?r.next():r.close(i.startEvent);break;case"zoom":"image"==c.type&&(c.isLoaded||c.$ghost)&&(r.canPan()?r.scaleToFit():r.isScaledDown()?r.scaleToActual(d,u):r.group.length<2&&r.close(i.startEvent))}};if((!e.originalEvent||2!=e.originalEvent.button)&&(s.is("img")||!(d>s[0].clientWidth+s.offset().left))){if(s.is(".fancybox-bg,.fancybox-inner,.fancybox-outer,.fancybox-container"))o="Outside";else if(s.is(".fancybox-slide"))o="Slide";else{if(!r.current.$content||!r.current.$content.find(s).addBack().filter(s).length)return;o="Content"}if(i.tapped){if(clearTimeout(i.tapped),i.tapped=null,Math.abs(d-i.tapX)>50||Math.abs(u-i.tapY)>50)return this;f("dblclick"+o)}else i.tapX=d,i.tapY=u,c.opts["dblclick"+o]&&c.opts["dblclick"+o]!==c.opts["click"+o]?i.tapped=setTimeout(function(){i.tapped=null,r.isAnimating||f("click"+o)},500):f("click"+o);return this}},n(e).on("onActivate.fb",function(t,e){e&&!e.Guestures&&(e.Guestures=new d(e))}).on("beforeClose.fb",function(t,e){e&&e.Guestures&&e.Guestures.destroy()})}(window,document,jQuery),function(t,e){"use strict";e.extend(!0,e.fancybox.defaults,{btnTpl:{slideShow:''},slideShow:{autoStart:!1,speed:3e3,progress:!0}});var n=function(t){this.instance=t,this.init()};e.extend(n.prototype,{timer:null,isActive:!1,$button:null,init:function(){var t=this,n=t.instance,o=n.group[n.currIndex].opts.slideShow;t.$button=n.$refs.toolbar.find("[data-fancybox-play]").on("click",function(){t.toggle()}),n.group.length<2||!o?t.$button.hide():o.progress&&(t.$progress=e('
').appendTo(n.$refs.inner))},set:function(t){var n=this,o=n.instance,i=o.current;i&&(!0===t||i.opts.loop||o.currIndex'},fullScreen:{autoStart:!1}}),e(t).on(n.fullscreenchange,function(){var t=o.isFullscreen(),n=e.fancybox.getInstance();n&&(n.current&&"image"===n.current.type&&n.isAnimating&&(n.isAnimating=!1,n.update(!0,!0,0),n.isComplete||n.complete()),n.trigger("onFullscreenChange",t),n.$refs.container.toggleClass("fancybox-is-fullscreen",t),n.$refs.toolbar.find("[data-fancybox-fullscreen]").toggleClass("fancybox-button--fsenter",!t).toggleClass("fancybox-button--fsexit",t))})}e(t).on({"onInit.fb":function(t,e){var i;if(!n)return void e.$refs.toolbar.find("[data-fancybox-fullscreen]").remove();e&&e.group[e.currIndex].opts.fullScreen?(i=e.$refs.container,i.on("click.fb-fullscreen","[data-fancybox-fullscreen]",function(t){t.stopPropagation(),t.preventDefault(),o.toggle()}),e.opts.fullScreen&&!0===e.opts.fullScreen.autoStart&&o.request(),e.FullScreen=o):e&&e.$refs.toolbar.find("[data-fancybox-fullscreen]").hide()},"afterKeydown.fb":function(t,e,n,o,i){e&&e.FullScreen&&70===i&&(o.preventDefault(),e.FullScreen.toggle())},"beforeClose.fb":function(t,e){e&&e.FullScreen&&e.$refs.container.hasClass("fancybox-is-fullscreen")&&o.exit()}})}(document,jQuery),function(t,e){"use strict";var n="fancybox-thumbs";e.fancybox.defaults=e.extend(!0,{btnTpl:{thumbs:''},thumbs:{autoStart:!1,hideOnClose:!0,parentEl:".fancybox-container",axis:"y"}},e.fancybox.defaults);var o=function(t){this.init(t)};e.extend(o.prototype,{$button:null,$grid:null,$list:null,isVisible:!1,isActive:!1,init:function(t){var e=this,n=t.group,o=0;e.instance=t,e.opts=n[t.currIndex].opts.thumbs,t.Thumbs=e,e.$button=t.$refs.toolbar.find("[data-fancybox-thumbs]");for(var i=0,a=n.length;i1));i++);o>1&&e.opts?(e.$button.removeAttr("style").on("click",function(){e.toggle()}),e.isActive=!0):e.$button.hide()},create:function(){var t,o=this,i=o.instance,a=o.opts.parentEl,s=[];o.$grid||(o.$grid=e('
').appendTo(i.$refs.container.find(a).addBack().filter(a)),o.$grid.on("click","a",function(){i.jumpTo(e(this).attr("data-index"))})),o.$list||(o.$list=e('
').appendTo(o.$grid)),e.each(i.group,function(e,n){t=n.thumb,t||"image"!==n.type||(t=n.src),s.push('")}),o.$list[0].innerHTML=s.join(""),"x"===o.opts.axis&&o.$list.width(parseInt(o.$grid.css("padding-right"),10)+i.group.length*o.$list.children().eq(0).outerWidth(!0))},focus:function(t){var e,n,o=this,i=o.$list,a=o.$grid;o.instance.current&&(e=i.children().removeClass("fancybox-thumbs-active").filter('[data-index="'+o.instance.current.index+'"]').addClass("fancybox-thumbs-active"),n=e.position(),"y"===o.opts.axis&&(n.top<0||n.top>i.height()-e.outerHeight())?i.stop().animate({scrollTop:i.scrollTop()+n.top},t):"x"===o.opts.axis&&(n.lefta.scrollLeft()+(a.width()-e.outerWidth()))&&i.parent().stop().animate({scrollLeft:n.left},t))},update:function(){var t=this;t.instance.$refs.container.toggleClass("fancybox-show-thumbs",this.isVisible),t.isVisible?(t.$grid||t.create(),t.instance.trigger("onThumbsShow"),t.focus(0)):t.$grid&&t.instance.trigger("onThumbsHide"),t.instance.update()},hide:function(){this.isVisible=!1,this.update()},show:function(){this.isVisible=!0,this.update()},toggle:function(){this.isVisible=!this.isVisible,this.update()}}),e(t).on({"onInit.fb":function(t,e){var n;e&&!e.Thumbs&&(n=new o(e),n.isActive&&!0===n.opts.autoStart&&n.show())},"beforeShow.fb":function(t,e,n,o){var i=e&&e.Thumbs;i&&i.isVisible&&i.focus(o?0:250)},"afterKeydown.fb":function(t,e,n,o,i){var a=e&&e.Thumbs;a&&a.isActive&&71===i&&(o.preventDefault(),a.toggle())},"beforeClose.fb":function(t,e){var n=e&&e.Thumbs;n&&n.isVisible&&!1!==n.opts.hideOnClose&&n.$grid.hide()}})}(document,jQuery),function(t,e){"use strict";function n(t){var e={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};return String(t).replace(/[&<>"'`=\/]/g,function(t){return e[t]})}e.extend(!0,e.fancybox.defaults,{btnTpl:{share:''},share:{url:function(t,e){return!t.currentHash&&"inline"!==e.type&&"html"!==e.type&&(e.origSrc||e.src)||window.location}, + tpl:''}}),e(t).on("click","[data-fancybox-share]",function(){var t,o,i=e.fancybox.getInstance(),a=i.current||null;a&&("function"===e.type(a.opts.share.url)&&(t=a.opts.share.url.apply(a,[i,a])),o=a.opts.share.tpl.replace(/\{\{media\}\}/g,"image"===a.type?encodeURIComponent(a.src):"").replace(/\{\{url\}\}/g,encodeURIComponent(t)).replace(/\{\{url_raw\}\}/g,n(t)).replace(/\{\{descr\}\}/g,i.$caption?encodeURIComponent(i.$caption.text()):""),e.fancybox.open({src:i.translate(i,o),type:"html",opts:{touch:!1,animationEffect:!1,afterLoad:function(t,e){i.$refs.container.one("beforeClose.fb",function(){t.close(null,0)}),e.$content.find(".fancybox-share__button").click(function(){return window.open(this.href,"Share","width=550, height=450"),!1})},mobile:{autoFocus:!1}}}))})}(document,jQuery),function(t,e,n){"use strict";function o(){var e=t.location.hash.substr(1),n=e.split("-"),o=n.length>1&&/^\+?\d+$/.test(n[n.length-1])?parseInt(n.pop(-1),10)||1:1,i=n.join("-");return{hash:e,index:o<1?1:o,gallery:i}}function i(t){""!==t.gallery&&n("[data-fancybox='"+n.escapeSelector(t.gallery)+"']").eq(t.index-1).focus().trigger("click.fb-start")}function a(t){var e,n;return!!t&&(e=t.current?t.current.opts:t.opts,""!==(n=e.hash||(e.$orig?e.$orig.data("fancybox")||e.$orig.data("fancybox-trigger"):""))&&n)}n.escapeSelector||(n.escapeSelector=function(t){return(t+"").replace(/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g,function(t,e){return e?"\0"===t?"�":t.slice(0,-1)+"\\"+t.charCodeAt(t.length-1).toString(16)+" ":"\\"+t})}),n(function(){!1!==n.fancybox.defaults.hash&&(n(e).on({"onInit.fb":function(t,e){var n,i;!1!==e.group[e.currIndex].opts.hash&&(n=o(),(i=a(e))&&n.gallery&&i==n.gallery&&(e.currIndex=n.index-1))},"beforeShow.fb":function(n,o,i,s){var r;i&&!1!==i.opts.hash&&(r=a(o))&&(o.currentHash=r+(o.group.length>1?"-"+(i.index+1):""),t.location.hash!=="#"+o.currentHash&&(s&&!o.origHash&&(o.origHash=t.location.hash),o.hashTimer&&clearTimeout(o.hashTimer),o.hashTimer=setTimeout(function(){"replaceState"in t.history?(t.history[s?"pushState":"replaceState"]({},e.title,t.location.pathname+t.location.search+"#"+o.currentHash),s&&(o.hasCreatedHistory=!0)):t.location.hash=o.currentHash,o.hashTimer=null},300)))},"beforeClose.fb":function(n,o,i){i&&!1!==i.opts.hash&&(clearTimeout(o.hashTimer),o.currentHash&&o.hasCreatedHistory?t.history.back():o.currentHash&&("replaceState"in t.history?t.history.replaceState({},e.title,t.location.pathname+t.location.search+(o.origHash||"")):t.location.hash=o.origHash),o.currentHash=null)}}),n(t).on("hashchange.fb",function(){var t=o(),e=null;n.each(n(".fancybox-container").get().reverse(),function(t,o){var i=n(o).data("FancyBox");if(i&&i.currentHash)return e=i,!1}),e?e.currentHash===t.gallery+"-"+t.index||1===t.index&&e.currentHash==t.gallery||(e.currentHash=null,e.close()):""!==t.gallery&&i(t)}),setTimeout(function(){n.fancybox.getInstance()||i(o())},50))})}(window,document,jQuery),function(t,e){"use strict";var n=(new Date).getTime();e(t).on({"onInit.fb":function(t,e,o){e.$refs.stage.on("mousewheel DOMMouseScroll wheel MozMousePixelScroll",function(t){var o=e.current,i=(new Date).getTime();e.group.length<2||!1===o.opts.wheel||"auto"===o.opts.wheel&&"image"!==o.type||(t.preventDefault(),t.stopPropagation(),o.$slide.hasClass("fancybox-animated")||(t=t.originalEvent||t,i-n<250||(n=i,e[(-t.deltaY||-t.deltaX||t.wheelDelta||-t.detail)<0?"next":"previous"]())))})}})}(document,jQuery); \ No newline at end of file diff --git a/blog-site/public/js/html2canvas.js b/blog-site/public/js/html2canvas.js new file mode 100644 index 00000000..43fc7a59 --- /dev/null +++ b/blog-site/public/js/html2canvas.js @@ -0,0 +1,3519 @@ +/* + html2canvas 0.5.0-beta3 + Copyright (c) 2016 Niklas von Hertzen + + Released under License +*/ + +!function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var f;"undefined"!=typeof window?f=window:"undefined"!=typeof global?f=global:"undefined"!=typeof self&&(f=self),f.html2canvas=e()}}(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= 0x80 (not a basic code point)', + 'invalid-input': 'Invalid input' + }, + + /** Convenience shortcuts */ + baseMinusTMin = base - tMin, + floor = Math.floor, + stringFromCharCode = String.fromCharCode, + + /** Temporary variable */ + key; + + /*--------------------------------------------------------------------------*/ + + /** + * A generic error utility function. + * @private + * @param {String} type The error type. + * @returns {Error} Throws a `RangeError` with the applicable error message. + */ + function error(type) { + throw RangeError(errors[type]); + } + + /** + * A generic `Array#map` utility function. + * @private + * @param {Array} array The array to iterate over. + * @param {Function} callback The function that gets called for every array + * item. + * @returns {Array} A new array of values returned by the callback function. + */ + function map(array, fn) { + var length = array.length; + while (length--) { + array[length] = fn(array[length]); + } + return array; + } + + /** + * A simple `Array#map`-like wrapper to work with domain name strings. + * @private + * @param {String} domain The domain name. + * @param {Function} callback The function that gets called for every + * character. + * @returns {Array} A new string of characters returned by the callback + * function. + */ + function mapDomain(string, fn) { + return map(string.split(regexSeparators), fn).join('.'); + } + + /** + * Creates an array containing the numeric code points of each Unicode + * character in the string. While JavaScript uses UCS-2 internally, + * this function will convert a pair of surrogate halves (each of which + * UCS-2 exposes as separate characters) into a single code point, + * matching UTF-16. + * @see `punycode.ucs2.encode` + * @see + * @memberOf punycode.ucs2 + * @name decode + * @param {String} string The Unicode input string (UCS-2). + * @returns {Array} The new array of code points. + */ + function ucs2decode(string) { + var output = [], + counter = 0, + length = string.length, + value, + extra; + while (counter < length) { + value = string.charCodeAt(counter++); + if (value >= 0xD800 && value <= 0xDBFF && counter < length) { + // high surrogate, and there is a next character + extra = string.charCodeAt(counter++); + if ((extra & 0xFC00) == 0xDC00) { // low surrogate + output.push(((value & 0x3FF) << 10) + (extra & 0x3FF) + 0x10000); + } else { + // unmatched surrogate; only append this code unit, in case the next + // code unit is the high surrogate of a surrogate pair + output.push(value); + counter--; + } + } else { + output.push(value); + } + } + return output; + } + + /** + * Creates a string based on an array of numeric code points. + * @see `punycode.ucs2.decode` + * @memberOf punycode.ucs2 + * @name encode + * @param {Array} codePoints The array of numeric code points. + * @returns {String} The new Unicode string (UCS-2). + */ + function ucs2encode(array) { + return map(array, function(value) { + var output = ''; + if (value > 0xFFFF) { + value -= 0x10000; + output += stringFromCharCode(value >>> 10 & 0x3FF | 0xD800); + value = 0xDC00 | value & 0x3FF; + } + output += stringFromCharCode(value); + return output; + }).join(''); + } + + /** + * Converts a basic code point into a digit/integer. + * @see `digitToBasic()` + * @private + * @param {Number} codePoint The basic numeric code point value. + * @returns {Number} The numeric value of a basic code point (for use in + * representing integers) in the range `0` to `base - 1`, or `base` if + * the code point does not represent a value. + */ + function basicToDigit(codePoint) { + if (codePoint - 48 < 10) { + return codePoint - 22; + } + if (codePoint - 65 < 26) { + return codePoint - 65; + } + if (codePoint - 97 < 26) { + return codePoint - 97; + } + return base; + } + + /** + * Converts a digit/integer into a basic code point. + * @see `basicToDigit()` + * @private + * @param {Number} digit The numeric value of a basic code point. + * @returns {Number} The basic code point whose value (when used for + * representing integers) is `digit`, which needs to be in the range + * `0` to `base - 1`. If `flag` is non-zero, the uppercase form is + * used; else, the lowercase form is used. The behavior is undefined + * if `flag` is non-zero and `digit` has no uppercase form. + */ + function digitToBasic(digit, flag) { + // 0..25 map to ASCII a..z or A..Z + // 26..35 map to ASCII 0..9 + return digit + 22 + 75 * (digit < 26) - ((flag != 0) << 5); + } + + /** + * Bias adaptation function as per section 3.4 of RFC 3492. + * http://tools.ietf.org/html/rfc3492#section-3.4 + * @private + */ + function adapt(delta, numPoints, firstTime) { + var k = 0; + delta = firstTime ? floor(delta / damp) : delta >> 1; + delta += floor(delta / numPoints); + for (/* no initialization */; delta > baseMinusTMin * tMax >> 1; k += base) { + delta = floor(delta / baseMinusTMin); + } + return floor(k + (baseMinusTMin + 1) * delta / (delta + skew)); + } + + /** + * Converts a Punycode string of ASCII-only symbols to a string of Unicode + * symbols. + * @memberOf punycode + * @param {String} input The Punycode string of ASCII-only symbols. + * @returns {String} The resulting string of Unicode symbols. + */ + function decode(input) { + // Don't use UCS-2 + var output = [], + inputLength = input.length, + out, + i = 0, + n = initialN, + bias = initialBias, + basic, + j, + index, + oldi, + w, + k, + digit, + t, + /** Cached calculation results */ + baseMinusT; + + // Handle the basic code points: let `basic` be the number of input code + // points before the last delimiter, or `0` if there is none, then copy + // the first basic code points to the output. + + basic = input.lastIndexOf(delimiter); + if (basic < 0) { + basic = 0; + } + + for (j = 0; j < basic; ++j) { + // if it's not a basic code point + if (input.charCodeAt(j) >= 0x80) { + error('not-basic'); + } + output.push(input.charCodeAt(j)); + } + + // Main decoding loop: start just after the last delimiter if any basic code + // points were copied; start at the beginning otherwise. + + for (index = basic > 0 ? basic + 1 : 0; index < inputLength; /* no final expression */) { + + // `index` is the index of the next character to be consumed. + // Decode a generalized variable-length integer into `delta`, + // which gets added to `i`. The overflow checking is easier + // if we increase `i` as we go, then subtract off its starting + // value at the end to obtain `delta`. + for (oldi = i, w = 1, k = base; /* no condition */; k += base) { + + if (index >= inputLength) { + error('invalid-input'); + } + + digit = basicToDigit(input.charCodeAt(index++)); + + if (digit >= base || digit > floor((maxInt - i) / w)) { + error('overflow'); + } + + i += digit * w; + t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); + + if (digit < t) { + break; + } + + baseMinusT = base - t; + if (w > floor(maxInt / baseMinusT)) { + error('overflow'); + } + + w *= baseMinusT; + + } + + out = output.length + 1; + bias = adapt(i - oldi, out, oldi == 0); + + // `i` was supposed to wrap around from `out` to `0`, + // incrementing `n` each time, so we'll fix that now: + if (floor(i / out) > maxInt - n) { + error('overflow'); + } + + n += floor(i / out); + i %= out; + + // Insert `n` at position `i` of the output + output.splice(i++, 0, n); + + } + + return ucs2encode(output); + } + + /** + * Converts a string of Unicode symbols to a Punycode string of ASCII-only + * symbols. + * @memberOf punycode + * @param {String} input The string of Unicode symbols. + * @returns {String} The resulting Punycode string of ASCII-only symbols. + */ + function encode(input) { + var n, + delta, + handledCPCount, + basicLength, + bias, + j, + m, + q, + k, + t, + currentValue, + output = [], + /** `inputLength` will hold the number of code points in `input`. */ + inputLength, + /** Cached calculation results */ + handledCPCountPlusOne, + baseMinusT, + qMinusT; + + // Convert the input in UCS-2 to Unicode + input = ucs2decode(input); + + // Cache the length + inputLength = input.length; + + // Initialize the state + n = initialN; + delta = 0; + bias = initialBias; + + // Handle the basic code points + for (j = 0; j < inputLength; ++j) { + currentValue = input[j]; + if (currentValue < 0x80) { + output.push(stringFromCharCode(currentValue)); + } + } + + handledCPCount = basicLength = output.length; + + // `handledCPCount` is the number of code points that have been handled; + // `basicLength` is the number of basic code points. + + // Finish the basic string - if it is not empty - with a delimiter + if (basicLength) { + output.push(delimiter); + } + + // Main encoding loop: + while (handledCPCount < inputLength) { + + // All non-basic code points < n have been handled already. Find the next + // larger one: + for (m = maxInt, j = 0; j < inputLength; ++j) { + currentValue = input[j]; + if (currentValue >= n && currentValue < m) { + m = currentValue; + } + } + + // Increase `delta` enough to advance the decoder's state to , + // but guard against overflow + handledCPCountPlusOne = handledCPCount + 1; + if (m - n > floor((maxInt - delta) / handledCPCountPlusOne)) { + error('overflow'); + } + + delta += (m - n) * handledCPCountPlusOne; + n = m; + + for (j = 0; j < inputLength; ++j) { + currentValue = input[j]; + + if (currentValue < n && ++delta > maxInt) { + error('overflow'); + } + + if (currentValue == n) { + // Represent delta as a generalized variable-length integer + for (q = delta, k = base; /* no condition */; k += base) { + t = k <= bias ? tMin : (k >= bias + tMax ? tMax : k - bias); + if (q < t) { + break; + } + qMinusT = q - t; + baseMinusT = base - t; + output.push( + stringFromCharCode(digitToBasic(t + qMinusT % baseMinusT, 0)) + ); + q = floor(qMinusT / baseMinusT); + } + + output.push(stringFromCharCode(digitToBasic(q, 0))); + bias = adapt(delta, handledCPCountPlusOne, handledCPCount == basicLength); + delta = 0; + ++handledCPCount; + } + } + + ++delta; + ++n; + + } + return output.join(''); + } + + /** + * Converts a Punycode string representing a domain name to Unicode. Only the + * Punycoded parts of the domain name will be converted, i.e. it doesn't + * matter if you call it on a string that has already been converted to + * Unicode. + * @memberOf punycode + * @param {String} domain The Punycode domain name to convert to Unicode. + * @returns {String} The Unicode representation of the given Punycode + * string. + */ + function toUnicode(domain) { + return mapDomain(domain, function(string) { + return regexPunycode.test(string) + ? decode(string.slice(4).toLowerCase()) + : string; + }); + } + + /** + * Converts a Unicode string representing a domain name to Punycode. Only the + * non-ASCII parts of the domain name will be converted, i.e. it doesn't + * matter if you call it with a domain that's already in ASCII. + * @memberOf punycode + * @param {String} domain The domain name to convert, as a Unicode string. + * @returns {String} The Punycode representation of the given domain name. + */ + function toASCII(domain) { + return mapDomain(domain, function(string) { + return regexNonASCII.test(string) + ? 'xn--' + encode(string) + : string; + }); + } + + /*--------------------------------------------------------------------------*/ + + /** Define the public API */ + punycode = { + /** + * A string representing the current Punycode.js version number. + * @memberOf punycode + * @type String + */ + 'version': '1.2.4', + /** + * An object of methods to convert from JavaScript's internal character + * representation (UCS-2) to Unicode code points, and back. + * @see + * @memberOf punycode + * @type Object + */ + 'ucs2': { + 'decode': ucs2decode, + 'encode': ucs2encode + }, + 'decode': decode, + 'encode': encode, + 'toASCII': toASCII, + 'toUnicode': toUnicode + }; + + /** Expose `punycode` */ + // Some AMD build optimizers, like r.js, check for specific condition patterns + // like the following: + if ( + typeof define == 'function' && + typeof define.amd == 'object' && + define.amd + ) { + define('punycode', function() { + return punycode; + }); + } else if (freeExports && !freeExports.nodeType) { + if (freeModule) { // in Node.js or RingoJS v0.8.0+ + freeModule.exports = punycode; + } else { // in Narwhal or RingoJS v0.7.0- + for (key in punycode) { + punycode.hasOwnProperty(key) && (freeExports[key] = punycode[key]); + } + } + } else { // in Rhino or a web browser + root.punycode = punycode; + } + +}(this)); + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],2:[function(_dereq_,module,exports){ +var log = _dereq_('./log'); + +function restoreOwnerScroll(ownerDocument, x, y) { + if (ownerDocument.defaultView && (x !== ownerDocument.defaultView.pageXOffset || y !== ownerDocument.defaultView.pageYOffset)) { + ownerDocument.defaultView.scrollTo(x, y); + } +} + +function cloneCanvasContents(canvas, clonedCanvas) { + try { + if (clonedCanvas) { + clonedCanvas.width = canvas.width; + clonedCanvas.height = canvas.height; + clonedCanvas.getContext("2d").putImageData(canvas.getContext("2d").getImageData(0, 0, canvas.width, canvas.height), 0, 0); + } + } catch(e) { + log("Unable to copy canvas content from", canvas, e); + } +} + +function cloneNode(node, javascriptEnabled) { + var clone = node.nodeType === 3 ? document.createTextNode(node.nodeValue) : node.cloneNode(false); + + var child = node.firstChild; + while(child) { + if (javascriptEnabled === true || child.nodeType !== 1 || child.nodeName !== 'SCRIPT') { + clone.appendChild(cloneNode(child, javascriptEnabled)); + } + child = child.nextSibling; + } + + if (node.nodeType === 1) { + clone._scrollTop = node.scrollTop; + clone._scrollLeft = node.scrollLeft; + if (node.nodeName === "CANVAS") { + cloneCanvasContents(node, clone); + } else if (node.nodeName === "TEXTAREA" || node.nodeName === "SELECT") { + clone.value = node.value; + } + } + + return clone; +} + +function initNode(node) { + if (node.nodeType === 1) { + node.scrollTop = node._scrollTop; + node.scrollLeft = node._scrollLeft; + + var child = node.firstChild; + while(child) { + initNode(child); + child = child.nextSibling; + } + } +} + +module.exports = function(ownerDocument, containerDocument, width, height, options, x ,y) { + var documentElement = cloneNode(ownerDocument.documentElement, options.javascriptEnabled); + var container = containerDocument.createElement("iframe"); + + container.className = "html2canvas-container"; + container.style.visibility = "hidden"; + container.style.position = "fixed"; + container.style.left = "-10000px"; + container.style.top = "0px"; + container.style.border = "0"; + container.width = width; + container.height = height; + container.scrolling = "no"; // ios won't scroll without it + containerDocument.body.appendChild(container); + + return new Promise(function(resolve) { + var documentClone = container.contentWindow.document; + + /* Chrome doesn't detect relative background-images assigned in inline \n\n", + body: "_body_" + } + }; + var options = { + maxWidth: 624 + }; + // Clone selected element before manipulating it + var markup = $(contentNode).clone(); + // Remove hidden elements from the output + markup.each(function() { + var self = $(contentNode); + if (self.is(':hidden')) + self.remove(); + }); + + // Embed all images using Data URLs + var images = Array(); + var img = markup.find('img'); + for (var i = 0; i < img.length; i++) { + // Calculate dimensions of output image + var w = Math.min(img[i].width, options.maxWidth); + var h = img[i].height * (w / img[i].width); + // Create canvas for converting image to data URL + var canvas = document.createElement("CANVAS"); + canvas.width = w; + canvas.height = h; + // Draw image to canvas + var context = canvas.getContext('2d'); + context.drawImage(img[i], 0, 0, w, h); + // Get data URL encoding of image + var uri = canvas.toDataURL("image/png"); + $(img[i]).attr("src", img[i].src); + img[i].width = w; + img[i].height = h; + // Save encoded image to array + images[i] = { + type: uri.substring(uri.indexOf(":") + 1, uri.indexOf(";")), + encoding: uri.substring(uri.indexOf(";") + 1, uri.indexOf(",")), + location: $(img[i]).attr("src"), + data: uri.substring(uri.indexOf(",") + 1) + }; + } + + // Prepare bottom of mhtml file with image data + var mhtmlBottom = "\n"; + for (const element of images) { + mhtmlBottom += "--NEXT.ITEM-BOUNDARY\n"; + mhtmlBottom += "Content-Location: " + element.location + "\n"; + mhtmlBottom += "Content-Type: " + element.type + "\n"; + mhtmlBottom += "Content-Transfer-Encoding: " + element.encoding + "\n\n"; + mhtmlBottom += element.data + "\n\n"; + } + mhtmlBottom += "--NEXT.ITEM-BOUNDARY--"; + + //TODO: load css from included stylesheet + var styles = ""; + + // Aggregate parts of the file together + var fileContent = static.mhtml.top.replace("_html_", static.mhtml.head.replace("_styles_", styles) + static.mhtml.body.replace("_body_", markup.html())) + mhtmlBottom; + + // Create a Blob with the file contents + var blob = new Blob([fileContent], { + type: "application/msword;charset=utf-8" + }); + return fileContent +} \ No newline at end of file diff --git a/blog-site/public/js/zozo.js b/blog-site/public/js/zozo.js new file mode 100644 index 00000000..145ac084 --- /dev/null +++ b/blog-site/public/js/zozo.js @@ -0,0 +1,207 @@ +'use strict'; + +// back-to-top +$(document).ready((function (_this) { + return function () { + let bt = $('#back_to_top') + if ($(document).width() > 480) { + $(window).scroll(function () { + return outDisplay(bt,$(window).scrollTop() >= 100) + }) + return bt.click(function () { + $('body,html').animate({ + scrollTop: 0, + }, 800) + return false + }) + } + } +})(this)) + + +// top_to_back +$(document).ready((function (_this) { + return function () { + let tb = $('#top_to_back') + const docHeight = $(document).height() + if ($(document).width() > 480) { + $(window).scroll(function () { + return outDisplay(tb,$(window).scrollTop() <= docHeight -1000) + }) + + return tb.click(function () { + $('body,html').animate({ + scrollTop: docHeight, + }, 800) + return false + }) + } + } +})(this)) + + +// 隐藏内容 +$(document).ready((function (_this) { + return function () { + let ch = $('#content_hidden') + let cd = $('#content_display') + + if ($(document).width() > 480) { + ch.css('display', 'block') + return ch.click(function () { + hiddenNotContent() + ch.css('display', 'none') + cd.css('display', 'block') + return false + }) + } + } +})(this)) + + +// 显示内容 +$(document).ready((function (_this) { + return function () { + let ch = $('#content_hidden') + let cd = $('#content_display') + + if ($(document).width() > 480) { + ch.css('display', 'block') + return cd.click(function () { + $('body,html').animate({ + scrollTop: 0, + }, 800) + + displayNotContent() + ch.css('display', 'block') + cd.css('display', 'none') + return false + }) + } + + } +})(this)) + + +// 超出显示 +function outDisplay(selector,displayCondition){ + if (displayCondition) { + return selector.css('display', 'block') + } else { + return selector.css('display', 'none') + } +} + +// nav-toggle +$(document).ready((function (_this) { + return function () { + let nav,icon + icon = $('#menu_icon') + nav = $('#site_nav') + icon.click(function () { + nav.slideToggle(250) + }) + } +})(this)) + +// FancyBox +$('[data-fancybox="gallery"]').fancybox({ + arrows: false, + infobar: false, + buttons: [], + clickContent: "close", + autoFocus: false, + backFocus: false, + wheel: false, + mobile: { + clickContent: "close", + clickSlide: "close", + dblclickContent: false, + dblclickSlide: false + }, +}); + + +// 点击更多图标 +$(document).ready((function (_this) { + return function () { + if ($(document).width() > 480) { + $('#icon_more').click(function () { + $(this).addClass('display_none') + $('#sys_function').removeClass('display_none') + $('#icon_less').removeClass('display_none') + }) + } + } +})(this)) + +// 点击更少图标 +$(document).ready((function (_this) { + return function () { + $('#icon_less').click(function () { + $(this).addClass('display_none') + $('#sys_function').addClass('display_none') + $('#icon_more').removeClass('display_none') + }) + } +})(this)) + + + +function hiddenNotContent(){ + const notContentObjs = getNotContent(); + for (const element of notContentObjs) { + if (!isEmpty(element)) { + element.classList.add('display_none') + } + } +} + +function removeNotContent(){ + const notContentObjs = getNotContent(); + for (const element of notContentObjs) { + if (!isEmpty(element)) { + element.remove() + } + } +} + +function displayNotContent(){ + const notContentObjs = getNotContent(); + for (const element of notContentObjs) { + if (!isEmpty(element)) { + element.classList.remove('display_none') + } + } +} + + +function getNotContent(){ + return [ + // 评论展示内容 + document.getElementById('doc_comments'), + // 顶部标签导航 + document.getElementById('site_nav'), + // 顶部内容 + document.getElementById('post_header'), + // 底部信息 + document.getElementById('post_footer_info'), + document.getElementById('footer_powered_by'), + document.getElementById('footer_slogan'), + // 标题日期 + document.getElementById('post_page_title_date'), + // 目录 + document.getElementById('post_content_toc'), + // 数学公式 + document.getElementById('MathJax_Message'), + // 夜间模式切换 + document.getElementById('darkmode-background'), + document.getElementById('darkmode-layer'), + document.getElementById('darkmode-toggle'), + // 侧边栏 + document.getElementById('back_to_top'), + document.getElementById('top_to_back'), + ] +} + + diff --git a/blog-site/public/page/1/index.html b/blog-site/public/page/1/index.html new file mode 100644 index 00000000..98223f79 --- /dev/null +++ b/blog-site/public/page/1/index.html @@ -0,0 +1,10 @@ + + + + http://localhost:1313/iblog/ + + + + + + diff --git a/blog-site/public/page/2/index.html b/blog-site/public/page/2/index.html new file mode 100644 index 00000000..b8b92fcc --- /dev/null +++ b/blog-site/public/page/2/index.html @@ -0,0 +1,844 @@ + + + + + + + + + + + + 唯手熟尔 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+ + +
+
+

20220422简历

+
+ +
+
+

自我介绍 1998 · 李济芝 河北唐山 15176733539  m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和三年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对SQL......

+
+
+ + +
+ +
+
+

Java小程序集合

+
+ +
+
+

写在前面 本文中所涉及的程序均为Java开发,如果您想要直接使用这些工具需要提前配置Java环境。所涉及到的程序均提供完整代码,如果您有兴趣可以尝试运行。 使用java -jar命令启动 某些程序功能并不是很完善,但是也可以凑合着用,写这些程序的主......

+
+
+ + +
+ +
+
+

数据结构与算法

+
+ +
+
+

数据结构 数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好数据结构可以编写出更有效率的代码。数据结构是算法的基础,想要学好算法,就必须把数据结构学到位。 数据结构包括:线性结构、非线性结构。 线性结构作为最常用的数据结构,......

+
+
+ + +
+ +
+
+

规范编写Java代码总结

+
+ +
+
+

编码规范 我们为什么要遵守规范来编码? 是因为通常在编码过程中我们不只自己进行开发,通常需要一个团队来进行,开发好之后还需要维护,所以编码规范就显的尤为重要。 代码维护时间比较长,那么保证代码可读性就显得很重要。作为一个程序员,咱们得有点追求和信......

+
+
+ + +
+ +
+
+

网络编程

+
+ +
+
+

网络协议 以下内容摘自百度百科: https://baike.baidu.com/item/网络协议/328636 https://baike.baidu.com/item/网络七层协议/6056879 网络协议指的是计算机网络中互相通信的对等实体......

+
+
+ + +
+ +
+
+

MQ详解

+
+ +
+
+

概念 MQ 即 messagequeue 消息队列,是分布式系统的重要组件,主要解决异步消息,应用解耦,消峰等问题。从而实现高可用,高性能,可伸缩和最终一致性的架构。使用较多的MQ有:activeMQ,rabbitMQ,kafka,metaMQ。 MQ优点 异步消息处理:可以......

+
+
+ + +
+ +
+
+

Java集合

+
+ +
+
+

概述 Java中的集合主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 如果你看过ArrayList类源码,就知道ArrayList底层是通过数组来存储元素的,所以如果严格来说,数组也算集合的一种......

+
+
+ + +
+ +
+
+

Java反射

+
+ +
+
+

概述 什么是反射 在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。 反射是Java语言的一个特性,它允许程序......

+
+
+ + +
+ +
+
+

常见故障排查及程序配置

+
+ +
+
+

故障排查基础 收录Linux常用命令,以下命令来自https://www.bilibili.com/video/BV14A411378a 关机/重启/注销 常用命令 作用 shutdown -h now 即刻关机 shutdown -h 10 10分钟后关机 shutdown -h 11:00 11:00关机 shutdown -h +10 预定时间关机(10......

+
+
+ + +
+ +
+
+

分布式事务详解

+
+ +
+
+

基础概念 什么是事务 什么是事务?举个例子:你去超市买东西,“一手交钱,一手交货"就是一个事务的例子,交钱和交货必须同时成功,事务才算成功,其中有一个环节失败,事务将会撤销所有已成功的活动。 所以事务可以看作是一次重大的活动......

+
+
+ + +
+ +
+
+

Object类方法

+
+ +
+
+

概览 Object 类位于 java.lang 包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承Object,成为Object的子类。 Object类可以显示继承,也可以隐式继承,效果都是一样的。 class A extends Object{ // to do } class A { // to do } Java Objec......

+
+
+ + +
+ +
+
+

微服务治理

+
+ +
+
+

什么是微服务架构 In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API。 These services are built around business capabilities and independently deployable by fully automated deployment machinery。 There is a bare minimum of centralized management of these services, which may be written in different programming......

+
+
+ + +
+ +
+
+

Redis详解

+
+ +
+
+

Redis概述 参考文章: https://www.runoob.com/redis/redis-intro.html https://www.redis.com.cn/redis-interview-questions.html 什么是Redis Redis(Remote Dictionary Server) Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API 的非关系型数据库。 简而言之,Redis是一个可基于内存亦可持久......

+
+
+ + +
+ +
+
+

Spring详解

+
+ +
+
+

概览 Spring是一个轻量级的Java开源框架,为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IOC)和面向切面(AOP)。 简单来说,Spring是一个分层的JavaSE/EE 一站式轻量级开源框架。在每一层都提供支持。......

+
+
+ + +
+ +
+
+

面试Java可能会被问到的问题

+
+ +
+
+

面试必问 自我介绍一下 你有什么职业规划 你为什么要离职 说一下你的优缺点 你的期望薪资是多少 你为什么要选择我们公司 你能否接受加班 你有对象了吗 你还有什么问题要问的吗 基础 说一下UDP、TCP及http与https 如何保证线程安全 线程池工作原理 如何避免死......

+
+
+ + +
+ +
+
+

JVM-垃圾回收器

+
+ +
+
+

垃圾回收器分类 垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。 由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。 Java不同版本新特性学习思路: 语法层面:Lambda表达式、s......

+
+
+ + +
+ +
+
+

Java多线程

+
+ +
+
+

相关概念 线程与进程 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。例如,一个正在运行的程序的实例就是一个进程。 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 一条线程指的是进程中一个单一......

+
+
+ + +
+ +
+
+

HashMap详解

+
+ +
+
+

相关概念 capacity: 容量,默认16; loadFactor: 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容; threshold: 阈值;阈值......

+
+
+ + +
+ +
+
+

JVM-相关概念

+
+ +
+
+

内存溢出 内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。 官方文档中对内存溢出的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。 由于GC一......

+
+
+ + +
+ +
+
+

面试中常见的问题

+
+ +
+
+

面试常见问题 自我介绍 个人经历可以进行适当包装,但是不能造假,一方面如果一旦被人拆穿,后果就不用我说了吧,另一方面如果你说谎,说了一些你自己不感兴趣的项目,在入职之后可能会被分配到该项目上; 在介绍的时候要说明你对面试的公司有什么用,根据不同类......

+
+
+ + +
+ + + +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/page/3/index.html b/blog-site/public/page/3/index.html new file mode 100644 index 00000000..e380bce1 --- /dev/null +++ b/blog-site/public/page/3/index.html @@ -0,0 +1,858 @@ + + + + + + + + + + + + 唯手熟尔 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+ + +
+
+

JVM-垃圾回收

+
+ +
+
+

垃圾回收 垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。 垃圾收集机制是Java的招牌能力,极大地提高了开发效率。 如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展......

+
+
+ + +
+ +
+
+

JVM-执行引擎

+
+ +
+
+

概述 执行引擎是Java虚拟机核心的组成部分之一,属于JVM的下层,里面包括 解释器、及时编译器、垃圾回收器。 “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力, 其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统......

+
+
+ + +
+ +
+
+

JVM-直接内存

+
+ +
+
+

直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。 直接内存是在Java堆外的、直接向系统申请的内存区间。 操作直接内存演示代码: public class MainTest { public static void main(String[] args) { ByteBuffer allocate = ByteBuffer.allocate(1024 * 1024 * 1024); System.out.println(&#......

+
+
+ + +
+ +
+
+

JVM-Java对象

+
+ +
+
+

对象实例化 对象的创建方式 使用new关键字创建:最常见的方式、单例类中调用getInstance的静态类方法,XXXFactory的静态方法; 使用反射方式创建: 使用Class的newInstance方法:在JDK9里面被标记为过时的方法,因为......

+
+
+ + +
+ +
+
+

Java语法糖

+
+ +
+
+

原文地址:https://www.jianshu.com/p/0f967298a5d7 语法糖 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法, 这种语法对语言的功能并没有......

+
+
+ + +
+ +
+
+

JavaIO

+
+ +
+
+

概念 Java IO通过数据流、序列化和文件系统提供系统输入和输出。 IO,即 in 和 out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。 传统的 IO 是通过流技术来处理的。 流(Stream),是一个抽象的概念,......

+
+
+ + +
+ +
+
+

JVM-方法区

+
+ +
+
+

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......

+
+
+ + +
+ +
+
+

JVM-堆

+
+ +
+
+

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......

+
+
+ + +
+ +
+
+

JVM-本地方法接口

+
+ +
+
+

概念 简单地讲,一个Native Methodt是一个Java调用非Java代码的接囗。 一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。 这个特征并非Java所特有,很多其它的编程语言都有这一机制,比......

+
+
+ + +
+ +
+
+

JVM-本地方法栈

+
+ +
+
+

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......

+
+
+ + +
+ +
+
+

JVM-虚拟机栈

+
+ +
+
+

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......

+
+
+ + +
+ +
+
+

JVM-程序计数寄存器

+
+ +
+
+

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......

+
+
+ + +
+ +
+
+

JVM-JVM介绍

+
+ +
+
+

为什么要学习JVM 大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。 一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要,......

+
+
+ + +
+ +
+
+

Nginx介绍

+
+ +
+
+

Nginx介绍 Nginx (“engine x”)是一个高性能的HTTP和反向代理服务器,特点是占有内存少,并发能力强,事实上Nginx的并发能力确实在同类型的网页服务器中表现较好. Nginx专为性能优化而开发,性能是其最重要的考量,实现上非常注重效率,......

+
+
+ + +
+ +
+
+

道德经

+
+ +
+
+

第一章 道可道,非常道。名可名,非常名。 无名天地之始﹔有名万物之母。 故常无,欲以观其妙﹔常有,欲以观其徼。 此两者,同出而异名,同谓之玄。 玄之又玄,众妙之门。 第二章 天下皆知美之为美,斯恶已。 皆知善之为善,斯不善已。 有无相生,难易相成,长短相形,......

+
+
+ + +
+ +
+
+

面向对象

+
+ +
+
+

面向对象是一种编程思想,包括三大特性和六大原则,其中,三大特性指的是封装、继承和多态;六大原则指的是单一职责原则、开放封闭原则、迪米特原则、里氏替换原则、依赖倒置原则以及接口隔离原则,其中,单一职责原则是指一个类应该是一组相关性很高的函数和......

+
+
+ + +
+ +
+
+

JVM-Java类加载机制

+
+ +
+
+

类加载过程 在Java中,类加载器把一个类装入JVM中,要经过以下步骤: 加载、验证、准备、解析和初始化。其中验证,准备,解析统称为连接。 这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。 类加载器只负责class文......

+
+
+ + +
+ +
+
+

Java运算

+
+ +
+
+

运算符与表达式 运算符 运算符指明对操作数的运算方式。组成表达式的Java操作符有很多种。运算符按照其要求的操作数数目来分,可以有单目运算符、双目运算符和三目运算符,它们分别对应于1个、2个、3个操作数。 种类 运算符按其功能来分:有算术运算符、赋......

+
+
+ + +
+ +
+
+

Java数据类型

+
+ +
+
+

基本类型 Java语言提供了八种基本类型。六种数值类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. 俗称4类8种 这里只介绍称4类8种.实际上,JAVA中还存在另外一种基本类型 void,它也有对应的包装类java.lang.Void......

+
+
+ + +
+ +
+
+

Java异常

+
+ +
+
+

异常类型 Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:Error 和 Exception。 其中 Error 用来表示Java程序无法处理的错误;这类错误一般与硬件有关,与程序本身无关,通常由系统进行处理,程序本身无法捕获和处理。是不可控制的。 Exception 分为两种......

+
+
+ + +
+ + + +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/page/4/index.html b/blog-site/public/page/4/index.html new file mode 100644 index 00000000..ae6ee59b --- /dev/null +++ b/blog-site/public/page/4/index.html @@ -0,0 +1,854 @@ + + + + + + + + + + + + 唯手熟尔 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+ + +
+
+

20201124简历

+
+ +
+
+

自我介绍 1998 · 李济芝 河北唐山 15176733539  m15176733539@163.com 专业技能 熟练使用 SSM,SpringBoot等框架技术; 熟练使用HTML,CSS等相关技术; 有Redis,VUE相关使用经验; 有对接第三方系统,调用外系统相关经验; 熟悉 MySQL,ORACLE.基本操作,熟练使......

+
+
+ + +
+ +
+
+

SpringBoot整合docker

+
+ +
+
+

MacOS上安装docker 下载 国内下载网站: http://get.daocloud.io 不推荐下载docker版本太旧了 官网下载: https://docs.docker.com/get-started/#download-and-install-docker 或用homebrew进行下载安装 brew install --cask --appdir=/Applications docker 配置镜像 由于网速原因,可以配置一下国内的镜像加速器 中科大镜像: https://docker.mirrors.ustc.edu.cn 网易: https://hub-mirror.c.163.com 阿里云: https://<你......

+
+
+ + +
+ +
+
+

SpringBoot整合kafka

+
+ +
+
+

kafka介绍 kafka官网: http://kafka.apache.org kafka中文官网: https://kafka.apachecn.org Kafka是一种分布式的,基于发布/订阅的消息系统。主要特点如下: 以时间复杂度为O(1)的方式提供消息持久化能力,并保证即使对TB级以上数据也能保证常数时间的访问性能 高吞吐率。即使在非常......

+
+
+ + +
+ +
+
+

线程状态及创建方式

+
+ +
+
+

线程状态及转换 线程状态共包含6种,6中状态又可以互相的转换。 新建状态(New): 创建了线程后尚未启动; 可运行状态(Runnable): 可能正在运行,也可能正在等待 CPU 时间片。包含了运行中(Running)和 就绪(Ready)状态; 就绪(Rea......

+
+
+ + +
+ +
+
+

Docker介绍

+
+ +
+
+

docker是什么 Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前流行的 Linux 容器解决方案。 Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样......

+
+
+ + +
+ +
+
+

Java中常用到的锁

+
+ +
+
+

公平锁 指多个线程按照申请锁的顺序来获取锁类似排队打饭 先来后到 优点: 所有的线程都能得到资源,不会饿死在队列中。 缺点: 吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。 非公平锁 指在多线程获取锁的顺序并......

+
+
+ + +
+ +
+
+

Java中集合的线程不安全问题

+
+ +
+
+

ArrayList ArrayList线程不安全示例: public static void main(String[] args) { ArrayList<String> arrayList = new ArrayList<>(); for(int i=0; i< 3; i++) { new Thread(() -> { arrayList.add(UUID.randomUUID().toString()); System.out.println(arrayList); },String.valueOf(i)).start(); } } // ConcurrentModificationException 同步修改异常 Exception in thread "8" java.util.ConcurrentModificationException [null, 2041b613-8068-4ddd-9d01-305f5680d377] [null, 2041b613-8068-4ddd-9d01-305f5680d377, b3e0296d-e263-4632-a023-4267cdec5e25] [null, 2041b613-8068-4ddd-9d01-305f5680d377] 原因分析: 当某个线程正在执行 add()方法时,被某个线程打断,添加到一半被打断,没有被添加完 解决方案: 使用Vec......

+
+
+ + +
+ +
+
+

CAS原理

+
+ +
+
+

CAS CAS全称为Compare and Swap被译为比较并交换。是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。 java.util.concurrent.atomic 并发包下的所有原子类都是基于 CAS 来实现的。 以 AtomicInteger 原子整型类为例。 public class MainTest { public static void main(String[] args) { new AtomicInteger().compareAndSet(1,2); } } 以上面的代码为例......

+
+
+ + +
+ +
+
+

SpringBoot整合redis

+
+ +
+
+

Redis介绍 redis是开源的一个高性能的 key-value 数据库。 主要特点 Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用 Redis支持数据的备份,即master-slave模式的数据备份 Redis 可以存储键与5种不同......

+
+
+ + +
+ +
+
+

SpringBoot整合elasticsearch

+
+ +
+
+

安装elasticsearch 要注意导入依赖的版本和安装elasticsearch的版本与springboot的兼容问题 用 docker 安装 elasticsearch 本例用elasticsearch-6.5.3和springboot-2.1.0.RELEASE版本 下载镜像: docker......

+
+
+ + +
+ +
+
+

2019工作总结

+
+ +
+
+

本人在进入公司起,期间一直对自己要求严谨,遵守公司的相应制度. 在过去的一个月时间里,我参与了贵州银行的电子验印系统的开发,一直努力完成和完善分配给我的任务,在这一个月发现了自身还有很多的不足,所以抱着虚心学习的态度,学习公司的开发流程,了解......

+
+
+ + +
+ +
+
+

Vue2.0学习笔记

+
+ +
+
+

参考资料 vue官方文档: https://cn.vuejs.org/v2/guide vue参考视频资料: https://www.bilibili.com/video/av50680998 vue菜鸟教程文档: https://www.runoob.com/vue2/vue-tutorial.html vue-组件 参考资料: https://cn.vuejs.org/v2/guide/components.html#ad 组件是可复用的 Vue 实例,且带有一个名字. 组件的出现是为了拆分vue实例的代码量,能够让我们以不同的组件,来划分不同的功能模块,将来我们需要什么样的功......

+
+
+ + +
+ +
+
+

Js雪花飘落

+
+ +
+
+

index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>snow</title> </head> <style> html { width: 100%; } body { margin: 0; padding: 0; overflow-y: hidden; width: 100%; } .header { width: 100%; height: 315px; background: url("images/header-bg.png") repeat; } .snow { position: relative; height: inherit; width: 960px; background: url("images/con-bg.png") no-repeat 0 204px, url("images/snow-bg.png") no-repeat 0 0;; margin: 0 auto; animation: auto 10s linear infinite; } /* 下雪动画 插入两个背景图片*/ @keyframes auto { from { background: url("images/con-bg.png") no-repeat 0 204px, url("images/snow-bg.png") repeat 0 0; } to { background: url("images/con-bg.png") no-repeat 0 204px, url("images/snow-bg.png") repeat 0 1000px; } } tree, snow { position: absolute; } tree { width: 112px; height: 137px; background: url("images/tree.png");......

+
+
+ + +
+ +
+
+

Js下雨特效

+
+ +
+
+

index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title> rain </title> <style> html { width: 100%; } body { width: 100%; margin: 0; padding: 0; background-color: #000; } .rain { display: block; } embed { display: block; } </style> </head> <body> <!-- 2、使用hidden="true"表示隐藏音乐播放按钮,相反使用hidden="false"表示开启音乐播放按钮。 3、使用a......

+
+
+ + +
+ +
+
+

Js换肤特效

+
+ +
+
+

index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>换肤特效</title> <style type="text/css"> body { margin: 0; background-image: url("images/1.jpg"); background-size: cover; } ul { margin: 0; padding: 0; list-style-type: none; } .bg-list { display: none; margin: 0; width: 100%; height: 200px; background: rgba(0, 0, 0, 0.5); } .img-wrap { height: 200px; display: flex; justify-content: space-around; align-items: center; } .tab-btn { background-image: url("images/upseek.png"); height: 50px; width: 50px; position: fixed; top: 0; right: 0; } .tab-btn:hover { background-position-y: -63.6px; } </style> </head> <body> <div class="bg-list"> <ul class="img-wrap"> <li class="img-item" data-src="images/1.jpg"> <img src="images/1-1.jpg" width="160px"/> </li>......

+
+
+ + +
+ +
+
+

Js折纸导航栏

+
+ +
+
+

index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>折纸导航栏</title> </head> <style> *{ margin: 0; padding: 0; } .content{ position: relative; width: 400px; height: 30px; margin: 50px auto; /*-webkit-perspective: 1000px; -moz-perspective: 1000px; -ms-perspective: 1000px;*/ perspective: 1000px;/*景深相当于眼睛距离元素的位置距离*/ } .content .open{ transform: rotateX(0); animation: open 1s linear; } @keyframes open { 0%{ transform: rotateX(-90deg); } 20%{ transform:rotateX(30deg); } 40%{ transform:rotateX(-60deg); } 60%{ transform:rotateX(60deg); } 80%{ transform:rotateX(-30deg);......

+
+
+ + +
+ +
+
+

Js表白神器

+
+ +
+
+

index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>love</title> <style> *{ margin: 0; padding: 0; } body{ background-color: #000; background-size: cover; overflow-y: hidden; } .love{ width: 400px; height: 400px; /*background-color: #7c7c7c;*/ margin: 130px auto; animation: move 1s infinite alternate; } @keyframes move { 100%{ transform: scale(1.5); } } .left{ float: left; width: 150px; height: 250px; background-color: #FF0000; border-radius: 75px 75px 0 5px; -webkit-transform: rotate(-45deg); -moz-transform: rotate(-45deg); -ms-transform: rotate(-45deg); -o-transform: rotate(-45deg); transform: rotate(-45deg); margin-left: 85px; box-shadow: 0 0 20px #FF0000; animation: shadow 1s infinite alternate; } @keyframes shadow { 100%{ box-shadow: 0 0 100px #FF0000; } } .right{ float: left; width: 150px; height: 250px; background-color: #FF0000; border-radius: 75px 75px 5px 0; -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -ms-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg);......

+
+
+ + +
+ +
+
+

Js懒加载

+
+ +
+
+

index.html <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="Generator" content="EditPlus®"> <meta name="Author" content=""> <meta name="Keywords" content=""> <meta name="Description" content=""> <title>懒加载技术</title> <style> *{ margin: 0; padding:0; } body{ background: rgb(0,0,0); } .box{ overflow: hidden; width: 948px; background-color: #7c7c7c; margin: 50px auto; -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; } .box img{ float: left; display: block; width: 300px; height: 150px; margin:......

+
+
+ + +
+ +
+
+

Js五子棋

+
+ +
+
+

index.html <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>五子棋</title> <meta name="viewport" content="device-width; initial-scale=1.0;" /> <style> #c1 { display: block; margin: 60px auto; box-shadow: 1px 1px 5px #000000; } </style> <script src="js/index.js"></script> </head> <body> <canvas id="c1" width="450px" height="450px"></canvas> </body> </html> index.js window.onload = function(){ var oC = document.getElementById('c1'); var oGc = oC.getContext('2d'); var over = false; oGc.strokeStyle = "#bfbfbf"; //绘制棋盘 for(var i=0;i<15;i++){ oGc.moveTo(15+i*30,15); oGc.lineTo(15+i*30,435); oGc.stroke(); oGc.moveTo(15,15+i*30); oGc.lineTo(435,15+i*30); oGc.stroke(); } /* AI难点解析 赢法......

+
+
+ + +
+ +
+
+

Js滑块拖拽

+
+ +
+
+

index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>滑块拖拽</title> </head> <style> body { margin: 0; padding: 0; user-select: none; } .content { position: relative; width: 300px; height: 40px; margin: 50px auto; background-color: #E8E8EB; text-align: center; line-height: 40px; } .rect { position: absolute; width: 100%; height: 100%; } .rect .bg { position: absolute; left: 0; top: 0; z-index: 1; width: 0; height: 100%; background: rgba(122,194,60,.4); } .rect .move { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; width: 45px; height: 40px; position: absolute; top: 0; left: 0; background-color: #fff; border: 1px solid #cccccc;......

+
+
+ + +
+ + + +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/page/5/index.html b/blog-site/public/page/5/index.html new file mode 100644 index 00000000..c66281c0 --- /dev/null +++ b/blog-site/public/page/5/index.html @@ -0,0 +1,227 @@ + + + + + + + + + + + + 唯手熟尔 | + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+ + +
+
+

Js生日礼物

+
+ +
+
+

index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>card</title> <style> body,html{ width: 100%; height: 100%; } body{ display: flex;/*弹性盒模型*/ justify-content: center;/*水平对齐 盒子位于中心*/ align-items: center;/*竖直对齐 居中对齐*/ background-color: yellow; perspective: 1000px;/*景深:眼到屏幕的距离*/ } body,h1,p{ margin: 0; } .card{ width: 520px; height: 350px; border-radius: 15px; background: linear-gradient(#020333 70%,#fff 75%);/*......

+
+
+ + +
+ + + +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/posts/annex/images/application/Springboot\345\257\271\345\272\224elasticseach\347\211\210\346\234\254.jpg" "b/blog-site/public/posts/annex/images/application/Springboot\345\257\271\345\272\224elasticseach\347\211\210\346\234\254.jpg" new file mode 100644 index 00000000..6d7693c7 Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/Springboot\345\257\271\345\272\224elasticseach\347\211\210\346\234\254.jpg" differ diff --git a/blog-site/public/posts/annex/images/application/dockerFile.jpg b/blog-site/public/posts/annex/images/application/dockerFile.jpg new file mode 100644 index 00000000..6e00c71c Binary files /dev/null and b/blog-site/public/posts/annex/images/application/dockerFile.jpg differ diff --git "a/blog-site/public/posts/annex/images/application/docker\344\277\241\346\201\257.jpg" "b/blog-site/public/posts/annex/images/application/docker\344\277\241\346\201\257.jpg" new file mode 100644 index 00000000..9f2b7140 Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/docker\344\277\241\346\201\257.jpg" differ diff --git "a/blog-site/public/posts/annex/images/application/docker\346\211\223\345\214\205.jpg" "b/blog-site/public/posts/annex/images/application/docker\346\211\223\345\214\205.jpg" new file mode 100644 index 00000000..8e4575b3 Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/docker\346\211\223\345\214\205.jpg" differ diff --git "a/blog-site/public/posts/annex/images/application/docker\346\236\266\346\236\204.png" "b/blog-site/public/posts/annex/images/application/docker\346\236\266\346\236\204.png" new file mode 100644 index 00000000..16c04ca3 Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/docker\346\236\266\346\236\204.png" differ diff --git "a/blog-site/public/posts/annex/images/application/docker\351\205\215\347\275\256.jpg" "b/blog-site/public/posts/annex/images/application/docker\351\205\215\347\275\256.jpg" new file mode 100644 index 00000000..335f2646 Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/docker\351\205\215\347\275\256.jpg" differ diff --git a/blog-site/public/posts/annex/images/application/hellodocker.jpg b/blog-site/public/posts/annex/images/application/hellodocker.jpg new file mode 100644 index 00000000..622fd344 Binary files /dev/null and b/blog-site/public/posts/annex/images/application/hellodocker.jpg differ diff --git "a/blog-site/public/posts/annex/images/application/kafka\346\266\210\350\264\271\350\200\205.jpg" "b/blog-site/public/posts/annex/images/application/kafka\346\266\210\350\264\271\350\200\205.jpg" new file mode 100644 index 00000000..dd9e42f6 Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/kafka\346\266\210\350\264\271\350\200\205.jpg" differ diff --git "a/blog-site/public/posts/annex/images/application/kafka\347\224\237\344\272\247\350\200\205.jpg" "b/blog-site/public/posts/annex/images/application/kafka\347\224\237\344\272\247\350\200\205.jpg" new file mode 100644 index 00000000..8e67c23d Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/kafka\347\224\237\344\272\247\350\200\205.jpg" differ diff --git "a/blog-site/public/posts/annex/images/application/nginx\345\212\250\351\235\231\345\210\206\347\246\273.jpg" "b/blog-site/public/posts/annex/images/application/nginx\345\212\250\351\235\231\345\210\206\347\246\273.jpg" new file mode 100644 index 00000000..a45a9c51 Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/nginx\345\212\250\351\235\231\345\210\206\347\246\273.jpg" differ diff --git "a/blog-site/public/posts/annex/images/application/nginx\345\217\215\345\220\221\344\273\243\347\220\206.jpg" "b/blog-site/public/posts/annex/images/application/nginx\345\217\215\345\220\221\344\273\243\347\220\206.jpg" new file mode 100644 index 00000000..cc6c44fe Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/nginx\345\217\215\345\220\221\344\273\243\347\220\206.jpg" differ diff --git "a/blog-site/public/posts/annex/images/application/nginx\345\256\211\350\243\205\350\267\257\345\276\204.jpg" "b/blog-site/public/posts/annex/images/application/nginx\345\256\211\350\243\205\350\267\257\345\276\204.jpg" new file mode 100644 index 00000000..9c9bdffb Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/nginx\345\256\211\350\243\205\350\267\257\345\276\204.jpg" differ diff --git "a/blog-site/public/posts/annex/images/application/nginx\346\234\254\345\234\260\350\267\257\345\276\204.jpg" "b/blog-site/public/posts/annex/images/application/nginx\346\234\254\345\234\260\350\267\257\345\276\204.jpg" new file mode 100644 index 00000000..9c00c40e Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/nginx\346\234\254\345\234\260\350\267\257\345\276\204.jpg" differ diff --git "a/blog-site/public/posts/annex/images/application/nginx\346\265\213\350\257\225\344\277\256\346\224\271hosts\346\226\207\344\273\266.jpg" "b/blog-site/public/posts/annex/images/application/nginx\346\265\213\350\257\225\344\277\256\346\224\271hosts\346\226\207\344\273\266.jpg" new file mode 100644 index 00000000..37187806 Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/nginx\346\265\213\350\257\225\344\277\256\346\224\271hosts\346\226\207\344\273\266.jpg" differ diff --git "a/blog-site/public/posts/annex/images/application/nginx\350\264\237\350\275\275\345\235\207\350\241\241.png" "b/blog-site/public/posts/annex/images/application/nginx\350\264\237\350\275\275\345\235\207\350\241\241.png" new file mode 100644 index 00000000..00540f95 Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/nginx\350\264\237\350\275\275\345\235\207\350\241\241.png" differ diff --git "a/blog-site/public/posts/annex/images/application/springmvc\345\216\237\347\220\206.jpg" "b/blog-site/public/posts/annex/images/application/springmvc\345\216\237\347\220\206.jpg" new file mode 100644 index 00000000..95973214 Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/springmvc\345\216\237\347\220\206.jpg" differ diff --git "a/blog-site/public/posts/annex/images/application/\346\265\213\350\257\225nginx\346\210\220\345\212\237.jpg" "b/blog-site/public/posts/annex/images/application/\346\265\213\350\257\225nginx\346\210\220\345\212\237.jpg" new file mode 100644 index 00000000..12a1829e Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/\346\265\213\350\257\225nginx\346\210\220\345\212\237.jpg" differ diff --git "a/blog-site/public/posts/annex/images/application/\350\216\267\345\217\226\351\230\277\351\207\214\344\272\221docker\345\234\260\345\235\200.jpg" "b/blog-site/public/posts/annex/images/application/\350\216\267\345\217\226\351\230\277\351\207\214\344\272\221docker\345\234\260\345\235\200.jpg" new file mode 100644 index 00000000..53801ac1 Binary files /dev/null and "b/blog-site/public/posts/annex/images/application/\350\216\267\345\217\226\351\230\277\351\207\214\344\272\221docker\345\234\260\345\235\200.jpg" differ diff --git a/blog-site/public/posts/annex/images/essays/-XXPrintGCDetails.png b/blog-site/public/posts/annex/images/essays/-XXPrintGCDetails.png new file mode 100644 index 00000000..4fde6f3d Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/-XXPrintGCDetails.png differ diff --git "a/blog-site/public/posts/annex/images/essays/@SpringbootApplication\345\216\237\347\220\206.png" "b/blog-site/public/posts/annex/images/essays/@SpringbootApplication\345\216\237\347\220\206.png" new file mode 100644 index 00000000..a572d8f2 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/@SpringbootApplication\345\216\237\347\220\206.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/AOP@EnableAspectJAutoProxy\345\216\237\347\220\206.png" "b/blog-site/public/posts/annex/images/essays/AOP@EnableAspectJAutoProxy\345\216\237\347\220\206.png" new file mode 100644 index 00000000..9a433ca5 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/AOP@EnableAspectJAutoProxy\345\216\237\347\220\206.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/AOP\346\240\270\345\277\203\347\273\204\344\273\266.png" "b/blog-site/public/posts/annex/images/essays/AOP\346\240\270\345\277\203\347\273\204\344\273\266.png" new file mode 100644 index 00000000..40c98717 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/AOP\346\240\270\345\277\203\347\273\204\344\273\266.png" differ diff --git a/blog-site/public/posts/annex/images/essays/AQS.png b/blog-site/public/posts/annex/images/essays/AQS.png new file mode 100644 index 00000000..f7b67857 Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/AQS.png differ diff --git "a/blog-site/public/posts/annex/images/essays/AQS\347\256\200\345\215\225\347\220\206\350\247\243.png" "b/blog-site/public/posts/annex/images/essays/AQS\347\256\200\345\215\225\347\220\206\350\247\243.png" new file mode 100644 index 00000000..49604b3e Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/AQS\347\256\200\345\215\225\347\220\206\350\247\243.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Aware\345\255\220\346\216\245\345\217\243.png" "b/blog-site/public/posts/annex/images/essays/Aware\345\255\220\346\216\245\345\217\243.png" new file mode 100644 index 00000000..3635a248 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Aware\345\255\220\346\216\245\345\217\243.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/CPU\345\244\232\347\272\247\347\274\223\345\255\230.jpg" "b/blog-site/public/posts/annex/images/essays/CPU\345\244\232\347\272\247\347\274\223\345\255\230.jpg" new file mode 100644 index 00000000..32d7713d Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/CPU\345\244\232\347\272\247\347\274\223\345\255\230.jpg" differ diff --git a/blog-site/public/posts/annex/images/essays/ConcurrentHashMap.png b/blog-site/public/posts/annex/images/essays/ConcurrentHashMap.png new file mode 100644 index 00000000..db8de5f7 Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/ConcurrentHashMap.png differ diff --git a/blog-site/public/posts/annex/images/essays/CyclicBarrier.gif b/blog-site/public/posts/annex/images/essays/CyclicBarrier.gif new file mode 100644 index 00000000..a356ea0a Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/CyclicBarrier.gif differ diff --git "a/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-01.png" "b/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-01.png" new file mode 100644 index 00000000..22f5ba48 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-01.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-02.png" "b/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-02.png" new file mode 100644 index 00000000..30535e6f Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-02.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-03.png" "b/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-03.png" new file mode 100644 index 00000000..f5da508b Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-03.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-04.png" "b/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-04.png" new file mode 100644 index 00000000..40a3f0ff Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-04.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-05.png" "b/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-05.png" new file mode 100644 index 00000000..13d717dd Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-05.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-06.png" "b/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-06.png" new file mode 100644 index 00000000..8334a617 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Elasticsearch\350\257\246\350\247\243-06.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/GC\344\275\234\347\224\250\345\214\272\345\237\237.png" "b/blog-site/public/posts/annex/images/essays/GC\344\275\234\347\224\250\345\214\272\345\237\237.png" new file mode 100644 index 00000000..dd2167b2 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/GC\344\275\234\347\224\250\345\214\272\345\237\237.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/HashMap\347\273\223\346\236\204.png" "b/blog-site/public/posts/annex/images/essays/HashMap\347\273\223\346\236\204.png" new file mode 100644 index 00000000..2321d740 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/HashMap\347\273\223\346\236\204.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Hotspot\347\274\226\350\257\221\345\231\250\350\247\243\351\207\212\345\231\250\345\210\207\346\215\242.png" "b/blog-site/public/posts/annex/images/essays/Hotspot\347\274\226\350\257\221\345\231\250\350\247\243\351\207\212\345\231\250\345\210\207\346\215\242.png" new file mode 100644 index 00000000..c7815960 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Hotspot\347\274\226\350\257\221\345\231\250\350\247\243\351\207\212\345\231\250\345\210\207\346\215\242.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/IO\344\270\216NIO-1.png" "b/blog-site/public/posts/annex/images/essays/IO\344\270\216NIO-1.png" new file mode 100644 index 00000000..af22d397 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/IO\344\270\216NIO-1.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/IO\344\270\216NIO-2.png" "b/blog-site/public/posts/annex/images/essays/IO\344\270\216NIO-2.png" new file mode 100644 index 00000000..06fb6046 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/IO\344\270\216NIO-2.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/IO\345\244\232\350\267\257\345\244\215\347\224\250\346\250\241\345\236\213.png" "b/blog-site/public/posts/annex/images/essays/IO\345\244\232\350\267\257\345\244\215\347\224\250\346\250\241\345\236\213.png" new file mode 100644 index 00000000..973223b4 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/IO\345\244\232\350\267\257\345\244\215\347\224\250\346\250\241\345\236\213.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/IO\350\257\273\345\206\231.png" "b/blog-site/public/posts/annex/images/essays/IO\350\257\273\345\206\231.png" new file mode 100644 index 00000000..978e9110 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/IO\350\257\273\345\206\231.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/JDK6\345\255\227\347\254\246\344\270\262\345\270\270\351\207\217\346\261\240\346\241\210\344\276\213\350\247\243\346\236\220.png" "b/blog-site/public/posts/annex/images/essays/JDK6\345\255\227\347\254\246\344\270\262\345\270\270\351\207\217\346\261\240\346\241\210\344\276\213\350\247\243\346\236\220.png" new file mode 100644 index 00000000..10c7aef1 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/JDK6\345\255\227\347\254\246\344\270\262\345\270\270\351\207\217\346\261\240\346\241\210\344\276\213\350\247\243\346\236\220.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/JDK78\345\255\227\347\254\246\344\270\262\345\270\270\351\207\217\346\261\240\346\241\210\344\276\213\350\247\243\346\236\220.png" "b/blog-site/public/posts/annex/images/essays/JDK78\345\255\227\347\254\246\344\270\262\345\270\270\351\207\217\346\261\240\346\241\210\344\276\213\350\247\243\346\236\220.png" new file mode 100644 index 00000000..e84e5609 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/JDK78\345\255\227\347\254\246\344\270\262\345\270\270\351\207\217\346\261\240\346\241\210\344\276\213\350\247\243\346\236\220.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/JUC.locks\345\214\205UML.png" "b/blog-site/public/posts/annex/images/essays/JUC.locks\345\214\205UML.png" new file mode 100644 index 00000000..99713f25 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/JUC.locks\345\214\205UML.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/JavaIO\346\265\201\345\210\206\347\261\273.png" "b/blog-site/public/posts/annex/images/essays/JavaIO\346\265\201\345\210\206\347\261\273.png" new file mode 100644 index 00000000..f6bc549a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/JavaIO\346\265\201\345\210\206\347\261\273.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Java\344\270\255\347\232\204\351\233\206\345\220\210-01.jpg" "b/blog-site/public/posts/annex/images/essays/Java\344\270\255\347\232\204\351\233\206\345\220\210-01.jpg" new file mode 100644 index 00000000..076acc6c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Java\344\270\255\347\232\204\351\233\206\345\220\210-01.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/Java\344\273\243\347\240\201\346\211\247\350\241\214\346\265\201\347\250\213.png" "b/blog-site/public/posts/annex/images/essays/Java\344\273\243\347\240\201\346\211\247\350\241\214\346\265\201\347\250\213.png" new file mode 100644 index 00000000..de6136be Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Java\344\273\243\347\240\201\346\211\247\350\241\214\346\265\201\347\250\213.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Java\345\206\205\345\255\230\346\250\241\345\236\213.jpg" "b/blog-site/public/posts/annex/images/essays/Java\345\206\205\345\255\230\346\250\241\345\236\213.jpg" new file mode 100644 index 00000000..979854a0 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Java\345\206\205\345\255\230\346\250\241\345\236\213.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/Java\345\257\271\350\261\241\347\232\204\345\270\203\345\261\200.png" "b/blog-site/public/posts/annex/images/essays/Java\345\257\271\350\261\241\347\232\204\345\270\203\345\261\200.png" new file mode 100644 index 00000000..a0f46ddb Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Java\345\257\271\350\261\241\347\232\204\345\270\203\345\261\200.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Java\345\274\202\345\270\270\345\210\206\347\261\273.png" "b/blog-site/public/posts/annex/images/essays/Java\345\274\202\345\270\270\345\210\206\347\261\273.png" new file mode 100644 index 00000000..dffde53c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Java\345\274\202\345\270\270\345\210\206\347\261\273.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Jdk1.6\346\226\271\346\263\225\345\214\272\345\217\230\345\214\226.png" "b/blog-site/public/posts/annex/images/essays/Jdk1.6\346\226\271\346\263\225\345\214\272\345\217\230\345\214\226.png" new file mode 100644 index 00000000..fe0e6ead Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Jdk1.6\346\226\271\346\263\225\345\214\272\345\217\230\345\214\226.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Jdk1.7\346\226\271\346\263\225\345\214\272\345\217\230\345\214\226.png" "b/blog-site/public/posts/annex/images/essays/Jdk1.7\346\226\271\346\263\225\345\214\272\345\217\230\345\214\226.png" new file mode 100644 index 00000000..7cdadee7 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Jdk1.7\346\226\271\346\263\225\345\214\272\345\217\230\345\214\226.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Jdk1.8\346\226\271\346\263\225\345\214\272\345\217\230\345\214\226.png" "b/blog-site/public/posts/annex/images/essays/Jdk1.8\346\226\271\346\263\225\345\214\272\345\217\230\345\214\226.png" new file mode 100644 index 00000000..3cf2a87a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Jdk1.8\346\226\271\346\263\225\345\214\272\345\217\230\345\214\226.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Jvm\345\206\205\345\255\230\345\233\276.png" "b/blog-site/public/posts/annex/images/essays/Jvm\345\206\205\345\255\230\345\233\276.png" new file mode 100644 index 00000000..752f60de Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Jvm\345\206\205\345\255\230\345\233\276.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Jvm\345\206\205\345\255\230\346\250\241\345\236\213.png" "b/blog-site/public/posts/annex/images/essays/Jvm\345\206\205\345\255\230\346\250\241\345\236\213.png" new file mode 100644 index 00000000..144561c7 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Jvm\345\206\205\345\255\230\346\250\241\345\236\213.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-001.png" "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-001.png" new file mode 100644 index 00000000..00c7cb5f Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-001.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-002.png" "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-002.png" new file mode 100644 index 00000000..4a150c4a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-002.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-003.png" "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-003.png" new file mode 100644 index 00000000..71798959 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-003.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-004.png" "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-004.png" new file mode 100644 index 00000000..0836b993 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-004.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-005.png" "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-005.png" new file mode 100644 index 00000000..01628e27 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-005.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-006.png" "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-006.png" new file mode 100644 index 00000000..4a329373 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-006.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-007.png" "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-007.png" new file mode 100644 index 00000000..f597aced Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-007.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-008.png" "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-008.png" new file mode 100644 index 00000000..1d56046f Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-008.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-009.png" "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-009.png" new file mode 100644 index 00000000..332facef Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-009.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-010.png" "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-010.png" new file mode 100644 index 00000000..3886cd63 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MQ\350\257\246\350\247\243-010.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-001.png" "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-001.png" new file mode 100644 index 00000000..a716f314 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-001.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-002.png" "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-002.png" new file mode 100644 index 00000000..26411dc3 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-002.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-003.png" "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-003.png" new file mode 100644 index 00000000..d237cadd Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-003.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-004.png" "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-004.png" new file mode 100644 index 00000000..6ecce429 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-004.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-005.png" "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-005.png" new file mode 100644 index 00000000..79569bee Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-005.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-006.png" "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-006.png" new file mode 100644 index 00000000..be841516 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-006.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-007.png" "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-007.png" new file mode 100644 index 00000000..dcb9bc91 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-007.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-008.png" "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-008.png" new file mode 100644 index 00000000..c90dd757 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-008.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-009.png" "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-009.png" new file mode 100644 index 00000000..0f43f1a6 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-009.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-010.png" "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-010.png" new file mode 100644 index 00000000..fb3c04ea Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-010.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-011.png" "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-011.png" new file mode 100644 index 00000000..b7e293b9 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-011.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-012.png" "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-012.png" new file mode 100644 index 00000000..13ea0c06 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-012.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-013.png" "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-013.png" new file mode 100644 index 00000000..6bc08d00 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-013.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-014.png" "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-014.png" new file mode 100644 index 00000000..e67b656e Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/MySqlSQL\344\274\230\345\214\226\345\217\212\351\224\201\346\234\272\345\210\266-014.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/NIO\350\257\273\345\206\231.png" "b/blog-site/public/posts/annex/images/essays/NIO\350\257\273\345\206\231.png" new file mode 100644 index 00000000..5723f159 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/NIO\350\257\273\345\206\231.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Netty\346\250\241\345\236\213.png" "b/blog-site/public/posts/annex/images/essays/Netty\346\250\241\345\236\213.png" new file mode 100644 index 00000000..135f589e Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Netty\346\250\241\345\236\213.png" differ diff --git a/blog-site/public/posts/annex/images/essays/NewRatio.png b/blog-site/public/posts/annex/images/essays/NewRatio.png new file mode 100644 index 00000000..e4ef6b42 Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/NewRatio.png differ diff --git a/blog-site/public/posts/annex/images/essays/OOM.png b/blog-site/public/posts/annex/images/essays/OOM.png new file mode 100644 index 00000000..3250621f Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/OOM.png differ diff --git "a/blog-site/public/posts/annex/images/essays/PC\345\257\204\345\255\230\345\231\250\344\277\235\345\255\230\346\214\207\344\273\244\347\244\272\346\204\217.png" "b/blog-site/public/posts/annex/images/essays/PC\345\257\204\345\255\230\345\231\250\344\277\235\345\255\230\346\214\207\344\273\244\347\244\272\346\204\217.png" new file mode 100644 index 00000000..359dcb9c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/PC\345\257\204\345\255\230\345\231\250\344\277\235\345\255\230\346\214\207\344\273\244\347\244\272\346\204\217.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/PrintGCDetails\346\237\245\347\234\213\345\240\206\345\206\205\345\255\230.png" "b/blog-site/public/posts/annex/images/essays/PrintGCDetails\346\237\245\347\234\213\345\240\206\345\206\205\345\255\230.png" new file mode 100644 index 00000000..7a39c036 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/PrintGCDetails\346\237\245\347\234\213\345\240\206\345\206\205\345\255\230.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Reactor\346\250\241\345\274\217.png" "b/blog-site/public/posts/annex/images/essays/Reactor\346\250\241\345\274\217.png" new file mode 100644 index 00000000..70102a6e Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Reactor\346\250\241\345\274\217.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-001.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-001.png" new file mode 100644 index 00000000..f147e38b Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-001.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-002.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-002.png" new file mode 100644 index 00000000..1a6e298a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-002.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-003.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-003.png" new file mode 100644 index 00000000..ca51ffc3 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-003.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-004.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-004.png" new file mode 100644 index 00000000..dcb2b902 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-004.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-005.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-005.png" new file mode 100644 index 00000000..0adff428 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-005.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-006.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-006.png" new file mode 100644 index 00000000..badc28cd Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-006.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-007.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-007.png" new file mode 100644 index 00000000..544e4f5b Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-007.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-008.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-008.png" new file mode 100644 index 00000000..6e088c04 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-008.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-009.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-009.png" new file mode 100644 index 00000000..926f5ed2 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-009.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-010.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-010.png" new file mode 100644 index 00000000..f825143e Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-010.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-011.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-011.png" new file mode 100644 index 00000000..91504eb9 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-011.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-012.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-012.png" new file mode 100644 index 00000000..4eb60750 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-012.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-013.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-013.png" new file mode 100644 index 00000000..df78a099 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-013.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-014.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-014.png" new file mode 100644 index 00000000..7a7e41e4 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-014.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-015.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-015.png" new file mode 100644 index 00000000..b421d094 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-015.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-016.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-016.png" new file mode 100644 index 00000000..56b0c250 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-016.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-017.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-017.png" new file mode 100644 index 00000000..04762b06 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-017.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-018.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-018.png" new file mode 100644 index 00000000..8467d8df Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-018.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-019.gif" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-019.gif" new file mode 100644 index 00000000..1e379ae4 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-019.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-020.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-020.png" new file mode 100644 index 00000000..a6a1e07d Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-020.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-021.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-021.png" new file mode 100644 index 00000000..006fd9e8 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-021.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-022.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-022.png" new file mode 100644 index 00000000..c82547fd Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-022.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-023.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-023.png" new file mode 100644 index 00000000..e3d1d462 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-023.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-024.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-024.png" new file mode 100644 index 00000000..40957d89 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-024.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-025.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-025.png" new file mode 100644 index 00000000..205adcae Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-025.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-026.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-026.png" new file mode 100644 index 00000000..a5fdf06f Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-026.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-027.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-027.png" new file mode 100644 index 00000000..ade1a3fd Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-027.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-028.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-028.png" new file mode 100644 index 00000000..67df188c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-028.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-029.png" "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-029.png" new file mode 100644 index 00000000..8ab74ae9 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/Redis\350\257\246\350\247\243-029.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/SpringMVC\344\270\216SpringWebFlux-01.jpg" "b/blog-site/public/posts/annex/images/essays/SpringMVC\344\270\216SpringWebFlux-01.jpg" new file mode 100644 index 00000000..95973214 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/SpringMVC\344\270\216SpringWebFlux-01.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/SpringMVC\344\270\216SpringWebFlux-02.jpg" "b/blog-site/public/posts/annex/images/essays/SpringMVC\344\270\216SpringWebFlux-02.jpg" new file mode 100644 index 00000000..465001e3 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/SpringMVC\344\270\216SpringWebFlux-02.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/SpringMVC\344\270\216SpringWebFlux-03.jpg" "b/blog-site/public/posts/annex/images/essays/SpringMVC\344\270\216SpringWebFlux-03.jpg" new file mode 100644 index 00000000..3e180085 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/SpringMVC\344\270\216SpringWebFlux-03.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/SpringMVC\344\270\216SpringWebFlux-04.jpg" "b/blog-site/public/posts/annex/images/essays/SpringMVC\344\270\216SpringWebFlux-04.jpg" new file mode 100644 index 00000000..b7e42c7a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/SpringMVC\344\270\216SpringWebFlux-04.jpg" differ diff --git a/blog-site/public/posts/annex/images/essays/SurvivorRatio.png b/blog-site/public/posts/annex/images/essays/SurvivorRatio.png new file mode 100644 index 00000000..b2494607 Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/SurvivorRatio.png differ diff --git a/blog-site/public/posts/annex/images/essays/TLAB.png b/blog-site/public/posts/annex/images/essays/TLAB.png new file mode 100644 index 00000000..ef9e3f25 Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/TLAB.png differ diff --git "a/blog-site/public/posts/annex/images/essays/TLAB\345\210\206\351\205\215\350\277\207\347\250\213.png" "b/blog-site/public/posts/annex/images/essays/TLAB\345\210\206\351\205\215\350\277\207\347\250\213.png" new file mode 100644 index 00000000..579fa52e Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/TLAB\345\210\206\351\205\215\350\277\207\347\250\213.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/concurrentHashMap\347\232\204\345\206\205\351\203\250\347\273\223\346\236\204\345\233\276.jpeg" "b/blog-site/public/posts/annex/images/essays/concurrentHashMap\347\232\204\345\206\205\351\203\250\347\273\223\346\236\204\345\233\276.jpeg" new file mode 100644 index 00000000..125dc8d6 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/concurrentHashMap\347\232\204\345\206\205\351\203\250\347\273\223\346\236\204\345\233\276.jpeg" differ diff --git "a/blog-site/public/posts/annex/images/essays/eden\344\270\216survivor\344\270\216tenured.png" "b/blog-site/public/posts/annex/images/essays/eden\344\270\216survivor\344\270\216tenured.png" new file mode 100644 index 00000000..5b748a53 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/eden\344\270\216survivor\344\270\216tenured.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-01.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-01.jpg" new file mode 100644 index 00000000..0937b6b8 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-01.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-02.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-02.jpg" new file mode 100644 index 00000000..ffb487c4 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-02.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-03.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-03.jpg" new file mode 100644 index 00000000..73825a97 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-03.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-04.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-04.jpg" new file mode 100644 index 00000000..fb216775 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-04.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-05.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-05.jpg" new file mode 100644 index 00000000..5178ec12 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-05.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-06.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-06.jpg" new file mode 100644 index 00000000..745ed34b Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-06.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-07.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-07.jpg" new file mode 100644 index 00000000..4f39d59c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-07.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-08.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-08.jpg" new file mode 100644 index 00000000..f465eae8 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-08.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-09.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-09.jpg" new file mode 100644 index 00000000..2d8ed130 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-09.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-10.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-10.jpg" new file mode 100644 index 00000000..9d4248bb Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-10.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-11.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-11.jpg" new file mode 100644 index 00000000..2d278663 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-11.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-12.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-12.jpg" new file mode 100644 index 00000000..511fec0b Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-12.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-13.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-13.jpg" new file mode 100644 index 00000000..f2425028 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-13.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-14.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-14.jpg" new file mode 100644 index 00000000..ae09575c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-14.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-15.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-15.jpg" new file mode 100644 index 00000000..c8cc5cf9 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-15.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-16.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-16.jpg" new file mode 100644 index 00000000..2ddf5ff6 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-16.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-17.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-17.jpg" new file mode 100644 index 00000000..74be0e50 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-17.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-18.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-18.jpg" new file mode 100644 index 00000000..4c4d30e0 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-18.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-19.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-19.jpg" new file mode 100644 index 00000000..15db415b Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-19.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-20.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-20.jpg" new file mode 100644 index 00000000..bdbdf2d1 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-20.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-21.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-21.jpg" new file mode 100644 index 00000000..bdc9c616 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-21.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-22.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-22.jpg" new file mode 100644 index 00000000..9d9e264c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-22.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-23.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-23.jpg" new file mode 100644 index 00000000..e93c38e2 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-23.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-24.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-24.jpg" new file mode 100644 index 00000000..a99e769b Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-24.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-25.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-25.jpg" new file mode 100644 index 00000000..5cf68c03 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-25.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-26.jpg" "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-26.jpg" new file mode 100644 index 00000000..6027f45d Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\345\270\270\347\224\250\351\205\215\347\275\256-26.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\346\243\200\346\237\245serialVersionUID.png" "b/blog-site/public/posts/annex/images/essays/idea\346\243\200\346\237\245serialVersionUID.png" new file mode 100644 index 00000000..8131ec4a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\346\243\200\346\237\245serialVersionUID.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/idea\350\207\252\345\212\250\347\224\237\346\210\220serialVersionUID.png" "b/blog-site/public/posts/annex/images/essays/idea\350\207\252\345\212\250\347\224\237\346\210\220serialVersionUID.png" new file mode 100644 index 00000000..b8dbe46b Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/idea\350\207\252\345\212\250\347\224\237\346\210\220serialVersionUID.png" differ diff --git a/blog-site/public/posts/annex/images/essays/jdk1.7ConcurrentHashMap.png b/blog-site/public/posts/annex/images/essays/jdk1.7ConcurrentHashMap.png new file mode 100644 index 00000000..8b11a10e Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/jdk1.7ConcurrentHashMap.png differ diff --git a/blog-site/public/posts/annex/images/essays/jdk1.8ConcurrentHashMap.png b/blog-site/public/posts/annex/images/essays/jdk1.8ConcurrentHashMap.png new file mode 100644 index 00000000..044a6e79 Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/jdk1.8ConcurrentHashMap.png differ diff --git "a/blog-site/public/posts/annex/images/essays/jinfo\345\221\275\344\273\244\346\237\245\347\234\213GC.png" "b/blog-site/public/posts/annex/images/essays/jinfo\345\221\275\344\273\244\346\237\245\347\234\213GC.png" new file mode 100644 index 00000000..74496c49 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/jinfo\345\221\275\344\273\244\346\237\245\347\234\213GC.png" differ diff --git a/blog-site/public/posts/annex/images/essays/jstat.png b/blog-site/public/posts/annex/images/essays/jstat.png new file mode 100644 index 00000000..b7119da5 Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/jstat.png differ diff --git a/blog-site/public/posts/annex/images/essays/juc.locks.png b/blog-site/public/posts/annex/images/essays/juc.locks.png new file mode 100644 index 00000000..f19096a2 Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/juc.locks.png differ diff --git a/blog-site/public/posts/annex/images/essays/jvisual-OOM.png b/blog-site/public/posts/annex/images/essays/jvisual-OOM.png new file mode 100644 index 00000000..74567281 Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/jvisual-OOM.png differ diff --git a/blog-site/public/posts/annex/images/essays/jvm1.8.png b/blog-site/public/posts/annex/images/essays/jvm1.8.png new file mode 100644 index 00000000..c5088be5 Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/jvm1.8.png differ diff --git "a/blog-site/public/posts/annex/images/essays/jvm1.8\344\271\213\345\211\215.png" "b/blog-site/public/posts/annex/images/essays/jvm1.8\344\271\213\345\211\215.png" new file mode 100644 index 00000000..bf52c66e Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/jvm1.8\344\271\213\345\211\215.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/markword\345\257\271\350\261\241\347\212\266\346\200\201.png" "b/blog-site/public/posts/annex/images/essays/markword\345\257\271\350\261\241\347\212\266\346\200\201.png" new file mode 100644 index 00000000..9c727433 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/markword\345\257\271\350\261\241\347\212\266\346\200\201.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/netty\347\273\223\346\236\204.png" "b/blog-site/public/posts/annex/images/essays/netty\347\273\223\346\236\204.png" new file mode 100644 index 00000000..79e24558 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/netty\347\273\223\346\236\204.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/reentrantLock\345\212\240\351\224\201.png" "b/blog-site/public/posts/annex/images/essays/reentrantLock\345\212\240\351\224\201.png" new file mode 100644 index 00000000..0c6f154a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/reentrantLock\345\212\240\351\224\201.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/reentrantLock\350\247\243\351\224\201.png" "b/blog-site/public/posts/annex/images/essays/reentrantLock\350\247\243\351\224\201.png" new file mode 100644 index 00000000..91f5e545 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/reentrantLock\350\247\243\351\224\201.png" differ diff --git a/blog-site/public/posts/annex/images/essays/slot.png b/blog-site/public/posts/annex/images/essays/slot.png new file mode 100644 index 00000000..72df0e5e Binary files /dev/null and b/blog-site/public/posts/annex/images/essays/slot.png differ diff --git "a/blog-site/public/posts/annex/images/essays/slot\351\207\215\345\244\215\345\210\251\347\224\250.png" "b/blog-site/public/posts/annex/images/essays/slot\351\207\215\345\244\215\345\210\251\347\224\250.png" new file mode 100644 index 00000000..4fa2f07b Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/slot\351\207\215\345\244\215\345\210\251\347\224\250.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/synchronized\345\216\237\347\220\206.gif" "b/blog-site/public/posts/annex/images/essays/synchronized\345\216\237\347\220\206.gif" new file mode 100644 index 00000000..64d9a385 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/synchronized\345\216\237\347\220\206.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/volitile\345\255\227\350\212\202\347\240\201.png" "b/blog-site/public/posts/annex/images/essays/volitile\345\255\227\350\212\202\347\240\201.png" new file mode 100644 index 00000000..1c808e57 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/volitile\345\255\227\350\212\202\347\240\201.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\344\270\215\347\224\250intern\346\226\271\346\263\225\346\265\213\350\257\225.png" "b/blog-site/public/posts/annex/images/essays/\344\270\215\347\224\250intern\346\226\271\346\263\225\346\265\213\350\257\225.png" new file mode 100644 index 00000000..d1de56a4 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\344\270\215\347\224\250intern\346\226\271\346\263\225\346\265\213\350\257\225.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\344\270\262\350\241\214\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250.png" "b/blog-site/public/posts/annex/images/essays/\344\270\262\350\241\214\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250.png" new file mode 100644 index 00000000..b182f49c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\344\270\262\350\241\214\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\344\270\273\344\273\216Reactor\347\272\277\347\250\213.png" "b/blog-site/public/posts/annex/images/essays/\344\270\273\344\273\216Reactor\347\272\277\347\250\213.png" new file mode 100644 index 00000000..26af2bae Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\344\270\273\344\273\216Reactor\347\272\277\347\250\213.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\344\273\243\347\240\201\346\274\224\347\244\272\345\257\271\350\261\241\345\210\206\351\205\215\350\277\207\347\250\213.gif" "b/blog-site/public/posts/annex/images/essays/\344\273\243\347\240\201\346\274\224\347\244\272\345\257\271\350\261\241\345\210\206\351\205\215\350\277\207\347\250\213.gif" new file mode 100644 index 00000000..6727100e Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\344\273\243\347\240\201\346\274\224\347\244\272\345\257\271\350\261\241\345\210\206\351\205\215\350\277\207\347\250\213.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\344\274\240\347\273\237IO\346\250\241\345\236\213.png" "b/blog-site/public/posts/annex/images/essays/\344\274\240\347\273\237IO\346\250\241\345\236\213.png" new file mode 100644 index 00000000..6b0cd84d Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\344\274\240\347\273\237IO\346\250\241\345\236\213.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\344\275\277\347\224\250intern\346\226\271\346\263\225\346\265\213\350\257\225.png" "b/blog-site/public/posts/annex/images/essays/\344\275\277\347\224\250intern\346\226\271\346\263\225\346\265\213\350\257\225.png" new file mode 100644 index 00000000..a357714c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\344\275\277\347\224\250intern\346\226\271\346\263\225\346\265\213\350\257\225.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\206\205\345\255\230\346\263\204\346\274\217.png" "b/blog-site/public/posts/annex/images/essays/\345\206\205\345\255\230\346\263\204\346\274\217.png" new file mode 100644 index 00000000..0f736160 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\206\205\345\255\230\346\263\204\346\274\217.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-001.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-001.png" new file mode 100644 index 00000000..57a771da Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-001.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-002.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-002.png" new file mode 100644 index 00000000..12e5811b Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-002.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-003.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-003.png" new file mode 100644 index 00000000..e34fbad5 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-003.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-004.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-004.png" new file mode 100644 index 00000000..e952ad27 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-004.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-005.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-005.png" new file mode 100644 index 00000000..7fb0832c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-005.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-006.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-006.png" new file mode 100644 index 00000000..164a3cd2 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-006.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-007.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-007.png" new file mode 100644 index 00000000..b69fd53f Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-007.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-008.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-008.png" new file mode 100644 index 00000000..a171f049 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-008.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-009.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-009.png" new file mode 100644 index 00000000..d37b6afe Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-009.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-010.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-010.png" new file mode 100644 index 00000000..b3700dc7 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-010.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-011.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-011.png" new file mode 100644 index 00000000..e4f63460 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-011.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-012.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-012.png" new file mode 100644 index 00000000..c5420b1c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-012.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-013.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-013.png" new file mode 100644 index 00000000..f04facb7 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-013.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-014.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-014.png" new file mode 100644 index 00000000..2c8ced35 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-014.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-015.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-015.png" new file mode 100644 index 00000000..4c170629 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-015.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-016.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-016.png" new file mode 100644 index 00000000..7f28ae50 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-016.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-017.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-017.png" new file mode 100644 index 00000000..f6c44e36 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-017.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-018.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-018.png" new file mode 100644 index 00000000..26021eed Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241-018.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-001.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-001.png" new file mode 100644 index 00000000..9d60ebac Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-001.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-002.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-002.png" new file mode 100644 index 00000000..745604da Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-002.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-003.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-003.png" new file mode 100644 index 00000000..5664fa07 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-003.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-004.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-004.png" new file mode 100644 index 00000000..35bc2de5 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-004.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-005.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-005.png" new file mode 100644 index 00000000..1fa6a3e1 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-005.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-006.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-006.png" new file mode 100644 index 00000000..1f94dca9 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-006.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-007.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-007.png" new file mode 100644 index 00000000..2aab2484 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-007.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-008.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-008.png" new file mode 100644 index 00000000..d9f24bf6 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-008.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-009.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-009.png" new file mode 100644 index 00000000..cfc8b9be Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-009.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-010.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-010.png" new file mode 100644 index 00000000..df9b91dd Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-010.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-011.png" "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-011.png" new file mode 100644 index 00000000..fee9d1e1 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-011.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\210\233\345\273\272\345\257\271\350\261\241\345\255\227\350\212\202\347\240\201\346\214\207\344\273\244.png" "b/blog-site/public/posts/annex/images/essays/\345\210\233\345\273\272\345\257\271\350\261\241\345\255\227\350\212\202\347\240\201\346\214\207\344\273\244.png" new file mode 100644 index 00000000..e84509c5 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\210\233\345\273\272\345\257\271\350\261\241\345\255\227\350\212\202\347\240\201\346\214\207\344\273\244.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\212\250\346\200\201\351\223\276\346\216\245.png" "b/blog-site/public/posts/annex/images/essays/\345\212\250\346\200\201\351\223\276\346\216\245.png" new file mode 100644 index 00000000..705de99a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\212\250\346\200\201\351\223\276\346\216\245.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\215\225Reactor\345\215\225\347\272\277\347\250\213.png" "b/blog-site/public/posts/annex/images/essays/\345\215\225Reactor\345\215\225\347\272\277\347\250\213.png" new file mode 100644 index 00000000..fb901675 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\215\225Reactor\345\215\225\347\272\277\347\250\213.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\215\225Reactor\345\244\232\347\272\277\347\250\213.png" "b/blog-site/public/posts/annex/images/essays/\345\215\225Reactor\345\244\232\347\272\277\347\250\213.png" new file mode 100644 index 00000000..55530a74 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\215\225Reactor\345\244\232\347\272\277\347\250\213.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\217\214\344\272\262\345\247\224\346\264\276\346\250\241\345\236\213.png" "b/blog-site/public/posts/annex/images/essays/\345\217\214\344\272\262\345\247\224\346\264\276\346\250\241\345\236\213.png" new file mode 100644 index 00000000..32ee5e71 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\217\214\344\272\262\345\247\224\346\264\276\346\250\241\345\236\213.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\217\245\346\237\204\350\256\277\351\227\256.png" "b/blog-site/public/posts/annex/images/essays/\345\217\245\346\237\204\350\256\277\351\227\256.png" new file mode 100644 index 00000000..85301309 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\217\245\346\237\204\350\256\277\351\227\256.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\217\257\350\276\276\346\200\247\345\210\206\346\236\220\347\256\227\346\263\225.png" "b/blog-site/public/posts/annex/images/essays/\345\217\257\350\276\276\346\200\247\345\210\206\346\236\220\347\256\227\346\263\225.png" new file mode 100644 index 00000000..50b9d5a3 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\217\257\350\276\276\346\200\247\345\210\206\346\236\220\347\256\227\346\263\225.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250\347\273\204\345\220\210\345\205\263\347\263\273.png" "b/blog-site/public/posts/annex/images/essays/\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250\347\273\204\345\220\210\345\205\263\347\263\273.png" new file mode 100644 index 00000000..df07f7c0 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250\347\273\204\345\220\210\345\205\263\347\263\273.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\240\206\345\206\205\345\255\230\345\210\206\351\205\215.png" "b/blog-site/public/posts/annex/images/essays/\345\240\206\345\206\205\345\255\230\345\210\206\351\205\215.png" new file mode 100644 index 00000000..6a09ca71 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\240\206\345\206\205\345\255\230\345\210\206\351\205\215.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\240\206\345\206\205\345\255\230\345\210\222\345\210\206.png" "b/blog-site/public/posts/annex/images/essays/\345\240\206\345\206\205\345\255\230\345\210\222\345\210\206.png" new file mode 100644 index 00000000..e0b70eb6 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\240\206\345\206\205\345\255\230\345\210\222\345\210\206.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\255\227\347\254\246\346\265\201.png" "b/blog-site/public/posts/annex/images/essays/\345\255\227\347\254\246\346\265\201.png" new file mode 100644 index 00000000..0849008c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\255\227\347\254\246\346\265\201.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\255\227\350\212\202\346\265\201.png" "b/blog-site/public/posts/annex/images/essays/\345\255\227\350\212\202\346\265\201.png" new file mode 100644 index 00000000..8d04cb91 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\255\227\350\212\202\346\265\201.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\255\227\350\212\202\347\240\201\344\273\213\347\273\215.png" "b/blog-site/public/posts/annex/images/essays/\345\255\227\350\212\202\347\240\201\344\273\213\347\273\215.png" new file mode 100644 index 00000000..e2b8dbd1 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\255\227\350\212\202\347\240\201\344\273\213\347\273\215.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\256\232\346\227\266\344\273\273\345\212\241\345\217\257\350\247\206\345\214\226\347\256\241\347\220\206-01.png" "b/blog-site/public/posts/annex/images/essays/\345\256\232\346\227\266\344\273\273\345\212\241\345\217\257\350\247\206\345\214\226\347\256\241\347\220\206-01.png" new file mode 100644 index 00000000..66a52853 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\256\232\346\227\266\344\273\273\345\212\241\345\217\257\350\247\206\345\214\226\347\256\241\347\220\206-01.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\257\271\350\261\241\345\206\205\345\255\230\345\210\206\351\205\215\347\255\226\347\225\245.png" "b/blog-site/public/posts/annex/images/essays/\345\257\271\350\261\241\345\206\205\345\255\230\345\210\206\351\205\215\347\255\226\347\225\245.png" new file mode 100644 index 00000000..5ede294a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\257\271\350\261\241\345\206\205\345\255\230\345\210\206\351\205\215\347\255\226\347\225\245.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\257\271\350\261\241\345\210\233\345\273\272\346\255\245\351\252\244.png" "b/blog-site/public/posts/annex/images/essays/\345\257\271\350\261\241\345\210\233\345\273\272\346\255\245\351\252\244.png" new file mode 100644 index 00000000..fcc37a00 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\257\271\350\261\241\345\210\233\345\273\272\346\255\245\351\252\244.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\257\271\350\261\241\346\214\207\351\222\210\350\256\277\351\227\256.png" "b/blog-site/public/posts/annex/images/essays/\345\257\271\350\261\241\346\214\207\351\222\210\350\256\277\351\227\256.png" new file mode 100644 index 00000000..41ee4695 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\257\271\350\261\241\346\214\207\351\222\210\350\256\277\351\227\256.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\257\271\350\261\241\351\233\206\345\220\210GCroots.png" "b/blog-site/public/posts/annex/images/essays/\345\257\271\350\261\241\351\233\206\345\220\210GCroots.png" new file mode 100644 index 00000000..d5afa15c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\257\271\350\261\241\351\233\206\345\220\210GCroots.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\261\200\351\203\250\345\217\230\351\207\217\350\241\250.png" "b/blog-site/public/posts/annex/images/essays/\345\261\200\351\203\250\345\217\230\351\207\217\350\241\250.png" new file mode 100644 index 00000000..c650a0fd Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\261\200\351\203\250\345\217\230\351\207\217\350\241\250.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-001.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-001.png" new file mode 100644 index 00000000..cb4b81de Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-001.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-002.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-002.png" new file mode 100644 index 00000000..2c241f9f Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-002.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-003.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-003.png" new file mode 100644 index 00000000..36719ae8 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-003.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-004.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-004.png" new file mode 100644 index 00000000..86938f4e Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-004.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-005.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-005.png" new file mode 100644 index 00000000..9593e00b Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-005.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-006.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-006.png" new file mode 100644 index 00000000..87064b0d Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-006.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-007.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-007.png" new file mode 100644 index 00000000..10f3fb1a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-007.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-008.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-008.png" new file mode 100644 index 00000000..3cd47dbc Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-008.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-009.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-009.png" new file mode 100644 index 00000000..302baf01 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-009.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-010.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-010.png" new file mode 100644 index 00000000..4c165a4a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-010.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-011.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-011.png" new file mode 100644 index 00000000..7a616988 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-011.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-012.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-012.png" new file mode 100644 index 00000000..324218fd Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-012.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-013.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-013.png" new file mode 100644 index 00000000..725b523f Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-013.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-014.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-014.png" new file mode 100644 index 00000000..e0f86ed3 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-014.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-015.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-015.png" new file mode 100644 index 00000000..76b6b99c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-015.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-016.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-016.png" new file mode 100644 index 00000000..35d99220 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-016.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-017.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-017.png" new file mode 100644 index 00000000..cf360734 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-017.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-018.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-018.png" new file mode 100644 index 00000000..c65ba2af Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-018.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-019.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-019.png" new file mode 100644 index 00000000..2eb0e964 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\350\247\201\346\225\205\351\232\234\346\216\222\346\237\245\345\217\212\347\250\213\345\272\217\351\205\215\347\275\256-019.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\270\270\351\207\217\346\261\240.png" "b/blog-site/public/posts/annex/images/essays/\345\270\270\351\207\217\346\261\240.png" new file mode 100644 index 00000000..06784994 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\270\270\351\207\217\346\261\240.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\271\264\350\275\273\344\273\243\344\270\216\350\200\201\345\271\264\344\273\243.png" "b/blog-site/public/posts/annex/images/essays/\345\271\264\350\275\273\344\273\243\344\270\216\350\200\201\345\271\264\344\273\243.png" new file mode 100644 index 00000000..0f2e24fc Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\271\264\350\275\273\344\273\243\344\270\216\350\200\201\345\271\264\344\273\243.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\271\266\345\217\221\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250.png" "b/blog-site/public/posts/annex/images/essays/\345\271\266\345\217\221\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250.png" new file mode 100644 index 00000000..aab54304 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\271\266\345\217\221\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\271\266\350\241\214\344\270\216\345\271\266\345\217\221\345\214\272\345\210\253.png" "b/blog-site/public/posts/annex/images/essays/\345\271\266\350\241\214\344\270\216\345\271\266\345\217\221\345\214\272\345\210\253.png" new file mode 100644 index 00000000..2fa7a8b2 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\271\266\350\241\214\344\270\216\345\271\266\345\217\221\345\214\272\345\210\253.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\271\266\350\241\214\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250.png" "b/blog-site/public/posts/annex/images/essays/\345\271\266\350\241\214\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250.png" new file mode 100644 index 00000000..d78e4b87 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\271\266\350\241\214\345\236\203\345\234\276\345\233\236\346\224\266\345\231\250.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\274\202\345\270\270\350\241\250.png" "b/blog-site/public/posts/annex/images/essays/\345\274\202\345\270\270\350\241\250.png" new file mode 100644 index 00000000..129b9a53 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\274\202\345\270\270\350\241\250.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\274\272\350\275\257\345\274\261\350\231\232.jpg" "b/blog-site/public/posts/annex/images/essays/\345\274\272\350\275\257\345\274\261\350\231\232.jpg" new file mode 100644 index 00000000..5cc18cfb Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\274\272\350\275\257\345\274\261\350\231\232.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/\345\276\252\347\216\257\345\274\225\347\224\250.png" "b/blog-site/public/posts/annex/images/essays/\345\276\252\347\216\257\345\274\225\347\224\250.png" new file mode 100644 index 00000000..19ab2675 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\345\276\252\347\216\257\345\274\225\347\224\250.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\202\262\350\247\202\351\224\201\344\270\216\344\271\220\350\247\202\351\224\201.png" "b/blog-site/public/posts/annex/images/essays/\346\202\262\350\247\202\351\224\201\344\270\216\344\271\220\350\247\202\351\224\201.png" new file mode 100644 index 00000000..508760ba Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\202\262\350\247\202\351\224\201\344\270\216\344\271\220\350\247\202\351\224\201.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\211\247\350\241\214\345\274\225\346\223\216.png" "b/blog-site/public/posts/annex/images/essays/\346\211\247\350\241\214\345\274\225\346\223\216.png" new file mode 100644 index 00000000..25bf63fd Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\211\247\350\241\214\345\274\225\346\223\216.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\211\247\350\241\214\345\274\225\346\223\216\345\267\245\344\275\234\346\265\201\347\250\213.png" "b/blog-site/public/posts/annex/images/essays/\346\211\247\350\241\214\345\274\225\346\223\216\345\267\245\344\275\234\346\265\201\347\250\213.png" new file mode 100644 index 00000000..8c0ead82 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\211\247\350\241\214\345\274\225\346\223\216\345\267\245\344\275\234\346\265\201\347\250\213.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\211\247\350\241\214\345\274\225\346\223\216\346\211\247\350\241\214\350\277\207\347\250\213.png" "b/blog-site/public/posts/annex/images/essays/\346\211\247\350\241\214\345\274\225\346\223\216\346\211\247\350\241\214\350\277\207\347\250\213.png" new file mode 100644 index 00000000..b05cbb29 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\211\247\350\241\214\345\274\225\346\223\216\346\211\247\350\241\214\350\277\207\347\250\213.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\211\276\345\210\260spring.factories.png" "b/blog-site/public/posts/annex/images/essays/\346\211\276\345\210\260spring.factories.png" new file mode 100644 index 00000000..04c7f364 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\211\276\345\210\260spring.factories.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\223\215\344\275\234\346\225\260\346\240\210add.png" "b/blog-site/public/posts/annex/images/essays/\346\223\215\344\275\234\346\225\260\346\240\210add.png" new file mode 100644 index 00000000..5859fd59 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\223\215\344\275\234\346\225\260\346\240\210add.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\223\215\344\275\234\346\225\260\346\240\210\344\273\243\347\240\201\344\270\276\344\276\213.png" "b/blog-site/public/posts/annex/images/essays/\346\223\215\344\275\234\346\225\260\346\240\210\344\273\243\347\240\201\344\270\276\344\276\213.png" new file mode 100644 index 00000000..37b5fc0e Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\223\215\344\275\234\346\225\260\346\240\210\344\273\243\347\240\201\344\270\276\344\276\213.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\224\266\351\233\206\345\231\250\344\270\216\345\236\203\345\234\276\345\210\206\344\273\243\344\271\213\351\227\264\347\232\204\345\205\263\347\263\273.png" "b/blog-site/public/posts/annex/images/essays/\346\224\266\351\233\206\345\231\250\344\270\216\345\236\203\345\234\276\345\210\206\344\273\243\344\271\213\351\227\264\347\232\204\345\205\263\347\263\273.png" new file mode 100644 index 00000000..87e5ced7 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\224\266\351\233\206\345\231\250\344\270\216\345\236\203\345\234\276\345\210\206\344\273\243\344\271\213\351\227\264\347\232\204\345\205\263\347\263\273.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-001.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-001.png" new file mode 100644 index 00000000..7badb518 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-001.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-002.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-002.png" new file mode 100644 index 00000000..6417853a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-002.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-003.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-003.png" new file mode 100644 index 00000000..5876dcd4 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-003.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-004.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-004.png" new file mode 100644 index 00000000..1dc82ae9 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-004.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-005.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-005.png" new file mode 100644 index 00000000..8c18d8f4 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-005.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-006.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-006.png" new file mode 100644 index 00000000..83ec075b Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-006.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-007.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-007.png" new file mode 100644 index 00000000..6869a8bd Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-007.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-008.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-008.png" new file mode 100644 index 00000000..f566897d Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-008.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-009.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-009.png" new file mode 100644 index 00000000..9753f56a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-009.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-010.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-010.png" new file mode 100644 index 00000000..64654648 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-010.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-011.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-011.png" new file mode 100644 index 00000000..14d6df04 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-011.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-012.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-012.png" new file mode 100644 index 00000000..936baf0a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-012.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-013.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-013.png" new file mode 100644 index 00000000..5d442808 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-013.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-014.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-014.gif" new file mode 100644 index 00000000..878ba376 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-014.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-015.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-015.gif" new file mode 100644 index 00000000..353459bc Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-015.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-016.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-016.gif" new file mode 100644 index 00000000..2702b14d Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-016.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-017.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-017.gif" new file mode 100644 index 00000000..f00923b6 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-017.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-018.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-018.gif" new file mode 100644 index 00000000..ad88d357 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-018.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-019.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-019.gif" new file mode 100644 index 00000000..a29ca19d Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-019.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-020.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-020.gif" new file mode 100644 index 00000000..2a556955 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-020.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-021.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-021.png" new file mode 100644 index 00000000..1d92dfb8 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-021.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-022.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-022.png" new file mode 100644 index 00000000..41917efd Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-022.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-023.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-023.png" new file mode 100644 index 00000000..57f744a8 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-023.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-024.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-024.png" new file mode 100644 index 00000000..573fdcce Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-024.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-025.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-025.png" new file mode 100644 index 00000000..56ed24bc Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-025.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-026.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-026.gif" new file mode 100644 index 00000000..f3bb2a72 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-026.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-027.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-027.png" new file mode 100644 index 00000000..7b35f942 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-027.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-028.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-028.png" new file mode 100644 index 00000000..d327b63e Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-028.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-029.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-029.png" new file mode 100644 index 00000000..b7c47fd7 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-029.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-030.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-030.png" new file mode 100644 index 00000000..9eab7c73 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-030.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-031.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-031.gif" new file mode 100644 index 00000000..4e51304c Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-031.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-032.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-032.png" new file mode 100644 index 00000000..cf90717a Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-032.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-033.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-033.png" new file mode 100644 index 00000000..5af67201 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-033.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-034.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-034.png" new file mode 100644 index 00000000..70344892 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-034.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-035.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-035.png" new file mode 100644 index 00000000..97dbfd87 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-035.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-036.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-036.png" new file mode 100644 index 00000000..45798fd1 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-036.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-037.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-037.png" new file mode 100644 index 00000000..72c24530 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-037.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-038.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-038.png" new file mode 100644 index 00000000..7a559409 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-038.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-039.png" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-039.png" new file mode 100644 index 00000000..74be3462 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-039.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-040.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-040.gif" new file mode 100644 index 00000000..98e2edfe Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-040.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-041.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-041.gif" new file mode 100644 index 00000000..23056e22 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-041.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-042.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-042.gif" new file mode 100644 index 00000000..e489d7ca Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-042.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-043.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-043.gif" new file mode 100644 index 00000000..6b93e8ff Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-043.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-044.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-044.gif" new file mode 100644 index 00000000..7f201a11 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-044.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-045.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-045.gif" new file mode 100644 index 00000000..558e613e Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-045.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-046.gif" "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-046.gif" new file mode 100644 index 00000000..45b8cfac Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\347\256\227\346\263\225-046.gif" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\264\345\220\210\346\224\257\344\273\230\345\212\237\350\203\275-01.jpg" "b/blog-site/public/posts/annex/images/essays/\346\225\264\345\220\210\346\224\257\344\273\230\345\212\237\350\203\275-01.jpg" new file mode 100644 index 00000000..d4161e69 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\264\345\220\210\346\224\257\344\273\230\345\212\237\350\203\275-01.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\225\264\345\220\210\346\226\207\344\273\266\344\270\212\344\274\240\345\212\237\350\203\275-01.jpg" "b/blog-site/public/posts/annex/images/essays/\346\225\264\345\220\210\346\226\207\344\273\266\344\270\212\344\274\240\345\212\237\350\203\275-01.jpg" new file mode 100644 index 00000000..a05f41f1 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\225\264\345\220\210\346\226\207\344\273\266\344\270\212\344\274\240\345\212\237\350\203\275-01.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\226\271\346\263\225\345\214\272\345\206\205\351\203\250\347\273\223\346\236\204.png" "b/blog-site/public/posts/annex/images/essays/\346\226\271\346\263\225\345\214\272\345\206\205\351\203\250\347\273\223\346\236\204.png" new file mode 100644 index 00000000..5c993527 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\226\271\346\263\225\345\214\272\345\206\205\351\203\250\347\273\223\346\236\204.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\226\271\346\263\225\345\214\272\345\256\232\344\275\215.png" "b/blog-site/public/posts/annex/images/essays/\346\226\271\346\263\225\345\214\272\345\256\232\344\275\215.png" new file mode 100644 index 00000000..1324419b Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\226\271\346\263\225\345\214\272\345\256\232\344\275\215.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\234\254\345\234\260\346\226\271\346\263\225\346\216\245\345\217\243.png" "b/blog-site/public/posts/annex/images/essays/\346\234\254\345\234\260\346\226\271\346\263\225\346\216\245\345\217\243.png" new file mode 100644 index 00000000..1f6045e0 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\234\254\345\234\260\346\226\271\346\263\225\346\216\245\345\217\243.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\234\254\345\234\260\346\226\271\346\263\225\346\240\210.png" "b/blog-site/public/posts/annex/images/essays/\346\234\254\345\234\260\346\226\271\346\263\225\346\240\210.png" new file mode 100644 index 00000000..6be72349 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\234\254\345\234\260\346\226\271\346\263\225\346\240\210.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\240\210\344\270\212\345\210\206\351\205\215\351\200\203\351\200\270\345\210\206\346\236\220\345\211\215.png" "b/blog-site/public/posts/annex/images/essays/\346\240\210\344\270\212\345\210\206\351\205\215\351\200\203\351\200\270\345\210\206\346\236\220\345\211\215.png" new file mode 100644 index 00000000..d5ea215f Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\240\210\344\270\212\345\210\206\351\205\215\351\200\203\351\200\270\345\210\206\346\236\220\345\211\215.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\240\210\344\270\212\345\210\206\351\205\215\351\200\203\351\200\270\345\210\206\346\236\220\345\220\216.png" "b/blog-site/public/posts/annex/images/essays/\346\240\210\344\270\212\345\210\206\351\205\215\351\200\203\351\200\270\345\210\206\346\236\220\345\220\216.png" new file mode 100644 index 00000000..f41ba445 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\240\210\344\270\212\345\210\206\351\205\215\351\200\203\351\200\270\345\210\206\346\236\220\345\220\216.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\240\210\344\270\255\345\255\230\345\202\250\345\206\205\345\256\271.png" "b/blog-site/public/posts/annex/images/essays/\346\240\210\344\270\255\345\255\230\345\202\250\345\206\205\345\256\271.png" new file mode 100644 index 00000000..3db698f4 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\240\210\344\270\255\345\255\230\345\202\250\345\206\205\345\256\271.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\346\240\210\346\241\242\347\232\204\347\273\223\346\236\204.png" "b/blog-site/public/posts/annex/images/essays/\346\240\210\346\241\242\347\232\204\347\273\223\346\236\204.png" new file mode 100644 index 00000000..1c82a812 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\346\240\210\346\241\242\347\232\204\347\273\223\346\236\204.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\347\233\264\346\216\245\347\274\223\345\206\262\345\214\272.png" "b/blog-site/public/posts/annex/images/essays/\347\233\264\346\216\245\347\274\223\345\206\262\345\214\272.png" new file mode 100644 index 00000000..ab23e5e7 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\347\233\264\346\216\245\347\274\223\345\206\262\345\214\272.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\347\233\264\346\216\245\350\256\277\351\227\256.png" "b/blog-site/public/posts/annex/images/essays/\347\233\264\346\216\245\350\256\277\351\227\256.png" new file mode 100644 index 00000000..5ac91ed8 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\347\233\264\346\216\245\350\256\277\351\227\256.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\347\256\241\351\201\223\346\265\201\350\256\276\350\256\241\347\273\223\345\220\210\344\270\232\345\212\241-01.jpg" "b/blog-site/public/posts/annex/images/essays/\347\256\241\351\201\223\346\265\201\350\256\276\350\256\241\347\273\223\345\220\210\344\270\232\345\212\241-01.jpg" new file mode 100644 index 00000000..bf0f2158 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\347\256\241\351\201\223\346\265\201\350\256\276\350\256\241\347\273\223\345\220\210\344\270\232\345\212\241-01.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/\347\256\241\351\201\223\346\265\201\350\256\276\350\256\241\347\273\223\345\220\210\344\270\232\345\212\241-02.jpg" "b/blog-site/public/posts/annex/images/essays/\347\256\241\351\201\223\346\265\201\350\256\276\350\256\241\347\273\223\345\220\210\344\270\232\345\212\241-02.jpg" new file mode 100644 index 00000000..78bba01e Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\347\256\241\351\201\223\346\265\201\350\256\276\350\256\241\347\273\223\345\220\210\344\270\232\345\212\241-02.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/\347\256\241\351\201\223\346\265\201\350\256\276\350\256\241\347\273\223\345\220\210\344\270\232\345\212\241-03.jpg" "b/blog-site/public/posts/annex/images/essays/\347\256\241\351\201\223\346\265\201\350\256\276\350\256\241\347\273\223\345\220\210\344\270\232\345\212\241-03.jpg" new file mode 100644 index 00000000..b67f3b06 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\347\256\241\351\201\223\346\265\201\350\256\276\350\256\241\347\273\223\345\220\210\344\270\232\345\212\241-03.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.png" "b/blog-site/public/posts/annex/images/essays/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.png" new file mode 100644 index 00000000..59404bf6 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213\350\257\246\347\273\206.png" "b/blog-site/public/posts/annex/images/essays/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213\350\257\246\347\273\206.png" new file mode 100644 index 00000000..3739a283 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\347\261\273\345\212\240\350\275\275\350\277\207\347\250\213\350\257\246\347\273\206.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\347\272\242\351\273\221\346\240\221\347\273\223\346\236\204.jpeg" "b/blog-site/public/posts/annex/images/essays/\347\272\242\351\273\221\346\240\221\347\273\223\346\236\204.jpeg" new file mode 100644 index 00000000..af66580f Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\347\272\242\351\273\221\346\240\221\347\273\223\346\236\204.jpeg" differ diff --git "a/blog-site/public/posts/annex/images/essays/\347\272\277\347\250\213\344\270\216\350\277\233\347\250\213\347\232\204\345\205\263\347\263\273.jpg" "b/blog-site/public/posts/annex/images/essays/\347\272\277\347\250\213\344\270\216\350\277\233\347\250\213\347\232\204\345\205\263\347\263\273.jpg" new file mode 100644 index 00000000..0dd11fd1 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\347\272\277\347\250\213\344\270\216\350\277\233\347\250\213\347\232\204\345\205\263\347\263\273.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/\347\272\277\347\250\213\346\261\240\345\267\245\344\275\234\345\216\237\347\220\206.png" "b/blog-site/public/posts/annex/images/essays/\347\272\277\347\250\213\346\261\240\345\267\245\344\275\234\345\216\237\347\220\206.png" new file mode 100644 index 00000000..0d510990 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\347\272\277\347\250\213\346\261\240\345\267\245\344\275\234\345\216\237\347\220\206.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\347\272\277\347\250\213\350\275\254\346\215\242\345\233\276.jpg" "b/blog-site/public/posts/annex/images/essays/\347\272\277\347\250\213\350\275\254\346\215\242\345\233\276.jpg" new file mode 100644 index 00000000..dc4c4481 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\347\272\277\347\250\213\350\275\254\346\215\242\345\233\276.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/\347\275\221\347\273\234\347\274\226\347\250\213-001.jpg" "b/blog-site/public/posts/annex/images/essays/\347\275\221\347\273\234\347\274\226\347\250\213-001.jpg" new file mode 100644 index 00000000..ab5469e0 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\347\275\221\347\273\234\347\274\226\347\250\213-001.jpg" differ diff --git "a/blog-site/public/posts/annex/images/essays/\350\207\252\346\227\213\351\224\201\344\270\216\351\235\236\350\207\252\346\227\213\351\224\201.png" "b/blog-site/public/posts/annex/images/essays/\350\207\252\346\227\213\351\224\201\344\270\216\351\235\236\350\207\252\346\227\213\351\224\201.png" new file mode 100644 index 00000000..f7077eb8 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\350\207\252\346\227\213\351\224\201\344\270\216\351\235\236\350\207\252\346\227\213\351\224\201.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\350\231\232\346\213\237\346\234\272\346\214\207\344\273\244\350\231\232\351\235\236\350\231\232\346\226\271\346\263\225.png" "b/blog-site/public/posts/annex/images/essays/\350\231\232\346\213\237\346\234\272\346\214\207\344\273\244\350\231\232\351\235\236\350\231\232\346\226\271\346\263\225.png" new file mode 100644 index 00000000..92842710 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\350\231\232\346\213\237\346\234\272\346\214\207\344\273\244\350\231\232\351\235\236\350\231\232\346\226\271\346\263\225.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\350\231\232\346\213\237\346\234\272\350\231\232\346\226\271\346\263\225\346\214\207\344\273\244.png" "b/blog-site/public/posts/annex/images/essays/\350\231\232\346\213\237\346\234\272\350\231\232\346\226\271\346\263\225\346\214\207\344\273\244.png" new file mode 100644 index 00000000..246014fc Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\350\231\232\346\213\237\346\234\272\350\231\232\346\226\271\346\263\225\346\214\207\344\273\244.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\350\231\232\346\226\271\346\263\225\350\241\250.png" "b/blog-site/public/posts/annex/images/essays/\350\231\232\346\226\271\346\263\225\350\241\250.png" new file mode 100644 index 00000000..cbb89419 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\350\231\232\346\226\271\346\263\225\350\241\250.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\350\247\243\351\207\212\345\231\250&\345\215\263\346\227\266\347\274\226\350\257\221\345\231\250.png" "b/blog-site/public/posts/annex/images/essays/\350\247\243\351\207\212\345\231\250&\345\215\263\346\227\266\347\274\226\350\257\221\345\231\250.png" new file mode 100644 index 00000000..79a37563 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\350\247\243\351\207\212\345\231\250&\345\215\263\346\227\266\347\274\226\350\257\221\345\231\250.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\350\256\276\347\275\256\345\240\206\345\244\247\345\260\217.png" "b/blog-site/public/posts/annex/images/essays/\350\256\276\347\275\256\345\240\206\345\244\247\345\260\217.png" new file mode 100644 index 00000000..91dd8538 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\350\256\276\347\275\256\345\240\206\345\244\247\345\260\217.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\350\277\220\350\241\214\346\227\266\346\225\260\346\215\256\345\214\272.png" "b/blog-site/public/posts/annex/images/essays/\350\277\220\350\241\214\346\227\266\346\225\260\346\215\256\345\214\272.png" new file mode 100644 index 00000000..5560ec43 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\350\277\220\350\241\214\346\227\266\346\225\260\346\215\256\345\214\272.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\351\223\266\350\241\214\345\256\266\347\256\227\346\263\225.png" "b/blog-site/public/posts/annex/images/essays/\351\223\266\350\241\214\345\256\266\347\256\227\346\263\225.png" new file mode 100644 index 00000000..ea3b4ee7 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\351\223\266\350\241\214\345\256\266\347\256\227\346\263\225.png" differ diff --git "a/blog-site/public/posts/annex/images/essays/\351\230\273\345\241\236\351\230\237\345\210\227\347\273\223\346\236\204.jpeg" "b/blog-site/public/posts/annex/images/essays/\351\230\273\345\241\236\351\230\237\345\210\227\347\273\223\346\236\204.jpeg" new file mode 100644 index 00000000..120e83a6 Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\351\230\273\345\241\236\351\230\237\345\210\227\347\273\223\346\236\204.jpeg" differ diff --git "a/blog-site/public/posts/annex/images/essays/\351\235\236\347\233\264\346\216\245\347\274\223\345\206\262\345\214\272.png" "b/blog-site/public/posts/annex/images/essays/\351\235\236\347\233\264\346\216\245\347\274\223\345\206\262\345\214\272.png" new file mode 100644 index 00000000..a8703b3d Binary files /dev/null and "b/blog-site/public/posts/annex/images/essays/\351\235\236\347\233\264\346\216\245\347\274\223\345\206\262\345\214\272.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-001.png" "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-001.png" new file mode 100644 index 00000000..8ca6b76a Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-001.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-002.png" "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-002.png" new file mode 100644 index 00000000..a9004f5a Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-002.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-003.png" "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-003.png" new file mode 100644 index 00000000..c76669e8 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-003.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-004.png" "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-004.png" new file mode 100644 index 00000000..1ecd990f Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-004.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-005.png" "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-005.png" new file mode 100644 index 00000000..9560f13a Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-005.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-006.png" "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-006.png" new file mode 100644 index 00000000..88538478 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-006.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-007.png" "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-007.png" new file mode 100644 index 00000000..997dc518 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-007.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-008.png" "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-008.png" new file mode 100644 index 00000000..fd9ac3aa Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-008.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-009.png" "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-009.png" new file mode 100644 index 00000000..ee8a602d Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Java\347\216\251\345\205\267-009.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js-\346\207\222\345\212\240\350\275\275-01.png" "b/blog-site/public/posts/annex/images/readme/Js-\346\207\222\345\212\240\350\275\275-01.png" new file mode 100644 index 00000000..4e80f839 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js-\346\207\222\345\212\240\350\275\275-01.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js-\346\215\242\350\202\244\347\211\271\346\225\210-01.png" "b/blog-site/public/posts/annex/images/readme/Js-\346\215\242\350\202\244\347\211\271\346\225\210-01.png" new file mode 100644 index 00000000..49d7ba77 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js-\346\215\242\350\202\244\347\211\271\346\225\210-01.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js-\347\224\237\346\227\245\347\244\274\347\211\251-01.png" "b/blog-site/public/posts/annex/images/readme/Js-\347\224\237\346\227\245\347\244\274\347\211\251-01.png" new file mode 100644 index 00000000..6c47a183 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js-\347\224\237\346\227\245\347\244\274\347\211\251-01.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js-\350\241\250\347\231\275\347\245\236\345\231\250-01.png" "b/blog-site/public/posts/annex/images/readme/Js-\350\241\250\347\231\275\347\245\236\345\231\250-01.png" new file mode 100644 index 00000000..94850e02 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js-\350\241\250\347\231\275\347\245\236\345\231\250-01.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js-\351\233\252\350\212\261\351\243\230\350\220\275-01.png" "b/blog-site/public/posts/annex/images/readme/Js-\351\233\252\350\212\261\351\243\230\350\220\275-01.png" new file mode 100644 index 00000000..cb656768 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js-\351\233\252\350\212\261\351\243\230\350\220\275-01.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js\344\270\213\351\233\250\347\211\271\346\225\210-01.png" "b/blog-site/public/posts/annex/images/readme/Js\344\270\213\351\233\250\347\211\271\346\225\210-01.png" new file mode 100644 index 00000000..49da9ba9 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js\344\270\213\351\233\250\347\211\271\346\225\210-01.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js\344\272\224\345\255\220\346\243\213-01.png" "b/blog-site/public/posts/annex/images/readme/Js\344\272\224\345\255\220\346\243\213-01.png" new file mode 100644 index 00000000..67c5a582 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js\344\272\224\345\255\220\346\243\213-01.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js\346\212\230\347\272\270\345\257\274\350\210\252\346\240\217-01.png" "b/blog-site/public/posts/annex/images/readme/Js\346\212\230\347\272\270\345\257\274\350\210\252\346\240\217-01.png" new file mode 100644 index 00000000..37f83d88 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js\346\212\230\347\272\270\345\257\274\350\210\252\346\240\217-01.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js\346\273\221\345\235\227\346\213\226\346\213\275-01.png" "b/blog-site/public/posts/annex/images/readme/Js\346\273\221\345\235\227\346\213\226\346\213\275-01.png" new file mode 100644 index 00000000..fa91d17b Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js\346\273\221\345\235\227\346\213\226\346\213\275-01.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-con-bg.png" "b/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-con-bg.png" new file mode 100644 index 00000000..778ff090 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-con-bg.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-header-bg.png" "b/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-header-bg.png" new file mode 100644 index 00000000..565f3ba8 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-header-bg.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-ice.png" "b/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-ice.png" new file mode 100644 index 00000000..3bc5cedd Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-ice.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-snow-bg.png" "b/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-snow-bg.png" new file mode 100644 index 00000000..a20755f1 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-snow-bg.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-snow.png" "b/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-snow.png" new file mode 100644 index 00000000..82413440 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-snow.png" differ diff --git "a/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-tree.png" "b/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-tree.png" new file mode 100644 index 00000000..236965c6 Binary files /dev/null and "b/blog-site/public/posts/annex/images/readme/Js\351\233\252\350\212\261\351\243\230\350\220\275-tree.png" differ diff --git "a/blog-site/public/posts/annex/images/resume/\347\256\200\345\216\206\347\231\275\345\272\225\347\205\247\347\211\207.jpg" "b/blog-site/public/posts/annex/images/resume/\347\256\200\345\216\206\347\231\275\345\272\225\347\205\247\347\211\207.jpg" new file mode 100644 index 00000000..4d6a2d51 Binary files /dev/null and "b/blog-site/public/posts/annex/images/resume/\347\256\200\345\216\206\347\231\275\345\272\225\347\205\247\347\211\207.jpg" differ diff --git "a/blog-site/public/posts/annex/images/resume/\347\256\200\345\216\206\350\223\235\345\272\225\347\205\247\347\211\207.jpg" "b/blog-site/public/posts/annex/images/resume/\347\256\200\345\216\206\350\223\235\345\272\225\347\205\247\347\211\207.jpg" new file mode 100644 index 00000000..daa65c84 Binary files /dev/null and "b/blog-site/public/posts/annex/images/resume/\347\256\200\345\216\206\350\223\235\345\272\225\347\205\247\347\211\207.jpg" differ diff --git "a/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-001.png" "b/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-001.png" new file mode 100644 index 00000000..e38e22af Binary files /dev/null and "b/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-001.png" differ diff --git "a/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-002.png" "b/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-002.png" new file mode 100644 index 00000000..d21c3c57 Binary files /dev/null and "b/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-002.png" differ diff --git "a/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-003.png" "b/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-003.png" new file mode 100644 index 00000000..0b1beaf1 Binary files /dev/null and "b/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-003.png" differ diff --git "a/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-004.png" "b/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-004.png" new file mode 100644 index 00000000..f1368337 Binary files /dev/null and "b/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-004.png" differ diff --git "a/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-005.png" "b/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-005.png" new file mode 100644 index 00000000..f2c07c97 Binary files /dev/null and "b/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-005.png" differ diff --git "a/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-006.png" "b/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-006.png" new file mode 100644 index 00000000..9fa30fe0 Binary files /dev/null and "b/blog-site/public/posts/annex/images/spring/SpringBoot\346\225\264\345\220\210nacos-006.png" differ diff --git "a/blog-site/public/posts/annex/images/spring/Spring\350\257\246\350\247\243-001.png" "b/blog-site/public/posts/annex/images/spring/Spring\350\257\246\350\247\243-001.png" new file mode 100644 index 00000000..126e72b1 Binary files /dev/null and "b/blog-site/public/posts/annex/images/spring/Spring\350\257\246\350\247\243-001.png" differ diff --git "a/blog-site/public/posts/annex/images/spring/Spring\350\257\246\350\247\243-002.png" "b/blog-site/public/posts/annex/images/spring/Spring\350\257\246\350\247\243-002.png" new file mode 100644 index 00000000..2f7e067e Binary files /dev/null and "b/blog-site/public/posts/annex/images/spring/Spring\350\257\246\350\247\243-002.png" differ diff --git "a/blog-site/public/posts/annex/images/spring/Spring\350\257\246\350\247\243-003.png" "b/blog-site/public/posts/annex/images/spring/Spring\350\257\246\350\247\243-003.png" new file mode 100644 index 00000000..4b976e7e Binary files /dev/null and "b/blog-site/public/posts/annex/images/spring/Spring\350\257\246\350\247\243-003.png" differ diff --git "a/blog-site/public/posts/annex/images/spring/Spring\350\257\246\350\247\243-004.png" "b/blog-site/public/posts/annex/images/spring/Spring\350\257\246\350\247\243-004.png" new file mode 100644 index 00000000..acfdcc15 Binary files /dev/null and "b/blog-site/public/posts/annex/images/spring/Spring\350\257\246\350\247\243-004.png" differ diff --git a/blog-site/public/posts/annex/jar/auto-pwd.jar b/blog-site/public/posts/annex/jar/auto-pwd.jar new file mode 100644 index 00000000..3f7acae7 Binary files /dev/null and b/blog-site/public/posts/annex/jar/auto-pwd.jar differ diff --git a/blog-site/public/posts/annex/jar/cut-screen.jar b/blog-site/public/posts/annex/jar/cut-screen.jar new file mode 100644 index 00000000..a8c88ac3 Binary files /dev/null and b/blog-site/public/posts/annex/jar/cut-screen.jar differ diff --git a/blog-site/public/posts/annex/jar/english-translation.jar b/blog-site/public/posts/annex/jar/english-translation.jar new file mode 100644 index 00000000..34aa0565 Binary files /dev/null and b/blog-site/public/posts/annex/jar/english-translation.jar differ diff --git a/blog-site/public/posts/annex/jar/extract-color.jar b/blog-site/public/posts/annex/jar/extract-color.jar new file mode 100644 index 00000000..0026999c Binary files /dev/null and b/blog-site/public/posts/annex/jar/extract-color.jar differ diff --git a/blog-site/public/posts/annex/jar/huffman-zip.jar b/blog-site/public/posts/annex/jar/huffman-zip.jar new file mode 100644 index 00000000..dd32c7c4 Binary files /dev/null and b/blog-site/public/posts/annex/jar/huffman-zip.jar differ diff --git a/blog-site/public/posts/annex/jar/img-watermarking.jar b/blog-site/public/posts/annex/jar/img-watermarking.jar new file mode 100644 index 00000000..0bf271cd Binary files /dev/null and b/blog-site/public/posts/annex/jar/img-watermarking.jar differ diff --git a/blog-site/public/posts/annex/jar/lib/fastdfs-client-java-1.27.jar b/blog-site/public/posts/annex/jar/lib/fastdfs-client-java-1.27.jar new file mode 100644 index 00000000..c1a7bee7 Binary files /dev/null and b/blog-site/public/posts/annex/jar/lib/fastdfs-client-java-1.27.jar differ diff --git a/blog-site/public/posts/annex/jar/lib/qrcode.jar b/blog-site/public/posts/annex/jar/lib/qrcode.jar new file mode 100644 index 00000000..2b84560c Binary files /dev/null and b/blog-site/public/posts/annex/jar/lib/qrcode.jar differ diff --git a/blog-site/public/posts/annex/jar/qr-code.jar b/blog-site/public/posts/annex/jar/qr-code.jar new file mode 100644 index 00000000..304107f1 Binary files /dev/null and b/blog-site/public/posts/annex/jar/qr-code.jar differ diff --git "a/blog-site/content/posts/annex/pdf/books/HeadFirst\350\256\276\350\256\241\346\250\241\345\274\217.pdf" "b/blog-site/public/posts/annex/pdf/books/HeadFirst\350\256\276\350\256\241\346\250\241\345\274\217.pdf" similarity index 100% rename from "blog-site/content/posts/annex/pdf/books/HeadFirst\350\256\276\350\256\241\346\250\241\345\274\217.pdf" rename to "blog-site/public/posts/annex/pdf/books/HeadFirst\350\256\276\350\256\241\346\250\241\345\274\217.pdf" diff --git "a/blog-site/content/posts/annex/pdf/books/Java\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225.pdf" "b/blog-site/public/posts/annex/pdf/books/Java\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225.pdf" similarity index 100% rename from "blog-site/content/posts/annex/pdf/books/Java\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225.pdf" rename to "blog-site/public/posts/annex/pdf/books/Java\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225.pdf" diff --git "a/blog-site/content/posts/annex/pdf/books/Java\346\240\270\345\277\203\346\212\200\346\234\257\345\215\267I\345\237\272\347\241\200\347\237\245\350\257\206.pdf" "b/blog-site/public/posts/annex/pdf/books/Java\346\240\270\345\277\203\346\212\200\346\234\257\345\215\267I\345\237\272\347\241\200\347\237\245\350\257\206.pdf" similarity index 100% rename from "blog-site/content/posts/annex/pdf/books/Java\346\240\270\345\277\203\346\212\200\346\234\257\345\215\267I\345\237\272\347\241\200\347\237\245\350\257\206.pdf" rename to "blog-site/public/posts/annex/pdf/books/Java\346\240\270\345\277\203\346\212\200\346\234\257\345\215\267I\345\237\272\347\241\200\347\237\245\350\257\206.pdf" diff --git "a/blog-site/content/posts/annex/pdf/books/Java\347\274\226\347\250\213\346\200\235\346\203\263.pdf" "b/blog-site/public/posts/annex/pdf/books/Java\347\274\226\347\250\213\346\200\235\346\203\263.pdf" similarity index 100% rename from "blog-site/content/posts/annex/pdf/books/Java\347\274\226\347\250\213\346\200\235\346\203\263.pdf" rename to "blog-site/public/posts/annex/pdf/books/Java\347\274\226\347\250\213\346\200\235\346\203\263.pdf" diff --git "a/blog-site/content/posts/annex/pdf/books/\344\273\243\347\240\201\346\225\264\346\264\201\344\271\213\351\201\223.pdf" "b/blog-site/public/posts/annex/pdf/books/\344\273\243\347\240\201\346\225\264\346\264\201\344\271\213\351\201\223.pdf" similarity index 100% rename from "blog-site/content/posts/annex/pdf/books/\344\273\243\347\240\201\346\225\264\346\264\201\344\271\213\351\201\223.pdf" rename to "blog-site/public/posts/annex/pdf/books/\344\273\243\347\240\201\346\225\264\346\264\201\344\271\213\351\201\223.pdf" diff --git "a/blog-site/content/posts/annex/pdf/books/\345\244\247\345\236\213\347\275\221\347\253\231\346\212\200\346\234\257\346\236\266\346\236\204.pdf" "b/blog-site/public/posts/annex/pdf/books/\345\244\247\345\236\213\347\275\221\347\253\231\346\212\200\346\234\257\346\236\266\346\236\204.pdf" similarity index 100% rename from "blog-site/content/posts/annex/pdf/books/\345\244\247\345\236\213\347\275\221\347\253\231\346\212\200\346\234\257\346\236\266\346\236\204.pdf" rename to "blog-site/public/posts/annex/pdf/books/\345\244\247\345\236\213\347\275\221\347\253\231\346\212\200\346\234\257\346\236\266\346\236\204.pdf" diff --git "a/blog-site/content/posts/annex/pdf/books/\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204.pdf" "b/blog-site/public/posts/annex/pdf/books/\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204.pdf" similarity index 100% rename from "blog-site/content/posts/annex/pdf/books/\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204.pdf" rename to "blog-site/public/posts/annex/pdf/books/\345\244\247\350\257\235\346\225\260\346\215\256\347\273\223\346\236\204.pdf" diff --git "a/blog-site/content/posts/annex/pdf/books/\346\267\261\345\205\245\345\210\206\346\236\220JavaWeb\346\212\200\346\234\257\345\206\205\345\271\225.pdf" "b/blog-site/public/posts/annex/pdf/books/\346\267\261\345\205\245\345\210\206\346\236\220JavaWeb\346\212\200\346\234\257\345\206\205\345\271\225.pdf" similarity index 100% rename from "blog-site/content/posts/annex/pdf/books/\346\267\261\345\205\245\345\210\206\346\236\220JavaWeb\346\212\200\346\234\257\345\206\205\345\271\225.pdf" rename to "blog-site/public/posts/annex/pdf/books/\346\267\261\345\205\245\345\210\206\346\236\220JavaWeb\346\212\200\346\234\257\345\206\205\345\271\225.pdf" diff --git "a/blog-site/content/posts/annex/pdf/books/\347\226\257\347\213\202Java\350\256\262\344\271\211.pdf" "b/blog-site/public/posts/annex/pdf/books/\347\226\257\347\213\202Java\350\256\262\344\271\211.pdf" similarity index 100% rename from "blog-site/content/posts/annex/pdf/books/\347\226\257\347\213\202Java\350\256\262\344\271\211.pdf" rename to "blog-site/public/posts/annex/pdf/books/\347\226\257\347\213\202Java\350\256\262\344\271\211.pdf" diff --git "a/blog-site/content/posts/annex/pdf/books/\351\207\215\346\236\204\357\274\232\346\224\271\345\226\204\346\227\242\346\234\211\344\273\243\347\240\201\347\232\204\350\256\276\350\256\241.pdf" "b/blog-site/public/posts/annex/pdf/books/\351\207\215\346\236\204\357\274\232\346\224\271\345\226\204\346\227\242\346\234\211\344\273\243\347\240\201\347\232\204\350\256\276\350\256\241.pdf" similarity index 100% rename from "blog-site/content/posts/annex/pdf/books/\351\207\215\346\236\204\357\274\232\346\224\271\345\226\204\346\227\242\346\234\211\344\273\243\347\240\201\347\232\204\350\256\276\350\256\241.pdf" rename to "blog-site/public/posts/annex/pdf/books/\351\207\215\346\236\204\357\274\232\346\224\271\345\226\204\346\227\242\346\234\211\344\273\243\347\240\201\347\232\204\350\256\276\350\256\241.pdf" diff --git "a/blog-site/content/posts/annex/pdf/books/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241.pdf" "b/blog-site/public/posts/annex/pdf/books/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241.pdf" similarity index 100% rename from "blog-site/content/posts/annex/pdf/books/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241.pdf" rename to "blog-site/public/posts/annex/pdf/books/\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241.pdf" diff --git a/blog-site/public/posts/annex/xmind/AQS.xmind b/blog-site/public/posts/annex/xmind/AQS.xmind new file mode 100644 index 00000000..785c9bb1 Binary files /dev/null and b/blog-site/public/posts/annex/xmind/AQS.xmind differ diff --git "a/blog-site/public/posts/annex/xmind/HashMap\347\273\223\346\236\204.xmind" "b/blog-site/public/posts/annex/xmind/HashMap\347\273\223\346\236\204.xmind" new file mode 100644 index 00000000..7f67de09 Binary files /dev/null and "b/blog-site/public/posts/annex/xmind/HashMap\347\273\223\346\236\204.xmind" differ diff --git a/blog-site/public/posts/annex/xmind/JavaIO.xmind b/blog-site/public/posts/annex/xmind/JavaIO.xmind new file mode 100644 index 00000000..166665f1 Binary files /dev/null and b/blog-site/public/posts/annex/xmind/JavaIO.xmind differ diff --git "a/blog-site/public/posts/annex/xmind/Java\345\274\202\345\270\270\345\210\206\347\261\273.xmind" "b/blog-site/public/posts/annex/xmind/Java\345\274\202\345\270\270\345\210\206\347\261\273.xmind" new file mode 100644 index 00000000..4244110e Binary files /dev/null and "b/blog-site/public/posts/annex/xmind/Java\345\274\202\345\270\270\345\210\206\347\261\273.xmind" differ diff --git "a/blog-site/public/posts/annex/xmind/ReentrantLock\345\212\240\351\224\201.xmind" "b/blog-site/public/posts/annex/xmind/ReentrantLock\345\212\240\351\224\201.xmind" new file mode 100644 index 00000000..480018dc Binary files /dev/null and "b/blog-site/public/posts/annex/xmind/ReentrantLock\345\212\240\351\224\201.xmind" differ diff --git "a/blog-site/public/posts/annex/xmind/ReentrantLock\350\247\243\351\224\201.xmind" "b/blog-site/public/posts/annex/xmind/ReentrantLock\350\247\243\351\224\201.xmind" new file mode 100644 index 00000000..7f466340 Binary files /dev/null and "b/blog-site/public/posts/annex/xmind/ReentrantLock\350\247\243\351\224\201.xmind" differ diff --git "a/blog-site/public/posts/annex/xmind/Springboot\345\220\257\345\212\250\346\265\201\347\250\213.xmind" "b/blog-site/public/posts/annex/xmind/Springboot\345\220\257\345\212\250\346\265\201\347\250\213.xmind" new file mode 100644 index 00000000..ec94b14f Binary files /dev/null and "b/blog-site/public/posts/annex/xmind/Springboot\345\220\257\345\212\250\346\265\201\347\250\213.xmind" differ diff --git a/blog-site/public/posts/annex/xmind/j.u.c.locks.xmind b/blog-site/public/posts/annex/xmind/j.u.c.locks.xmind new file mode 100644 index 00000000..ec3b79e6 Binary files /dev/null and b/blog-site/public/posts/annex/xmind/j.u.c.locks.xmind differ diff --git a/blog-site/public/posts/annex/xmind/jdk1.7ConcurrentHashMap.xmind b/blog-site/public/posts/annex/xmind/jdk1.7ConcurrentHashMap.xmind new file mode 100644 index 00000000..a3f4db89 Binary files /dev/null and b/blog-site/public/posts/annex/xmind/jdk1.7ConcurrentHashMap.xmind differ diff --git a/blog-site/public/posts/annex/xmind/jdk1.8ConcurrentHashMap.xmind b/blog-site/public/posts/annex/xmind/jdk1.8ConcurrentHashMap.xmind new file mode 100644 index 00000000..8d35d249 Binary files /dev/null and b/blog-site/public/posts/annex/xmind/jdk1.8ConcurrentHashMap.xmind differ diff --git "a/blog-site/public/posts/annex/xmind/netty\346\250\241\345\236\213.xmind" "b/blog-site/public/posts/annex/xmind/netty\346\250\241\345\236\213.xmind" new file mode 100644 index 00000000..31f53bbb Binary files /dev/null and "b/blog-site/public/posts/annex/xmind/netty\346\250\241\345\236\213.xmind" differ diff --git "a/blog-site/public/posts/annex/xmind/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.xmind" "b/blog-site/public/posts/annex/xmind/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.xmind" new file mode 100644 index 00000000..190d9a9e Binary files /dev/null and "b/blog-site/public/posts/annex/xmind/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277.xmind" differ diff --git "a/blog-site/public/posts/annex/xmind/\345\257\271\350\261\241\345\210\206\351\205\215\347\255\226\347\225\245.xmind" "b/blog-site/public/posts/annex/xmind/\345\257\271\350\261\241\345\210\206\351\205\215\347\255\226\347\225\245.xmind" new file mode 100644 index 00000000..f18e2032 Binary files /dev/null and "b/blog-site/public/posts/annex/xmind/\345\257\271\350\261\241\345\210\206\351\205\215\347\255\226\347\225\245.xmind" differ diff --git "a/blog-site/public/posts/annex/xmind/\345\276\256\346\234\215\345\212\241\346\262\273\347\220\206.xmind" "b/blog-site/public/posts/annex/xmind/\345\276\256\346\234\215\345\212\241\346\262\273\347\220\206.xmind" new file mode 100644 index 00000000..7afe462e Binary files /dev/null and "b/blog-site/public/posts/annex/xmind/\345\276\256\346\234\215\345\212\241\346\262\273\347\220\206.xmind" differ diff --git "a/blog-site/public/posts/annex/xmind/\346\265\205\345\240\206\343\200\201\346\267\261\345\240\206\344\270\216\345\256\236\351\231\205\345\244\247\345\260\217.xmind" "b/blog-site/public/posts/annex/xmind/\346\265\205\345\240\206\343\200\201\346\267\261\345\240\206\344\270\216\345\256\236\351\231\205\345\244\247\345\260\217.xmind" new file mode 100644 index 00000000..1b049f0e Binary files /dev/null and "b/blog-site/public/posts/annex/xmind/\346\265\205\345\240\206\343\200\201\346\267\261\345\240\206\344\270\216\345\256\236\351\231\205\345\244\247\345\260\217.xmind" differ diff --git a/blog-site/public/posts/annex/zip/idea-settings.zip b/blog-site/public/posts/annex/zip/idea-settings.zip new file mode 100644 index 00000000..33705c7e Binary files /dev/null and b/blog-site/public/posts/annex/zip/idea-settings.zip differ diff --git a/blog-site/public/posts/annex/zip/settings.zip b/blog-site/public/posts/annex/zip/settings.zip new file mode 100644 index 00000000..bcaa7188 Binary files /dev/null and b/blog-site/public/posts/annex/zip/settings.zip differ diff --git a/blog-site/public/posts/annex/zip/vue2.x-lib.zip b/blog-site/public/posts/annex/zip/vue2.x-lib.zip new file mode 100644 index 00000000..60bf14be Binary files /dev/null and b/blog-site/public/posts/annex/zip/vue2.x-lib.zip differ diff --git a/blog-site/public/posts/books/books-daodejing/index.html b/blog-site/public/posts/books/books-daodejing/index.html new file mode 100644 index 00000000..388a0b17 --- /dev/null +++ b/blog-site/public/posts/books/books-daodejing/index.html @@ -0,0 +1,2896 @@ + + + + + + + + + + + 道德经 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

道德经

+ 2021.03.03 +
+

第一章

+

道可道,非常道。名可名,非常名。 +无名天地之始﹔有名万物之母。 +故常无,欲以观其妙﹔常有,欲以观其徼。 +此两者,同出而异名,同谓之玄。 +玄之又玄,众妙之门。

+

第二章

+

天下皆知美之为美,斯恶已。 +皆知善之为善,斯不善已。 +有无相生,难易相成,长短相形,高下相盈,音声相和,前后相随。恒也。 +是以圣人处无为之事,行不言之教﹔ +万物作而弗始,生而弗有,为而弗恃,功成而不居。 +夫唯弗居,是以不去。

+

第三章

+

不尚贤,使民不争 +不贵难得之货,使民不为盗﹔ +不见可欲,使民心不乱。 +是以圣人之治, +虚其心,实其腹, +弱其志,强其骨。 +常使民无知无欲。 +使夫智者不敢为也。 +为无为,则无不治。

+

第四章

+

道冲,而用之或不盈。 +渊兮,似万物之宗﹔湛兮,似或存。 +吾不知谁之子,象帝之先。

+

第五章

+

天地不仁,以万物为刍狗﹔ +圣人不仁,以百姓为刍狗。 +天地之间,其犹橐龠乎。 +虚而不屈,动而愈出。 +多言数穷,不如守中。

+

第六章

+

谷神不死,是谓玄牝。 +玄牝之门,是谓天地根。 +绵绵若存,用之不勤。

+

第七章

+

天长地久。 +天地所以能长且久者, +以其不自生,故能长生。 +是以圣人后其身而身先﹔外其身而身存。 +非以其无私邪。故能成其私。

+

第八章

+

上善若水。 +水善利万物而不争, +处众人之所恶,故几于道。 +居善地,心善渊, +与善仁,言善信, +政善治,事善能,动善时。 +夫唯不争,故无尤。

+

第九章

+

持而盈之,不如其已﹔ +揣而锐之,不可长保。 +金玉满堂,莫之能守﹔ +富贵而骄,自遗其咎。 +功遂身退,天之道也。

+

第十章

+

载营魄抱一,能无离乎。 +专气致柔,能如婴儿乎。 +涤除玄鉴,能无疵乎。 +爱国治民,能无为乎。 +天门开阖,能为雌乎。 +明白四达,能无知乎。

+

第十一章

+

三十辐,共一毂,当其无,有车之用。 +埏埴以为器,当其无,有器之用。 +凿户牖以为室,当其无,有室之用。 +故有之以为利,无之以为用。

+

第十二章

+

五色令人目盲﹔五音令人耳聋﹔五味令人口爽﹔ +驰骋畋猎,令人心发狂﹔难得之货,令人行妨。 +是以圣人为腹不为目,故去彼取此。

+

第十三章

+

宠辱若惊,贵大患若身。 +何谓宠辱若惊。 +宠为下,得之若惊,失之若惊,是谓宠辱若惊。 +何谓贵大患若身。 +吾所以有大患者,为吾有身, +及吾无身,吾有何患。 +故贵以身为天下,若可寄天下﹔ +爱以身为天下,若可托天下。

+

第十四章

+

视之不见,名曰夷﹔ +听之不闻,名曰希﹔ +搏之不得,名曰微。 +此三者不可致诘,故混而为一。 +其上不皦,其下不昧。 +绳绳兮不可名,复归于无物。 +是谓无状之状,无物之象,是谓惚恍。 +迎之不见其首,随之不见其后。 +执古之道,以御今之有。 +能知古始,是谓道纪。

+

第十五章

+

古之善为道者,微妙玄通,深不可识。 +夫唯不可识,故强为之容: +豫兮若冬涉川﹔ +犹兮若畏四邻﹔ +俨兮其若容﹔ +涣兮若冰之将释﹔ +敦兮其若朴﹔ +旷兮其若谷﹔ +混兮其若浊﹔ +澹兮其若海﹔ +飂兮若无止。 +孰能浊以静之徐清。 +孰能安以动之徐生。 +保此道者,不欲盈。 +夫唯不盈,故能蔽而新成。

+

第十六章

+

致虚极,守静笃。 +万物并作,吾以观复。 +夫物芸芸,各复归其根。 +归根曰静,静曰复命。 +复命曰常,知常曰明。 +不知常,妄作凶。 +知常容,容乃公, +公乃全,全乃天, +天乃道,道乃久,没身不殆。

+

第十七章

+

太上,不知有之﹔ +其次,亲而誉之﹔ +其次,畏之﹔ +其次,侮之。 +信不足焉,有不信焉。 +悠兮其贵言。 +功成事遂,百姓皆谓:「我自然」。

+

第十八章

+

大道废,有仁义﹔ +智慧出,有大伪﹔ +六亲不和,有孝慈﹔ +国家昏乱,有忠臣。

+

第十九章

+

绝圣弃智,民利百倍﹔ +绝仁弃义,民复孝慈﹔ +绝巧弃利,盗贼无有。 +此三者以为文不足,故令有所属。 +见素抱朴,少思寡欲,绝学无忧。

+

第二十章

+

唯之与阿,相去几何。 +善之与恶,相去若何。 +人之所畏,不可不畏。 +荒兮,其未央哉。 +众人熙熙,如享太牢,如春登台。 +我独泊兮,其未兆﹔ +沌沌兮,如婴儿之未孩﹔ +儽儽兮,若无所归。 +众人皆有余,而我独若遗。我愚人之心也哉。 +俗人昭昭,我独昏昏。 +俗人察察,我独闷闷。 +众人皆有以,而我独顽且鄙。 +我独异于人,而贵食母。

+

第二十一章

+

孔德之容,惟道是从。 +道之为物,惟恍惟惚。 +惚兮恍兮,其中有象﹔ +恍兮惚兮,其中有物。 +窈兮冥兮,其中有精﹔ +其精甚真,其中有信。 +自今及古,其名不去,以阅众甫。 +吾何以知众甫之状哉。以此。

+

第二十二章

+

曲则全,枉则直,洼则盈, +敝则新,少则得,多则惑。 +是以圣人抱一为天下式。 +不自见,故明﹔ +不自是,故彰﹔ +不自伐,故有功﹔ +不自矜,故长。 +夫唯不争,故天下莫能与之争。 +古之所谓「曲则全」者,岂虚言哉。 +诚全而归之。

+

第二十三章

+

希言自然。 +故飘风不终朝,骤雨不终日。 +孰为此者。 +天地。天地尚不能久,而况于人乎。 +故从事于道者,同于道﹔ +德者,同于德﹔失者,同于失。 +同于道者,道亦乐得之﹔ +同于德者,德亦乐得之﹔ +同于失者,失亦乐得之。 +信不足焉,有不信焉。

+

第二十四章

+

企者不立﹔跨者不行﹔ +自见者不明﹔自是者不彰﹔ +自伐者无功﹔自矜者不长。 +其在道也,曰:余食赘形。 +物或恶之,故有道者不处。

+

第二十五章

+

有物混成,先天地生。 +寂兮寥兮,独立而不改, +周行而不殆,可以为天地母。 +吾不知其名,强字之曰道,强为之名曰大。 +大曰逝,逝曰远,远曰反。 +故道大,天大,地大,人亦大。 +域中有四大,而人居其一焉。 +人法地,地法天,天法道,道法自然。

+

第二十六章

+

重为轻根,静为躁君。 +是以君子终日行不离辎重。 +虽有荣观,燕处超然。 +奈何万乘之主,而以身轻天下。 +轻则失根,躁则失君。

+

第二十七章

+

善行无辙迹,善言无瑕谪﹔ +善数不用筹策﹔ +善闭无关楗而不可开, +善结无绳约而不可解。 +是以圣人常善救人,故无弃人﹔ +常善救物,故无弃物。 +是谓袭明。 +故善人者,不善人之师﹔ +不善人者,善人之资。 +不贵其师,不爱其资, +虽智大迷,是谓要妙。

+

第二十八章

+

知其雄,守其雌,为天下溪。 +为天下溪,常德不离,复归于婴儿。 +知其白,守其黑,为天下式。 +为天下式,常德不忒,复归于无极。 +知其荣,守其辱,为天下谷。 +为天下谷,常德乃足。 +复归於朴,朴散则为器。 +圣人用之,则为官长,故大制不割。

+

第二十九章

+

将欲取天下而为之,吾见其不得已。 +天下神器,不可为也,不可执也。 +为者败之,执者失之。 +是以圣人无为,故无败﹔ +无执,故无失。 +夫物或行或随﹔或嘘或吹﹔ +或强或羸﹔或挫或隳。 +是以圣人去甚,去奢,去泰。

+

第三十章

+

以道佐人主者,不以兵强天下。 +其事好远。 +师之所处,荆棘生焉。 +大军之后,必有凶年。 +善有果而已,不以取强。 +果而勿矜,果而勿伐,果而勿骄。 +果而不得已,果而勿强。 +物壮则老,是谓不道,不道早已。

+

第三十一章

+

夫兵者,不祥之器, +物或恶之,故有道者不处。 +君子居则贵左,用兵则贵右。 +兵者不祥之器,非君子之器, +不得已而用之,恬淡为上。 +胜而不美,而美之者,是乐杀人。 +夫乐杀人者,则不可得志于天下矣。 +吉事尚左,凶事尚右。 +偏将军居左,上将军居右,言以丧礼处之。 +杀人之众,以悲哀泣之,战胜以丧礼处之。

+

第三十二章

+

道常无名。 +朴虽小,天下莫能臣。 +侯王若能守之,万物将自宾。 +天地相合,以降甘露,民莫之令而自均。 +始制有名,名亦既有, +夫亦将知止,知止可以不殆。 +譬道之在天下,犹川谷之于江海。

+

第三十三章

+

知人者智,自知者明。 +胜人者有力, +自胜者强,知足者富。 +强行者有志。 +不失其所者久。 +死而不亡者寿。

+

第三十四章

+

大道泛兮,其可左右。 +万物恃之以生而不辞,功成而不有。 +衣养万物而不为主。常无欲可名于小﹔ +万物归焉而不为主,可名为大。 +以其终不自为大,故能成其大。

+

第三十五章

+

执大象,天下往。 +往而不害,安平泰。 +乐与饵,过客止。 +道之出口,淡乎其无味, +视之不足见,听之不足闻,用之不足既。

+

第三十六章

+

将欲歙之,必故张之﹔ +将欲弱之,必故强之﹔ +将欲废之,必故兴之﹔ +将欲取之,必故与之。 +是谓微明。 +柔弱胜刚强。 +鱼不可脱于渊,国之利器不可以示人。

+

第三十七章

+

道常无为而无不为。 +侯王若能守之,万物将自化。 +化而欲作,吾将镇之以无名之朴。 +无名之朴,夫亦将不欲。 +不欲以静,天下将自定。

+

第三十八章

+

上德不德,是以有德﹔ +下德不失德,是以无德。 +上德无为而无以为﹔ +下德无为而有以为。 +上仁为之而无以为﹔ +上义为之而有以为。 +上礼为之而莫之应, +则攘臂而扔之。 +故失道而后德,失德而后仁, +失仁而后义,失义而后礼。 +夫礼者,忠信之薄,而乱之首。 +前识者,道之华,而愚之始。 +是以大丈夫处其厚,不居其薄﹔ +处其实,不居其华。故去彼取此。

+

第三十九章

+

昔之得一者: +天得一以清﹔ +地得一以宁﹔ +神得一以灵﹔ +谷得一以生﹔ +侯王得一以为天下贞。 +其致之也,谓天无以清,将恐裂﹔ +地无以宁,将恐废﹔ +神无以灵,将恐歇﹔ +谷无以盈,将恐竭﹔ +万物无以生,将恐灭﹔ +侯王无以贞,将恐蹶。 +故贵以贱为本,高以下为基。 +是以侯王自称孤、寡、不谷。 +此非以贱为本邪。非乎。故致誉无誉。 +是故不欲琭琭如玉,珞珞如石。

+

第四十章

+

反者道之动﹔弱者道之用。 +天下万物生于有,有生于无。

+

第四十一章

+

上士闻道,勤而行之﹔ +中士闻道,若存若亡﹔ +下士闻道,大笑之。 +不笑不足以为道。 +故建言有之: +明道若昧﹔进道若退﹔夷道若颣﹔ +上德若谷﹔广德若不足﹔ +建德若偷﹔质真若渝﹔ +大白若辱﹔大方无隅﹔ +大器晚成﹔大音希声﹔ +大象无形﹔道隐无名。 +夫唯道,善贷且成。

+

第四十二章

+

道生一,一生二,二生三,三生万物。 +万物负阴而抱阳,冲气以为和。 +人之所恶,唯孤、寡、不谷,而王公以为称。 +故物或损之而益,或益之而损。 +人之所教,我亦教之。 +强梁者不得其死,吾将以为教父。

+

第四十三章

+

天下之至柔,驰骋天下之至坚。 +无有入无间,吾是以知无为之有益。 +不言之教,无为之益,天下希及之。

+

第四十四章

+

名与身孰亲。身与货孰多。得与亡孰病。 +甚爱必大费﹔多藏必厚亡。 +故知足不辱,知止不殆,可以长久。

+

第四十五章

+

大成若缺,其用不弊。 +大盈若冲,其用不穷。 +大直若屈,大巧若拙,大辩若讷。 +静胜躁,寒胜热。清静为天下正。

+

第四十六章

+

天下有道,却走马以粪。 +天下无道,戎马生于郊。 +祸莫大于不知足﹔咎莫大于欲得。 +故知足之足,常足矣。

+

第四十七章

+

不出户,知天下﹔不窥牖,见天道。 +其出弥远,其知弥少。 +是以圣人不行而知,不见而明,不为而成。

+

第四十八章

+

为学日益,为道日损。 +损之又损,以至于无为。 +无为而无不为。 +取天下常以无事,及其有事,不足以取天下。

+

第四十九章

+

圣人常无心,以百姓心为心。 +善者,吾善之﹔不善者,吾亦善之﹔德善。 +信者,吾信之﹔不信者,吾亦信之﹔德信。 +圣人在天下,歙歙焉,为天下浑其心, +百姓皆注其耳目,圣人皆孩之。

+

第五十章

+

出生入死。 +生之徒,十有三﹔ +死之徒,十有三﹔ +人之生,动之于死地,亦十有三。 +夫何故,以其生之厚。 +盖闻善摄生者,路行不遇兕虎,入军不被甲兵﹔ +兕无所投其角,虎无所用其爪,兵无所容其刃。 +夫何故,以其无死地。

+

第五十一章

+

道生之,德畜之, +物形之,势成之。 +是以万物莫不尊道而贵德。 +道之尊,德之贵,夫莫之命而常自然。 +故道生之,德畜之﹔ +长之育之﹔成之熟之﹔养之覆之。 +生而不有,为而不恃, +长而不宰。是谓玄德。

+

第五十二章

+

天下有始,以为天下母。 +既得其母,以知其子, +复守其母,没身不殆。 +塞其兑,闭其门,终身不勤。 +开其兑,济其事,终身不救。 +见小曰明,守柔曰强。 +用其光,复归其明, +无遗身殃﹔是为袭常。

+

第五十三章

+

使我介然有知,行于大道,唯施是畏。 +大道甚夷,而人好径。 +朝甚除,田甚芜,仓甚虚﹔ +服文采,带利剑,厌饮食, +财货有余﹔是为盗夸。 +非道也哉。

+

第五十四章

+

善建者不拔, +善抱者不脱,子孙以祭祀不辍。 +修之于身,其德乃真﹔ +修之于家,其德乃余﹔ +修之于乡,其德乃长﹔ +修之于邦,其德乃丰﹔ +修之于天下,其德乃普。 +故以身观身,以家观家,以乡观乡,以邦观邦,以天下观天下。 +吾何以知天下然哉。以此。

+

第五十五章

+

含「德」之厚,比于赤子。 +毒虫不螫,猛兽不据,攫鸟不搏。 +骨弱筋柔而握固。 +未知牝牡之合而峻作,精之至也。 +终日号而不嗄,和之至也。 +知和曰「常」,知常曰「明」。 +益生曰祥。心使气曰强。 +物壮则老,谓之不道,不道早已。

+

第五十六章

+

知者不言,言者不知。 +挫其锐,解其纷。 +和其光,同其尘,是谓「玄同」。 +故不可得而亲,不可得而疏﹔ +不可得而利,不可得而害﹔ +不可得而贵,不可得而贱。故为天下贵。

+

第五十七章

+

以正治国,以奇用兵,以无事取天下。 +吾何以知其然哉。以此: +天下多忌讳,而民弥贫﹔ +人多利器,国家滋昏﹔ +人多伎巧,奇物滋起﹔ +法令滋彰,盗贼多有。 +故圣人云: +「我无为,而民自化﹔ +我好静,而民自正﹔ +我无事,而民自富﹔ +我无欲,而民自朴。」

+

第五十八章

+

其政闷闷,其民淳淳﹔ +其政察察,其民缺缺。 +祸兮福之所倚,福兮祸之所伏。 +孰知其极。其无正也。 +正复为奇,善复为妖。 +人之迷,其日固久。 +是以圣人方而不割,廉而不刿,直而不肆,光而不耀。

+

第五十九章

+

治人事天,莫若啬。 +夫唯啬,是谓早服﹔ +早服谓之重积德﹔重积德则无不克﹔ +无不克则莫知其极﹔莫知其极,可以有国﹔ +有国之母,可以长久﹔ +是谓深根固柢,长生久视之道。

+

第六十章

+

治大国,若烹小鲜。 +以道莅天下,其鬼不神﹔ +非其鬼不神,其神不伤人﹔ +非其神不伤人,圣人亦不伤人。 +夫两不相伤,故德交归焉。

+

第六十一章

+

大邦者下流,天下之交,天下之牝。 +牝常以静胜牡,以静为下。 +故大邦以下小邦,则取小邦﹔ +小邦以下大邦,则取大邦。 +故或下以取,或下而取。 +大邦不过欲兼畜人, +小邦不过欲入事人。 +夫两者各得所欲,大者宜为下。

+

第六十二章

+

道者万物之奥。善人之宝,不善人之所保。 +美言可以市尊,美行可以加人。 +人之不善,何弃之有。 +故立天子,置三公, +虽有拱璧以先驷马,不如坐进此道。 +古之所以贵此道者何。 +不曰:求以得,有罪以免邪。故为天下贵。

+

第六十三章

+

为无为,事无事,味无味。 +图难于其易,为大于其细﹔ +天下难事,必作于易, +天下大事,必作于细。 +是以圣人终不为大,故能成其大。 +夫轻诺必寡信,多易必多难。 +是以圣人犹难之,故终无难矣。

+

第六十四章

+

其安易持,其未兆易谋。 +其脆易泮,其微易散。 +为之于未有,治之于未乱。 +合抱之木,生于毫末﹔ +九层之台,起于累土﹔ +千里之行,始于足下。 +民之从事,常于几成而败之。 +慎终如始,则无败事。

+

第六十五章

+

古之善为道者,非以明民,将以愚之。 +民之难治,以其智多。 +故以智治国,国之贼﹔ +不以智治国,国之福。 +知此两者亦稽式。 +常知稽式,是谓「玄德」。 +「玄德」深矣,远矣,与物反矣,然后乃至大顺。

+

第六十六章

+

江海之所以能为百谷王者, +以其善下之,故能为百谷王。 +是以圣人欲上民,必以言下之﹔ +欲先民,必以身后之。 +是以圣人处上而民不重,处前而民不害。 +是以天下乐推而不厌。 +以其不争,故天下莫能与之争。

+

第六十七章

+

天下皆谓我道大,似不肖。 +夫唯大,故似不肖。 +若肖,久矣其细也夫。 +我有三宝,持而保之。 +一曰慈,二曰俭, +三曰不敢为天下先。 +慈故能勇﹔俭故能广﹔ +不敢为天下先,故能成器长。 +今舍慈且勇﹔舍俭且广﹔ +舍后且先﹔死矣。 +夫慈以战则胜,以守则固。 +天将救之,以慈卫之。

+

第六十八章

+

善为士者,不武﹔ +善战者,不怒﹔ +善胜敌者,不与﹔ +善用人者,为之下。 +是谓不争之德, +是谓用人之力, +是谓配天古之极。

+

第六十九章

+

用兵有言: +「吾不敢为主,而为客﹔ +不敢进寸,而退尺。」 +是谓行无行﹔攘无臂﹔ +扔无敌﹔执无兵。 +祸莫大于轻敌,轻敌几丧吾宝。 +故抗兵相若,哀者胜矣。

+

第七十章

+

吾言甚易知,甚易行。 +天下莫能知,莫能行。 +言有宗,事有君。 +夫唯无知,是以不我知。 +知我者希,则我者贵。 +是以圣人被褐而怀玉。

+

第七十一章

+

知不知,尚矣﹔ +不知知,病也。 +圣人不病,以其病病。 +夫唯病病,是以不病。

+

第七十二章

+

民不畏威,则大威至。 +无狎其所居,无厌其所生。 +夫唯不厌,是以不厌。 +是以圣人自知不自见﹔ +自爱不自贵。故去彼取此。

+

第七十三章

+

勇于敢则杀,勇于不敢则活。 +此两者,或利或害。 +天之所恶,孰知其故。 +天之道,不争而善胜,不言而善应,不召而自来,繟然而善谋。 +天网恢恢,疏而不失。

+

第七十四章

+

民不畏死,奈何以死惧之。 +若使民常畏死,而为奇者, +吾得执而杀之,孰敢。 +常有司杀者杀。 +夫代司杀者杀,是谓代大匠斲, +夫代大匠斲者,希有不伤其手矣。

+

第七十五章

+

民之饥,以其上食税之多,是以饥。 +民之难治,以其上之有为,是以难治。 +民之轻死,以其上求生之厚,是以轻死。 +夫唯无以生为者,是贤于贵生。

+

第七十六章

+

人之生也柔弱,其死也坚强。 +草木之生也柔脆,其死也枯槁。 +故坚强者死之徒,柔弱者生之徒。 +是以兵强则灭,木强则折。 +强大处下,柔弱处上。

+

第七十七章

+

天之道,其犹张弓欤。 +高者抑之,下者举之﹔ +有余者损之,不足者补之。 +天之道,损有余而补不足。 +人之道,则不然,损不足以奉有余。 +孰能有余以奉天下,唯有道者。 +是以圣人为而不恃,功成而不处,其不欲见贤。

+

第七十八章

+

天下莫柔弱于水,而攻坚强者莫之能胜,以其无以易之。 +弱之胜强,柔之胜刚, +天下莫不知,莫能行。 +是以圣人云: +「受国之垢,是谓社稷主﹔ +受国不祥,是为天下王。」 +正言若反。

+

第七十九章

+

和大怨,必有余怨﹔ +报怨以德,安可以为善。 +是以圣人执左契,而不责于人。 +有德司契,无德司彻。 +天道无亲,常与善人。

+

第八十章

+

小国寡民。 +使有什伯之器而不用﹔ +使民重死而不远徙。 +虽有舟舆,无所乘之, +虽有甲兵,无所陈之。 +使民复结绳而用之。 +甘其食,美其服,安其居,乐其俗。 +邻国相望,鸡犬之声相闻, +民至老死,不相往来。

+

第八十一章

+

信言不美,美言不信。 +善者不辩,辩者不善。 +知者不博,博者不知。 +圣人不积,既以为人己愈有, +既以与人己愈多。 +天之道,利而不害﹔ +圣人之道,为而不争。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/books/books-sunzibingfa/index.html b/blog-site/public/posts/books/books-sunzibingfa/index.html new file mode 100644 index 00000000..34dfca21 --- /dev/null +++ b/blog-site/public/posts/books/books-sunzibingfa/index.html @@ -0,0 +1,704 @@ + + + + + + + + + + + 孙子兵法 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

孙子兵法

+ 2022.12.19 +
+

始计

+

兵者,国之大事,死生之地,存亡之道,不可不察也。

+

故经之以五事,校之以计,而索其情:一曰道,二曰天,三曰地,四曰将,五曰法。道者,令民于上同意,可与之死,可与之生,而不危也;天者,阴阳、寒暑、时制也;地者,远近、险易、广狭、死生也;将者,智、信、仁、勇、严也;法者,曲制、官道、主用也。凡此五者,将莫不闻,知之者胜,不知之者不胜。故校之以计,而索其情,曰:主孰有道?将孰有能?天地孰得?法令孰行?兵众孰强?士卒孰练?赏罚孰明?吾以此知胜负矣。将听吾计,用之必胜,留之;将不听吾计,用之必败,去之。

+

计利以听,乃为之势,以佐其外。势者,因利而制权也。兵者,诡道也。故能而示之不能,用而示之不用,近而示之远,远而示之近。利而诱之,乱而取之,实而备之,强而避之,怒而挠之,卑而骄之,佚而劳之,亲而离之,攻其无备,出其不意。此兵家之胜,不可先传也。

+

夫未战而庙算胜者,得算多也;未战而庙算不胜者,得算少也。多算胜少算,而况于无算乎!吾以此观之,胜负见矣。

+

作战

+

凡用兵之法,驰车千驷,革车千乘,带甲十万,千里馈粮。则内外之费,宾客之用,胶漆之材,车甲之奉,日费千金,然后十万之师举矣。

+

其用战也,胜久则钝兵挫锐,攻城则力屈,久暴师则国用不足。夫钝兵挫锐,屈力殚货,则诸侯乘其弊而起,虽有智者不能善其后矣。故兵闻拙速,未睹巧之久也。夫兵久而国利者,未之有也。故不尽知用兵之害者,则不能尽知用兵之利也。

+

善用兵者,役不再籍,粮不三载,取用于国,因粮于敌,故军食可足也。国之贫于师者远输,远输则百姓贫;近师者贵卖,贵卖则百姓财竭,财竭则急于丘役。力屈中原、内虚于家,百姓之费,十去其七;公家之费,破军罢马,甲胄矢弓,戟盾矛橹,丘牛大车,十去其六。故智将务食于敌,食敌一钟,当吾二十钟;萁杆一石,当吾二十石。故杀敌者,怒也;取敌之利者,货也。车战得车十乘以上,赏其先得者而更其旌旗。车杂而乘之,卒善而养之,是谓胜敌而益强。

+

故兵贵胜,不贵久。

+

故知兵之将,民之司命。国家安危之主也。

+

谋攻

+

夫用兵之法,全国为上,破国次之;全军为上,破军次之;全旅为上,破旅次之;全卒为上,破卒次之;全伍为上,破伍次之。

+

是故百战百胜,非善之善也;不战而屈人之兵,善之善者也。故上兵伐谋,其次伐交,其次伐兵,其下攻城。攻城之法,为不得已。修橹轒辒,具器械,三月而后成;距堙,又三月而后已。将不胜其忿而蚁附之,杀士卒三分之一,而城不拔者,此攻之灾也。故善用兵者,屈人之兵而非战也,拔人之城而非攻也,毁人之国而非久也,必以全争于天下,故兵不顿而利可全,此谋攻之法也。

+

故用兵之法,十则围之,五则攻之,倍则分之,敌则能战之,少则能逃之,不若则能避之。故小敌之坚,大敌之擒也。

+

夫将者,国之辅也。辅周则国必强,辅隙则国必弱。故君之所以患于军者三:不知军之不可以进而谓之进,不知军之不可以退而谓之退,是谓縻军;不知三军之事而同三军之政,则军士惑矣;不知三军之权而同三军之任,则军士疑矣。三军既惑且疑,则诸侯之难至矣。是谓乱军引胜。

+

故知胜有五:知可以战与不可以战者胜,识众寡之用者胜,上下同欲者胜,以虞待不虞者胜,将能而君不御者胜。此五者,知胜之道也。故曰:知己知彼,百战不贻;不知彼而知己,一胜一负;不知彼不知己,每战必败。

+

军形

+

昔之善战者,先为不可胜,以待敌之可胜。不可胜在己,可胜在敌。故善战者,能为不可胜,不能使敌之必可胜。故曰:胜可知,而不可为。不可胜者,守也;可胜者,攻也。守则不足,攻则有余。善守者藏于九地之下,善攻者动于九天之上,故能自保而全胜也。见胜不过众人之所知,非善之善者也;战胜而天下曰善,非善之善者也。故举秋毫不为多力,见日月不为明目,闻雷霆不为聪耳。古之所谓善战者,胜于易胜者也。故善战者之胜也,无智名,无勇功,故其战胜不忒。不忒者,其所措胜,胜已败者也。故善战者,立于不败之地,而不失敌之败也。是故胜兵先胜而后求战,败兵先战而后求胜。善用兵者,修道而保法,故能为胜败之政。

+

兵法:一曰度,二曰量,三曰数,四曰称,五曰胜。地生度,度生量,量生数,数生称,称生胜。故胜兵若以镒称铢,败兵若以铢称镒。

+

称胜者之战民也,若决积水于千仞之溪者,形也。

+

兵势

+

凡治众如治寡,分数是也;斗众如斗寡,形名是也;三军之众,可使必受敌而无败者,奇正是也;兵之所加,如以碫投卵者,虚实是也。

+

凡战者,以正合,以奇胜。故善出奇者,无穷如天地,不竭如江海。终而复始,日月是也。死而更生,四时是也。声不过五,五声之变,不可胜听也;色不过五,五色之变,不可胜观也;味不过五,五味之变,不可胜尝也;战势不过奇正,奇正之变,不可胜穷也。奇正相生,如循环之无端,孰能穷之哉!

+

激水之疾,至于漂石者,势也;鸷鸟之疾,至于毁折者,节也。故善战者,其势险,其节短。势如扩弩,节如发机。纷纷纭纭,斗乱而不可乱;浑浑沌沌,形圆而不可败。乱生于治,怯生于勇,弱生于强。治乱,数也;勇怯,势也;强弱,形也。

+

故善动敌者,形之,敌必从之;予之,敌必取之。以利动之,以卒待之。故善战者,求之于势,不责于人故能择人而任势。任势者,其战人也,如转木石。木石之性,安则静,危则动,方则止,圆则行。

+

故善战人之势,如转圆石于千仞之山者,势也。

+

虚实

+

凡先处战地而待敌者佚,后处战地而趋战者劳。故善战者,致人而不致于人。能使敌人自至者,利之也;能使敌人不得至者,害之也。故敌佚能劳之,饱能饥之,安能动之。出其所必趋,趋其所不意。

+

行千里而不劳者,行于无人之地也;攻而必取者,攻其所不守也。守而必固者,守其所必攻也。故善攻者,敌不知其所守;善守者,敌不知其所攻。微乎微乎,至于无形;神乎神乎,至于无声,故能为敌之司命。进而不可御者,冲其虚也;退而不可追者,速而不可及也。故我欲战,敌虽高垒深沟,不得不与我战者,攻其所必救也;我不欲战,虽画地而守之,敌不得与我战者,乖其所之也。故形人而我无形,则我专而敌分。我专为一,敌分为十,是以十攻其一也。则我众敌寡,能以众击寡者,则吾之所与战者约矣。吾所与战之地不可知,不可知则敌所备者多,敌所备者多,则吾所与战者寡矣。故备前则后寡,备后则前寡,备左则右寡,备右则左寡,无所不备,则无所不寡。寡者,备人者也;众者,使人备己者也。故知战之地,知战之日,则可千里而会战;不知战之地,不知战日,则左不能救右,右不能救左,前不能救后,后不能救前,而况远者数十里,近者数里乎!

+

以吾度之,越人之兵虽多,亦奚益于胜哉!

+

故曰:胜可为也。敌虽众,可使无斗。故策之而知得失之计,候之而知动静之理,形之而知死生之地,角之而知有余不足之处。故形兵之极,至于无形。无形则深间不能窥,智者不能谋。因形而措胜于众,众不能知。人皆知我所以胜之形,而莫知吾所以制胜之形。故其战胜不复,而应形于无穷。

+

夫兵形象水,水之行避高而趋下,兵之形避实而击虚;水因地而制流,兵因敌而制胜。故兵无常势,水无常形。能因敌变化而取胜者,谓之神。故五行无常胜,四时无常位,日有短长,月有死生。

+

军争

+

凡用兵之法,将受命于君,合军聚众,交和而舍,莫难于军争。军争之难者,以迂为直,以患为利。

+

故迂其途,而诱之以利,后人发,先人至,此知迂直之计者也。军争为利,军争为危。举军而争利则不及,委军而争利则辎重捐。是故卷甲而趋,日夜不处,倍道兼行,百里而争利,则擒三将军,劲者先,疲者后,其法十一而至;五十里而争利,则蹶上将军,其法半至;三十里而争利,则三分之二至。是故军无辎重则亡,无粮食则亡,无委积则亡。故不知诸侯之谋者,不能豫交;不知山林、险阻、沮泽之形者,不能行军;不用乡导者,不能得地利。故兵以诈立,以利动,以分和为变者也。故其疾如风,其徐如林,侵掠如火,不动如山,难知如阴,动如雷震。掠乡分众,廓地分利,悬权而动。先知迂直之计者胜,此军争之法也。

+

《军政》曰:“言不相闻,故为之金鼓;视不相见,故为之旌旗。”夫金鼓旌旗者,所以一民之耳目也。民既专一,则勇者不得独进,怯者不得独退,此用众之法也。故夜战多金鼓,昼战多旌旗,所以变人之耳目也。

+

三军可夺气,将军可夺心。是故朝气锐,昼气惰,暮气归。善用兵者,避其锐气,击其惰归,此治气者也。以治待乱,以静待哗,此治心者也。以近待远,以佚待劳,以饱待饥,此治力者也。无邀正正之旗,无击堂堂之陈,此治变者也。

+

故用兵之法,高陵勿向,背丘勿逆,佯北勿从,锐卒勿攻,饵兵勿食,归师勿遏,围师遗阙,穷寇勿迫,此用兵之法也。

+

九变

+

凡用兵之法,将受命于君,合军聚合。泛地无舍,衢地合交,绝地无留,围地则谋,死地则战,途有所不由,军有所不击,城有所不攻,地有所不争,君命有所不受。

+

故将通于九变之利者,知用兵矣;将不通九变之利,虽知地形,不能得地之利矣;治兵不知九变之术,虽知五利,不能得人之用矣。

+

是故智者之虑,必杂于利害,杂于利而务可信也,杂于害而患可解也。是故屈诸侯者以害,役诸侯者以业,趋诸侯者以利。故用兵之法,无恃其不来,恃吾有以待之;无恃其不攻,恃吾有所不可攻也。

+

故将有五危,必死可杀,必生可虏,忿速可侮,廉洁可辱,爱民可烦。凡此五者,将之过也,用兵之灾也。覆军杀将,必以五危,不可不察也。

+

行军

+

凡处军相敌,绝山依谷,视生处高,战隆无登,此处山之军也。绝水必远水,客绝水而来,勿迎之于水内,令半渡而击之利,欲战者,无附于水而迎客,视生处高,无迎水流,此处水上之军也。绝斥泽,唯亟去无留,若交军于斥泽之中,必依水草而背众树,此处斥泽之军也。平陆处易,右背高,前死后生,此处平陆之军也。凡此四军之利,黄帝之所以胜四帝也。凡军好高而恶下,贵阳而贱阴,养生而处实,军无百疾,是谓必胜。丘陵堤防,必处其阳而右背之,此兵之利,地之助也。上雨水流至,欲涉者,待其定也。凡地有绝涧、天井、天牢、天罗、天陷、天隙,必亟去之,勿近也。吾远之,敌近之;吾迎之,敌背之。军旁有险阻、潢井、蒹葭、小林、蘙荟者,必谨覆索之,此伏奸之所处也。

+

敌近而静者,恃其险也;远而挑战者,欲人之进也;其所居易者,利也;众树动者,来也;众草多障者,疑也;鸟起者,伏也;兽骇者,覆也;尘高而锐者,车来也;卑而广者,徒来也;散而条达者,樵采也;少而往来者,营军也;辞卑而备者,进也;辞强而进驱者,退也;轻车先出居其侧者,陈也;无约而请和者,谋也;奔走而陈兵者,期也;半进半退者,诱也;杖而立者,饥也;汲而先饮者,渴也;见利而不进者,劳也;鸟集者,虚也;夜呼者,恐也;军扰者,将不重也;旌旗动者,乱也;吏怒者,倦也;杀马肉食者,军无粮也;悬甀不返其舍者,穷寇也;谆谆𧬈𧬈,徐与人言者,失众也;数赏者,窘也;数罚者,困也;先暴而后畏其众者,不精之至也;来委谢者,欲休息也。兵怒而相迎,久而不合,又不相去,必谨察之。

+

兵非贵益多也,惟无武进,足以并力料敌取人而已。夫惟无虑而易敌者,必擒于人。卒未亲而罚之,则不服,不服则难用。卒已亲附而罚不行,则不可用。故合之以文,齐之以武,是谓必取。令素行以教其民,则民服;令素不行以教其民,则民不服。令素行者,与众相得也。

+

地形

+

地形有通者、有挂者、有支者、有隘者、有险者、有远者。我可以往,彼可以来,曰通。通形者,先居高阳,利粮道,以战则利。可以往,难以返,曰挂。挂形者,敌无备,出而胜之,敌若有备,出而不胜,难以返,不利。我出而不利,彼出而不利,曰支。支形者,敌虽利我,我无出也,引而去之,令敌半出而击之利。隘形者,我先居之,必盈之以待敌。若敌先居之,盈而勿从,不盈而从之。险形者,我先居之,必居高阳以待敌;若敌先居之,引而去之,勿从也。远形者,势均难以挑战,战而不利。凡此六者,地之道也,将之至任,不可不察也。

+

凡兵有走者、有驰者、有陷者、有崩者、有乱者、有北者。凡此六者,非天地之灾,将之过也。夫势均,以一击十,曰走;卒强吏弱,曰驰;吏强卒弱,曰陷;大吏怒而不服,遇敌怼而自战,将不知其能,曰崩;将弱不严,教道不明,吏卒无常,陈兵纵横,曰乱;将不能料敌,以少合众,以弱击强,兵无选锋,曰北。凡此六者,败之道也,将之至任,不可不察也。

+

夫地形者,兵之助也。料敌制胜,计险隘远近,上将之道也。知此而用战者必胜,不知此而用战者必败。故战道必胜,主曰无战,必战可也;战道不胜,主曰必战,无战可也。故进不求名,退不避罪,唯民是保,而利于主,国之宝也。

+

视卒如婴儿,故可以与之赴深溪;视卒如爱子,故可与之俱死。厚而不能使,爱而不能令,乱而不能治,譬若骄子,不可用也。

+

知吾卒之可以击,而不知敌之不可击,胜之半也;知敌之可击,而不知吾卒之不可以击,胜之半也;知敌之可击,知吾卒之可以击,而不知地形之不可以战,胜之半也。故知兵者,动而不迷,举而不穷。故曰:知彼知己,胜乃不殆;知天知地,胜乃可全。

+

九地

+

用兵之法,有散地,有轻地,有争地,有交地,有衢地,有重地,有泛地,有围地,有死地。诸侯自战其地者,为散地;入人之地不深者,为轻地;我得亦利,彼得亦利者,为争地;我可以往,彼可以来者,为交地;诸侯之地三属,先至而得天下众者,为衢地;入人之地深,背城邑多者,为重地;山林、险阻、沮泽,凡难行之道者,为泛地;所由入者隘,所从归者迂,彼寡可以击吾之众者,为围地;疾战则存,不疾战则亡者,为死地。是故散地则无战,轻地则无止,争地则无攻,交地则无绝,衢地则合交,重地则掠,泛地则行,围地则谋,死地则战。

+

古之善用兵者,能使敌人前后不相及,众寡不相恃,贵贱不相救,上下不相收,卒离而不集,兵合而不齐。合于利而动,不合于利而止。敢问敌众而整将来,待之若何曰:先夺其所爱则听矣。兵之情主速,乘人之不及。由不虞之道,攻其所不戒也。

+

凡为客之道,深入则专。主人不克,掠于饶野,三军足食。谨养而勿劳,并气积力,运兵计谋,为不可测。

+

投之无所往,死且不北。死焉不得,士人尽力。兵士甚陷则不惧,无所往则固,深入则拘,不得已则斗。是故其兵不修而戒,不求而得,不约而亲,不令而信,禁祥去疑,至死无所之。

+

吾士无余财,非恶货也;无余命,非恶寿也。令发之日,士卒坐者涕沾襟,偃卧者涕交颐,投之无所往,诸、刿之勇也。故善用兵者,譬如率然。率然者,常山之蛇也。击其首则尾至,击其尾则首至,击其中则首尾俱至。敢问兵可使如率然乎?曰可。夫吴人与越人相恶也,当其同舟而济而遇风,其相救也如左右手。是故方马埋轮,未足恃也;齐勇如一,政之道也;刚柔皆得,地之理也。故善用兵者,携手若使一人,不得已也。

+

将军之事,静以幽,正以治,能愚士卒之耳目,使之无知;易其事,革其谋,使人无识;易其居,迂其途,使民不得虑。帅与之期,如登高而去其梯;帅与之深入诸侯之地,而发其机。若驱群羊,驱而往,驱而来,莫知所之。聚三军之众,投之于险,此谓将军之事也。

+

九地之变,屈伸之力,人情之理,不可不察也。

+

凡为客之道,深则专,浅则散。去国越境而师者,绝地也;四彻者,衢地也;入深者,重地也;入浅者,轻地也;背固前隘者,围地也;无所往者,死地也。

+

是故散地吾将一其志,轻地吾将使之属,争地吾将趋其后,交地吾将谨其守,交地吾将固其结,衢地吾将谨其恃,重地吾将继其食,泛地吾将进其途,围地吾将塞其阙,死地吾将示之以不活。

+

故兵之情:围则御,不得已则斗,过则从。

+

是故不知诸侯之谋者,不能预交;不知山林、险阻、沮泽之形者,不能行军;不用乡导,不能得地利。四五者,一不知,非霸王之兵也。夫霸王之兵,伐大国,则其众不得聚;威加于敌,则其交不得合。是故不争天下之交,不养天下之权,信己之私,威加于敌,则其城可拔,其国可隳。

+

施无法之赏,悬无政之令。犯三军之众,若使一人。犯之以事,勿告以言;犯之以害,勿告以利。投之亡地然后存,陷之死地然后生。夫众陷于害,然后能为胜败。

+

故为兵之事,在顺详敌之意,并敌一向,千里杀将,是谓巧能成事。是故政举之日,夷关折符,无通其使,厉于廊庙之上,以诛其事。敌人开阖,必亟入之,先其所爱,微与之期,践墨随敌,以决战事。是故始如处女,敌人开户;后如脱兔,敌不及拒。

+

火攻

+

凡火攻有五:一曰火人,二曰火积,三曰火辎,四曰火库,五曰火队。

+

行火必有因,因必素具。发火有时,起火有日。时者,天之燥也。日者,月在箕、壁、翼、轸也。凡此四宿者,风起之日也。凡火攻,必因五火之变而应之:火发于内,则早应之于外;火发而其兵静者,待而勿攻,极其火力,可从而从之,不可从则上。火可发于外,无待于内,以时发之,火发上风,无攻下风,昼风久,夜风止。凡军必知五火之变,以数守之。

+

故以火佐攻者明,以水佐攻者强。水可以绝,不可以夺。

+

夫战胜攻取而不惰其功者凶,命曰“费留”。故曰:明主虑之,良将惰之,非利不动,非得不用,非危不战。主不可以怒而兴师,将不可以愠而攻战。合于利而动,不合于利而上。怒可以复喜,愠可以复说,亡国不可以复存,死者不可以复生。故明主慎之,良将警之。此安国全军之道也。

+

用间

+

凡兴师十万,出征千里,百姓之费,公家之奉,日费千金,内外骚动,怠于道路,不得操事者,七十万家。相守数年,以争一日之胜,而爱爵禄百金,不知敌之情者,不仁之至也,非民之将也,非主之佐也,非胜之主也。故明君贤将所以动而胜人,成功出于众者,先知也。先知者,不可取于鬼神,不可象于事,不可验于度,必取于人,知敌之情者也。

+

故用间有五:有因间,有内间,有反间,有死间,有生间。五间俱起,莫知其道,是谓神纪,人君之宝也。乡间者,因其乡人而用之;内间者,因其官人而用之;反间者,因其敌间而用之;死间者,为诳事于外,令吾闻知之而传于敌间也;生间者,反报也。故三军之事,莫亲于间,赏莫厚于间,事莫密于间,非圣贤不能用间,非仁义不能使间,非微妙不能得间之实。微哉微哉!无所不用间也。间事未发而先闻者,间与所告者兼死。凡军之所欲击,城之所欲攻,人之所欲杀,必先知其守将、左右、谒者、门者、舍人之姓名,令吾间必索知之。敌间之来间我者,因而利之,导而舍之,故反间可得而用也;因是而知之,故乡间、内间可得而使也;因是而知之,故死间为诳事,可使告敌;因是而知之,故生间可使如期。五间之事,主必知之,知之必在于反间,故反间不可不厚也。

+

昔殷之兴也,伊挚在夏;周之兴也,吕牙在殷。故明君贤将,能以上智为间者,必成大功。此兵之要,三军之所恃而动也。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/books/java-books/index.html b/blog-site/public/posts/books/java-books/index.html new file mode 100644 index 00000000..1669b9bf --- /dev/null +++ b/blog-site/public/posts/books/java-books/index.html @@ -0,0 +1,314 @@ + + + + + + + + + + + 关于编程的书籍 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

关于编程的书籍

+ 2022.08.08 +
+
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/cas-principle/index.html b/blog-site/public/posts/essays/cas-principle/index.html new file mode 100644 index 00000000..ca955bdd --- /dev/null +++ b/blog-site/public/posts/essays/cas-principle/index.html @@ -0,0 +1,649 @@ + + + + + + + + + + + CAS原理 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

CAS原理

+ 2020.04.04 +
+

CAS

+

CAS全称为Compare and Swap被译为比较并交换。是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。 +java.util.concurrent.atomic 并发包下的所有原子类都是基于 CAS 来实现的。

+

AtomicInteger 原子整型类为例。

+
public class MainTest {
+    public static void main(String[] args) {
+        new AtomicInteger().compareAndSet(1,2);
+    }
+}
+

以上面的代码为例,调用栈如下:

+
compareAndSet --> unsafe.compareAndSwapInt ---> unsafe.compareAndSwapInt --> (C++) cmpxchg
+

AtomicInteger 内部方法都是基于 Unsafe 类实现的。

+
public final boolean compareAndSet(int expect, int update) {
+    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
+}
+

参数:

+
    +
  • this: Unsafe 对象本身,需要通过这个类来获取 value 的内存偏移地址;
  • +
  • valueOffset: value 变量的内存偏移地址;
  • +
  • expect: 期望更新的值;
  • +
  • update: 要更新的最新值;
  • +
+

偏移量valueOffset

+
// setup to use Unsafe.compareAndSwapInt for updates
+    private static final Unsafe unsafe = Unsafe.getUnsafe();
+    private static final long valueOffset;
+
+    static {
+        try {
+            valueOffset = unsafe.objectFieldOffset
+                (AtomicInteger.class.getDeclaredField("value"));
+        } catch (Exception ex) { throw new Error(ex); }
+    }
+
+    private volatile int value;
+
    +
  1. Unsafe 是CAS的核心类,Java无法直接访问底层操作系统,而是通过 native 方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类 Unsafe,它提供了硬件级别的原子操作。
  2. +
  3. valueOffset 表示的是变量值在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数据的原值的。
  4. +
  5. value 是用 volatile 修饰的,保证了多线程之间看到的 value 值是同一份。
  6. +
+

继续向底层深入,就会看到Unsafe类中的一些方法,同时也是CAS的核心方法:

+
public final class Unsafe {
+
+    // ...
+
+    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
+
+    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
+
+    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
+    
+    // ...
+}
+

上面的三个方法可以对应去查看 openjdkhotspot源码:src/share/vm/prims/unsafe.cpp

+
#define FN_PTR(f) CAST_FROM_FN_PTR(void*, &f)
+
+{CC"compareAndSwapObject", CC"("OBJ"J"OBJ""OBJ")Z",  FN_PTR(Unsafe_CompareAndSwapObject)},
+
+{CC"compareAndSwapInt",  CC"("OBJ"J""I""I"")Z",      FN_PTR(Unsafe_CompareAndSwapInt)},
+
+{CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z",      FN_PTR(Unsafe_CompareAndSwapLong)},
+

最终在 hotspot 源码实现中都会调用统一的 cmpxchg 函数,/src/share/vm/runtime/Atomic.cpp

+
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte*dest, jbyte compare_value) {
+         assert (sizeof(jbyte) == 1,"assumption.");
+         uintptr_t dest_addr = (uintptr_t) dest;
+         uintptr_t offset = dest_addr % sizeof(jint);
+         volatile jint*dest_int = ( volatile jint*)(dest_addr - offset);
+         // 对象当前值
+         jint cur = *dest_int;
+         // 当前值cur的地址
+         jbyte * cur_as_bytes = (jbyte *) ( & cur);
+         // new_val地址
+         jint new_val = cur;
+         jbyte * new_val_as_bytes = (jbyte *) ( & new_val);
+          // new_val存exchange_value,后面修改则直接从new_val中取值
+         new_val_as_bytes[offset] = exchange_value;
+         // 比较当前值与期望值,如果相同则更新,不同则直接返回
+         while (cur_as_bytes[offset] == compare_value) {
+          // 调用汇编指令cmpxchg执行CAS操作,期望值为cur,更新值为new_val
+             jint res = cmpxchg(new_val, dest_int, cur);
+             if (res == cur) break;
+             cur = res;
+             new_val = cur;
+             new_val_as_bytes[offset] = exchange_value;
+         }
+         // 返回当前值
+         return cur_as_bytes[offset];
+}
+

从上述源码可以看出CAS的原理就是调用了汇编指令 cmpxchg ,最终其实也就调用了CPU的某些指令。

+

CAS作用也一目了然,在多线程环境中,就是比较当前线程工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较,直到主内存和工作内存中的值一直为止。例如代码:

+
public final int getAndAddInt(Object var1, long var2, int var4) {
+        int var5;
+        do {
+            var5 = this.getIntVolatile(var1, var2);
+        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
+        return var5;
+}
+

如何保证数据一致性

+

从源码可以看出,是通过CPU指令进行调用,当CPU中某个处理器对缓存中的共享变量进行了操作,其他处理器会有个嗅探机制,将其他处理器的该共享变量的缓存失效,待其他线程读取时会重新从主内存中读取最新的数据,基于 MESI 缓存一致性协议来实现的。

+

简述,就是通过CPU的缓存一致性协议来保证线程之间的数据一致性的。

+
+

CPU 处理器速度远远大于在主内存中的,为了解决速度差异,在他们之间架设了多级缓存,如 L1、L2、L3 级别的缓存,这些缓存离CPU越近就越快,将频繁操作的数据缓存到这里,加快访问速度。

+
+

JMM

+

CAS与Unsafe关系

+

CAS的作用是比较并交换,就是先拿这个期望值,与主内存的值比较,判断主内存中该位置是否存在期望值, +如果存在,则改为新的值,这个修改的过程是具有原子性的. +因为CAS是cpu并发源语,并发源语体现在Java sun.misc.Unsafa类上. +调用Unsafe类中的CAS方法,JVM会帮我们实现CAS汇编指令。这是一种完全依赖于硬件的功能,通过他实现了原子操作。 +由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成数据不一致问题。

+
+

PS Unsafe类 +CAS其实是调用了 Unsafe 类的方法 Unsafa 类是CAS核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe 相当于一个后门,基于该类可以直接操作特定内存数据。 +Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针(内存地址)一样直接操作内存,因此Java中CAS操作的执行依赖于Unsafe类的方法。 +Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

+
+

缺点

+
    +
  1. +

    因为是采用自旋锁的方式来实现所以,自然有自旋锁的缺点,循环时间长开销大,例如:getAndAddInt 方法执行,有个do while循环,如果CAS失败,一直会进行尝试,如果CAS长时间不成功,可能会给CPU带来很大的开销。

    +
    public final int getAndAddInt(Object var1, long var2, int var4) {
    +        int var5;
    +        do {
    +            var5 = this.getIntVolatile(var1, var2);
    +        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
    +        return var5;
    +}
    +
  2. +
  3. +

    只能保证一个共享变量的原子操作,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。

    +
  4. +
  5. +

    ABA问题。

    +

    ABA问题示例代码:

    +
    public class MainTest {
    +    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    +    public static void main(String[] args) {
    +        new Thread(() -> {
    +            // 先改到101在改回来,CAS会认为value没有被修改过
    +            atomicReference.compareAndSet(100, 101);
    +            atomicReference.compareAndSet(101, 100);
    +        }, "Thread 1").start();
    +
    +        new Thread(() -> {
    +            try {
    +                //保证线程1完成一次ABA操作
    +                TimeUnit.SECONDS.sleep(1);
    +            } catch (InterruptedException e) {
    +                e.printStackTrace();
    +            }
    +            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
    +        }, "Thread 2").start();
    +        try {
    +            TimeUnit.SECONDS.sleep(2);
    +        } catch (InterruptedException e) {
    +            e.printStackTrace();
    +        }
    +    }
    +}
    +

    CAS算法实现一个重要前提是,需要去除内存中某个时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

    +

    比如,线程1从内存位置V取出A,线程2同时也从内存取出A,并且线程2进行一些操作将值改为B,然后线程2又将V位置数据改成A,这时候线程1进行CAS操作发现内存中的值依然时A,然后线程1操作成功。 +尽管线程1的CAS操作成功,但是不代表这个过程没有问题。

    +

    简单说,如果一个线程改了一个值,最后又改回到初始值了,这时候CAS会认为它没有被修改过。简而言之就是只比较结果,不比较过程。

    +

    3.1 ABA问题解决

    +

    3.1.1 利用 AtomicReference 类进行原子引用

    +
    public class AtomicRefrenceDemo {
    +    public static void main(String[] args) {
    +        User z3 = new User("张三", 22);
    +        User l4 = new User("李四", 23);
    +        AtomicReference<User> atomicReference = new AtomicReference<>();
    +        atomicReference.set(z3);
    +        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
    +        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
    +    }
    +}
    +
    +@Getter
    +@ToString
    +@AllArgsConstructor
    +class User {
    +    String userName;
    +    int age;
    +}
    +
    // 输出结果
    +true	User(userName=李四, age=23)
    +false	User(userName=李四, age=23)
    +

    3.1.2 使用时间戳的原子引用AtomicStampedReference修改版本号。主要是在对象中额外再增加一个标记来标识对象是否有过变更

    +
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
    +
    +public static void main(String[] args) {
    +    new Thread(() -> {
    +            int stamp = atomicStampedReference.getStamp();
    +            System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
    +            try {
    +                TimeUnit.SECONDS.sleep(2);
    +            } catch (InterruptedException e) {
    +                e.printStackTrace();
    +            }
    +            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
    +            System.out.println(Thread.currentThread().getName() + "\t第2次版本号" + atomicStampedReference.getStamp());
    +            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
    +            System.out.println(Thread.currentThread().getName() + "\t第3次版本号" + atomicStampedReference.getStamp());
    +        }, "Thread 3").start();
    +
    +        new Thread(() -> {
    +            int stamp = atomicStampedReference.getStamp();
    +            System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
    +            try {
    +                TimeUnit.SECONDS.sleep(4);
    +            } catch (InterruptedException e) {
    +                e.printStackTrace();
    +            }
    +            boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
    +
    +            System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
    +            System.out.println(Thread.currentThread().getName() + "\t当前最新实际值:" + atomicStampedReference.getReference());
    +        }, "Thread 4").start();
    +}
    +
    Thread 3	第1次版本号1
    +Thread 4	第1次版本号1
    +Thread 3	第2次版本号2
    +Thread 3	第3次版本号3
    +Thread 4	修改是否成功false	当前最新实际版本号:3
    +Thread 4	当前最新实际值:100
    +
  6. +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/clean-code/index.html b/blog-site/public/posts/essays/clean-code/index.html new file mode 100644 index 00000000..79db9e6f --- /dev/null +++ b/blog-site/public/posts/essays/clean-code/index.html @@ -0,0 +1,648 @@ + + + + + + + + + + + 整洁的代码 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

整洁的代码

+ 2022.09.01 +
+

为什么要写整洁的代码

+

为什么要写整洁的代码,回答这个问题之前,也许应该想想写糟糕的代码的原因

+

是想快点完成吗?还是要赶时间吗?有可能.或许你觉得自己要干好所需要的时间不够;假使花时间清理代码,老板就会大发雷霆.或许你只是不耐烦再搞这套程序,期望早点结束.或许你看了看,自己承诺要做的其他事情,意识到得赶紧弄完手上的东西,好接着做下一件工作.这种事情我们都干过.

+

“只要你干过编程,就有可能曾经被某人的糟糕代码绊倒过.如果你编程不止两三年,有可能被这种代码拖过腿.进度延缓的程度非常严重.有些团队在项目初期进展迅速,但是有那么一两年的时间却慢如蜗行.如对代码的每次修改都影响到了其他两三处代码.修改无小事.每次修改或添加代码都对那对扭纹柴了然于心,这样才能网上扔更多的扭纹柴.这团乱麻越来越大,在也无法清理,最后束手无策.

+

随着混乱的增加,团队生产力也持续下降,趋势余零.当生产力下降时,管理层就只有一件事情可做了:增加更多的人手到项目中,期望提高生产力.可新人不熟悉系统的设计.他们搞不清什么样的修改符合设计的意图,什么样的修改违背设计意图.而且,他们以及团队中的其他人都背负着提高生产力的压力.于是,他们制造了更多的混乱,驱动生产力向零的那端不断下降.

+

最后,开发团队造反了,他们告诉管理层,再也无法在这令人生厌的代码基础上做开发.他们要求全新设计.管理层不愿意投入资源完全重启炉灶,他们也不能否认生产力低得可怕.他们只好同意开发者的要求,授权去做一套看上去很美的华丽新设计.

+

于是就组建了一只新军.谁都想加入这个团队,因为它是张白纸.他们可以重新来过,搞出点真正漂亮的东西来.但只有最优秀,最聪明的家伙被选中.其余人则继续维护现有的系统.

+

现在有两支队伍在竞赛.新团队必须搭建一套新系统,要求实现旧系统的所有功能.另外,还得跟得上旧系统的持续改动.在新系统功能足以对抗旧系统之前,管理层就不会替换掉旧的系统.

+

竞赛可能会持续极长的时间.到了完成的时候,新团队的老成员已不知去向,而现有成员则需求重新设计一套新系统,因为这套系统太烂了.

+

假如你经历过哪怕是一小段我谈到的这种事,那么你一定知道,花时间保持整洁的代码不但有关于效率,还有关于生存. "

+

有时我们抱怨需求变化背离了初期设计.哀叹进度太紧张,没法好好干活.我们把问题归咎于那些愚蠢的经理,苛刻的用户,没用的营销方式.

+

经理和营销人员指望从我们这里得到必须的信息,然后才能做出承诺和保证;即便他们没开口问,我们也不该羞于告知自己的想法。用户指望我们验证需求是否都在系统中实现了。项目经理指望我们遵守进度。我们与项目的规划脱不了干系,对失败负有极大的责任:特别是当失败与糟糕的代码有关时尤为如此! +“且慢!”你说。“不听经理的,我就会被炒鱿鱼。”多半不会。多数经理想要知道实情,即便他们看起来不喜欢实情。多数经理想要好代码,即便他们总是痴缠于进度。他们会奋力卫护进度和需求;那是他们该干的。你则当以同等的热情卫护代码。

+

再说明白些,假使你是位医生,病人请求你在给他做手术前别洗手,因为那会花太多时间,你会照办吗?本该是病人说了算;但医生却绝对应该拒绝遵从。为什么?因为医生比病人更了解疾病和感染的风险。医生如果按病人说的办,就是一种不专业的态度,更别说是犯罪了。同理,程序员遵从不了解混乱风险的经理的意愿,也是不专业的做法。我们应该加强这方面的意识。

+

程序员面临着一种基础价值谜题。有那么几年经验的开发者都知道,之前的混乱拖了自己的后腿。但开发者们背负期限的压力,只好制造混乱。简言之,他们没花时间让自己做得更快!真正的专业人士明白,这道谜题的第二部分说错了。制造混乱无助于赶上期限。混乱只会立刻拖慢你,叫你错过期限。赶上期限的唯一方法一做得快的唯一方法就是始终尽可能保持代码整洁。

+

“如果你不明白整洁对于代码有何意义,尝试去写整洁代码就会毫无所益.”

+

截取自《代码整洁之道》

+

什么是整洁的代码

+

每个人对于整洁的代码理解肯定不同,在我看来,满足业务场景的情况下,可读性强,运行效率高,细节处理好,易扩展的代码就是整洁代码. 抛开业务场景不谈,只谈所谓的"整洁代码"就是所谓的耍流氓。

+

整洁的代码总是看起来总是像是某位特别在意它的人写的.几乎没有改进的余地.代码的作者什么都想到了.如果你想改进它,总会回到原点。

+

代码的作用是为了解决人们的某种需求,什么语言不重要(但是总有人非要争个高低),问题解决了才重要.在规定的业务场景下,写出能解决用户需求的代码就是程序员的日常工作,而需求并不是一成不变的,需求会变代码也会改变,所以我们就需要再这个特定的业务场景中尽量把代码变得灵活起来,之后增加需求或者修改需求时,会变的容易一些。

+

至于方法的规范命名,可读性,注释等.这些也很重要,毕竟开发的时候一般都是团队一起来开发,代码不止你自己看,还需要别人看,说简单一些就是别人好接手你的代码,即代码的维护

+

可读性

+

在一个产品的周期中,开发其实只占了一小段时间,绝大多数时间都在维护代码.代码写出来首先是给人看的,其次是给电脑看,所以代码的可读性至关重要.所以,如果我非要在代码的可读性和运行效率之间选择一个,非可读性莫属.一般来说,要权衡代码的可读性和运行效率,如果差距太大,要看实际的业务场景来决定,毕竟写程序的最终目的是为了解决用户的某些问题.

+

可读性通常表现在代码易于理解

+
    +
  • 容易理解整个应用程序的执行流程
  • +
  • 容易理解不同对象之间的协作方式
  • +
  • 容易理解每个类的作用和责任
  • +
  • 容易理解每个方法的作用
  • +
  • 容易理解每个表达式和变量的目的是什么
  • +
+

如果一个方法的行数过多也会影响代码的可读性,一般控制在80行左右。过多无用的注释、API,只会加重使用者的认知负担,过多无用的信息读起来只会浪费时间,所以要尽量保持API的精简,代码注释的合理,保持规范的命名,使注释看起来没那么臃肿。要知道代码有人维护,可注释没有人维护。

+

代码依赖性导致了变化的放大和高认知负荷。模糊性造成了未知的不可知性,导致了认知负荷。从而使得代码更加难以理解,从而不能很好的维护. 所以整洁的代码总是复杂性低的。

+

运行效率

+

运行效率即代码的运行效率,包括运行所占用的时间和空间。如果数据量不是很大(单表在300w左右)可能几乎不用考虑这个问题,空间就更不用说了,现在大多数公司都是用空间来换时间的,即通过增加服务器的配置或数量来提高程序运算速度。所以很多人并不关心程序运行的效率。

+

诚然,我也不是很关心软件的运行效率,因为软件的运行效率主要还是取决与硬件的发展水平,现在硬件发展比软件发展快了一个档次,不然现在也不能一下子涌起那么多的软件公司。

+

但是,如果业务量非常大,电脑的运行效率也是有限的,当服务器达到一定数量后,企业就会考虑成本毕竟不能一直毫无节制的增加下去,这时候就需要考虑程序的运行效率了。

+

作为一个好的程序员,你不得不具备这项技能。

+

怎样提高程序的运行效率,有没有想过?程序是算法和数据结构组成的,数据结构决定一个程序的空间复杂度,算法则决定一个程序的时间复杂度。 想要程序跑的更快,空间占用更少,可以从这两个维度来进行探索。

+

一个好的算法离不开一个好的想法,这对于一个程序来说是至关重要的,因为它是决定了程序运行速度的关键原因。可能很多人都有一个误区,就是代码越少执行效率就越高,在改进算法的时候会通过删减代码来进行。举个例子:匹配字符串,在数据量很大的情况下,暴力匹配的方式无论你怎么改,都会比那些运用了好的算法的程序慢。

+

不整洁的代码是混乱的,代码混乱到一定程度就会对程序的运行速度产生影响。所以,代码的整洁程度一定程度上影响了代码的运行速度。

+

扩展性

+

整洁的代码除了是可读性强、运行效率高还有最重要的一点是它是容易扩展的。扩展性可理解为易于修改的代码。程序的扩展性代表了维护该程序程度的难易,当然可读性也是,二者都很重要。

+

在所有的设计模式中,几乎所有的设计模式都是为了符合开闭原则,即保持程序的扩展性,重要程度可见一斑。

+

代码都是为了一定的需求服务的,但是这些需求并不是一成不变的,当需求变更了,如果我们代码的扩展性很好,我们可能只需要简单的添加或者删除模块就行了,如果扩展性不好,可能所有代码都需要重写,那就是一场灾难了,所以提高代码的扩展性是很重要的。

+

衡量代码扩展性可以从高内聚,低耦合这两个方面来衡量。

+
+

高内聚:一个软件单位内部的关联紧密程度;有关联的事务应该放在一起。 +低耦合:两个或多个软件单位之间的关联紧密程度;软件单位之间尽可能的不要相互影响。

+
+

怎么写整洁的代码

+

好的代码是不断的迭代出来的,没有人能一下子写完整 ,需求会变代码也会改变 第一次迭代可能写的代码很糟糕,这时一定要再次回头去看之前的代码,去优化,重构,让代码变得易于维护.

+

如何写出整洁的代码,那就看你怎么理解整洁的代码,理解的不一样写出来的肯定就不一样.下面是我的几点建议

+

注释&命名

+

注释只是二手的信息,它和代码所传达的信息并不等价.所以,不要写没有意义的注释(冗余注释,废弃注释,等一些没有意义的信息和不恰当的信息),要知道代码有人维护,可注释没有人维护,最好的办法就是规范变量,方法的命名,做到见名知意; 如果你的方法命名足够明确就可以不用写注释了,当然一段好的注释一定是包含代码中没有体现的信息。

+

如果要编写一条注释,就花时间尽量保证写出最好的注释 不要画蛇添足,要保持注释的简洁。比如,无用的代码直接删掉,不要注释它,不用担心会丢,版本服务会有记录能找回。编写注释和迭代代码是一样的道理。但是一般注释是没有人来维护的,因为它不会影响程序的正常运行。

+

同样的,命名也不会影响程序的正常运行。注释和命名是不会影响程序的执行的,但是这两个因素是会影响到开发者编写代码的。它会导致开发者的认知负荷增加,从而降低编写代码的效率。

+

“ 好的代码本身就是注释,我们要尽量规范和美化自己的代码来减少不必要的注释。若编程语言足够有表达力,就不需要注释,尽量通过代码来阐述。”编程语言的表达力很大程度上就取决于方法的命名。

+

那什么是好的命名呢?说实话这也是我编程一直头疼的原因,总觉得命名不够好。在网络上看到一种方法觉得很不错: 把你的变量名拎出来,问别人,你看到这个名字会想到什么,他说的和你想的一致,就用,否则就改。改到基本上不懂程序的人都能大概看懂你写的是什么,那大概就是好的命名了。

+

程序的命名我认为是约定俗成的,最初开始编程的开发者们,他们一定会遇到这个问题的,久而久之就会建立一套规则将这些命名进行统一。到了现在一定是有很多成熟的命名规则的,所以我们可以踩着前辈们的肩膀前行。

+

虽然是约定俗成的,但是事物的发展一定不是一成不变的,我并不知道在我写下这么多文字后是否适用于未来,或许大概只有思想会适用,而那些具体的方法是一定不会适用的,但是现在我们需要具体的解决方案,或许我能给你提点建议,如果能帮到你我会很高兴的.

+

注释

+
    +
  • 尽量保持代码的简洁,能不用注释尽量不用注释,切记注释应该展示代码中没有展示的信息
  • +
  • 注释代码,那么应该在注释代码的上方详细的说明,而不是简单的注释,如果没有用则应该删除
  • +
  • 注释也要随着程序的修改而不断更新,一个误导的注释往往比没有注释更糟糕
  • +
+

命名

+
    +
  • 命名尽量不要使用缩写,因为意义不明确,除非缩写是外界公认的,比如: EN,CH
  • +
  • 在Java中较为适用的是驼峰命名法,如: helloWord,HelloWord ,但是这种方法如果命名过长也不能很直观的展示信息,所以尽量不要起太长的名字,否则什么方法都不管用,如果一个方法的名字过长那么很可能这个方法不止做了一件事,这时候我们需要将它拆分.
  • +
  • 接口和类的名称首字母大写(MyClass/MyInterface) ,方法的名称首字母小写(myMethod),常量的命名全部大写(MY _CONSTANT)同时用蛇形命名法,单词的分割用下划线隔开
  • +
  • 方法的命名用动词,类的命名用名词,属性的命名用名词,接口的命名用形容词或动词,抽象类的命名应以Abstract/Base为前缀,实现类命名要以impl结尾,异常类以Exception结尾
  • +
+

函数&类

+

最好的方法入参参数是0个,其次是1,最多建议不超过3个,大于三个建议封装成对象,这样做的好处是方便扩展管理;在一个方法中声明变量应该在方法的最前面,我们应该降低方法的复杂度,避免出现if..else多层嵌套的情况,当然,如果一个方法过于复杂可将该方法进行拆分;还应当注意方法中异常块的处理,一般情况下是不会写的,因为有全局异常处理,如果非要写那么可能代码看起来不是那么简洁清晰.注意方法的封装.在方法调用本地方法的时候,本地的方法尽量使用基础类型作为返回值,好处是避免空指针判断和隐式拆包和打包.当我们写方法返回值的时候,如果返回值类型是数组或集合,我们应该避免返回null,否则调用方就有可能出现空指针.可以避免不必要的空指针判断。

+

或许我应该更加细致的标出

+
    +
  • 减少函数的入参数量,控制在三个以内,超过三个封装成对象
  • +
  • 使用卫语句,策略模式,职责链模式来减少if..else多层嵌套和不必要的else;用三元运算符代替简单if…else(根据不同的情境使用,可能会影响代码的可读性)
  • +
  • 拆分超长函数(函数行数阈值80~100行左右就要考虑拆分);复杂的条件表达式,循环语句,代码块,lambda匿名内部类,都可以单独将其封装成方法;如果是方法内部调用方法的返回值应尽量用基础类型 (并不是方法越多越好,方法之间的互相调用也会影响性能,增加复杂度,请根据实际情况拆分)
  • +
  • 方法的返回值如果是集合或者数组,不要返回null,尽量返回空值,这样可以避免空指针的判断,从而精简代码
  • +
+

除了上述的几点以外,在写函数的时候还要遵循单一职责原则,即每个方法只做一件事,好处是方便管理,代码可读性会提高,复杂度降低 易于维护。也要遵循开闭原则,这会使你的代码更加灵活。当然这些代码肯定不是一次就写出来的,好的代码需要迭代,需要打磨。在你写完几个函数之后,可能会发现重复的地方,这时候就需要将他们抽象出来。

+

许许多多的函数组成了类,当函数之间相互调用的时候,就会存在多个类之间的联系,所以在编写类的时候要注意类之间的依赖关系,使它们别那么耦合,一般会遵循迪米特法则。

+

属性的存在使类中的元素更加丰富,一般情况下属性在类中都是私有的,会对外提供set,get方法供外部调用修改;这样做的好处是方便控制外部调用,假如你想公共处理某个属性给它加个前缀,就可以通过调用该类中涉及到该属性的方法进行修改,如果你直接修改属性那么改起来会很麻烦。

+

极少数情况是公共的,比如定义一个常量类,公共的资源属性。还有多数情况下是受保护的,该种情况一般是用来给子类使用的,当然同一个包下也能访问得到。

+

代码结构

+

高内聚低耦合,这是我们写代码应该遵循的标准。内聚代表着职责的专一,这是整洁的一个很重要准则。从大的方面来说,系统的使用与构造需要明确的区分开,才不会将整个结构混杂在一起。与此同时,决定了系统的数据流走向也是决定了整个系统的层级划分,不同的层级也需要明确的区分开来。

+

那么应该怎么划分代码的结构?最简单的应该同类型的相关联的表需要放在一个类中或一个包中,写一些方法对外提供api,供其他方法调用,而不是跨层调用。

+

一个好的结构使代码看上去更加清晰,更加容易维护,其实它更像是对系统架构的拆分。最常见的系统分层应该是MVC结构,即模型层、视图层、控制层。我们应当更加详细的将MVC进行划分,最常见的我们将控制层又分为业务层(service)和持久层(dao)。 划分的目的是规划软件系统的逻辑结构便于开发维护。但是,随着微服务的演变和不同研发的编码习惯,往往导致了代码分层不彻底导致引入了“坏味道”。

+

划分代码我认为最重要的作用是使结构单一,减少代码之间的依赖性,降低耦合度,从而提高代码的可维护性。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/data-structures-algorithms/index.html b/blog-site/public/posts/essays/data-structures-algorithms/index.html new file mode 100644 index 00000000..a5b4c181 --- /dev/null +++ b/blog-site/public/posts/essays/data-structures-algorithms/index.html @@ -0,0 +1,6560 @@ + + + + + + + + + + + 数据结构与算法 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

数据结构与算法

+ 2021.12.10 +
+

数据结构

+

数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好数据结构可以编写出更有效率的代码。数据结构是算法的基础,想要学好算法,就必须把数据结构学到位。

+

数据结构包括:线性结构、非线性结构。

+

线性结构作为最常用的数据结构,其特点是数据元素之间存在一对一的线性存储关系。线性结构有两种不同的存储结构,即顺序存储和链式存储。顺序存储的线性表被称为顺序表,顺序表中存储元素的地址是连续的;链式存储的线性表被称为链表,链表中存储元素的地址不一定是连续的,元素结点中存放数据元素以及相邻元素地址信息。

+

常见的线性结构有:数组、链表、队列、栈;常见非线性结构有:多维数组、广义表、树结构、图结构。

+

稀疏数组

+

使用稀疏数组可以用来压缩数据。稀疏数组的第一行依次记录原数组一共有几行几列,有多少个不为零的值,之后的每行记录原数组中不为零的值所在的行数、列数以及数组中元素该值。如图所示:

+

数据结构与算法-001

+

二维数组转稀疏数组

+
class TwoDimensionArrayDemo {
+  // 将二维数组转换为稀疏数组
+  public static int[][] twoDimensionArrayToSparseArray(int[][] array) {
+    // 记录棋盘中有效值的数量
+    int sum = 0;
+    int row = array.length;
+    int column = 0;
+    for (int[] ints : array) {
+      column = ints.length;
+      for (int item : ints) {
+        if (item != 0) {
+          sum++;
+        }
+      }
+    }
+
+    // 创建稀疏数组
+    int[][] sparseArray = new int[sum + 1][3];
+    sparseArray[0][0] = row;
+    sparseArray[0][1] = column;
+    sparseArray[0][2] = sum;
+
+    // 给稀疏数组赋值
+    int count = 0;
+    for (int i = 0; i < array.length; i++) {
+      for (int j = 0; j < array[i].length; j++) {
+        if (array[i][j] != 0) {
+          count++;
+          sparseArray[count][0] = i;
+          sparseArray[count][1] = j;
+          sparseArray[count][2] = array[i][j];
+        }
+      }
+    }
+
+    System.out.println("稀疏数组====》");
+    for (int i = 0; i < sparseArray.length; i++) {
+      System.out.printf("%d\t%d\t%d\t\n", sparseArray[i][0], sparseArray[i][1], sparseArray[i][2]);
+    }
+    return sparseArray;
+  }
+}
+

稀疏数组转二维数组

+
class TwoDimensionArrayDemo {
+  // 稀疏数组转二维数组
+  public static int[][] sparseArrayToTwoDimensionArray(int[][] sparseArray) {
+    int[][] toTwoDimensionArray = new int[sparseArray[0][0]][sparseArray[0][1]];
+    // 给二维数组赋值
+    for (int i = 1; i < sparseArray.length; i++) {
+      toTwoDimensionArray[sparseArray[i][0]][sparseArray[i][1]] = sparseArray[i][2];
+    }
+
+    System.out.println("二维数组====》");
+    for (int[] row : toTwoDimensionArray) {
+      for (int data : row) {
+        System.out.printf("%d\t", data);
+      }
+      System.out.println();
+    }
+    return toTwoDimensionArray;
+  }
+}
+

队列

+

队列是一个有序列表,可以使用数组或链表来实现。队列遵循先入先出的原则。即,先存入队列的数据,要先取出。后存入的要后取出。

+

使用数组模拟队列示意图:

+

数据结构与算法-002

+

数组模拟单向队列

+
    public class ArrayQueue{
+
+        // 队列容量
+        private int capacity;
+
+        // 保存队列中的数据
+        private int[] arr;
+
+        // 头部指针
+        private int front;
+
+        // 尾部指针
+        private int rear;
+
+        public ArrayQueue(int capacity) {
+            this.capacity = capacity;
+            arr = new int[capacity];
+            front = -1;
+            rear = -1;
+        }
+
+        public boolean isEmpty() {
+            return front == rear;
+        }
+
+        public boolean isFull() {
+            return capacity - 1 == rear;
+        }
+
+        public void add(int data) {
+            if (isFull()) {
+                System.out.println("队列已经满了,不能在继续添加");
+                return;
+            }
+            arr[++rear] = data;
+        }
+
+        public int get() {
+            if (isEmpty()) {
+                System.out.println("队列为空,不能获取元素");
+                return -1;
+            }
+            return arr[++front];
+        }
+
+        // 显示队列的所有数据
+        public void show() {
+            if (isEmpty()) {
+                System.out.println("队列空的,没有数据~~");
+                return;
+            }
+            System.out.println("开始遍历队列:");
+            for (int i = front + 1; i <= rear; i++) {
+                System.out.printf("arr[%d]=%d\n", i, arr[i]);
+            }
+        }
+
+
+        // 显示队列的头数据, 注意不是取出数据
+        public int peek() {
+            if (isEmpty()) {
+                throw new RuntimeException("队列空的,没有数据~~");
+            }
+            return arr[front + 1];
+        }
+
+    }
+

数组模拟环形队列

+
    public  class CircleArrayQueue{
+
+        // 队列容量
+        private int capacity;
+
+        // 保存队列中的数据
+        private int[] arr;
+
+        // 头部指针
+        private int front;
+
+        // 尾部指针
+        private int rear;
+
+        public CircleArrayQueue(int capacity) {
+            this.capacity = capacity;
+            arr = new int[capacity];
+        }
+
+        public boolean isEmpty() {
+            return front == rear;
+        }
+
+        public boolean isFull() {
+            // 此处+1 是因为存储元素从0算起
+            return (rear + 1) % capacity  == front;
+        }
+
+        public void add(int data) {
+            if (isFull()) {
+                System.out.println("队列已经满了,不能在继续添加");
+                return;
+            }
+            arr[rear] = data;
+            rear = (rear + 1) % capacity;
+        }
+
+        public int get() {
+            if (isEmpty()) {
+                System.out.println("队列为空,不能获取元素");
+                return -1;
+            }
+            int value = arr[front];
+            front = (front + 1) % capacity;
+            return value;
+        }
+
+        // 显示队列的所有数据
+        public void show() {
+            if (isEmpty()) {
+                System.out.println("队列空的,没有数据~~");
+                return;
+            }
+            System.out.println("开始遍历队列:");
+            for (int i = front % capacity; i < front + ((rear + capacity - front) % capacity); i++) {
+                System.out.printf("arr[%d]=%d\n", i, arr[i]);
+            }
+        }
+
+        // 显示队列的头数据, 注意不是取出数据
+        public int peek() {
+            if (isEmpty()) {
+                throw new RuntimeException("队列空的,没有数据~~");
+            }
+            return arr[front];
+        }
+
+    }
+

链表

+

链表属于线性结构,存储空间不连续。

+

链表特点:

+
    +
  • 链表是以节点的方式来存储,是链式存储;data 域存放数据,next 域指向下一个节点;
  • +
  • 链表分带头节点的链表和没有头节点的链表,根据实际的需求来确定;
  • +
+

数据结构与算法-003

+

单向链表

+

操作单向链表:对于插入、删除操作,只能定位至待操作节点的前一个节点,如果定位至当前节点,那么其上一个节点的信息便会丢失。

+

单向链表,链表的增、删、查、改

+
class SingleLinkedList{
+
+    // 头结点
+    private Node headNode = new Node(0,"");
+
+    // 添加方法
+    public  void add(Node node){
+        Node tmpNode = headNode;
+
+        while (tmpNode.next != null){
+            // 指向下一个结点
+            tmpNode = tmpNode.next;
+        }
+        // 退出循环意味着tmpNode.next == null 即找到最后一个结点了
+        tmpNode.next = node;
+    }
+
+    // 顺序添加
+    public void addByOrder(Node node){
+        boolean flag = false;
+        Node tmp = headNode;
+        while (true){
+            if (tmp.next == null) {
+                break;
+            }
+
+            // 将新插入的结点num跟链表中已经存在的num进行比较,如果 < 链表中的结点 则说明找到了该位置
+            if (node.num < tmp.next.num){
+                break;
+            }
+            // 如果num相同则不能添加
+            if (node.num == tmp.next.num){
+                flag = true;
+                break;
+            }
+            tmp = tmp.next;
+        }
+
+        if (!flag){
+            node.next = tmp.next;
+            tmp.next = node;
+            return;
+        }
+        System.out.printf("需要添加的结点编号:%d已经存在了",node.num);
+    }
+
+    // 遍历链表
+    public void list() {
+        // 遍历除了头结点外的所有结点
+        Node tmpNode = headNode.next;
+        if (tmpNode == null){
+            System.out.println("链表为空!");
+            return;
+        }
+
+        while (tmpNode != null){
+            System.out.println(tmpNode);
+            // 指向下一个结点
+            tmpNode = tmpNode.next;
+        }
+    }
+
+
+}
+
+
+class Node {
+
+    int num;
+    String name;
+    Node next;
+
+    public Node(int num,String name){
+        this.num = num;
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return "Node{" +
+                "num=" + num +
+                ", name='" + name + '\'' +
+                '}';
+    }
+}
+

反转单向链表

+

数据结构与算法-004

+
class LinkedListDemo{
+    // 反转链表
+    public void reserve(Node head) {
+      if (head.next == null || head.next.next == null) {
+        return;
+      }
+
+      Node reserve = new Node(0, "");
+      Node cur = head.next;
+      Node next = null;
+      // 遍历链表
+      // 遍历原来的链表,每遍历一个节点,就将其取出,并放在新的链表reverseHead 的最前端
+      while (cur != null) {
+
+        // 保存当前结点的下一个结点
+        next = cur.next;
+
+        // 将cur的下一个节点指向新的链表的最前端(覆盖掉)保证将最新的结点放到reseve的最前面
+        cur.next = reserve.next;
+
+        // 将cur 连接到新的链表上
+        reserve.next = cur;
+
+        // 将之前保存好的结点赋值给当前结点
+        cur = next;
+      }
+    }
+}
+

利用栈逆序打印单向链表

+
class LinkedListDemo {
+  public void reservePrint(Node head) {
+    if (head.next == null || head.next.next == null) {
+      return;
+    }
+
+    Stack<Node> nodes = new Stack<>();
+    Node tmp = head.next;
+    while (tmp != null) {
+      nodes.push(tmp);
+      tmp = tmp.next;
+    }
+
+    // 从stack中取出结点
+    while (nodes.size() > 0) {
+      System.out.println(nodes.pop());
+    }
+  }
+}
+

双向链表

+

对比单向链表:

+
    +
  • 单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找;
  • +
  • 单向链表不能自我删除,需要靠辅助节点,而双向链表,则可以自我删除;
  • +
+

数据结构与算法-005

+

双向链表,增、删、改、查

+
class DoubleLinkedList{
+
+    // 头结点
+    private Node headNode = new Node(0,"");
+
+
+    public Node getHeadNode() {
+        return headNode;
+    }
+
+
+    // 添加方法
+    public  void add(Node node){
+        Node tmpNode = headNode;
+
+        while (tmpNode.next != null){
+            // 指向下一个结点
+            tmpNode = tmpNode.next;
+        }
+        // 退出循环意味着tmpNode.next == null 即找到最后一个结点了
+        tmpNode.next = node;
+        node.prev = tmpNode;
+    }
+
+
+    // 双向链表修改
+    public void update(Node node){
+        if (headNode == null) {
+            return;
+        }
+
+        Node tmp = headNode.next;
+        while (true){
+            if (tmp == null){
+                break;
+            }
+            if (node.num == tmp.num){
+                tmp.name = node.name;
+                break;
+            }
+            tmp = tmp.next;
+        }
+    }
+
+    // 双向链表删除
+    public void remove(int num){
+        if (headNode.next == null){
+            System.out.println("链表为空,无法删除!");
+            return;
+        }
+        Node tmp = headNode.next;
+        while (tmp != null){
+            if (num == tmp.num){
+                tmp.prev.next = tmp.next;
+
+                // 最后一个结点的next 为null null.pre会出现空指针异常
+                if(tmp.next != null) {
+                    tmp.next.prev = tmp.prev;
+                }
+                break;
+            }
+            tmp = tmp.next;
+        }
+    }
+
+    // 遍历链表
+    public void list() {
+        // 遍历除了头结点外的所有结点
+        Node tmpNode = headNode.next;
+        if (tmpNode == null){
+            System.out.println("链表为空!");
+            return;
+        }
+
+        while (tmpNode != null){
+            System.out.println(tmpNode);
+            // 指向下一个结点
+            tmpNode = tmpNode.next;
+        }
+    }
+
+
+}
+
+
+class Node {
+
+    int num;
+    String name;
+    Node next;
+    Node prev;
+
+    public Node(int num,String name){
+        this.num = num;
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return "Node{" +
+                "num=" + num +
+                ", name='" + name + '\'' +
+                '}';
+    }
+}
+

约瑟夫问题

+
+

约瑟夫问题为:设编号为 1,2,…n的n个人围坐一圈,约定编号为 k(1<=k<=n) 的人从 1 开始报数,数到 m 的那个人出列,它的下一位又从 1 开始报数,数到 m 的那个人又出列, 依次类推,直到所有人出列为止,由此产生一个出队编号的序列。

+
+

用一个不带头结点的循环链表来处理约瑟夫问题:先构成一个有 n 个结点的单循环链表,然后由 k 结点起从 1 开始计数,计到 m 时,对应结点从链表中删除,然后再从被删除结点的下一个结点又从 1 开始计数,直到最后一个结点从链表中删除算法结束。

+

数据结构与算法-006

+
class JoesphSingletonLinkedList {
+
+    private Node first = null;
+
+
+    // 向单向链表添加数据
+    public void add(int nums) {
+        if (nums < 1) {
+            System.out.println("nums的值不正确");
+            return;
+        }
+        Node cur = null;
+        for (int i = 1; i <= nums; i++) {
+            Node node = new Node(i);
+            if (i == 1) {
+                first = node;
+                first.next = first;
+                cur = first;
+            } else {
+                cur.next = node;
+                node.next = first;
+                cur = node;
+            }
+        }
+    }
+
+
+    // 遍历单向循环链表
+    public void list() {
+        Node tmp = first;
+        while (true){
+            System.out.printf("当前结点为:%d\n",tmp.num);
+            if (tmp.next == first){
+                break;
+            }
+            tmp = tmp.next;
+        }
+    }
+
+
+    // 约瑟夫问题
+    public void joseph(int startNum,int countNum,int sum){
+        if (startNum > sum || startNum < 0 || countNum < 0) {
+            System.out.println("输入的参数不正确!");
+            return;
+        }
+        // 创建辅助指针,将该指针指向 first 的前一个
+        Node helper = first;
+        while (helper.next != first) {
+            helper = helper.next;
+        }
+
+        // 将first 和 help指针循环 (startNum - 1)次;因为从startNum开始,需要减一
+        for (int i = 0; i < startNum - 1; i++) {
+            first = first.next;
+            helper = helper.next;
+        }
+
+        while (true){
+            // 当环形链表中只存在一个结点
+            if (first == helper){
+                break;
+            }
+
+            // 因为是环形链表,所以需要循环挨个出链表
+            for (int i = 0; i < countNum - 1; i++) {
+                first = first.next;
+                helper = helper.next;
+            }
+
+            // 当前 first 就是出圈的结点
+            System.out.printf("当前出队列的结点编号为:%d\n",first.num);
+            first = first.next;
+            helper.next = first;
+        }
+        System.out.printf("最后的结点为:%d\n",first.num);
+    }
+
+}
+
+
+class Node {
+
+    int num;
+    Node next;
+
+    public Node(int num){
+        this.num = num;
+    }
+
+    @Override
+    public String toString() {
+        return "Node{" +
+                "num=" + num +
+                '}';
+    }
+}
+

+

栈的英文为stack,是一个先入后出(FILO-First In Last Out)的有序列表,最先放入栈中元素在栈底,最后放入的元素在栈顶,而删除元素刚好相反,最后放入的元素最先删除,最先放入的元素最后删除。

+

栈是限制线性表中元素的插入和删除只能在线性表的同一端进行的一种特殊线性表。允许插入和删除的一端,为变化的一端,称为栈顶(Top),另一端为固定的一端,称为栈底(Bottom)。

+

下图是出栈和入栈

+

数据结构与算法-007

+

栈的应用场景:

+
    +
  • 子程序的调用: 在跳往子程序前, 会先将下个指令的地址存到堆栈中, 直到子程序执行完后再将地址取出, 以回到原来的程序中;
  • +
  • 处理递归调用: 和子程序的调用类似, 只是除了储存下一个指令的地址外, 也将参数、 区域变量等数据存入栈中;
  • +
  • 表达式的转换:中缀表达式转后缀表达式与求值;
  • +
  • 二叉树的遍历;
  • +
  • 图形的深度优先(depth 一 first)搜索法;
  • +
+

数组模拟栈

+
class ArrayStack<T>{
+
+    private int size;
+    private int top;
+    private Object[] stack;
+
+    public ArrayStack(int size) {
+        this.size = size;
+        this.stack = new String[size];
+        top = -1;
+    }
+
+    public boolean isFull(){
+        return top == size -1;
+    }
+
+    public boolean isEmpty() {
+        return top == -1;
+    }
+
+    // 入栈
+    public void push(T item) {
+        if (isFull()) {
+            System.out.println("栈已经满了,不能继续添加!");
+            return;
+        }
+        top++;
+        this.stack[top] = item;
+    }
+
+    // 出栈操作
+    public T pop() {
+        if (isEmpty()) {
+            throw new RuntimeException("栈已经为空,不能继续pop");
+        }
+        T val = (T)this.stack[top];
+        top--;
+        return val;
+    }
+
+    // 遍历栈
+    public void list() {
+        if (isEmpty()) {
+            System.out.println("栈为空不能继续遍历!");
+            return;
+        }
+
+        System.out.println("遍历栈==》");
+        for (int i = top; i >=0; i--){
+            System.out.println(this.stack[i]);
+        }
+    }
+
+}
+

栈实现中缀表达式

+

数据结构与算法-008

+
class CalcInfixExpressions {
+
+
+    public int calcInfixExpressions(String expression) {
+        // 定义变量
+        char[] chars = expression.toCharArray();
+        int len = chars.length;
+        Stack<Integer> numStack = new Stack<>();
+        Stack<Character> oprStack = new Stack<>();
+        int index = 0;
+
+        for (int j = 0; j < len; j++) {
+
+            char ch = chars[j];
+
+            index++;
+
+            // 判断字符是否为数字,如果是数字就放入数栈中
+            if (Character.isDigit(ch)) {
+
+                // 接收多位数
+                int num = ch;
+                boolean flag = false;
+
+                // 从当前字符开始遍历,如果下一位字符不是数字,则将该数字压入栈中并退出循环,如果是数字,则需要拼接起来
+                for (int i = index; i < len; i++) {
+                    if (Character.isDigit(expression.charAt(i))) {
+                        String strNum = String.valueOf(ch) + expression.charAt(i);
+                        num = Integer.parseInt(strNum);
+                        flag = true;
+                        index++;
+                        j++;
+                    }else {
+                        break;
+                    }
+                }
+                if (!flag) {
+                    num -= 48;
+                }
+                numStack.push(num);
+                continue;
+            }
+
+            // 非数字,即运算符,如果为空直接加入栈中
+            if (oprStack.isEmpty()) {
+                oprStack.push(ch);
+                continue;
+            }
+
+            // 如果运算符栈不为空,需要比较运算符的优先级,如果当前运算符的优先级 <= 栈顶的运算符的优先级,需要计算在压入栈中
+            if (oprPriority(oprStack.peek()) >= oprPriority(ch)) {
+                numStack.push(calc(numStack.pop(), numStack.pop(), oprStack.pop()));
+            }
+
+            // 将字符压入操作符栈中
+            oprStack.push(ch);
+        }
+
+
+        // 将处理好的数据按照顺序弹出,进行计算,得到数栈中最后一个数就是最终的结果
+        while (!oprStack.isEmpty()){
+            numStack.push(calc(numStack.pop(), numStack.pop(), oprStack.pop()));
+        }
+
+        return numStack.pop();
+    }
+
+
+    // 获取字符的优先级
+    private int oprPriority(int ch) {
+        if (ch == '*' || ch == '/') {
+            return 2;
+        }
+        if (ch == '+' || ch == '-') {
+            return 1;
+        }
+        return -1;
+    }
+
+
+    // 计算
+    private int calc(int num1, int num2, int opr) {
+        int res = 0;
+        switch (opr) {
+            case '+':
+                res = num1 + num2;
+                break;
+            case '-':
+                res = num2 - num1;
+                break;
+            case '*':
+                res = num1 * num2;
+                break;
+            case '/':
+                res = num2 / num1;
+                break;
+        }
+        return res;
+    }
+
+}
+

栈实现后缀表达式

+

后缀表达式又称逆波兰表达式,与前缀表达式相似,只是运算符位于操作数之后。例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 –

+

思路:从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果。

+
class PolandNotation{
+
+    // 计算后缀表达式
+    public int calcSuffixExceptions(String suffixExpres) {
+        char[] chars = suffixExpres.toCharArray();
+        Stack<Integer> stack = new Stack<>();
+        int res =0;
+
+        for (int i = 0; i < chars.length; i++) {
+            char ch = suffixExpres.charAt(i);
+            if (!Character.isDigit(ch)) {
+                res = calc(stack.pop(),stack.pop(),ch);
+                stack.push(res);
+            }else {
+                stack.push(ch - 48);
+            }
+        }
+        return res;
+    }
+
+    // 计算
+    private int calc(int num1, int num2, int opr) {
+        int res = 0;
+        switch (opr) {
+            case '+':
+                res = num1 + num2;
+                break;
+            case '-':
+                res = num2 - num1;
+                break;
+            case '*':
+                res = num1 * num2;
+                break;
+            case '/':
+                res = num2 / num1;
+                break;
+            default:
+                throw new RuntimeException("运算符有误");
+        }
+        return res;
+    }
+
+}
+

中缀表达式转后缀表达式

+

数据结构与算法-009

+

中缀表达式转后缀表达式代码实现

+
class InfixToPolandNotation{
+
+    // 根据ASCII 判断是否为数字
+    private boolean isNumber(char ch){
+        return ch >=48 && ch <= 57;
+    }
+
+    // 获取字符的优先级
+    private int oprPriority(String opr) {
+        if (opr.equals("*") || opr.equals("/")) {
+            return 2;
+        }
+        if (opr.equals("+") || opr.equals("-")) {
+            return 2;
+        }
+        return -1;
+    }
+
+    // 将中缀表达式字符串转成中缀表达式集合
+    public List<String> toInfixExceptionList(String str) {
+        ArrayList<String> list = new ArrayList<>();
+        int index = 0;
+        StringBuilder number;
+        char c;
+
+        while (index < str.length()){
+            if (!isNumber((c = str.charAt(index)))){
+                list.add(String.valueOf(c));
+                index++;
+            }else {
+                number = new StringBuilder();
+                while (index < str.length() && isNumber((c = str.charAt(index)))){
+                    index++;
+                    number.append(c);
+                }
+                list.add(number.toString());
+            }
+        }
+        return list;
+    }
+
+
+    // 将中缀表达式转为后缀表达式
+    public List<String> infixExpressionToSuffixExpress(List<String> list) {
+        Stack<String> stack = new Stack<>();
+        ArrayList<String> finalList = new ArrayList<>();
+
+        for (String item : list) {
+            // 如果是数字或者为( 将该值压入栈中
+            if (item.matches("\\d+")){
+                finalList.add(item);
+                continue;
+            }
+
+            if (item.equals("(")){
+                stack.push(item);
+                continue;
+            }
+
+            // 如果是 )则将 ()中间的数重新压入list中,最后将 ) 移除掉
+            if (item.equals(")")){
+                while (!stack.peek().equals("(")) {
+                    finalList.add(stack.pop());
+                }
+                stack.pop();
+            }else {
+                // 如果不是 )则判断运算符的优先级,如果符号栈栈顶的优先级 >= 当前的优先级,则将该运算符加入数字栈中
+                while (stack.size() > 0 && oprPriority(stack.peek()) >= oprPriority(item)){
+                    finalList.add(stack.pop());
+                }
+                stack.push(item);
+            }
+        }
+
+        // 将operStack中剩余的运算符依次弹出并加入tempList
+        while (stack.size() != 0) {
+            finalList.add(stack.pop());
+        }
+        return finalList;
+    }
+
+}
+

完整逆波兰表达式代码,支持小数、支持消除空格

+
public class ReversePolishMultiCalc {
+
+	 /**
+     * 匹配 + - * / ( ) 运算符
+     */
+    static final String SYMBOL = "\\+|-|\\*|/|\\(|\\)";
+
+    static final String LEFT = "(";
+    static final String RIGHT = ")";
+    static final String ADD = "+";
+    static final String MINUS= "-";
+    static final String TIMES = "*";
+    static final String DIVISION = "/";
+
+    /**
+     * 加減 + -
+     */
+    static final int LEVEL_01 = 1;
+    /**
+     * 乘除 * /
+     */
+    static final int LEVEL_02 = 2;
+
+    /**
+     * 括号
+     */
+    static final int LEVEL_HIGH = Integer.MAX_VALUE;
+
+
+    static Stack<String> stack = new Stack<>();
+    static List<String> data = Collections.synchronizedList(new ArrayList<String>());
+
+    /**
+     * 去除所有空白符
+     * @param s
+     * @return
+     */
+    public static String replaceAllBlank(String s ){
+        // \\s+ 匹配任何空白字符,包括空格、制表符、换页符等等, 等价于[ \f\n\r\t\v]
+        return s.replaceAll("\\s+","");
+    }
+
+    /**
+     * 判断是不是数字 int double long float
+     * @param s
+     * @return
+     */
+    public static boolean isNumber(String s){
+        Pattern pattern = Pattern.compile("^[-\\+]?[.\\d]*$");
+        return pattern.matcher(s).matches();
+    }
+
+    /**
+     * 判断是不是运算符
+     * @param s
+     * @return
+     */
+    public static boolean isSymbol(String s){
+        return s.matches(SYMBOL);
+    }
+
+    /**
+     * 匹配运算等级
+     * @param s
+     * @return
+     */
+    public static int calcLevel(String s){
+        if("+".equals(s) || "-".equals(s)){
+            return LEVEL_01;
+        } else if("*".equals(s) || "/".equals(s)){
+            return LEVEL_02;
+        }
+        return LEVEL_HIGH;
+    }
+
+    /**
+     * 匹配
+     * @param s
+     */
+    public static List<String> doMatch (String s) throws Exception{
+        if(s == null || "".equals(s.trim())) throw new RuntimeException("data is empty");
+        if(!isNumber(s.charAt(0)+"")) throw new RuntimeException("data illeagle,start not with a number");
+
+        s = replaceAllBlank(s);
+
+        String each;
+        int start = 0;
+
+        for (int i = 0; i < s.length(); i++) {
+            if(isSymbol(s.charAt(i)+"")){
+                each = s.charAt(i)+"";
+                //栈为空,(操作符,或者 操作符优先级大于栈顶优先级 && 操作符优先级不是( )的优先级 及是 ) 不能直接入栈
+                if(stack.isEmpty() || LEFT.equals(each)
+                        || ((calcLevel(each) > calcLevel(stack.peek())) && calcLevel(each) < LEVEL_HIGH)){
+                    stack.push(each);
+                }else if( !stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek())){
+                    //栈非空,操作符优先级小于等于栈顶优先级时出栈入列,直到栈为空,或者遇到了(,最后操作符入栈
+                    while (!stack.isEmpty() && calcLevel(each) <= calcLevel(stack.peek()) ){
+                        if(calcLevel(stack.peek()) == LEVEL_HIGH){
+                            break;
+                        }
+                        data.add(stack.pop());
+                    }
+                    stack.push(each);
+                }else if(RIGHT.equals(each)){
+                    // ) 操作符,依次出栈入列直到空栈或者遇到了第一个)操作符,此时)出栈
+                    while (!stack.isEmpty() && LEVEL_HIGH >= calcLevel(stack.peek())){
+                        if(LEVEL_HIGH == calcLevel(stack.peek())){
+                            stack.pop();
+                            break;
+                        }
+                        data.add(stack.pop());
+                    }
+                }
+                start = i ;    //前一个运算符的位置
+            }else if( i == s.length()-1 || isSymbol(s.charAt(i+1)+"") ){
+                each = start == 0 ? s.substring(start,i+1) : s.substring(start+1,i+1);
+                if(isNumber(each)) {
+                    data.add(each);
+                    continue;
+                }
+                throw new RuntimeException("data not match number");
+            }
+        }
+        //如果栈里还有元素,此时元素需要依次出栈入列,可以想象栈里剩下栈顶为/,栈底为+,应该依次出栈入列,可以直接翻转整个stack 添加到队列
+        Collections.reverse(stack);
+        data.addAll(new ArrayList<>(stack));
+
+        System.out.println(data);
+        return data;
+    }
+
+    /**
+     * 算出结果
+     * @param list
+     * @return
+     */
+    public static Double doCalc(List<String> list){
+        Double d = 0d;
+        if(list == null || list.isEmpty()){
+            return null;
+        }
+        if (list.size() == 1){
+            System.out.println(list);
+            d = Double.valueOf(list.get(0));
+            return d;
+        }
+        ArrayList<String> list1 = new ArrayList<>();
+        for (int i = 0; i < list.size(); i++) {
+            list1.add(list.get(i));
+            if(isSymbol(list.get(i))){
+                Double d1 = doTheMath(list.get(i - 2), list.get(i - 1), list.get(i));
+                list1.remove(i);
+                list1.remove(i-1);
+                list1.set(i-2,d1+"");
+                list1.addAll(list.subList(i+1,list.size()));
+                break;
+            }
+        }
+        doCalc(list1);
+        return d;
+    }
+
+    /**
+     * 运算
+     * @param s1
+     * @param s2
+     * @param symbol
+     * @return
+     */
+    public static Double doTheMath(String s1,String s2,String symbol){
+        Double result ;
+        switch (symbol){
+            case ADD : result = Double.valueOf(s1) + Double.valueOf(s2); break;
+            case MINUS : result = Double.valueOf(s1) - Double.valueOf(s2); break;
+            case TIMES : result = Double.valueOf(s1) * Double.valueOf(s2); break;
+            case DIVISION : result = Double.valueOf(s1) / Double.valueOf(s2); break;
+            default : result = null;
+        }
+        return result;
+
+    }
+
+    public static void main(String[] args) {
+        //String math = "9+(3-1)*3+10/2";
+        String math = "12.8 + (2 - 3.55)*4+10/5.0";
+        try {
+            doCalc(doMatch(math));
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+}
+

哈希表

+

哈希表也叫散列表,是根据关键码值(Key value)而直接进行访问的数据结构。

+

它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

+

手写模拟哈希表

+
public class HashTableDemo {
+
+    public static void main(String[] args) {
+        HashTab hashTab = new HashTab(3);
+        Node node1 = new Node(1, "zs");
+        Node node2 = new Node(2, "lx");
+        Node node3 = new Node(3, "ex");
+        Node node4 = new Node(4, "as");
+        Node node5 = new Node(7, "we");
+
+        hashTab.put(node1);
+        hashTab.put(node2);
+        hashTab.put(node3);
+        hashTab.put(node4);
+        hashTab.put(node5);
+
+        System.out.println("添加元素后===》");
+        System.out.println(hashTab.toString());
+
+        System.out.println("删除后===》");
+        hashTab.remove(4);
+        System.out.println(hashTab.toString());
+    }
+}
+
+class HashTab {
+
+    private NodeList[] nodes;
+    private int size;
+
+    public HashTab(int size) {
+        this.size = size;
+        nodes = new NodeList[size];
+        for (int i = 0; i < size; i++) {
+            nodes[i] = new NodeList();
+        }
+    }
+
+    public void put(Node node) {
+        // 放入hash表的位置
+        nodes[getPosition(node.id)].add(node);
+    }
+
+    public void remove(int id) {
+        nodes[getPosition(id)].delete(id);
+    }
+
+
+    private int getPosition(int id) {
+        return id % size;
+    }
+
+    @Override
+    public String toString() {
+        return "HashTab{" +
+                "nodes=\n" + Arrays.toString(nodes) +
+                "}";
+    }
+}
+
+class NodeList {
+    // 头结点
+    Node head = null;
+
+    // 添加结点方法
+    public void add(Node node) {
+        if (head == null) {
+            head = node;
+            return;
+        }
+
+        // 头结点不要动,将添加的结点放到链表的最后一个位置
+        Node tmp = head;
+        // 当下一个结点等于null时,找到最后一个结点
+        while (tmp.next != null) {
+            tmp = tmp.next;
+        }
+        tmp.next = node;
+    }
+
+
+    // 展示当前链表
+    public void list() {
+        if (head == null) {
+            System.out.println("当前链表为空");
+            return;
+        }
+        // 辅助结点
+        Node tmp = head;
+        while (true) {
+            System.out.println(tmp);
+            if (tmp.next == null) {
+                break;
+            }
+            tmp = tmp.next;
+        }
+    }
+
+    // 根据ID删除链表中的某个结点
+    public void delete(int id) {
+        if (head == null) {
+            System.out.println("当前链表为空");
+            return;
+        }
+        // 判断删除的是否是头结点
+        if (head.id == id) {
+            head = head.next;
+            return;
+        }
+
+        Node preNode = head;
+        Node curNode = preNode.next;
+        while (curNode != null) {
+            if (curNode.id == id) {
+                preNode.next = curNode.next;
+                System.out.println("删除成功,删除的是: " + curNode.id + "," + curNode.name);
+                curNode = null;
+                return;
+            }
+            preNode = preNode.next;
+            curNode = curNode.next;
+        }
+        System.out.println("删除失败,节点不存在");
+    }
+
+
+    @Override
+    public String toString() {
+        return "NodeList{" +
+                "head=" + head +
+                "}\n";
+    }
+}
+
+class Node {
+    int id;
+    String name;
+    Node next;
+
+    public Node(int id, String name) {
+        this.id = id;
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return "Node{" +
+                "id=" + id +
+                ", name='" + name + '\'' +
+                ", next=" + next +
+                "}";
+    }
+}
+

二叉树

+
+

二叉树(Binary tree)是树形结构的一个重要类型。许多实际问题抽象出来的数据结构往往是二叉树形式,即使是一般的树也能简单地转换为二叉树,而且二叉树的存储结构及其算法都较为简单,因此二叉树显得特别重要。二叉树特点是每个结点最多只能有两棵子树,且有左右之分。

+
+

二叉树是n个有限元素的集合,该集合或者为空、或者由一个称为根的元素及两个不相交的、被分别称为左子树和右子树的二叉树组成,是有序树。当集合为空时,称该二叉树为空二叉树。在二叉树中,一个元素也称作一个结点。

+

如果该二叉树的所有叶子节点都在最后一层,并且结点总数为 2^n -1, n 为层数,则称为满二叉树。

+

如果该二叉树的所有叶子节点都在最后一层或者倒数第二层,而且最后一层的叶子节点在左边连续,倒数第二层的叶子节点在右边连续,我们称为完全二叉树。

+
+

PS: 二叉树中结点等价于节点

+
+

数据结构与算法-022

+

二叉树的遍历及查找

+

对于二叉树来讲最主要、最基本的运算是遍历。遍历二叉树 是指以一定的次序访问二叉树中的每个结点。所谓访问结点是指对结点进行各种操作的简称。

+

例如,查询结点数据域的内容,或输出它的值,或找出结点位置,或是执行对结点的其他操作。遍历二叉树的过程实质是把二叉树的结点进行线性排列的过程。

+

二叉树的遍历方法:

+
    +
  • 前序遍历:先访问根结点,然后前序遍历左子树,在前序遍历右子树;
  • +
  • 中序遍历:中序遍历根结点的左子树,然后访问根结点,最后遍历右子树;
  • +
  • 后序遍历:后序遍历左子树,然后后序遍历右子树,最后访问根结点;
  • +
+
public class BinaryTreeDemo {
+
+    public static void main(String[] args) {
+        BinaryTreeObj root = new BinaryTreeObj(1,"zs");
+        BinaryTreeObj binaryTreeObj2 = new BinaryTreeObj(2,"ls");
+        BinaryTreeObj binaryTreeObj3 = new BinaryTreeObj(3,"ww");
+        BinaryTreeObj binaryTreeObj4 = new BinaryTreeObj(4,"zq");
+        BinaryTreeObj binaryTreeObj5 = new BinaryTreeObj(5,"111");
+
+        root.setLeft(binaryTreeObj2);
+        root.setRight(binaryTreeObj3);
+        binaryTreeObj3.setLeft(binaryTreeObj4);
+        binaryTreeObj3.setRight(binaryTreeObj5);
+
+        BinaryTree binaryTree = new BinaryTree();
+        binaryTree.setRoot(root);
+        binaryTree.preOrderShow();
+
+        BinaryTreeObj binaryTreeObj = binaryTree.preOrderSearch(11);
+        if (binaryTreeObj == null) {
+            System.out.println("没有找到该结点~");
+            return;
+        }
+        System.out.printf("找到当前结点:id: %d, name: %s",binaryTreeObj.getId(),binaryTreeObj.getName());
+    }
+}
+
+
+class BinaryTree {
+
+    private BinaryTreeObj root;
+
+    public void setRoot(BinaryTreeObj root) {
+        this.root = root;
+    }
+
+    public void preOrderShow() {
+        if (this.root != null) {
+            this.root.preOrder();
+        }
+    }
+
+
+    public void postOrderShow() {
+        if (this.root != null) {
+            this.root.postOrder();
+        }
+    }
+
+
+    public void midOrderShow() {
+        if (this.root != null) {
+            this.root.midOrder();
+        }
+    }
+
+    public BinaryTreeObj preOrderSearch(int id){
+        if (this.root != null) {
+            return this.root.preOrderSearch(id);
+        }
+        return null;
+    }
+
+    public BinaryTreeObj infixOrderSearch(int id){
+        if (this.root != null) {
+            return this.root.infixOrderSearch(id);
+        }
+        return null;
+    }
+
+    public BinaryTreeObj postOrderSearch(int id){
+        if (this.root != null) {
+            return this.root.postOrderSearch(id);
+        }
+        return null;
+    }
+
+}
+
+
+class BinaryTreeObj {
+
+    private Integer id;
+    private String name;
+
+    public BinaryTreeObj(Integer id,String name){
+        this.id = id;
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public Integer getId() {
+        return id;
+    }
+
+    private BinaryTreeObj left;
+    private BinaryTreeObj right;
+
+    public void setLeft(BinaryTreeObj left) {
+        this.left = left;
+    }
+
+    public void setRight(BinaryTreeObj right) {
+        this.right = right;
+    }
+
+    // 前序遍历:根结点 =》 左结点 =》 右结点
+    public void preOrder(){
+        System.out.println(this);
+        if (this.left != null){
+            this.left.preOrder();
+        }
+
+        if (this.right != null){
+            this.right.preOrder();
+        }
+    }
+
+    // 后序遍历: 左结点 =》 右结点 =》 根结点
+    public void postOrder(){
+
+        if (this.left != null){
+            this.left.postOrder();
+        }
+
+        if (this.right != null){
+            this.right.postOrder();
+        }
+        System.out.println(this);
+    }
+
+    // 中序遍历: 左结点 =》 根结点 =》 右结点
+    public void midOrder(){
+
+        if (this.left != null){
+            this.left.midOrder();
+        }
+        System.out.println(this);
+
+        if (this.right != null){
+            this.right.midOrder();
+        }
+    }
+
+    // 前序查找
+    public BinaryTreeObj preOrderSearch(int id) {
+        if (id == this.id){
+            return this;
+        }
+
+        BinaryTreeObj binaryTreeObj = null;
+        if (this.left != null){
+            binaryTreeObj = this.left.preOrderSearch(id);
+        }
+
+        if (binaryTreeObj != null){
+            return binaryTreeObj;
+        }
+
+        if (this.right != null){
+            binaryTreeObj = this.right.preOrderSearch(id);
+        }
+        return binaryTreeObj;
+    }
+
+    // 中序查找
+    public BinaryTreeObj infixOrderSearch(int id) {
+
+        BinaryTreeObj binaryTreeObj = null;
+        if (this.left != null){
+            binaryTreeObj = this.left.infixOrderSearch(id);
+        }
+
+        if (id == this.id){
+            return this;
+        }
+
+        if (binaryTreeObj != null){
+            return binaryTreeObj;
+        }
+
+        if (this.right != null){
+            binaryTreeObj = this.right.infixOrderSearch(id);
+        }
+        return binaryTreeObj;
+    }
+
+    // 后序查找
+    public BinaryTreeObj postOrderSearch(int id) {
+
+        BinaryTreeObj binaryTreeObj = null;
+        if (this.left != null){
+            binaryTreeObj = this.left.postOrderSearch(id);
+        }
+
+        if (binaryTreeObj != null){
+            return binaryTreeObj;
+        }
+
+        if (this.right != null){
+            binaryTreeObj = this.right.postOrderSearch(id);
+        }
+        if (id == this.id){
+            return this;
+        }
+        return binaryTreeObj;
+    }
+
+    @Override
+    public String toString() {
+        return "BinaryTreeObj{" +
+                "id=" + id +
+                ", name='" + name + '\'' +
+                '}';
+    }
+}
+

二叉树的删除

+

数据结构与算法-023

+
class BinaryTree {
+    public void del(int id){
+        if (this.root == null){
+            return;
+        }
+        if (this.root.getId() == id){
+            this.root = null;
+            return;
+        }
+        this.root.delNo(id);
+    }
+}
+
class BinaryTreeObj {
+    // 根据ID删除结点
+    public void delNo(int id){
+        // 找到当前结点的左子树结点是否为指定结点,如果是则将其置空
+        if (this.left != null && this.left.id == id){
+            this.left = null;
+            return;
+        }
+
+        // 与上面同理删除右子树结点
+        if (this.right != null && this.right.id == id){
+            this.right = null;
+            return;
+        }
+
+
+        if (this.left == null && this.right == null){
+            return;
+        }
+
+        // 如果当前左结点或右结点 不是要删除的结点 则进行递归删除
+        if (this.left != null){
+            this.left.delNo(id);
+        }
+
+        if (this.right != null) {
+            this.right.delNo(id);
+        }
+    }
+}
+

顺序存储二叉树

+

二叉树的顺序存储,指的是使用顺序表(数组)存储二叉树。

+

需要注意的是,顺序存储只适用于完全二叉树。换句话说,只有完全二叉树才可以使用顺序表存储。因此,如果我们想顺序存储普通二叉树,需要提前将普通二叉树转化为完全二叉树。

+
+

顺序存储二叉树应用实例:八大排序算法中的堆排序,就会使用到顺序存储二叉树。

+
+
public class ArrBinaryTreeDemo {
+    public static void main(String[] args) {
+        int[] arr = {1, 2, 3, 4, 5, 6, 7};
+        ArrBinaryTree arrBinaryTree = new ArrBinaryTree();
+        arrBinaryTree.setArrayTree(arr);
+        arrBinaryTree.preArrBinaryTree(0);
+    }
+}
+
+class ArrBinaryTree {
+
+    private int[] arr = null;
+
+    public void setArrayTree(int[] arr) {
+        this.arr = arr;
+    }
+
+    public void preArrBinaryTree(int index) {
+        if (arr == null || arr.length == 0) {
+            return;
+        }
+        System.out.println(arr[index]);
+
+        int nextLeftIndex = (index << 1) + 1;
+        int nextRightIndex = (index << 1) + 2;
+
+        if (nextLeftIndex < arr.length) {
+            preArrBinaryTree(nextLeftIndex);
+        }
+
+        if (nextRightIndex < arr.length) {
+            preArrBinaryTree(nextRightIndex);
+        }
+    }
+
+}
+

线索化二叉树

+

n 个结点的二叉链表中含有 n+1【公式 2n-(n-1)=n+1】个空指针域。利用二叉链表中的空指针域,存放指向该结点在 某种遍历次序 下的前驱后继结点的指针,这种附加的指针称为"线索"。这种加上了线索的二叉链表称为线索链表,相应的二叉树称为线索二叉树。

+

根据线索性质的不同,线索二叉树可分为前序线索二叉树、中序线索二叉树和后序线索二叉树三种。

+

如图,按照前序遍历可以得到数组 {1, 2, 4, 5, 3, 6} 其中

+
    +
  • 【2】的前驱结点为【1】,后继结点为【4】
  • +
  • 【6】的前驱结点为【3】,后继结点为 null
  • +
+

数据结构与算法-024

+

中序线索化二叉树示意图

+

数据结构与算法-025

+
public class ThreadedBinaryTreeDemo {
+
+    public static void main(String[] args) {
+        BinaryTreeObj root = new BinaryTreeObj(1,"zs");
+        BinaryTreeObj binaryTreeObj2 = new BinaryTreeObj(2,"ls");
+        BinaryTreeObj binaryTreeObj3 = new BinaryTreeObj(3,"ww");
+        BinaryTreeObj binaryTreeObj4 = new BinaryTreeObj(4,"zq");
+        BinaryTreeObj binaryTreeObj5 = new BinaryTreeObj(5,"111");
+
+        root.setLeft(binaryTreeObj2);
+        root.setRight(binaryTreeObj3);
+        binaryTreeObj3.setLeft(binaryTreeObj4);
+        binaryTreeObj3.setRight(binaryTreeObj5);
+
+        // 中序线索化二叉树
+        ThreadedBinaryTree threadedBinaryTree = new ThreadedBinaryTree();
+        threadedBinaryTree.infixThreadedBinaryTree(root);
+
+        // 遍历中序线索化二叉树
+        threadedBinaryTree.forEachInfixThreadedBinaryTree(root); // 2,1,4,3,5
+    }
+}
+
+
+class ThreadedBinaryTree{
+
+    private BinaryTreeObj pre;
+
+    // 中序线索化二叉树
+    public void infixThreadedBinaryTree(BinaryTreeObj node){
+        if (node == null){
+            return;
+        }
+        // 左递归线索化二叉树
+        infixThreadedBinaryTree(node.getLeft());
+
+        // 线索化核心代码 将左结点线索化
+        if (node.getLeft() == null){
+            node.setLeft(pre);
+            node.setLeftType(1);
+        }
+        // 将右结点线索化
+        if (pre != null && pre.getRight() == null) {
+            pre.setRight(node);
+            pre.setRightType(1);
+        }
+        // 使辅助结点指针指向当前结点
+        pre = node;
+
+        // 右递归线索化二叉树
+        infixThreadedBinaryTree(node.getRight());
+    }
+
+    // 遍历中序线索二叉树
+    public void forEachInfixThreadedBinaryTree(BinaryTreeObj root) {
+        // 用辅助结点保存根结点
+        BinaryTreeObj node = root;
+
+        while (node != null){
+
+            // 向左子树遍历,直到找到 leftType=1 的结点,等于1代表该结点为前驱结点
+            while (node.getLeftType() == 0){
+                node = node.getLeft();
+            }
+            System.out.println(node);
+
+            // 向右子树遍历,直到找到 rightType=0 的结点, 等于0代表该结点为右子树
+            while (node.getRightType() == 1){
+                node = node.getRight();
+                System.out.println(node);
+            }
+
+            // node 结点向右边找
+            node = node.getRight();
+        }
+    }
+}
+
+class BinaryTreeObj {
+
+    private Integer id;
+    private String name;
+
+    private BinaryTreeObj left;
+    private BinaryTreeObj right;
+
+    // 如果leftType == 0 表示指向的是左子树, 如果 1 则表示指向前驱结点
+    // 如果rightType == 0 表示指向是右子树, 如果 1 表示指向后继结点
+    private int leftType;
+    private int rightType;
+
+    public BinaryTreeObj(Integer id,String name){
+        this.id = id;
+        this.name = name;
+    }
+    public String getName() {
+        return name;
+    }
+    public Integer getId() {
+        return id;
+    }
+    public BinaryTreeObj getLeft() {
+        return left;
+    }
+    public BinaryTreeObj getRight() {
+        return right;
+    }
+    public void setLeft(BinaryTreeObj left) {
+        this.left = left;
+    }
+    public void setRight(BinaryTreeObj right) {
+        this.right = right;
+    }
+    public void setLeftType(int leftType) {
+        this.leftType = leftType;
+    }
+    public void setRightType(int rightType) {
+        this.rightType = rightType;
+    }
+    public int getLeftType() {
+        return leftType;
+    }
+    public int getRightType() {
+        return rightType;
+    }
+}
+

哈夫曼树

+

哈夫曼树重要概念:

+
    +
  • 路径和路径长度:在一棵树中,从一个结点往下可以达到的孩子或孙子结点之间的通路,称为路径。通路中分支的数目称为路径长度。若规定根结点的层数为 1,则从根结点到第 L 层结点的路径长度为 L-1;
  • +
  • 结点的权及带权路径长度:若将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权。结点的带权路径长度为:从根结点到该结点之间的路径长度与该结点的权的乘积;
  • +
  • 树的带权路径长度:树的带权路径长度规定为所有叶子结点的带权路径长度之和,记为WPL(weighted path length) ,权值越大的结点离根结点越近的二叉树才是最优二叉树。WPL 最小的就是赫夫曼树;
  • +
+

给定n个权值作为n个叶子结点,构造一棵二叉树,若该树的带权路径长度(wpl)达到最小,称这样的二叉树为最优二叉树,称为哈夫曼树,有些资料也译为赫夫曼树。

+

数据结构与算法-028

+

WPL最小的就是哈夫曼树,如上图,中间的树就是哈夫曼树。

+
public class HuffmanTreeDemo {
+    public static void main(String[] args) {
+        HuffmanTree huffmanTree = new HuffmanTree();
+        HuffmanTreeNode root = huffmanTree.buildHuffmanTree(new int[]{13, 7, 8, 3, 29, 6, 1});
+        System.out.println("前序遍历huffman树:");
+        huffmanTree.preOrder(root);
+    }
+}
+
+class HuffmanTree{
+
+    public void preOrder(HuffmanTreeNode root){
+        if (root != null){
+            root.preOrder();
+        }
+    }
+
+    public HuffmanTreeNode buildHuffmanTree(int[] arr){
+
+        List<HuffmanTreeNode> list = new ArrayList<>();
+        for (int value : arr) {
+            list.add(new HuffmanTreeNode(value));
+        }
+
+        // 如果集合中的元素大于1则继续循环
+        while (list.size() > 1){
+            // 从大到小进行排序
+            Collections.sort(list);
+
+            // 获取集合中两个较小的元素进行构建 huffman 树
+            HuffmanTreeNode leftNode = list.get(0);
+            HuffmanTreeNode rightNode = list.get(1);
+            HuffmanTreeNode parentNode = new HuffmanTreeNode(leftNode.value + rightNode.value);
+            parentNode.left = leftNode;
+            parentNode.right = rightNode;
+
+            // 构建后将 leftNode, rightNode 移除集合;将parentNode加入集合;然后重新排序
+            list.remove(leftNode);
+            list.remove(rightNode);
+            list.add(parentNode);
+        }
+        return list.get(0);
+    }
+
+}
+
+
+class HuffmanTreeNode implements Comparable<HuffmanTreeNode>{
+    int value;
+    HuffmanTreeNode left;
+    HuffmanTreeNode right;
+
+    public HuffmanTreeNode(int value){
+        this.value = value;
+    }
+
+    public void preOrder(){
+        System.out.println(this);
+        if (this.left != null){
+            this.left.preOrder();
+        }
+        if (this.right != null){
+            this.right.preOrder();
+        }
+    }
+
+    @Override
+    public int compareTo(HuffmanTreeNode o) {
+        // 从小到大排序
+        return this.value - o.value;
+    }
+
+    @Override
+    public String toString() {
+        return "HuffmanTreeNode{" +
+                "value=" + value +
+                '}';
+    }
+}
+

二叉排序树

+

二叉排序树(Binary Sort Tree),又称二叉查找树(Binary Search Tree),亦称二叉搜索树。是数据结构中的一类。在一般情况下,查询效率比链表结构要高。

+

对于二叉排序树的任何一个非叶子节点,要求左子节点的值比当前节点的值小,右子节点的值比当前节点的值大。如果有相同的值,可以将该节点放在左子节点或右子节点,二叉排序树的中序遍历为有序数列。

+

数据结构与算法-030

+
class BinarySortTree {
+
+    private Node root;
+
+    public void setRoot(Node root) {
+        this.root = root;
+    }
+
+    // 添加结点
+    public void add(Node node) {
+        if (this.root == null) {
+            this.root = node;
+        } else {
+            this.root.add(node);
+        }
+    }
+
+    // 中序遍历
+    public void infixOrder() {
+        if (this.root != null) {
+            this.root.infixOrder();
+        }
+    }
+
+    // 查找结点
+    public Node search(int target) {
+        if (this.root == null) {
+            return null;
+        }
+        return this.root.search(target);
+    }
+
+    // 查找当前结点的父结点
+    public Node searchParentNode(int target) {
+        if (this.root == null) {
+            return null;
+        }
+        return this.root.searchParentNode(target);
+    }
+
+    // 删除结点
+    public void delNode(int target) {
+        if (this.root == null) {
+            return;
+        }
+        // 如果删除的结点是根结点
+        if (this.root.left == null && this.root.right == null) {
+            this.root = null;
+            return;
+        }
+        Node targetNode = search(target);
+        if (targetNode == null) {
+            return;
+        }
+        // 获取当前结点的父结点
+        Node parentNode = searchParentNode(target);
+        // 删除的结点是叶子结点
+        if (targetNode.left == null && targetNode.right == null) {
+            // 判断是左结点还是右结点
+            if (parentNode.left != null && parentNode.left.val == target) {
+                parentNode.left = null;
+            } else {
+                parentNode.right = null;
+            }
+            return;
+        }
+
+        // 删除的结点有两个结点
+        if (targetNode.left != null && targetNode.right != null) {
+            // 从右子树找到最小的值并删除,将该值赋值给targetNode
+            targetNode.val = delRightTreeMin(targetNode.right);
+            return;
+        }
+
+        // 删除只有一颗子树的结点
+        if (targetNode.left != null) {
+            if(parentNode == null){
+                root = targetNode.left;
+                return;
+            }
+            // 当前结点存在左子树
+            if (parentNode.left.val == target) {
+                parentNode.left = targetNode.left;
+            } else {
+                parentNode.right = targetNode.left;
+            }
+        }
+        if (targetNode.right != null) {
+            if(parentNode == null){
+                root = targetNode.right;
+                return;
+            }
+            // 当前结点存在右子树
+            if (parentNode.right.val == target) {
+                parentNode.left = targetNode.right;
+            } else {
+                parentNode.right = targetNode.right;
+            }
+        }
+    }
+
+    private int delRightTreeMin(Node node) {
+        Node target = node;
+        // 循环的查找左子节点,就会找到最小值
+        while (target.left != null) {
+            target = target.left;
+        }
+        // 此时 target就指向了最小结点 删除最小结点(该节点肯定是左叶子节点)
+        delNode(target.val);
+        return target.val;
+    }
+
+}
+
+class Node {
+    int val;
+    Node left;
+    Node right;
+
+    public Node(int val) {
+        this.val = val;
+    }
+
+    // 查找结点
+    public Node search(int target) {
+        if (this.val == target) {
+            return this;
+        } else if (this.val > target) {
+            if (this.left == null) {
+                return null;
+            }
+            return this.left.search(target);
+        } else {
+            if (this.right == null) {
+                return null;
+            }
+            return this.right.search(target);
+        }
+    }
+
+    // 查找当前结点的父结点
+    public Node searchParentNode(int target) {
+        if ((this.left != null && this.left.val == target) || (this.right != null && this.right.val == target)) {
+            return this;
+        } else if (this.left != null && this.val > target) {
+            return this.left.searchParentNode(target);
+        } else if (this.right != null && this.val <= target) {
+            return this.right.searchParentNode(target);
+        } else {
+            return null;
+        }
+    }
+
+    // 添加结点
+    public void add(Node node) {
+        if (node == null) {
+            return;
+        }
+        // 如果当前待插入结点的值小于当前结点,将其插入在左子树中
+        if (node.val < this.val) {
+            if (this.left == null) {
+                this.left = node;
+            } else {
+                this.left.add(node);
+            }
+        } else {
+            // 将当前结点插入右子树
+            if (this.right == null) {
+                this.right = node;
+            } else {
+                this.right.add(node);
+            }
+        }
+    }
+
+    // 中序遍历
+    public void infixOrder() {
+        if (this.left != null) {
+            this.left.infixOrder();
+        }
+        System.out.println(this);
+        if (this.right != null) {
+            this.right.infixOrder();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "Node{" +
+                "val=" + val +
+                '}';
+    }
+}
+

平衡二叉树

+

二叉搜索树一定程度上可以提高搜索效率,但是当序列构造二叉搜索树,可能会将二叉树退化成单链表,从而降低搜索效率。

+

平衡二叉树也叫平衡二叉搜索树(Self-balancing binary search tree)又被称为AVL树,可以保证查询效率较高。

+

平衡二叉树的特点:

+
    +
  • 平衡二叉树一定是二叉排序;
  • +
  • 是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树;
  • +
+

数据结构与算法-031

+

将有序二叉树变为平衡二叉树代码

+
public class AVLTreeDemo {
+    public static void main(String[] args) {
+        AVLTree avlTree = new AVLTree();
+        int[] arr = {10, 11, 7, 6, 8, 9 };
+        for (int i = 0; i < arr.length; i++) {
+            Node node = new Node(arr[i]);
+            avlTree.add(node);
+        }
+        System.out.println("当前树高度:"+ avlTree.height());
+        System.out.println("当前根结点:" + avlTree.getRoot());
+        System.out.println("当前左子树高度:"+ avlTree.getRoot().leftHeight());
+        System.out.println("当前右子树高度:"+ avlTree.getRoot().rightHeight());
+    }
+}
+
+class AVLTree{
+    private Node root;
+
+    public void setRoot(Node root) {
+        this.root = root;
+    }
+
+    public Node getRoot() {
+        return root;
+    }
+
+    public int height(){
+        if (root == null){
+            return 0;
+        }
+        return this.root.height();
+    }
+
+    // 添加结点
+    public void add(Node node) {
+        if (this.root == null) {
+            this.root = node;
+        } else {
+            this.root.add(node);
+        }
+    }
+
+    // 中序遍历
+    public void infixOrder() {
+        if (this.root != null) {
+            this.root.infixOrder();
+        }
+    }
+
+}
+
+class Node {
+
+    int value;
+    Node left;
+    Node right;
+
+    public Node(int value) {
+        this.value = value;
+    }
+    
+    // 获取当前左子树的高度
+    public int leftHeight(){
+        if (this.left == null){
+            return 0;
+        }
+        return this.left.height();
+    }
+    
+    // 获取当前结点右子树的高度
+    public int rightHeight(){
+        if (this.right == null){
+            return 0;
+        }
+        return this.right.height();
+    }
+
+    // 获取当前结点的高度
+    public int height() {
+        return Math.max(
+                (this.left == null ? 0 : this.left.height()),
+                (this.right == null ? 0 : this.right.height())
+        ) + 1;
+    }
+
+    // 左旋转
+    public void leftRote(){
+        // 创建一个新结点,并设置值等于当前结点的值
+        Node newNode = new Node(value);
+        // 使新结点的左结点指向当前结点的左结点
+        newNode.left = left;
+        // 新结点的右结点指向当前结点的右结点的左结点
+        newNode.right = right.left;
+        // 使当前结点的值指向新结点
+        value = right.value;
+        // 使当前结点的右结点指向当前结点的右结点的右结点
+        right = right.right;
+        // 使当前结点的左结点指向新结点
+        left = newNode;
+    }
+
+    // 右旋转
+    public void rightRote(){
+        Node newNode = new Node(value);
+        newNode.right = right;
+        newNode.left = left.right;
+        value = left.value;
+        left = left.left;
+        right = newNode;
+    }
+
+    // 添加结点
+    public void add(Node node) {
+        if (node == null) {
+            return;
+        }
+        // 如果当前待插入结点的值小于当前结点,将其插入在左子树中
+        if (node.value < this.value) {
+            if (this.left == null) {
+                this.left = node;
+            } else {
+                this.left.add(node);
+            }
+        } else {
+            // 将当前结点插入右子树
+            if (this.right == null) {
+                this.right = node;
+            } else {
+                this.right.add(node);
+            }
+        }
+        // 如果左子树的高度-右子树的高度 > 1 进行右旋转 反之进行左旋转
+        if (this.leftHeight() - this.rightHeight() > 1){
+            // 如果当前结点的左子树的右子树的高度>当前结点左子树的左子树的高度 则进行左旋转
+            if (this.left != null && this.left.rightHeight() > this.left.leftHeight()){
+                // 对当前结点的左结点进行左旋转
+                this.left.leftRote();
+                // 对当前结点右旋转
+                this.rightRote();
+            }else {
+                this.rightRote();
+            }
+            return;
+        }
+        if (this.rightHeight() - this.leftHeight() > 1) {
+            if (this.right != null && this.right.leftHeight() > this.right.rightHeight()){
+                // 对当前结点的右结点进行右旋转
+                this.right.rightRote();
+                // 对当前结点进行左旋转
+                this.leftRote();
+            }else {
+                this.leftRote();
+            }
+        }
+    }
+
+    // 中序遍历
+    public void infixOrder() {
+        if (this.left != null) {
+            this.left.infixOrder();
+        }
+        System.out.println(this);
+        if (this.right != null) {
+            this.right.infixOrder();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "Node{" +
+                "value=" + value +
+                '}';
+    }
+}
+

多路查找树

+

在二叉树中,每个节点有数据项,最多有两个子节点。如果允许每个节点可以有更多的数据项和更多的子节点,就是多叉树(multiway tree)。多叉树通过重新组织节点,减少树的高度,能对二叉树进行优化。典型的多叉树有:2-3树、2-3-4树、红黑树和B树。

+

多叉树的前提是有序二叉树。

+

2-3树

+

2-3树是由二节点和三节点构成的树,是最简单的B树结构,2-3树的所有叶子节点都在同一层(只要是B树都满足这个条件)。

+
    +
  • 有两个子节点的节点叫二节点,二节点要么没有子节点,要么有两个子节点;
  • +
  • 有三个子节点的节点叫三节点,三节点要么没有子节点,要么有三个子节点;
  • +
+

数据结构与算法-032

+

2-3-4树,与2-3树类似。

+

数据结构与算法-033

+

B树

+

B-tree 树即 B 树,B 即 Balanced ,平衡的意思。 B树通过重新组织节点,降低树的高度,并且减少i/o读写次数来提升效率。

+

文件系统及数据库系统的设计者利用了磁盘预读原理,将一个节点的大小设为等于一个页(页的大小通常为4k),这样每个节点只需要一次I/O就可以完全载入。

+
+

B树的阶(度):节点的最多子节点个数。比如2-3树的阶是3,2-3-4树的阶是4。

+
+

B树的搜索,从根结点开始,对结点内的关键字(有序)序列进行二分查找,如果命中则结束,否则进入查询关键字所属范围的儿子结点;重复,直到所对应的儿子指针为空,或已经是叶子结点。

+

B树的关键字集合分布在整颗树中,即叶子节点和非叶子节点都存放数据,搜索有可能在非叶子结点结束,其搜索性能等价于在关键字全集内做一次二分查找。

+

数据结构与算法-034

+

B+树

+

B+树是B树的变体,也是一种多路搜索树。

+

B+树的搜索与B树也基本相同,区别是B+树只有达到叶子结点才命中(B树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找。

+

B+树所有关键字都出现在叶子结点的链表中(即数据只能在叶子节点【也叫稠密索引】),且链表中的关键字(数据)恰好是有序的。所以不可能在非叶子结点命中。

+

B+树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录, B+树更适合文件索引系统,B树和B+树各有自己的应用场景,不能说B+树完全比B树好,反之亦然。

+

数据结构与算法-035

+

B*树

+

B* 树是B+树的变体,在B+树的非根和非叶子结点再增加指向兄弟的指针。

+

数据结构与算法-036

+

+

图是一种数据结构,其中结点可以具有零个或多个相邻元素。两个结点之间的连接称为边。结点也可以称为顶点。

+

如果给图的每条边规定一个方向,那么得到的图称为有向图。在有向图中,与一个节点相关联的边有出边和入边之分。相反,边没有方向的图称为无向图。

+

数据结构与算法-037

+

可用二维数组表示图(邻接矩阵);或链表表示(邻接表)。

+

数据结构与算法-038

+

数据结构与算法-039

+

用java模拟图,包括图的深度遍历,广度遍历。

+

数据结构与算法-040

+

数据结构与算法-041

+
public class GraphDemo {
+  public static void main(String[] args) {
+    String[] vertexes = {"A", "B", "C", "D", "E"};
+    int n = vertexes.length;
+    Graph graph = new Graph(n);
+    for (String vertex : vertexes) {
+      graph.addVertex(vertex);
+    }
+    graph.addEdge(0, 1, 1); // A-B
+    graph.addEdge(0, 2, 1); // A-C
+    graph.addEdge(1, 2, 1); // B-C
+    graph.addEdge(1, 3, 1); // B-D
+    graph.addEdge(1, 4, 1); // B-E
+    // 展示图转换的矩阵
+    graph.showEdges();
+    // 图的深度遍历
+    graph.dfs();
+    System.out.println();
+    // 图的广度遍历
+    graph.bfs();
+  }
+}
+
+class Graph {
+
+  // 保存顶点
+  private List<String> vertexList;
+
+  // 保存边的数量
+  private int sideNums;
+
+  // 保存图的矩阵
+  private int[][] edges;
+
+  private boolean[] isVisited;
+
+  public Graph(int n) {
+    vertexList = new ArrayList<>(n);
+    edges = new int[n][n];
+    sideNums = 0;
+  }
+
+  // 获取第一个结点的下一个结点
+  private int getFirstNeighbor(int index) {
+    for (int i = 0; i < vertexList.size(); i++) {
+      if (edges[index][i] > 1) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  // 获取当前结点的下一个结点
+  private int getNextNeighbor(int vertex, int index) {
+    for (int i = vertex + 1; i < vertexList.size(); i++) {
+      if (edges[index][i] > 1) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  // 图的深度遍历
+  private void dfs(boolean[] isVisited, int i) {
+    System.out.print(getVertexValByIndex(i) + "->");
+    // 将当前遍历后的顶点标记为true
+    isVisited[i] = true;
+    // 获取当前结点的下一个结点的索引位置
+    int firstNeighborIndex = getFirstNeighbor(i);
+    // 如果 != -1 代表当前结点没有找到下一个结点,需要向下移动
+    while (firstNeighborIndex != -1) {
+      // 判断该结点是否被遍历过
+      if (!isVisited[firstNeighborIndex]) {
+        dfs(isVisited, firstNeighborIndex);
+      }
+      // 当前结点向后移动,否则是死循环
+      firstNeighborIndex = getNextNeighbor(firstNeighborIndex, i);
+    }
+  }
+
+  public void dfs() {
+    isVisited = new boolean[vertexList.size()];
+    for (int i = 0; i < getVertexCount(); i++) {
+      if (!isVisited[i]) {
+        dfs(isVisited, i);
+      }
+    }
+  }
+
+  // 一个结点的广度优先遍历
+  private void bfs(boolean[] isVisited, int i) {
+    // 队列头结点下标索引
+    int headIndex;
+    // 相邻结点下标索引
+    int neighborIndex;
+    LinkedList<Integer> queue = new LinkedList<>();
+
+    System.out.print(getVertexValByIndex(i) + "->");
+    isVisited[i] = true;
+    queue.addLast(i);
+
+    // 如果队列不等于空 则需要遍历循环查找
+    while (!queue.isEmpty()) {
+      headIndex = queue.removeFirst();
+      // 得到第一个邻接结点的下标
+      neighborIndex = getFirstNeighbor(headIndex);
+      while (neighborIndex != -1) {
+        // 是否访问过
+        if (!isVisited[neighborIndex]) {
+          System.out.print(getVertexValByIndex(neighborIndex) + "->");
+          isVisited[neighborIndex] = true;
+          queue.addLast(neighborIndex);
+        }
+        // neighborIndex 向下找
+        neighborIndex = getNextNeighbor(headIndex, neighborIndex);
+      }
+    }
+  }
+
+  // 广度优先遍历
+  public void bfs() {
+    isVisited = new boolean[vertexList.size()];
+    for (int i = 0; i < getVertexCount(); i++) {
+      if (!isVisited[i]) {
+        bfs(isVisited, i);
+      }
+    }
+  }
+
+  // 添加顶点
+  public void addVertex(String vertex) {
+    vertexList.add(vertex);
+  }
+
+  // 添加边
+  public void addEdge(int vertex1, int vertex2, int weight) {
+    edges[vertex1][vertex2] = weight;
+    edges[vertex2][vertex1] = weight;
+    sideNums++;
+  }
+
+  // 获取边的数量
+  public int getSideNums() {
+    return sideNums;
+  }
+
+  // 遍历矩阵
+  public void showEdges() {
+    for (int[] edge : edges) {
+      System.out.println(Arrays.toString(edge));
+    }
+  }
+
+  // 获取顶点数量
+  public int getVertexCount() {
+    return vertexList.size();
+  }
+
+  // 获取边之间的权值
+  public int getVertexWeight(int vertex1, int vertex2) {
+    return edges[vertex1][vertex2];
+  }
+
+  // 根据下标获取结点的值
+  public String getVertexValByIndex(int index) {
+    return vertexList.get(index);
+  }
+
+}
+

算法

+

英文对应的单词是Algorithm,它的本意为:解决问题的方法,所以算法的直接理解就是解决问题的方法。在计算机领域定义的话就是:一系列解决问题的、清晰、可执行的计算机指令。

+

一个算法的优劣可以用空间复杂度与时间复杂度来衡量。

+

算法复杂度

+

时间复杂度

+

度量一个算法执行时间的两种方法:

+
    +
  • +

    事后统计法:即直接运行程序,统计需要的时间和空间。但是,这种方法有两个问题:

    +
      +
    1. 结果非常依赖于测试环境。比如,用i3和用i8运行程序所需的时间是不同的;
    2. +
    3. 结果受测试规模的影响特别大。比如,对有序数组进行排序的时间比对逆序数组排序的时间短;对于小规模数据而言,插入排序所需时间比快速排序要短;
    4. +
    +

    所以,就需要有一种不用具体测试数据,也能估计算法执行效率的方法,就是算法复杂度分析,包括时间、空间复杂度分析。

    +
  • +
  • +

    事前估算法:通过分析某个算法的时间复杂度来判断那个算法更优;

    +
  • +
+

一般情况下,算法中的基本操作语句的重复执行次数是问题规模 n 的某个函数, 用T(n)表示,若有某个辅助函数f(n),使得当 n 趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数,则称f(n)T(n)的同数量级函数。记作 T(n)=O(f(n)), 称O(f(n))为算法的渐进时间复杂度,简称时间复杂度。

+

例如,T(n) = n + 1 与 T(n) = n 就是同数量级函数,因为 n+1/n 的极限值为不等于零的常数。 +T(n) 不同,但时间复杂度可能相同: T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的 T(n) 不同, 但时间复杂度相同,都为 O(n²)。

+

计算时间复杂度的方法:

+
    +
  • 用常数 1 代替运行时间中的所有加法常数: T(n)=n²+7n+6 => T(n)=n²+7n+1
  • +
  • 修改后的运行次数函数中,只保留最高阶项: T(n)=n²+7n+1 => T(n) = n²
  • +
  • 去除最高阶项的系数:T(n) = n² => T(n) = n² => O(n²)
  • +
+
+

时间频度

+

一个算法花费的时间与算法中的语句的执行次数成正比例,哪个算法执行次数多,他花费的时间就多.一个算法中的语句执行次数称为语句频度或时间频度.记为T(n)。

+

随着时间的推移,一些复杂度花费时间无限接近:

+
    +
  1. 忽略常数项:
  2. +
+
    +
  • 2n+20 和 2n 随着 n 变大,执行曲线无限接近,20 可以忽略
  • +
  • 3n+10 和 3n 随着 n 变大,执行曲线无限接近,10 可以忽略
  • +
+
    +
  1. 忽略低次项:
  2. +
+
    +
  • 2n²+3n+10 和 2n² 随着 n 变大, 执行曲线无限接近,可以忽略 3n+10
  • +
  • n²+5n+20 和 n² 随着 n 变大,执行曲线无限接近,可以忽略 5n+20
  • +
+
    +
  1. 忽略系数:
  2. +
+
    +
  • 随着 n 值变大, 5n²+7n 和 3n² + 2n ,执行曲线重合, 说明这种情况下, 5 和 3 可以忽略
  • +
  • 对于 n^3+5n 和 6n^3+4n,执行曲线分离,不能忽略系数,说明多少次方是关键
  • +
+
+

常见的时间复杂度

+
    +
  • +

    常数阶 O(1): 无论代码执行了多少行,只要是没有循环等复杂结构,这个代码的时间复杂度就都是O(1);

    +
    int i = 1;
    +int j = 2;
    +++i;
    +j++;
    +int m = i + j;
    +
  • +
  • +

    对数阶 O(log2n)

    +

    数据结构与算法-012

    +
    int i = 1;
    +while(i<n){
    +    i = i*2;
    +}
    +
  • +
  • +

    线性阶 O(n): 它消耗的时间是随着n的变化而变化的,与n成正比或反比;

    +
    for(int i=1; i<=n; ++i){
    +    j = i;
    +    j++;
    +}
    +
  • +
  • +

    线性对数阶 O(nlog2n): 将时间复杂度为O(logn)的代码循环n遍,那么它的时间复杂度就是 n * O(logN),也就是了O(nlogN);

    +
    for(int m=1; m<=n; ++m){
    +    int i = 1;
    +    while(i<n){
    +        i = i*2;
    +    }
    +}
    +
  • +
  • +

    平方阶 O(n^2): 如果把 O(n) 的代码再嵌套循环一遍,它的时间复杂度就是 O(n²),这段代码其实就是嵌套了2层n循环,它的时间复杂度就是 O(n*n),即 O(n²) 如果将其中一层循环的n改成m,那它的时间复杂度就变成了 O(m*n);

    +
    for(int i=1; i<=n; ++i){
    +    for(int j=1; j<=n; ++j){
    +        j=i;
    +    }
    +}
    +
  • +
  • +

    立方阶 O(n^3)

    +
    for(int i=1; i<=n; ++i){
    +    for(int j=1; j<=n; ++j){
    +        for(int x=1; x<=n; ++x){
    +            int m = 0;
    +            i = x+j;
    +        }
    +    }
    +}
    +
  • +
  • +

    k 次方阶 O(n^k)

    +
  • +
  • +

    指数阶 O(2^n)

    +
  • +
+

常见的算法时间复杂度由小到大依次为:

+
Ο (1)<Ο (log2n)<Ο (n)<Ο (nlog2n)<Ο (n2)<Ο (n3)< Ο (nk) < Ο (2n)
+

随着问题规模 n 的不断增大,上述时间复杂度不断增大,算法的执行效率越低。

+

空间复杂度

+

类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储空间,它也是问题规模 n 的函数。

+

空间复杂度全称为渐进空间复杂度,是对一个算法在运行过程中临时占用存储空间大小的量度。 有的算法需要占用的临时工作单元数与解决问题的规模 n 有关, 它随着 n 的增大而增大, 当 n 较大时, 将占用较多的存储单元, 例如快速排序和归并排序算法, 基数排序就属于这种情况 +在做算法分析时, 主要讨论的是时间复杂度。 从用户使用体验上看, 更看重的程序执行的速度。 一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间

+

空间复杂度较为简单,常见的空间复杂度为 O(1),O(n) 和 O(n ^ 2)。

+

递归

+

递归就是方法自己调用自己,每次调用时传入不同的变量,递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。

+

递归能解决什么问题?

+
    +
  • 各种数学问题如: 8 皇后问题,汉诺塔,阶乘问题,迷宫问题,球和篮子的问题(google 编程大赛);
  • +
  • 各种算法中也会使用到递归, 比如快排, 归并排序, 二分查找, 分治算法等;
  • +
  • 用栈解决的问题,使用递归替换代码更佳简洁;
  • +
+

使用递归遵守的规则:

+
    +
  • 执行一个方法时,就创建一个新的受保护的独立空间(一个线程有自己独立的一个栈空间,每个方法调用对应着一个栈帧);
  • +
  • 方法的局部变量是独立的,不会相互影响;
  • +
  • 如果方法中使用的是引用类型变量,就会共享该引用类型的数据,比如数组;
  • +
  • 递归必须向退出递归的条件逼近,否则就是无限递归,出现 StackOverflowError;
  • +
  • 当一个方法执行完毕,或者遇到return,就会返回,遵守谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕;
  • +
+

迷宫回溯

+

迷宫回溯问题,寻找最短路径可以通过改变策略,将每个策略都经过的路保存在集合中,最后看哪个集合最小即最小路径。

+
public class MyTest {
+
+    public static void main(String[] args) {
+        Mazeback mazeback = new Mazeback();
+
+        int[][] map = mazeback.createMap();
+        mazeback.list(map);
+
+        System.out.println("自动寻找路线:");
+
+        mazeback.setWay(map, 1, 1);
+        mazeback.list(map);
+    }
+
+}
+
+
+class Mazeback {
+
+    // 创建地图
+    public int[][] createMap() {
+        // 地图
+        int[][] map = new int[7][8];
+
+        for (int i = 0; i < 7; i++) {
+            map[i][0] = 1;
+            map[i][7] = 1;
+            map[6][i] = 1;
+            map[0][i] = 1;
+        }
+        map[3][1] = 1;
+        map[3][2] = 1;
+        return map;
+    }
+
+    // 遍历地图
+    public void list(int[][] map) {
+        for (int i = 0; i < map.length; i++) {
+            for (int j = 0; j < map[i].length; j++) {
+                System.out.print(map[i][j] + "");
+            }
+            System.out.println();
+        }
+    }
+
+    // 寻找路径
+    // 1:墙;2:通路,3:死路
+    public boolean setWay(int[][] map, int row, int column) {
+        if (map[5][6] == 2) {
+            return true;
+        }
+        if (map[row][column] == 0) {
+
+            // 先假设是通路
+            map[row][column] = 2;
+
+            // 寻找路径顺序:下,右,上,左
+            if (setWay(map, row + 1, column)) {
+                return true;
+            } else if (setWay(map, row, column + 1)) {
+                return true;
+            } else if (setWay(map, row - 1, column)) {
+                return true;
+            } else if (setWay(map, row, column - 1)) {
+                return true;
+            } else {
+                // 当标记为3时,说明是死路走不通
+                map[row][column] = 3;
+                return false;
+            }
+        }
+        return false;
+    }
+
+}
+

八皇后问题

+

八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848 年提出:在 8 × 8 格的国际象棋上摆放八个皇后,使其不能互相攻击, 即: 任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。

+
+

八皇后问题共92中解法

+
+

数据结构与算法-010

+
public class MyTest {
+
+    public static void main(String[] args) {
+        EightQueens eightQueens = new EightQueens();
+        eightQueens.exec(0);
+    }
+
+}
+
+
+class EightQueens {
+
+    private int[] arr;
+
+    private int max;
+
+    public EightQueens() {
+        this.max = 8;
+        this.arr = new int[max];
+    }
+
+
+    // 算法
+    public void exec(int position) {
+        // 如果当前位置等于max说明解法成立,需要回溯
+        if (position == max) {
+            print();
+            return;
+        }
+        for (int i = 0; i < max; i++) {
+            arr[position] = i;
+            if (check(position)){
+                exec(position + 1);
+            }
+        }
+    }
+
+
+    // 判断皇后位置是否冲突
+    private boolean check(int position) {
+        for (int j = 0; j < position; j++) {
+            // 判断是否在同一列或在同一斜线上
+            if (arr[position] == arr[j] || Math.abs(position - j) == Math.abs(arr[position] - arr[j])) {
+               return false;
+            }
+        }
+        return true;
+    }
+
+
+    // 打印数组
+    private void print() {
+        for (int i = 0; i < arr.length; i++) {
+            System.out.print(arr[i] + "");
+        }
+        System.out.println();
+    }
+
+}
+

排序算法

+

排序也称排序算法,是将一组数据,依指定的顺序进行排列的过程。

+

排序算法分类:

+
    +
  • 内部排序:指将需要处理的所有数据都加载到内部存储器(内存)中进行排序;
  • +
  • 外部排序法:数据量过大,无法全部加载到内存中,需要借助外部存储(文件等)进行排序;
  • +
+

数据结构与算法-011

+

常见内排序算法复杂度比较

+

数据结构与算法-013

+

名词解释:

+
    +
  • n:数据规模
  • +
  • k:“桶"的个数
  • +
  • In-place:占用常数内存,不占用额外内存
  • +
  • Out-place:占用额外内存
  • +
  • 稳定性:排序后两个相等键值的顺序和排序之前它们的顺序相同,不稳定性则相反
  • +
  • 时间复杂度:一个算法执行所耗费的时间
  • +
  • 空间复杂度:运行完一个程序所需内存的大小
  • +
+

冒泡排序

+

冒泡排序的基本思想是:通过对待排序序列从前向后(从下标较小的元素开始),依次比较相邻元素的值,若发现逆序(当前值大于比较的值)则交换,使值较大的元素逐渐从前移向后部,就像水底下的气泡一样逐渐向上冒。

+

数据结构与算法-014

+

优化:因为排序的过程中, 各元素不断接近自己的位置,如果一趟比较下来没有进行过交换,就说明序列有序,因此要在排序过程中设置一个标志 flag 判断元素是否进行过交换。 从而减少不必要的比较。

+
class BubbleSorting {
+
+    public int[] sort(int[] data) {
+        int len = data.length - 1;
+        int tmp;
+        boolean flag = false;
+
+        for (int i = 0; i <len; i++){
+            for (int j = 0; j < len - i; j++) {
+                // 将当前值与next值进行比较,如果当前值大于next值则交换两者之间的位置
+                if (data[j] > data[j+1]){
+                    flag = true;
+                    tmp = data[j];
+                    data[j] = data[j+1];
+                    data[j+1] = tmp;
+                }
+            }
+
+            // 加入标志为进行判断,如果整个循环下啦都没有交换位置,说明该数组是有序的,所以直接退出循环
+            if (!flag){
+                break;
+            }else {
+                flag = false;
+            }
+        }
+        return data;
+    }
+}
+

选择排序

+

选择式排序也属于内部排序法,是从欲排序的数据中,按指定的规则选出某一元素,再依规定交换位置后达到排序的目的。

+

首先在未排序序列中找到最小元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小元素,然后放到排序序列末尾。以此类推,直到所有元素均排序完毕。

+

数据结构与算法-015

+
class SelectSorting{
+
+    public int[] sort(int[] data) {
+        for (int i = 0; i < data.length; i++){
+
+            int min = i;
+
+            for (int j = i+1; j < data.length; j++) {
+                // 如果next值大于当前值,则记录该值和该值的位置,等全部比较完毕后,将最大的一个与数据的末尾进行替换
+                if (data[i] > data[j]){
+                    min = j;
+                }
+            }
+
+            // 将每次循环中的最小的值,调整到最前面
+            if (min != i){
+                int tmp = data[i];
+                data[i] = data[min];
+                data[min] = tmp;
+            }
+        }
+        return data;
+    }
+}
+

插入排序

+

插入排序(Insertion Sorting) 的基本思想是:把 n 个待排序的元素看成为一个有序表和一个无序表;

+

开始时有序表中只包含一个元素,无序表中包含有 n-1 个元素,排序过程中每次从无序表中取出第一个元素,把它的排序码依次与有序表元素的排序码进行比较, 将它插入到有序表中的适当位置,使之成为新的有序表。

+

对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

+

数据结构与算法-016

+
class InsertSorting{
+
+    public int[] sort(int[] data) {
+        for (int i = 1; i < data.length; i++){
+            // 如果当前要插入的值 data[i] > 有序队列中最后一个,则将其直接插入到最后一个
+            if (data[i] > data[i - 1]){
+                continue;
+            }
+
+            int tmp = data[i];
+            int index = i - 1;
+            // 如果当前位置的值【tmp】小于 上一个位置的值【data[index]】说明当前值需要插入到有序队列中
+            while (index >= 0 && tmp < data[index]){
+                data[index + 1] = data[index];
+                index--;
+            }
+            data[index + 1] = tmp;
+        }
+        return data;
+    }
+
+}
+

希尔排序

+

希尔排序是希尔(Donald Shell) 于1959年提出的一种排序算法。希尔排序也是一种插入排序,它是简单插入排序经过改进之后的一个更高效的版本,也称为缩小增量排序。

+

希尔排序按照增量将数组进行分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。

+

数据结构与算法-017

+
class ShellSorting{
+
+    // 希尔排序,交换法
+    public void sortSwap(int[] data){
+        int size = data.length;
+        int tmp = 0;
+
+        // 将数据分组,分组数量:data.length/2
+        for (int gap = size >> 1; gap > 0; gap >>= 1){
+
+            for (int i = gap; i < size; i++) {
+                // 遍历每组中的元素
+                for (int j = i - gap; j >= 0; j -= gap) {
+                    // 将每组中的元素进行排序(交换元素)
+                    if (data[j] > data[j + gap]){
+                        tmp = data[j];
+                        data[j] = data[j+gap];
+                        data[j+gap] = tmp;
+                    }
+                }
+            }
+        }
+    }
+
+    // 插入法,融入 插入排序 思想
+    public void sort(int[] data){
+        int size = data.length;
+        int tmp = 0;
+
+        // 将数据分组,分组数量:data.length/2
+        for (int gap = size >> 1; gap > 0; gap >>= 1) {
+            for (int i = gap; i < size; i++) {
+
+                tmp = data[i];
+                int index = i;
+
+                // 如果当前位置的值【tmp】小于 上一个位置的值【data[index]】说明当前值需要插入到有序队列中
+                while (index - gap >= 0 && tmp < data[index - gap]){
+                    data[index] = data[index - gap];
+                    index -= gap;
+                }
+                data[index] = tmp;
+            }
+        }
+    }
+    
+}
+

快速排序

+

快速排序是由东尼·霍尔所发展的一种排序算法。在平均状况下,排序n个项目要 Ο(nlogn) 次比较。在最坏状况下则需要 Ο(n2) 次比较,但这种状况并不常见。事实上,快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环可以在大部分的架构上很有效率地被实现出来。

+

快速排序又是一种分而治之思想在排序算法上的典型应用。本质上来看,快速排序应该算是在冒泡排序基础上的递归分治法。快速排序思路:

+
    +
  • 选定pivot基准数
  • +
  • 将大于pivot基准数放在基准数右边
  • +
  • 将小于pivot基准数放在基准数左边
  • +
+

数据结构与算法-018

+
class QuickSorting {
+
+    public void sort(int[] data, int l, int r) {
+
+        // 如果开始的位置大于等于结束的位置则不用进行比较直接退出
+        if (l >= r){
+            return;
+        }
+
+        int left = l;
+        int right = r;
+
+        // 基准数值,将小于该数值的放在该数字的左边,大于该数值的放在右边
+        int pivot = data[left];
+
+        while (left < right) {
+
+            // 从右向左开始比较,如果此数大于等于基准数则将right索引向前移动,否则,将该值覆盖到对应的 data[left] 中
+            while (left < right && data[right] >= pivot) {
+                --right;
+            }
+            data[left] = data[right];
+
+            // 从左向右开始比较,如果此数小于等于基准数则将left索引向后移动,否则,将该值覆盖到对应的 data[right] 中
+            while (left < right && data[left] <= pivot) {
+                ++left;
+            }
+            data[right] = data[left];
+        }
+
+        // 此时left与right指向重合的位置为基准所在的位置,需要将该位置覆盖掉为基准的值
+        data[left] = pivot;
+
+        // 递归排序
+        sort(data, l, left);
+        sort(data, right+1,r);
+    }
+} 
+

归并排序

+

归并排序是利用归并的思想实现的排序方法, 该算法采用经典的分治(divide-and-conquer)策略;分治法将问题分成一些小的问题然后递归求解, 而治的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之。

+

归并排序算法思路:采用分治算法思想,首先将序列使用递归进行拆分,然后进行合并;合并思路,将两个有序队列中的元素分别按顺序进行比较,将结果保存在一个临时数组中,最后将临时数组合并到最后的队列中。

+

数据结构与算法-019

+
class MergeSorting {
+
+    // 递归拆分算法
+    public void divide(int[] arr, int start, int end, int[] tmpArr) {
+        if (start >= end) {
+            return;
+        }
+        int mid = (start + end) >> 1;
+
+        // 分别向左向右递归
+        divide(arr, start, mid, tmpArr);
+        divide(arr, mid + 1, end, tmpArr);
+
+        // 拆分一次合并一次
+        merge(arr, start, mid, end, tmpArr);
+
+    }
+
+    // 合并算法
+    public void merge(int[] arr, int start, int mid, int end, int[] tmpArr) {
+        int leftIndex = start;
+        int rightIndex = mid + 1;
+        int tmpArrIndex = 0;
+
+        // 判断是否超出范围
+        while (leftIndex <= mid && rightIndex <= end) {
+
+            // 将两组数据进行比较,按照从小到大的顺序将两组数据填入 tmpArr 中
+            if (arr[leftIndex] <= arr[rightIndex]) {
+                tmpArr[tmpArrIndex] = arr[leftIndex];
+                ++leftIndex;
+            } else {
+                tmpArr[tmpArrIndex] = arr[rightIndex];
+                ++rightIndex;
+            }
+            ++tmpArrIndex;
+        }
+
+        // 判断两组数据是否还有剩余,如果有剩余数据,则直接将数据追加到 tmpArr 数组后边
+        while (leftIndex <= mid) {
+            tmpArr[tmpArrIndex] = arr[leftIndex];
+            ++leftIndex;
+            ++tmpArrIndex;
+        }
+
+        while (rightIndex <= end) {
+            tmpArr[tmpArrIndex] = arr[rightIndex];
+            ++rightIndex;
+            ++tmpArrIndex;
+        }
+
+        // 将两组数据进行合并
+        tmpArrIndex = 0;
+        int tmpLeftIndex = leftIndex;
+        while (tmpLeftIndex <= end) {
+            arr[tmpLeftIndex] = tmpArr[tmpArrIndex];
+            ++tmpLeftIndex;
+            ++tmpArrIndex;
+        }
+    }
+    
+}
+

基数排序

+

基数排序是 1887 年赫尔曼·何乐礼发明的。基数排序属于“分配式排序”,又称“桶子法”或 bin sort,顾名思义,它是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的作用,基数排序法是属于稳定性的排序, 基数排序法的是效率高的稳定性排序法。

+

基数排序是桶排序的扩展,它是这样实现的: 将所有待比较数值统一为同样的数位长度,数位较短的数前面补零;然后, 从最低位开始, 依次进行一次排序。这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列。

+

数据结构与算法-020

+
class BucketSorting {
+
+    public void sort(int[] arr) {
+        // 求数组中最大数的长度
+        int maxNum = arr[0];
+        for (int i = 0; i < arr.length; i++) {
+            if (arr[i] > maxNum) {
+                maxNum = arr[i];
+            }
+        }
+        int maxNumLen = String.valueOf(maxNum).length();
+
+        // 桶,用于保存数据
+        int[][] buckets = new int[10][arr.length];
+
+        // 存放每个桶的保存数据的索引
+        int[] bucketElementIndex = new int[10];
+
+        // 将数组中的元素 按照 个、十、百、千 …… 的顺序依次放入桶中
+        for (int i = 0, n = 1; i < maxNumLen; i++, n *= 10) {
+
+            // 遍历二维数组
+            for (int j = 0; j < arr.length; j++) {
+                // 计算放入的桶的下标
+                int index = arr[j] / n % 10;
+                buckets[index][bucketElementIndex[index]] = arr[j];
+                bucketElementIndex[index]++;
+            }
+
+            // 从桶中依次取出元素并放入原数组中
+            int index = 0;
+            for (int f = 0; f < bucketElementIndex.length; f++) {
+                // 判断桶中是否保存数据
+                if (bucketElementIndex[f] == 0) {
+                    continue;
+                }
+                for (int h = 0; h < bucketElementIndex[f]; h++) {
+                    arr[index] = buckets[f][h];
+                    index++;
+                }
+                bucketElementIndex[f] = 0;
+            }
+        }
+
+    }
+}
+

堆排序

+

堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序,它的最坏、最好、平均时间复杂度均为O(nlogn),它也是不稳定排序。

+

堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。

+
+

大顶堆:每个节点的值都大于或等于其子节点的值,在堆排序算法中用于升序排列; +小顶堆:每个节点的值都小于或等于其子节点的值,在堆排序算法中用于降序排列;

+
+

数据结构与算法-027

+

数据结构与算法-026

+
class HeapSorting {
+
+    public void sort(int[] arr) {
+        // 计算树中的叶子结点位置
+        int leafNode = (arr.length >> 1) - 1;
+
+        // 构建大顶堆,此处需要注意 i >= 0 需要算根结点
+        for (int i = leafNode; i >= 0; i--) {
+            buildMaxHeap(arr, i, arr.length);
+        }
+
+        // 构建大顶堆后,将根元素与树中的最后一个进行交换,再循环构建大顶堆
+        int tmp;
+        for (int i = arr.length - 1; i > 0; i--) {
+            tmp = arr[i];
+            arr[i] = arr[0];
+            arr[0] = tmp;
+            buildMaxHeap(arr, 0, i);
+        }
+    }
+
+    /**
+     * 堆排序核心代码 构建大顶堆
+     *
+     * @param arr 需要调整的数组
+     * @param i   非叶子结点的索引位置
+     * @param len 每次调整的长度
+     */
+    public void buildMaxHeap(int[] arr, int i, int len) {
+        // 保存非叶子结点的位置如果该结点的值小于子结点的值,则需要进行交换
+        int tmp = arr[i];
+
+        // 从上至下,从左至右 遍历. 从第一个左子结点开始遍历
+        for (int n = (i << 1) + 1; n < len; n = (n << 1) + 1) {
+            // 如果左子结点 < 右结点,则需要将 n 指向右结点,即后移
+            if (n + 1 < len && arr[n] < arr[n + 1]) {
+                n++;
+            }
+            // 当前非叶子结点 < 当前子结点
+            if (arr[n] > tmp) {
+                // 将当前非叶子结点指向叶子结点
+                arr[i] = arr[n];
+                // 将i指向当前叶子结点,待最后将其变为 非叶子结点的值 即tmp的值
+                i = n;
+            } else {
+                break;
+            }
+        }
+        // 与前面互相呼应
+        arr[i] = tmp;
+    }
+}
+

查找算法

+

线性查找

+

线性查找又称顺序查找,是一种最简单的查找方法,它的基本思想是从第一个记录开始,逐个比较记录的关键字,直到和给定的K值相等,则查找成功;若比较结果与文件中n个记录的关键字都不等,则查找失败。

+
class LinearSearch{
+
+    public int search(int[] arr, int value){
+        for (int i = 0; i < arr.length; i++){
+            if (arr[i] == value) {
+                return i;
+            }
+        }
+        return - 1;
+    }
+
+}
+

二分查找

+

二分查找也称折半查找,它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。

+

二分查找算法的前提,数组必须是有序数组,如果没有有序列表,请使用排序算法对列表进行排序。

+

递归实现二分查找

+
class BinarySearch {
+
+    // 使用二分查找时,arr必须为有序列表
+    public int search(int start, int end, int[] arr, int value) {
+        // 在 {@param arr} 中 没有查到 {@param value}
+        if (start > end || value > arr[end] || value < arr[start]) {
+            return -1;
+        }
+
+        // 获取中间值,用于分割列表
+        int mid = (start + end) >> 1;
+        int midVal = arr[mid];
+
+        // 如果 查找的值< 中间值,说明该值可能在mid的左边
+        if (value < midVal) {
+            return search(start, mid - 1, arr, value);
+        }
+
+        // 相反如果 查找的值 > 中间值,说明该值可能在mid的右边
+        if (value > midVal) {
+            return search(mid + 1, end, arr, value);
+        }
+
+        // 使用递归不停的向下细分,当 value == arr[mid] 时 返回该值,说明此时已经找到了
+        return mid;
+    }
+}
+

非递归实现二分查找

+
public class BinarySearchDemo {
+    public static void main(String[] args) {
+        int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
+        System.out.println(new BinarySearch().search(arr, 3));
+    }
+}
+
+class BinarySearch {
+
+    // 使用二分查找时,arr必须为有序列表
+    public int search(int[] arr, int target) {
+        int left = 0;
+        int right = arr.length - 1;
+        while (left <= right) {
+            int mid = (left + right) >> 1;
+            if (arr[mid] == target) {
+                return mid;
+            }
+            // 如果目标值小于中间值则向左边找,反之向右边找
+            if (target < arr[mid]) {
+                right = mid - 1;
+            }
+
+            if (target > arr[mid]) {
+                left = mid + 1;
+            }
+        }
+        return -1;
+    }
+
+}
+

插值查找

+

插值查找算法类似于二分查找,与二分查找不同的是插值查找每次从自适应 mid 处开始查找,而不是像二分查找那样每次都从中间开始找。

+

数据结构与算法-021

+

注意:对于数据量较大,关键字分布比较均匀(最好是线性分布)的查找表来说,采用插值查找,速度较快;对于关键字分布不均匀的情况下,该方法不一定比二分查找要好。

+
class InsertValueSearch {
+
+    // 与二分查找基本相同,只是查找 mid 值发生了变动
+    public int search(int start, int end, int[] arr, int value) {
+        // 在 {@param arr} 中 没有查到 {@param value}
+        if (start > end || value > arr[end] || value < arr[start]) {
+            return -1;
+        }
+
+        int mid = start + (value - arr[start]) / (arr[end] - arr[start]) * (end - start);
+        int midVal = arr[mid];
+
+        if (value < midVal) {
+            return search(start, mid - 1, arr, value);
+        }
+
+        if (value > midVal) {
+            return search(mid + 1, end, arr, value);
+        }
+        return mid;
+    }
+
+}
+

斐波那契查找

+

斐波那契查找是基于【黄金分割】的二分查找。即在斐波那契队列中,将二分查找中的分割点替换为黄金分割点,来查找。

+
+

黄金分割是指将整体一分为二,较大部分与整体部分的比值等于较小部分与较大部分的比值,其 比值 约为 0.618。这个比例被公认为是最能引起美感的 比例,因此被称为黄金分割。

+
+

斐波那契查找特点:

+
    +
  • 平均性能「斐波那契查找」好于「二分查找」;
  • +
  • 「斐波那契查找」计算 mid 的时候 使用加减法而不是除法,会微弱提升效率;
  • +
+
class FibonacciSearch{
+    // lookupTable,需要传入斐波那契数列,例如:{1,1,2,3,5,8,13,21,34,55};
+    public static int search(int[] lookupTable,int[] f,int target){
+
+        int low = 0;
+        int high = lookupTable.length - 1;
+
+        // k 是 Fibonacci 分割数组下标
+        int k = 0;
+        int middle = 0;
+
+        while (f[k] < high){
+            k ++;
+        }
+
+        //利用 java 工具类构造 f[k] 长度的查找表,解决原有查找表元素不够的问题
+        int[] temp = Arrays.copyOf(lookupTable,f[k]);
+        while (low <= high){
+            middle = low + f[k - 1];
+            if (target < lookupTable[middle]){
+                high = middle -1;
+                k --;
+            }else if (target > lookupTable[middle]){
+                low = middle + 1;
+                k -= 2;
+            }else{
+                return Math.min(middle,high);
+            }
+        }
+        return -1;
+    }
+}
+

哈夫曼编码

+

赫夫曼编码也翻译为哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,属于一种程序算法。

+

哈夫曼编码是哈夫曼树在电讯通信中的经典的应用之一。哈夫曼编码广泛地用于数据文件压缩。其压缩率通常在20%~90%之间。哈夫曼码是可变字长编码的一种。Huffman于1952年提出一种编码方法,称之为最佳编码。

+
+

定长编码与变长编码,以字符串like like为例:

+
    +
  • 定长编码:
  • +
+
    +
  1. 将上述字符串转换对应的ASCII: 108 105 107 101 32 108 105 107 101
  2. +
  3. ASCII转换为二进制:01101100 01101001 01101011 01100101 00100000 01101100 01101001 01101011 01100101 +
  4. +
+
    +
  • 变长编码:
  • +
+
    +
  1. 统计上述字符串出现的各字符出现的次数:l:2 i:2 k:2 e:2 :1
  2. +
  3. 按照各个字符出现的次数进行编码,原则是出现次数越多的,则编码越小:0=l 1=i 10=k 11=e 100=
  4. +
  5. 最终转换为变长编码为:011011100011011
  6. +
+
+

上述的变长编码 011011100011011 在解码的时候会出现多意现象,比如当匹配到数字1,是把1解成i还是按照10来进行解码。因为这种现象的存在,所以在进行变长编码时,编码要符合前缀编码。

+
+

字符的编码都不能是其他字符编码的前缀,符合此要求的编码叫做前缀编码, 即不能匹配到重复的编码。

+
+

构建哈夫曼编码思路:

+
    +
  1. 统计字节数组中各个数据的权重,即字符出现的次数;
  2. +
  3. 将统计好的字符出现的次数,构建成哈夫曼树
  4. +
  5. 根据上面创建的哈夫曼树获得每个数值对应的可变长编码值,规定结点的左边为0 ,右边为1;
  6. +
  7. 以每个数值新的编码重新对字符数组进行编码,即可得到赫夫曼编码后的值;
  8. +
+

假如一段信息里只有A,B,C,D,E,F这6个字符,他们出现的次数依次是2次,3次,7次,9次,18次,25次,最终构建成哈夫曼编树为下图所示:

+

数据结构与算法-029

+

得到哈夫曼编码:

+
A=11100 B=11101 C=1111 D=110 E=10 F=0
+

利用哈夫曼编码,压缩解压文件:

+
public class HuffmanCodeTest {
+
+    public static void main(String[] args) {
+
+        // 测试压缩文件
+        String srcFile = "/Users/whitepure/Desktop/1.png";
+        String dstFile = "/Users/whitepure/Desktop/1.zip";
+
+        zipFile(srcFile, dstFile);
+        System.out.println("压缩文件成功");
+
+        // 测试解压文件
+        srcFile = "/Users/whitepure/Desktop/1.zip";
+        dstFile = "/Users/whitepure/Desktop/1copy.png";
+        unZipFile(srcFile, dstFile);
+        System.out.println("解压成功!");
+    }
+
+
+    // 将一个文件进行压缩
+    public static void zipFile(String srcFile, String dstFile) {
+        // 创建输出流
+        OutputStream os = null;
+        ObjectOutputStream oos = null;
+        // 创建文件的输入流
+        FileInputStream is = null;
+        try {
+            // 创建文件的输入流
+            is = new FileInputStream(srcFile);
+            // 创建一个和源文件大小一样的byte[]
+            byte[] b = new byte[is.available()];
+            // 读取文件
+            is.read(b);
+            HuffmanCode huffmanCode = new HuffmanCode();
+            // 直接对源文件压缩
+            byte[] huffmanBytes = huffmanCode.encode(b);
+            // 创建文件的输出流, 存放压缩文件
+            os = new FileOutputStream(dstFile);
+            // 创建一个和文件输出流关联的ObjectOutputStream
+            oos = new ObjectOutputStream(os);
+            // 把 赫夫曼编码后的字节数组写入压缩文件
+            oos.writeObject(huffmanBytes); // 我们是把
+            // 这里我们以对象流的方式写入 赫夫曼编码,是为了以后我们恢复源文件时使用
+            // 注意一定要把赫夫曼编码 写入压缩文件
+            oos.writeObject(huffmanCode.getHuffmanCodes());
+
+        } catch (Exception e) {
+            // TODO: handle exception
+            System.out.println(e.getMessage());
+        } finally {
+            try {
+                is.close();
+                oos.close();
+                os.close();
+            } catch (Exception e) {
+                // TODO: handle exception
+                System.out.println(e.getMessage());
+            }
+        }
+
+    }
+
+
+    // 完成对压缩文件的解压
+    public static void unZipFile(String zipFile, String dstFile) {
+
+        // 定义文件输入流
+        InputStream is = null;
+        // 定义一个对象输入流
+        ObjectInputStream ois = null;
+        // 定义文件的输出流
+        OutputStream os = null;
+        try {
+            // 创建文件输入流
+            is = new FileInputStream(zipFile);
+            // 创建一个和 is关联的对象输入流
+            ois = new ObjectInputStream(is);
+            // 读取byte数组 huffmanBytes
+            byte[] huffmanBytes = (byte[]) ois.readObject();
+            // 读取赫夫曼编码表
+            Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
+            HuffmanCode huffmanCode = new HuffmanCode();
+            // 解码
+            byte[] bytes = huffmanCode.decode(huffmanCodes, huffmanBytes);
+            // 将bytes 数组写入到目标文件
+            os = new FileOutputStream(dstFile);
+            // 写数据到 dstFile 文件
+            os.write(bytes);
+        } catch (Exception e) {
+            // TODO: handle exception
+            System.out.println(e.getMessage());
+        } finally {
+
+            try {
+                os.close();
+                ois.close();
+                is.close();
+            } catch (Exception e2) {
+                // TODO: handle exception
+                System.out.println(e2.getMessage());
+            }
+
+        }
+    }
+}
+
+class HuffmanCode {
+
+    private final Map<Byte, String> huffmanCodes = new HashMap<>();
+
+    public Map<Byte, String> getHuffmanCodes() {
+        return huffmanCodes;
+    }
+
+    // 生成 huffman 编码 压缩
+    public byte[] encode(byte[] bytes) {
+        List<Node> nodes = buildHuffmanNodes(bytes);
+        Node huffmanTreeRoot = buildHuffmanTree(nodes);
+        Map<Byte, String> huffmanCodes = buildHuffmanCodeTab(huffmanTreeRoot);
+        return zip(bytes, huffmanCodes);
+    }
+
+    // 将 huffman编码 解码 解压缩
+    public byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
+        StringBuilder stringBuilder = new StringBuilder();
+
+        // 将byte数组转成二进制的字符串
+        for (int i = 0; i < huffmanBytes.length - 1; i++) {
+            byte b = huffmanBytes[i];
+            String strToAppend = byteToBitString(b);
+            // 判断是不是最后一个字节
+            boolean isLastByte = (i == huffmanBytes.length - 2);
+            if (isLastByte) {
+                // 得到最后一个字节的有效位数
+                byte validBits = huffmanBytes[huffmanBytes.length - 1];
+                strToAppend = strToAppend.substring(0, validBits);
+            }
+            stringBuilder.append(strToAppend);
+        }
+
+        // 把字符串按照指定的赫夫曼编码进行解码
+        // 把赫夫曼编码表进行调换,因为反向查询 a->100 100->a
+        Map<String, Byte> map = new HashMap<>();
+        huffmanCodes.forEach((key, value) -> map.put(value, key));
+
+        // 创建要给集合,存放byte
+        List<Byte> list = new ArrayList<>();
+        // i 可以理解成就是索引,扫描 stringBuilder
+        for (int i = 0; i < stringBuilder.length(); ) {
+            int count = 1;
+            boolean flag = true;
+            Byte b = null;
+
+            while (flag) {
+                // 递增的取出 key
+                String key = stringBuilder.substring(i, i + count);
+                b = map.get(key);
+                if (b == null) {
+                    // 没有匹配到
+                    count++;
+                } else {
+                    // 匹配到
+                    flag = false;
+                }
+            }
+            list.add(b);
+            i += count;
+        }
+        byte[] b = new byte[list.size()];
+        IntStream.range(0, b.length).forEach(i -> b[i] = list.get(i));
+        return b;
+    }
+
+    // 计算字符串中每个字符出现的次数
+    private List<Node> buildHuffmanNodes(byte[] bytes) {
+        ArrayList<Node> nodes = new ArrayList<>();
+
+        // 利用map记录集合中元素出现的次数
+        Map<Byte, Integer> counts = new HashMap<>();
+        for (byte b : bytes) {
+            counts.merge(b, 1, Integer::sum);
+        }
+
+        // 把每一个键值对转成一个Node 对象,并加入到nodes集合
+        counts.forEach((key, value) -> nodes.add(new Node(key, value)));
+        return nodes;
+    }
+
+    // 构建Huffman树
+    private Node buildHuffmanTree(List<Node> nodes) {
+        while (nodes.size() > 1) {
+            // 排序, 从小到大
+            Collections.sort(nodes);
+            // 取出第一颗最小的二叉树
+            Node leftNode = nodes.get(0);
+            // 取出第二颗最小的二叉树
+            Node rightNode = nodes.get(1);
+            // 创建一颗新的二叉树,它的根节点 没有data, 只有权值
+            Node parent = new Node(null, leftNode.weight + rightNode.weight);
+            parent.left = leftNode;
+            parent.right = rightNode;
+
+            // 将已经处理的两颗二叉树从nodes删除
+            nodes.remove(leftNode);
+            nodes.remove(rightNode);
+            // 将新的二叉树,加入到nodes
+            nodes.add(parent);
+        }
+        // nodes 最后的结点,就是赫夫曼树的根结点
+        return nodes.get(0);
+    }
+
+    // 重载 getCodes
+    private Map<Byte, String> buildHuffmanCodeTab(Node root) {
+        if (root == null) {
+            return null;
+        }
+        // 处理root的左子树
+        buildHuffmanCodeTab(root.left, "0", new StringBuilder());
+        // 处理root的右子树
+        buildHuffmanCodeTab(root.right, "1", new StringBuilder());
+        return huffmanCodes;
+    }
+
+    // 获取huffman编码表
+    private void buildHuffmanCodeTab(Node node, String code, StringBuilder stringBuilder) {
+        StringBuilder curNodeCode = new StringBuilder(stringBuilder);
+        curNodeCode.append(code);
+        if (node == null) {
+            return;
+        }
+        // 判断当前node 是叶子结点还是非叶子结点
+        if (node.data == null) { // 非叶子结点
+            // 向左递归
+            buildHuffmanCodeTab(node.left, "0", curNodeCode);
+            // 向右递归
+            buildHuffmanCodeTab(node.right, "1", curNodeCode);
+        } else {
+            // 表示找到某个叶子结点的最后
+            huffmanCodes.put(node.data, curNodeCode.toString());
+        }
+    }
+
+    // 压缩传入字节(将传入字符串转成字节类型)将待压缩字节转换为字节数组
+    private byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
+        // 利用 huffmanCodes 将 bytes 转成 赫夫曼编码对应的字符串
+        StringBuilder stringBuilder = new StringBuilder();
+        // 遍历bytes 数组
+        for (byte b : bytes) {
+            stringBuilder.append(huffmanCodes.get(b));
+        }
+
+        // 统计返回 byte[] huffmanCodeBytes 长度
+        int len;
+        // 等同于 int len = (stringBuilder.length() + 7) / 8;
+        byte countToEight = (byte) (stringBuilder.length() & 7);
+        if (countToEight == 0) {
+            len = stringBuilder.length() >> 3;
+        } else {
+            len = (stringBuilder.length() >> 3) + 1;
+            // 后面补零
+            for (int i = countToEight; i < 8; i++) {
+                stringBuilder.append("0");
+            }
+        }
+
+        // 创建 存储压缩后的 byte数组,huffmanCodeBytes[len]记录赫夫曼编码最后一个字节的有效位数
+        byte[] huffmanCodeBytes = new byte[len + 1];
+        huffmanCodeBytes[len] = countToEight;
+        int index = 0;
+        // 因为是每8位对应一个byte,所以步长 +8
+        for (int i = 0; i < stringBuilder.length(); i += 8) {
+            String strByte;
+            strByte = stringBuilder.substring(i, i + 8);
+            // 将strByte 转成一个byte,放入到 huffmanCodeBytes
+            huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
+            index++;
+        }
+        return huffmanCodeBytes;
+    }
+
+    // 将 byte 转换为对应的字符串
+    private String byteToBitString(byte b) {
+        int temp = b;
+        // 如果是正数我们需要将高位补零
+        temp |= 0x100;
+        // 转换为二进制字符串,正数:高位补 0 即可,然后截取低八位即可;负数直接截取低八位即可
+        // 负数在计算机内存储的是补码,补码转原码:先 -1 ,再取反
+        String binaryStr = Integer.toBinaryString(temp);
+        return binaryStr.substring(binaryStr.length() - 8);
+    }
+
+}
+
+class Node implements Comparable<Node> {
+    Byte data;
+    int weight;
+    Node left;
+    Node right;
+
+    public Node(Byte data, int weight) {
+        this.data = data;
+        this.weight = weight;
+    }
+
+    @Override
+    public int compareTo(Node o) {
+        // 从小到大排序
+        return this.weight - o.weight;
+    }
+
+    public String toString() {
+        return "Node [data = " + data + " weight=" + weight + "]";
+    }
+}
+

分治算法

+

分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……

+

分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。

+

使用分治算饭解决汉诺塔问题

+
+

汉诺塔(又称河内塔)问题是源于印度一个古老传说的益智玩具。大梵天创造世界的时候做了三根金刚石柱子,在一根柱子上从下往上按照大小顺序摞着64片黄金圆盘。大梵天命令婆罗门把圆盘从下面开始按大小顺序重新摆放在另一根柱子上。并且规定,在小圆盘上不能放大圆盘,在三根柱子之间一次只能移动一个圆盘。

+
+
public class HanoiTowerDemo {
+    public static void main(String[] args) {
+        HanoiTower hanoiTower = new HanoiTower();
+        hanoiTower.hanoiTower(3, 'A', 'B', 'C');
+    }
+}
+
+class HanoiTower {
+
+    public void hanoiTower(int n, char a, char b, char c) {
+        if (n <= 0) {
+            return;
+        }
+        if (n == 1) {
+            System.out.println(a + "->" + c);
+            return;
+        }
+        // 将a塔上面除了底盘外的所有盘移动到b塔
+        hanoiTower(n - 1, a, c, b);
+
+        // 将a塔遗留的底盘移动到c塔
+        System.out.println(a + "->" + c);
+
+        // 将b塔上面的所有盘移动到c塔
+        hanoiTower(n - 1, b, a, c);
+    }
+
+}
+

动态规划算法

+

动态规划(Dynamic Programming)算法的核心思想是:将大问题划分为小问题进行解决,从而一步步获取最优解的处理算法。

+

动态规划算法与分治算法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往不是互相独立的。 ( 即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解 )

+

关于动态规划最经典的问题当属背包问题。

+
+

背包问题主要是指一个给定容量的背包、若干具有一定价值和重量的物品,如何选择物品放入背包使物品的价值最大。其中又分01背包和完全背包(完全背包指的是:每种物品都有无限件可用)这里的问题属于01背包,即每个物品最多放一个。而无限背包可以转化为01背包。

+ + + + + + + + + + + + + + + + + + + + + + + + + +
物品重量价格
吉他(G)11500
音响(S)43000
电脑(L)32000
+
+
public class KnapsackProblemDemo {
+    public static void main(String[] args) {
+        KnapsackProblem knapsackProblem = new KnapsackProblem();
+        System.out.println(knapsackProblem.knapsackProblem());
+    }
+}
+
+class KnapsackProblem {
+
+    public int knapsackProblem() {
+        // 物品的重量
+        int[] w = {1, 4, 3};
+        // 物品的价值
+        int[] val = {1500, 3000, 2000};
+        // 背包的容量
+        int m = 4;
+        // 物品的个数
+        int n = val.length;
+        // 物品规划表
+        int[][] v = new int[n + 1][m + 1];
+
+        // 将v[][] 第一列和第一行重置为0
+        for (int i = 0; i < v.length; i++) {
+            v[i][0] = 0;
+        }
+        for (int i = 0; i < v[0].length; i++) {
+            v[0][i] = 0;
+        }
+
+        // 处理 生成物品价格表
+        for (int i = 1; i < v.length; i++) {
+            for (int j = 1; j < v[0].length; j++) {
+                // 如果当前商品的重量 是否能写入当前表格中
+                if (w[i - 1] > j) {
+                    v[i][j] = v[i - 1][j];
+                } else {
+                    v[i][j] = Math.max(v[i - 1][j], val[i - 1] + v[i - 1][j - w[i - 1]]);
+                }
+            }
+        }
+
+        // 处理完后 v[][] 表中数值最大的就是最后的结果
+        int max = 0;
+        for (int[] ints : v) {
+            System.out.println(Arrays.toString(ints));
+            for (int anInt : ints) {
+                if (anInt > max) {
+                    max = anInt;
+                }
+            }
+        }
+        return max;
+    }
+}
+

KMP算法

+
+

KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。

+
+
    +
  • +

    常规算法匹配字符串

    +

    数据结构与算法-042

    +

    从主串的起始位置(或指定位置)开始与模式串的第一个字符比较,若相等,则继续逐个比较后续字符;否则从主串的下一个字符再重新和模式串的字符比较。依次类推,直到模式串成功匹配,返回主串中第一次出现模式串字符的位置,或者模式串匹配不成功,这里约定返回-1。

    +
  • +
  • +

    KMP算法匹配字符串

    +

    数据结构与算法-043

    +

    主要就是改进了暴力匹配中i回溯的操作,KMP算法中当一趟匹配过程中出现字符比较不等时,不直接回溯i,而是利用已经得到的“部分匹配”的结果将模式串向右移动(j-next[j-1])的距离。

    +
    public class KMPDemo {
    +    public static void main(String[] args) {
    +        KMP kmp = new KMP();
    +        String str1 = "BBC ABCDAB ABCDABCDABDE";
    +        String str2 = "ABCDABD";
    +        int[] next = kmp.getMatchTab(str2);
    +        System.out.println(Arrays.toString(next));
    +        System.out.println(kmp.kmpSearch(str1, str2, next));
    +    }
    +}
    +
    +class KMP {
    +
    +    // 获取KMP 部分匹配表
    +    public int[] getMatchTab(String dest) {
    +        int[] result = new int[dest.length()];
    +        // 部分匹配表第一个值始终为0
    +        result[0] = 0;
    +        for (int i = 1, j = 0; i < result.length; i++) {
    +            // KMP 核心(特点,公式)
    +            while (j > 0 && dest.charAt(i) != dest.charAt(j)) {
    +                j = result[j - 1];
    +            }
    +            if (dest.charAt(j) == dest.charAt(i)) {
    +                j++;
    +            }
    +            result[i] = j;
    +        }
    +        return result;
    +    }
    +
    +    /**
    +     * KMP查找算法
    +     *
    +     * @param str1 原字符串
    +     * @param str2 子字符串
    +     * @param next 部分匹配表
    +     * @return 匹配到字符串的第一个索引位置
    +     */
    +    public int kmpSearch(String str1, String str2, int[] next) {
    +        for (int i = 0, j = 0; i < str1.length(); i++) {
    +            while (j > 0 && str1.charAt(i) != str2.charAt(j)) {
    +                j = next[j - 1];
    +            }
    +            if (str1.charAt(i) == str2.charAt(j)) {
    +                j++;
    +            }
    +            if (j == str2.length()) {
    +                return i - j + 1;
    +            }
    +        }
    +        return -1;
    +    }
    +
    +}
    +
  • +
+

贪心算法

+

贪心算法又称贪婪算法,是指在对问题进行求解时,在每一步选择中都采取最好或者最优(即最有利)的选择,从而希望能够导致结果是最好或者最优的算法。贪婪算法所得到的结果不一定是最优的结果(有时候会是最优解),但是都是相对近似(接近)最优解的结果。

+

举例,假设存在下面需要付费的广播台,以及广播台信号可以覆盖的地区。 如何选择最少的广播台,让所有的地区都可以接收到信号。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
广播台覆盖地区
K1“北京”, “上海”, “天津”
K2“广州”, “北京”, “深圳”
K3“成都”, “上海”, “杭州”
K4“上海”, “天津”
K5“杭州”, “大连”
+
public class GreedyAlgorithmDemo {
+    public static void main(String[] args) {
+        HashMap<String, HashSet<String>> broadcasts = new HashMap<>();
+        HashSet<String> hashSet1 = new HashSet<>();
+        hashSet1.add("北京");
+        hashSet1.add("上海");
+        hashSet1.add("天津");
+
+        HashSet<String> hashSet2 = new HashSet<>();
+        hashSet2.add("广州");
+        hashSet2.add("北京");
+        hashSet2.add("深圳");
+
+        HashSet<String> hashSet3 = new HashSet<>();
+        hashSet3.add("成都");
+        hashSet3.add("上海");
+        hashSet3.add("杭州");
+
+        HashSet<String> hashSet4 = new HashSet<>();
+        hashSet4.add("上海");
+        hashSet4.add("天津");
+
+        HashSet<String> hashSet5 = new HashSet<>();
+        hashSet5.add("杭州");
+        hashSet5.add("大连");
+
+        broadcasts.put("K1", hashSet1);
+        broadcasts.put("K2", hashSet2);
+        broadcasts.put("K3", hashSet3);
+        broadcasts.put("K4", hashSet4);
+        broadcasts.put("K5", hashSet5);
+
+        // allAreas 存放所有的地区
+        HashSet<String> allAreas = new HashSet<>();
+        for (Map.Entry<String, HashSet<String>> broadcast : broadcasts.entrySet()) {
+            allAreas.addAll(broadcast.getValue());
+        }
+
+        System.out.println(new GreedyAlgorithm().getRadioByGreedyAlgorithm(allAreas, broadcasts));
+    }
+}
+
+class GreedyAlgorithm {
+
+    public List<String> getRadioByGreedyAlgorithm(HashSet<String> allAreas, HashMap<String, HashSet<String>> broadcasts) {
+        // 存放选择的电台
+        ArrayList<String> selects = new ArrayList<>();
+
+        // 存放每次选择最优的电台
+        String maxKey = null;
+
+        // 临时集合 从 broadcasts 中选出能覆盖的电台,即存放 allAreas 与 broadcasts 的交集
+        HashSet<String> tmpSet = new HashSet<>();
+
+        while (allAreas.size() > 0) {
+            // 每次需要清空
+            maxKey = null;
+
+            for (String key : broadcasts.keySet()) {
+                tmpSet.clear();
+                tmpSet.addAll(broadcasts.get(key));
+
+                // 计算覆盖的电台 并赋值给tmpSet
+                tmpSet.retainAll(allAreas);
+
+                // 此处进行比较 体现贪心算法
+                if (tmpSet.size() > 0 && (maxKey == null || tmpSet.size() > broadcasts.get(maxKey).size())) {
+                    maxKey = key;
+                }
+            }
+
+            // 每进行一次循环最后需要移除选中的maxKey对应的电台城市
+            if (maxKey != null) {
+                selects.add(maxKey);
+                allAreas.removeAll(broadcasts.get(maxKey));
+            }
+        }
+        return selects;
+    }
+
+}
+

普里姆算法

+

普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点,且其所有边的权值之和亦为最小。

+
+

最小生成树:给定一个带权的无向连通图,如何选取一棵生成树,使树上所有边上权的总和为最小,这叫最小生成树。简称MST。 +求最小生成树的算法主要是普里姆算法和克鲁斯卡尔算法。

+
    +
  • 普里姆算法:O(n^2),适合稠密图(边多的图)
  • +
  • 克鲁斯卡尔算法:O,适合稀疏图(边少的图)
  • +
+
+

普利姆(Prim)算法求最小生成树,也就是在包含n个顶点的连通图中,找出只有(n-1)条边包含所有n个顶点的连通子图,也就是所谓的极小连通子图。

+

数据结构与算法-044

+
public class PrimAlgorithmDemo {
+    public static void main(String[] args) {
+        char[] data = new char[]{'A', 'B', 'C', 'D', 'E', 'F', 'G'};
+        int vertex = data.length;
+        //邻接矩阵的关系使用二维数组表示,10000这个大数,表示两个点不联通
+        int[][] weight = new int[][]{
+                {10000, 5, 7, 10000, 10000, 10000, 2},
+                {5, 10000, 10000, 9, 10000, 10000, 3},
+                {7, 10000, 10000, 10000, 8, 10000, 10000},
+                {10000, 9, 10000, 10000, 10000, 4, 10000},
+                {10000, 10000, 8, 10000, 10000, 5, 4},
+                {10000, 10000, 10000, 4, 5, 10000, 6},
+                {2, 3, 10000, 10000, 4, 6, 10000}
+        };
+
+        MGraph graph = new MGraph(vertex);
+        PrimAlgorithm minTree = new PrimAlgorithm();
+        graph.create(graph, vertex, data, weight);
+        graph.show(graph);
+        minTree.prim(graph, 0);
+    }
+}
+
+class PrimAlgorithm {
+
+    /**
+     * 最小生成树问题 prim算法
+     *
+     * @param graph  图对象
+     * @param vertex 开始的顶点
+     */
+    public void prim(MGraph graph, int vertex) {
+        int i = 0, j = 0;
+        int row = -1, column = -1;
+        // 存放已经访问过的顶点
+        int[] visited = new int[graph.vertex];
+        // 用1表示两点之间已经连接, 0表示未连接
+        visited[vertex] = 1;
+        int minWeight = 10000;
+
+        for (int k = 1; k < graph.vertex; k++){
+            // 比较两点之间的权值,每次都获取最小的权值
+            for (i = 0; i < graph.vertex; i++) {
+                for(j = 0; j < graph.vertex; j++){
+                    if (visited[i] == 1 && visited[j] == 0 && graph.weight[i][j] < minWeight){
+                        minWeight = graph.weight[i][j];
+                        row = i;
+                        column = j;
+                    }
+                }
+            }
+            System.out.println("边<" + graph.data[row] + "," + graph.data[column] + "> 权值:" + minWeight);
+            // 将顶点标记为已经访问过
+            visited[column] = 1;
+
+            // 每次比较完后需要将minWeight重置
+            minWeight = 10000;
+        }
+
+    }
+
+}
+
+class MGraph {
+    int vertex;
+    char[] data;
+    int[][] weight;
+
+    public MGraph(int vertex) {
+        this.vertex = vertex;
+        data = new char[vertex];
+        weight = new int[vertex][vertex];
+    }
+
+    /**
+     * 创建图的邻接矩阵
+     *
+     * @param graph  图对象
+     * @param vertex 图对应的顶点个数
+     * @param data   图的各个顶点的值
+     * @param weight 图的邻接矩阵
+     */
+    public void create(MGraph graph, int vertex, char[] data, int[][] weight) {
+        int i, j;
+        for (i = 0; i < vertex; i++) {
+            graph.data[i] = data[i];
+            for (j = 0; j < vertex; j++) {
+                graph.weight[i][j] = weight[i][j];
+            }
+        }
+    }
+
+    // 显示图的邻接矩阵
+    public void show(MGraph graph) {
+        for (int[] link : graph.weight) {
+            System.out.println(Arrays.toString(link));
+        }
+    }
+}
+

克鲁斯卡尔算法

+

克鲁斯卡尔算法是求连通网的最小生成树的另一种方法。基本思想是, 将所有边按照权值的大小进行升序排序,然后从小到大一一判断,条件为:如果这个边不会与之前选择的所有边组成回路,就可以作为最小生成树的一部分;反之,舍去。 直到具有 n 个顶点的连通网筛选出来 n-1(n为顶点个数) 条边为止。

+
+

判断是否构成回路: 当每次需要将一条边添加到最小生成树时,判断该边的两个顶点终点是否相同,相同就会构成回路。

+

关于终点的说明:就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是与它连通的最大顶点。 就是将所有顶点按照从小到大的顺序排列好之后;某个顶点的终点就是与它连通的最大顶点。

+

举例

+
    +
  • 首先ABCDEFG这7个顶点,在顶点集合中是按照顺序存放的;
  • +
  • 第一次选择的是EF,毫无疑问这一条边的终点是F;
  • +
  • 第二次选择的CD的终点D;
  • +
  • 第三次选择的DE,终点是F,因为此时D和E相连,D又和F相连,所以D的终点是F。而且,因为C和D是相连的,D和E相连,E和F也是相连的,所以C的终点此时变成了F。也就是说,当选择了EF、CD、DE这三条边后,C、D、E的终点都是F。当然F的终点也是F,因为F还没和后面的哪个顶点连接。
  • +
  • 本来接下来应该选择CE的,但是由于C和E的终点都是F,所以就会形成回路;
  • +
+
+

数据结构与算法-045

+
public class KruskalCaseDemo {
+
+    //使用 INF 表示两个顶点不能连通
+    private static final int INF = Integer.MAX_VALUE;
+
+    public static void main(String[] args) {
+        char[] vertexs = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
+        int matrix[][] = {
+                /*A*//*B*//*C*//*D*//*E*//*F*//*G*/
+                /*A*/ {0, 12, INF, INF, INF, 16, 14},
+                /*B*/ {12, 0, 10, INF, INF, 7, INF},
+                /*C*/ {INF, 10, 0, 3, 5, 6, INF},
+                /*D*/ {INF, INF, 3, 0, 4, INF, INF},
+                /*E*/ {INF, INF, 5, 4, 0, 2, 8},
+                /*F*/ {16, 7, 6, INF, 2, 0, 9},
+                /*G*/ {14, INF, INF, INF, 8, 9, 0}};
+
+        KruskalCase kruskalCase = new KruskalCase(vertexs, matrix);
+        kruskalCase.print();
+        kruskalCase.kruskal();
+    }
+
+}
+
+class KruskalCase {
+    //使用 INF 表示两个顶点不能连通
+    private static final int INF = Integer.MAX_VALUE;
+    private int edgeNum; //边的个数
+    private char[] vertexs; //顶点数组
+    private int[][] matrix; //邻接矩阵
+
+    // 构造器
+    public KruskalCase(char[] vertexs, int[][] matrix) {
+        // 初始化顶点数和边的个数
+        int vlen = vertexs.length;
+
+        // 初始化顶点, 复制拷贝的方式
+        this.vertexs = new char[vlen];
+        for (int i = 0; i < vertexs.length; i++) {
+            this.vertexs[i] = vertexs[i];
+        }
+
+        // 初始化边, 使用的是复制拷贝的方式
+        this.matrix = new int[vlen][vlen];
+        for (int i = 0; i < vlen; i++) {
+            for (int j = 0; j < vlen; j++) {
+                this.matrix[i][j] = matrix[i][j];
+            }
+        }
+        // 统计边的条数
+        for (int i = 0; i < vlen; i++) {
+            for (int j = i + 1; j < vlen; j++) {
+                if (this.matrix[i][j] != INF) {
+                    edgeNum++;
+                }
+            }
+        }
+    }
+
+    public void print() {
+        System.out.println("邻接矩阵为: \n");
+        for (int i = 0; i < vertexs.length; i++) {
+            for (int j = 0; j < vertexs.length; j++) {
+                System.out.printf("%12d", matrix[i][j]);
+            }
+            System.out.println();
+        }
+    }
+
+    /**
+     * 功能:对边进行排序处理, 冒泡排序
+     *
+     * @param edges 边的集合
+     */
+    private void sortEdges(EdgeData[] edges) {
+        for (int i = 0; i < edges.length - 1; i++) {
+            for (int j = 0; j < edges.length - 1 - i; j++) {
+                if (edges[j].weight > edges[j + 1].weight) {
+                    EdgeData tmp = edges[j];
+                    edges[j] = edges[j + 1];
+                    edges[j + 1] = tmp;
+                }
+            }
+        }
+    }
+
+    /**
+     * @param ch 顶点的值,比如'A','B'
+     * @return 返回ch顶点对应的下标,如果找不到,返回-1
+     */
+    private int getPosition(char ch) {
+        for (int i = 0; i < vertexs.length; i++) {
+            if (vertexs[i] == ch) {
+                return i;
+            }
+        }
+        // 找不到,返回-1
+        return -1;
+    }
+
+    /**
+     * 功能: 获取图中边,放到EData[] 数组中,后面我们需要遍历该数组
+     * 是通过matrix 邻接矩阵来获取
+     * EData[] 形式 [['A','B', 12], ['B','F',7], .....]
+     */
+    private EdgeData[] getEdges() {
+        int index = 0;
+        EdgeData[] edges = new EdgeData[edgeNum];
+        for (int i = 0; i < vertexs.length; i++) {
+            for (int j = i + 1; j < vertexs.length; j++) {
+                if (matrix[i][j] != INF) {
+                    edges[index++] = new EdgeData(vertexs[i], vertexs[j], matrix[i][j]);
+                }
+            }
+        }
+        return edges;
+    }
+
+    /**
+     * 功能: 获取下标为i的顶点的终点, 用于后面判断两个顶点的终点是否相同
+     *
+     * @param ends : 数组就是记录了各个顶点对应的终点是哪个,ends 数组是在遍历过程中,逐步形成
+     * @param i    : 表示传入的顶点对应的下标
+     * @return 返回的就是 下标为i的这个顶点对应的终点的下标, 一会回头还有来理解
+     */
+    private int getEnd(int[] ends, int i) { // i = 4 [0,0,0,0,5,0,0,0,0,0,0,0]
+        while (ends[i] != 0) {
+            i = ends[i];
+        }
+        return i;
+    }
+
+    public void kruskal() {
+        int index = 0;
+        // 用于保存"已有最小生成树" 中的每个顶点在最小生成树中的终点
+        int[] ends = new int[vertexs.length];
+        // 创建结果数组, 保存最后的最小生成树
+        EdgeData[] rets = new EdgeData[edgeNum];
+
+        // 获取图中 所有的边的集合 , 一共有12边
+        EdgeData[] edges = getEdges();
+
+        // 按照边的权值大小进行排序(从小到大)
+        sortEdges(edges);
+
+        // 遍历edges 数组,将边添加到最小生成树中时,判断是准备加入的边否形成了回路,如果没有,就加入 rets, 否则不能加入
+        for (int i = 0; i < edgeNum; i++) {
+            // 获取到第i条边的第一个顶点(起点)
+            int point1 = getPosition(edges[i].start);
+            // 获取到第i条边的第2个顶点
+            int point2 = getPosition(edges[i].end);
+            // 获取p1这个顶点在已有最小生成树中的终点
+            int endPointOfPoint1 = getEnd(ends, point1);
+            // 获取p2这个顶点在已有最小生成树中的终点
+            int endPointOfPoint2 = getEnd(ends, point2);
+
+            // 克鲁斯卡尔核心点:判断是否构成回路
+            if (endPointOfPoint1 != endPointOfPoint2) {
+                // 假设没有构成回路
+                // 将该边上终点上的值赋值给起点位置的值
+                ends[endPointOfPoint1] = endPointOfPoint2;
+                // 将该边保存起来
+                rets[index++] = edges[i];
+            }
+        }
+        System.out.println("最小生成树为");
+        for (int i = 0; i < index; i++) {
+            System.out.println(rets[i]);
+        }
+    }
+
+}
+
+class EdgeData {
+    char start; // 边的一个点
+    char end; // 边的另外一个点
+    int weight; // 边的权值
+
+    public EdgeData(char start, char end, int weight) {
+        this.start = start;
+        this.end = end;
+        this.weight = weight;
+    }
+
+    // 重写toString, 便于输出边信息
+    @Override
+    public String toString() {
+        return "EData [<" + start + ", " + end + ">= " + weight + "]";
+    }
+}
+

迪杰斯特拉算法

+

迪杰斯特拉算法是典型最短路径算法,用于计算一个结点到其他结点的最短路径。迪杰斯特拉算法是基于贪心思想,从起始位置触发,每次寻找与起点位置距离且未访问过的顶点,以该顶点作为中间结点,更新从起点到其他顶点的距离,直到全部顶点都作为了中间结点,并完成了路径更新,算法结束。 它的主要特点是以起始点为中心向外层层扩展,即图的广度优先搜索思想,直到扩展到终点为止。

+

视频讲解:bilibili

+

数据结构与算法-046

+
public class DijkstraAlgorithmDemo {
+    public static void main(String[] args) {
+        char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
+        int[][] matrix = new int[vertex.length][vertex.length];
+        // 表示不可以连接
+        final int N = 65535;
+        matrix[0] = new int[] { N, 5, 7, N, N, N, 2 };
+        matrix[1] = new int[] { 5, N, N, 9, N, N, 3 };
+        matrix[2] = new int[] { 7, N, N, N, 8, N, N };
+        matrix[3] = new int[] { N, 9, N, N, N, 4, N };
+        matrix[4] = new int[] { N, N, 8, N, N, 5, 4 };
+        matrix[5] = new int[] { N, N, N, 4, 5, N, 6 };
+        matrix[6] = new int[] { 2, 3, N, N, 4, 6, N };
+        Graph graph = new Graph(vertex, matrix);
+        graph.showGraph();
+        graph.dsj(6);
+    }
+}
+
+class Graph {
+    private char[] vertex;
+    private int[][] matrix;
+    private VisitedVertex vv;
+
+    // 构造器
+    public Graph(char[] vertex, int[][] matrix) {
+        this.vertex = vertex;
+        this.matrix = matrix;
+    }
+
+    // 显示结果
+    public void showDijkstra() {
+        vv.showArrays();
+    }
+
+    // 显示图
+    public void showGraph() {
+        for (int[] link : matrix) {
+            for (int i : link) {
+                System.out.printf("%8d", i);
+            }
+            System.out.println();
+        }
+    }
+
+    /**
+     * 迪杰斯特拉算法实现
+     * @param index 表示出发顶点对应的下标
+     */
+    public void dsj(int index) {
+        vv = new VisitedVertex(vertex.length, index);
+        update(index);
+        vv.showArrays();
+        for (int j = 1; j < vertex.length; j++) {
+            index = vv.findNextStartPoint();
+            update(index);
+            vv.showArrays();
+        }
+    }
+
+    // 更新index下标顶点到周围顶点的距离和周围顶点的前驱顶点,
+    private void update(int index) {
+        int len = 0;
+        // 根据遍历我们的邻接矩阵的 matrix[index]行
+        for (int j = 0; j < matrix[index].length; j++) {
+            // len 含义是 : 出发顶点到index顶点的距离 + 从index顶点到j顶点的距离的和
+            len = vv.getDis(index) + matrix[index][j];
+            // 如果j顶点没有被访问过,并且 len 小于出发顶点到j顶点的距离,就需要更新
+            if (!vv.isVisited(j) && len < vv.getDis(j)) {
+                vv.updatePre(j, index);
+                vv.updateDis(j, len);
+            }
+        }
+    }
+}
+
+class VisitedVertex {
+    public int[] alreadyArr;
+    public int[] preVisited;
+    public int[] dis;
+
+    /**
+     *
+     * @param length :表示顶点的个数
+     * @param index: 出发顶点对应的下标, 比如G顶点,下标就是6
+     */
+    public VisitedVertex(int length, int index) {
+        this.alreadyArr = new int[length];
+        this.preVisited = new int[length];
+        this.dis = new int[length];
+        // 初始化 dis数组
+        Arrays.fill(dis, 65535);
+        this.dis[index] = 0;
+        this.alreadyArr[index] = 1;
+
+    }
+
+    /**
+     * 功能: 判断index顶点是否被访问过
+     * @return 如果访问过,就返回true, 否则访问false
+     */
+    public boolean isVisited(int index) {
+        return alreadyArr[index] == 1;
+    }
+
+    /**
+     * 功能: 更新出发顶点到index顶点的距离
+     */
+    public void updateDis(int index, int len) {
+        dis[index] = len;
+    }
+
+    /**
+     * 功能: 更新pre这个顶点的前驱顶点为index顶点
+     */
+    public void updatePre(int pre, int index) {
+        preVisited[pre] = index;
+    }
+
+    /**
+     * 功能:返回出发顶点到index顶点的距离
+     */
+    public int getDis(int index) {
+        return dis[index];
+    }
+
+
+    /**
+     * 继续选择并返回新的访问顶点, 比如这里的G 完后,就是 A点作为新的访问顶点(注意不是出发顶点)
+     */
+    public int findNextStartPoint() {
+        int min = 65535, index = 0;
+        for (int i = 0; i < alreadyArr.length; i++) {
+            if (alreadyArr[i] == 0 && dis[i] < min) {
+                min = dis[i];
+                index = i;
+            }
+        }
+        // 更新 index 顶点被访问过
+        alreadyArr[index] = 1;
+        return index;
+    }
+
+    //显示最后的结果
+    public void showArrays() {
+        System.out.println("核心数组的值如下:");
+        for (int i : alreadyArr) {
+            System.out.print(i + " ");
+        }
+        System.out.println();
+        for (int i : dis) {
+            System.out.print(i + " ");
+        }
+        System.out.println();
+        for (int i : preVisited) {
+            System.out.print(i + " ");
+        }
+        System.out.println();
+
+        char[] vertex = { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };
+        int count = 0;
+        for (int i : dis) {
+            if (i != 65535) {
+                System.out.print(vertex[count] + "(" + i + ") ");
+            } else {
+                System.out.print("N ");
+            }
+            count++;
+        }
+        System.out.println();
+        System.out.println();
+    }
+
+}
+

弗洛伊德算法

+

弗洛伊德算法又称为插点法,是一种利用动态规划的思想寻找给定的加权图中多源点之间最短路径的算法,与迪杰斯特拉算法类似。

+
+

迪杰斯特拉算法对比弗洛伊德算法: +迪杰斯特拉算法通过选定的被访问顶点,求出从出发访问顶点到其他顶点的最短路径;弗洛伊德算法中每一个顶点都是出发访问点,所以需要将每一个顶点看做被访问顶点,求出从每一个顶点到其他顶点的最短路径。

+
    +
  • 弗洛伊德算法计算图中各个顶点之间的最短路径
  • +
  • 迪杰斯特拉算法用于计算图中某一个顶点到其他顶点的最短路径
  • +
+
+
public class FloydAlgorithmDemo {
+    public static void main(String[] args) {
+        char[] vertex = {'A', 'B', 'C', 'D', 'E', 'F', 'G'};
+        int[][] matrix = new int[vertex.length][vertex.length];
+        final int N = 65535;
+        matrix[0] = new int[]{0, 5, 7, N, N, N, 2};
+        matrix[1] = new int[]{5, 0, N, 9, N, N, 3};
+        matrix[2] = new int[]{7, N, 0, N, 8, N, N};
+        matrix[3] = new int[]{N, 9, N, 0, N, 4, N};
+        matrix[4] = new int[]{N, N, 8, N, 0, 5, 4};
+        matrix[5] = new int[]{N, N, N, 4, 5, 0, 6};
+        matrix[6] = new int[]{2, 3, N, N, 4, 6, 0};
+
+        FloydGraph graph = new FloydGraph(vertex, vertex.length, matrix);
+        graph.floyd();
+        graph.show();
+    }
+}
+
+class FloydGraph {
+    private char[] vertex;
+    private int[][] pre;
+    private int[][] dis;
+
+    public FloydGraph(char[] vertex, int length, int[][] dis) {
+        this.vertex = vertex;
+        this.dis = dis;
+        this.pre = new int[length][length];
+        for (int i = 0; i < length; i++) {
+            Arrays.fill(pre[i], i);
+        }
+    }
+
+    public void show() {
+        for (int k = 0; k < dis.length; k++) {
+            // 先将pre数组输出的一行
+            for (int i = 0; i < dis.length; i++) {
+                System.out.print(vertex[pre[k][i]] + " ");
+            }
+            System.out.println();
+            // 输出dis数组的一行数据
+            for (int i = 0; i < dis.length; i++) {
+                System.out.print("(" + vertex[k] + "到" + vertex[i] + "的最短路径是" + dis[k][i] + ") ");
+            }
+            System.out.println();
+            System.out.println();
+        }
+    }
+
+    public void floyd(){
+        int len;
+        for (int m = 0; m < dis.length; m++) {
+            for (int a = 0; a < dis.length; a++) {
+                for (int b = 0; b < dis.length; b++) {
+                    len = dis[a][m] + dis[m][b];
+                    if (len < dis[a][b]){
+                        dis[a][b] = len;
+                        pre[a][b] = pre[m][b];
+                    }
+                }
+            }
+        }
+    }
+
+}
+

马踏棋盘算法

+

马踏棋盘算法也被称为骑士周游问题。将马随机放在国际象棋的8×8棋盘0~7的某个方格中,马按走棋规则(马走日字)进行移动。要求每个方格只进入一次,走遍棋盘上全部64个方格。

+

马踏棋盘问题实际上是图的深度优先搜索(DFS)的应用。

+
public class HouseChessBoardDemo {
+    public static void main(String[] args) {
+        HouseChessBoard houseChessBoard = new HouseChessBoard(7, 7, 2, 4);
+        houseChessBoard.showChessBoard();
+    }
+}
+
+class HouseChessBoard {
+
+    /**
+     * 表示棋盘的列
+     */
+    private int x;
+    /**
+     * 表示棋盘的行
+     */
+    private int y;
+    /**
+     * 创建一个数组,标记棋盘的各个位置是否被访问过,true表示已经访问过
+     */
+    private boolean visited[];
+    /**
+     * 使用一个属性,标记是否棋盘的所有位置都被访问 如果为true,表示成功
+     */
+    private boolean finished;
+
+    private int[][] chessboard;
+
+    public HouseChessBoard(int x, int y, int row, int column) {
+        this.x = x;
+        this.y = y;
+        this.visited = new boolean[x * y];
+        this.chessboard = new int[x][y];
+        traversalChess(this.chessboard, row-1, column-1, 1);
+    }
+
+    public void showChessBoard(){
+        for (int[] ints : this.chessboard) {
+            for (int anInt : ints) {
+                System.out.print(anInt + "\t");
+            }
+            System.out.println();
+        }
+    }
+
+    public void traversalChess(int[][] chessboard, int row, int column, int step) {
+        chessboard[row][column] = step;
+
+        // 将当前位置标记为已经访问过
+        visited[row * x + column] = true;
+
+        // 获取当前位置的下一个可走通的位置的集合
+        ArrayList<Point> nextPos = getNext(new Point(column, row));
+
+        sort(nextPos);
+
+        while (nextPos.size() > 0) {
+            // 获取当前可走通的位置
+            Point current = nextPos.remove(0);
+            // 判断当前该点是否被访问过,如果没有被访问过则继续向下访问
+            if (!visited[current.y * x + current.x]) {
+                traversalChess(chessboard, current.y, current.x, step + 1);
+            }
+        }
+
+        // 当遍历完可走的位置集合后,如果发现该路不通,则进行回溯,否则标记为完成
+        if (step < x * y && !finished) {
+            chessboard[row][column] = 0;
+            visited[row * x + column] = false;
+        } else {
+            finished = true;
+        }
+    }
+
+    // 将可走通路根据回溯次数进行从小到大排序
+    public void sort(ArrayList<Point> points){
+        points.sort((o1, o2) -> {
+            int next1 = getNext(o1).size();
+            int next2 = getNext(o2).size();
+            return next1 - next2;
+        });
+    }
+
+
+
+    // 获取下一个可走的位置
+    public ArrayList<Point> getNext(Point current) {
+        ArrayList<Point> ps = new ArrayList<Point>();
+        Point p1 = new Point();
+        // 表示马儿可以走5这个位置
+        if ((p1.x = current.x - 2) >= 0 && (p1.y = current.y - 1) >= 0) {
+            ps.add(new Point(p1));
+        }
+        // 判断马儿可以走6这个位置
+        if ((p1.x = current.x - 1) >= 0 && (p1.y = current.y - 2) >= 0) {
+            ps.add(new Point(p1));
+        }
+        // 判断马儿可以走7这个位置
+        if ((p1.x = current.x + 1) < x && (p1.y = current.y - 2) >= 0) {
+            ps.add(new Point(p1));
+        }
+        // 判断马儿可以走0这个位置
+        if ((p1.x = current.x + 2) < x && (p1.y = current.y - 1) >= 0) {
+            ps.add(new Point(p1));
+        }
+        // 判断马儿可以走1这个位置
+        if ((p1.x = current.x + 2) < x && (p1.y = current.y + 1) < y) {
+            ps.add(new Point(p1));
+        }
+        // 判断马儿可以走2这个位置
+        if ((p1.x = current.x + 1) < x && (p1.y = current.y + 2) < y) {
+            ps.add(new Point(p1));
+        }
+        // 判断马儿可以走3这个位置
+        if ((p1.x = current.x - 1) >= 0 && (p1.y = current.y + 2) < y) {
+            ps.add(new Point(p1));
+        }
+        // 判断马儿可以走4这个位置
+        if ((p1.x = current.x - 2) >= 0 && (p1.y = current.y + 1) < y) {
+            ps.add(new Point(p1));
+        }
+        return ps;
+    }
+
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/dev-idea/index.html b/blog-site/public/posts/essays/dev-idea/index.html new file mode 100644 index 00000000..0a0fdfe5 --- /dev/null +++ b/blog-site/public/posts/essays/dev-idea/index.html @@ -0,0 +1,1152 @@ + + + + + + + + + + + IDEA常用配置及使用技巧 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

IDEA常用配置及使用技巧

+ 2022.12.16 +
+

下载

+

工欲善其事必先利其器,一个好的开发工具,能极大提高开发效率.

+ +

idea常用配置

+

插件

+

一些实用的插件,能提高开发速度

+

JRebel and XRebel

+

代码调试热部署插件,使用需要花钱; 破解教程供参考.

+

启动完成需要改动代码调试,编译(快捷键: ctrl+b)一下即完成热部署,非常方便.

+

idea常用配置

+

Chinese Language Pack

+

中文语言包,对英文不太好的人很友好,根据使用习惯自行添加.

+

idea常用配置

+

Mybatis X

+

现在几乎用mybatis-plus / mybatis-plus-join 取代了mybatis,所以该插件根据需要安装吧

+

功能:

+
    +
  1. XML和Mapper跳转
  2. +
  3. Mapper和XML代码生成
  4. +
  5. Mapper单表条件查询生成XML类似JPA
  6. +
+

idea常用配置

+

EasyCode

+

该插件可替代mybatis-generator生成代码,且支持支持导入导出模板,由于集成到IDEA中使用更加方便,配置好模板(Velocity模板引擎)即可生成. 使用文档: https://gitee.com/makejava/EasyCode/wikis/pages

+

idea常用配置

+

idea常用配置

+

Translation

+

一款比较好用的翻译插件,可以使用快捷键 Ctrl+Shift+X 替换单词,从此妈妈再也不用担心变量方法命名的问题了.

+

idea常用配置

+

Auto filling Java call arguments

+

自动填充调用参数,一些方法的参数非常多,可以用这个插件提高效率,根据需要下载

+

idea常用配置

+

Codota AI Autocomplete for Java and JavaScript

+

该插件适用于 Java 和 JavaScript 的 AI 更好地完成代码,与之相关的国产有一个AiXcoder Code Completer 都挺不错的.

+

idea常用配置

+

Alibaba Java Coding Guidelines

+

可以使你写的代码不至于太烂

+
+

对于Java代码规范,业界有统一的标准,不少公司对此都有一定的要求。但是即便如此,庞大的Java使用者由于经验很水平的限制,未必有规范编码的意识,而且即便经验丰富的老Java程序员也无法做到时刻将规范牢记于心。所以对于代码规范扫描工具,一经问世就广受青睐,阿里巴巴出品的Alibaba Java Coding Guidelines(阿里巴巴Java代码规约扫描,以下简称为AJCG)插件便是其中之一.

+
+

idea常用配置

+

EasyYapi

+

公司用Yapi作为前后端项目文档,那么使用该插件可以快速导入到yapi中,操作详情查看文档: https://easyyapi.com/documents/index.html

+

idea常用配置

+

GenerateAllSetter

+

一键生成一个对象的所有set,get方法,可赋默认值,支持链式调用

+

idea常用配置

+

Git Commit Template

+

该插件最主要作用就算规范git提交信息,方便统一管理生成release note,当然也可以在项目根目录建立模板文件(git config commit.template)这种方式来进行规范,请根据具体使用场景来

+

idea常用配置

+

GitToolBox

+

GitToolBox是的git增强工具,能够帮你开始查看当前代码的提交记录。比如什么时间、谁提交的。对于快速查看代码提交记录是一款不错的工具

+

可以当前编辑行的后面显示git记录,不想看可以取消,当然如果你觉得碍眼,可以不下载.请根据使用习惯来进行下载

+

idea常用配置

+

SQL Params Setter

+

鼠标选中日志中打印的mybatis日志,右键选择 Sql Params Setter 自动将参数拼接到sql语句里,并复制到剪切板上.

+

idea常用配置

+

Key Promoter X

+

当你在IDEA里面使用鼠标的时候,如果这个鼠标操作是能够用快捷键替代的,那么Key Promoter X会弹出一个提示框,告知你这个鼠标操作可以用什么快捷键替代。对于想完全使用快捷键在IDEA的,这个插件就很有用。

+

idea常用配置

+

Maven Helper

+

Maven是个很好用的依赖管理工具,但是再好的东西也不是完美的。Maven的依赖机制会导致Jar包的冲突。举个例子,现在你的项目中,使用了两个Jar包,分别是A和B。现在A需要依赖另一个Jar包C,B也需要依赖C。但是A依赖的C的版本是1.0,B依赖的C的版本是2.0。这时候,Maven会将这1.0的C和2.0的C都下载到你的项目中,这样你的项目中就存在了不同版本的C,这时Maven会依据依赖路径最短优先原则,来决定使用哪个版本的Jar包,而另一个无用的Jar包则未被使用,这就是所谓的依赖冲突。

+

在大多数时候,依赖冲突可能并不会对系统造成什么异常,因为Maven始终选择了一个Jar包来使用。但是,不排除在某些特定条件下,会出现类似找不到类的异常,所以,只要存在依赖冲突,在我看来,最好还是解决掉,不要给系统留下隐患。

+

解决依赖冲突的方法,就是使用Maven提供的标签,标签需要放在标签内部。

+

Maven Helper插件可以帮助我们分析依赖关系,从而解决依赖冲突。

+

idea常用配置

+

Rainbow Brackets

+

不同括号不同颜色,能增加代码可读性

+

idea常用配置

+

GsonFormatPlus

+

能将json转java对象,按住alt + s然后进行配置转换

+

idea常用配置

+

配置及技巧

+

自定义模板

+

配置一些常用代码字母缩写,在输入简写时可以出现你预定义的固定模式的代码,使得开发效率大大提高,同时也可以增加个性化。例如: 输入 sout 会出现 System.out.println();

+

idea常用配置

+

Idea快捷键及设置

+
+

本人Idea设置(windows版)供参考 下载

+
+

IDEA windows 版本常用快捷键如下:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
快捷键介绍
Ctrl+Shift+V粘贴板列表
Ctrl+G转到行&列
Ctrl+F在当前文件进行文本查找
Ctrl+Y删除光标所在行或删除选中的行
Shift+Shift随处搜索,常用查找接口
Ctrl+Shift+F按照文本的内容查找,行内搜索
Ctrl+D复制当前行
Alt+Enter代码提示补全
Ctrl+Tab切换文件
Alt+Insert代码自动生成
Ctrl+Shift+L格式化代码
Ctrl+Shift+R全局重命名
Alt+鼠标左键选中修改多行
Ctrl+鼠标左键点击快速找到成员变量的出处
Ctrl+B编译(配合热部署插件使用)
Ctrl+Shift+F10运行快捷键
+

除此之外,根据自己的使用习惯,可以用[Key Promoter X](#Key Promoter X)插件来配置你自己的快捷键.

+

在这个地方可以自己设置快捷键,如果你之前用的是eclipse,那么可以使用eclipse映射的快捷键,大大降低了学习成本 +idea常用配置

+

快速发起请求

+

一般写完接口,我们会使用Postman等其他测试接口工具来发起请求,看符不符合自己的预期. 这里不是在介绍Postman,而是介绍IDEA中的一个插件,它也能做到Postman的功能,而且由于集成到了idea中使开发效率大大增加.

+

HTTP Client 是 IDEA 自带的一款简洁轻量级的接口调用插件,通过它,我们能在 IDEA 上开发、调试、测试Restful Web服务.有了它 Postman 可以扔掉了

+

idea常用配置 +idea常用配置 +idea常用配置

+

快速开发

+

配置maven项目骨架(模板),可以快速开发,可自定义项目模板,参考教程,maven骨架下载地址

+
+

Maven骨架简单的来说就是一种模型 (结构),Maven根据我们的不同的项目和需求,提供了不同的模型,这样就不需要我们自己建模型了。举个简单的例子:就比如我们要做一套普通的楼房,我们使用Maven就不需要我们自己打地基,直接把使用Maven打好的地基就可以了。同时种类的楼房(写字楼,商场,套房,别墅) 就有不同的地基,因此,Maven就有很多种模型。

+
+

配置maven骨架 +idea常用配置

+

设置自动导入包,清除无用的包,使代码更加整洁

+

idea常用配置

+

在开发一些功能时需要的某些类库 https://www.21doc.net/ 这个网站做了一个导航供参考

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/docker-start/index.html b/blog-site/public/posts/essays/docker-start/index.html new file mode 100644 index 00000000..eb364a43 --- /dev/null +++ b/blog-site/public/posts/essays/docker-start/index.html @@ -0,0 +1,568 @@ + + + + + + + + + + + Docker介绍 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Docker介绍

+ 2020.04.07 +
+

docker是什么

+

Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前流行的 Linux 容器解决方案。 +Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样。有了 Docker,就不用担心环境问题。 +总体来说,Docker 的接口相当简单,用户可以方便地创建和使用容器,把自己的应用放入容器。 +容器还可以进行版本管理、复制、分享、修改,就像管理普通的代码一样。

+

docker用途

+

提供一次性的环境。比如,本地测试他人的软件、持续集成的时候提供单元测试和构建的环境。 +提供弹性的云服务。因为 Docker 容器可以随开随关,很适合动态扩容和缩容。 +组建微服务架构。通过多个容器,一台机器可以跑多个服务,因此在本机就可以模拟出微服务架构。

+

安装

+

以linux上安装为例

+

1.安装依赖,docker依赖于系统的一些必要的工具

+
yum install -y yum-utils device-mapper-persistent-data lvm2
+

2.添加软件源, 阿里云镜像(在阿里云镜像站上面可以找到docker-ce的软件源,使用国内的源速度比较快)

+
yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
+

3.安装docker-ce(社区版,免费)

+
yum clean allyum makecache fastyum -y install docker-ce
+

4.启动服务

+
 #service 命令的用法
+$ sudo service docker start
+
+#systemctl 命令的用法
+$ sudo systemctl start docker
+

5.查看安装版

+
docker version
+

6.测试,检查 docker 是否正确安装并运行 hello-world 镜像

+
docker run hello-world
+

7.Docker 需要用户具有 sudo 权限,为了避免每次命令都输入sudo,可以把用户加入 Docker 用户组

+
sudo usermod -aG docker $USER
+

docker架构

+

docker架构分为三部分客户端,宿主机,注册中心

+

docker架构

+

例如:输入 docker run mysql:5.6命令,docker程序会从docker_host去找对应的镜像, +如果mysql不存在则会去从register下载,下载完成后docker会自动给该镜像分配一个contains,mysql就会运行起来

+

docker基本操作

+

以安装运行nginx为例

+
// docker run 包括下载镜像(pull),创建容器(create),运行容器(start) 可用dock -h查看帮助
+// --rm 表明这是一个临时的容器,关闭的话会自动删除
+// --name 容器名称
+// -p 外部服务器端口映射docker容器端口
+docker run --rm --name myNginx -p 80:80 nginx:版本
+
+// 查看容器日志
+docker logs myNginx
+
+// 进入容器
+docker exec -it myNginx bash
+

docker常用命令

+
// 查看当前运行的镜像
+docker ps
+
+//查看所有镜像
+docker ps -a
+
+//停止容器
+docker stop 容器名称
+
+// 删除镜像
+docker rmi 镜像名称
+
+//下载镜像 若不指定版本为最新版本
+docker pull 镜像:版本
+
+//查看当前本地仓库的镜像
+docker images
+
+//查看远程仓库镜像
+docker search 镜像名
+

相关链接

+ +
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/elasticsearch/index.html b/blog-site/public/posts/essays/elasticsearch/index.html new file mode 100644 index 00000000..73f2c655 --- /dev/null +++ b/blog-site/public/posts/essays/elasticsearch/index.html @@ -0,0 +1,2642 @@ + + + + + + + + + + + Elasticsearch详解 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Elasticsearch详解

+ 2023.02.14 +
+

概览

+

Elasticsearch,简称为 ES, ES是一个开源的高扩展的分布式全文搜索引擎, 是整个 ElasticStack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。

+
+

Elastic Stack, 包括 Elasticsearch、 Kibana、 Beats 和 Logstash(也称为 ELK Stack)。能够安全可靠地获取任何来源、任何格式的数据,然后实时地对数据进行搜索、分析和可视化。

+
+

Elasticsearch 是面向文档型数据库,一条数据在这里就是一个文档。 Elasticsearch 里存储文档数据和关系型数据库 MySQL 存储数据的概念进行一个类比,如图: +Elasticsearch详解-01

+

ES 里的 Index 可以看做一个库,而 Types 相当于表, Documents 则相当于表的行。这里 Types 的概念已经被逐渐弱化, Elasticsearch 6.X 中,一个 index 下已经只能包含一个type, Elasticsearch 7.X 中, Type 的概念已经被删除了。

+

官网下载地址: https://www.elastic.co/cn/downloads/past-releases

+

使用

+

与ES交互

+

索引创建

+

发送put请求

+
http://127.0.0.1:9200/_indexname
+

索引查询

+

发送get请求,查询单条

+
http://127.0.0.1:9200/_indexname
+

查询所有索引,发送get请求

+
http://127.0.0.1:9200/_cat/indices
+

索引删除

+

发送delete请求

+
http://127.0.0.1:9200/_indexname
+

文档创建修改

+

发送post请求,第一次为创建,再一次发送为修改

+
http://127.0.0.1:9200/_indexname/_docname/idstr
+

请求参数

+
{
+    "title":"华为手机",
+    "category":"小米",
+    "images":"http://www.gulixueyuan.com/xm.jpg",
+    "price":3999.00
+}
+

文档局部修改

+

发送post请求

+
http://127.0.0.1:9200/_indexname/_update/idstr
+

请求参数

+
{
+  "doc": {
+    "title":"小米手机",
+    "category":"小米"
+  }
+}
+

文档查询

+

发送get请求,查询单条

+
http://127.0.0.1:9200/_indexname/_docname/idstr
+

文档删除

+

发送delete请求

+
http://127.0.0.1:9200/_indexname/_docname/idstr
+

全查询

+

发送get请求,能看到全部数据

+
http://127.0.0.1:9200/_indexname/_search
+

url带参查询,发get请求

+
http://127.0.0.1:9200/_indexname/_search?q=category:小米
+

请求体带参查询,发送get请求

+
http://127.0.0.1:9200/_indexname/_search
+

查询category是华为和小米的,price大于2000,只显示title,显示第一页,每页显示两个,根据price降序

+
{
+  "query": {
+    "bool": {
+      "should": [{
+        "match": {
+          "category": "小米"
+        }
+      },
+        {
+          "match": {
+            "category": "华为"
+          }
+        }]
+    },
+    "filter": {
+      "range": {
+        "price": {
+          "gt": 2000
+        }
+      }
+    }
+  },
+  "_source": ["title"],
+  "from": 0,
+  "size": 2,
+  "sort": {
+    "price": {
+      "order": "desc"
+    }
+  }
+}
+

整合SpringBoot

+

pom依赖

+
    <dependency>
+        <groupId>org.elasticsearch.client</groupId>
+        <artifactId>elasticsearch-rest-high-level-client</artifactId>
+        <version>7.5.2</version>
+    </dependency>
+    <dependency>
+        <groupId>org.elasticsearch.client</groupId>
+        <artifactId>elasticsearch-rest-client</artifactId>
+        <version>7.5.2</version>
+    </dependency>
+    <dependency>
+        <groupId>org.elasticsearch</groupId>
+        <artifactId>elasticsearch</artifactId>
+        <version>7.5.2</version>
+        <exclusions>
+            <exclusion>
+                <groupId>org.elasticsearch.client</groupId>
+                <artifactId>elasticsearch-rest-client</artifactId>
+            </exclusion>
+            <exclusion>
+                <groupId>org.elasticsearch</groupId>
+                <artifactId>elasticsearch</artifactId>
+            </exclusion>
+        </exclusions>
+    </dependency>
+

application.yml

+
demo:
+  data:
+    elasticsearch:
+      cluster-name: elasticsearch
+      cluster-nodes: [127.0.0.1:1001,127.0.0.1:1002,127.0.0.1:1003]
+      index:
+        number-of-replicas: 0
+        number-of-shards: 3
+

ElasticsearchAutoConfiguration

+
/**
+ * ElasticsearchAutoConfiguration
+ *
+ * @since 2019-09-15 22:59
+ */
+@Configuration
+@RequiredArgsConstructor(onConstructor_ = @Autowired)
+@EnableConfigurationProperties(ElasticsearchProperties.class)
+public class ElasticsearchAutoConfiguration {
+
+    private final ElasticsearchProperties elasticsearchProperties;
+
+    private List<HttpHost> httpHosts = new ArrayList<>();
+
+    @Bean
+    @ConditionalOnMissingBean
+    public RestHighLevelClient restHighLevelClient() {
+
+        List<String> clusterNodes = elasticsearchProperties.getClusterNodes();
+        clusterNodes.forEach(node -> {
+            try {
+                String[] parts = StringUtils.split(node, ":");
+                Assert.notNull(parts, "Must defined");
+                Assert.state(parts.length == 2, "Must be defined as 'host:port'");
+                httpHosts.add(new HttpHost(parts[0], Integer.parseInt(parts[1]), elasticsearchProperties.getSchema()));
+            } catch (Exception e) {
+                throw new IllegalStateException("Invalid ES nodes " + "property '" + node + "'", e);
+            }
+        });
+        RestClientBuilder builder = RestClient.builder(httpHosts.toArray(new HttpHost[0]));
+
+        return getRestHighLevelClient(builder, elasticsearchProperties);
+    }
+
+
+    /**
+     * get restHistLevelClient
+     *
+     * @param builder                 RestClientBuilder
+     * @param elasticsearchProperties elasticsearch default properties
+     * @return {@link org.elasticsearch.client.RestHighLevelClient}
+     * @author fxbin
+     */
+    private static RestHighLevelClient getRestHighLevelClient(RestClientBuilder builder, ElasticsearchProperties elasticsearchProperties) {
+
+        // Callback used the default {@link RequestConfig} being set to the {@link CloseableHttpClient}
+        builder.setRequestConfigCallback(requestConfigBuilder -> {
+            requestConfigBuilder.setConnectTimeout(elasticsearchProperties.getConnectTimeout());
+            requestConfigBuilder.setSocketTimeout(elasticsearchProperties.getSocketTimeout());
+            requestConfigBuilder.setConnectionRequestTimeout(elasticsearchProperties.getConnectionRequestTimeout());
+            return requestConfigBuilder;
+        });
+
+        // Callback used to customize the {@link CloseableHttpClient} instance used by a {@link RestClient} instance.
+        builder.setHttpClientConfigCallback(httpClientBuilder -> {
+            httpClientBuilder.setMaxConnTotal(elasticsearchProperties.getMaxConnectTotal());
+            httpClientBuilder.setMaxConnPerRoute(elasticsearchProperties.getMaxConnectPerRoute());
+            return httpClientBuilder;
+        });
+
+        // Callback used the basic credential auth
+        ElasticsearchProperties.Account account = elasticsearchProperties.getAccount();
+        if (!StringUtils.isEmpty(account.getUsername()) && !StringUtils.isEmpty(account.getUsername())) {
+            final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
+
+            credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(account.getUsername(), account.getPassword()));
+        }
+        return new RestHighLevelClient(builder);
+    }
+
+}
+

ElasticsearchProperties

+
/**
+ * ElasticsearchProperties
+ *
+ * @version v1.0
+ * @since 2019-09-15 22:58
+ */
+@Data
+@Builder
+@Component
+@NoArgsConstructor
+@AllArgsConstructor
+@ConfigurationProperties(prefix = "demo.data.elasticsearch")
+public class ElasticsearchProperties {
+
+    /**
+     * 请求协议
+     */
+    private String schema = "http";
+
+    /**
+     * 集群名称
+     */
+    private String clusterName = "elasticsearch";
+
+    /**
+     * 集群节点
+     */
+    @NotNull(message = "集群节点不允许为空")
+    private List<String> clusterNodes = new ArrayList<>();
+
+    /**
+     * 连接超时时间(毫秒)
+     */
+    private Integer connectTimeout = 1000;
+
+    /**
+     * socket 超时时间
+     */
+    private Integer socketTimeout = 30000;
+
+    /**
+     * 连接请求超时时间
+     */
+    private Integer connectionRequestTimeout = 500;
+
+    /**
+     * 每个路由的最大连接数量
+     */
+    private Integer maxConnectPerRoute = 10;
+
+    /**
+     * 最大连接总数量
+     */
+    private Integer maxConnectTotal = 30;
+
+    /**
+     * 索引配置信息
+     */
+    private Index index = new Index();
+
+    /**
+     * 认证账户
+     */
+    private Account account = new Account();
+
+    /**
+     * 索引配置信息
+     */
+    @Data
+    public static class Index {
+
+        /**
+         * 分片数量
+         */
+        private Integer numberOfShards = 3;
+
+        /**
+         * 副本数量
+         */
+        private Integer numberOfReplicas = 2;
+
+    }
+
+    /**
+     * 认证账户
+     */
+    @Data
+    public static class Account {
+
+        /**
+         * 认证用户
+         */
+        private String username;
+
+        /**
+         * 认证密码
+         */
+        private String password;
+
+    }
+
+}
+

ElasticsearchConstant

+
/**
+ * ElasticsearchConstant
+ *
+ * @version v1.0
+ * @since 2019-09-15 23:03
+ */
+public interface ElasticsearchConstant {
+
+    /**
+     * 索引名称
+     */
+    String INDEX_NAME = "person";
+
+
+    /**
+     * 文档名称(字段名称)
+     */
+    String COLUMN_NAME_1 = "column_1";
+    String COLUMN_NAME_2 = "column_2";
+    String COLUMN_NAME_3 = "column_3";
+    String COLUMN_NAME_4 = "column_4";
+    String COLUMN_NAME_5 = "column_5";
+
+    /**
+     * 高亮标签
+     */
+    String TAG_HIGH_LIGHT_START = "<label style='color:red'>";
+    String TAG_HIGH_LIGHT_END = "</label>";
+
+}
+

Person

+
/**
+ * Person
+ *
+ * @version v1.0
+ * @since 2019-09-15 23:04
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Person implements Serializable {
+
+    private static final long serialVersionUID = 8510634155374943623L;
+
+    /**
+     * 主键
+     */
+    private Long id;
+
+    /**
+     * 名字
+     */
+    private String name;
+
+    /**
+     * 国家
+     */
+    private String country;
+
+    /**
+     * 年龄
+     */
+    private Integer age;
+
+    /**
+     * 生日
+     */
+    private Date birthday;
+
+    /**
+     * 介绍
+     */
+    private String remark;
+
+}
+

SearchPageHelper

+
/**
+ * @author: whitepure
+ * @date: 2023/1/6 11:25
+ * @description: SearchPageHelper
+ */
+@Data
+@Accessors(chain = true)
+public class SearchPageHelper<E> {
+
+    private Long current;
+
+    private Long pageSize;
+
+    private Long total;
+
+    private List<E> records;
+
+}
+

PersonService

+
/**
+ * PersonService
+ *
+ * @version v1.0
+ * @since 2019-09-15 23:07
+ */
+public interface PersonService {
+
+    /**
+     * create Index
+     *
+     * @param index elasticsearch index name
+     * @author fxbin
+     */
+    void createIndex(String index);
+
+    /**
+     * delete Index
+     *
+     * @param index elasticsearch index name
+     * @author fxbin
+     */
+    void deleteIndex(String index);
+
+    /**
+     * insert document source
+     *
+     * @param index elasticsearch index name
+     * @param list  data source
+     * @author fxbin
+     */
+    void insert(String index, List<Person> list);
+
+    /**
+     * update document source
+     *
+     * @param index elasticsearch index name
+     * @param list  data source
+     * @author fxbin
+     */
+    void update(String index, List<Person> list);
+
+    /**
+     * delete document source
+     *
+     * @param person delete data source and allow null object
+     * @author fxbin
+     */
+    void delete(String index, @Nullable Person person);
+
+    /**
+     * search all doc records
+     *
+     * @param index elasticsearch index name
+     * @return person list
+     * @author fxbin
+     */
+    List<Person> searchList(String index);
+
+
+    /**
+     * 分页查询
+     *
+     * @param searchRequest search condition
+     * @return search list
+     */
+    SearchPageHelper<Person> searchPage(SearchRequest searchRequest);
+
+}
+

BaseElasticsearchService

+
/**
+ * BaseElasticsearchService
+ *
+ * @version 1.0v
+ * @since 2019-09-16 15:44
+ */
+@Slf4j
+public abstract class BaseElasticsearchService {
+
+    @Resource
+    protected RestHighLevelClient client;
+
+    @Resource
+    private ElasticsearchProperties elasticsearchProperties;
+
+    protected static final RequestOptions COMMON_OPTIONS;
+
+    static {
+        RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
+
+        // 默认缓冲限制为100MB,此处修改为30MB。
+        builder.setHttpAsyncResponseConsumerFactory(new HttpAsyncResponseConsumerFactory.HeapBufferedResponseConsumerFactory(30 * 1024 * 1024));
+        COMMON_OPTIONS = builder.build();
+    }
+
+    /**
+     * create elasticsearch index (asyc)
+     *
+     * @param index elasticsearch index
+     * @author fxbin
+     */
+    protected void createIndexRequest(String index) {
+        try {
+            CreateIndexRequest request = new CreateIndexRequest(index);
+            // Settings for this index
+            request.settings(Settings.builder().put("index.number_of_shards", elasticsearchProperties.getIndex().getNumberOfShards()).put("index.number_of_replicas", elasticsearchProperties.getIndex().getNumberOfReplicas()));
+
+            CreateIndexResponse createIndexResponse = client.indices().create(request, COMMON_OPTIONS);
+
+            log.info(" whether all of the nodes have acknowledged the request : {}", createIndexResponse.isAcknowledged());
+            log.info(" Indicates whether the requisite number of shard copies were started for each shard in the index before timing out :{}", createIndexResponse.isShardsAcknowledged());
+        } catch (IOException e) {
+            throw new ElasticsearchException("创建索引 {" + index + "} 失败");
+        }
+    }
+
+    /**
+     * delete elasticsearch index
+     *
+     * @param index elasticsearch index name
+     * @author fxbin
+     */
+    protected void deleteIndexRequest(String index) {
+        DeleteIndexRequest deleteIndexRequest = buildDeleteIndexRequest(index);
+        try {
+            client.indices().delete(deleteIndexRequest, COMMON_OPTIONS);
+        } catch (IOException e) {
+            throw new ElasticsearchException("删除索引 {" + index + "} 失败");
+        }
+    }
+
+    /**
+     * build DeleteIndexRequest
+     *
+     * @param index elasticsearch index name
+     * @author fxbin
+     */
+    private static DeleteIndexRequest buildDeleteIndexRequest(String index) {
+        return new DeleteIndexRequest(index);
+    }
+
+    /**
+     * build IndexRequest
+     *
+     * @param index  elasticsearch index name
+     * @param id     request object id
+     * @param object request object
+     * @return {@link org.elasticsearch.action.index.IndexRequest}
+     * @author fxbin
+     */
+    protected static IndexRequest buildIndexRequest(String index, String id, Object object) {
+        return new IndexRequest(index).id(id).source(BeanUtil.beanToMap(object), XContentType.JSON);
+    }
+
+    /**
+     * exec updateRequest
+     *
+     * @param index  elasticsearch index name
+     * @param id     Document id
+     * @param object request object
+     * @author fxbin
+     */
+    protected void updateRequest(String index, String id, Object object) {
+        try {
+            UpdateRequest updateRequest = new UpdateRequest(index, id).doc(BeanUtil.beanToMap(object), XContentType.JSON);
+            client.update(updateRequest, COMMON_OPTIONS);
+        } catch (IOException e) {
+            throw new ElasticsearchException("更新索引 {" + index + "} 数据 {" + object + "} 失败");
+        }
+    }
+
+    /**
+     * exec deleteRequest
+     *
+     * @param index elasticsearch index name
+     * @param id    Document id
+     * @author fxbin
+     */
+    protected void deleteRequest(String index, String id) {
+        try {
+            DeleteRequest deleteRequest = new DeleteRequest(index, id);
+            client.delete(deleteRequest, COMMON_OPTIONS);
+        } catch (IOException e) {
+            throw new ElasticsearchException("删除索引 {" + index + "} 数据id {" + id + "} 失败");
+        }
+    }
+
+    /**
+     * 查询全部
+     *
+     * @param indices elasticsearch 索引名称
+     * @return {@link SearchResponse}
+     */
+    protected SearchResponse search(String... indices) {
+        SearchRequest searchRequest = new SearchRequest(indices);
+        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
+        searchSourceBuilder.query(QueryBuilders.matchAllQuery());
+        searchRequest.source(searchSourceBuilder);
+        return search(searchRequest);
+    }
+
+
+    /**
+     * 查询全部
+     *
+     * @param searchRequest 查询条件
+     * @return {@link SearchResponse}
+     */
+    protected SearchResponse search(SearchRequest searchRequest) {
+        SearchResponse searchResponse;
+        try {
+            searchResponse = client.search(searchRequest, COMMON_OPTIONS);
+        } catch (IOException e) {
+            throw new org.elasticsearch.ElasticsearchException("查询索引 %s 失败" , e, Arrays.toString(Arrays.stream(searchRequest.indices()).toArray()));
+        }
+        return searchResponse;
+    }
+}
+

PersonServiceImpl

+
/**
+ * PersonServiceImpl
+ *
+ * @version v1.0
+ * @since 2019-09-15 23:08
+ */
+@Service
+public class PersonServiceImpl extends BaseElasticsearchService implements PersonService {
+
+    @Override
+    public void createIndex(String index) {
+        createIndexRequest(index);
+    }
+
+    @Override
+    public void deleteIndex(String index) {
+        deleteIndexRequest(index);
+    }
+
+    @SneakyThrows
+    @Override
+    public void insert(String index, List<Person> list) {
+        for (Person person : list) {
+            IndexRequest request = buildIndexRequest(index, String.valueOf(person.getId()), person);
+            client.index(request, COMMON_OPTIONS);
+        }
+    }
+
+    @Override
+    public void update(String index, List<Person> list) {
+        list.forEach(person -> updateRequest(index, String.valueOf(person.getId()), person));
+    }
+
+    @Override
+    public void delete(String index, Person person) {
+        if (ObjectUtils.isEmpty(person)) {
+            // 如果person 对象为空,则删除全量
+            searchList(index).forEach(p -> {
+                deleteRequest(index, String.valueOf(p.getId()));
+            });
+        }
+        deleteRequest(index, String.valueOf(person.getId()));
+    }
+
+    @Override
+    public List<Person> searchList(String index) {
+        return toSearchList(search(index));
+    }
+
+
+    @Override
+    public SearchPageHelper<Person> searchPage(SearchRequest searchRequest) {
+        SearchResponse searchResponse = search(searchRequest);
+        TotalHits totalHits = searchResponse.getHits().getTotalHits();
+        return new SearchPageHelper<Person>()
+            .setTotal(totalHits == null ? 0 : totalHits.value)
+            .setRecords(toSearchList(searchResponse))
+            .setPageSize(20L);
+    }
+
+
+    private List<Person> toSearchList(SearchResponse searchResponse) {
+        SearchHit[] hits = searchResponse.getHits().getHits();
+        List<Person> searchList = new ArrayList<>();
+
+        Arrays.stream(hits).forEach(hit -> {
+            Map<String, Object> sourceAsMap = hit.getSourceAsMap();
+            // 处理高亮数据
+            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
+            highlightFields.forEach((k, v) -> {
+                    if (v != null && v.getFragments().length > 0) {
+                        sourceAsMap.put(k, StrUtil.strip(Arrays.toString(v.getFragments()), "[]"));
+                    }
+                }
+            );
+            Person search = BeanUtil.mapToBean(sourceAsMap, Person.class, true);
+            searchList.add(search);
+        });
+        return searchList;
+    }
+
+}
+

ElasticsearchApplicationTests

+
@RunWith(SpringRunner.class)
+@SpringBootTest
+public class ElasticsearchApplicationTests {
+
+    @Autowired
+    private PersonService personService;
+
+    /**
+     * 测试删除索引
+     */
+    @Test
+    public void deleteIndexTest() {
+        personService.deleteIndex(ElasticsearchConstant.INDEX_NAME);
+    }
+
+    /**
+     * 测试创建索引
+     */
+    @Test
+    public void createIndexTest() {
+        personService.createIndex(ElasticsearchConstant.INDEX_NAME);
+    }
+
+    /**
+     * 测试新增
+     */
+    @Test
+    public void insertTest() {
+        List<Person> list = new ArrayList<>();
+        list.add(Person.builder().age(11).birthday(new Date()).country("CN").id(1L).name("哈哈").remark("test1").build());
+        list.add(Person.builder().age(22).birthday(new Date()).country("US").id(2L).name("hiahia").remark("test2").build());
+        list.add(Person.builder().age(33).birthday(new Date()).country("ID").id(3L).name("呵呵").remark("test3").build());
+
+        personService.insert(ElasticsearchConstant.INDEX_NAME, list);
+    }
+
+    /**
+     * 测试更新
+     */
+    @Test
+    public void updateTest() {
+        Person person = Person.builder().age(33).birthday(new Date()).country("ID_update").id(3L).name("呵呵update").remark("test3_update").build();
+        List<Person> list = new ArrayList<>();
+        list.add(person);
+        personService.update(ElasticsearchConstant.INDEX_NAME, list);
+    }
+
+    /**
+     * 测试删除
+     */
+    @Test
+    public void deleteTest() {
+        personService.delete(ElasticsearchConstant.INDEX_NAME, Person.builder().id(1L).build());
+    }
+
+    /**
+     * 测试查询
+     */
+    @Test
+    public void searchListTest() {
+        List<Person> personList = personService.searchList(ElasticsearchConstant.INDEX_NAME);
+        System.out.println(personList);
+    }
+
+
+    /**
+     * 测试分页查询
+     */
+    @Test
+    public void searchPageTest(){
+        int current = 1;
+        int pageSize = 20;
+        int maxCurrent = 500;
+
+        // 此处最大页数设置为500 为es浅分页; 如需深度分页需更换分页方法并移除此条件
+        if (current > maxCurrent) {
+            return;
+        }
+
+        // 构造查询条件
+        SearchRequest searchRequest = new SearchRequest(ElasticsearchConstant.INDEX_NAME);
+        searchRequest.source(new SearchSourceBuilder()
+            .trackTotalHits(true)
+            // 查询条件
+            .query(
+                QueryBuilders.boolQuery()
+                    // and
+                    .must(QueryBuilders.termQuery(ElasticsearchConstant.COLUMN_NAME_2, true))
+                    // or
+                    .must(
+                        QueryBuilders.boolQuery()
+                            .should(QueryBuilders.matchQuery(ElasticsearchConstant.COLUMN_NAME_1, 1))
+                            .should(QueryBuilders.matchQuery(ElasticsearchConstant.COLUMN_NAME_2, 2))
+                            .should(QueryBuilders.matchQuery(ElasticsearchConstant.COLUMN_NAME_3, 3))
+                    )
+            )
+            // 分页
+            .from((current - 1) * pageSize)
+            .size(pageSize)
+            // 相关度排序: SortBuilders.scoreSort()
+            .sort(
+                // 字段排序 可根据时间
+                SortBuilders
+                    .fieldSort(ElasticsearchConstant.COLUMN_NAME_2)
+                    .order(SortOrder.DESC)
+            )
+            // 高亮字段
+            .highlighter(new HighlightBuilder()
+                .requireFieldMatch(true)
+                .preTags(ElasticsearchConstant.TAG_HIGH_LIGHT_START)
+                .field(ElasticsearchConstant.COLUMN_NAME_1)
+                .field(ElasticsearchConstant.COLUMN_NAME_2)
+                .field(ElasticsearchConstant.COLUMN_NAME_3)
+                .postTags(ElasticsearchConstant.TAG_HIGH_LIGHT_END)
+            ));
+
+        SearchPageHelper<Person> personSearchPageHelper = personService.searchPage(searchRequest);
+        System.out.println(personSearchPageHelper);
+    }
+    
+}
+

集群架构

+

一个运行中的 Elasticsearch 实例称为一个节点,而集群是由一个或者多个拥有相同 cluster.name 配置的节点组成, 它们共同承担数据和负载的压力。 当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。

+

当一个节点被选举成为主节点时, 它将负责管理集群范围内的所有变更,例如增加、 删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量的增加它也不会成为瓶颈。 任何节点都可以成为主节点。我们的示例集群就只有一个节点,所以它同时也成为了主节点。

+

作为用户,我们可以将请求发送到集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回给客户端。

+

搭建集群

+
    +
  1. +

    创建 elasticsearch-cluster 文件夹,在内部复制三个 elasticsearch 服务。 +Elasticsearch详解-02

    +
  2. +
  3. +

    修改节点配置; config/elasticsearch.yml 文件

    +
      +
    • node-1001 节点 +
      #集群名称,节点之间要保持一致
      +cluster.name: my-elasticsearch
      +#节点名称,集群内要唯一
      +node.name: node-1001
      +node.master: true
      +node.data: true
      +#ip 地址
      +network.host: localhost
      +#http 端口
      +http.port: 1001
      +#tcp 监听端口
      +transport.tcp.port: 9301
      +#discovery.seed_hosts: ["localhost:9301", "localhost:9302","localhost:9303"]
      +#discovery.zen.fd.ping_timeout: 1m
      +#discovery.zen.fd.ping_retries: 5
      +#集群内的可以被选为主节点的节点列表
      +#cluster.initial_master_nodes: ["node-1", "node-2","node-3"]
      +#跨域配置
      +#action.destructive_requires_name: true
      +http.cors.enabled: true
      +http.cors.allow-origin: "*"
      +
    • +
    • node-1002 节点 +
       #集群名称,节点之间要保持一致
      + cluster.name: my-elasticsearch
      + #节点名称,集群内要唯一
      + node.name: node-1002
      + node.master: true
      + node.data: true
      + #ip 地址
      + network.host: localhost
      + #http 端口
      + http.port: 1002
      + #tcp 监听端口
      + transport.tcp.port: 9302
      + discovery.seed_hosts: ["localhost:9301"]
      + discovery.zen.fd.ping_timeout: 1m
      + discovery.zen.fd.ping_retries: 5
      + #集群内的可以被选为主节点的节点列表
      + #cluster.initial_master_nodes: ["node-1", "node-2","node-3"]
      + #跨域配置
      + #action.destructive_requires_name: true
      + http.cors.enabled: true
      + http.cors.allow-origin: "*"
      +
    • +
    • node-1003 节点 +
      #集群名称,节点之间要保持一致
      +cluster.name: my-elasticsearch
      +#节点名称,集群内要唯一
      +node.name: node-1003
      +node.master: true
      +node.data: true
      +#ip 地址
      +network.host: localhost
      +#http 端口
      +http.port: 1003
      +#tcp 监听端口
      +transport.tcp.port: 9303
      +#候选主节点的地址,在开启服务后可以被选为主节点
      +discovery.seed_hosts: ["localhost:9301", "localhost:9302"]
      +discovery.zen.fd.ping_timeout: 1m
      +discovery.zen.fd.ping_retries: 5
      +#集群内的可以被选为主节点的节点列表
      +#cluster.initial_master_nodes: ["node-1", "node-2","node-3"]
      +#跨域配置
      +#action.destructive_requires_name: true
      +http.cors.enabled: true
      +http.cors.allow-origin: "*"
      +
    • +
    +
  4. +
  5. +

    启动集群; 点击 bin\elasticsearch.bat +Elasticsearch详解-03

    +
  6. +
+

如果启动不起来可能原因是分配内存不足,需要修改 config\jvm.options 文件中的内存属性

+

启动之后使用ES可视化工具查看,可使用elasticsearch-head,ElasticHD

+

分布式架构原理

+

ElasticSearch 设计的理念就是分布式搜索引擎,底层其实还是基于 lucene 的。核心思想就是在多台机器上启动多个 ES 进程实例,组成了一个 ES 集群。

+

ES分布式架构实际上就是对index的拆分,将index拆分成多个分片(shard),将分片分别放到不同的ES上实现集群部署.

+

Elasticsearch详解-05

+

分片优点:

+
    +
  • 支持横向扩展: 比如你数据量是 3T,3 个 shard,每个 shard 就 1T 的数据,若现在数据量增加到 4T,怎么扩展,很简单,重新建一个有 4 个 shard 的索引,将数据导进去;
  • +
  • 提高性能: 数据分布在多个 shard,即多台服务器上,所有的操作,都会在多台机器上并行分布式执行,提高了吞吐量和性能;
  • +
+

分片的数据实际上是有多个备份存在的,会存在一个主分片,还有几个副本分片. 当写入数据的时候先写入主分片,然后并行将数据同步到副本分片上;当读数据的时候会获取到所有分片,负载均衡轮询读取.

+

当某个节点宕机了,还有其他分片副本保存在其他的机器上,从而实现了高可用. 如果是非主节点宕机了,那么会由主节点,让那个宕机节点上的主分片的数据转移到其他机器上的副本数据。接着你要是修复了那个宕机机器,重启了之后,主节点会控制将缺失的副本数据分配过去,同步后续修改的数据之类的,让集群恢复正常. 如果是主节点宕机,那么会重新选举一个节点为主节点.

+

故障转移

+

在一个网络环境里,失败随时都可能发生,在某个分片/节点不知怎么的就处于离线状态,或者由于任何原因消失了,这种情况下,有一个故障转移机制是非常有用并且是强烈推荐的。为此目的,Elasticsearch 允许你创建分片的一份或多份拷贝,这些拷贝叫做复制分片。

+

当集群中只有一个节点在运行时,意味着会有一个单点故障问题——没有冗余。 幸运的是,我们只需再启动一个节点即可防止数据丢失。当你在同一台机器上启动了第二个节点时,只要它和第一个节点有同样的 cluster.name 配置,它就会自动发现集群并加入到其中。

+

ES最好部署3个以上的节点,并且配置仲裁数大于一半节点,防止master选举的脑裂问题。

+
    +
  • 当一个节点掉线,如果该节点是master节点,则通过比较node ID,选择较小ID的节点为master;
  • +
  • 然后由master节点决定分片如何重新分配。同理,新加入节点也是由master决定如何分配分片;
  • +
+
+

关于master的选举: +主要是由ZenDiscovery模块负责,包含Ping(节点之间通过这个RPC来发现彼此)和Unicast(单播模块包含-一个主机列表以控制哪些节点需要ping通)这两部分. +首先对所有可以成为master的节点(可以配置)根据nodeId排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个,暂且认为它是master节点 +如果对某个节点的投票数达到一定的值(可以成为master节点数n/2+1)并且该节点自己也选举自己,那这个节点就是master。否则重新选举一直到满足上述条件

+
+
+

ES在主节点上产生分歧,产生多个主节点,从而使集群分裂,使得集群处于异常状态。这个现象叫做脑裂。脑裂问题其实就是同一个集群的不同节点对于整个集群的状态有不同的理解,导致操作错乱,类似于精神分裂。

+
+

分片控制

+

当写入一个文档的时候,文档会被存储到一个主分片中。 Elasticsearch 集群如何知道一个文档应该存放到哪个分片中呢?

+

Elasticsearch 集群路由计算公式:

+
shard = hash(routing) % number_of_primary_shards
+

routing 是一个可变值,默认是文档的 _id ,也可以设置成一个自定义的值。 routing 通过hash 函数生成一个数字,然后这个数字再除以 number_of_primary_shards (主分片的数量)后得到余数 。这个分布在 0 到 number_of_primary_shards-1 之间的余数,就是我们所寻求的文档所在分片的位置。

+

这也就是创建索引的时候主分片的数量永远也不会改变的原因,如果数量变化了,那么所有之前路由的值都会无效,文档也再也找不到了.

+

用户可以访问任何一个节点获取数据,因为存放的规则一致(副本和主分片存放的数据一致),这个节点称之为协调节点.如果当前节点访问量较大可能被转到其他节点上,所以当发送请求的时候,为了扩展负载,更好的做法是轮询集群中所有的节点.

+

写数据流程

+

Elasticsearch详解-06

+
    +
  1. 客户端请请求任意集群节点(协调节点);
  2. +
  3. 协调节点将请求转换到指定节点(路由计算);
  4. +
  5. 主分片需要将数据保存;
  6. +
  7. 主分片将保存数据的请求发送到各个副本;
  8. +
  9. 各个副本保存后,进行响应;
  10. +
  11. 主分片进行响应;
  12. +
  13. 客户端获取响应;
  14. +
+

在客户端收到成功响应时,文档变更已经在主分片和所有副本分片执行完成,变更是安全的。有一些可选的请求参数允许您影响这个过程,可能以数据安全为代价提升性能。

+

设置 consistency 参数值会影响写入操作.consistency 参数的值可以设为:

+
    +
  • one :只要主分片状态 ok 就允许执行写操作;
  • +
  • all:必须要主分片和所有副本分片的状态没问题才允许执行写操作;
  • +
  • quorum:默认值为quorum , 即大多数的分片副本状态没问题就允许执行写操作;
  • +
+

当consistency值设置为quorum时,如果没有足够的副本分片Elasticsearch 会等待.默认情况下,它最多等待 1 分钟,可以使用timeout参数使它更早终止.

+

读数据流程

+
    +
  1. 客户端发送查询请求到协调节点;
  2. +
  3. 协调节点计算数据所在的分片及全部的副本位置,为了能负载均衡要轮询所有的分片;
  4. +
  5. 将请求转发给具体的节点;
  6. +
  7. 节点返回查询结果,将结果返回给客户端;
  8. +
+

在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。在文档被检索时,已经保存的数据可能已经存在于主分片上但是还没有复制到副本分片。 在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。 一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。

+

搜索数据过程

+
    +
  1. 客户端发送请求到一个协调节点;
  2. +
  3. 协调节点计算数据所在的分片及全部的副本位置;
  4. +
  5. 每个分片将自己的搜索结果(其实就是一些 doc id )返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果;
  6. +
  7. 接着由协调节点根据 doc id 去各个节点上拉取实际的 document 数据,最终返回给客户端;
  8. +
+

更新流程

+
    +
  1. 客户端向某一节点发送更新请求;
  2. +
  3. 将请求转发到主分片所在的节点;
  4. +
  5. 从主分片检索文档,修改_source字段中的JSON,并且尝试重新索引主分片的文档。如果文档已经被另一个进程修改,它会重试步骤3 ,超过retry_on_conflict次后放弃;
  6. +
  7. 如果主节点成功地更新文档,它将新版本的文档并行转发到副本分片,重新建立索引。一旦所有副本分片都返回成功,主节点向协调节点也返回成功,协调节点向客户端返回成功;
  8. +
+

在步骤4中,主分片把更改转发到副本分片时, 它不会转发更新请求。 相反,它转发完整文档的新版本。请记住,这些更改将会异步转发到副本分片,并且不能保证它们以发送它们相同的顺序到达。 如果 Elasticsearch 仅转发更改请求,则可能以错误的顺序应用更改,导致得到损坏的文档。

+

原理

+

分片是Elasticsearch最小的工作单元。传统的数据库每个字段存储单个值,但这对全文检索并不够。文本字段中的每个单词需要被搜索,对数据库意味着需要单个字段有索引多值的能力。最好的支持是一个字段多个值需求的数据结构是倒排索引。

+

倒排索引

+

Elasticsearch 使用一种称为 倒排索引 的结构,它适用于快速的全文搜索。一个倒排索引由文档中所有不重复词的列表构成,对于其中每个词,有一个包含它的文档列表。 倒排索引(Inverted Index)也叫反向索引,有反向索引必有正向索引。通俗地来讲,正向索引是通过key找value,反向索引则是通过value找key。

+

倒排索引示例:

+
| value    | key               |  
+|----------|-------------------|   
+| my name is zhangsan   | 1001 |  
+
| key     | value|
+|---------|------|
+| name    | 1001 |
+| zhang   | 1001 |
+| zhangsan| 1001 |
+

倒排索引搜索过程: 查询单词是否在词典中,如果不在搜索结束,如果在词典中需要查询单词在倒排列表中的指针,获取单词对应的文档ID,根据文档ID查询时哪一条数据

+

词条: 索引中最小的存储和查询单元 +词典: 词条的集合;一般用hash表或B+tree存储 +倒排表: 记录了出现过某个单词的所有文档的文档列表及单词在该文档中出现的位置信息,每条记录称为一个倒排项.根据倒排列表,即可获知哪些文档包含某个单词.

+

分析器

+

倒排索引总是和分词分不开的,中文分词和英文分词是不一样的,所以就需要分析器.

+

分析器的主要功能是将一块文本分成适合于倒排索引的独立词条,分析器组成:

+
    +
  • 字符过滤器: 在分词前整理字符串,一个字符过滤器可以用来去掉 HTML,或者将 & 转化成 and;
  • +
  • 分词器: 字符串被分词器分为单个的词条,一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条;
  • +
  • 词单元过滤器: 按顺序通过每个过滤器,这个过程可能会改变词条(例如,小写化Quick ),删除词条(例如, 像 a, and, the 等无用词),或者增加词条(例如,像jump和leap这种同义词);
  • +
+

Elasticsearch附带了可以直接使用的预包装的分析器:

+
    +
  • 标准分析器: 默认使用的分析器。它是分析各种语言文本最常用的选择。它根据Unicode 联盟定义的单词边界划分文本。删除绝大部分标点;
  • +
  • 简单分析器: 在任何不是字母的地方分隔文本,将词条小写;
  • +
  • 空格分析器: 在空格的地方划分文本;
  • +
  • 语言分析器: 考虑指定语言的特点,根据语法进行分词; 例如,英语分析器附带了一组英语无用词(常用单词,例如and或者the ,它们对相关性没有多少影响),它们会被删除;
  • +
+

常用中文分词器: ik分词器, 将解压后的后的文件夹放入 ES 根目录下的 plugins 目录下,重启 ES 即可使用.

+

文档搜索

+

早期的全文检索会为整个文档集合建立一个很大的倒排索引并将其写入到磁盘。 被写入的索引不可变化,一旦新的索引就绪,旧的就会被其替换.如果你需要让一个新的文档可被搜索,你需要重建整个索引。这要么对一个索引所能包含的数据量造成了很大的限制,要么对索引可被更新的频率造成了很大的限制。

+

如何在保留不变性的前提下实现倒排索引的更新?

+

用更多的索引。通过增加新的补充索引来反映新的修改,而不是直接重写整个倒排索引。每一个倒排索引都会被轮流查询到,从最早的开始查询完后再对结果进行合并。

+

当一个文档被删除时,它实际上只是在文件中被标记删除。一个被标记删除的文档仍然可以被查询匹配到,但它会在最终结果被返回前从结果集中过滤掉。 文档更新也是类似的操作方式:当一个文档被更新时,旧版本文档被标记删除,文档的新版本被索引到一个新的段中。可能两个版本的文档都会被一个查询匹配到,但被删除的那个旧版本文档在结果集返回前就已经被移除。 +当一个查询被触发,所有已知的段按顺序被查询。词项统计会对所有段的结果进行聚合,此时会将标记删除的数据真正的删除.

+

近实时搜索

+

Elasticsearch 的主要功能就是搜索,但是Elasticsearch的搜索功能不是实时的,而是近实时的,主要原因在于ES搜索是分段搜索.

+

ES中的每一段就是一个倒排索引,最新的数据更新会体现在最新的段中,而最新的段落盘之后ES才能进行搜索,所以磁盘性能极大影响了ES软件的搜索.ES的主要作用就是快速准确的获取想要的数据,所以降低处理数据的延迟就显得尤为重要.

+

ES近实时搜索实现: +Elasticsearch详解-04

+
    +
  1. 一个文档被索引之后,就会被添加到内存缓冲区,并且追加到了 translog 事务日志中;(先写入索引中,再写入到日志中,目的防止数据丢失,类似数据库中的事务日志)
  2. +
  3. 将内存缓冲区中的分片刷新到磁盘中(refresh);此时缓冲区的数据可被搜索,当完全将数据写入磁盘会清空缓冲区中的数据;
  4. +
  5. 随着不断的刷写,磁盘中的文件会越来越多,此时需要文件段合并;当一个新的索引文件产生之后,文件的更新,删除便会体现出,此时在合并文件的时候便会真正的将数据删除;小的段被合并到大的段,然后这些大的段再被合并到更大的段;
  6. +
+

优化

+

Elasticsearch 在数据量很大的情况下(数十亿级别)如何提高查询效率?

+

合理设置分片数

+

分片和副本的设计为 ES 提供了支持分布式和故障转移的特性,但并不意味着分片和副本是可以无限分配的。而且索引的分片完成分配后由于索引的路由机制,我们是不能重新修改分片数的.否则将无法找到对应的数据.

+
    +
  • 控制每个分片占用的硬盘容量不超过 ES 的最大 JVM 的堆空间设置,因此,如果索引的总容量在 500G 左右,那分片大小在 16 个左右即可;
  • +
  • 考虑一下 node 数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了 1 个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以, 一般都设置分片数不超过节点数的 3 倍;
  • +
  • 主分片,副本和节点最大数之间数量,我们分配的时候可以参考: 节点数<=主分片数 *(副本数+1);
  • +
+

文件缓冲区

+

往 ES 里写的数据,实际上都写到磁盘文件里去了,查询的时候,操作系统会将磁盘文件里的数据自动缓存到 filesystem cache 里面去;ES 的搜索引擎严重依赖于底层的 filesystem cache ,你如果给 filesystem cache 更多的内存,尽量让内存可以容纳所有的 idx segment file 索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。

+
+

案例: 某个公司 ES 节点有 3 台机器,每台机器看起来内存很多,64G,总内存就是 64 * 3 = 192G 。每台机器给 ES jvm heap 是 32G ,那么剩下来留给 filesystem cache 的就是每台机器才 32G ,总共集群里给 filesystem cache 的就是 32 * 3 = 96G 内存。而此时,整个磁盘上索引数据文件,在 3 台机器上一共占用了 1T 的磁盘容量,ES 数据量是 1T ,那么每台机器的数据量是 300G 。这样性能好吗? filesystem cache 的内存才 100G,十分之一的数据可以放内存,其他的都在磁盘,然后你执行搜索操作,大部分操作都是走磁盘,性能肯定差。

+
+

归根结底,你要让 ES 性能要好,最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半。

+

根据生产环境实践经验,最佳的情况下,是仅仅在 ES 中就存少量的数据,就是你要用来搜索的那些索引,如果内存留给 filesystem cache 的是 100G,那么你就将索引数据控制在 100G 以内,这样的话,你的数据几乎全部走内存来搜索,性能非常之高,一般可以在 1 秒以内。

+

比如说你现在有一行数据。 id,name,age …. 30 个字段。但是你现在搜索,只需要根据 id,name,age 三个字段来搜索。如果你傻乎乎往 ES 里写入一行数据所有的字段,就会导致说 90% 的数据是不用来搜索的,结果硬是占据了 ES 机器上的 filesystem cache 的空间,单条数据的数据量越大,就会导致 filesystem cahce 能缓存的数据就越少。其实,仅仅写入 ES 中要用来检索的少数几个字段就可以了,比如说就写入 ES id,name,age 三个字段,然后你可以把其他的字段数据存在 mysql/hbase 里,我们一般是建议用 ES + hbase 这么一个架构。

+

写入 ES 的数据最好小于等于,或者是略微大于 ES 的 filesystem cache 的内存容量。然后你从 ES 检索可能就花费 20ms,然后再根据 ES 返回的 id 去 hbase 里查询,查 20 条数据,可能也就耗费个 30ms,可能你原来那么玩儿,1T 数据都放 es,会每次查询都是 5~10s,现在可能性能就会很高,每次查询就是 50ms。

+

数据预热

+

假如说,哪怕是你就按照上述的方案去做了,ES 集群中每个机器写入的数据量还是超过了 filesystem cache 一倍,比如说你写入一台机器 60G 数据,结果 filesystem cache 就 30G,还是有 30G 数据留在了磁盘上。

+

其实可以做数据预热。

+

举个例子,拿微博来说,你可以把一些大 V,平时看的人很多的数据,你自己提前后台搞个系统,每隔一会儿,自己的后台系统去搜索一下热数据,刷到 filesystem cache 里去,后面用户实际上来看这个热数据的时候,他们就是直接从内存里搜索了,很快。

+

或者是电商,你可以将平时查看最多的一些商品,比如说 iphone 8,热数据提前后台搞个程序,每隔 1 分钟自己主动访问一次,刷到 filesystem cache 里去。

+

对于那些你觉得比较热的、经常会有人访问的数据,最好做一个专门的缓存预热子系统,就是对热数据每隔一段时间,就提前访问一下,让数据进入 filesystem cache 里面去。这样下次别人访问的时候,性能一定会好很多。

+

冷热分离

+

ES 可以做类似于 mysql 的水平拆分,就是说将大量的访问很少、频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引。最好是将冷数据写入一个索引中,然后热数据写入另外一个索引中,这样可以确保热数据在被预热之后,尽量都让他们留在 filesystem os cache 里,别让冷数据给冲刷掉。

+

假设你有 6 台机器,2 个索引,一个放冷数据,一个放热数据,每个索引 3 个 shard。3 台机器放热数据 index,另外 3 台机器放冷数据 index。然后这样的话,你大量的时间是在访问热数据 index,热数据可能就占总数据量的 10%,此时数据量很少,几乎全都保留在 filesystem cache 里面了,就可以确保热数据的访问性能是很高的。但是对于冷数据而言,是在别的 index 里的,跟热数据 index 不在相同的机器上,大家互相之间都没什么联系了。如果有人访问冷数据,可能大量数据是在磁盘上的,此时性能差点,就 10% 的人去访问冷数据,90% 的人在访问热数据,也无所谓了。

+

document模型设计

+

对于 MySQL,我们经常有一些复杂的关联查询。在 ES 里该怎么玩儿,ES 里面的复杂的关联查询尽量别用,一旦用了性能一般都不太好。

+

最好是先在 Java 系统里就完成关联,将关联好的数据直接写入 ES 中。搜索的时候,就不需要利用 ES 的搜索语法来完成 join 之类的关联搜索了。

+

document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。ES 能支持的操作就那么多,不要考虑用 ES 做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/eye-beam/index.html b/blog-site/public/posts/essays/eye-beam/index.html new file mode 100644 index 00000000..7fa681ac --- /dev/null +++ b/blog-site/public/posts/essays/eye-beam/index.html @@ -0,0 +1,3138 @@ + + + + + + + + + + + 常见故障排查及程序配置 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

常见故障排查及程序配置

+ 2021.09.08 +
+

故障排查基础

+

收录Linux常用命令,以下命令来自https://www.bilibili.com/video/BV14A411378a

+

关机/重启/注销

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
常用命令作用
shutdown -h now即刻关机
shutdown -h 1010分钟后关机
shutdown -h 11:0011:00关机
shutdown -h +10预定时间关机(10分钟后)
shutdown -c取消指定时间关机
shutdown -r now重启
shutdown -r 1010分钟之后重启
shutdown -r 11:00定时重启
reboot重启
init 6重启
init 0⽴刻关机
telinit 0关机
poweroff⽴刻关机
halt关机
syncbuff数据同步到磁盘
logout退出登录Shell
+

系统信息和性能查看

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
常用命令作用
uname -a查看内核/OS/CPU信息
uname -r查看内核版本
uname -m查看处理器架构
arch查看处理器架构
hostname查看计算机名
who显示当前登录系统的⽤户
who am i显示登录时的⽤户名
whoami显示当前⽤户名
cat /proc/version查看linux版本信息
cat /proc/cpuinfo查看CPU信息
cat /proc/interrupts查看中断
cat /proc/loadavg查看系统负载
uptime查看系统运⾏时间、⽤户数、负载
env查看系统的环境变量
lsusb -tv查看系统USB设备信息
lspci -tv查看系统PCI设备信息
lsmod查看已加载的系统模块
grep MemTotal /proc/meminfo查看内存总量
grep MemFree /proc/meminfo查看空闲内存量
free -m查看内存⽤量和交换区⽤量
date显示系统⽇期时间
cal 2021显示2021⽇历表
top动态显示cpu/内存/进程等情况
vmstat 1 20每1秒采⼀次系统状态,采20次
iostat查看io读写/cpu使⽤情况
查看io读写/cpu使⽤情况查询cpu使⽤情况(1秒⼀次,共10次)
sar -d 1 10查询磁盘性能
+

磁盘和分区

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
常用命令作用
fdisk -l查看所有磁盘分区
swapon -s查看所有交换分区
df -h查看磁盘使⽤情况及挂载点
df -hl同上
du -sh /dir查看指定某个⽬录的⼤⼩
du -sk * | sort -rn从⾼到低依次显示⽂件和⽬录⼤⼩
mount /dev/hda2 /mnt/hda2挂载hda2盘
mount -t ntfs /dev/sdc1 /mnt/usbhd1指定⽂件系统类型挂载(如ntfs)
mount -o loop xxx.iso /mnt/cdrom挂 载 iso ⽂ 件
umount -v /dev/sda1通过设备名卸载
umount -v /mnt/mymnt通过挂载点卸载
fuser -km /mnt/hda1强制卸载(慎⽤)
+

⽤户和⽤户组

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
常用命令作用
useradd codesheep创建⽤户
userdel -r codesheep删除⽤户
usermod -g group_name user_name修改⽤户的组
usermod -aG group_name user_name将⽤户添加到组
usermod -s /bin/ksh -d /home/codepig –g dev codesheep修改⽤户codesheep的登录Shell、主⽬录以及⽤户组
groups test查看test⽤户所在的组
groupadd group_name创建⽤户组
groupdel group_name删除⽤户组
groupmod -n new_name old_name重命名⽤户组
su - user_namesu - user_name
passwd修改⼝令
passwd codesheep修改某⽤户的⼝令
w查看活动⽤户
id codesheep查看指定⽤户codesheep信息
last查看⽤户登录⽇志
crontab -l查看当前⽤户的计划任务
cut -d: -f1 /etc/passwd查看系统所有⽤户
cut -d: -f1 /etc/group查看系统所有组
+

⽹络和进程管理

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
常用命令作用
ifconfig查看⽹络接⼝属性
ifconfig eth0查看某⽹卡的配置
route -n查看路由表
netstat -lntp查看所有监听端⼝
netstat -antp查看已经建⽴的TCP连接
netstat -lutp查看TCP/UDP的状态信息
ifup eth0启⽤eth0⽹络设备
ifdown eth0禁⽤eth0⽹络设备
iptables -L查看iptables规则
ifconfig eth0 192.168.1.1 netmask 255.255.255.0配置ip地址
dhclient eth0以dhcp模式启⽤eth0
route add -net 0/0 gw Gateway_IP配置默认⽹关
route add -net 192.168.0.0 netmask 255.255.0.0 gw 192.168.1.1配置静态路由到达⽹络'192.168.0.0/16'
route del 0/0 gw Gateway_IP删除静态路由
hostname查看主机名
host www.baidu.com解析主机名
nslookup www.baidu.com查询DNS记录,查看域名解析是否正常
ps -ef查看所有进程
ps -ef | grep codesheep过滤出你需要的进程
kill -s namekill指定名称的进程
kill -s pidkill指定pid的进程
top实时显示进程状态
vmstat 1 20每1秒采⼀次系统状态,采20次
iostatiostat
sar -u 1 10查询cpu使⽤情况(1秒⼀次,共10次)
sar -d 1 10查询磁盘性能
+

常⻅系统服务命令

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
常用命令作用
chkconfig –list列出系统服务
service <服务名> status查看某个服务
service <服务名> start启动某个服务
service <服务名> stop终⽌某个服务
service <服务名> restart重启某个服务
systemctl status <服务名>查看某个服务
systemctl start <服务名>启动某个服务
systemctl stop <服务名>终⽌某个服务
systemctl restart <服务名>重启某个服务
systemctl enable <服务名>关闭⾃启动
systemctl disable <服务名>关闭⾃启动
+

⽂件和⽬录操作

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
常用命令作用
cd <⽬录名>进⼊某个⽬录
cd ..回上级⽬录
cd ../..回上两级⽬录
cd进个⼈主⽬录
cd -回上⼀步所在⽬录
pwd显示当前路径
ls查看⽂件⽬录列表
ls -F查看⽬录中内容(显示是⽂件还是⽬录)
ls -l查看⽂件和⽬录的详情列表
ls -a查看隐藏⽂件
ls -lh查看⽂件和⽬录的详情列表(增强⽂件⼤⼩易读性)
ls -lSr查看⽂件和⽬录列表(以⽂件⼤⼩升序查看)
tree查看⽂件和⽬录的树形结构
mkdir <⽬录名>创建⽬录
mkdir dir1 dir2同时创建两个⽬录
mkdir -p /tmp/dir1/dir2创建⽬录树
rm -f file1删除’file1’⽂件
rmdir dir1删除’dir1’⽬录
rm -rf dir1删除’dir1’⽬录和其内容
rm -rf dir1 dir2同时删除两个⽬录及其内容
mv old_dir new_dir重命名/移动⽬录
cp file1 file2复制⽂件
cp dir/* .复制某⽬录下的所有⽂件⾄当前⽬录
cp -a dir1 dir2复制⽬录
cp -a /tmp/dir1 .复制⼀个⽬录⾄当前⽬录
ln -s file1 link1创建指向⽂件/⽬录的软链接
ln file1 lnk1创建指向⽂件/⽬录的物理链接
find / -name file1从跟⽬录开始搜索⽂件/⽬录
find / -user user1搜索⽤户user1的⽂件/⽬录
find /dir -name *.bin在⽬录/dir中搜带有.bin后缀的⽂件
locate <关键词>快速定位⽂件
locate *.mp4寻找.mp4结尾的⽂件
whereis <关键词>显示某⼆进制⽂件/可执⾏⽂件的路径
which <关键词>查找系统⽬录下某的⼆进制⽂件
chmod ugo+rwx dir1设置⽬录所有者(u)、群组(g)及其他⼈(o)的读(r)写(w)执⾏(x)权限
chmod go-rwx dir1移除群组(g)与其他⼈(o)对⽬录的读写执⾏权限
chown user1 file1改变⽂件的所有者属性
chown -R user1 dir1改变⽬录的所有者属性
chgrp group1 file1改变⽂件群组
chown user1:group1 file1改变⽂件的所有⼈和群组
+

⽂件查看和处理

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
常用命令作用
cat file1查看⽂件内容
cat -n file1查看内容并标示⾏数
tac file1从最后⼀⾏开始反看⽂件内容
more file1more file1
less file1类似more命令,但允许反向操作
head -2 file1查看⽂件前两⾏
tail -2 file1查看⽂件后两⾏
tail -f /log/msg实时查看添加到⽂件中的内容
grep codesheep hello.txt在⽂件hello.txt中查找关键词codesheep
grep ^sheep hello.txt在⽂件hello.txt中查找以sheep开头的内容
grep [0-9] hello.txt选择hello.txt⽂件中所有包含数字的⾏
sed ’s/s1/s2/g’ hello.txt将hello.txt⽂件中的s1替换成s2
sed ‘/^$/d’ hello.txt从hello.txt⽂件中删除所有空⽩⾏
sed ‘/ *#/d; /^$/d’ hello.txt从hello.txt⽂件中删除所有注释和空⽩⾏
sed -e ‘1d’ hello.txt从⽂件hello.txt 中排除第⼀⾏
sed -n ‘/s1/p’ hello.txt查看只包含关键词"s1"的⾏
sed -e ’s/ *$//’ hello.txt删除每⼀⾏最后的空⽩字符
sed -e ’s/s1//g’ hello.txt从⽂档中只删除词汇s1并保留剩余全部
sed -n ‘1,5p;5q’ hello.txt查看从第⼀⾏到第5⾏内容
sed -n ‘5p;5q’ hello.txt查看第5⾏
paste file1 file2合并两个⽂件或两栏的内容
paste -d ‘+’ file1 file2合并两个⽂件或两栏的内容,中间⽤"+“区分
sort file1 file2排序两个⽂件的内容
comm -1 file1 file2⽐较两个⽂件的内容(去除’file1’所含内容)
comm -2 file1 file2⽐较两个⽂件的内容(去除’file2’所含内容
comm -3 file1 file2⽐较两个⽂件的内容(去除两⽂件共有部分)
+

打包和解压

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
常用命令作用
zip xxx.zip file压缩⾄zip包
zip -r xxx.zip file1 file2 dir1将多个⽂件+⽬录压成zip包
unzip xxx.zip解压zip包
tar -cvf xxx.tar file创建⾮压缩tar包
tar -cvf xxx.tar file1 file2 dir1将多个⽂件+⽬录打tar包
tar -tf xxx.tar查看tar包的内容
tar -xvf xxx.tar解压tar包
tar -xvf xxx.tar -C /dir将tar包解压⾄指定⽬录
tar -cvfj xxx.tar.bz2 dir创建bz2压缩包
tar -jxvf xxx.tar.bz2解压bz2压缩包
tar -cvfz xxx.tar.gz dir创建gzip压缩包
tar -zxvf xxx.tar.gz解压gzip压缩包
bunzip2 xxx.bz2解压bz2压缩包
bzip2 filename压缩⽂件
gunzip xxx.gz解压gzip压缩包
gzip filename压缩⽂件
gzip -9 filename最⼤程度压缩
+

RPM包管理命令

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
常用命令作用
rpm -qa查看已安装的rpm包
rpm -q pkg_name查询某个rpm包
rpm -q –whatprovides xxx显示xxx功能是由哪个包提供的
rpm -q –whatrequires xxx显示xxx功能被哪个程序包依赖的
rpm -q –changelog xxx显示xxx包的更改记录
rpm -qi pkg_name查看⼀个包的详细信息
rpm -qd pkg_name查询⼀个包所提供的⽂档
rpm -qc pkg_name查看已安装rpm包提供的配置⽂件
rpm -ql pkg_name查看⼀个包安装了哪些⽂件
rpm -qf filename查看某个⽂件属于哪个包
rpm -qR pkg_name查询包的依赖关系
rpm -ivh xxx.rpm安装rpm包
rpm -ivh –test xxx.rpm测试安装rpm包
rpm -ivh –nodeps xxx.rpm安装rpm包时忽略依赖关系
rpm -e xxx卸载程序包
rpm -Fvh pkg_name升级确定已安装的rpm包
rpm -Uvh pkg_name升级rpm包(若未安装则会安装)
rpm -V pkg_nameRPM包详细信息校验
+

YUM包管理命令

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
常用命令作用
yum repolist enabled显示可⽤的源仓库
yum search pkg_name搜索软件包
yum install pkg_name下载并安装软件包
yum install –downloadonly pkg_name只下载不安装
yum list显示所有程序包
yum list installed查看当前系统已安装包
yum list updates查看可以更新的包列表
yum check-update查看可升级的软件包
yum update更新所有软件包
yum update pkg_name升级指定软件包
yum deplist pkg_name列出软件包依赖关系
yum remove pkg_name删除软件包
yum clean all清除缓存
yum clean packages清除缓存的软件包
yum clean headers清除缓存的header
+

DPKG包管理命令

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
常用命令作用
dpkg -c xxx.deb列出deb包的内容
dpkg -i xxx.deb安装/更新deb包
dpkg -r pkg_name移除deb包
dpkg -P pkg_name移除deb包(不保留配置)
dpkg -l查看系统中已安装deb包
dpkg -l pkg_name显示包的⼤致信息
dpkg -L pkg_name查看deb包安装的⽂件
dpkg -s pkg_name查看包的详细信息
dpkg –unpack xxx.deb解开deb包的内容
+

APT软件⼯具

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
常用命令作用
apt-cache search pkg_name搜索程序包
apt-cache show pkg_name获取包的概览信息
apt-get install pkg_name安装/升级软件包
apt-get purge pkg_name卸载软件(包括配置)
apt-get remove pkg_name卸载软件(不包括配置)
apt-get update更新包索引信息
apt-get upgrade更新已安装软件包
apt-get clean清理缓存
+

分析工具

+

JDK自带分析工具

+

参考文章:

+ +

jps

+

jps查询系统内所有HotSpot进程,它位于java的bin目录下。

+ + + + + + + + + + + + + + + + + + + + + + + + + +
命令含义
jps输出当前运行主类名称,进程ID
jps -q只列出进程ID
jps -l输出当前运行主类的全称,进程ID
jps -v输出虚拟机进程启动时JVM参数
+

jstat

+

jstat是JDK自带的一个轻量级小工具。全称“Java Virtual Machine statistics monitoring tool”,和jps一样,都在bin目录下。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
命令含义
jstat -gc vmid 1000 10查看进程pidGC信息,每1000毫秒 输出一次,输出10次
jstat -gccause vmid 1000 10查看进程pidGC发生的原因,每一秒(1000毫秒)输出一次,输出10次
jstat -class vmid查看pid的加载类信息
jstat -gcutil vmidjava垃圾回收信息的统计
jstat -gcnew vmid显示新生代GC的情况
jstat -gcold vmid显示老年代GC的情况
+

jinfo

+

jinfo查看虚拟机参数信息,也可用于调整虚拟机配置参数。我们通过jinfo --help能看到相应的参数。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
命令含义
jinfo pid输出关于pid的一堆相关信息
jinfo -flags pid查看当前进程曾经赋过值的一些参数
jinfo -flag name pid查看指定进程的JVM参数名称的参数的值
jinfo -flag [+-]name pid开启或者关闭指定进程对应名称的JVM参数
jinfo -sysprops pid来输出当前 JVM进行的全部的系统属性
+

当使用jinfo进行修改对应进程JVM参数时,有一定的局限性。并不是所有的参数都支持修改,只有参数被标记为manageable的参数才可以被实时修改。

+

可以使用命令查看被标记为manageable的参数:java -XX:+PrintFlagsFinal -version | grep manageable

+

jmap

+

jmap全称:Java Memory Map,主要用于打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。jmap以生成 java程序的dump文件, 也可以查看堆内对象示例的统计信息、查看ClassLoader 的信息以及 finalizer 队列。

+

jmap命令可以获得运行中的JVM的堆的快照,从而可以离线分析堆,以检查内存泄漏,检查一些严重影响性能的大对象的创建,检查系统中什么对象最多,各种对象所占内存的大小等等。可以使用jmap生成Heap Dump。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
命令含义
jmap -heap pid输出整个堆详细信息,包括GC的使用、堆的配置信息,以及内存的使用信息
jmap -histo:live pid输出堆中对象的相关统计信息;第一列是序号,第二列是对象个数,第三列是对象大小byte,第四列是class name
jmap -finalizerinfo pid输出等待终结的对象信息
jmap -clstats pid输出类加载器信息
jmap -dump:[live],format=b,file=filename.hprof pid把进程堆内存使用情况生成到堆转储dump文件中,live子选项是可选的,假如指定live选项,那么只输出活的对象到文件。dump文件主要作用,如果发生溢出可以使用dump文件分析是哪些数据导致的
+
+

Heap Dump又叫堆转储文件,指一个java进程在某一个时间点的内存快照文件。Heap Dump在触发内存快照的时候会保存以下信息:

+
    +
  • 所有的对象
  • +
  • 所有的class
  • +
  • GC Roots
  • +
  • 本地方法栈和本地变量
  • +
+

通常在写Dump文件前会触发一次Full GC,所以Heap Dump文件里保存的对象都是Full GC后保留的对象信息。 +由于生成dump文件比较耗时,所以请耐心等待,尤其是大内存镜像生成的dump文件,则需要更长的时间来完成。

+
+

可以通过参数配置当发生OOM时自动生成dump文件:-XX:+HeapDumpOnOutOfMemeryError -XX:+HeapDumpPath=<filename.hprof>,当然此种方式获取dump文件较大,如果想要获取dump文件较小可以手动获取dump文件并指定只获取存活的对象。

+

jhat

+

JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump文件,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,可以在浏览器中查看。在此要注意,一般不会直接在服务器上进行分析,因为jhat是一个耗时并且耗费硬件资源的过程,一般把服务器生成的dump文件复制到本地或其他机器上进行分析。

+

注意,jhat在jdk9中已经移除,官方对贱使用visualvm来配置jmap进行分析。

+ + + + + + + + + + + + + + + + + + + + + +
命令含义
jhat -port 9998 /tmp/dump.dat配合jmap命令使用,查看导出的/tmp/dump.dat文件,端口为9998;注意如果dump文件太大,可能需要加上-J-Xmx512m这种参数指定最大堆内存,即jhat -J-Xmx512m -port 9998 /tmp/dump.dat
jhat -baseline dump2.phrof dump1.phrof对比dump2.phrof dump1.phrof文件
jhat heapDump分析dump文件,默认端口为7000
+

jstack

+

jstack,全称JVM Stack Trace栈空间追踪,用于生成虚拟机指定进程当前线程快照;主要分析堆栈空间,也就是分析线程的情况,可以分析出死锁问题,以及cpu100%的问题。jstack可以定位到线程堆栈,根据堆栈信息我们可以定位到具体代码,所以它在JVM性能调优中使用得非常多。

+
+

jstack主要用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因, +如线程间死锁、死循环、请求外部资源导致的长时间等待等。

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
命令含义
jstack pid打印出所有的线程,包括用户自己启动的线程和JVM后台线程
jstack 13324 >1.txt将13324进程中线程信息写入到1.txt文件中
jstack 21711|grep 54ee在进程21711中查找线程ID为54ee(16进制)的信息
jstack -l pid除了堆栈信息外-l参数会显示线程锁的附加信息
+

除了可以使用jstack打印栈的信息,在java层面也可以使用Thread.getAllStackTraces()方法获取堆栈信息。

+

jcmd

+

在JDK1.7之后,新增了一个命令行工具jcmd。

+

它是一个多功能的工具,可以实现前面除了jstat之外的所有功能。例如,导出dump文件、查看线程信息、导出线程信息、执行GC,JVM运行时间等。

+

jcmd拥有jmap的大部分功能,并且在官方网站上也推荐使用jcmd代替jmap。

+ + + + + + + + + + + + + + + + + + + + + +
命令含义
jcmd -l列出所有JVM的进程
jcmd pid help针对指定进程罗列出可执行的命令
jcmd pid <具体命令>显示指定进程的指令命令的数据
+

GUI分析工具

+

jconsole

+

JConsole 是一个内置 Java 性能分析器,可以从命令行(直接输入jconsole)或在 GUI shell (jdk\bin下打开)中运行。

+

它用于对JVM中内存,线程和类等的监控。这款工具的好处在于,占用系统资源少,而且结合Jstat,可以有效监控到java内存的变动情况,以及引起变动的原因。在项目追踪内存泄露问题时,很实用。

+

常见故障排查及程序配置-002

+

常见故障排查及程序配置-001

+

常见故障排查及程序配置-003

+

visual vm

+

visual vm 是一个功能强大的多合一故障诊断和性能监控的可视化工具。它集成了多个JDK命令行工具,使用visual vm可用于显示虚拟机进程及进程的配置和环境信息,监视应用程序的CPU、GC、堆、方法区及线程的信息等,甚至代替jconsole。

+

在JDK7,visual vm便作为JDK的一部分发布,在JDK的bin目录下,即:它完全免费。此外,visual vm也可以作为独立软件进行安装。

+

主要功能:

+
    +
  • 生成读取dump文件
  • +
  • 查看JVM参数和系统属性
  • +
  • 查看运行中虚拟机进程
  • +
  • 生成读取线程快照
  • +
  • 程序资源的实时监控
  • +
+

visual vm 支持插件扩展,可以在visual vm上安装插件,也可以将visual vm安装在idea上:

+

常见故障排查及程序配置-006

+

常见故障排查及程序配置-007

+

visual vm可以生成dump文件,生成的dump文件是临时的,如果想要保留该文件需要右键另存为即可:

+

常见故障排查及程序配置-004

+

常见故障排查及程序配置-005

+

如果堆文件数据较大,排查起来很困难,可以使用OQL语句进行筛选。

+
+

OQL:全称,Object Query Language 类似于SQL查询的一种语言,OQL使用SQL语法,可以在堆中进行对象的筛选。

+

基本语法:

+
select <JavaScript expression to select> 
+[ from (instanceof) <class name> <identifier>
+( where <JavaScript boolean expression to filter> ) ]
+

1.class name是java类的完全限定名 +2.instanceof表示也查询某一个类的子类 +3.from和where子句都是可选的 +4.可以使用obj.field_name语法访问Java字段

+

例如

+
-- 查询长度大于等于100的字符串
+select s from java.lang.String s where s.value.length >= 100
+
+-- 显示所有File对象的文件路径 
+select file.path.value.toString() from java.io.File file
+
+-- 显示由给定id字符串标识的Class的实例
+select o from instanceof 0x741012748 o
+
+

visual vm也可以将两个dump文件进行比较:

+

常见故障排查及程序配置-008

+

visual vm不但可以生成堆的dump文件,也可以对线程dump:

+

常见故障排查及程序配置-009

+

eclipse MAT

+

MAT全称,Memory Analyzer Tool 是一款功能强大的Java堆内存分析器。可以用于查找内存泄漏以及查看内存消耗情况。

+

MAT是eclipse开发的,不仅可以单独使用,还可以作为插件嵌入在eclipse中使用。是一款免费的性能分析工具,使用起来很方便。

+

MAT的主要功能就是分析dump文件。分析dump最终目的是为了找出内存泄漏的疑点,防止内存泄漏。

+

JVM内存包含信息:

+
    +
  • 所有对象信息,包括对象实例、成员变量、存储于栈中的基本数据类型和存储于堆中的其他对象的引用值;
  • +
  • 所有的类信息,包括classloader、类名称、父类的信息、静态变量等;
  • +
  • GCRoot到所有的这些对象的引用路径;
  • +
  • 线程信息,包括线程的调用栈及线程的局部变量;
  • +
+

常见获取dump文件方式:

+
    +
  • 通过jmap或jcmd命令行方式获取;
  • +
  • 通过配置JVM参数”-XX:+HeapDumpOnOutOfMemoryError"或"-XX:+HeapDumpBeforeFullGC"
  • +
  • 使用第三方工具生成dump文件,如:visual vm
  • +
+
+

MAT介绍

+

导入dump文件:

+

在生成可疑泄漏报告后,会在对应的堆转储文件目录下生成一个zip文件。

+

常见故障排查及程序配置-010

+

常见故障排查及程序配置-011

+

常见故障排查及程序配置-012

+

常见故障排查及程序配置-013

+

常见故障排查及程序配置-016

+

MAT最主要的功能是分析dump文件,其中比较重要的功能就是histogram(直方图)和dominator tree(支配树)

+
+

直方图

+

常见故障排查及程序配置-015

+
    +
  • 浅堆:一个对象结构所占用的大小,即对象头+实例数据+对齐填充,不包括内部引用对象大小;
  • +
  • 深堆:一个对象被 GC 回收后,可以真实释放的内存大小;
  • +
  • 对象的实际大小:一个对象所能触及的所有对象的浅堆大小之和;
  • +
+

常见故障排查及程序配置-017

+

如上图所示:(浅堆<= 深堆 <= 实际大小)

+
    +
  • Object2浅堆大小:为Object2本身;
  • +
  • Object2深堆大小:Object2本身加上Object6;
  • +
  • Object2实际大小:Object2本身加上Object6加上Object5;
  • +
+

常见故障排查及程序配置-014

+
+

支配树对象图

+

常见故障排查及程序配置-018

+

支配树概念源自图论。它体现了对象实例之间的支配关系。在对象的引用图中,所有指向对象B的路径都要经过对象A,则认为对象A支配对象B。如果对象A是离对象B最近的一个支配对象,则认为对象A为对象B的直接支配者。

+

支配树是基于对象间的引用图建立的,它有以下性质:

+
    +
  • 对象A的子树,即所有被对象A支配的对象集合,表示对象A的保留集,即深堆;
  • +
  • 如果对象A支配对象B,那么对象A直接支配者也支配对象B;
  • +
  • 支配树的边与对象引用图的边不相对应;
  • +
+

分配树能直观的体现对象能否被回收的情况,如图所示,左为对象的引用图,右为对象的支配图。

+
    +
  • C与E的关系为,C支配E,C是E的直接支配者,G和E为C的保留集;
  • +
  • C与H不是支配关系,因为H被F引用;
  • +
+

常见故障排查及程序配置-019

+

Java应用程序配置

+

JVM常用参数

+

官方:

+ +

理想的情况下,一个Java程序使用JVM的默认设置也可以运行得很好,所以一般来说,没有必要设置任何JVM参数。然而,由于一些性能问题,我们需要设置合理的JVM参数。

+

可以通过java -XX:+PrintFlagsInitial 命令查看JVM所有参数。

+

常用参数:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
参数含义描述
-Xms堆初始值Xmx和Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍
-Xmx堆最大值为了防止自动扩容降低性能,建议将-Xms和-Xmx的值设置为相同值
-XX:MaxHeapFreeRatio最大堆内存使用率默认70,当超过该比例会进行扩容堆,Xms=Xmx时该参数无效
-XX:MinHeapFreeRatio最小堆内存使用率默认40,当低于该比例会缩减堆,Xms=Xmx时该参数无效
-Xmn年轻代内存最大值年轻代设置的越大,老年代区域就会减少。一般不允许年轻代比老年代还大,因为要考虑GC时最坏情况,所有对象都晋升到老年代。建议设置为老年代存活对象的1-1.5倍,最大可以设置为-Xmx/2 。考虑性能,一般会通过参数 -XX:NewSize 设置年轻代初始大小。如果知道了年轻代初始分配的对象大小,可以节省新生代自动扩展的消耗。
-XX:SurvivorRatio年轻代中两个Survivor区和Eden区大小比率例如: -XX:SurvivorRatio=10 表示伊甸园区是幸存者其中一个区大小的10倍,所以,伊甸园区占新生代大小的10/12, 幸存区From和幸存区To 每个占新生代的1/12
-XX:NewRatio年轻生代和老年代的比率例如:-XX:NewRatio=3 指定老年代/新生代为3/1. 老年代占堆大小的 3/4 ,新生代占 1/4 。如果针对新生代,同时定义绝对值和相对值,绝对值将起作用,建议将年轻代的大小为整个堆的3/8左右。
-XX:+HeapDumpOnOutOfMemoryError让JVM在发生内存溢出时自动的生成堆内存快照可以通过-XX:HeapDumpPath=path参数将生成的快照放到指定路径下
-XX:OnOutOfMemoryError当内存溢发生时可以执行一些指令比如发个E-mail通知管理员或者执行一些清理工作,执行脚本
-XX:ThreadStackSize每个线程栈最大值栈设置太大,会导致线程创建减少,栈设置小,会导致深入不够,深度的递归会导致栈溢出,建议栈深度设置在3000-5000k。
-XX:MetaspaceSize初始化的元空间大小如果元空间大小达到了这个值,就会触发Full GC为了避免频繁的Full GC,建议将- XX:MetaspaceSize设置较大值。如果释放了空间之后,元空间还是不足,那么就会自动增加MetaspaceSize的大小
-XX:MaxMetaspaceSize元空间最大值默认情况下,元空间最大的大小是系统内存的大小,元空间一直扩大,虚拟机可能会消耗完所有的可用系统内存。
+

JVM调优

+

JVM优化是到最后不得已才采用的手段,对JVM内存的系统级的调优主要的目的是减少GC的频率和Full GC的次数。

+

何时调优:

+
    +
  1. Full GC 次数频繁
  2. +
  3. GC 停顿时间过长
  4. +
  5. 应用出现OutOfMemory 等内存异常
  6. +
  7. 堆内存持续上涨达到设置的最大内存值
  8. +
+

调优原则:

+
    +
  1. 多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题
  2. +
  3. 在实际使用中,分析GC情况优化代码比优化JVM参数更好
  4. +
  5. 减少创建对象的数量、减少使用全局变量和大对象
  6. +
+

调优思路:

+
    +
  1. 分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点。如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。
  2. +
  3. 确定JVM调优目标。如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行测试,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择。
  4. +
  5. 不断的分析和调整,直到找到合适的JVM参数配置。
  6. +
+

Java程序shell脚本示例

+
#!/bin/sh
+
+#非特殊应用下面内存分配已经够用
+HEAP_MEMORY=1024M
+METASPACE_SIZE=256M
+
+SERVER_HOME="$( cd "$( dirname "$0"  )" && pwd  )"
+APP_NAME=${@: -1}
+
+#使用说明,用来提示输入参数  
+help() {
+    echo "Usage: start.sh {start|stop|restart|status|help} APP_NAME.jar" >&2
+    echo "Examples:"
+    echo "  sh start.sh start APP_NAME.jar"
+    echo "  sh start.sh stop APP_NAME.jar"
+    echo "  sh start.sh start -Heap 1024M -MetaspaceSize 256M APP_NAME.jar"
+}
+
+#检查程序是否在运行  
+is_exist() {
+    pid=`ps -ef | grep ${SERVER_HOME} | grep ${APP_NAME} | grep -v grep | awk '{print $2}' `
+    #如果不存在返回1,存在返回0  
+    if [ -z "${pid}" ]; then
+      return 1
+    else
+      return 0
+    fi
+}
+
+#启动方法  
+start() {
+   is_exist
+   if [ $? -eq "0" ]; then
+      echo "${APP_NAME} is already running. pid=${pid} ."  
+   else
+      echo "${APP_NAME} running..."
+      JAVA_OPTS="-server -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError"
+      JAVA_OPTS="${JAVA_OPTS} -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"
+      #JAVA_OPTS="${JAVA_OPTS} -Djava.rmi.server.hostname=${LOCAL_IP} -Dcom.sun.management.jmxremote.port=${JMX_PORT} -Dcom.sun.management.jmxremote.rmi.port=${JMX_PORT}"
+
+      shift
+      ARGS=($*)
+      for ((i=0; i<${#ARGS[@]}; i++)); do
+          case "${ARGS[$i]}" in
+          -D*)    JAVA_OPTS="${JAVA_OPTS} ${ARGS[$i]}" ;;
+          -Heap*) HEAP_MEMORY="${ARGS[$i+1]}" ;;
+          -MetaspaceSize*) METASPACE_SIZE="${ARGS[$i+1]}" ;;
+          esac
+      done
+
+      JAVA_OPTS="${JAVA_OPTS} -Xms${HEAP_MEMORY} -Xmx${HEAP_MEMORY} -XX:MaxMetaspaceSize=${METASPACE_SIZE} -XX:MetaspaceSize=${METASPACE_SIZE}"
+      #生产环境加上下面这个配置 服务启动的时候真实的分配物理内存给jvm
+      #JAVA_OPTS="${JAVA_OPTS} -XX:+AlwaysPreTouch" 
+      JAVA_OPTS="${JAVA_OPTS} -Duser.dir=${SERVER_HOME}"
+      #下面两段根据需要酌情配置
+      #JAVA_OPTS="${JAVA_OPTS} -Xloggc:${APP_NAME}.gc.log"
+      #JAVA_OPTS="${JAVA_OPTS} -Dapp.name=${SERVER_NAME} -Dlogging.config=${SERVER_HOME}/logback-spring.xml -Dspring.profiles.active=dev"
+      echo "jvm args: ${JAVA_OPTS}"
+      java ${JAVA_OPTS} -jar ${APP_NAME} >/dev/null 2>&1 &
+   fi
+}
+
+#停止方法  
+stop() {
+   is_exist
+   if [ $? -eq "0" ]; then
+     echo "${APP_NAME} is stopping..."
+     kill -9 $pid
+   else
+     echo "${APP_NAME} is not running"  
+   fi
+}
+
+#输出运行状态  
+status() {
+   is_exist
+   if [ $? -eq "0" ]; then
+     echo "${APP_NAME} is running. Pid is ${pid}"  
+   else
+     echo "${APP_NAME} is not running."  
+   fi
+}
+
+#根据输入参数,选择执行对应方法,不输入则执行使用说明  
+case "$1" in
+   "start")
+     start $@;
+     ;;
+   "stop")
+     stop $@;
+     ;;
+   "status")
+     status $@;
+     ;;
+   "restart")
+     stop $@;
+     start $@;
+     ;;
+   *)
+     help
+     ;;
+esac
+

常见故障排查

+

CPU使用过高定位分析

+

一般在生产环境排查程序故障,都会查看日志什么的,但是有些故障日志是看不出来的,就比如:CPU使用过高。

+

那应该怎么办呢?我们需要结合linux命令和JDK相关命令来排查程序故障。

+

步骤:

+
    +
  • 首先使用top命令,找出CPU占比最高的Java进程;然后进一步定位后台程序,如果发现使用过高的进程ID,记录下来方便排查;
  • +
  • 定位到具体的线程;使用ps -mp 进程ID -o THREAD,tid,time命令可以找到有问题的线程ID; +
    +

    ps -mp 进程ID -o THREAD,tid,time 说明: +-m:显示所有线程 +-p:pid进程使用CPU的时间 +-o:该参数后是用户自定义参数

    +
    +
  • +
  • 获取到线程ID后,将线程ID转化为16进制格式,如果有英文要小写格式;可以用命令printf "%x\n" 线程ID,当然也可以使用工具从10进制转16进制。 +
    printf "%x\n" 16
    +
  • +
  • 线程ID转成16进制后,执行最后一个命令:jstack 进程ID | grep 16进制线程ID -A50,就能看到有问题的代码。
  • +
+

内存使用过高定位分析

+

与CPU使用过高同样的,内存如果占用过大,查看程序日志也看不出来。

+

步骤:

+
    +
  • 使用top命令查看应用程序内存占用情况,查看内存使用情况,如果发现使用过高的进程ID,记录下来;
  • +
  • 根据进程ID查询具体线程ID: ps p进程ID -L -o pcpu,pmem,pid,tid,time,tname,cmd,记下使用内存异常的记下线程ID;
  • +
  • 将内存使用较高的线程的堆栈信息写入文件:jstack -l 进程ID > 文件名,写入文件后将文件中的线程ID转换为16进制,在文件中搜索16进制线程ID即可;
  • +
+

死锁编码及定位分析

+

死锁经常表现为程序的停顿,或者不再响应用户的请求。从操作系统上观察,对应进程的CPU占用率为零,很快会从top或prstat的输出中消失。

+

死锁示例代码:

+
public class MainTest {
+
+    public static void main(String[] args) {
+         String lockA = "lockA";
+         String lockB = "lockB";
+        new Thread(new ThreadHolderLock(lockA,lockB),"线程AAA").start();
+        new Thread(new ThreadHolderLock(lockB,lockA),"线程BBB").start();
+    }
+}
+
+class ThreadHolderLock implements Runnable{
+
+    private String lockA;
+    private String lockB;
+
+    public ThreadHolderLock(String lockA, String lockB){
+        this.lockA = lockA;
+        this.lockB = lockB;
+    }
+
+    @Override
+    public void run() {
+        synchronized (lockA){
+            System.out.println(Thread.currentThread().getName() + "\t 持有锁 "+ lockA+", 尝试获得"+ lockB);
+
+            try {
+                Thread.sleep(1000);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+
+            synchronized (lockB){
+                System.out.println(Thread.currentThread().getName() + "\t 持有锁 "+ lockB+", 尝试获得"+ lockA);
+            }
+        }
+    }
+}
+

步骤:

+
    +
  • 使用jps -l命令找到程序进程;
  • +
  • 使用jstack pid命令打印堆栈信息;
  • +
+

上面死锁示例代码使用jstack pid后的一些信息:

+
Found one Java-level deadlock:
+=============================
+"线程BBB":
+  waiting to lock monitor 0x00007feb0d80b018 (object 0x000000076af2d588, a java.lang.String),
+  which is held by "线程AAA"
+"线程AAA":
+  waiting to lock monitor 0x00007feb0d80d8a8 (object 0x000000076af2d5c0, a java.lang.String),
+  which is held by "线程BBB"
+
+Java stack information for the threads listed above:
+===================================================
+"线程BBB":
+	at com.github.springcloud.service.ThreadHolderLock.run(MainTest.java:35)
+	- waiting to lock <0x000000076af2d588> (a java.lang.String)
+	- locked <0x000000076af2d5c0> (a java.lang.String)
+	at java.lang.Thread.run(Thread.java:748)
+"线程AAA":
+	at com.github.springcloud.service.ThreadHolderLock.run(MainTest.java:35)
+	- waiting to lock <0x000000076af2d5c0> (a java.lang.String)
+	- locked <0x000000076af2d588> (a java.lang.String)
+	at java.lang.Thread.run(Thread.java:748)
+
+Found 1 deadlock.
+

内存泄露排查分析

+

Java虚拟机是使用引用计数法和可达性分析来判断对象是否可回收,本质是判断一个对象是否还被引用,如果没有引用则回收。在开发的过程中,由于代码的实现不同就会出现很多种内存泄漏问题,让gc误以为此对象还在引用中,无法回收,造成内存泄漏。

+

当内存泄露时,如果不是JVM参数中的内存分配太小了,那么从根本上解决Java内存泄露的唯一方法就是修改程序。

+

内存泄露主要原因:

+
    +
  • 在内存中加载过大的数据,例如,从数据库取出过多数据;
  • +
  • 资源未关闭造成的内存泄漏;
  • +
  • 变量不合理的作用域,使用完毕,如果没有及时的赋值为null,则会造成内存泄露;
  • +
  • 长生命周期的对象中引用短生命周期对象,很可能会出现内存泄露;
  • +
+

内存泄漏排查:

+
    +
  1. 内存泄漏的主要表象就是内存不足,所以首先要看一下JVM启动参数中内存空间分配是否过小,如果是这种问题调整该参数即可;
  2. +
  3. 从代码层面找问题,如果之前从未出现过此类问题,新增接口或者引入新第三包的时候后出现该问题,则可能是新增的部分代码存在问题;
  4. +
  5. 使用jdk相关命令进行排查分析: +
      +
    • 使用jstat -gc 查看GC垃圾回收统计信息,看Full GC后堆空间使用内存还持续增长,且有增长到Xmx设定值的趋势基本可以肯定存在内存泄露,如果当前完全垃圾回收后内存增长到一个值之后,又能回落,总体上处于一个动态平衡,那么内存泄漏基本可以排除;也可以隔断时间抽取老年代占用内存情况,如果老年代占用情况持续上升也很有可能存在内存泄露的情况;
    • +
    • 把堆dump下来再用工具进行分析,但dump堆要花较长的时间,并且文件巨大,不建议这样;可以使用jmap -histo:live 在线进行分析,查看输出的对象数量如果过大就需要额外注意;
    • +
    +
  6. +
+

使用MAT找到内存泄漏的代码思路:

+ +
    +
  1. 打开MAT中histogram,找到堆内存中占用最大的对象(内存泄漏很有可能就是由大对象导致的);
  2. +
  3. 由大对象找被哪些线程引用,查看内存占用最大的线程;
  4. +
  5. 从线程中的堆栈信息找到项目中自定义的包和对象,从而可定位到具体的代码;
  6. +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/front-learning-route/index.html b/blog-site/public/posts/essays/front-learning-route/index.html new file mode 100644 index 00000000..b9a5ea9d --- /dev/null +++ b/blog-site/public/posts/essays/front-learning-route/index.html @@ -0,0 +1,1516 @@ + + + + + + + + + + + 前端学习路线 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

前端学习路线

+ 2024.02.29 +
+

基础知识

+

网络知识

+

HTTP

+

DNS

+

域名

+

云服务

+

网络安全

+
    +
  • HTTPS
  • +
  • CORS
  • +
  • 网络渗透
  • +
  • OWASP
  • +
+

HTML

+

CSS

+

JavaScript

+

JQuery

+

Ajax

+

ES6-ES11

+

综合应用

+

工程化体系

+

代码规范

+

CSS预处理器

+
    +
  • Less
  • +
  • Sass
  • +
  • PostCSS
  • +
+

Node

+

Promise

+

Axios

+

工具

+

包管理工具

+
    +
  • Npm
  • +
  • Yarn
  • +
+

打包工具

+
    +
  • Webpack
  • +
  • Parcel
  • +
+

代码格式化工具

+
    +
  • ESLint
  • +
  • Prettier
  • +
+

调试工具

+
    +
  • Chrome
  • +
  • IETest
  • +
  • Postman
  • +
+

版本管理工具

+
    +
  • Git
  • +
  • GitLab
  • +
  • GitHub
  • +
+

部署发布工具

+
    +
  • Jenkins
  • +
  • CICD
  • +
+

主流技术

+
    +
  • TypeScript
  • +
  • Vue
  • +
  • React
  • +
  • Angular
  • +
  • 综合应用
  • +
+

静态站点生成器

+
    +
  • Next
  • +
  • GatsbyJS
  • +
  • Nuxt
  • +
  • Vuepress
  • +
  • Hugo
  • +
+

性能优化和监控

+

性能优化概览

+

浏览器及工作方式

+

SEO

+

资源管理

+
    +
  • 延迟加载
  • +
  • 按需加载
  • +
  • 缓存复用
  • +
  • CDN部署
  • +
  • 请求合并
  • +
  • 异步同步
  • +
+

移动端

+

Native App

+
    +
  • 安卓原生
  • +
  • IOS原生
  • +
  • 鸿蒙原生
  • +
+

Web App

+
    +
  • Uni-App
  • +
  • Taro
  • +
  • React Native
  • +
  • Flutter +
      +
    1. 基础
    2. +
    3. 实战
    4. +
    +
  • +
+

微信小程序

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/java-bugs/index.html b/blog-site/public/posts/essays/java-bugs/index.html new file mode 100644 index 00000000..1ab13a58 --- /dev/null +++ b/blog-site/public/posts/essays/java-bugs/index.html @@ -0,0 +1,335 @@ + + + + + + + + + + + 如何减少及解决bug | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

如何减少及解决bug

+ 2023.03.10 +
+
+

bug的起源: +1945年,一只小飞蛾钻进了计算机电路里,导致系统无法工作,一位名叫格蕾丝·赫柏的人把飞蛾拍死在工作日志上,写道:就是这个 bug(虫子),害我们今天的工作无法完成——于是,bug一词成了电脑系统程序的专业术语,形容那些系统中的缺陷或问题。

+
+

bug一词,是指“故障”、“缺陷”。了解软件开发的朋友都非常熟悉,程序员和测试人员更不用说,在工作中会常遇到。 作为一名开发人员,项目出现bug是避免不了的。无论你是一名初入职场的小白,还是拥有经验丰富的大佬,只要经常写代码,梳理业务逻辑,很难免不出bug。正所谓常在河边走,哪能不湿鞋。记得以前经常听人说,如果你没有把系统搞宕机过,就不是一名合格的CTO,成为一名出色的开发人员,经验都是一个一个积累起来的。

+

所谓的bug指的是生产环境发现的问题;那么只要线上不发现问题,那就不是bug。之所以这么讲是因为一旦影响程序的正常使用便会影响公司的利益,公司利益被影响你自己的利益也会被影响;所以为了自己写程序的时候尽量多测试,将可能会应对到的情况想得全一点,减少bug的出现。

+

程序员能为了减少bug的出现能做的可以从代码方面下手:

+
    +
  • 在正式开发前,做好准备工作,先梳理一下业务逻辑,根据业务场景可以使用什么设计模式,是否需要缓存,列一下需要用到的框架等,尽量考虑的全面一些,考虑一下不同的情况;
  • +
  • 在编写代码的时候需要注意以下几点,供参考: +
      +
    • 在关键的地方写注释,标注一些关键的信息;
    • +
    • 增加校验;如: 调用第三方程序或者使用一些中间件的时候,拿到结果不要着急使用,先做校验,这样可以很大程度上避免NPE的出现;
    • +
    • 当在程序某个功能下加一个非堵塞性功能的时候,需要考虑是否会影响到主流程的使用,有时候可能不经意的一个问题会将它的影响无限放大,随之而来的就是用户投诉,老板亏钱;
    • +
    • 当编写代码的时候尽量写一些简洁,清晰的代码,比如一个方法实现的功能较多,应把它拆开。如果代码变得易于维护起来那么bug的数量也会大大降低;
    • +
    • 尽量编写一些能测试的代码,将一个方法的职责做到最小,一个方法就干一件事情;
    • +
    • 当开发功能完成之后不要着急测试,先自己审查一下代码,确认哪些地方可以进行优化,之后再调试功能进行单元测试;
    • +
    +
  • +
  • 当开发完成之后一定要进行单元测试,这可以大大降低bug的数量; 测试的时候需要模拟线上的环境数据来测试新增或修复的功能,否则即使改完功能之后可能会由于请求次数或数据量巨大问题导致功能不能使用。
  • +
+

除此之外,一方面可以借助一些工具来进行bug的规避,比如sonar,可以发现一些潜在的错误和问题。另一方面我们要不断学习和提高自己的编程技能,以提高代码质量和减少错误的发生。

+

尽管这样bug这种东西还是不可避免的,我们只能减少bug而不能消除bug,没有人保证自己的程序一定是没有问题的。减少bug的出现只能多测多验证,哪怕单元测试通过都不能非常有效减少bug,因为受到写单元测试的人的思维角度限制,导致单元测试的片面性。

+

关于bug的种类,最容易出现的bug是逻辑上的bug,如复杂庞大一点软件如果不是所有地方都熟悉就写代码是比较容易遗漏一些特殊情况的。除了逻辑上的bug外在开发中还有框架中存在的bug,但是这种bug是我们避免不了的。如2021年爆发的log4j堪称史诗级的bug。 +没什么好的办法可以提前避免掉,就多用一些稳定的框架吧,有apache就用apache,没有就优先使用fork,start高的,从概率上减少。

+

一个人的力量毕竟有限必要的时候可以叫上同事,进行code review。

+

因为bug是不可避免的,所以解决bug能力就显得尤为重要,程序员归根到底拼的是解决问题的能力。

+

bug的解决思路:

+
    +
  • 确认bug的存在,包括复现bug的步骤和现象;
  • +
  • 分析bug的根本原因,找到导致bug的代码或逻辑,一般通过日志查看一些栈的信息;
  • +
  • 修复bug的代码或逻辑,并重新测试确认修复成功;
  • +
  • 编写针对bug的单元测试,验证bug是否修复;
  • +
  • 记录bug修复的过程和结果,以备将来参考和验证;
  • +
+

亲自复现问题,关注第一现场,确定是必现还是偶现; 区分是人的问题还是环境的问题;如果是人的问题,那是配置参数的问题还是代码逻辑的问题。如果是配置参数的问题,则通过对比正常运行的配置参数发现问题。 +如果是代码逻辑的问题,则通过commit的历史二分查找缩小出现问题的逻辑范围; 如果是机器的问题,确定是单机问题还是集群问题; 如果是单机问题,则替换机器,如果是集群问题则考虑升级硬件设备。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/java-code-rule/index.html b/blog-site/public/posts/essays/java-code-rule/index.html new file mode 100644 index 00000000..c39f66c0 --- /dev/null +++ b/blog-site/public/posts/essays/java-code-rule/index.html @@ -0,0 +1,2106 @@ + + + + + + + + + + + 规范编写Java代码总结 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

规范编写Java代码总结

+ 2021.11.25 +
+

编码规范

+

我们为什么要遵守规范来编码?

+

是因为通常在编码过程中我们不只自己进行开发,通常需要一个团队来进行,开发好之后还需要维护,所以编码规范就显的尤为重要。

+

代码维护时间比较长,那么保证代码可读性就显得很重要。作为一个程序员,咱们得有点追求和信仰。推荐《阿里巴巴Java开发手册》

+

内存泄露问题

+

内存泄漏是指不使用的对象持续占有内存使得内存得不到释放,从而造成内存空间的浪费。

+

内存泄露导致问题:

+
    +
  • 最明显问题频繁GC,从而STW次数增加,导致用户体验变差;
  • +
  • 如果内存泄露问题严重,会导致OOM,直接导致程序不能正常运行;
  • +
+

内存泄露是很严重的问题,在出现内存泄露的情况下,想要解决是肯定要修改代码的,所以在编写代码的时候要避免出现内存泄露。

+

内存泄露原因

+

大多数内存泄露的原因是,长生命周期的对象引用了短生命周期的对象。例如,A对象引用B对象,A对象的生命周期(t1-t4)比B对象的生命周期(t2-t3)长的多。当B对象没有被应用程序使用之后,A对象仍然在引用着B对象。这样,垃圾回收器就没办法将B对象从内存中移除,从而导致内存泄露问题。

+

变量作用域不合理导致内存泄露

+

一个变量的定义的作用范围大于其使用范围,很有可能会造成内存泄漏

+
public class UsingRandom {
+    private String msg;
+    public void receiveMsg(){
+        readFromNet();//从网络中接受数据保存到msg中
+        saveDB();//把msg保存到数据库中
+    }
+}
+

解决办法,将msg定义在receiveMsg方法中或将msg重置为null。

+
public class UsingRandom {
+    private String msg;
+    public void receiveMsg(){
+        readFromNet();//从网络中接受数据保存到msg中
+        saveDB();//把msg保存到数据库中
+        msg = null;
+    }
+}
+

向静态集合添加数据导致内存泄露

+

HashMap、LinkedList 等集合类,如果这些集合是静态的并且向集合中添加了对象,这些对象就算不再使用,也不会被GC主动回收的,它们的生命周期与JVM程序一致,容器中的对象在程序结束之前将不能被释放,从而造成内存泄漏。

+
public class MyTest {
+    static Map<String, User> map = new HashMap<>();
+    public static void main(String[] args) throws InterruptedException {
+        User user = new User();
+        map.put("01",user);
+    }
+}
+

解决办法是将使用完后的集合和对象重置为null,或将集合替换成弱引用集合(只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存)。

+
    static Map<String, User> map = new HashMap<>();
+    public static void main(String[] args) throws InterruptedException {
+
+        User user = new User();
+        map.put("01",user);
+
+        user = null;
+        map = null;
+
+        System.gc();
+        Thread.sleep(1000);
+    }
+
    static Map<String, User> map = new WeakHashMap<>();
+    public static void main(String[] args) throws InterruptedException {
+        User user = new User();
+        map.put("01",user);
+    }
+

内部类持有外部类引用导致内存泄露

+

非静态内部类,自动生成的构造方法,默认的参数是外部类的类型,因此使用非内部内部类的时候会保留一个外部类的引用。如果换成静态内部类则不会生成默认的构造方法。

+
public class MyClass {
+
+
+    public static void main(String[] args) throws Throwable {
+
+    }
+
+    public class A{
+        public void methed1(){
+
+        }
+    }
+
+    public static  class B{
+        public void methed1(){
+
+        }
+    }
+}
+

如果一个外部类的实例对象的方法返回了一个内部类的实例对象。这个内部类对象被长期引用了,即使那个外部类实例对象不再被使用,但由于内部类持有外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。

+

数据结构中移除元素导致内存泄露

+
    +
  • 链表中移除元素,没有将移除的元素置为null导致的内存泄露问题
  • +
+

Java容器LinkedList移除元素方法核心源码:

+

+//删除指定节点并返回被删除的元素值
+E unlink(Node<E> x) {
+    //获取当前值和前后节点
+    final E element = x.item;
+    final Node<E> next = x.next;
+    final Node<E> prev = x.prev;
+    if (prev == null) {
+        first = next; //如果前一个节点为空(如当前节点为首节点),后一个节点成为新的首节点
+    } else {
+        prev.next = next;//如果前一个节点不为空,那么他先后指向当前的下一个节点
+        x.prev = null;
+    }
+    if (next == null) {
+        last = prev; //如果后一个节点为空(如当前节点为尾节点),当前节点前一个成为新的尾节点
+    } else {
+        next.prev = prev;//如果后一个节点不为空,后一个节点向前指向当前的前一个节点
+        x.next = null;
+    }
+    x.item = null; // help gc
+    size--;
+    modCount++;
+    return element;
+}
+

除了修改节点间的关联关系,我们还要做的就是赋值为null的操作,不管GC何时会开始清理,我们都应及时的将无用的对象标记为可被清理的对象。

+
+
    +
  • 在栈中将该元素出栈,没有将出栈的元素置为null导致的内存泄露问题
  • +
+
public E pop(){
+    if(size == 0)
+        return null;
+    else{
+        E e = (E) elementData[--size];
+        elementData[size] = null; // help gc
+        return e;
+    }
+}
+

+
    +
  • 改变hash表中对象的属性值,再将该元素移除导致的内存泄露问题
  • +
+

因为改变了对象属性的值相当于改变了改对象的hash值,删除的时候是根据对象的hash值来删除的,删除对象的时候找不到对应的hash值,所以不能删除,最终导致内存泄露。

+
public class ChangeHashCode {
+    public static void main(String[] args) {
+        HashSet set = new HashSet();
+        Person p1 = new Person(1001, "AA");
+        Person p2 = new Person(1002, "BB");
+ 
+        set.add(p1);
+        set.add(p2);
+ 
+        p1.name = "CC"; // 导致了内存的泄漏
+        set.remove(p1);  // 对象哈希值发生变化,检索不到,导致删除失败
+ 
+        System.out.println(set);
+ 
+        set.add(new Person(1001, "CC"));
+        System.out.println(set);
+ 
+        set.add(new Person(1001, "AA"));
+        System.out.println(set);
+    }
+}
+ 
+class Person {
+    int id;
+    String name;
+ 
+    public Person(int id, String name) {
+        this.id = id;
+        this.name = name;
+    }
+ 
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (!(o instanceof Person)) return false;
+ 
+        Person person = (Person) o;
+ 
+        if (id != person.id) return false;
+        return name != null ? name.equals(person.name) : person.name == null;
+    }
+ 
+    @Override
+    public int hashCode() {
+        int result = id;
+        result = 31 * result + (name != null ? name.hashCode() : 0);
+        return result;
+    }
+ 
+    @Override
+    public String toString() {
+        return "Person{" +
+                "id=" + id +
+                ", name='" + name + '\'' +
+                '}';
+    }
+}
+

连接未释放

+

如数据库连接、网络连接和IO连接等。当不再使用时,需要调用close方法来释放连接。只有连接被关闭后,垃圾回收器才会回收对应的对象。否则,如果在连接过程中,对一些对象不显性地关闭,将会造成大量的对象无法被回收,从而引起内存泄漏。

+
public static void main(String[] args) {
+    try{
+        Connection conn =null;
+        Class.forName("com.mysql.jdbc.Driver");
+        conn =DriverManager.getConnection("url","","");
+        Statement stmt =conn.createStatement();
+        ResultSet rs =stmt.executeQuery("....");
+    } catch(Exception e){
+    } finally {
+        // 1.关闭结果集 Statement
+        // 2.关闭声明的对象 ResultSet
+        // 3.关闭连接 Connection
+    }
+}
+

代码结构

+

编写代码最终要的原则就是要具有扩展性,如果没有扩展性那么代码维护起来会非常麻烦。

+

优雅判空

+

使用Spring提供的工具类Assert判空

+
public class MyTest {
+    public static void main(String[] args) {
+        Object obj = new Object();
+        Assert.notNull(obj,"对象不能为空");
+        Assert.isTrue(obj != null,"对象不能为空");
+        ArrayList<Object> list = new ArrayList<>();
+        Assert.notEmpty(list,"list不能为空");
+    }
+}
+

使用Java8链式调用特性判空

+
public class MyTest {
+    public static void main(String[] args) {
+        Object obj = new Object();
+        System.out.println("是否不为空:" + Optional.of(obj).isPresent());
+
+        User user = new User();
+        user.setId(1);
+        user.setName("ls");
+        Optional<User> zs = Optional.of(user).filter(u -> u.getName().equals("zs"));
+        System.out.println(zs.get());
+        zs.ifPresent(item ->{
+             System.out.println("对象不等于空,做的一系列操作");
+         });
+        User user1 = Optional.of(zs.orElse(new User())).get();
+        System.out.println(user1);
+    }
+
+
+    static class User {
+        private int id;
+        private String name;
+
+        public String getName() {
+            return name;
+        }
+        public int getId() {
+            return id;
+        }
+        public void setName(String name) {
+            this.name = name;
+        }
+        public void setId(int id) {
+            this.id = id;
+        }
+
+        @Override
+        public String toString() {
+            return "User{" +
+                    "id=" + id +
+                    ", name='" + name + '\'' +
+                    '}';
+        }
+    }
+}
+

自定义链式调用,对Optional扩展:

+ +
@Data
+public class User {
+
+    private String name;
+
+    private String gender;
+
+    private School school;
+
+    @Data
+    public static class School {
+
+        private String scName;
+
+        private String adress;
+    }
+}
+
/**
+* @author Axin
+* @since 2020-09-10
+* @summary 链式调用 bean  value 的方法
+*/
+public final class OptionalBean<T> {
+
+    private static final OptionalBean<?> EMPTY = new OptionalBean<>();
+
+    private final T value;
+
+    private OptionalBean() {
+        this.value = null;
+    }
+
+    /**
+     * 空值会抛出空指针
+     * @param value
+     */
+    private OptionalBean(T value) {
+        this.value = Objects.requireNonNull(value);
+    }
+
+    /**
+     * 包装一个不能为空的 bean
+     * @param value
+     * @param <T>
+     * @return
+     */
+    public static <T> OptionalBean<T> of(T value) {
+        return new OptionalBean<>(value);
+    }
+
+    /**
+     * 包装一个可能为空的 bean
+     * @param value
+     * @param <T>
+     * @return
+     */
+    public static <T> OptionalBean<T> ofNullable(T value) {
+        return value == null ? empty() : of(value);
+    }
+
+    /**
+     * 取出具体的值
+     * @param fn
+     * @param <R>
+     * @return
+     */
+    public T get() {
+        return Objects.isNull(value) ? null : value;
+    }
+
+    /**
+     * 取出一个可能为空的对象
+     * @param fn
+     * @param <R>
+     * @return
+     */
+    public <R> OptionalBean<R> getBean(Function<? super T, ? extends R> fn) {
+        return Objects.isNull(value) ? OptionalBean.empty() : OptionalBean.ofNullable(fn.apply(value));
+    }
+
+    /**
+     * 如果目标值为空 获取一个默认值
+     * @param other
+     * @return
+     */
+    public T orElse(T other) {
+        return value != null ? value : other;
+    }
+
+    /**
+     * 如果目标值为空 通过lambda表达式获取一个值
+     * @param other
+     * @return
+     */
+    public T orElseGet(Supplier<? extends T> other) {
+        return value != null ? value : other.get();
+    }
+
+    /**
+     * 如果目标值为空 抛出一个异常
+     * @param exceptionSupplier
+     * @param <X>
+     * @return
+     * @throws X
+     */
+    public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
+        if (value != null) {
+            return value;
+        } else {
+            throw exceptionSupplier.get();
+        }
+    }
+
+    public boolean isPresent() {
+        return value != null;
+    }
+
+    public void ifPresent(Consumer<? super T> consumer) {
+        if (value != null)
+            consumer.accept(value);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(value);
+    }
+
+    /**
+     * 空值常量
+     * @param <T>
+     * @return
+     */
+    public static<T> OptionalBean<T> empty() {
+        @SuppressWarnings("unchecked")
+        OptionalBean<T> none = (OptionalBean<T>) EMPTY;
+        return none;
+    }
+
+}
+
public static void main(String[] args) {
+
+    User axin = new User();
+    User.School school = new User.School();
+    axin.setName("hello");
+
+    // 1. 基本调用
+    String value1 = OptionalBean.ofNullable(axin)
+            .getBean(User::getSchool)
+            .getBean(User.School::getAdress).get();
+    System.out.println(value1);
+
+    // 2. 扩展的 isPresent方法 用法与 Optional 一样
+    boolean present = OptionalBean.ofNullable(axin)
+            .getBean(User::getSchool)
+            .getBean(User.School::getAdress).isPresent();
+    System.out.println(present);
+
+
+    // 3. 扩展的 ifPresent 方法
+    OptionalBean.ofNullable(axin)
+            .getBean(User::getSchool)
+            .getBean(User.School::getAdress)
+            .ifPresent(adress -> System.out.println(String.format("地址存在:%s", adress)));
+
+    // 4. 扩展的 orElse
+    String value2 = OptionalBean.ofNullable(axin)
+            .getBean(User::getSchool)
+            .getBean(User.School::getAdress).orElse("家里蹲");
+
+    System.out.println(value2);
+
+    // 5. 扩展的 orElseThrow
+    try {
+        String value3 = OptionalBean.ofNullable(axin)
+                .getBean(User::getSchool)
+                .getBean(User.School::getAdress).orElseThrow(() -> new RuntimeException("空指针了"));
+    } catch (Exception e) {
+        System.out.println(e.getMessage());
+    }
+}
+

使用BeanValidator注解判空

+

思路:定义一个注解,将需要校验的参数对象都标注该注解,利用SpringAOP,拦截该注解,将其中标注的参数取出,最后通过BeanValidator进行校验。

+

所需依赖:

+
<dependency>
+    <groupId>org.hibernate</groupId>
+    <artifactId>hibernate-validator</artifactId>
+</dependency>
+
/**
+ * facade接口注解, 用于统一对facade进行参数校验及异常捕获
+ * @author whitepure
+ */
+@Target({ElementType.METHOD,ElementType.PARAMETER})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Facade {
+
+}
+
@Slf4j
+@Aspect
+@Component
+public class FacadeAspect {
+
+
+    @Around("@annotation(com.spring.example.annotation.Facade)")
+    public Object facade(ProceedingJoinPoint pjp) throws Exception {
+
+        // 获取,执行目标方法
+        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
+
+        Object[] args = pjp.getArgs();
+
+        log.info("获取@Facede注解参数列表,参数: {}", args);
+
+        // 参数类型
+        Class<?> returnType = ((MethodSignature) pjp.getSignature()).getMethod().getReturnType();
+
+        //循环遍历所有参数,进行参数校验
+        for (Object parameter : args) {
+            try {
+                BeanValidator.validateObject(parameter);
+            } catch (ValidationException e) {
+                return getFailedResponse(returnType, e);
+            }
+        }
+
+        try {
+            // 目标方法执行
+            return pjp.proceed();
+        } catch (Throwable throwable) {
+            // 返回通用失败响应
+            return getFailedResponse(returnType, throwable);
+        }
+    }
+
+    /**
+     * 定义并返回一个通用的失败响应
+     */
+    private Object getFailedResponse(Class<?> returnType, Throwable throwable)
+            throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
+
+        //如果返回值的类型为BaseResponse 的子类,则创建一个通用的失败响应
+        if (returnType.getDeclaredConstructor().newInstance() instanceof ApiResponse) {
+            ApiResponse response = (ApiResponse) returnType.getDeclaredConstructor().newInstance();
+            String message = throwable.getMessage();
+            log.error("校验bean异常:", throwable);
+            response.setMessage(message);
+            response.setCode(Status.ERROR.getCode());
+            return response;
+        }
+        log.error("failed to getFailedResponse , returnType ({}) is not instanceof BaseResponse", returnType);
+        return null;
+    }
+
+}
+
public class BeanValidator {
+
+
+    private static final Validator validator = Validation
+            .byProvider(HibernateValidator.class)
+            .configure().failFast(true)
+            .buildValidatorFactory().getValidator();
+
+    /**
+     * 校验对象
+     *
+     * @param object object
+     * @param groups groups
+     */
+    public static void validateObject(Object object, Class<?>... groups) throws ValidationException {
+        Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
+        if (constraintViolations.stream().findFirst().isPresent()) {
+            throw new ValidationException(constraintViolations.stream().findFirst().get().getMessage());
+        }
+    }
+
+
+    /**
+     * 校验对象bean
+     *
+     * @param t      bean
+     * @param groups 校验组
+     * @return ValidResult
+     */
+    public static <T> ValidResult validateBean(T t, Class<?>... groups) {
+        ValidResult result = new ValidResult();
+        Set<ConstraintViolation<T>> violationSet = validator.validate(t, groups);
+        boolean hasError = violationSet != null && violationSet.size() > 0;
+        result.setHasErrors(hasError);
+        if (hasError) {
+            for (ConstraintViolation<T> violation : violationSet) {
+                result.addError(violation.getPropertyPath().toString(), violation.getMessage());
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 校验bean的某一个属性
+     *
+     * @param obj          bean
+     * @param propertyName 属性名称
+     * @return ValidResult
+     */
+    public static <T> ValidResult validateProperty(T obj, String propertyName) {
+        ValidResult result = new ValidResult();
+        Set<ConstraintViolation<T>> violationSet = validator.validateProperty(obj, propertyName);
+        boolean hasError = violationSet != null && violationSet.size() > 0;
+        result.setHasErrors(hasError);
+        if (hasError) {
+            for (ConstraintViolation<T> violation : violationSet) {
+                result.addError(propertyName, violation.getMessage());
+            }
+        }
+        return result;
+    }
+
+    /**
+     * 校验结果类
+     */
+    @Data
+    public static class ValidResult {
+
+        /**
+         * 是否有错误
+         */
+        private boolean hasErrors;
+
+        /**
+         * 错误信息
+         */
+        private List<ErrorMessage> errors;
+
+        public ValidResult() {
+            this.errors = new ArrayList<>();
+        }
+
+        public boolean hasErrors() {
+            return hasErrors;
+        }
+
+        public void setHasErrors(boolean hasErrors) {
+            this.hasErrors = hasErrors;
+        }
+
+        /**
+         * 获取所有验证信息
+         *
+         * @return 集合形式
+         */
+        public List<ErrorMessage> getAllErrors() {
+            return errors;
+        }
+
+        /**
+         * 获取所有验证信息
+         *
+         * @return 字符串形式
+         */
+        public String getErrors() {
+            StringBuilder sb = new StringBuilder();
+            for (ErrorMessage error : errors) {
+                sb.append(error.getPropertyPath()).append(":").append(error.getMessage()).append(" ");
+            }
+            return sb.toString();
+        }
+
+        public void addError(String propertyName, String message) {
+            this.errors.add(new ErrorMessage(propertyName, message));
+        }
+    }
+
+    @Data
+    public static class ErrorMessage {
+
+        private String propertyPath;
+
+        private String message;
+
+        public ErrorMessage() {
+        }
+
+        public ErrorMessage(String propertyPath, String message) {
+            this.propertyPath = propertyPath;
+            this.message = message;
+        }
+    }
+
+}
+

过多if..else

+

替换if..else并不会降低代码的复杂度,相反比较少见的写法可能会增加认知负荷,从而进一步增加了复杂度。之所以要替换过多的if..else是为了对代码进行解耦合,方便扩展代码,最终方便对代码的维护。

+

以下有几种常见的方法来替换过多的if..else。

+

使用枚举代替if..else

+
public class MyTest {
+    public static void main(String[] args) {
+        String val = "FOUR";
+        condition(val);
+        System.out.println("--------------");
+        newCondition(val);
+    }
+
+    public static void condition(String val) {
+        if ("ONE".equals(val)) {
+            System.out.println("val:" + 1111111);
+        } else if ("TWO".equals(val)) {
+            System.out.println("val:" + 2222222);
+        } else if ("THREE".equals(val)) {
+            System.out.println("val:" + 3333333);
+        } else {
+            System.out.println("val:" + val);
+        }
+    }
+
+    public static void newCondition(String val) {
+        ConditionEnum conditionEnum;
+        try {
+            conditionEnum = ConditionEnum.valueOf(val);
+        } catch (IllegalArgumentException e) {
+            System.out.println("val:" + val);
+            return;
+        }
+        exec(conditionEnum);
+    }
+
+    public static void exec(ConditionEnum conditionEnum) {
+        conditionEnum.context();
+    }
+
+    enum ConditionEnum {
+        ONE {
+            @Override
+            public void context() {
+                System.out.println("val:" + 1111111);
+            }
+        },
+        TWO {
+            @Override
+            public void context() {
+                System.out.println("val:" + 2222222);
+            }
+        },
+        THREE {
+            @Override
+            public void context() {
+                System.out.println("val:" + 3333333);
+            }
+        };
+
+        public abstract void context();
+    }
+
+}
+

也可用枚举替换成这样:

+
    public static void newCondition(String val) {
+        ConditionEnum condition = ConditionEnum.getCondition(val);
+        System.out.println("val:" + (condition == null ? val : condition.value));
+    }
+
+    enum ConditionEnum {
+        ONE("ONE",1111111),
+        TWO("TWO",2222222),
+        THREE("THREE",3333333)
+
+        ;
+
+        private String key;
+        private Integer value;
+
+        ConditionEnum(String key,Integer value){
+            this.key = key;
+            this.value = value;
+        }
+
+        public  static ConditionEnum getCondition(String key){
+            return Arrays.stream(ConditionEnum.values()).filter(x -> Objects.equals(x.key, key)).findFirst().orElse(null);
+        }
+    }
+

放入数据结构中进行判断从而减少if..else

+
public class MyTest {
+    public static void main(String[] args) {
+        String val = "1";
+        condition(val);
+        System.out.println("--------------");
+        newCondition(val);
+    }
+    public static void condition(String val) {
+        if ("1".equals(val)){
+            System.out.println("val:" + 1111111);
+        }else if ("2".equals(val)){
+            System.out.println("val:" + 2222222);
+        }else if ("3".equals(val)){
+            System.out.println("val:" + 3333333);
+        }else {
+            System.out.println("val:" + val);
+        }
+    }
+    public static void newCondition(String val){
+        Map<String, Function<?,?>> map = new HashMap<>();
+        map.put("1",(action) -> 1111111);
+        map.put("2",(action) -> 2222222);
+        map.put("3",(action) -> 3333333);
+        System.out.println("val:" + (map.get(val) != null ? map.get(val).apply(null) : val));
+    }
+
+}
+

使用switch..case代替if..else

+
public class MyTest {
+    public static void main(String[] args) {
+        String val = "FOUR";
+        condition(val);
+        System.out.println("--------------");
+        newCondition(val);
+    }
+
+    public static void condition(String val) {
+        if ("ONE".equals(val)) {
+            System.out.println("val:" + 1111111);
+        } else if ("TWO".equals(val)) {
+            System.out.println("val:" + 2222222);
+        } else if ("THREE".equals(val)) {
+            System.out.println("val:" + 3333333);
+        } else {
+            System.out.println("val:" + val);
+        }
+    }
+
+    public static void newCondition(String val) {
+        switch (val) {
+            case "ONE":
+                System.out.println("val:" + 1111111);
+                break;
+            case "TWO":
+                System.out.println("val:" + 2222222);
+                break;
+            case "THREE":
+                System.out.println("val:" + 3333333);
+                break;
+            default:
+                System.out.println("val:" + val);
+        }
+    }
+}
+

尽早结束if使代码结构更清晰

+
public class MyTest {
+    public static void main(String[] args) {
+        String val = "222";
+        condition(val);
+        System.out.println("--------------");
+        newCondition(val);
+    }
+
+    public static void condition(String val) {
+        if ("ONE".equals(val)) {
+            System.out.println("val:" + 1111111);
+        } else if ("TWO".equals(val)) {
+            System.out.println("val:" + 2222222);
+        } else if ("THREE".equals(val)) {
+            System.out.println("val:" + 3333333);
+        } else {
+            System.out.println("val:" + val);
+        }
+    }
+
+    public static void newCondition(String val) {
+        if ("ONE".equals(val)) {
+            System.out.println("val:" + 1111111);
+            return;
+        }
+        if ("TWO".equals(val)) {
+            System.out.println("val:" + 2222222);
+            return;
+        }
+        if ("THREE".equals(val)) {
+            System.out.println("val:" + 3333333);
+            return;
+        }
+        System.out.println("val:" + val);
+    }
+}
+

使用职责链模式代替if..else

+
public class MyTest {
+    public static void main(String[] args) {
+        String val = "1";
+        condition(val);
+        System.out.println("--------------");
+        newCondition(val);
+    }
+
+    public static void condition(String val) {
+        if ("ONE".equals(val)) {
+            System.out.println("val:" + 1111111);
+        } else if ("TWO".equals(val)) {
+            System.out.println("val:" + 2222222);
+        } else if ("THREE".equals(val)) {
+            System.out.println("val:" + 3333333);
+        } else {
+            System.out.println("val:" + val);
+        }
+    }
+
+    public static void newCondition(String val) {
+        One one = new One();
+        Two two = new Two();
+        Three three = new Three();
+
+        // 设置调用链,可设置成死循环
+        one.setAbstractHandler(two);
+        two.setAbstractHandler(three);
+
+        // 执行
+        one.exec(val);
+    }
+}
+
+abstract class AbstractHandler {
+
+    protected AbstractHandler abstractHandler;
+
+    protected void setAbstractHandler(AbstractHandler abstractHandler) {
+        this.abstractHandler = abstractHandler;
+    }
+
+    protected abstract void exec(String val);
+
+}
+
+class One extends AbstractHandler {
+
+    @Override
+    protected void exec(String val) {
+        if (!val.equals("ONE")){
+            abstractHandler.exec(val);
+            return;
+        }
+        System.out.println("val:" + 1111111);
+    }
+}
+
+class Two extends AbstractHandler {
+
+    @Override
+    protected void exec(String val) {
+        if (!val.equals("TWO")){
+            abstractHandler.exec(val);
+            return;
+        }
+        System.out.println("val:" + 2222222);
+    }
+}
+
+class Three extends AbstractHandler {
+
+    @Override
+    protected void exec(String val) {
+        System.out.println(val.equals("THREE") ? "val:" + 3333333 : "val:" + val);
+    }
+}
+

使用模板方法模式代替if..else

+
public class MyTest {
+    public static void main(String[] args) {
+        String val = "ONE";
+        condition(val);
+        System.out.println("--------------");
+        newCondition(val);
+    }
+
+    public static void condition(String val) {
+        if ("ONE".equals(val)) {
+            System.out.println("val:" + 1111111);
+        } else if ("TWO".equals(val)) {
+            System.out.println("val:" + 2222222);
+        } else if ("THREE".equals(val)) {
+            System.out.println("val:" + 3333333);
+        } else {
+            System.out.println("val:" + val);
+        }
+    }
+
+    public static void newCondition(String val) {
+        if (!Objects.equals(val, "ONE") && !Objects.equals(val, "TWO") && !Objects.equals(val, "THREE")) {
+            System.out.println("val:" + val);
+            return;
+        }
+        List<ConditionTemplate> list = new ArrayList<>(Arrays.asList(new One(), new Two(), new Three()));
+        for (ConditionTemplate item : list) {
+            item.template(val);
+        }
+    }
+
+}
+
+abstract class ConditionTemplate {
+
+    public void template(String val){
+        if (supportIns(val)){
+            support();
+        }
+    }
+
+    public abstract void support();
+
+    public abstract boolean supportIns(String val);
+
+}
+
+class One extends ConditionTemplate {
+    @Override
+    public void support() {
+        System.out.println("val:" + 1111111);
+    }
+    @Override
+    public boolean supportIns(String val) {
+        return "ONE".equals(val);
+    }
+}
+
+class Two extends ConditionTemplate {
+    @Override
+    public void support() {
+        System.out.println("val:" + 2222222);
+    }
+    @Override
+    public boolean supportIns(String val) {
+        return "TWO".equals(val);
+    }
+}
+
+class Three extends ConditionTemplate {
+    @Override
+    public void support() {
+        System.out.println("val:" + 3333333);
+    }
+    @Override
+    public boolean supportIns(String val) {
+        return "THREE".equals(val);
+    }
+}
+

使用工厂方法模式代替if..else

+
public class MyTest {
+    public static void main(String[] args) {
+        String val = "ONE";
+        condition(val);
+        System.out.println("--------------");
+        newCondition(val);
+    }
+
+    public static void condition(String val) {
+        if ("ONE".equals(val)) {
+            System.out.println("val:" + 1111111);
+        } else if ("TWO".equals(val)) {
+            System.out.println("val:" + 2222222);
+        } else if ("THREE".equals(val)) {
+            System.out.println("val:" + 3333333);
+        } else {
+            System.out.println("val:" + val);
+        }
+    }
+
+    public static void newCondition(String val) {
+         Map<String, ConditionFactory> operationMap = new HashMap<>();
+        operationMap.put("ONE",new One());
+        operationMap.put("TWO",new Two());
+        operationMap.put("THREE",new Three());
+        if (operationMap.get(val) == null) {
+            System.out.println("val:" + val);
+        }else {
+            operationMap.get(val).printCondition();
+        }
+    }
+
+}
+
+interface ConditionFactory{
+    void printCondition();
+}
+
+class One implements ConditionFactory {
+    @Override
+    public void printCondition() {
+        System.out.println("val:" + 1111111);
+    }
+}
+
+class Two implements ConditionFactory {
+    @Override
+    public void printCondition() {
+        System.out.println("val:" + 2222222);
+    }
+}
+
+class Three implements ConditionFactory {
+    @Override
+    public void printCondition() {
+        System.out.println("val:" + 3333333);
+    }
+}
+

使用注解代替if..else

+ +
@Retention(RetentionPolicy.RUNTIME)  
+@Target(ElementType.TYPE)  
+public @interface PayCode {  
+
+     String value();    
+     String name();  
+}
+
@PayCode(value = "alia", name = "支付宝支付")  
+@Service
+public class AliaPay implements IPay {  
+
+     @Override
+     public void pay() {  
+         System.out.println("===发起支付宝支付===");  
+     }  
+}  
+
+
+@PayCode(value = "weixin", name = "微信支付")  
+@Service
+public class WeixinPay implements IPay {  
+
+     @Override
+     public void pay() {  
+         System.out.println("===发起微信支付===");  
+     }  
+} 
+
+
+@PayCode(value = "jingdong", name = "京东支付")  
+@Service
+public class JingDongPay implements IPay {  
+
+     @Override
+     public void pay() {  
+        System.out.println("===发起京东支付===");  
+     }  
+}
+
@Service
+public class PayService2 implements ApplicationListener<ContextRefreshedEvent> {  
+
+     private static Map<String, IPay> payMap = null;  
+
+     @Override
+     public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {  
+         ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();  
+         Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(PayCode.class);  
+
+         if (beansWithAnnotation != null) {  
+             payMap = new HashMap<>();  
+             beansWithAnnotation.forEach((key, value) ->{  
+                 String bizType = value.getClass().getAnnotation(PayCode.class).value();  
+                 payMap.put(bizType, (IPay) value);  
+             });  
+         }  
+     }  
+
+     public void pay(String code) {  
+        payMap.get(code).pay();  
+     }  
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/java-design/index.html b/blog-site/public/posts/essays/java-design/index.html new file mode 100644 index 00000000..538c2867 --- /dev/null +++ b/blog-site/public/posts/essays/java-design/index.html @@ -0,0 +1,600 @@ + + + + + + + + + + + 如何做好程序设计功能 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

如何做好程序设计功能

+ 2022.08.02 +
+

产品需求澄清、PN排期及任务分解

+

开发设计评审

+

功能设计流程图

+

与外部系统交互、本系统模块之间流程,比较好用的画圈软件draw .io或在线的process on

+

数据库设计

+

从DDD角度界限上下文、ER图、评审表结构设计是否合理,表的关联关系是否合理、是否创建索引、是否大数据量表考虑放到分片库以及分片字段设计

+

缓存设计

+
    +
  • 缓存结构是否合理 redis结构选择(string、hash、 list、set): +
      +
    • 对于C端高并发接口,hash结构要慎重用,key不能用固定的key,防止key倾 斜,可以采用string结构,value尽可能压缩下,比如 {“groupId:”:“536363737”, “groupId”:“989898989”}改为"536363737#989898989",大大节省redis空间。
    • +
    • 对于数据量很大,单条valve报文很大的业务,如果经常判断某个值是否存在,可以考虑采用set结构,利用sismember方法,时间复杂度O(1)
    • +
    +
  • +
  • 缓存过期时间是否合理:不能怕脑袋胡乱给一个时间,如果存放的数量,过期时间一定要给短一些,比如2日分钟或半小时,否则增长很长,打爆redis
  • +
  • 缓存是否设计考虑预热和重建机制
  • +
  • 缓存中尽可能不能使用delete命令;delete会阻塞redis,特别是存放数量很大的hash、set、list, 容易阻塞redis,引1发事故,替换方案:expire -1
  • +
  • 是否是否设计二级缓存;本地缓存+分布式缓存,实现方式caffine或quavatredis,对于数据量不大热点数据(一般1W条以下,单条报文10kb以下,比如网点数据、航线数据)可以放本地缓存,这些热点数据查询非常频繁,容易造成redis倾斜,本地缓存可以抗这种高井发流量.
  • +
  • 防穿透处理 +
      +
    • 提供c端的接口,查询不到数据,尽可能不要穿透到数据库,防止打爆数据库,查不到一定记得打印告營日志,及时人工千预
    • +
    • 提前将数据预热到缓存中(系统启动预热或者运营后台有手动刷新缓存按钮或者有个job刷缓存)
    • +
    +
  • +
+

job设计

+
    +
  • job逻辑要考虑幂等设计
  • +
  • job逻辑尽可能轻量级,不要太重,导致执行逻辑很久(如果确实逻辑比较复杂,可以分拆job或者从代码层面优化,例如:分批井行处理、减少单次处理数据〕
  • +
  • 构建数据到缓存类似的job,尽可能选择时间在晚上12点以后,尽量不要白天进行,因为白天流量很大,重建缓存容易引起接口抖动
  • +
  • 对于定时执行的job,设计执行时间的时候,要慎重考虑线上整个job执行的时间,根据这个时间配置cron表达式,不要拍脑袋随意设置,
  • +
  • 不然本次job没执行完,下一次job执行的时候容易错过。
  • +
  • 对于大表数据(百万、千万甚至亿级),可以利用xxjob、saturn等分布式调度框架的分片处理能力,并行刷数据,提供处理速度。
  • +
+

接口设计

+
    +
  • 高并发接口必须在测试环境压测,清楚的知道接口支持的QPS,另外根据压测报告给出的优化建议,及时调整线上的配置、代码优化。
  • +
  • 提供给C端的接口,要清楚曝光位省、然后根据接口可能预计调用的流量、压测的接口最大支持的QPS,决定是否扩容、是否需要限流及兜底逻镇、是否熔断
  • +
  • 程序中涉及的异常信息,及时配置错误监控告警,遇事做到心中有数。
  • +
  • 对于业务关键位置,及时打印info级别的业务追踪日志,如果是高并发接口,要做好开关,能秒级关闭,因为打印日志也特别耗性能。
  • +
  • 程序设计方面,高井发接口,尽可能使用缓存、能异步就异步 (一般用mq,不要用多线程异步)、 尽可能采用无锁设计防止线程Lock CPU冲高,批量写库
  • +
  • 接口前置逻铜提前,尽早过滤掉不合规運银,缩短接口整个调用链路时长,提升接口整体吞吐量。
  • +
  • 接口要考虑无状态设计、昇等设计(考虑分布式锁唯—key)、安全设计(接口方法签名、是否接第三方防刷服务判断是否黑产用户) +
    +

    无状态服务:客户端的每次请求必须具备自描述信息,通过这些信息识别客户端身份。服务端不保存任何客户端请求者信息 +有状态服务: 即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如 tomcat 中的 session

    +
    +
  • +
  • 对第三方服务做到零信任,考虑降级、熔断,与业务方确认好兜底逻辑处理。
  • +
  • 工具类尽可能使用apache、google等出的、 一些github小众的开源框架很多在高井发场最有bug和性能瓶颈。
  • +
  • 接口的时间复杂度,部分在设计的时候尽可能保证是0 (1),例如调用第三方接口,for循环调用的,是否可以提到缩环外,传多个id批量调用一次,时间复杂度从0(n)降为0(1)
  • +
+

监控设计

+
    +
  • 核心接西做好监控,例如:调用量上涨/下跌80%、接口健康度监控 (5分钟error超过100次、5分钟接口超过1s 100日次、5分钟不可用次数超过1000次)
  • +
  • 应用监控,例如:应用cpu、内存层、gc、繁忙线程数、数据库连接池连接数监控告警
  • +
  • 中间件监控,例如:redis可用性、cpu-内存-流量-大key-热key-慢查询监控、kafka可用性-消息积压情况-丢失率监控告警
  • +
  • 数据库监控,例如:mysql cpu、慢sql监控告警
  • +
  • 业务监控,例如:发券量暴涨或暴跌、活动注册人数同比或环比异常
  • +
+

预案设计

+

设计预案减少损失

+
    +
  • 设计全局开关(系统接近不可用,通过配置中心全局开关快速切断接口流量,保证系統可用)
  • +
  • 遇到系统故障,非核心功能通过配置中心开关下线,保证整体服务可用
  • +
  • 应用能自动扩容(这里就要求应用做到无状态设计,比如:我们之间有一些应用有调用加密机,但是新应用节点需要申请白名单,这种设计方式就没法自动扩容
  • +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/java-dict/index.html b/blog-site/public/posts/essays/java-dict/index.html new file mode 100644 index 00000000..9e162fe6 --- /dev/null +++ b/blog-site/public/posts/essays/java-dict/index.html @@ -0,0 +1,633 @@ + + + + + + + + + + + 编程常用词汇汇总 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

编程常用词汇汇总

+ 2023.02.13 +
+

QPS

+

即 Queries Per Second的缩写,每秒能处理查询数目。是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。

+

TPS

+

即 Transactions Per Second的缩写,每秒处理的事务数目。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使用的时间和完成的事务个数,最终利用这些信息作出的评估分。

+

RPS

+

即 Requests Per Second的缩写,每秒能处理的请求数目。等效于QPS。

+

PV

+

即 Page view,页面浏览量;用户每一次对网站中的每个页面访问均被记录1次。用户对同一页面的多次刷新,访问量累计。

+

UV

+

即 Unique visitor,独立访客;通过客户端的cookies实现。即同一页面,客户端多次点击只计算一次,访问量不累计。

+

RT

+

即 Response time 响应时间,处理一次请求所需要的平均处理时间。

+

ROI

+

即 Return on Investment,也就是投资回报率(投入产出比),它是一个投资术语。ROI 对于工作而言,主要体现在:绩效晋升、技术能力。

+

吞吐量

+

系统的吞吐量与请求对CPU的消耗、外部接口、IO等等紧密关联。单个请求对CPU消耗越高,外部系统接口、IO速度越慢,系统吞吐能力越低,反之越高。

+

Code Review

+

Code Review 翻译成中文是代码评审。Code Review 是一种通过复查代码提高代码质量的过程,通过这个机制我们可以对代码、测试过程和注释进行检查。

+

重构

+
    +
  • 重构(名词形式): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
  • +
  • 重构(动词形式): 使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构。
  • +
+

重构的目的是使软件更容易被理解和修改。可以在软件内部做很多修改,但必须对软件可观察的外部行为只造成很小的变化,甚至不造成变化。

+

CDN

+

全称Content Delivery Network,即内容分发网络。通过将内容缓存在终端用户附近,使用户可就近取得所需内容,解决 Internet网络拥挤的状况,提高用户访问网站的响应速度。

+

CND加速主要是加速静态资源,如网站上面上传的图片、媒体,以及引入的一些Js、css等文件。 +CDN是只对网站的某一个具体的域名加速。如果同一个网站有多个域名,则访问加入CDN的域名获得加速效果,访问未加入CDN的域名,或者直接访问IP地址,则无法获得CDN效果。

+

DNS

+

即Domain Name System,域名系统,是将域名解析为IP地址的系统。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/java-hashmap/index.html b/blog-site/public/posts/essays/java-hashmap/index.html new file mode 100644 index 00000000..2fe1cf0d --- /dev/null +++ b/blog-site/public/posts/essays/java-hashmap/index.html @@ -0,0 +1,1171 @@ + + + + + + + + + + + HashMap详解 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

HashMap详解

+ 2021.05.03 +
+

相关概念

+
    +
  • capacity: 容量,默认16;
  • +
  • loadFactor: 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容;
  • +
  • threshold: 阈值;阈值 = 容量 * 负载因子。默认12;
  • +
  • hash碰撞(hash冲突):两个不同的输入值,根据同一散列函数计算出的散列值相同的现象叫做碰撞。hash碰撞就是用同一hash散列函数计算出相同的散列值;当插入hashmap中元素的key出现重复时,这个时候就发生了hash碰撞;
  • +
+

结构

+

HashMap结构

+
    +
  • JDK1.7:数组 + 单向链表;
  • +
  • JDK1.8: 数组 + 单向链表/红黑树;
  • +
+

在JDK1.8时,如果存储Map中数组元素对应的索引的每个链表超过8,就将单向链表转化为红黑树;当红黑树的节点少于6个的时候又开始使用链表。

+

为什么要使用红黑树

+

当有发生大量的hash冲突时,因为链表遍历效率很慢,为了提升查询的效率,所以使用了红黑树的数据结构。

+

为什么不一开始就用红黑树代替链表结构

+

JDK文档注释:

+
+

Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). +And when they become too small (due to removal or resizing) they are converted back to plain bins.

+
+

单个 TreeNode 需要占用的空间大约是普通 Node 的两倍,所以只有当包含足够多的 Nodes 时才会转成 TreeNodes,而是否足够多就是由 TREEIFY_THRESHOLD 的值(默认值8)决定的。而当桶中节点数由于移除或者 resize 变少后,又会变回普通的链表的形式,以便节省空间,这个阈值是 UNTREEIFY_THRESHOLD(默认值6)。

+

为什么树化阈值为8

+

JDK1.8HashMap文档注释:

+
+

如果 hashCode 分布良好,也就是 hash 计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。 +在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率,通常我们的 Map 里面是不会存储这么多的数据的,所以通常情况下,并不会发生从链表向红黑树的转换。

+
+

HashMap是通过hash算法,来判断对象应该放在哪个桶里面的;JDK 并不能阻止我们用户实现自己的哈希算法,如果我们故意把哈希算法变得不均匀,那么每次存放对象很容易造成hash冲突。

+

链表长度超过 8 就转为红黑树的设计,更多的是为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低,而此时转为红黑树更多的是一种保底策略,用来保证极端情况下查询的效率。红黑树的引入保证了在大量hash冲突的情况下,HashMap还具有良好的查询性能。

+

为什么树化阈值和链表阈值不设置成一样

+

为了防止出现节点个数频繁在一个相同的数值来回切换。

+

举个极端例子,现在单链表的节点个数是9,开始变成红黑树,然后红黑树节点个数又变成8,就又得变成单链表,然后节点个数又变成9,就又得变成红黑树,这样的情况消耗严重浪费。因此干脆错开两个阈值的大小,使得变成红黑树后“不那么容易”就需要变回单链表,同样,使得变成单链表后,“不那么容易”就需要变回红黑树。

+

引入红黑树后,如果单链表节点个数超过8个是否一定会树化

+

不一定,在进行树化之前会进行判断(n = tab.length) < MIN_TREEIFY_CAPACITY)是否需要扩容,如果表中数组元素小于这个阈值(默认是64),就会进行扩容。 因为扩容不仅能增加表中的容量,还能缩短单链表的节点数,从而更长远的解决链表遍历慢问题。

+
    /**
+     * Replaces all linked nodes in bin at index for given hash unless
+     * table is too small, in which case resizes instead.
+     */
+    final void treeifyBin(Node<K,V>[] tab, int hash) {
+        int n, index; Node<K,V> e;
+        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
+            resize();
+        else if ((e = tab[index = (n - 1) & hash]) != null) {
+            TreeNode<K,V> hd = null, tl = null;
+            do {
+                TreeNode<K,V> p = replacementTreeNode(e, null);
+                if (tl == null)
+                    hd = p;
+                else {
+                    p.prev = tl;
+                    tl.next = p;
+                }
+                tl = p;
+            } while ((e = e.next) != null);
+            if ((tab[index] = hd) != null)
+                hd.treeify(tab);
+        }
+    }
+

容量

+

为什么负载因子默认是0.75

+

HashMap中的负载因子这个值现在在JDK的源码中默认是0.75:

+
/**
+ * The load factor used when none specified in constructor.
+ */
+static final float DEFAULT_LOAD_FACTOR = 0.75f;
+

JDK的官方文档中解释如下:

+
+

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. +Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). +The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. +If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.

+
+

大意:一般来说,默认的负载因子(0.75)在时间和空间成本之间提供了很好的权衡。更高的值减少了空间开销,但增加了查找成本(反映在HashMap类的大多数操作中,包括get和put)。在设置映射的初始容量时,应该考虑映射中预期的条目数及其负载因子,以便最小化重哈希操作的数量。如果初始容量大于最大条目数除以负载因子,则不会发生重新散列操作。

+

负载因子和hashmap中的扩容有关,当hashmap中的元素大于临界值(threshold = loadFactor * capacity)就会扩容。所以负载因子的大小决定了什么时机扩容,扩容又影响到了hash碰撞的频率。所以设置一个合理的负载因子可以有效的避免hash碰撞。

+

设置为0.75的其他解释:

+
    +
  • 根据数学公式推算。这个值在log(2)的时候比较合理;
  • +
  • 为了提升扩容效率,HashMap的容量有一个固定的要求,那就是一定是2的幂。如果负载因子是3/4的话,那么和容量的乘积结果就可以是一个整数;
  • +
+

如果指定容量大小为10,那么实际大小是多少

+

实际大小是16。其容量为不小于指定容量的2的幂数。

+

为什么容量始终是2的N次方?

+

为了减少Hash碰撞,尽量使Hash算法的结果均匀分布。

+

当使用put方法时,到底存入HashMap中的那个数组中?这时是通过hash算法决定的,如果某一个数组中的链表过长旧会影响查询的效率;那么为了避免出现hash碰撞,让hash尽可能的散列分布,就需要在hash算法上做文章。

+

JDK1.7通过逻辑与运算,来判断这个元素该进入哪个数组;在下面的代码中length的长度始终为不小于指定容量的2的幂数。

+
static int indexFor(int h, int length) {
+    return h & (length - 1);
+}
+

为了更好的理解举个例子:假设h=2或h=3,length=15,进行与运算,最终逻辑与运算后的结果是一致的,因为最终结果是一致的所以就发生了hash碰撞,这种问题多了以后会造成容器中的元素分布不均匀,都分配在同一个数组上,在查询的时候就减慢了查询的效率,另一方面也造成空间的浪费。

+
-- 2转换为2进制与15-1进行&运算
+  0000 0010
+& 0000 1110 
+———————————— 
+  0000 1110
+
+-- 3转换为2进制与15-1进行&运算
+  0000 0011
+& 0000 1110 
+————————————
+  0000 1110
+

为了避免上面length=15这类问题出现,所以集合的容量采用必须是2的N次幂这种方式,因为2的N次幂的结果减一转换为二进制后都是以...1111结尾的,所以在进行逻辑与运算时碰撞几率小。

+

在JDK1.8中,在putVal()方法中通过i = (n - 1) & hash来计算key的散列地址:

+
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
+                   boolean evict) {
+        // 此处省略了代码
+        // i = (n - 1) & hash]
+        if ((p = tab[i = (n - 1) & hash]) == null)
+            
+            tab[i] = newNode(hash, key, value, null);
+        
+ 
+        else {
+            // 省略了代码
+        }
+}
+
+

这里的 “&” 等同于 %",但是"%“运算的速度并没有”&“的操作速度快;”&“操作能代替”%“运算,必须满足一定的条件,也就是a%b=a&(b-1)仅当b是2的n次方的时候方能成立。

+
+

容器容量怎么保持始终为2的N次方?

+

HashMaptableSizeFor()方法做了处理,能保证n永远都是2次幂。

+

如果用户制定了初始容量,那么HashMap会计算出比该数大的第一个2的幂作为初始容量;另外就是在扩容的时候,也是进行成倍的扩容,即4变成8,8变成16。

+
/**
+ * Returns a power of two size for the given target capacity.
+ */
+static final int tableSizeFor(int cap) {
+
+    // 假设n=17
+    // n = 00010001 - 00010000 = 00010000 = 16
+    int n = cap - 1;
+
+    // n = (00010000 | 00001000) = 00011000 = 24
+    n |= n >>> 1;
+
+    // n = (00011000 | 00000110) = 00011110 = 30
+    n |= n >>> 2;
+
+    // n = (00011110 | 00000001) = 00011111 = 31
+    n |= n >>> 4;
+
+    // n = (00011111 | 00000000) = 00011111 = 31
+    n |= n >>> 8;
+
+    // n = (00011111 | 00000000) = 00011111 = 31
+    n |= n >>> 16;
+
+    // n = 00011111 = 31,MAXIMUM_CAPACITY:Integer的最大长度
+    // (31 < 0) ? 1 : (31 >= Integer的最大长度) ? Integer的最大长度 : 31 + 1 ;
+    // 即最终返回 32 = 2 的 (n=5)次方
+    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
+}
+

发现上面在进行>>>操作时会将cap的二进制值变为最高位后边全是1,00010001 -> 00011111这个算法就导致了任意传入一个数值,会将该数字变为它的2倍减1,因为任何尾数全为1的在加1都为2的倍数。

+

至于开头减1,是因为如果给定的n已经是2的次幂,但是不进行减1操作的话,那么得到的值就是大于给定值的最小2的次幂值,例如传入4就会返回8。

+

为什么最大右移到16位,因为可以得到的最大值是32个1,这个是int类型存储变量的最大值,在往后就没意义了。

+

默认初始化容量为什么是16

+

没有找到相关解释,推断这应该就是个经验值,既然一定要设置一个默认的2^n 作为初始值,那么就需要在效率和内存使用上做一个权衡。这个值既不能太小,也不能太大。太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。所以,16就作为一个经验值被采用了。

+

关于默认容量的定义:

+
/**
+ * The default initial capacity - MUST be a power of two.
+ */
+static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
+

故意把16写成1 << 4这种形式,就是提醒开发者,这个地方要是2的次幂。

+

初始化容量设置多少合适

+

当我们使用HashMap(int initialCapacity)来初始化容量的时候,HashMap并不会使用我们传进来的initialCapacity直接作为初始容量。JDK会默认帮我们计算一个相对合理的值当做初始容量。所谓合理值,其实是找到第一个比用户传入的值大的2的幂。

+

如果创建hashMap初始化容量设置为7,那么JDK通过计算会创建一个初始化为8的hashMap。当hashMap中的元素到0.75 * 8 = 6就会进行扩容,这明显是我们不希望看到的。

+

参考JDK8中putAll方法中的实现:

+
(int) ((float) expectedSize / 0.75F + 1.0F);
+

通过expectedSize / 0.75F + 1.0F计算,7/0.75 + 1 = 10 ,10经过JDK处理之后,会被设置成16,这就大大的减少了扩容的几率。

+

当我们明确知道HashMap中元素的个数的时候,把默认容量设置成expectedSize / 0.75F + 1.0F 是一个在性能上相对好的选择,但是,同时也会牺牲些内存。

+

这个算法在guava中有实现,开发的时候,可以直接通过Maps类创建一个HashMap:

+
Map<String, String> map = Maps.newHashMapWithExpectedSize(7);
+
public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
+    return new HashMap(capacity(expectedSize));
+}
+
+static int capacity(int expectedSize) {
+    if (expectedSize < 3) {
+        CollectPreconditions.checkNonnegative(expectedSize, "expectedSize");
+        return expectedSize + 1;
+    } else {
+        return expectedSize < 1073741824 ? (int)((float)expectedSize / 0.75F + 1.0F) : 2147483647;
+    }
+}
+

扩容

+
    +
  • JDK1.7: 先扩容在添加元素;
  • +
  • JDK1.8: 先添加元素在扩容;
  • +
+

为什么要进行扩容

+

随着HashMap中的元素增加,Hash碰撞导致获取元素方法的效率就会越来越低,为了保证获取元素方法的效率,所以针对HashMap中的数组进行扩容。扩容数组的方式只能再去开辟一个新的数组,并把之前的元素转移到新数组上。

+
+

PS 如何能避免哈希碰撞?

+
    +
  • 容量太小。容量小,碰撞的概率就高了。狼多肉少,就会发生争抢。
  • +
  • hash算法不够好。算法不合理,就可能都分到同一个或几个桶中。分配不均,也会发生争抢。
  • +
+
+

什么时机进行扩容

+

HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。在HashMap中,threshold = loadFactor * capacity。默认情况下负载因子为0.75,理解为当容器中元素到容器的3/4时就会扩容。

+
 if (++size > threshold)
+    resize();
+

HashMap的容量是有上限的,必须小于1<<30,即1073741824。如果容量超出了这个数,则不再增长,且阈值会被设置为Integer.MAX_VALUE

+
// Java8
+if (oldCap >= MAXIMUM_CAPACITY) {
+    threshold = Integer.MAX_VALUE;
+    return oldTab;
+}
+// Java7
+if (oldCapacity == MAXIMUM_CAPACITY) { 
+    threshold = Integer.MAX_VALUE;
+    return;
+}
+

1.7扩容

+
    +
  • 新容量 = 旧容量 * 2
  • +
  • 新阈值 = 新容量 * 负载因子
  • +
+
void addEntry(int hash, K key, V value, int bucketIndex) {  
+    //sizeThe number of key-value mappings contained in this map.  
+    //thresholdThe next size value at which to resize (capacity * load factor)  
+    //数组扩容条件:1.已经存在的key-value mappings的个数大于等于阈值  
+    //             2.底层数组的bucketIndex坐标处不等于null  
+    if ((size >= threshold) && (null != table[bucketIndex])) {  
+        resize(2 * table.length);//扩容之后,数组长度变了  
+        hash = (null != key) ? hash(key) : 0;//为什么要再次计算一下hash值呢  
+        bucketIndex = indexFor(hash, table.length);//扩容之后,数组长度变了,在数组的下标跟数组长度有关,得重算。  
+    }  
+    createEntry(hash, key, value, bucketIndex);  
+} 
+
void resize(int newCapacity) {   //传入新的容量
+    Entry[] oldTable = table;    //引用扩容前的Entry数组
+    int oldCapacity = oldTable.length;
+    if (oldCapacity == MAXIMUM_CAPACITY) {  //扩容前的数组大小如果已经达到最大(2^30)
+        threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
+        return;
+    }
+
+    Entry[] newTable = new Entry[newCapacity];  //初始化一个新的Entry数组
+    transfer(newTable);                         //!!将数据转移到新的Entry数组里
+    table = newTable;                           //HashMap的table属性引用新的Entry数组
+    threshold = (int) (newCapacity * loadFactor);//修改阈值
+}
+

通过transfer方法将旧数组上的元素转移到扩容后的新数组上

+
void transfer(Entry[] newTable) {
+    Entry[] src = table;                   //src引用了旧的Entry数组
+    int newCapacity = newTable.length;
+    for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
+        Entry<K, V> e = src[j];             //取得旧Entry数组的每个元素
+        if (e != null) {
+            src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
+            do {
+                Entry<K, V> next = e.next;
+                int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
+                e.next = newTable[i]; //标记[1]
+                newTable[i] = e;      //将元素放在数组上
+                e = next;             //访问下一个Entry链上的元素
+            } while (e != null);
+        }
+    }
+}
+

1.8扩容

+

容量变为原来的2倍,阈值也变为原来的2倍。容量和阈值都变为原来的2倍时,负载因子还是不变。

+

在1.8时做了一些优化,文档注释写的很清楚:“元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置”。也就是对比1.7的迁移到新的数组上省去了重新计算hash值的时间。

+

这里的"2次幂的位置"是指长度为原来数组元素的两倍的位置;举个例子,现在容量为16,要扩容到32,要将之前的元素迁移过去,要根据hash值来判断迁移过去的位置;假设元素A:hash值:0101 0101;根据代码h & (length - 1)可得元素A & 15元素A & 31

+
扩容之前的位置:
+  0101 0101
+& 0000 1111
+————————————
+  0000 0101
+
+扩容之后的位置:
+  0101 0101
+& 0001 1111
+————————————
+  0001 0101
+

发现规律:扩容前的hash值和扩容后的hash值,如果元素A二进制形式第三位如果是0,扩容之后就还是原来的位置,如果是1扩容后就是原来的位置加16,而16就是扩容的大小。

+
 /**
+     * Initializes or doubles table size.  If null, allocates in
+     * accord with initial capacity target held in field threshold.
+     * Otherwise, because we are using power-of-two expansion, the
+     * elements from each bin must either stay at same index, or move
+     * with a power of two offset in the new table.
+     *
+     * @return the table
+     */
+    final Node<K,V>[] resize() {
+        Node<K,V>[] oldTab = table;
+        int oldCap = (oldTab == null) ? 0 : oldTab.length;
+        int oldThr = threshold;
+        int newCap, newThr = 0;
+        if (oldCap > 0) {
+            if (oldCap >= MAXIMUM_CAPACITY) {
+                threshold = Integer.MAX_VALUE;
+                return oldTab;
+            }
+            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
+                     oldCap >= DEFAULT_INITIAL_CAPACITY)
+                newThr = oldThr << 1; // double threshold
+        }
+        else if (oldThr > 0) // initial capacity was placed in threshold
+            newCap = oldThr;
+        else {               // zero initial threshold signifies using defaults
+            newCap = DEFAULT_INITIAL_CAPACITY;
+            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
+        }
+        if (newThr == 0) {
+            float ft = (float)newCap * loadFactor;
+            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
+                      (int)ft : Integer.MAX_VALUE);
+        }
+        threshold = newThr;
+        @SuppressWarnings({"rawtypes","unchecked"})
+        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
+        table = newTab;
+        if (oldTab != null) {
+            for (int j = 0; j < oldCap; ++j) {
+                Node<K,V> e;
+                if ((e = oldTab[j]) != null) {
+                    oldTab[j] = null;
+                    if (e.next == null)
+                        newTab[e.hash & (newCap - 1)] = e;
+                    else if (e instanceof TreeNode)
+                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
+                    else { // preserve order
+                        Node<K,V> loHead = null, loTail = null;
+                        Node<K,V> hiHead = null, hiTail = null;
+                        Node<K,V> next;
+                        do {
+                            next = e.next;
+                            if ((e.hash & oldCap) == 0) {
+                                if (loTail == null)
+                                    loHead = e;
+                                else
+                                    loTail.next = e;
+                                loTail = e;
+                            }
+                            else {
+                                if (hiTail == null)
+                                    hiHead = e;
+                                else
+                                    hiTail.next = e;
+                                hiTail = e;
+                            }
+                        } while ((e = next) != null);
+                        if (loTail != null) {
+                            loTail.next = null;
+                            newTab[j] = loHead;
+                        }
+                        if (hiTail != null) {
+                            hiTail.next = null;
+                            newTab[j + oldCap] = hiHead;
+                        }
+                    }
+                }
+            }
+        }
+        return newTab;
+    }
+

参考文章

+ +
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/java-improve/index.html b/blog-site/public/posts/essays/java-improve/index.html new file mode 100644 index 00000000..f63cfd46 --- /dev/null +++ b/blog-site/public/posts/essays/java-improve/index.html @@ -0,0 +1,639 @@ + + + + + + + + + + + 接口优化 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

接口优化

+ 2022.12.20 +
+

接口优化

+

线上接口很慢,线上生产问题,我们绝对不能马虎放过抱着侥幸心理,必须要找到根本原因及时处理,防止下次留下更大的坑.大致思路要定位接口问题,然后具体问题具体分析,讨论不同解决方案.

+

定位问题

+

要快速定位接口哪一个环节比较慢,性能瓶颈在哪里,可以使用应用性能监控工具(APM)定位问题。常见工具: skywalking、pinpoint、cat、zipkin。

+

如果应用程序没有接入APM,可以在生产环境装一下arthas,利用trace接口方法和火焰图,大概能分析是那一块比较慢,定位能力稍微有点粗糙。亦可以利用程序中的告警日志定位问题。

+

解决办法

+
    +
  • 扩容;哪里扛不住了哪里扩容,应用自动扩容、redis扩容、mysql在线扩容、kafka分区扩容等;
  • +
  • 应用重启;如果部分节点线程已经扛不住了,就需要重启释放对应资源;
  • +
  • 优化代码逻辑;上面两种是比较应急的做法,如果已经定位出来,就需要优化代码逻辑,完成后走hotfix灰度发版;
  • +
+

常见优化接口性能方案

+

数据库慢sql

+

如果是数据库sql慢,可以使用执行计划去分析一下,常见sql慢的几种情况:

+
    +
  • 锁表;先把锁表的sqlkill一波,在分析具体原因;
  • +
  • 未加索引;添加索引,有可能会锁表,引发一系列问题,需要综合评估;
  • +
  • 索引失效;分析索引失效原因,如:索引列区分度(值大都相同)很低、索引列大量空值、对所索引列加方法转换等;
  • +
  • 小表驱动大表;在连接查询时尽量过滤数据,使用小表驱动大表,使笛卡尔积尽量小一些;
  • +
  • sql太复杂;join超过3张表或者子查询比较多,建议拆分为多个sql,接口间相互调用;比如先从某个著接口查询某个表数据,然后关联字段作为条件从另一个表查询,进行内存拼接;
  • +
  • 返回的数据量数据太多;当超过数据库一定限制的时候返回大量数据就会很慢,可以使用分页多批次完成,针对访问量不多的接口可使用多线程批量查询;
  • +
  • 单表数据量太大;(mysql超500w较慢)如果单表数据量较大,考虑在数据库设计做文章,如:分片分库、利用es存储等;
  • +
+

调用第三方接口慢

+
    +
  • 设置合理的超时时间;调用第三方接口一定要设置合理的超时时间,在设置时一定要大于调用接口的平均相应时间;
  • +
  • 第三方接口大量超时;可以集成sentinel或hystrix限流熔断框架,防止第三方接口拖垮自己的接口(兜底逻辑);
  • +
  • 事务型操作根据实际情况决定是否采用补偿机制(本地消息表);比如新增、修改等操作要考虑对方接口是否支持幂等,防止超发;
  • +
  • 循环调用,改为单次批量调用,减少IO损耗;如:调用根据id查询单条数据的接口,可优化为批量查询接口;
  • +
  • 缓存查询结果;考虑当前查询结果是否能做缓存,如用户信息等短时间内不会变化的信息,根据业务形态来决定;
  • +
+

中间件慢

+
    +
  • redis慢;是否有大key、热key,可接入hotkeys监控;针对热key可以使用本地缓存来抗,针对大key可以将其拆分,采用set结构的sismember等方法
  • +
  • kafka慢;生产端慢:可以使用堵塞队列接收,批量丢消息;消费端慢:消费端慢会造成消息积压,可以扩分区、增加消费节点、增加消费线程,用数据机构接受批量写入库;
  • +
+

程序逻辑慢

+
    +
  • 非法校验逻辑前置;避免无用数据穿透小号系统资源,减少无效调用;
  • +
  • 循环调用改为单次批量调用;在查询数据库或调用第三方接口,能批量就批量,数据在内存组装处理;
  • +
  • 同步调用改为异步调用;在接口没有相互依赖的关系的时候可以将其优化为异步查询;
  • +
  • 非核心逻辑剥离;将接口的大事务拆分为小事务,一些非核心逻辑可以异步处理,可以使用mq异步解耦;
  • +
  • 线程池合理设置参数;不要使用JDK默认参数,如果在高并发的情况下容易OOM,线程池满了以后要重写拒绝策略,考虑告警加数据持久化处理;
  • +
  • 锁合理设置;本地读写锁设计使用不合理,要控制锁的力度,尽量小一些;分布式锁合理使用防止热key;
  • +
  • 优化GC参数;考虑GC是否频繁,调整GC算法,新生代老年代比例,根据长时间观察可以设置出来;
  • +
  • 只打印必要日志;当并发量比较高的时候打印日志也会损耗性能,所以日志应加上开关能不打就不打;
  • +
+

架构优化

+
    +
  • 高并发读逻辑走redis,尽可能不要穿透到DB;redis查询不到也不要查DB,可通过定时任务,MQ写入redis。尽量不要把风险给DB,DB如果挂了整个应用就用不了了;
  • +
  • 设计写逻辑数据,尽量异步、批量处理、分库分表提升写入性能;
  • +
  • 接口接入限流熔断兜底;
  • +
  • 接入监控告警;error日志告警、接口慢查询或者不可用或限流熔断告警、DB告警、中间件告警、应用系统告警等;
  • +
  • 接口需要加动态配置开关;能够快速切断流量或降级某些非核心服务调用;
  • +
  • 设计程序自愈能力;比如如果数据有问题,用配置好的程序逻辑自动去修复;
  • +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/java-lock/index.html b/blog-site/public/posts/essays/java-lock/index.html new file mode 100644 index 00000000..a1dbffa8 --- /dev/null +++ b/blog-site/public/posts/essays/java-lock/index.html @@ -0,0 +1,776 @@ + + + + + + + + + + + Java中常用到的锁 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Java中常用到的锁

+ 2020.04.07 +
+

公平锁

+

指多个线程按照申请锁的顺序来获取锁类似排队打饭 先来后到

+
    +
  • 优点: 所有的线程都能得到资源,不会饿死在队列中。
  • +
  • 缺点: 吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
  • +
+

非公平锁

+

指在多线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取到锁,在高并发的情况下,有可能造成优先级反转或者饥饿现象

+
    +
  • 优点: 可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
  • +
  • 缺点: 你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死.
  • +
+

并发包ReentrantLock的创建可以指定构造函数的boolean类型来得到公平锁或者非公平锁 默认是非公平锁,synchronized也是非公平锁. +源码如下:

+
   /**
+     * Creates an instance of {@code ReentrantLock}.
+     * This is equivalent to using {@code ReentrantLock(false)}.
+     */
+    public ReentrantLock() {
+        sync = new NonfairSync();
+    }
+
+    /**
+     * Creates an instance of {@code ReentrantLock} with the
+     * given fairness policy.
+     *
+     * @param fair {@code true} if this lock should use a fair ordering policy
+     */
+    public ReentrantLock(boolean fair) {
+        sync = fair ? new FairSync() : new NonfairSync();
+    }
+

可重入锁(递归锁)

+

指同一个线程外层函数获得锁之后,内层仍然能获取到该锁,在同一个线程在外层方法获取锁的时候,在进入内层方法或会自动获取该锁. +可重入锁最大的作用就是避免死锁

+

不可重入锁

+

所谓不可重入锁,即若当前线程执行某个方法已经获取了该锁,那么在方法中尝试再次获取锁时,就会获取不到被阻塞

+
    +
  • +

    举个栗子: 当你进入你家时门外会有锁,进入房间后厨房卫生间都可以随便进出,这个叫可重入锁.当你进入房间时,发现厨房,卫生间都有上锁.这个叫不可重入锁

    +
  • +
  • +

    以下的代码证明 synchronized ReentrantLock都是可重入锁

    +
  • +
+

synchronized

+
public class SynchronziedDemo {
+
+   private synchronized void print() {
+       doAdd();
+   }
+   private synchronized void doAdd() {
+       System.out.println("doAdd...");
+   }
+
+   public static void main(String[] args) {
+       SynchronziedDemo synchronziedDemo = new SynchronziedDemo();
+       synchronziedDemo.print(); // doAdd...
+   }
+}
+

ReentrantLock

+
public class ReentrantLockDemo {
+    private Lock lock = new ReentrantLock();
+
+    private void print() {
+        lock.lock();
+        doAdd();
+        lock.unlock();
+    }
+
+    private void doAdd() {
+	// doAdd 方法中加两次锁和解两次锁也可以,不会报错
+	// 如果少了一个unlock()也不会报错,会形成死锁
+        lock.lock();
+        lock.lock();
+        System.out.println("doAdd...");
+        lock.unlock();
+        lock.unlock();
+    }
+
+    public static void main(String[] args) {
+        ReentrantLockDemo reentrantLockDemo = new ReentrantLockDemo();
+        reentrantLockDemo.print();
+    }
+}
+

自旋锁

+

自旋锁(spin lock)是一种非阻塞锁,也就是说,如果某线程需要获取锁,但该锁已经被其他线程占用时,该线程不会被挂起,而是在不断的消耗CPU的时间,不停的试图获取锁.这样的好处是减少线程上线文切换的消耗,缺点就是循环会消耗 CPU.原理是CAS原理

+
public class SpinLock {
+    private AtomicReference<Thread> atomicReference = new AtomicReference<>();
+    private void lock () {
+        System.out.println(Thread.currentThread() + " coming...");
+        while (!atomicReference.compareAndSet(null, Thread.currentThread())) {
+            // loop
+        }
+    }
+
+    private void unlock() {
+        Thread thread = Thread.currentThread();
+        atomicReference.compareAndSet(thread, null);
+        System.out.println(thread + " unlock...");
+    }
+
+    public static void main(String[] args) throws InterruptedException {
+        SpinLock spinLock = new SpinLock();
+        new Thread(() -> {
+            spinLock.lock();
+            try {
+                Thread.sleep(3000);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            System.out.println("hahaha");
+            spinLock.unlock();
+
+        }).start();
+
+        Thread.sleep(1);
+
+        new Thread(() -> {
+            spinLock.lock();
+            System.out.println("hehehe");
+            spinLock.unlock();
+        }).start();
+    }
+}
+
Thread[Thread-0,5,main] coming...
+Thread[Thread-1,5,main] coming...
+hahaha
+Thread[Thread-0,5,main] unlock...
+hehehe
+Thread[Thread-1,5,main] unlock...
+

独占锁(写锁)

+

指该锁一次只能被一个线程独占,所持有.对于synchronizedReentrantLock而言都是独占锁

+

共享锁(读锁)

+

该锁可以被多个线程持有.对于 ReentrantLocksynchronized都是独占锁;对与 ReentrantReadWriteLock 其读锁是共享锁而写锁是独占锁。 +读锁的共享可保证并发读是非常高效的,读写、写读和写写的过程是互斥的.

+

读写锁案例

+

`ReentrantReadWriteLock 能保证读写、写读和写写的过程是互斥的时候是独享的,读读的时候是共享的

+
class MyCache {
+
+    private volatile Map<String, Object> map = new HashMap<>();
+
+    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
+
+    public void put(String key, Object value) {
+        rwLock.writeLock().lock();
+        try {
+            System.out.println("开始 写入 ..." + key);
+            map.put(key, value);
+            System.out.println("写入完成 ...");
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            rwLock.writeLock().unlock();
+        }
+
+    }
+
+    public Object get(String key) {
+        Object obj = null;
+        rwLock.writeLock().lock();
+        try {
+            System.out.println("开始读取 ..." + key);
+            obj = map.get(key);
+            System.out.println("读取完成 ..." + obj);
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            rwLock.writeLock().unlock();
+        }
+        return obj;
+    }
+
+}
+
+public class Test {
+
+    public static void main(String[] args) {
+        MyCache myCache = new MyCache();
+        for (int i = 0; i < 10; i++) {
+            int finalI = i;
+            new Thread(() -> {
+                myCache.put(finalI + "", finalI + "");
+            }, String.valueOf(i)).start();
+        }
+
+        System.out.println("---------------");
+
+        for (int i = 0; i < 10; i++) {
+            int finalI = i;
+            new Thread(() -> {
+                myCache.get(finalI + "");
+            }, String.valueOf(i)).start();
+        }
+    }
+}
+
开始 写入 ...0
+写入完成 ...
+开始 写入 ...1
+写入完成 ...
+开始 写入 ...2
+写入完成 ...
+开始 写入 ...3
+写入完成 ...
+开始 写入 ...4
+写入完成 ...
+开始 写入 ...6
+写入完成 ...
+开始 写入 ...5
+写入完成 ...
+开始 写入 ...7
+写入完成 ...
+开始读取 ...0
+读取完成 ...0
+开始读取 ...3
+读取完成 ...3
+开始读取 ...4
+读取完成 ...4
+开始读取 ...5
+读取完成 ...5
+开始读取 ...7
+读取完成 ...7
+开始读取 ...9
+读取完成 ...null
+开始 写入 ...8
+写入完成 ...
+开始读取 ...1
+读取完成 ...1
+开始读取 ...2
+读取完成 ...2
+开始读取 ...6
+读取完成 ...6
+开始读取 ...8
+读取完成 ...8
+开始 写入 ...9
+写入完成 ...
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/java-mq/index.html b/blog-site/public/posts/essays/java-mq/index.html new file mode 100644 index 00000000..e9ee84a2 --- /dev/null +++ b/blog-site/public/posts/essays/java-mq/index.html @@ -0,0 +1,1257 @@ + + + + + + + + + + + MQ详解 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

MQ详解

+ 2021.10.19 +
+

概念

+

MQ 即 messagequeue 消息队列,是分布式系统的重要组件,主要解决异步消息,应用解耦,消峰等问题。从而实现高可用,高性能,可伸缩和最终一致性的架构。使用较多的MQ有:activeMQ,rabbitMQ,kafka,metaMQ。

+

MQ优点

+
    +
  • 异步消息处理:可以将一些非核心流程,如日志,短信,邮件等,通过MQ的方式异步去处理。这样做的好处是缩短主流程的响应时间,提升用户体验;
  • +
  • 应用解耦合:商品服务和订单服务之间。用户下单后,订单服务会通知商品服务。不使用MQ的情况是订单服务调用商品服务的接口,这样订单服务和商品服务之间是耦合的;使用MQ,订单服务完成持久化处理,将消息写入MQ消息队列中,返回用户订单下单成功,商品服务来订阅这个下单的消息,采用拉或推的方式获得下单信息,商品服务根据商品下单信息进行商品库存信息修改,这样当下单时商品服务不可用时,也不影响正常下单,这就完成了订单服务和商品服务之间的解耦;
  • +
  • 流量消峰:秒杀活动流量过大,导致流量暴增,最终可能导致应用挂掉。一般会在应用前端加入消息队列来控制活动人数,假如消息队列超过最大数量,应该直接抛弃用户请求或者跳转到错误页面。秒杀业务根据消息队列中的请求信息在做后续的业务处理。比如在抢购时,可能一下子过来了10万个请求,但MQ只接受前100个用户的请求,超过100个不接收了。这样就成功限制了用户请求;
  • +
+

MQ缺点

+
    +
  • 系统可用性降低: 系统引入的外部依赖越多,越容易挂掉;
  • +
  • 系统复杂度提高: 加个 MQ 进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已;
  • +
  • 一致性问题: A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,你这数据就不一致了;
  • +
+

常见MQ对比

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
特性ActiveMQRabbitMQRocketMQKafka
单机吞吐量万级,比 RocketMQ、Kafka 低一个数量级同 ActiveMQ10 万级,支撑高吞吐10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景
topic 数量对吞吐量的影响topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topictopic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源
时效性ms 级微秒级,这是 RabbitMQ 的一大特点,延迟最低ms 级延迟在 ms 级以内
可用性高,基于主从架构实现高可用同 ActiveMQ非常高,分布式架构非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
消息可靠性有较低的概率丢失数据基本不丢经过参数优化配置,可以做到 0 丢失同 RocketMQ
功能支持MQ 领域的功能极其完备基于 erlang 开发,并发能力很强,性能极好,延时很低MQ 功能较为完善,还是分布式的,扩展性好功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用
+

一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了。

+

后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高。

+

不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险,目前 RocketMQ 已捐给 Apache,但 GitHub 上的活跃度其实不算高,对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。

+

所以中小型公司,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;大型公司,基础架构研发实力较强,用 RocketMQ 是很好的选择。

+

如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。

+

JMS消息模型

+

JMS即JavaMessageService,Java消息服务应用程序接口,是一个Java平台中关于面向消息中间件的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。

+

JMS是基于JVM的消息代理规范,ActiveMQ、HornetMQ等是JMS的实现。

+

Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。我们可以简单的理解:两个应用程序之间需要进行通信,我们使用一个JMS服务,进行中间的转发,通过JMS的使用,我们可以解除两个程序之间的耦合。

+

点对点模式

+

消息发送者发送消息,消息代理将其放入消息队列中,消息接受者从队列中获取消息,消息读取后被移除消息队列。每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到它们被消费或超时。

+

MQ详解-001

+

虽然可能有多个客户端在队列中侦听消息,但只有一个可以读取到消息,之后消息将不存在,其他消费者将无法读取。也就是说消息队列只有唯一一个发送者和接受者,但是并不能说只有一个接收者

+

特点:

+
    +
  • 每个消息只有一个消费者,即消息一旦被消费,消息就不在消息队列中;
  • +
  • 发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行,它不会影响到消息被发送到队列;
  • +
  • 接收者在成功接收消息之后需向队列应答成功;
  • +
+

发布订阅模式

+

发布者将消息发送到主题Topic中,多个订阅者订阅这个主题,订阅者不断的去轮询监听消息队列中的消息,那么就会在消息到达的同时接收消息。

+

MQ详解-002

+

特点:

+
    +
  • 每个消息可以有多个消费者,消费完消息之后消息不会清除;
  • +
  • 发布者和订阅者之间有时间上的依赖性:针对某个主题(Topic)的订阅者,它必须创建一个订阅之后,才能消费发布者的消息,而且为了消费消息,订阅者必须保持运行的状态。当然,为了缓和这种严格的时间相关性,JMS允许订阅者创建一个可持久化的订阅。这样即使订阅者没有运行,它也能接收到发布者的消息;
  • +
+

kafka

+

kafka是一个分布式的基于发布/订阅模式的消息队列,主要应用于大数据实时处理领域。

+

基础架构

+

MQ详解-003

+
    +
  • Producer:消息生产者,向kafka推送消息;
  • +
  • Consumer:消息消费者,向kafka中broker获取消息的客户端;
  • +
  • Topic:主题,可以理解为一个队列,生产者和消费者面向的都是一个topic;
  • +
  • Broker:经纪人,一台kafka服务器就是一个broker。一个集群由多个broker组成。一个broker可以容纳多个topic;
  • +
  • Partition:分区,为了实现扩展性,一个非常大的topic可以分布到多个服务器上,一个主题可以分为多个分区,每个分区是一个有序的队列;
  • +
  • ConsumerGroup:消费者组,由多个消费者组成。消费者组内每个消费者负责消费不同分区的数据,一个分区只能由一个消费者组内的一个消费者消费;消费者组之间互不影响。所有的消费者都属于某个消费者组,即消费者组是逻辑上的一个订阅者;
  • +
  • Replica:副本,为保证集群中的某个节点发生故障时,该节点上的分区数据不丢失,且kafka仍然能够继续工作,kafka提供了副本机制,一个topic的每个分区都有若干个副本,一个leader和若干个follower;
  • +
  • Leader:每个分区多个副本的“主”,生产者发送数据的对象,以及消费者消费数据的对象都是leader。leader是针对topic的,而不是broker的,即不同的kafka服务区中会出现同一个leader;
  • +
  • Follower:每个分区多个副本中的“从”,实时从leader中同步数据,保持和leader数据的同步。leader发生故障时,某个Follower会成为新的leader;
  • +
  • Controller:Kafka 集群中有一个broker会被选举为Controller,负责管理集群broker的上下线,所有topic的分区副本分配和leader选举等工作,Controller的管理工作都是依赖于Zookeeper的;
  • +
+

消费者组的作用为了提高消费能力,即提高并发。

+

解耦合是消息队列作用之一,当消费者宕机后,再次启动的时候会继续消费消息,而不是从头消费消息。因为这个特性所以消费者会保存一些消费的进度信息,被称为offset,保存的位置在kafka0.9之前保存在zookpeer当中,在此之后保存在kafka本地。即最终kafka会将消息保存在本地磁盘中,默认保留168个小时,即7天。

+

kafka中消息是以topic进行分类的,producer生产消息,consumer消费消息,都是面向topic的。topic是逻辑上的概念,而partition是物理上的概念,每个partition对应于一个log文件,该log文件中存储的就是producer生产的数据。

+

Producer生产的数据会被不断追加到该log文件末端,且每条数据都有自己的offset。consumer组中的每个consumer,都会实时记录自己消费到了哪个offset,以便出错恢复时,从上次的位置继续消费。

+

发布订阅工作流程

+

MQ详解-004

+
    +
  1. 生产者定期向主题发送消息;
  2. +
  3. kafka代理将所有消息存储在为特定主题配置的分区中。它确保消息在分区之间平均共享。如果生产者发送了两个消息,并且有两个分区,kafka将在第一个分区中存储一个消息,在第二个分区中存储第二个消息;
  4. +
  5. 消费者订阅主题后,kafka将向消费者提供该主题的当前偏移量,并将偏移量保存在Zookeeper集合中;
  6. +
  7. 消费者将定期向kafka请求新消息,一旦kafka从生产者那里收到消息,它将把这些消息转发给消费者,消费者将收到消息并进行处理;
  8. +
  9. 消息处理后,消费者将向kafka代理发送确认;
  10. +
  11. 一旦kafka收到确认,它将偏移量更改为新值并在Zookeeper中对其进行更新。由于在Zookeeper中保留了偏移量,因此即使在服务器出现故障时,使用者也可以正确读取下一条消息;
  12. +
  13. 4、5、6步骤流程将重复进行,直到消费者停止请求为止;
  14. +
+

消费者可以选择随时倒退或跳至所需的主题偏移量并阅读所有后续消息。

+

生产者

+

生产者文件存储

+

参考文章:

+ +

MQ详解-005

+

由于生产者生产的消息会不断追加到log文件末尾,为防止log文件过大导致数据定位效率低下,kafka采取了分片和索引机制,将每个partition分为多个segment。 +每个segment对应两个文件:“.index”文件和“.log”文件。这些文件位于一个文件夹下,该文件夹的命名规则为:topic名称+分区序号。例如,first这个topic有三个分区,则其对应的文件夹为 first-0,first-1,first-2。其中,每个segment中的日志数据文件大小均相等。

+
+

该日志数据文件的大小可以通过在kafka Broker的config/server.properties配置文件的中的“log.segment.bytes”进行设置,默认为1G大小(1073741824字节),在顺序写入消息时如果超出该设定的阈值,将会创建一组新的日志数据和索引文件。

+
+

“.index”文件存储大量的索引信息,“.log”文件存储大量的数据,索引文件中的元数据指向对应数据文件中message的物理偏移地址。其中文件的命名是以第一个数据的偏移量来命名的。

+

kafka如何通过index文件快速找到log文件中的数据?

+

根据指定的偏移量,使用二分法查询定位出该偏移量对应的消息所在的分段索引文件和日志数据文件。然后通过二分查找法,继续查找出小于等于指定偏移量的最大偏移量,同时也得出了对应的position即实际物理位置。

+

根据该物理位置在分段的日志数据文件中顺序扫描查找偏移量与指定偏移量相等的消息。由于index文件中的每条对应log文件中存储内容大小都相同,所以想要找到指定的消息,只需要用index文件中的该条的大小加上该条的偏移量即可得出log文件中指定消息的位置。

+

MQ详解-006

+

生产者分区策略

+

分区的原因:

+
    +
  • 方便在集群中扩展,每个Partition可以通过调整以适应它所在的机器,而一个topic又可以有多个Partition组成,因此整个集群就可以适应适合的数据了;
  • +
  • 可以提高并发,因为可以以Partition为单位读写了,每个partition在不同的服务区上;
  • +
+

MQ详解-007

+
    +
  • 在指明partition的情况下,直接将指明的值直接作为partiton值;
  • +
  • 在没有指明partition值但有key的情况下,将key的hash 值与topic的partition数进行取余得到partition值;
  • +
  • 既没有partition值又没有key值的情况下,第一次调用时随机生成一个整数(后面每次调用在这个整数上自增),将这个值与topic可用的partition总数取余得到partition值,也就是常说的round-robin算法;
  • +
+

生产者数据可靠性保证

+

为保证 producer 发送的数据,能可靠的发送到指定的 topic,topic 的每个 partition 收到 producer 发送的数据后,都需要向 producer 发送 ack(acknowledgement 确认收到),如果 producer 收到 ack,就会进行下一轮的发送,否则重新发送数据。

+

MQ详解-008

+ + + + + + + + + + + + + + + + + + + + +
方案优点缺点
半数以上完成同步,就发 送 ack延迟低选举新的 leader 时,容忍 n 台 节点的故障,需要 2n+1 个副本
全部完成同步,才发送ack选举新的 leader 时,容忍 n 台 节点的故障,需要 n+1 个副 本延迟高
+
+

理解 2n+1: +半数以上完成同步才可以发ACK,如果挂了n台有副本的服务器,那么就需要有另外n台正常发送(这样正常发送的刚好是总数(挂的和没挂的)的一半(n(挂的)+n(正常的)=2n)),因为是半数以上所以2n+1.(所以总数2n+1的时候最多只能容忍n台有故障)

+

即,如果挂了n台有副本的服务器,那么存在副本的服务器的总和为 2n+1

+
+

kafka选择了第二种方案,原因如下: +1.同样为了容忍 n 台节点的故障,第一种方案需要 2n+1 个副本,而第二种方案只需要 n+1 个副本,而 kafka 的每个分区都有大量的数据,第一种方案会造成大量数据的冗余; +2.虽然第二种方案的网络延迟会比较高,但网络延迟对 kafka 的影响较小;

+

采用第二种方案之后,设想以下情景: leader 收到数据,所有 follower 都开始同步数据,但有一个 follower,因为某种故障,迟迟不能与 leader 进行同步,那 leader 就要一直等下去,直到它完成同步,才能发送 ack。这个问题怎么解决呢?

+

Leader 维护了一个动态的 in-sync replica set 即ISR。

+

当和 leader 保持同步的 follower 集合。当 ISR 中的 follower 完成数据的同步之后,就会给 leader 发送 ack。如果 follower长时间未向leader同步数据,则该follower将被踢出ISR,该时间阈值由replica.lag.time.max.ms参数设定。 Leader 发生故障之后,就会从 ISR 中选举新的 leader。

+
+

kafka是通过消息条数差值(replica.lag.time.max.messages) 加 通信时间长短(同步时间replica.lag.time.max.ms) 两个条件来选副本进ISR,在高版本中不再关注副本的消息条数最大条件。

+
+

为何会去掉消息条数差值参数?

+

因为kafka一般是按batch批量发数据到leader, 如果批量条数12条,replica.lag.time.max.messages参数设置是10条,那么当一个批次消息发到kafka leader,此时,ISR中就要踢掉所有的follower,很快follower同步完所有数据后,follower又要被加入到ISR,而且要加入到zookeeper中频繁操作,所以去除掉该条件。

+

生产者数据一致性保证

+

MQ详解-009

+
    +
  • +

    LEO:(Log End offset)每个副本的最后一个offset;

    +
  • +
  • +

    HW:(High Watermark)高水位,指的是消费者能见到的最大的 offset, ISR 队列中最小的 LEO;

    +
  • +
  • +

    follower 故障:follower 发生故障后会被临时踢出 ISR,待该 follower 恢复后, follower 会读取本地磁盘记录的上次的 HW,并将 log 文件高于 HW 的部分截取掉,从 HW 开始向 leader 进行同步。等该 follower 的 LEO 大于等于该 Partition 的 HW,即 follower 追上 leader 之后,就可以重新加入 ISR 了。

    +
  • +
  • +

    leader 故障:leader 发生故障之后,会从 ISR 中选出一个新的 leader,之后,为保证多个副本之间的数据一致性, 其余的 follower 会先将各自的 log 文件高于 HW 的部分截掉,然后从新的 leader同步数据;如果少于 leader 中的数据则会从 leader 中进行同步。

    +
  • +
+

生产者ack机制

+

对于某些不太重要的数据,对数据的可靠性要求不是很高,能够容忍数据的少量丢失,所以没必要等 ISR 中的 follower 全部接收成功。kafka为生产者提供了三种ack可靠性级别配置:

+
    +
  • 0:producer 不等待 broker 的 ack,这一操作提供了一个最低的延迟,broker 一接收到还没有写入磁盘就已经返回,当 broker 故障时有可能丢失数据;
  • +
  • 1: producer 等待 broker 的 ack, partition 的 leader 落盘成功后返回 ack,如果在 follower同步成功之前 leader 故障,那么将会丢失数据;
  • +
  • -1/all:producer 等待 broker 的 ack, ISR队列中 partition 的 leader 和 ISR 的follower 全部落盘成功后才返回 ack。但是如果在 follower 同步完成后,broker 发送 ack 之前, leader 发生故障,那么会造成数据重复;
  • +
+

ExactlyOnce

+

将服务器的 ACK 级别设置为-1,可以保证 Producer 到 Server 之间不会丢失数据,即 At Least Once 语义,至少发送一次;相对的,将服务器 ACK 级别设置为 0,可以保证生产者每条消息只会被发送一次,即 At Most Once 语义,至多发送一次。

+

至少发送一次可以保证数据不丢失,但是不能保证数据不重复;相对的,至多发送一次可以保证数据不重复,但是不能保证数据不丢失。 但是,对于一些非常重要的信息,比如说交易数据,下游数据消费者要求数据既不重复也不丢失,即 Exactly Once 语义,准确发送一次。

+

在0.11 版本的 Kafka,引入了一项重大特性:幂等性。所谓的幂等性就是指生产者不论向 Server 发送多少次重复数据, Server 端都只会持久化一条。幂等性结合 At Least Once 语义,就构成了 Kafka 的 Exactly Once 语义。

+

要启用幂等性,只需要将 Producer 的参数中 enable.idempotence 设置为 true 即可。

+

Kafka的幂等性实现其实就是将原来下游需要做的去重放在了数据上游。开启幂等性的 Producer 在初始化的时候会被分配一个 PID,发往同一 Partition 的消息会附带 Sequence Number。而Broker 端会对<PID, Partition, SeqNumber>做缓存,当具有相同主键的消息提交时, Broker 只会持久化一条。但是 PID 重启就会变化,同时不同的 Partition 也具有不同主键,所以幂等性无法保证跨分区跨会话的 Exactly Once。

+

生产者发送消息流程

+

kafka的生产者发送消息采用的是异步发送的方式。在消息发送的过程中,涉及到了两个线程——main 线程和 Sender 线程,以及一个线程共享变量——RecordAccumulator。

+

MQ详解-010

+

在生产者发送消息时,main线程将消息发送给 RecordAccumulator,当数据积累到 batch.size 之后,sender线程才会不断从 RecordAccumulator 中拉取消息发送到 kafka的broker;如果数据迟迟未达到 batch.size,sender 线程等待 linger.time 之后就会发送数据。

+

消费者

+

消费者分区分配策略

+

一个消费者组中有多个消费者,一个主题有多个分区,所以必然会涉及到分区的分配问题,即确定哪个个分区由哪个消费者来消费。

+

消费分配策略:

+
    +
  • round-robin:会采用轮询的方式将当前所有的分区依次分配给所有的消费者;
  • +
  • range:首先会计算每个消费者可以消费的分区个数,然后按照顺序将指定个数范围的分区分配给各个消费者;
  • +
  • sticky:这种分区策略是最新版本中新增的一种策略,将现有的分区尽可能均衡的分配给各个消费者,存在此目的的原因在于Round-Robin和Range分配策略实际上都会导致某几个消费者承载过多的分区,从而导致消费压力不均衡;
  • +
+

轮询就不必说了,就是把分区按照hash排序,然后分配。range按范围分配,先将所有的分区放到一起然后排序,按照平均分配的方式计算每个消费者会得到多少个分区,如果没有除尽,则会将多出来的分区依次计算到前面几个消费者。 +比如这里是三个分区和两个消费者,那么每个消费者至少会得到1个分区,而3除以2后还余1,那么就会将多余的部分依次算到前面几个消费者,也就是这里的1会分配给第一个消费者,

+

如果按照Range分区方式进行分配,其本质上是依次遍历每个topic,然后将这些topic的分区按照其所订阅的消费者数量进行平均的范围分配。这种方式从计算原理上就会导致排序在前面的消费者分配到更多的分区,从而导致各个消费者的压力不均衡。

+

消费者消费数据问题

+

消费者在消费的时候,需要维护一个offset,用于记录消费的位置,当offset提交时会有两个问题:重复消费和漏消费:

+
    +
  • 当提交的offset小于当前程序处理的最后一条消息的offset,会造成重复消费。情景:先消费,后提交offset,如果消费成功、提交失败,消费者下次获取的offset还是以前的,所以会造成重复消费。
  • +
  • 当提交的offset大于当前程序处理的最后一条消息的offset,会造成漏消费。情景:先提交offset,后消费,如果提交成功、消费失败,消费者下次获取的offset已经是新的,所以会造成漏消费。
  • +
+

这里就需要注意offset的提交方式,offset默认是自动提交,当然这会造成消费的不准确。offset提交方式:

+
    +
  • 异步提交当前offset;
  • +
  • 同步提交当前offset;
  • +
  • 异步提交当前offset;
  • +
  • 同步和异步组合提交;
  • +
  • 提交指定的offset;
  • +
+

建议将offset保存在数据库中,使当前业务与offset提交绑定起来,这样可以一定程度避免重复消费问题,重复消费的问题,一方面需要消息中间件来进行保证。另一方面需要自己的处理逻辑来保证消息的幂等性。

+

kafka事务

+

kafka从0.11版本开始引入了事务支持。事务可以保证kafka在Exactly Once语义的基础上,生产和消费可以跨分区和会话,要么全部成功,要么全部失败。

+
    +
  • 生产者:为了实现跨分区跨会话的事务,需要引入一个全局唯一的事务ID,并将生产者获得的PID和事务ID绑定。这样当生产者重启后就可以通过正在进行的事务ID获得原来的PID,进而保证精准发送一次;
  • +
+
+

为了管理 Transaction, Kafka 引入了一个新的组件 Transaction Coordinator。 Producer 就是通过和 Transaction Coordinator 交互获得 Transaction ID 对应的任务状态。 Transaction Coordinator 还负责将事务所有写入 Kafka 的一个内部 Topic,这样即使整个服务重启,由于事务状态得到保存,进行中的事务状态可以得到恢复,从而继续进行。

+
+
    +
  • 消费者:对于消费者而言,事务的保证就会相对较弱,尤其时无法保证提交的信息被精确消费。这是由于消费者可以通过offset访问任意信息,而且不同的Segment File生命周期不同,同一事务的消息可能会出现重启后被删除的情况;
  • +
+

消息积压

+

如果线上遇到大量消息积压,那就是线上故障了.最可能是消费者出现故障

+

一般这个时候,只能临时紧急扩容了:

+
    +
  • 先修复消费者的问题,确保其恢复消费速度,然后将现有消费者都停掉
  • +
  • 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量;
  • +
  • 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue;
  • +
  • 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据;
  • +
  • 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息;
  • +
+

如果没有消费者没有出现问题,而出现了消费积压的情况可以参考以下思路:

+
    +
  • 提高消费并行度
  • +
  • 批量方式消费
  • +
  • 跳过非重要方式消费
  • +
  • 优化消息消费业务处理过程,简化过程
  • +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/java-project-reconstitution/index.html b/blog-site/public/posts/essays/java-project-reconstitution/index.html new file mode 100644 index 00000000..78fb539f --- /dev/null +++ b/blog-site/public/posts/essays/java-project-reconstitution/index.html @@ -0,0 +1,599 @@ + + + + + + + + + + + 重构一个程序 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

重构一个程序

+ 2023.04.20 +
+

什么是重构

+
+

摘自《重构:改善既有代码的设计》

+
    +
  • 重构(名词形式): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。
  • +
  • 重构(动词形式): 使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构。
  • +
+

重构的目的是使软件更容易被理解和修改。可以在软件内部做很多修改,但必须对软件可观察的外部行为只造成很小的变化,甚至不造成变化。与之形成对比的是性能优化,和重构一样,性能优化通常不会改变组件的行为,只会改变其内部结构。但是两者的出发点不同:性能优化往往使代码较难理解,但为了得到所需的性能你不得不这么做。

+

重构不会改变软件可观察的行为,重构之后的软件功能一往如此。

+
+

重构代码更像是整理代码,重构可以重构但是不要改变代码原本的功能,需要修改其内部结构,在不改变软件可观测行为的前提下,调整代码结构,当然这种修改肯定是有利的一面。提高软件的可理解性,降低变更成本。

+

为什么重构

+

首先需要明确的一点是,重构是一种经济适用性行为,而非道德使然,重构只有一个目的,就是让我们更快更好的开发代码,为什么需要重构,是因为现有的代码不能够提高开发效率,有时不但不能提高,还会降低很多很多。

+

其次重构是保证代码质量的一个极其有效的手段,不至于让代码腐化到无可救药的地步。项目在演进,代码不停地在堆砌。如果没有人为代码的质量负责任,代码总是会往越来越混乱的方向演进。当混乱到一定程度之后,量变引起质变,项目的维护成本已经高过重新开发一套新代码的成本,想要再去重构,已经没有人能做到了。

+

当我们遇到这段代码实在是太恶心了,花了很长时间才看懂,并且代码非常僵硬,而正好这个需求需要改动到这里,代码真的就像一坨屎。而最后是怎么处理的,我们通常是又给它加了一坨。

+

我们为什么会这么去做?因为重构会减慢当前任务速度,所以保持最快速度。

+

为什么这么做能干得又多又快?因为他将成本放到了未来。软件工程最大的成本在于维护,我们每一次代码的改动,都应该是对历史代码的一次整理,而非单一的功能堆积。 +这样虽然能赢得现在,但终将失去未来,而这个失败的未来或许需要全团队与他一起买单。 这或许就是我们重构的最根本原因。

+

优秀的代码或架构不是一开始就能完全设计好的,就像优秀的公司和产品也都是迭代出来的。所以当我们做一个需求一定要回过头整理代码,保证代码的质量。傻瓜都能写出计算机可以理解的代码。唯有能写出人类容易理解的代码的,才是优秀的程序员。

+

除此之外重构对程序员本身的意义:

+
    +
  1. 更快速的定位问题,节省调试时间。
  2. +
  3. 最小化变更风险,提高代码质量,减少修复事故的时间,提高ROI。
  4. +
  5. 得到程序员同行的认可,更好的发展机会。
  6. +
+

重构场景

+

因为重构是一种经济实用性行为,所以重构的时候需要考虑成本,仅对必要的代码进行重构,某个工作行为如果重复三次就可以认为未来也会存在重复,因此通过重构使得下次工作更加高效,这是一种务实的作法。

+

举例,在添加一个新功能时,看之前的旧代码,如果有一些公共的功能可以提取出来,而这个功能,你新开发的功能又恰好用的上,那么重构这个功能可以帮助你梳理、理解代码,程序如果如此迭代下去,长久下来会提高团队的开发效率。

+

在《重构:改善既有代码的设计》一书中提到持续重构的理念。重构本来就不是一件应该特别拨出时间来做的事,重构应该随时随地的进行。重构不一定是需要大规模的展开的任务,重构应该是不断持续进行的,将任务拆解为多个具有完备性的任务,每周完成一个,每个任务的上线都不会引起问题,并使项目变得更好,这是一种持续重构的精神态度,是高效能程序员最应该具有的工作习惯。

+

不要代码烂到一定程度之后才去重构。当代码真的烂到出现开发效率低,招了很多人,天天加班,出活却不多,线上 bug 频发,领导发飙,中层束手无策,工程师抱怨不断,查找 bug 困难的时候,基本上重构也无法解决问题了。

+

我们要正确地看待代码质量和重构这件事情。技术在更新、需求在变化、人员在流动,代码质量总会在下降,代码总会存在不完美,重构就会持续在进行。应当时刻具有持续重构意识,才能避免开发初期就过度设计,避免代码维护的过程中质量的下降。

+

怎么重构

+

通读代码,分析现状,找到可重构代码。列出重构计划,确定重构目标,不要盲目的重构,明确的描述出重构后能达到的预期是什么。重构计划中必须给出测试验证方案,保证代码的可测试性,保证重构前与重构后软件的行为一致。 +将重构任务当作项目来管理,对指定任务的人明确的排期和进度同步。这是重构大体上基本的步骤。

+

在重构项目时,我们可以借鉴分治思想,将重构任务拆分成每周都能见到效果的小任务,形成正反馈,定期开会同步进度,不断加强团队的重构意识。

+

下面是重构代码的一些常见问题和重构的一些建议

+

代码命名

+
    +
  • 见名知意;避免增加认知负荷;
  • +
  • 考虑命名对整体架构的影响,要与整体的风格保持一致;
  • +
+

注释命名

+

复杂度与依赖传递

+
+

所谓复杂性,就是任何使得软件难于理解和修改的因素。

+
+

模糊性与依赖性是引起复杂性的2个主要因素,模糊性产生了最直接的复杂度,让我们很难读懂代码真正想表达的含义,无法读懂这些代码,也就意味着我们更难去改变它。而依赖性又导致了复杂性不断传递,不断外溢的复杂性最终导致系统的无限腐化,一旦代码变成意大利面条,几乎不可能修复,成本将成指数倍增长。

+

我们可以找到很多因素导致系统腐化的原因:

+
    +
  • 想简单图省事,没有及时治理不合理的内容
  • +
  • 缺少匠心追求,对肮脏代码视而不见
  • +
  • 技术能力不够,无法应对复杂系统
  • +
+

除了上述内容外,还可以想到很多理由。但我们发现他们好像有一个共同的指向点 - 软件工程师,似乎所有复杂的源头就是软件工程师的不合格导致,所以其实一些罪恶的根因是我们自己?

+
+

软件复杂才是常态,不复杂才不正常。软件的复杂性是固有的,包括问题域的复杂性、管理开发过程的困难性、通过软件可能实现的灵活性与刻画离散系统行为的问题,这4个方面来分析了软件的发展一定伴随着复杂,这是软件工程这本科学所必然伴随的一个特性。 +所以所有的软件架构万变不离其宗,都在致力解决软件的复杂性。

+
+
+

世间万物都需要额外的能量和秩序来维持自身,无一例外。没有外部力量的注入事物就会逐渐崩溃,这是世间万物的规律,而非我们哪里做得不对。所以我们才需要对系统进行维护甚至重构,降低系统的复杂度来保障系统的正常运行及降低维护的成本。

+
+

过度设计

+

过度设计就是增加复杂度的一种。

+

在初学编程的时候,只管埋头写程序,浑浑噩噩的进行开发。然而很快便发现事先做好设计可以节省返工的成本。许多人把设计看作软件开发的关键环节,而把编程看成是机械式的劳动。他们认为设计就像画工程图纸而编码就像施工。 +因此需要把更更多得精力放在预先设计上,以免日后修改。

+

但是当过分的考虑程序未来所要面对的需求时,将陷入过度设计的陷阱,为了当下用不上的能力,而使程序变得复杂,这些设计很可能是现在未来都不需要的。

+

过度设计是一种负面的设计,所以要合理的进行设计,避免过度设计,把握好设计的尺度,适度设计。下面是避免过度设计的几点建议:

+
    +
  • 将注意力集中在应用程序的核心功能上,以满足用户的基本需求。避免在边缘功能上过度设计和开发,这些功能可能会增加程序的复杂性和难度。尽量采用简单的解决方案,而不是过度复杂的设计。
  • +
  • 设计的目的之一是为产品和用户带来更多的价值和服务,因此必须保证所有的设计都有其存在的“价值”,这个“价值”可以是为用户带来更好的体验,也可以是是为产品带来更多的收益。任何没有“价值”的设计,就是“过度设计”,可以直接删除。
  • +
+

要明确一点,设计是产品实现用户需求之间的一种手段,不是最终的目的,不能为了设计而设计,添加一些乱七八糟的功能,那样产品的用户体验也是很差的。在设计中应当确保应用程序的核心功能得到优先关注,避免过度边缘化。

+

没有时间重构

+

这是重构所面临最多的借口,是自己也是团队的借口。

+

为此必须要明确重构是经济行为而不是一种道德行为,重构使得开发效率变得更高,因此仅对必要的代码进行重构,某个工作行为如果重复三次就可以认为未来也会存在重复,因此通过重构使得下次工作更加高效,这是一种务实的作法,而重构不一定是需要大规模的展开的任务,重构应该是不断持续进行的,将任务拆解为多个具有完备性的任务,每周完成一个,每个任务的上线都不会引起问题,并使项目变得更好,这是一种持续重构的精神态度,是高效能程序员最应该具有的工作习惯。

+

如果你在给项目添加新的特性,发现当前的代码不能高效的完成这个任务,并且同样的任务出现三次以上,那么这时你应该先重构,再开发新特性。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/java-redis/index.html b/blog-site/public/posts/essays/java-redis/index.html new file mode 100644 index 00000000..64380644 --- /dev/null +++ b/blog-site/public/posts/essays/java-redis/index.html @@ -0,0 +1,3776 @@ + + + + + + + + + + + Redis详解 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Redis详解

+ 2021.06.17 +
+

Redis概述

+

参考文章:

+ +

什么是Redis

+

Redis(Remote Dictionary Server) Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API 的非关系型数据库

+

简而言之,Redis是一个可基于内存亦可持久化的日志型、Key-Value非关系型数据库。

+

非关系型数据库

+

非关系型数据库,简称NoSql,是Not Only SQL 的缩写,是对不同于传统的关系型数据库的数据库管理系统的统称,泛指非关系型的数据库。 +NoSql 不依赖业务逻辑方式存储,而以简单的key-value模式存储。因此大大的增加了数据库的扩展能力。

+

NoSql特点:

+
    +
  • 不遵循SQL标准;
  • +
  • 不支持ACID;
  • +
  • 远超于SQL的性能;
  • +
+

NoSql适用场景:

+
    +
  • 对数据高并发的读写;
  • +
  • 海量数据的读写;
  • +
  • 对数据高可扩展性的;
  • +
+

NoSql不适用场景:

+
    +
  • 需要事务支持;
  • +
  • 基于sql的结构化查询存储,处理复杂的关系,需要及时查询;
  • +
  • 用不着sql的和用了sql也不行的情况,请考虑用NoSql;
  • +
+

传统数据库遵循 ACID 规则。而 Nosql 一般为分布式,而分布式一般遵循 CAP 定理。

+

Redis相关知识

+

Redis 默认16个数据库,类似数组下标从0开始,初始默认使用0号库。可使用命令 select <dbid>来切换数据库。如: select 8

+

Redis是单线程+多路IO复用技术

+

多路复用: +指使用一个线程来检查多个文件描述符(Socket)的就绪状态,比如调用select和poll函数,传入多个文件描述符,如果有一个文件描述符就绪,则返回,否则阻塞直到超时。得到就绪状态后进行真正的操作可以在同一个线程里执行,也可以启动线程执行,比如使用线程池。

+

Redis与原子性: +所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。 +在单线程中,能够在单条指令中完成的操作都可以认为是"原子操作",因为中断只能发生于指令之间;在多线程中,不能被其它进程或线程打断的操作就叫原子操作; +Redis命令的原子性主要得益于Redis的单线程机制。

+

Redis使用场景:

+
    +
  • 配合关系型数据库做高速缓存 +
      +
    • 高频次,热门访问的数据,放入Redis中可降低数据库IO
    • +
    • 分布式架构,做session共享
    • +
    +
  • +
  • 多样的数据结构存储持久化数据 +
      +
    • 利用zset排序,做排行榜功能、大数据去重;
    • +
    • 利用Redis key的实效性数据,如将手机验证码放入Redis;
    • +
    • 发布订阅消息系统,构建队列;
    • +
    • 利用Redis的原子性来做计数器、秒杀等;
    • +
    +
  • +
+

Redis特点:

+
    +
  • Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用;
  • +
  • Redis 支持数据的备份,即master-slave模式的数据备份;
  • +
  • Redis 可以存储键与不同数据结构类型之间的映射:
  • +
+

Redis优点:

+
    +
  • 性能极高 – 因为是纯内存操作,Redis能读的速度是110000次/s,写的速度是81000次/s;
  • +
  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性;
  • +
  • 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作;
  • +
  • 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来;
  • +
+

Redis缺点:

+
    +
  • Redis 的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
  • +
+

Redis基础命令

+
+

如果你使用docker可以先启动Redis容器再用docker exec -it <容器ID> redis-cli命令进入redis客户端。

+
+

基础命令:

+
    +
  • 查看当前库所有key:keys *
  • +
  • 判断某个key是否存在:exists <key>
  • +
  • 查看key是什么类型:type <key>
  • +
  • 删除指定的key数据:del <key>
  • +
  • 根据value选择非阻塞删除,异步删除:unlink <key>
  • +
  • 为给定的key设置过期时间:expire key <time>
  • +
  • 查看还有多少秒过期,-1表示永不过期,-2表示已过期:ttl <key>
  • +
  • 切换数据库:select <dbid>
  • +
  • 查看当前数据库的key的数量:dbsize
  • +
  • 清空当前库(慎用):flushdb
  • +
  • 清空全部库(慎用):flushall
  • +
+

更多命令详见:

+ +

Redis数据类型

+

Redis 可以存储键和不同类型的值之间的映射。键的类型只能为字符串,值常见有五种数据类型:字符串、列表、集合、散列表、有序集合,Redis后续的更新中又新添加了位图、地理位置等数据类型。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
名称使用场景
string-字符串作为常规的key-value缓存应用
hash-哈希主要用来存储对象信息
list-列表缓存一些列表数据:关注列表、粉丝列表等
set-集合去重;提供了求交集、并集、差集等操作,可以用来做共同关注、共同好友
sorted set-有序集合用来做排行榜
bitmaps-位图可以用来统计状态,如日活是否浏览过某个东西
hyperloglog-基数统计用来做统计独立IP数、搜索记录数
geospatial-地理位置可以用来做附近的人、地图的一些推送接口
+

String

+

String是Redis最基本的类型,一个key对应一个value。String类型是二进制安全的。意味着Redis的string可以包含任何数据。比如jpg图片或者序列化的对象。String类型是Redis最基本的数据类型,一个Redis中字符串value最多可以是512M。

+

Redis详解-001

+

String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配.

+

如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。

+

常用命令:

+
    +
  • 添加键值对:set <key> <value>
  • +
  • 查询对应键值:get <key>
  • +
  • 将给定的追加到原值的末尾:append <key> <value>
  • +
  • 获得值的长度:strlen <key>
  • +
  • 只有在不存在时设置的值:setnx <key> <value>
  • +
  • 中储存的数字值增1,只能对数字值操作,如果为空,新增值为1:incr <key>
  • +
  • 中储存的数字值减1,只能对数字值操作,如果为空,新增值为-1:decr <key>
  • +
  • 中储存的数字值增减,自定义步长:incrby/decrby <key><步长>
  • +
+

List

+

Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边),单键多值。它的底层实际是个双向链表,对两端的操作性能很高,通过索引下标的操作中间的节点性能会较差。

+

Redis详解-002

+

List的数据结构为快速链表quickList。首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist。

+

常用命令:

+
    +
  • 从左边/右边插入一个或多个值:lpush/rpush <key><value1><value2><value3>
  • +
  • 从左边/右边吐出一个值:lpop/rpop <key>
  • +
  • 列表右边吐出一个值,插到列表左边:rpoplpush <key1><key2>
  • +
  • 按照索引下标获得元素,从左到右,0 -1表示获取所有:lrange <key><start><stop>
  • +
  • 获得列表长度:llen <key>
  • +
  • 的后面插入插入值:linsert <key> before <value><newvalue>
  • +
  • 从左边删除n个lrem <key><n><value>
  • +
  • 将列表key下标为的值替换成value:lset <key><index><value>
  • +
+

Set

+

Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动去重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。

+

Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。

+
+

复杂度O(1):数据增加,查找数据的时间不变。

+
+

Set数据结构是dict字典,字典是用哈希表实现的。在Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。

+

常用命令:

+
    +
  • 将一个或多个元素加入到集合key中,已经存在的元素将被忽略:sadd <key><value1><value2>
  • +
  • 取出该集合的所有值:smembers <key>
  • +
  • 判断集合是否为含有该值,有1,没有0:sismember <key><value>
  • +
  • 返回该集合的元素个数:scard<key>
  • +
  • 删除集合中的某个元素:srem <key><value1><value2>
  • +
  • 随机从该集合中吐出一个值,可指定key,会从集合中删除:spop <key>
  • +
  • 随机从该集合中取出n个值,不会从集合中删除:srandmember <key><n>
  • +
  • 把集合中一个值从一个集合移动到另一个集合:smove <source Key><destination Key><value>
  • +
  • 返回两个集合的交集元素:sinter <key1><key2>
  • +
  • 返回两个集合的并集元素:sunion <key1><key2>
  • +
  • 返回两个集合的差集元素(key1中的,不包含key2中的):sdiff <key1><key2>
  • +
+

Hash

+

Redis hash 是一个键值对集合。Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象。类似Java里面的Map<String,Object>。

+

Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

+

常用命令:

+
    +
  • 集合中的键赋值hset <key><field><value>
  • +
  • 集合取出 value:hget <key1><field>
  • +
  • 批量设置hash的值:hmset <key1><field1><value1><field2><value2>...
  • +
  • 查看哈希表中,给定域是否存在:hexists<key1><field>
  • +
  • 列出该hash集合的所有hkeys <key>
  • +
  • 列出该hash集合的所有hvals <key>
  • +
  • 为哈希表中的域的值加上增量1:hincrby <key><field><increment>
  • +
  • 将哈希表中的域 的值设置为,当且仅当域不存在:hsetnx <key><field><value>
  • +
+

ZSet

+

sorted set 有序集合也称为 zset,Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。不同之处是有序集合的每个成员都关联了一个评分(score),这个评分被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。

+

因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。 +访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。

+

SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

+

zset底层使用了两个数据结构:

+
    +
  • hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值;
  • +
  • 跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表;
  • +
+
+

跳跃表:
+有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便元素的插入、删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。Redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。 +
+举例:对比有序链表和跳跃表,从链表中查询出51

+

有序链表: 查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到;共需要6次比较。 +Redis详解-003

+

跳跃表:从第2层开始,1节点比51节点小,向后比较;21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层;在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下;在第0层,51节点为要查找的节点,节点被找到,共查找4次。 +Redis详解-004

+

可以看出跳跃表比有序链表效率要高。

+
+

常用命令:

+
    +
  • 将一个或多个元素及其score值加入到有序集key当中:zadd <key><score1><value1><score2><value2>...
  • +
  • 返回有序集 key 中,下标在之间的元素,带withscores,可以让分数一起和值返回到结果集:zrange <key><start><stop> [WITHSCORES]
  • +
  • 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员;有序集成员按 score 值递增(从小到大)次序排列:zrangebyscore key minmax [withscores] [limit offset count]
  • +
  • 为元素的 score 加上增量:zincrby <key><increment><value>
  • +
  • 删除该集合下,指定值的元素:zrem <key><value>
  • +
  • 统计该集合,分数区间内的元素个数:zcount <key><min><max>
  • +
  • 返回该值在集合中的排名,从0开始:zrank <key><value>
  • +
+

Bitmaps

+

Bitmaps 并不是一种数据结构,实际上它就是字符串,但是可以对字符串的位进行操作。

+
+

bit(位)简介:
+现代计算机用二进制(位) 作为信息的基础单位, 1个字节等于8位, 例如“abc”字符串是由3个字节组成, 但实际在计算机存储时将其用二进制表示, “abc”分别对应的ASCII码分别是97、 98、 99, 对应的二进制分别是01100001、 01100010和01100011。

+
+

Bitmaps单独提供了一套命令, 所以在Redis中使用Bitmaps和使用字符串的方法不太相同。 可以把Bitmaps想象成一个以位为单位的数组, 数组的每个单元只能存储0和1, 数组的下标在Bitmaps中叫做偏移量。

+

Redis详解-009

+

常用命令:

+
    +
  • 设置Bitmaps中某个偏移量的值(0或1),offset偏移量从0开始:setbit <key><offset><value>
  • +
  • 获取Bitmaps中某个偏移量的值,获取键的第offset位的值,offset偏移量从0开始,不存在则返回0:getbit <key><offset>
  • +
  • 统计字符串从start字节到end字节bit值为1的数量:bitcount <key>[start end]
  • +
+

虽然使用位操作能够极大提高内存使用效率,但也并非总是如此,合理地使用操作位能才能够有效地提高内存使用率和开发效率。

+

假设网站有1亿用户, 每天独立访问的用户有5千万, 如果每天用集合类型和Bitmaps分别存储活跃用户可以得到表:

+

Set和Bitmaps存储一天活跃用户对比:

+
+
    +
  • Set每个用户id占用空间:假设用户id用的是long存储占8字节,所以Set用户id占用空间是8*8bit=64bit。
  • +
  • Bitmaps需要存储的用户量:因为Bitmaps 并不是一种数据结构,实际上是字符串,所以在存储的时候需要存储全部的用户,此处为1亿。
  • +
+
+ + + + + + + + + + + + + + + + + + + + + + + +
数据类型每个用户id占用空间需要存储的用户量全部内存量
Set64bit5000000064位*50000000 = 400MB
Bitmaps1bit1000000001位*100000000 = 12.5MB
+

但Bitmaps并不是万金油,假如该网站每天的独立访问用户很少,例如只有10万,这时候使用Bitmaps就不太合适了,两者的对比如下表所示:

+ + + + + + + + + + + + + + + + + + + + + + + +
数据类型每个用户id占用空间需要存储的用户量全部内存量
Set64位10000064位*100000 = 800KB
Bitmaps1位1000000001位*100000000 = 12.5MB
+

HyperLogLog

+

Redis 在 2.8.9 版本添加了 HyperLogLog 结构。Redis HyperLogLog 是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。

+
+

HyperLogLog中的基数:比如数据集 {1, 3, 5, 7, 5, 7, 8}, 那么这个数据集的基数集为 {1, 3, 5 ,7, 8}, 不重复元素为5个,5就是基数。 基数估计就是在误差可接受的范围内,快速计算基数。

+
+

在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。

+

但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素。

+

常用命令:

+
    +
  • 添加指定元素到 HyperLogLog 中,估计的近似基数发生变化,则返回1,否则返回0:pfadd <key>< element> [element ...]
  • +
  • 计算的近似基数:pfcount<key> [key ...]
  • +
  • 将一个或多个合并后的结果存储在另一个中:pfmerge <destkey><sourcekey> [sourcekey ...]
  • +
+

Geospatial

+

GEO,Geographic,地理信息的缩写。该类型,就是元素的2维坐标,在地图上就是经纬度。redis基于该类型,提供了经纬度设置、查询、范围查询、距离查询、经纬度Hash等常见操作。

+

常用命令:

+
    +
  • 添加地理位置,key名称、经度、纬度、名称:geoadd <key>< longitude><latitude><member> [longitude latitude member...]
  • +
+
+

两极无法直接添加,一般会下载城市数据,直接通过 Java 程序一次性导入。有效的经度从 -180 度到 180 度。有效的纬度从 -85.05112878 度到 85.05112878 度。当坐标位置超出指定范围时,该命令将会返回一个错误。已经添加的数据,是无法再次往里面添加的。

+
+
    +
  • 获得指定地区的坐标值:geopos <key><member> [member...]
  • +
  • 获取两个位置之间的直线距离,默认单位,米、km表示单位为千米、mi表示单位为英里、ft表示单位为英尺。:geodist <key><member1><member2> [m|km|ft|mi ]
  • +
  • 以给定的经纬度为中心,找出某一半径内的元素:georadius <key><longitude><latitude><radius><m|km|ft|mi> [withcoord]
  • +
+

Redis发布与订阅

+

Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。Redis 客户端可以订阅任意数量的频道。

+

客户端可以订阅频道:

+

Redis详解-005

+

给这个频道发布消息后,消息就会发送给订阅的客户端:

+

Redis详解-006

+

实现Redis发布订阅模式:

+
    +
  1. +

    打开两个Redis客户端;

    +
  2. +
  3. +

    在其中一个客户端输入:subscribe <channel>,channel为订阅的频道名称: +Redis详解-008

    +
  4. +
  5. +

    在另外一个客户端输入:publish <channel> <message>,channel为订阅的频道名称,message为推送的消息,返回的1是订阅者数量: +Redis详解-007

    +
  6. +
+

Redis事务

+

Redis事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

+

Redis事务的主要作用就是串联多个命令防止别的命令插队

+

Redis事务操作相关命令:Multi、Exec、discard

+

Redis详解-014

+

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。组队的过程中可以通过discard来放弃组队。 +可以将exec命令理解为提交操作,discard理解为回滚操作。

+

multi,exec组队成功,提交成功举例:

+
127.0.0.1:6379> multi
+OK
+127.0.0.1:6379(TX)> set k1 v1
+QUEUED
+127.0.0.1:6379(TX)> set k2 v2
+QUEUED
+127.0.0.1:6379(TX)> set k3 v3
+QUEUED
+127.0.0.1:6379(TX)> exec
+1) OK
+2) OK
+3) OK
+

multi,discard组队失败举例:

+
127.0.0.1:6379> multi
+OK
+127.0.0.1:6379(TX)> set k1 v1
+QUEUED
+127.0.0.1:6379(TX)> set k2 v2
+QUEUED
+127.0.0.1:6379(TX)> set k3 v3
+QUEUED
+127.0.0.1:6379(TX)> discard
+OK
+

如果在组队过程中执行某个命令失败了,则认为组队失败整个队列都会被取消:

+

Redis详解-015

+
127.0.0.1:6379> multi
+OK
+127.0.0.1:6379(TX)> set k1 v1
+QUEUED
+127.0.0.1:6379(TX)> set k2
+(error) ERR wrong number of arguments for 'set' command
+127.0.0.1:6379(TX)> exec
+(error) EXECABORT Transaction discarded because of previous errors.
+

如果执行阶段某个命令出了错,则只有报错的命令不会被执行,而其他的命令都会执行且不会回滚:

+

Redis详解-016

+
127.0.0.1:6379> multi 
+OK
+127.0.0.1:6379(TX)> set k1 v1
+QUEUED
+127.0.0.1:6379(TX)> incr k1
+QUEUED
+127.0.0.1:6379(TX)> set k2 v2
+QUEUED
+127.0.0.1:6379(TX)> exec
+1) OK
+2) (error) ERR value is not an integer or out of range
+3) OK
+

Redis事务特性

+
    +
  • 单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。
  • +
  • 没有隔离级别的概念:队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行
  • +
  • 不保证原子性:事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚
  • +
+

事务冲突

+

为什么要使用事务?

+

想想一个场景:有很多人有你的账户,同时去参加抢购。一个请求想给金额减8000、一个请求想给金额减5000、一个请求想给金额减1000。

+

如果不加事务,有可能一个线程在减8000后,还没有写入数据库,此时另一个线程执行减5000操作写入数据库,然后再将减8000的操作写入数据库,就会造成数据异常。针对于高并发这种情况下我们就需要用事务来控制,可以用加锁来处理。

+

在Redis中可以使用悲观锁、乐观锁的思想来处理。

+

悲观锁

+

Redis详解-017

+

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

+

悲观锁实现:在Redis中没有悲观锁,但是可以通过调用lua脚本来实现。

+
+

Lua 是一个小巧的脚本语言,Lua脚本可以很容易的被C/C++ 代码调用,也可以反过来调用C/C++的函数,Lua并没有提供强大的库,一个完整的Lua解释器不过200k,所以Lua不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。 +很多应用程序、游戏使用LUA作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。

+
+

LUA脚本在Redis中的优势:

+
    +
  • 将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数。提升性能。
  • +
  • LUA脚本是类似redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务性的操作。
  • +
+

乐观锁

+

Redis详解-018

+

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

+

乐观锁实现:在Redis中如果涉及到了操作数据需要用事务控制的情况可以用 WATCH key … 命令来监控,可以监控多个key。如果在事务执行之前这个key被其他命令所改动,那么事务将被打断。

+

Redis详解-019

+

UNWATCH 取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或DISCARD命令先被执行了的话,那么就不需要再执行UNWATCH了。

+

Redis与缓存

+

参考文章:

+ +

因为Redis操作的是纯内存所以性能极高,常用来做缓存,来存放一些热点数据。

+

用户第一次访问数据库中的某些数据。整个过程会比较慢,因为是从硬盘上读取的。如果将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快,但随之而来的也会存在一些问题。

+

缓存穿透

+

Redis详解-027

+

Redis中的key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。简单理解为,避开缓存,疯狂请求数据库里没有的数据.从而造成服务器宕机。

+

解决方法

+
    +
  • 对空值缓存:如果一个查询返回的数据为空,不管是数据是否不存在,我们仍然把这个空结果进行缓存,设置空结果的过期时间会很短,最长不超过五分钟;
  • +
+
    @Resource
+    private RedisTemplate<Long, String> redisTemplate;
+    
+    @Resource
+    private TestMapper testMapper;
+
+    public Test findById(Long id) {
+        String testStr = redisTemplate.opsForValue().get(id);
+        //判断缓存是否存在,是否为空对象
+        if (StringUtils.isEmpty(testStr)) {
+            // 这里的锁,使用的时候注意锁的力度,这里建议换成分布式锁,这里做演示
+            synchronized (TestServiceImpl.class){
+                testStr = redisTemplate.opsForValue().get(id);
+                if (StringUtils.isEmpty(testStr)) {
+                    Test test = testMapper.findById(id);
+                    if(test == null){
+                        //构建一个空对象
+                        test= new Test();
+                    }
+                    testStr = JSON.toJSONString(test);
+                    // 存入Redis中,下次再次访问就不访问数据库
+                    redisTemplate.opsForValue().set(id, testStr);
+                }
+            }
+        }
+        Test test = JSON.parseObject(testStr, Test.class);
+        //空对象处理
+        if(test.getId() == null){
+            return null;
+        }
+        return JSON.parseObject(testStr, Test.class);
+    }
+
    +
  • 设置可访问的白名单:使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
  • +
  • 使用布隆过滤器:布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
  • +
+
    @Resource
+    private TestMapper testMapper;
+
+    @Resource
+    private RedisTemplate<Long, String> redisTemplate;
+
+    private static BloomFilter<Long> bloomFilter = BloomFilter.create(Funnels.longFunnel(), 1000000000L);
+
+    @Override
+    public Test findById(Long id) {
+        String testStr = redisTemplate.opsForValue().get(id);
+        if (StringUtils.isEmpty(testStr)) {
+            //校验是否在布隆过滤器中
+            if(bloomFilter.mightContain(id)){
+                return null;
+            }
+            // 这里的锁,使用的时候注意锁的力度,这里建议换成分布式锁,这里做演示
+            synchronized (TestServiceImpl.class){
+                testStr = redisTemplate.opsForValue().get(id);
+                if (StringUtils.isEmpty(testStr) ) {
+                    if(bloomFilter.mightContain(id)){
+                        return null;
+                    }
+                    Test test = testMapper.findById(id);
+                    if(test == null){
+                        //放入布隆过滤器中
+                        bloomFilter.put(id);
+                        return null;
+                    }
+                    testStr = JSON.toJSONString(test);
+                    redisTemplate.opsForValue().set(id, testStr);
+                }
+            }
+        }
+        return JSON.parseObject(testStr, Test.class);
+    }
+

缓存击穿

+

Redis详解-028

+

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端数据库加载数据并回设到缓存,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。这个时候大并发的请求可能会瞬间把后端数据源压垮。

+

解决方法

+
    +
  • 预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长;
  • +
  • 实时调整:现场监控哪些数据热门,实时调整key的过期时长;
  • +
  • 使用锁:在缓存失效的时候判断拿出来的值为空,先使用缓存工具的某些带成功操作返回值的操作,去set一个mutex key,而不是立即去访问数据源;当操作返回成功时,再进行访问数据库的操作,并回设缓存,最后删除mutex key;当操作返回失败,证明有线程在访问数据源,当前线程睡眠一段时间再重试整个get缓存的方法;
  • +
+
 @Resource
+ private RedisTemplate<String,Long> template;
+ 
+ @Resource
+ private TestMapper testMapper;
+ 
+ public Long findById(Long id){
+     Long value = template.opsForValue().get(id);
+     
+     if (StringUtils.isEmpty(value)){
+         String key = id + ":nx";
+         // 使用 redis setnx 命令如果设置为 1 则代表成功
+         if (template.opsForValue().setIfAbsent(key, 1L, 3 * 60, TimeUnit.SECONDS)){
+             value = testMapper.findById(id);
+             template.opsForValue().set(id.toString(),value,30 * 60);
+             template.delete(key);
+         }else {
+             try {
+                 // 睡眠50ms后重试
+                 Thread.sleep(50);
+                 value = template.opsForValue().get(id);
+             } catch (InterruptedException e) {
+                 Thread.interrupted();
+             }
+         }
+         return value;
+     }else {
+         return value;
+     }
+ }
+

缓存雪崩

+

Redis详解-029

+

key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端数据源加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端数据源压垮,简单理解为,在某一个时间段,缓存key大面积失效,集中过期.可能会导致服务器宕机。

+

解决方法

+
    +
  • 构建多级缓存架构:nginx缓存 + redis缓存 + 其他缓存等;
  • +
  • 使用锁或队列:限流降级,在同一时间段,只允许某个线程访问,性能较差;
  • +
  • 设置缓存key永不过期,异步更新;虽然这种方式不会堵塞线程,但是不保证数据一致性,代码复杂度较大,容易堆积垃圾数据;
  • +
  • 将缓存失效时间分散开:比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件;
  • +
+
    @Resource
+    private RedisTemplate<String,Long> template;
+
+    public void setKeys(){
+        template.opsForValue().set("k1",1L,30 * 60 + (new Random().nextInt(9999)));
+        template.opsForValue().set("k2",2L,30 * 60 + (new Random().nextInt(9999)));
+    }
+

Redis部署策略

+

Redis主从复制

+

Redis详解-022

+

主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。

+

主从复制的作用:

+
    +
  • 读写分离,性能扩展
  • +
  • 负载均衡
  • +
  • 容灾快速恢复
  • +
+

搭建主从复制

+
    +
  1. 至少启动三个redis服务器,并分别连接其客户端;
  2. +
  3. 在客户端使用命令info replication 查看主从相关信息;
  4. +
  5. 选定从服务器,并在其客户端执行slaveof <ip> <port>命令将该服务器设置为从服务器;
  6. +
  7. 在客户端使用命令info replication 查看主从相关信息,查看是否生效;
  8. +
  9. 在主从服务器上测试读写;
  10. +
+

主从复制过程

+

Redis详解-023

+
    +
  1. 当从服务器连接到主服务器后,从服务器会向主服务器发送同步数据请求;
  2. +
  3. 主服务器接收到从服务器发送过来的请求,首先会把主服务器数据进行持久化,变为rdb文件,把rdb文件发送到从服务器,从服务器拿到rdb文件进行读取;
  4. +
  5. 每次主服务器进行写操作后,会向从服务器进行数据同步;
  6. +
+

全量复制:用于初次复制或其他无法进行部分复制的情况,将主节点中的所有数据都发送给从节点,是一个非常重型的操作。

+

部分复制:用于网络中断等情况后的复制,只将中断期间主节点执行的写命令发送给从节点,与全量复制相比更加高效。需要注意的是,如果网络中断时间过长,导致主节点没有能够完整地保存中断期间执行的写命令,则无法进行部分复制,仍使用全量复制。

+

一主二仆

+

Redis详解-024

+

特点:

+
    +
  • 当主服务挂掉之后,从服务器不会上位;
  • +
  • 当从服务器挂掉之后,主服务器再次写入数据,此时从服务器再次启动,数据与主服务器保持一致;
  • +
  • 从服务器不能执行写操作;
  • +
+

薪火相传

+

Redis详解-025

+

特点:

+
    +
  • 当主服务挂掉之后,从服务器不会上位;
  • +
  • 上一个slaver可以是下一个slaver的master,可以有效减轻master的写压力,去中心化降低风险;
  • +
  • 一旦某个slave宕机,后面的slave都没法备份,主机挂了,从机还是从机,都无法写数据了;
  • +
+

可以使用命令slaveof <ip> <port>将该服务器设置为某服务器的从机。

+

反客为主

+

当一个master宕机后,后面的slave可以立刻升为master,其后面的slave不用做任何修改,手动执行命令slaveof no one 将从机变为主机。可以使用哨兵模式让"反客为主"模式变为自动。

+

Redis哨兵模式

+

Redis详解-026

+

反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库。当主机挂掉,从机选举中产生新的主机,当之前的主机再次启动,会变为从机。

+

搭建哨兵模式

+
    +
  1. 搭建主从模式
  2. +
  3. 新建哨兵配置sentinel.conf文件:
  4. +
+
+
    +
  • sentinel:哨兵模式
  • +
  • monitor:监控
  • +
  • mymaster:主服务器别名
  • +
  • 127.0.0.1 6379:ip端口
  • +
  • 1:至少有多少个哨兵同意迁移的数量 +
    +sentinel monitor mymaster 127.0.0.1 6379 1
  • +
+
+
    +
  1. 执行redis-sentinel ./sentinel.conf启动哨兵
  2. +
+

复制延时

+

由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。

+

选举机制

+
    +
  1. 优先选择优先级靠前的; +
    +

    优先级在redis.conf中默认:slave-priority 100,值越小优先级越高。

    +
    +
  2. +
  3. 选择偏移量最大的; +
    +

    偏移量是指获得原主机数据最全的。

    +
    +
  4. +
  5. 选择runid最小的服务; +
    +

    每个redis实例启动后都会随机生成一个40位的runid,这里也就是随机选择。

    +
    +
  6. +
+

Redis集群

+

Redis 集群是 Redis 提供的分布式数据库方案,集群通过分片来实现数据共享,并提供复制和故障转移。

+

Redis集群模式是哨兵模式的一种拓展,在没有Redis 集群的时候,人们使用哨兵模式,所有的数据都存在 master 上面,master 的压力越来越大,垂直扩容再多的 salve 已经不能分担 master 的压力的,因为所有的写操作集中都集中在 master 上。所以人们就想到了水平扩容,就是搭建多个 master 节点。客户端进行分片,手动的控制访问其中某个节点。但是这几个节点之间的数据是不共享的。并且如果增加一个节点,需要手动的将数据进行迁移,维护起来很麻烦。 +另外,主从模式,薪火相传模式,主机宕机,导致ip地址发生变化,应用程序中配置需要修改对应的主机地址、端口等信息。所以才产生了 Redis 集群。

+

Redis集群是无中心化集群,即每个结点都是入口。Redis 集群通过分区来提供一定程度的可用性;即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求。

+

Redis集群故障恢复:

+
    +
  • 如果主节点挂掉后,从结点会上位,主节点恢复后,变为从结点;
  • +
  • 如果某一段插槽的主从节点都宕掉,redis服务是否能运行,这个与redis.conf中的参数cluster-require-full-coverage相关;如果为yes则可以继续使用,为no,那么,该插槽数据全都不能使用,也无法存储;
  • +
+

Redis集群优点:

+
    +
  • 实现扩容;
  • +
  • 分摊服务器压力;
  • +
  • 无中心配置相对简单;
  • +
+

Redis集群缺点:

+
    +
  • 多键操作是不被支持、多键的Redis事务是不被支持、lua脚本不被支持;
  • +
  • 由于集群方案出现较晚,很多公司已经采用了其他的集群方案,而代理或者客户端分片的方案想要迁移至redis cluster,需要整体迁移而不是逐步过渡,复杂度较大;
  • +
+

Redis集群数据分片

+

Redis 集群没有使用一致性hash, 而是引入了 哈希槽的概念. Redis cluster 有固定的 16384 个 hash slot,对每个 key 计算 CRC16 值,然后对 16384 取模,可以获取来决定放置哪个槽.

+

集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:

+
    +
  • 节点 A 包含 0 到 5500号哈希槽.
  • +
  • 节点 B 包含5501 到 11000 号哈希槽.
  • +
  • 节点 C 包含11001 到 16384号哈希槽.
  • +
+

这种结构很容易添加或者删除节点. 比如如果我想新添加个节点D, 我需要从节点 A, B, C中得部分槽到D上. 如果我想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可. 由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态.

+

搭建Redis集群

+
    +
  1. 修改redis.conf配置文件,修改前建议备份,修改之后启动redis服务; +
    +

    redis.conf: +// 引入原始配置文件 +include /home/bigdata/redis.conf +// 端口 +port 6379 +// pid文件名称 +pidfile “/var/run/redis_6379.pid” +// rdb文件名称 +dbfilename “dump6379.rdb” +// 日志文件 +logfile “/home/bigdata/redis_cluster/redis_err_6379.log” +// 启用集群模式 +cluster-enabled yes +// 设定节点配置文件名,启动后会自动生成 +cluster-config-file nodes-6379.conf +// 设定节点失联时间,超过该时间(毫秒),集群自动进行主从切换 +cluster-node-timeout 15000

    +
    +
  2. +
  3. 确保所有redis实例启动后,nodes-xxxx.conf文件都正常生成;
  4. +
  5. cd /opt/redis-6.2.1/src进入redis的src目录,执行: +
    +

    此处不要用127.0.0.1,请用真实IP地址 +redis-cli –cluster create –cluster-replicas 1 192.168.11.101:6379 192.168.11.101:6380 192.168.11.101:6381 192.168.11.101:6389 192.168.11.101:6390 192.168.11.101:6391

    +
    +
  6. +
  7. 使用命令redis-cli -c -p<port>登陆redis集群;
  8. +
  9. 在客户端使用命令cluster nodes查看集群信息;
  10. +
+

操作Redis集群

+
    +
  • 执行写入一个键值操作: +
    set k1 v1
    +->Redirected to slot [12706] located at 192.168.137.3:6381
    +OK
    +
    +

    这里的slot是插槽: +一个 Redis 集群包含 16384 个插槽(hash slot),数据库中的每个键都属于这 16384 个插槽的其中一个;集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和。集群中的每个节点负责处理一部分插槽。 +在redis客户端每次录入、查询键值,redis都会计算出该key应该送往的插槽。

    +

    举个例子,如果一个集群可以有主节点,其中:节点 A 负责处理 0 号至 5460 号插槽;节点 B 负责处理 5461 号至 10922 号插槽;节点 C 负责处理 10923 号至 16383 号插槽。

    +
    +
  • +
  • 执行写入多个键值操作: +
    mset k1 v1 k2 v2 k3 v3
    +(error) CORSSSLOT Keys in request don't hash to the same slot 
    +
    不在一个slot下的键值,是不能使用mget,mset等多键操作,但是可以可以通过{}来定义组的概念,从而使key中{}内相同内容的键值对放到一个slot中去: +
    mset k1{cust} v1 k2{cust} v2 k3{cust} v3
    +
  • +
  • 查询集群中的值: +
      +
    • 查询count个slot槽中的键:cluster getkeysinslot <slot> <count>
    • +
    • 计算key的插槽值:cluster keyslot <key>
    • +
    • 计算插槽值有几个key:cluster countkeysinslot <slot>
    • +
    +
  • +
+

Redis分布式锁

+

在代码里面我们常用ReetrantLock、synchronized保证线程安全。通过上面的锁,在某个时刻只能保证一个线程执行锁作用域内的代码。

+

类似这样:

+
public class MainTest {
+
+    private static final  ReentrantLock lock = new ReentrantLock();
+    
+    public static void main(String[] args) {
+        lock.lock();
+        try {
+            System.out.println("hello world");
+        }finally {
+            lock.unlock();
+        }
+    }
+}
+

但是,当项目采用分布式部署方式之后,再使用ReetrantLock、synchronized就不能保证数据的准确性,可能会出现严重bug。 +Redis详解-010

+

举个例子,当很多个请求过来的时候,会先经过Nginx,然后Nginx再根据算法分发请求,到哪些服务器的程序上。 +此时商品的库存为一件,有两个请求,到达不同服务器上的不同程序的相同代码,先后执行了查询SQL,查出来的数据是相同的,然后依次执行库存减一操作,此时库存会变成-1件。这就造成了超卖问题。

+

针对超卖问题,我们可以使用Redis分布式锁来解决。当然也有其他分布式锁,这里不做介绍。

+

Redis分布式锁演进

+

方式一

+

使用Redis,setnx

+
+

SETNX 是SET IF NOT EXISTS的简写.日常命令格式是SETNX key value,如果 key不存在,则SETNX成功返回1,如果这个key已经存在了,则返回0。

+
+
    @Autowired
+    private RedisTemplate redisTemplate;
+
+    // 保证value值唯一,这里是伪代码
+    final String value = "";
+    final String REDIS_LOCK = "redis_lock_demo";
+    public void context(){
+        try {
+            Boolean flag =  redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK,value);
+            if (!flag) {
+                System.out.println("抢锁失败!");
+            }
+            String redisKey =  redisTemplate.opsForvalue().get("redis_key");
+            int num0 = redisKey == null ? 0 : Integer.parseInt(redisKey);
+    
+            if (num0 <= 0){
+                System.out.println("商品已售完!");
+                return;
+            }
+    
+            // 卖出商品,存入Redis中
+            int num1  = num0 - 1;
+            redisTemplate.opsForvalue().set("redis_key",num1);
+        }finally {
+            redisTemplate.delete(REDIS_LOCK);
+        }
+    }
+

需要注意的是redisTemplate.delete()方法要加在finally中是为了程序出现异常不释放锁。

+

但是这种写法会有一个问题,如果Redis服务器宕机了,或Redis服务被其他人kill掉了,此时恰好没有执行finally中的代码,就会造成Redis中永远都会存在这把锁,不会释放。

+

方式二

+

针对上面Redis宕机的问题,我们可以对这个key加一个过期时间,来解决:

+
    @Autowired
+    private RedisTemplate redisTemplate;
+
+    // 保证value值唯一,这里是伪代码
+    final String value = "";
+    final String REDIS_LOCK = "redis_lock_demo";
+    public void context(){
+        try {   
+            // 加锁
+            Boolean flag =  redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK,value);
+            // 设置过期时间,假设为10s
+            redisTemplate.expire(REDIS_LOCK,10, TimeUnit.SECONDS);
+    
+            if (!flag) {
+                System.out.println("抢锁失败!");
+            }
+            String redisKey =  redisTemplate.opsForvalue().get("redis_key");
+            int num0 = redisKey == null ? 0 : Integer.parseInt(redisKey);
+    
+            if (num0 <= 0){
+                System.out.println("商品已售完!");
+                return;
+            }
+    
+            // 卖出商品,存入Redis中
+            int num1  = num0 - 1;
+            redisTemplate.opsForvalue().set("redis_key",num1);
+        }finally {
+            redisTemplate.delete(REDIS_LOCK);
+        }
+    }
+

上面的代码虽然解决了Redis宕机的问题,但是也带来了一个新的问题:设置过期时间和加锁并不再一行,即是非原子操作。

+

举个例子,如果执行完setnx加锁,正要执行expire设置过期时间时,进程要重启维护了,那么这个锁就“长生不老”了,别的线程永远获取不到锁了。

+

方式三

+

针对上面加锁和设置过期时间的问题,我们可以使用Redis提供的一个方法,使其具备原子性:

+
    @Autowired
+    private RedisTemplate redisTemplate;
+
+    // 保证value值唯一,这里是伪代码
+    final String value = "";
+    final String REDIS_LOCK = "redis_lock_demo";
+    public void context(){
+        try {
+            Boolean flag =  redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK,value, 10, TimeUnit.SECONDS);
+
+            if (!flag) {
+                System.out.println("抢锁失败!");
+            }
+            String redisKey =  redisTemplate.opsForvalue().get("redis_key");
+            int num0 = redisKey == null ? 0 : Integer.parseInt(redisKey);
+
+            if (num0 <= 0){
+                System.out.println("商品已售完!");
+                return;
+            }
+
+            // 卖出商品,存入Redis中
+            int num1  = num0 - 1;
+            redisTemplate.opsForvalue().set("redis_key",num1);
+        }finally {
+            redisTemplate.delete(REDIS_LOCK);
+        }
+    }
+

加过期时间释放锁的这种方式会带来另一个问题,某个线程加锁,然后执行业务代码,业务代码执行的时间超过了限定时间,此时Redis会释放锁,然后第二个请求就进来了,此时第一个线程业务代码执行完毕,执行释放锁步骤。这就造成误删除其他线程的锁。

+

简单说就是,张冠李戴,当前线程删除了其他线程的锁。

+

方式四

+

针对方式三带来的问题,需要加一个判断,来避免误删除其他线程的锁:

+
    @Autowired
+    private RedisTemplate redisTemplate;
+
+    final String REDIS_LOCK = "redis_lock_demo";
+    // 保证value值唯一,这里是伪代码
+    final String value = "";
+    public void context(){
+        try {
+            Boolean flag =  redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK,value, 10, TimeUnit.SECONDS);
+
+            if (!flag) {
+                System.out.println("抢锁失败!");
+            }
+            String redisKey =  redisTemplate.opsForvalue().get("redis_key");
+            int num0 = redisKey == null ? 0 : Integer.parseInt(redisKey);
+
+            if (num0 <= 0){
+                System.out.println("商品已售完!");
+                return;
+            }
+
+            // 卖出商品,存入Redis中
+            int num1  = num0 - 1;
+            redisTemplate.opsForvalue().set("redis_key",num1);
+        }finally {
+            // 判断是否是当前线程,如果是当前线程则允许释放锁
+            if (redisTemplate.opsForvalue().get(REDIS_LOCK).equalsIgnoreCase(value)){
+                redisTemplate.delete(REDIS_LOCK);
+            }
+        }
+    }
+

实际上这种方式判断和删除的操作不是原子的,不是原子性的就会出现问题。即该锁没有保存持有者的唯一标识,可能被别的客户端解锁。

+

方式五

+

针对方式四的问题,Redis官网有推荐的解决方法,即,使用Lua脚本

+
if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then
+   redis.call('expire',KEYS[1],ARGV[2])
+else
+   return 0
+end;
+
    @Autowired
+    private RedisTemplate redisTemplate;
+
+    final String REDIS_LOCK = "redis_lock_demo";
+    // 保证value值唯一,这里是伪代码
+    final String value = "";
+    public void context(){
+        try {
+            Boolean flag =  redisTemplate.opsForValue().setIfAbsent(REDIS_LOCK,value, 10, TimeUnit.SECONDS);
+
+            if (!flag) {
+                System.out.println("抢锁失败!");
+            }
+            String redisKey =  redisTemplate.opsForvalue().get("redis_key");
+            int num0 = redisKey == null ? 0 : Integer.parseInt(redisKey);
+
+            if (num0 <= 0){
+                System.out.println("商品已售完!");
+                return;
+            }
+
+            // 卖出商品,存入Redis中
+            int num1  = num0 - 1;
+            redisTemplate.opsForvalue().set("redis_key",num1);
+        }finally {
+            // 伪代码
+            JRedis = jedis = JRedisUtils.getJRedis();
+
+            String lua_scripts =
+                    "if redis.call('setnx',KEYS[1],ARGV[1]) == 1 then\n" +
+                    "   redis.call('expire',KEYS[1],ARGV[2])\n" +
+                    "else\n" +
+                    "   return 0\n" +
+                    "end;";
+            Object result = jedis.eval(lua_scripts, Collections.singletonList(REDIS_LOCK), Collections.singletonList(value))
+            if (result.equals("1")) {
+                System.out.println("删除key成功");
+            }
+                    
+            if (jedis != null) {
+                jedis.close();
+            }
+
+            if (redisTemplate.opsForvalue().get(REDIS_LOCK).equalsIgnoreCase(value)){
+                redisTemplate.delete(REDIS_LOCK);
+            }
+        }
+    }
+

除了用这中方式,也可以用Redis事务来处理方式四带来的问题。

+

对于上面的解决方法,其实并没有真正的解决缓存续期的问题,还是会带来能存在锁过期释放,业务没执行完的问题。

+

方式六

+

参考文章:

+ +

针对缓存续期的问题,我们可以开一个守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。

+

Redisson框架解决了这个问题:

+
    @Autowired
+    private RedisTemplate redisTemplate;
+
+    @Autowired
+    private RedissonClient redisson;
+
+    final String REDIS_LOCK = "redis_lock_demo";
+    // 保证value值唯一,这里是伪代码
+    final String value = "";
+    public void context(){
+        RLock lock = redisson.getLock(REDIS_LOCK);
+        try {
+            lock.lock(REDIS_LOCK);
+
+            String redisKey =  redisTemplate.opsForvalue().get("redis_key");
+            int num0 = redisKey == null ? 0 : Integer.parseInt(redisKey);
+
+            if (num0 <= 0){
+                System.out.println("商品已售完!");
+                return;
+            }
+
+            // 卖出商品,存入Redis中
+            int num1  = num0 - 1;
+            redisTemplate.opsForvalue().set("redis_key",num1);
+        }finally {
+            lock.unlock();
+        }
+    }
+

Redisson大致工作原理:只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程一还持有锁,那么就会不断的延长锁key的生存时间。 +因此,Redisson解决了锁过期释放,业务没执行完问题。

+

Redis详解-011

+

看似完美的解决方案,但是在高并发下可能也会出现下面的异常:

+
Caused by: java.lang.IllegalMonitorStateException: attempt to unlock lock, not locked by current thread by node id: 32caba49-5799-491b-aa7b-47d789dbca93 thread-id: 1
+

异常出现的原因,加锁和解锁的线程不是同一个。

+

方式七

+

针对上面的异常,需要判断当前线程是否持有锁,如果还持有,则释放,如果未持有,则说明已被释放:

+
    @Autowired
+    private RedisTemplate redisTemplate;
+
+    @Autowired
+    private RedissonClient redisson;
+
+    final String REDIS_LOCK = "redis_lock_demo";
+    // 保证value值唯一,这里是伪代码
+    final String value = "";
+    public void context(){
+         RLock lock = redisson.getLock(REDIS_LOCK);
+        try {
+            lock.lock(REDIS_LOCK);
+
+            String redisKey =  redisTemplate.opsForvalue().get("redis_key");
+            int num0 = redisKey == null ? 0 : Integer.parseInt(redisKey);
+
+            if (num0 <= 0){
+                System.out.println("商品已售完!");
+                return;
+            }
+
+            // 卖出商品,存入Redis中
+            int num1  = num0 - 1;
+            redisTemplate.opsForvalue().set("redis_key",num1);
+        }finally {
+            // 查询当前线程是否持有此锁
+            if (lock.isLocked() && lock.isHeldByCurrentThread()) {
+                lock.unlock();
+            }
+        }
+    }
+

这样写,程序的健壮性会更好,代码会更加严谨。

+

Redis内存淘汰策略

+

长期把Redis做缓存用,总有一天Redis内存总会满的。有没有相关这个问题,Redis内存满了会怎么样?

+

redis.conf中把Redis内存设置为1个字节,做一个测试:

+
// 默认单位就是字节
+maxmemory 1 
+

设置完之后重启为了确保测试的准确性,重启一下Redis,之后在用下面的命令,向Redis中存入键值对,模拟Redis打满的情况:

+
set k1 v1
+

执行完后会看到下面的信息:

+
(error) OOM command not allowed when used memory > 'maxmemory'.
+

大意:OOM,当当前内存大于最大内存时,这个命令不允许被执行。

+

是的,Redis也会出现OOM,正因如此,我们才要避免这种情况发生,正常情况下,不考虑极端业务,Redis不是MySql数据库,不能什么都往里边写,一般情况下Redis只存放热点数据。

+

Redis默认最大内存是全部的内存,我们在实际配置的时候,一般配实际服务器内存的3/4也就足够了。

+

删除策略

+

正因为Redis内存打满后报OOM,为了避免出现该情况所以要设置Redis的删除策略。思考一个问题,一个键到了过期时间之后是不是马上就从内存中被删除的?

+

当然不是的,那过期之后到底什么时候被删除?是个什么操作?

+

Redis提供了三种删除策略:

+
    +
  1. 定时删除:创建一个定时器,定时随机的对key执行删除操作;
  2. +
  3. 惰性删除:类似与懒加载,每次只有用到key的时候才会检查,该key是否已经过期,如果过期进行删除操作;
  4. +
  5. 定期删除:每隔一段时间,就会检查删除掉过期的key;
  6. +
+

定时删除,即用时间换空间;它对于内存来说是友好的,定时清理出干净的空间,但是对于CPU来说并不是友好的,程序需要维护一个定时器,这就会占用CPU资源。

+

惰性的删除,即用空间换时间;它对于CPU来说是友好的,CPU不需要维护其它额外的操作,但是对于内存来说是不友好的,因为要是有些key一直没有被访问到,就会一直占用着内存。

+

定期删除,是上面两种方案的折中方案,它每隔一段时间删除过期的key,也就是根据具体的业务,合理的取一个时间定期的删除key。

+

若果在数据量很大的情况下,定时删除时,key从来没有被检查到过;惰性删除时,key从来没有被使用过,这样就会造成内存泄漏,大量的key堆积在内存中,导致Redis内存空间紧张。

+

所以我们必须有一个兜底方案,即Redis的内存淘汰策略。

+

淘汰策略

+

Redis 4.0 版本之前有 6 种策略,4.0 增加了 2种,主要新增了 LFU 算法。下图为 Redis 6.2.0 版本的配置文件:

+

Redis详解-012

+

淘汰策略默认,使用noeviction,意思是不再驱逐的,即等着内存被打满。

+
The default is:
+maxmemory-policy noeviction
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
策略名称描述
noeviction(默认策略)不会驱逐任何key,即内存满了就报错。
allkeys-lru所有key都是使用LRU算法进行淘汰。
volatile-lru所有设置了过期时间的key使用LRU算法进行淘汰。
allkeys-random所有的key使用随机淘汰的方式进行淘汰。
volatile-random所有设置了过期时间的key使用随机淘汰的方式进行淘汰。
volatile-ttl所有设置了过期时间的key根据过期时间进行淘汰,越早过期就越快被淘汰
+

假如在Redis中的数据有一部分是热点数据,而剩下的数据是冷门数据,或者我们不太清楚我们应用的缓存访问分布状况,这时可以使用allkeys-lru

+

可以在redis.conf配置文件中配置:

+
maxmemory-policy allkeys-lru   // 淘汰策略名字
+

当然也可以动态的配置,在Redis运行时修改:

+
// 设置内存淘汰策略
+config set maxmemory-policy allkeys-lru
+
+// 查看内存淘汰策略
+config get maxmemory-policy
+

LRU算法及实现

+

LRU是,Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法。

+
+

页面置换算法: +进程运行时,若其访问的页面不在内存而需将其调入,但内存已无空闲空间时,就需要从内存中调出一页程序或数据,送入磁盘的对换区,其中选择调出页面的算法就称为页面置换算法。

+
+

这个算法的思想就是: 如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。所以,当指定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。

+

明白了思想之后,要实现LRU算法,首先要确定数据结构,再确定实现思路。如果对算法有要求,查询和插入的时间复杂度都是O(1),可以选用链表+哈希的结构来存储:

+

Redis详解-013

+

方式一

+

链表+哈希,我们不难想到JDK中的LinkedHashMap,在LinkedHashMap文档注释中找到关于LRU算法的相关描述:

+
+

A special {@link #LinkedHashMap(int,float,boolean) constructor} is provided to create a linked hash map whose order of iteration is the order in which its entries were last accessed,from least-recently accessed to most-recently (access-order). This kind of map is well-suited to building LRU caches.

+
+

大意:{@link #LinkedHashMap(int,float,boolean)}提供了一个特殊的构造器来创建一个链表散列映射,其迭代顺序为其条目最后访问的顺序,从最近最少访问到最近最近(access-order)。这种映射非常适合构建LRU缓存。

+

参照LinkedHashMap实现LRU算法:

+
public class MainTest {
+
+    public static void main(String[] args) {
+        LRUDemo<Integer,String> list0 = new LRUDemo<>(3,true);
+        System.out.println("-------------accessOrder等于true-------------");
+        context(list0);
+        System.out.println("-------------accessOrder等于false-------------");
+        LRUDemo<Integer,String> list1 = new LRUDemo<>(3,false);
+        context(list1);
+    }
+    
+    public static void context(LRUDemo<Integer,String> list){
+        list.put(1,"a");
+        list.put(2,"b");
+        list.put(3,"c");
+        System.out.println(list.keySet());
+
+        list.put(4,"d");
+        System.out.println(list.keySet());
+        System.out.println();
+
+        list.put(3,"123");
+        System.out.println(list.keySet());
+
+        list.put(3,"1234");
+        System.out.println(list.keySet());
+
+        list.put(3,"12345");
+        System.out.println(list.keySet());
+        System.out.println();
+
+        list.put(5,"123456");
+        System.out.println(list.keySet());
+    }
+}
+
+class LRUDemo<K,V> extends LinkedHashMap<K,V>{
+
+    private int capacity;
+
+    public LRUDemo(int capacity, boolean accessOrder) {
+        /**
+         * accessOrder the ordering mode -
+         * <tt>true</tt> for access-order 存取顺序:如果存贮集合中有相同的元素,再次插入时先删除在插入
+         * <tt>false</tt> for insertion-order 插入顺序:不会因为集合中有相同元素,再次插入该元素就会打乱位置
+         */
+        super(capacity,0.75f,accessOrder);
+        this.capacity = capacity;
+    }
+
+    @Override
+    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
+        return super.size() > capacity;
+    }
+}
+

方式二

+

除了可以参照LinkedHashMap,也可以自己手动实现:

+
public class MainTest {
+
+    public static void main(String[] args) {
+        LRUDemo<Integer,String> list0 = new LRUDemo<>(3);
+        context(list0);
+    }
+
+    public static void context(LRUDemo<Integer,String> list){
+        list.put(1,"a");
+        list.put(2,"b");
+        list.put(3,"c");
+        System.out.println(list.getMap().keySet());
+
+        list.put(4,"d");
+        System.out.println(list.getMap().keySet());
+        System.out.println();
+
+        list.put(3,"123");
+        System.out.println(list.getMap().keySet());
+
+        list.put(3,"1234");
+        System.out.println(list.getMap().keySet());
+
+        list.put(3,"12345");
+        System.out.println(list.getMap().keySet());
+        System.out.println();
+
+        list.put(5,"123456");
+        System.out.println(list.getMap().keySet());
+    }
+}
+
+class LRUDemo<K, V> {
+
+    static class Node<K,V>{
+        K key;
+        V value;
+        Node<K,V> prev;
+        Node<K, V> next;
+
+        public Node(){
+            prev = next = null;
+        }
+
+        public Node(K key,V value){
+            this.key = key;
+            this.value = value;
+        }
+    }
+
+    static class DoubleLinkedList<K,V>{
+        Node<K,V> head;
+        Node<K, V> tail;
+
+        public DoubleLinkedList(){
+            head = new Node<>();
+            tail = new Node<>();
+            
+            // 如果变成尾插法,需要调换头、尾指针的指向
+            head.next = tail;
+            tail.prev = head;
+        }
+
+        public void putHead(Node<K, V> node){
+            node.next = head.next;
+            node.prev = head; // 将新结点插到头部
+            head.next.prev = node;
+            head.next = node;
+        }
+
+        public void remove(Node<K, V> node){
+            node.prev.next = node.next;
+            node.next.prev = node.prev;
+            node.prev = node.next = null;
+        }
+
+        public Node<K,V> getLastNode(){
+            return tail.prev;
+        }
+    }
+
+    private int capacity;
+    private Map<K, Node<K,V>> map;
+    private DoubleLinkedList<K,V> doubleLinkedList;
+
+    public Map<K, Node<K, V>> getMap() {
+        return map;
+    }
+
+    public LRUDemo(int capacity){
+        this.capacity = capacity;
+        this.map = new HashMap<>();
+        doubleLinkedList = new DoubleLinkedList<>();
+    }
+
+    public void put(K key,V val){
+        if (key == null || val == null){
+            return;
+        }
+        // 如果集合中key已经存在,则先删除
+        if (map.containsKey(key)){
+            Node<K, V> node = map.get(key);
+            node.value = val;
+            map.put(key,node);
+
+            // 刷新node
+            doubleLinkedList.remove(node);
+            doubleLinkedList.putHead(node);
+        }else {
+            // 删除最少使用的key
+            if (map.size() == capacity){
+                Node<K, V> lastNode = doubleLinkedList.getLastNode();
+                doubleLinkedList.remove(lastNode);
+                map.remove(lastNode.key);
+            }
+            Node<K, V> newNode = new Node<>(key,val);
+            map.put(key,newNode);
+            doubleLinkedList.putHead(newNode);
+        }
+    }
+
+    public V get(K key){
+        if (!map.containsKey(key)){
+            return null;
+        }
+        Node<K, V> node = map.get(key);
+
+        // 刷新结点位置,将该key移动到队列头部
+        doubleLinkedList.remove(node);
+        doubleLinkedList.putHead(node);
+        return node.value;
+    }
+}
+

Redis持久化

+

什么是Redis持久化? 持久化就是把内存的数据写到磁盘中去,防止服务宕机了内存数据丢失。Redis 提供了两种持久化方式:RDB(默认) 和AOF。

+
    +
  • RDB,简而言之,就是在指定的时间间隔内,将 redis 存储的数据生成快照并存储到磁盘等介质上;
  • +
  • AOF,则是换了一个角度来实现持久化,那就是将 redis 执行过的所有写指令记录下来,在下次 redis 重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了;
  • +
+

其实 RDB 和 AOF 两种方式也可以同时使用,在这种情况下,则会优先采用 AOF 方式来进行数据恢复,这是因为 AOF 方式的数据恢复完整度更高。如果你没有数据持久化的需求,也完全可以关闭 RDB 和 AOF 方式,这样的话,redis 将变成一个纯内存数据库,就像 memcache 一样。

+

RDB持久化

+

RDB (Redis DataBase)方式,是将 redis 某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。

+

相关配置

+
    +
  • 在redis.conf中配置文件名称,默认是dump.rdb: +
    dbfilename dump.rdb
    +
  • +
  • rdb文件的保存路径,也可以修改。默认为Redis启动时命令行所在的目录下: +
    dir ./
    +
  • +
  • 默认的快照配置: +
    save 3600 1
    +save 30 10
    +save 60 10000
    +
  • +
  • 当Redis无法写入磁盘的话,直接关掉Redis的写操作,默认yes: +
    stop-writes-on-bgsave-error yes
    +
  • +
  • 对于存储到磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩: +
    rdbcompression yes
    +
  • +
  • 检查数据完整性,在存储快照后,还可以让redis使用CRC64算法来进行数据校验,但是这样做会增加大约10%的性能消耗,如果希望获取到最大的性能提升,可以关闭此功能,推荐yes: +
    rdbchecksum yes
    +
  • +
+

执行过程

+

redis 在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程都结束了,才会用这个临时文件替换上次持久化好的文件。正是这种特性,让我们可以随时来进行备份,因为快照文件总是完整可用的。对于 RDB 方式,redis 会单独创建(fork)一个子进程来进行持久化,而主进程是不会进行任何 IO 操作的,这样就确保了 redis 极高的性能。

+
+

fork:

+
    +
  • 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux中引入了“写时复制技术”;
  • +
  • 一般情况父进程和子进程会共用同一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程;
  • +
+
+

Redis详解-020

+

如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加的高效。虽然 RDB 有不少优点,但它的缺点也是不容忽视的:丢失数据风险较大,fork进程在保存rdb文件时会先复制旧文件,如果文件较大则耗时较多。如果你对数据的完整性非常敏感,那么 RDB 方式就不太适合你,因为即使你每 5 分钟都持久化一次,当 redis 故障时,仍然会有近 5 分钟的数据丢失。所以,redis 还提供了另一种持久化方式,那就是 AOF。

+

AOF持久化

+

AOF,英文是 Append Only File,即只允许追加不允许改写的文件。如前面介绍的,AOF 方式是将执行过的写指令记录下来,在数据恢复时按照从前到后的顺序再将指令都执行一遍。

+

相关配置

+
    +
  • +

    在redis中AOF默认时不开启的:

    +
    appendonly no
    +
  • +
  • +

    AOF文件名称,文件路径与rdb保持一致:

    +
    appendfilename "appendonly.aof"
    +
  • +
  • +

    AOF同步频率设置:默认的 AOF 持久化策略是每秒钟 fsync 一次(fsync 是指把缓存中的写指令记录到磁盘中),因为在这种情况下,redis 仍然可以保持很好的处理性能,即使 redis 故障,也只会丢失最近 1 秒钟的数据。

    +
      +
    1. 始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好:
    2. +
    +
    appendfsync always
    +
      +
    1. 每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失:
    2. +
    +
    appendfsync everysec
    +
      +
    1. redis不主动进行同步,把同步时机交给操作系统:
    2. +
    +
    appendfsync no
    +
  • +
  • +

    Rewrite压缩:AOF采用文件追加方式,文件会越来越大为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集。举个例子或许更形象,假如我们调用了 100 次 INCR 指令,在 AOF 文件中就要存储 100 条指令,但这明显是很低效的,完全可以把这 100 条指令合并成一条 SET 指令,这就是重写机制的原理。在进行 AOF 重写时,仍然是采用先写临时文件,全部完成后再替换的流程,所以断电、磁盘满等问题都不会影响 AOF 文件的可用性。

    +
      +
    1. 设置重写的基准值,文件达到100%时开始重写:
    2. +
    +
    auto-aof-rewrite-percentage
    +
      +
    1. 设置重写的基准值,最小文件64MB。达到这个值开始重写:
    2. +
    +
    auto-aof-rewrite-min-size
    +
    +

    例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写? +系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,如果Redis的AOF文件当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。

    +

    当前base_size为50MB,根据公式:base_size +base_size*100% = 100MB

    +
    +
  • +
  • +

    如果在追加日志时,恰好遇到磁盘空间满或断电等情况导致日志写入不完整,也没有关系,可以进行恢复:

    +
    redis-check-aof --fix AOP文件名称
    +
  • +
+

执行过程

+

Redis详解-021

+
    +
  • 客户端的请求写命令会被append追加到AOF缓冲区内;
  • +
  • AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;
  • +
  • AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
  • +
  • Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;
  • +
+

AOF 方式的一个好处,我们通过一个“场景再现”来说明。某同学在操作 redis 时,不小心执行了 FLUSHALL,导致 redis 内存中的数据全部被清空了,这是很悲剧的事情。不过这也不是世界末日,只要 redis 配置了 AOF 持久化方式,且 AOF 文件还没有被重写(rewrite),我们就可以用最快的速度暂停 redis 并编辑 AOF 文件,将最后一行的 FLUSHALL 命令删除,然后重启 redis,就可以恢复 redis 的所有数据到 FLUSHALL 之前的状态了。这就是 AOF 持久化方式的好处之一。但是如果 AOF 文件已经被重写了,那就无法通过这种方法来恢复数据了。

+

虽然优点多多,但 AOF 方式也同样存在缺陷,比如在同样数据规模的情况下,AOF 文件要比 RDB 文件的体积大。而且 AOF 方式的恢复速度也要慢于 RDB 方式。

+

对比及使用

+

官方推荐两个都启用,如果对数据不敏感,可以选单独用RDB,如果对数据不敏感,可以选单独用RDB,如果只是做纯内存缓存,可以都不用。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
RDBAOF
定义在指定的时间间隔能对你的数据进行快照存储记录每次对服务器的写操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾
优先级
数据完整性易丢失不易丢失
恢复数据速度较快较慢
数据文件损坏不能修复可使用命令进行修复
数据文件大小较小较大,但是可压缩
+

Redis大key问题

+

所谓的大key问题是某个key的value比较大,所以本质上是大value问题.因为key往往是程序可以自行设置的,value往往不受程序控制,因此可能导致value很大。

+

因为Redis是单线程的,单线程中请求任务的处理是串行的,前面完不成,后面处理不了,同时也导致分布式架构中内存数据和CPU的不平衡.所以大key问题最典型的就是阻塞线程,并发量下降,导致客户端超时,服务端业务成功率下降。

+

大key问题一般是由于业务方案设计不合理,没有预见value的动态增长问题产生的.一直往value塞数据,没有删除机制,迟早要爆炸或数据没有合理做分片,将大key变成小key. 在线上一般通过集成Redis可视化工具,来发现和定位大key问题.

+

解决大key问题,根据大key的实际用途可以分为两种情况:可删除和不可删除。如果发现某些大key并非热key就可以在DB中查询使用,则可以在Redis中删掉.

+
    +
  • +

    删除

    +
      +
    1. 当Redis版本大于4.0时,可使用UNLINK命令安全地删除大Key,该命令能够以非阻塞的方式,逐步地清理传入的Key。
    2. +
    3. 当Redis版本小于4.0时,避免使用阻塞式命令KEYS,而是建议通过SCAN命令执行增量迭代扫描key,然后判断进行删除。
    4. +
    +
  • +
  • +

    不可删除

    +
      +
    1. 当value是string时,比较难拆分,则使用序列化、压缩算法将key的大小控制在合理范围内,但是序列化和反序列化都会带来更多时间上的消耗。
    2. +
    3. 当value是string,压缩之后仍然是大key,则需要进行拆分,一个大key分为不同的部分,记录每个部分的key,使用 multi get 等操作实现事务读取。
    4. +
    +
  • +
+

Redis数据库双写一致性问题

+

你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题.

+

一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统不是严格要求 “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:读请求和写请求串行化,串到一个内存队列里去。

+

串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。

+

最经典的Redis使用方案:

+
    +
  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应;
  • +
  • 更新的时候,先更新数据库,然后再删除缓存;
  • +
+

场景1

+

先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致

+

解决思路:

+
    +
  • 先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。(删除缓存失败会导致程序运行终止的情况下)
  • +
  • 延时双删。依旧是先更新数据库,再删除缓存,唯一不同的是,我们把这个删除的动作,在不久之后再执行一次,比如 5s 之后。 +
    +

    延迟双删的主要思路为异步重试,一般会把重试的步骤放在MQ中进行不断尝试

    +
    +
  • +
  • 订阅数据库变更日志。 我们可以通过工具(canal)订阅这个日志,拿到具体操作的数据,然后再根据这条数据,去删除对应的缓存。
  • +
+

场景2

+

并发,数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改

+
+

这种场景一般只会在数据在并发的进行读写的时候,才可能会出现这种问题,如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景

+
+

解决思路: 当第二个请求进来的时候,第一个请求删除了缓存,正要去修改数据库,此时把第二个请求加入到一个队列中,让其等待第一个请求执行完毕,在从队列中获取第二个请求,让其执行.

+

该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。 如果一个内存队列中可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少.

+

粗略测算,如果一秒有 500 的写操作,如果分成 5 个时间片,每 200ms 就 100 个写操作,放到 20 个内存队列中,每个内存队列,可能就积压 5 个写操作。每个写操作性能测试后,一般是在 20ms 左右就完成,那么针对每个内存队列的数据的读请求,也就最多 hang 一会儿,200ms 以内肯定能返回了.

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/java-small-service/index.html b/blog-site/public/posts/essays/java-small-service/index.html new file mode 100644 index 00000000..ebd2ab8a --- /dev/null +++ b/blog-site/public/posts/essays/java-small-service/index.html @@ -0,0 +1,1224 @@ + + + + + + + + + + + 微服务治理 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

微服务治理

+ 2021.06.21 +
+

什么是微服务架构

+
+

In short, the microservice architectural style is an approach to developing a single application as a suite of small services, +each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API。 +These services are built around business capabilities and independently deployable by fully automated deployment machinery。 +There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies。 ——James Lewis and Martin Fowler (2014)

+
+

大意:简而言之,微服务体系结构风格是一种将单个应用程序开发为一套小型服务的方法,每个服务运行在自己的进程中,并与轻量级机制(通常是HTTP资源API)通信。 +这些服务是围绕业务功能构建的,可以通过全自动部署机制进行独立部署。对这些服务的集中管理是最低限度的,这些服务可能用不同的编程语言编写,并使用不同的数据存储技术。

+

微服务是一种架构风格,将服务围绕业务功能拆分,一个应用拆分为一组小型服务,每个服务运行在自己的进程内,也就是可独立部署和升级,服务之间使用轻量级HTTP交互,可以由全自动部署机制独立部署,去中心化,服务自治。服务可以使用不同的语言、不同的存储技术。

+

为什么要使用微服务

+

说起微服务就不得不说微服务之前的单体应用,有对比才能看的出微服务对比之前的单体应用到底强在哪?为什么我们要抛弃单体应用?

+
    +
  • 单体应用往往所有的功能打包在一个包里,包含了 DO/DAO,Service,UI等所有逻辑,如果要修改一部分代码就需要全部重新部署,所以改动影响大,风险高。微服务模式弥补了这个缺陷,它将应用合理的拆分,实现敏捷开发和快速部署。
  • +
  • 单体应用只能通过在负载均衡器后面放置整个应用程序的多个实例来进行水平扩展,如果想要扩展特定的程序,显然是不行的,则需要使用到微服务模式实现。
  • +
  • 因为单体应用所有的功能全部都达到一个包里面,所以耦合性比较强,对于程序员来说代码维护会收到影响,出现了什么问题也不太方便进行排查。
  • +
  • 如果没有正确的设计,单体应用的一部分失败可能会级联并导致整个系统崩溃。而这种情况可以使用微服务的熔断、降级的思想来解决。
  • +
+

微服务虽然好处多多,但是缺点就是服务众多,不好管理,这也就是为什么要治理微服务,治理成本高,不利于维护系统,项目成本会升高,所以项目架构设计时应综合考量。

+

建议设计的原则是业务驱动、设计保障、演进式迭代、保守治疗的方式。搞不清楚,有争议的地方先尽量不要拆,如果确实要拆,要经过业务分析后慎重设计,把真正相对独立的部分拆分出来,可以借鉴 DDD 的方式。拆了以后要观察微服务的接口是否稳定,针对业务需求的变更微服务的模块是否可以保持相对稳定,是否可以独立演进。

+
+

DDD: Domain-driven design的简称,译为领域驱动设计,是一种通过将实现连接到持续进化的模型来满足复杂需求的软件开发方法。 +
+核心思想:DDD其实是面向对象方法论的一个升华。无外乎是通过划分领域(聚合根、实体、值对象)、领域行为封装到领域对象(充血模式)、内外交互封装到防腐层、职责封装到对应的模块和分层,从而实现了高内聚低耦合。 +链接:https://blog.csdn.net/qq_31960623/article/details/119840131

+
+

从单体架构迁移到微服务架构

+

重写代码,但是不要大规模重写代码,重写代码听起来很好但风险较大,如果大规模重写代码可能会导致程序出现各种各样的bug;(当你承担重建一套全新基于微服务的应用程序不需要使用时候,可以采用重写这种方法)

+

逐步迁移单体应用的功能,独立出来形成新的微服务,同时需要与旧的单体应用集成,这可以保证系统的正常运行,单体式应用在整个架构中比例逐渐下降直到消失或者成为微服务架构一部分;

+

虽然在逐步的迁移功能,但是也会有源源不断的需求需要开发,此时该停止让单体式应用继续变大,也就是说当开发新功能时不应该为旧单体应用添加新代码,应该是将新功能开发成独立微服务;

+

一个巨大的复杂单体应用由成十上百个模块构成,每个都是被抽取对象。决定第一个被抽取模块一般都是挑战,一般最好是从最容易抽取的模块开始,这会让开发者积累足够经验,这些经验可以为后续模块化工作带来巨大好处。

+

转换模块成为微服务一般很耗费时间,一般可以根据获益程度来排序,一般从经常变化模块开始会获益最大。一旦转换一个模块为微服务,就可以将其开发部署成独立模块,从而加速开发进程。比如,抽取一些消耗内存资源较大的代码,将其弄成一个服务,然后可以将其部署在大内存主机上。同样的,将对计算资源很敏感的算法应用抽取出来也是非常有益的,这种服务可以被部署在有很多 CPU 的主机上。 +有三种策略可以考虑:将新功能以微服务方式实现;将表现层与业务数据访问层分离;将现存模块抽取变成微服务。

+

首先要将抽离的相关功能,封装成相关几个方法,几个类,几个包中;定义好模块和单体应用之间的几个大概的接口,从程序内部开始调用; 一旦完成相关的接口,也就将此模块转换成独立微服务。为了实现,必须写代码使得单体应用和微服务之间通过使用进程间通信机制的API来交换信息;服务和单体应用整合的API代码就成为了容灾层;

+

然后,将抽离的功能,转化为独立的服务进行部署,将其整合成一个微服务基础框架; 每抽取一个服务,就朝着微服务方向前进一步,随着时间推移,单体应用将会越来越简单,用户就可以增加更多独立的微服务。

+

服务治理治的是什么

+

摘自《微服务设计》:

+
+

在进行城市交通规划之前首先要做的第一个事情是收集信息,要能够知道这个城市发生了什么,所以在各个路口需要安装采集探头,记录车来车往的信息。有了信息以后就需要对信息进行分析了,那么就需要可视化的图形界面,能够一眼就看出什么地方出了问题,通往哪个工厂的路坏了。发现了问题就要解决问题了,限制一下拥堵路段的流量,把去往一个公园的车辆导向到另外一个类似的公园。最后,如果把城市作为一个国家来考虑,那么每个进入这个城市的车辆都需要进行检查,看看有没有携带违禁品,最后给这些不熟悉道路的外地车规划路线。通过上面这个思考的过程,我们发现要对一个城市进行治理的时候,第一要采集信息,然后要能够对采集的信息进行监控和分析,最后根据分析的结果采取对应的治理策略。另外从整体安全的角度考虑还需要一个守门人

+
+

我们也用同样的思路来思考服务治理,网关就是整个整体的守门人,日志采集,追踪工具,服务注册发现都是用来采集信息的,然后需要监控平台来展现这些采集的信息,并进行监控和分析。 +最后根据分析的结果采取治理策略,有的服务快撑不住了要限流,有的服务坏了要熔断,并且还能够及时的调整这些服务的配置。

+

分布式架构-011

+

SpringCloud

+

随着微服务模式的使用,服务之间的调用带来的问题有很多,例如:数据一致性、网络波动、缓存、事务等问题,针对这一系列的问题就要有对应的框架来解决,目前主流一站式微服务解决方案有Spring CloudSpring Cloud Alibaba

+

Spring Cloud它并不是一个框架,而是很多个框架。它是分布式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶。

+

分布式架构-001

+

分布式架构-010

+

服务注册发现

+

在服务注册与发现中,有一个注册中心。当服务器启动的时候,会把当前自己服务器的信息比如服务地址通讯地址等以别名方式注册到注册中心上。另一方消费者服务提供者,以该别名的方式去注册中心上获取到实际的服务通讯地址,然后再实现本地RPC调用。RPC远程调用框架核心设计思想:在于注册中心,因为使用注册中心管理每个服务与服务之间的一个依赖关系。在任何RPC远程框架中,都会有一个注册中心存放服务地址相关信息。

+

Eureka

+

参考文章:

+ +
+

Eureka is a REST (Representational State Transfer) based service that is primarily used in the AWS cloud for locating services for the purpose of load balancing and failover of middle-tier servers。 We call this service, the Eureka Server。 Eureka also comes with a Java-based client component,the Eureka Client, which makes interactions with the service much easier。 The client also has a built-in load balancer that does basic round-robin load balancing。 —https://github.com/Netflix/eureka

+
+

大意:Eureka是一个REST (Representational State Transfer)服务,它主要用于AWS云,用于定位服务,以实现中间层服务器的负载平衡和故障转移,我们称此服务为Eureka服务器。Eureka也有一个基于java的客户端组件,Eureka客户端,这使得与服务的交互更加容易,同时客户端也有一个内置的负载平衡器,它执行基本的循环负载均衡。

+

Eureka采用了Client / Server 的设计架构,提供了完整的服务注册和服务发现,可以和 Spring Cloud 无缝集成。Eureka Sever作为服务注册功能的服务器,它是服务注册中心。而系统中的其他微服务,使用Eureka的客户端连接到 Eureka Server并维持心跳连接。这样系统的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行。

+

分布式架构-002

+
    +
  • Eureka Server 提供服务注册服务;各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。
  • +
  • Eureka Client 通过注册中心进行访问;它是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的使用轮询负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳默认周期为30秒。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除,默认90秒。 +
      +
    • Service Provider:服务提供方,将自身服务注册到Eureka,从而使服务消费方能够找到;
    • +
    • Service Consumer:服务消费方,从Eureka获取注册服务列表,从而能够消费服务;
    • +
    +
  • +
+

Eureka的自我保护机制:

+

默认情况下,如果Eureka Server在一定时间内(默认90秒)没有接收到某个微服务实例的心跳,Eureka Server将会移除该实例。但是当网络分区故障发生时,微服务与Eureka Server之间无法正常通信,而微服务本身是正常运行的,此时不应该移除这个微服务,所以引入了自我保护机制。

+
+

自我保护模式正是一种针对网络异常波动的安全保护措施,使用自我保护模式能使Eureka集群更加的健壮、稳定的运行。

+
+

如果在15分钟内超过85%的客户端节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,Eureka Server自动进入自我保护机制:

+
    +
  • Eureka Server不再从注册列表中移除因为长时间没收到心跳而应该过期的服务;
  • +
  • Eureka Server仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上,保证当前节点依然可用;
  • +
  • 当网络稳定时,当前Eureka Server新的注册信息会被同步到其它节点中;
  • +
+

简而言之,某时刻某一个微服务不可用了,Eureka不会立刻清理,依旧会对该微服务的信息进行保存。所以Eureka属于AP

+

服务熔断降级

+

由于分布式架构中应用程序依赖关系可能非常多,每个依赖关系在某些时候将不可避免地失败,针对服务调用失败,为了缩小调用失败的影响,引入了服务熔断降级的思想。

+

多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务B和微服务C又调用其它的微服务,这就是所谓的“扇出”。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的“雪崩效应”。

+
    +
  • 服务熔断:熔断机制是应对雪崩效应的一种微服务链路保护机制,当链路的某个微服务不可用或者响应时间太长时,将快速的返回设置好的提示信息。
  • +
  • 服务降级:服务降级一般是指在服务器压力剧增的时候,根据实际业务使用情况以及流量,对一些服务和页面有策略的不处理或者用一种简单的方式进行处理,从而释放服务器资源的资源以保证核心业务的正常高效运行。
  • +
+

服务熔断是应对系统服务雪崩的一种保险措施,给出的一种特殊降级措施。而服务降级则是更加宽泛的概念,主要是对系统整体资源的合理分配以应对压力。服务熔断可看作特殊降级。

+

Hystrix

+

Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。

+
+

“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。

+
+

“断路器"思想:https://martinfowler.com/bliki/CircuitBreaker.html

+

分布式架构-003

+
    +
  • 熔断打开:请求不再进行调用当前服务,内部定时一般为MTTR(平均故障处理时间),当打开时长达到所设的时长则进入半熔断状态;
  • +
  • 熔断半开:部分请求根据规则调用当前服务,如果请求成功且符合规则则认为当前服务恢复正常,关闭熔断;
  • +
  • 熔断关闭:熔断关闭不会对服务进行熔断;
  • +
+

Hystrix工作流程

+

分布式架构-004

+
    +
  1. 创建HystrixCommandHystrixObserableCommand对象。
  2. +
  3. 命令执行: +
      +
    • 其中HystrixCommand实现了下面前两种执行方式: +
        +
      • execute:同步执行,从依赖的服务返回一个单一的结果对象或是在发生错误的时候抛出异常。
      • +
      • queue:异步执行,直接返回一个Future对象,其中包含了服务执行结束时要返回的单一结果对象。
      • +
      +
    • +
    • HystrixObservableCommand实现了后两种执行方式: +
        +
      • obseve():返回Observable对象,它代表了操作的多个结果,它是一个Hot Observable,不论“事件源”是否有“订阅者”,都会在创建后对事件进行发布,所以对于Hot Observable的每一个“订阅者”都有可能是从“事件源”的中途开始的,并可能只是看到了整个操作的局部过程。
      • +
      • toObservable():同样会返回Observable对象,也代表了操作的多个结果,但它返回的是一个Cold Observable,没有“订间者”的时候并不会发布事件,而是进行等待,直到有“订阅者"之后才发布事件,所以对于Cold Observable 的订阅者,它可以保证从一开始看到整个操作的全部过程。
      • +
      +
    • +
    +
  4. +
  5. 判断当前命令的请求缓存功能是被启用的,并且该命令缓存命中,那么缓存的结果会立即以Observable对象的形式返回。
  6. +
  7. 检查断路器是否为打开状态。如果断路器是打开的,那么Hystrix不会执行命令,而是转接到fallback处理逻辑(第8步);如果断路器是关闭的,检查是否有可用资源来执行命令(第5步)。
  8. +
  9. 判断线程池/请求队列信号量是否占满。如果命令依赖服务的专有线程地和请求队列,或者信号量已经被占满,那么Hystrix也不会执行命令,而是转接到fallback处理理辑(第8步) 。
  10. +
  11. Hystrix会根据我们编写的方法来决定采取什么样的方式去请求依赖服务: +
      +
    • HystrixCommand.run():返回一个单一的结果,或者抛出异常。
    • +
    • HystrixObservableCommand.construct():返回一个Observable对象来发射多个结果,或通过onError发送错误通知。
    • +
    +
  12. +
  13. Hystrix会将“成功”、“失败”、“拒绝”、“超时” 等信息报告给断路器,而断路器会维护一组计数器来统计这些数据。断路器会使用这些统计数据来决定是否要将断路器打开,来对某个依赖服务的请求进行"熔断/短路”。
  14. +
  15. 当命令执行失败的时候,Hystrix会进入fallback尝试回退处理,我们通常也称波操作为“服务降级”。而能够引起服务降级处理的情况有下面几种: +
      +
    • 第4步∶当前命令处于“熔断/短路”状态,断路器是打开的时候。
    • +
    • 第5步∶当前命令的线程池、请求队列或者信号量被占满的时候。
    • +
    • 第6步∶HystrixObsevableCommand.construct()HytrixCommand.run()抛出异常的时候。
    • +
    +
  16. +
  17. 当Hystrix命令执行成功之后,它会将处理结果直接返回或是以Observable的形式返回给调用方。
  18. +
+

服务网关

+

参考文章:

+ +

分布式架构-005

+

网关的角色是作为一个 API 架构,用来保护、增强和控制对于 API 服务的访问。API 网关是一个处于应用程序或服务(提供 REST API 接口服务)之前的系统,用来管理授权、访问控制和流量限制等,这样 REST API 接口服务就被 API 网关保护起来,对所有的调用者透明。因此,隐藏在 API 网关后面的业务系统就可以专注于创建和管理服务,而不用去处理这些策略性的基础设施。

+

网关作用:

+
    +
  • 作为所有API接口服务的请求接入点;
  • +
  • 作为所有后端业务服务的聚合点;
  • +
  • 实现安全、验证、路由、过滤、流量等策略;
  • +
  • 对所有API服务和策略进行统一管理;
  • +
+

GetWay

+

Spring Cloud Gateway是Spring官方基于Spring 5.0,Spring Boot 2.0和Project Reactor等技术开发的网关,Spring Cloud Gateway旨在为微服务架构提供一种简单而有效的统一的API路由管理方式。Spring Cloud Gateway作为Spring Cloud生态系中的网关,目标是替代ZUUL,其不仅提供统一的路由方式,并且基于Filter链的方式提供了网关基本的功能,例如:安全,监控/埋点,和限流等。

+

核心组件:

+
    +
  • Route - 路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如断言为true则匹配该路由;
  • +
  • Predicate - 断言参考的是Java8的java.util.function。Predicate,开发人员可以匹配HTTP请求中的所有内容,例如请求头或请求参数,如果请求与断言相匹配则进行路由;
  • +
  • Filter - 过滤指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改;
  • +
+

工作流程: +分布式架构-006

+

客户端向Spring Cloud Gateway发出请求。然后在Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到GatewayWeb Handler,Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。

+

过滤器分为前置过滤器和后置过滤器,可以在发送代理请求之前或之后执行业务逻辑。

+
    +
  • 前置过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等;
  • +
  • 后置过滤器可以做响应内容、响应头的修改,日志的输出,流量监控等。
  • +
+

服务调用

+

参考文章:

+ +

服务调用可以说是微服务中最关键的,如果没有服务调用不会出现熔断降级、也不会出现服务负载、服务注册,分布式的服务治理可以说是围绕服务调用展开的。

+

常见的服务之间的调用方式有两种:

+
    +
  • RPC远程过程调用:定义数据式,基于原生TCP通信,速度快,效率高。早期的wedservice,现在热门的dubbo,都是RPC的典型代表;
  • +
  • Http:http其实是一种网络传输协议,基于TCP,规定了数据传输的格式。现在客户端浏览器与服务器端通信基本都是采用HTTP协议,也可以用来进行远程服务调用。缺点是消息封装臃肿,优势是对服务的提供和调用方没有任何技术限定,自由灵活,更符合为服务理念;
  • +
+

现在热门的Rest风格,就可以通过HTTP协议来实现,如果公司全部采用Java技术栈,那么使用Dubbo作为微服务框架是一个不错的选择;如果公司的技术栈多样化,而且你更青睐Spring家族,那么SpringCloud搭建微服务是不二之选。

+

OpenFeign

+
+

Feign is a declarative web service client。 It makes writing web service clients easier。 To use Feign create an interface and annotate it。 It has pluggable annotation support including Feign annotations and JAX-RS annotations。 Feign also supports pluggable encoders and decoders。 Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web。 Spring Cloud integrates Ribbon and Eureka, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign。

+
+

大意:Feign是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡。

+

Feign和OpenFeign:

+
    +
  • Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务;
  • +
  • OpenFeign是Spring Cloud在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@Feignclient可以解析SpringMVc的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务;
  • +
+

通过使用OpenFeign可以使代码变得更加简洁,减轻程序员负担:

+
 // 伪代码
+ @Component
+ // 括号中,为在注册中心注册的服务名称
+ @FeignClient("CLOUD-PAYMENT-SERVICE")
+ public interface PaymentFeignService {
+ 
+     @GetMapping("/payment/get/{id}")
+     CommonResult<Payment> getPayment(@PathVariable("id") Long id);
+ 
+     @PostMapping("/payment/timeout")
+     String feignTimeoutTest();
+ }
+

openFeign工作原理:

+

分布式架构-008

+
    +
  1. 使用注解@FeignClient注册FactoryBean到IOC容器, 最终产生了一个虚假的实现类代理;
  2. +
  3. 使用Feign调用接口的地方,最终拿到的都是一个假的代理实现类;
  4. +
  5. 所有发生在代理实现类上的调用,都被转交给Feign框架,翻译成HTTP的形式发送出去,并得到返回结果,再翻译回接口定义的返回值形式;
  6. +
+

服务负载

+
+

负载均衡,英文名称为Load Balance,其含义就是指将工作任务进行平衡、分摊到多个操作单元上进行运行,例如FTP服务器、Web服务器、企业核心应用服务器和其它主要任务服务器等,从而协同完成工作任务。

+
+

当前服务与服务之间进行相互调用时,在分布式架构下应用都是集群部署,所以这个时候就需要进行服务负载,即将收到的请求分摊到对应的服务器上,从而达到系统的高可用。

+

常见负载均衡算法:

+
    +
  • 随机分配:随机选择一台服务器来分配任务。它保证了请求的分散性达到了均衡的目的。同时它是没有状态的不需要维持上次的选择状态和均衡因子。但是随着任务量的增大,它的效果趋向轮询后也会具有轮询算法的部分缺点;
  • +
  • 轮询分配:将任务分配给此时具有最小连接数的节点,因此它是动态负载均衡算法。一个节点收到一个任务后连接数就会加1,当节点故障时就将节点权值设置为0,不再给节点分配任务;
  • +
  • 最小连接分配:将任务分配给此时具有最小连接数的节点,因此它是动态负载均衡算法。一个节点收到一个任务后连接数就会加1,当节点故障时就将节点权值设置为0,不再给节点分配任务;
  • +
  • hash分配:对请求中的关键信息进行hash计算,hash值相同的请求分配到同一台服务器,具体原理可查看HashMap分配原理;
  • +
  • 根据性能分配:根据服务器的响应时间来进行任务分配,优先将新任务分配给响应最快的服务器;
  • +
+

Ribbon

+

Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡的工具,是一个内置软件负载平衡器的进程间通信(远程过程调用)库。主要功能是提供客户端的软件负载均衡算法和服务调用。Ribbon客户端组件提供一系列完善的配置项,如连接超时、重试等。

+

本地负载与服务端负载:

+
    +
  • Nginx是服务器负载均衡,客户端所有请求都会交给nginx,然后由nginx实现转发请求。即负载均衡是由服务端实现的;
  • +
  • Ribbon是本地负载均衡,在调用微服务接口时候,会在注册中心上获取注册信息服务列表之后缓存到JVM本地,从而在本地实现RPC远程服务调用技术;
  • +
+

Ribbon其实是一个软负载均衡的客户端组件,它可以和其他所需请求的客户端结合使用,例如与Eureka结合:

+

分布式架构-007

+

消费方和服务方在注册中心注册服务,当消费方发起请求时,Ribbon会去注册中心寻找请求的服务名,即服务方集群,Ribbon默认负载算法会根据接口第几次请求 % 服务器集群总数量算出实际消费方服务器的位置,每次服务重启动后rest接口计数从1开始。

+

模拟Ribbon默认负载均衡算法:

+
public interface ILoadBalance {
+    ServiceInstance instance(List<ServiceInstance> instances); 
+}
+
@Component
+public class MyLoadBalance implements ILoadBalance{
+
+    /**
+     * 轮询索引
+     */
+    private final AtomicInteger index = new AtomicInteger(0);
+
+    /**
+     * 负载均衡算法:rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标,每次服务重启动后rest接口计数从1开始。
+     * @param instances 服务器集群数量
+     * @return 实际服务器的下标
+     */
+    @Override
+    public ServiceInstance instance(List<ServiceInstance> instances) {
+        return instances.get(incrementAndGet() % instances.size());
+    }
+
+    public final int incrementAndGet() {
+        int current = 0;
+        int next = 0;
+        do {
+            current = index.get();
+            // 当最大数量超过 Integer。MAX_VALUE 归0
+            next = current >= 2147483647 ? 0 : current + 1;
+        }while (!index.compareAndSet(current,next));
+        return next;
+    }
+}
+
 @Resource
+ private RestTemplate restTemplate;
+
+ @Resource
+ private DiscoveryClient discoveryClient;
+
+ @Resource
+ private ILoadBalance iLoadBalance;
+
+ @GetMapping("/myLoadBalance")
+ public String myLoadBalanceTest() {
+
+     List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
+     ServiceInstance instance = iLoadBalance.instance(instances);
+
+     URI uri = instance.getUri();
+     String finalUri = String.format("%s/%s", uri, PaymentConstant。PAYMENT_GETPORT_API);
+
+     log.info("自定义负载均衡,请求地址:{}", finalUri);
+
+     return restTemplate.getForObject(finalUri, String.class);
+ }
+

服务配置中心

+

在分布式微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少的。

+

配置中心应运而生。配置中心,顾名思义,就是来统一管理项目中所有配置的系统。对于单机版,我们称之为配置文件;对于分布式集群系统,我们称之为配置中心。

+

配置中心作用:

+
    +
  • 统一管理不同环境、不同集群的配置;
  • +
  • 将配置与应用分离、解耦合;
  • +
  • 版本发布管理;
  • +
+

SpringCloud Config

+

参考文章:

+ +

SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。

+

分布式架构-009

+

SpringCloud Config是配置中心的一种,它分为服务端和客户端两部分:

+
    +
  • 服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密/解密信息等访问接口;Config-Server端集中式存储/管理配置文件,并对外提供接口方便Config-Client访问,接口使用HTTP的方式对外提供访问;
  • +
  • 客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容;
  • +
+

使用SpringCloud Config非常简单,需要在服务端和客户端分别进行改造:

+
    +
  1. 引入依赖就不说了,首先将分布式中系统中的配置放到git或svn上,在服务端配置文件里配置好git或svn用户名密码、配置文件所在的分支及配置文件的目录,再将服务端启动类加上@EnableConfigServer注解,就大功告成了;
  2. +
  3. 放在git或svn上的配置文件名称要符合规则,才能通过配置中心访问得到该文件。一些常用的配置文件规则,其中,label:分支、profiles:环境(dev/test/prod)、application:服务名: +
     ## http://127.0.0.1:3344/master/config-dev.yml
    + 1. /{label}/{application}-{profile}.yml
    +
    + ## http://127.0.0.1:3344/config-dev.yml
    + 2. /{application}-{profile}.yml
    +
    + ## http://127.0.0.1:3344/config/dev/master
    + 3. /{application}/{profile}[/{label}]
    +
  4. +
  5. 在客户端也要引入依赖和进行相关配置:配置中心地址、分支名称、配置文件名称、环境,需要注意的是要将客户端模块下的application.yml文件改为bootstrap.yml,再将主启动类加上@EnableEurekaClient注解; +
    +

    当配置客户端启动时,它绑定到配置服务器(通过spring.cloud.config.uri引导配置属性),并使用远程属性源初始化Spring Environment。 +这种行为的最终结果是,所有想要使用Config Server的客户端应用程序都需要bootstrap.yml或一个环境变量

    +

    applicaiton.yml是用户级的资源配置项,而bootstrap.yml是系统级的,优先级更加高,在加载配置时优先加载bootstrap.yml

    +
    +
  6. +
+

当将SpringCloud Config客户端服务端都配置好之后,修改配置时会发现修改的配置文件不能实时生效;针对这个问题,可以将服务重启或者调用actuator的刷新接口使其生效,使用之前需要引入actuator的依赖:

+
 # 使用SpringCloud Config修改完配置后,调用刷新接口使客户端配置生效
+ curl -X POST "http://localhost:3355/actuator/refresh"
+

为了避免手动的调用刷新接口,可以使用SpringCloud Bus配合SpringCloud Config实现配置的动态刷新。

+

服务开发

+

长期以来 Java 的开发一直让人所诟病:项目开发复杂度极其高、项目的维护非常困难;即便使用了大量的开发框架,发现我们的开发也没少多少。为了解决让开发更佳简单,项目更容易管理,SpringBoot诞生了。

+

Spring Boot是一个广泛用来构建Java微服务的框架,它基于Spring依赖注入框架来进行工作。

+

SpringBoot

+

官网地址:https://spring.io/projects/spring-boot

+
+

SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。 +该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。SpringBoot 提供了一种新的编程范式,可以更加快速便捷地开发 Spring 项目,在开发过程当中可以专注于应用程序本身的功能开发,而无需在 Spring 配置上花太大的工夫。

+
+

SpringBoot 基于 Sring4 进行设计,继承了原有 Spring 框架的优秀基因。 +SpringBoot 准确的说并不是一个框架,而是一些类库的集合。 +maven 或者 gradle 项目导入相应依赖即可使用 SpringBoot,而无需自行管理这些类库的版本。

+

特点:

+
    +
  • 独立运行的 Spring 项目: +SpringBoot 可以以 jar 包的形式独立运行,运行一个 SpringBoot 项目只需通过 java–jar xx.jar 来运行。
  • +
  • 内嵌 Servlet 容器: +SpringBoot 可选择内嵌 TomcatJetty 或者 Undertow,这样我们无须以 war 包形式部署项目。
  • +
  • 提供 starter 简化 Maven 配置: +Spring 提供了一系列的 starter pom 来简化 Maven 的依赖加载,例如,当你使用了spring-boot-starter-web 时,会自动加入依赖包。
  • +
  • 自动配置 Spring: +SpringBoot 会根据在类路径中的 jar 包、类,为 jar 包里的类自动配置 Bean,这样会极大地减少我们要使用的配置。当然,SpringBoot 只是考虑了大多数的开发场景,并不是所有的场景,若在实际开发中我们需要自动配置 Bean,而 SpringBoot 没有提供支持,则可以自定义自动配置。
  • +
  • 准生产的应用监控: +SpringBoot 提供基于 http、ssh、telnet 对运行时的项目进行监控。
  • +
  • 无代码生成和 xml 配置: +SpringBoot 的神奇的不是借助于代码生成来实现的,而是通过条件注解来实现的,这是 Spring 4.x 提供的新特性。Spring 4.x 提倡使用 Java 配置和注解配置组合,而 SpringBoot 不需要任何 xml 配置即可实现 Spring 的所有配置。
  • +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/java-syntax-sugar/index.html b/blog-site/public/posts/essays/java-syntax-sugar/index.html new file mode 100644 index 00000000..2a5a647f --- /dev/null +++ b/blog-site/public/posts/essays/java-syntax-sugar/index.html @@ -0,0 +1,1527 @@ + + + + + + + + + + + Java语法糖 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Java语法糖

+ 2021.04.10 +
+

原文地址:https://www.jianshu.com/p/0f967298a5d7

+

语法糖

+

语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法, +这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。

+
+

在编程领域,除了语法糖,还有语法盐和语法糖精的说法,篇幅有限这里不做扩展了。 +我们所熟知的编程语言中几乎都有语法糖。作者认为,语法糖的多少是评判一个语言够不够牛逼的标准之一。

+
+

很多人说Java是一个“低糖语言”,其实从Java 7开始Java语言层面上一直在添加各种糖, +主要是在“Project Coin”项目下研发。尽管现在Java有人还是认为现在的Java是低糖,未来还会持续向着“高糖”的方向发展。

+

解语法糖

+

前面提到过,语法糖的存在主要是方便开发人员使用。但其实,Java虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖。

+

说到编译,大家肯定都知道,Java语言中,javac命令可以将后缀名为.java的源文件编译为后缀名为.class的可以运行于Java虚拟机的字节码。

+

如果你去看com.sun.tools.javac.main.JavaCompiler的源码,你会发现在compile()中有一个步骤就是调用desugar(),这个方法就是负责解语法糖的实现的。

+

Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。本文主要来分析下这些语法糖背后的原理。一步一步剥去糖衣,看看其本质。

+

switch 支持 String 与枚举

+

前面提到过,从Java 7 开始,Java语言中的语法糖在逐渐丰富,其中一个比较重要的就是Java 7中switch开始支持String。

+

在开始coding之前先科普下,Java中的swith自身原本就支持基本类型。比如int、char等。

+

对于int类型,直接进行数值的比较。对于char类型则是比较其ascii码。

+

所以,对于编译器来说,switch中其实只能使用整型,任何类型的比较都要转换成整型。比如byte。short,char(ackii码是整型)以及int。

+

那么接下来看下switch对String得支持,有以下代码:

+
public class switchDemoString {
+    public static void main(String[] args) {
+        String str = "world";
+        switch (str) {
+            case "hello":
+                        System.out.println("hello");
+            break;
+            case "world":
+                        System.out.println("world");
+            break;
+            default:
+                        break;
+        }
+    }
+}
+

反编译后内容如下:

+
public class switchDemoString
+{
+    public switchDemoString()
+        {
+    }
+    public static void main(String args[])
+        {
+        String str = "world";
+        String s;
+        switch((s = str).hashCode())
+                {
+            default:
+                        break;
+            case 99162322:
+                        if(s.equals("hello"))
+                            System.out.println("hello");
+            break;
+            case 113318802:
+                        if(s.equals("world"))
+                            System.out.println("world");
+            break;
+        }
+    }
+}
+

看到这个代码,你知道原来字符串的switch是通过equals()和hashCode()方法来实现的。还好hashCode()方法返回的是int,而不是long。

+
+

仔细看下可以发现,进行switch的实际是哈希值,然后通过使用equals方法比较进行安全检查,这个检查是必要的, +因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行switch或者使用纯整数常量,但这也不是很差。

+
+

泛型

+

我们都知道,很多语言都是支持泛型的,但是很多人不知道的是,不同的编译器对于泛型的处理方式是不同的。

+

通常情况下,一个编译器处理泛型有两种方式:Code specializationCode sharing

+

C++和C#是使用Code specialization的处理机制,而Java使用的是Code sharing的机制。

+
+

Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的。 +也就是说,对于Java虚拟机来说,他根本不认识Map<String, String> map这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖。

+
+

类型擦除的主要过程如下:

+
    +
  • 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。
  • +
  • 移除所有的类型参数。
  • +
+

以下代码:

+
Map<String, String> map = new HashMap<String, String>();  
+map.put("name", "suxiansheng");  
+map.put("wechat", "java");  
+map.put("blog", "https://www.jianshu.com/u/94111742c97c");  
+

解语法糖之后会变成:

+
Map map = new HashMap();  
+map.put("name", "suxiansheng");  
+map.put("wechat", "Java");  
+map.put("blog", "https://www.jianshu.com/u/94111742c97c");  
+

以下代码:

+
public static <A extends Comparable<A>> A max(Collection<A> xs) {
+    Iterator<A> xi = xs.iterator();
+    A w = xi.next();
+    while (xi.hasNext()) {
+        A x = xi.next();
+        if (w.compareTo(x) < 0)
+            w = x;
+    }
+    return w;
+}
+

类型擦除后会变成:

+
 public static Comparable max(Collection xs){
+    Iterator xi = xs.iterator();
+    Comparable w = (Comparable)xi.next();
+    while(xi.hasNext())
+    {
+        Comparable x = (Comparable)xi.next();
+        if(w.compareTo(x) < 0)
+            w = x;
+    }
+    return w;
+}
+

虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的Class类对象。 +比如并不存在List<String>.class或是List<Integer>.class,而只有List.class

+

自动装箱与拆箱

+

自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。参考:一文读懂什么是Java中的自动拆装箱

+

因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。

+

原始类型byte, short, char, int, long, float, double,boolean 对应的封装类为Byte, Short, Character, Integer, Long, Float, Double, Boolean

+

先来看个自动装箱的代码:

+
 public static void main(String[] args) {
+    int i = 10;
+    Integer n = i;
+}
+

反编译后代码如下:

+
public static void main(String args[])
+{
+    int i = 10;
+    Integer n = Integer.valueOf(i);
+}
+

再来看个自动拆箱的代码:

+
public static void main(String[] args) {
+
+    Integer i = 10;
+    int n = i;
+}
+

反编译后代码如下:

+
public static void main(String args[])
+{
+    Integer i = Integer.valueOf(10);
+    int n = i.intValue();
+}
+

从反编译得到内容可以看出,在装箱的时候自动调用的是Integer的valueOf(int)方法。 +而在拆箱的时候自动调用的是Integer的intValue方法。 +所以,装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的。

+

方法变长参数

+

可变参数(variable arguments)是在Java 1.5中引入的一个特性。它允许一个方法把任意数量的值作为参数。

+

看下以下可变参数代码,其中print方法接收可变参数:

+
public static void main(String[] args)
+    {
+        print("java", "123", "456", "789");
+    }
+
+public static void print(String... strs)
+{
+    for (int i = 0; i < strs.length; i++)
+    {
+        System.out.println(strs[i]);
+    }
+}
+

反编译后代码:

+
public static void main(String args[])
+{
+    print(new String[] {
+        "java", "123", "456", "789"
+    });
+}
+
+public static transient void print(String strs[])
+{
+    for(int i = 0; i < strs.length; i++)
+        System.out.println(strs[i]);
+
+}
+

从反编译后代码可以看出,可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数, +然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。

+

枚举

+

Java SE5提供了一种新的类型-Java的枚举类型,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。

+

要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是enum吗?

+

答案很明显不是,enum就和class一样,只是一个关键字,他并不是一个类。

+

那么枚举是由什么类维护的呢,我们简单的写一个枚举:

+
public enum t {
+    SPRING,SUMMER;
+}
+

然后我们使用反编译,看看这段代码到底是怎么实现的,反编译后代码内容如下:

+
public final class T extends Enum
+{
+    private T(String s, int i)
+    {
+        super(s, i);
+    }
+    public static T[] values()
+    {
+        T at[];
+        int i;
+        T at1[];
+        System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
+        return at1;
+    }
+
+    public static T valueOf(String s)
+    {
+        return (T)Enum.valueOf(demo/T, s);
+    }
+
+    public static final T SPRING;
+    public static final T SUMMER;
+    private static final T ENUM$VALUES[];
+    static
+    {
+        SPRING = new T("SPRING", 0);
+        SUMMER = new T("SUMMER", 1);
+        ENUM$VALUES = (new T[] {
+            SPRING, SUMMER
+        });
+    }
+}
+

通过反编译后代码我们可以看到,public final class T extends Enum,说明, +该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。

+

当我们使用enmu来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承Enum类,所以枚举类型不能被继承。

+

内部类

+

内部类又称为嵌套类,可以把内部类理解为外部类的一个普通成员。

+

内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念。

+

outer.java里面定义了一个内部类inner,一旦编译成功,就会生成两个完全不同的.class文件了, +分别是outer.classouter$inner.class。所以内部类的名字完全可以和它的外部类名字相同。

+
public class OutterClass {
+    private String userName;
+
+    public String getUserName() {
+        return userName;
+    }
+
+    public void setUserName(String userName) {
+        this.userName = userName;
+    }
+
+    public static void main(String[] args) {
+
+    }
+
+    class InnerClass{
+        private String name;
+
+        public String getName() {
+            return name;
+        }
+
+        public void setName(String name) {
+            this.name = name;
+        }
+    }
+}
+

以上代码编译后会生成两个class文件:OutterClass$InnerClass.class 、OutterClass.class

+

当我们尝试使用jad对OutterClass.class文件进行反编译的时候,命令行会打印以下内容:

+
Parsing OutterClass.class...
+Parsing inner class OutterClass$InnerClass.class...
+Generating OutterClass.jad
+

他会把两个文件全部进行反编译,然后一起生成一个OutterClass.jad文件。文件内容如下:

+
public class OutterClass
+{
+    class InnerClass
+    {
+        public String getName()
+        {
+            return name;
+        }
+        public void setName(String name)
+        {
+            this.name = name;
+        }
+        private String name;
+        final OutterClass this$0;
+
+        InnerClass()
+        {
+            this.this$0 = OutterClass.this;
+            super();
+        }
+    }
+
+    public OutterClass()
+    {
+    }
+    public String getUserName()
+    {
+        return userName;
+    }
+    public void setUserName(String userName){
+        this.userName = userName;
+    }
+    public static void main(String args1[])
+    {
+    }
+    private String userName;
+}
+

七 、条件编译

+

—般情况下,程序中的每一行代码都要参加编译。 +但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。

+

如在C或CPP中,可以通过预处理语句来实现条件编译。其实在Java中也可实现条件编译。我们先来看一段代码:

+
public class ConditionalCompilation {
+    public static void main(String[] args) {
+        final boolean DEBUG = true;
+        if(DEBUG) {
+            System.out.println("Java, DEBUG!");
+        }
+
+        final boolean ONLINE = false;
+
+        if(ONLINE){
+            System.out.println("Java, ONLINE!");
+        }
+    }
+}
+

反编译后代码如下:

+
public class ConditionalCompilation
+{
+
+    public ConditionalCompilation()
+    {
+    }
+
+    public static void main(String args[])
+    {
+        boolean DEBUG = true;
+        System.out.println("Java, DEBUG!");
+        boolean ONLINE = false;
+    }
+}
+

首先,我们发现,在反编译后的代码中没有System.out.println("Hello, ONLINE!");,这其实就是条件编译。

+

当if(ONLINE)为false的时候,编译器就没有对其内的代码进行编译。 +所以,Java语法的条件编译,是通过判断条件为常量的if语句实现的。根据if判断条件的真假,编译器直接把分支为false的代码块消除。 +通过该方式实现的条件编译,必须在方法体内实现,而无法在正整个Java类的结构或者类的属性上进行条件编译。

+

这与C/C++的条件编译相比,确实更有局限性。在Java语言设计之初并没有引入条件编译的功能,虽有局限,但是总比没有更强。

+

断言

+

在Java中,assert关键字是从JAVA SE 1.4 引入的,为了避免和老版本的Java代码中使用了assert关键字导致错误, +Java在执行的时候默认是不启动断言检查的(这个时候,所有的断言语句都将忽略!)。

+

如果要开启断言检查,则需要用开关-enableassertions-ea来开启。

+

看一段包含断言的代码:

+
public class AssertTest {
+    public static void main(String args[]) {
+        int a = 1;
+        int b = 1;
+        assert a == b;
+        System.out.println("Java");
+        assert a != b : "suxiansheng";
+        System.out.println("博客:https://www.jianshu.com/u/94111742c97c");
+    }
+}
+

反编译后代码如下:

+
public class AssertTest {
+   public AssertTest()
+    {
+    }
+    public static void main(String args[])
+{
+    int a = 1;
+    int b = 1;
+    if(!$assertionsDisabled && a != b)
+        throw new AssertionError();
+    System.out.println("\u516C\u4F17\u53F7\uFF1AJava");
+    if(!$assertionsDisabled && a == b)
+    {
+        throw new AssertionError("Java");
+    } else
+    {
+        System.out.println("\u535A\u5BA2\uFF1Awww.jianshu.com/u/94111742c97c");
+        return;
+    }
+}
+
+static final boolean $assertionsDisabled = !com/hollis/suguar/AssertTest.desiredAssertionStatus();
+
+}
+

很明显,反编译之后的代码要比我们自己的代码复杂的多。所以,使用了assert这个语法糖我们节省了很多代码。

+

其实断言的底层实现就是if语言,如果断言结果为true,则什么都不做,程序继续执行,如果断言结果为false,则程序抛出AssertError来打断程序的执行。

+

数值字面量

+

在java 7中,数值字面量,不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。

+

比如:

+
public class Test {
+    public static void main(String... args) {
+        int i = 10_000;
+        System.out.println(i);
+    }
+}
+

反编译后:

+
public class Test
+{
+  public static void main(String[] args)
+  {
+    int i = 10000;
+    System.out.println(i);
+  }
+}
+

反编译后就是把删除了。也就是说编译器并不认识在数字字面量中的,需要在编译阶段把他去掉

+

for-each

+

增强for循环(for-each)相信大家都不陌生,日常开发经常会用到的,他会比for循环要少写很多代码,那么这个语法糖背后是如何实现的呢?

+
public static void main(String... args) {
+    String[] strs = {"suxiansehng", "Java", "博客:www.jianshu.com/u/94111742c97c"};
+    for (String s : strs) {
+        System.out.println(s);
+    }
+    List<String> strList = ImmutableList.of("suxiansheng", "java", "博客:www.jianshu.com/u/94111742c97c");
+    for (String s : strList) {
+        System.out.println(s);
+    }
+}
+

反编译后代码如下:

+
public static transient void main(String args[])
+{
+    String strs[] = {
+        "suxiansheng", "\u516C\u4F17\u53F7\uFF1AJava", "\u535A\u5BA2\uFF1Awww.jianshu.com/u/94111742c97c"
+    };
+    String args1[] = strs;
+    int i = args1.length;
+    for(int j = 0; j < i; j++)
+    {
+        String s = args1[j];
+        System.out.println(s);
+    }
+
+    List strList = ImmutableList.of("suxiansheng", "\u516C\u4F17\u53F7\uFF1AJava", "\u535A\u5BA2\uFF1Awww.jianshu.com/u/94111742c97c");
+    String s;
+    for(Iterator iterator = strList.iterator(); iterator.hasNext(); System.out.println(s))
+        s = (String)iterator.next();
+
+}
+

代码很简单,for-each的实现原理其实就是使用了普通的for循环和迭代器。

+

try-with-resource

+

Java里,对于文件操作IO流、数据库连接等开销非常昂贵的资源,用完之后必须及时通过close方法将其关闭,否则资源会一直处于打开状态,可能会导致内存泄露等问题。

+

关闭资源的常用方式就是在finally块里是释放,即调用close方法。比如,我们经常会写这样的代码:

+
public static void main(String[] args) {
+    BufferedReader br = null;
+    try {
+        String line;
+        br = new BufferedReader(new FileReader("d:\\hollischuang.xml"));
+        while ((line = br.readLine()) != null) {
+            System.out.println(line);
+        }
+    } catch (IOException e) {
+        // handle exception
+    } finally {
+        try {
+            if (br != null) {
+                br.close();
+            }
+        } catch (IOException ex) {
+            // handle exception
+        }
+    }
+}
+

从Java 7开始,jdk提供了一种更好的方式关闭资源,使用try-with-resources语句,改写一下上面的代码,效果如下:

+
public static void main(String... args) {
+    try (BufferedReader br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"))) {
+        String line;
+        while ((line = br.readLine()) != null) {
+            System.out.println(line);
+        }
+    } catch (IOException e) {
+        // handle exception
+    }
+}
+

看,这简直是一大福音啊,虽然我之前一般使用IOUtils去关闭流,并不会使用在finally中写很多代码的方式,但是这种新的语法糖看上去好像优雅很多呢。

+

反编译以上代码,看下他的背后原理:

+
public static transient void main(String args[])
+    {
+        BufferedReader br;
+        Throwable throwable;
+        br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"));
+        throwable = null;
+        String line;
+        try
+        {
+            while((line = br.readLine()) != null)
+                System.out.println(line);
+        }
+        catch(Throwable throwable2)
+        {
+            throwable = throwable2;
+            throw throwable2;
+        }
+        if(br != null)
+            if(throwable != null)
+                try
+                {
+                    br.close();
+                }
+                catch(Throwable throwable1)
+                {
+                    throwable.addSuppressed(throwable1);
+                }
+            else
+                br.close();
+            break MISSING_BLOCK_LABEL_113;
+            Exception exception;
+            exception;
+            if(br != null)
+                if(throwable != null)
+                    try
+                    {
+                        br.close();
+                    }
+                    catch(Throwable throwable3)
+                      {
+                        throwable.addSuppressed(throwable3);
+                    }
+                else
+                    br.close();
+        throw exception;
+        IOException ioexception;
+        ioexception;
+    }
+}
+

其实背后的原理也很简单,那些我们没有做的关闭资源的操作,编译器都帮我们做了。

+

所以,再次印证了,语法糖的作用就是方便程序员的使用,但最终还是要转成编译器认识的语言。

+

Lambda表达式

+

关于lambda表达式,有人可能会有质疑,因为网上有人说他并不是语法糖。其实我想纠正下这个说法。

+

Labmda表达式不是匿名内部类的语法糖,但是他也是一个语法糖。实现方式其实是依赖了几个JVM底层提供的lambda相关api。

+

先来看一个简单的lambda表达式。遍历一个list:

+
public static void main(String... args) {
+    List<String> strList = ImmutableList.of("suxiansheng", "Java", "博客:www.jianshu.com/u/94111742c97c");
+
+    strList.forEach( s -> { System.out.println(s); } );
+}
+

为啥说他并不是内部类的语法糖呢,前面讲内部类我们说过,内部类在编译之后会有两个class文件,但是,包含lambda表达式的类编译后只有一个文件。

+

反编译后代码如下:

+
public static /* varargs */ void main(String ... args) {
+    ImmutableList strList = ImmutableList.of((Object)"Java", (Object)"\u516c\u4f17\u53f7\uff1aJava", (Object)"\u535a\u5ba2\uff1awww.jianshu.com/u/94111742c97c");
+    strList.forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)());
+}
+
+private static /* synthetic */ void lambda$main$0(String s) {
+    System.out.println(s);
+}
+

可以看到,在forEach方法中,其实是调用了java.lang.invoke.LambdaMetafactory#metafactory方法, +该方法的第四个参数implMethod指定了方法实现。可以看到这里其实是调用了一个lambda0方法进行了输出。

+

再来看一个稍微复杂一点的,先对List进行过滤,然后再输出:

+
public static void main(String... args) {
+    List<String> strList = ImmutableList.of("suxiansheng", "Java, "博客:www.jianshu.com/u/94111742c97c");
+
+    List HollisList = strList.stream().filter(string -> string.contains("Java")).collect(Collectors.toList());
+
+    HollisList.forEach( s -> { System.out.println(s); } );
+}
+

反编译后代码如下:

+
public static /* varargs */ void main(String ... args) {
+    ImmutableList strList = ImmutableList.of((Object)"Java", (Object)"\u516c\u4f17\u53f7\uff1aJava", (Object)"\u535a\u5ba2\uff1awww.jianshu.com/u/94111742c97c");
+    List<Object> HollisList = strList.stream().filter((Predicate<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)Z, lambda$main$0(java.lang.String ), (Ljava/lang/String;)Z)()).collect(Collectors.toList());
+    HollisList.forEach((Consumer<Object>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$1(java.lang.Object ), (Ljava/lang/Object;)V)());
+}
+
+private static /* synthetic */ void lambda$main$1(Object s) {
+    System.out.println(s);
+}
+
+private static /* synthetic */ boolean lambda$main$0(String string) {
+    return string.contains("Hollis");
+}
+

两个lambda表达式分别调用了lambda1和lambda0两个方法。

+

所以,lambda表达式的实现其实是依赖了一些底层的api,在编译阶段,编译器会把lambda表达式进行解糖,转换成调用内部api的方式。

+

可能遇到的坑

+

泛型——当泛型遇到重载

+
public class GenericTypes {
+
+    public static void method(List<String> list) {  
+        System.out.println("invoke method(List<String> list)");  
+    }  
+
+    public static void method(List<Integer> list) {  
+        System.out.println("invoke method(List<Integer> list)");  
+    }  
+} 
+

上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是List另一个是List, +但是,这段代码是编译通不过的。因为我们前面讲过,参数List和List编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。

+

泛型——当泛型遇到catch

+

泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException和MyException

+

泛型——当泛型内包含静态变量

+
public class StaticTest{
+    public static void main(String[] args){
+        GT<Integer> gti = new GT<Integer>();
+        gti.var=1;
+        GT<String> gts = new GT<String>();
+        gts.var=2;
+        System.out.println(gti.var);
+    }
+}
+class GT<T>{
+    public static int var=0;
+    public void nothing(T x){}
+}
+

以上代码输出结果为:2!由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。

+

自动装箱与拆箱——对象相等比较

+
public static void main(String[] args) {
+    Integer a = 1000;
+    Integer b = 1000;
+    Integer c = 100;
+    Integer d = 100;
+    System.out.println("a == b is " + (a == b));
+    System.out.println(("c == d is " + (c == d)));
+}
+

输出结果:

+
a == b is false
+c == d is true
+

在Java 5中,在Integer的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。

+
+

适用于整数值区间-128 至 +127。只适用于自动装箱。使用构造函数创建对象不适用。

+
+

增强for循环

+
for (Student stu : students) {    
+    if (stu.getId() == 2)     
+        students.remove(stu);    
+}
+

会抛出ConcurrentModificationException异常。

+

Iterator是工作在一个独立的线程中,并且拥有一个 mutex 锁。 +Iterator被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象, +所以按照 fail-fast 原则 Iterator 会马上抛出java.util.ConcurrentModificationException异常。

+

所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 Iterator 本身的方法remove()来删除对象,Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。

+

总结

+
    +
  • 前面介绍了12种Java中常用的语法糖。所谓语法糖就是提供给开发人员便于开发的一种语法而已。
  • +
  • 但是这种语法只有开发人员认识。要想被执行,需要进行解糖,即转成JVM认识的语法。
  • +
  • 当我们把语法糖解糖之后,你就会发现其实我们日常使用的这些方便的语法,其实都是一些其他更简单的语法构成的。
  • +
  • 有了这些语法糖,我们在日常开发的时候可以大大提升效率,但是同时也要避免过渡使用。使用之前最好了解下原理,避免掉坑。
  • +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/java-thread-collection/index.html b/blog-site/public/posts/essays/java-thread-collection/index.html new file mode 100644 index 00000000..a01b8670 --- /dev/null +++ b/blog-site/public/posts/essays/java-thread-collection/index.html @@ -0,0 +1,634 @@ + + + + + + + + + + + Java中集合的线程不安全问题 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Java中集合的线程不安全问题

+ 2020.04.05 +
+

ArrayList

+

ArrayList线程不安全示例:

+
	public static void main(String[] args) {
+		ArrayList<String> arrayList = new ArrayList<>();
+		for(int i=0; i< 3; i++) {
+			new Thread(() -> {
+				arrayList.add(UUID.randomUUID().toString());
+				System.out.println(arrayList);
+			},String.valueOf(i)).start();
+		}
+	}
+
// ConcurrentModificationException 同步修改异常
+Exception in thread "8" java.util.ConcurrentModificationException
+
+[null, 2041b613-8068-4ddd-9d01-305f5680d377]
+[null, 2041b613-8068-4ddd-9d01-305f5680d377, b3e0296d-e263-4632-a023-4267cdec5e25]
+[null, 2041b613-8068-4ddd-9d01-305f5680d377]
+

原因分析: +当某个线程正在执行 add()方法时,被某个线程打断,添加到一半被打断,没有被添加完

+

解决方案:

+
    +
  • 使用Vector来代替ArrayList,Vector是线程安全的ArrayList,但是由于,并发量太小,被淘汰
  • +
  • 使用Collections.synchronizedArrayList(new ArrayList<>())来创建ArrayList.使用Collections工具类来创建ArrayList的思路是,在ArrayList的外边套了一个外壳,来使ArrayList线程安全
  • +
  • 使用new CopyOnWriteArrayList()来保证ArrayList线程安全
  • +
+

CopyOnWriteArrayList原理

+

CopyWriteArrayList字面意思就是在写的时候复制,思想就是读写分离的思想

+

以下是CopyOnWriteArrayListadd()方法源码

+
/** The array, accessed only via getArray/setArray. */
+    private transient volatile Object[] array;
+
+/** The lock protecting all mutators */
+    final transient ReentrantLock lock = new ReentrantLock();
+
+  /**
+     * Gets the array.  Non-private so as to also be accessible
+     * from CopyOnWriteArraySet class.
+     */
+    final Object[] getArray() {
+        return array;
+    }
+
+/**
+     * 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();
+        }
+    }
+

因为在源码里面加了ReentrantLock所以保证了某个线程在写的时候不会被打断, +可以看到源码开始先是复制了一份数组(因为同一时刻只有一个线程写,其余的线程会读),在复制的数组上边进行写操作,写好以后在返回true. +这样写的就把读写进行了分离.写好以后因为array加了volatile关键字,所以该数组是对于其他的线程是可见的,就会读取到最新的值.

+

HashSet

+

HashSetArrayList类似,也是线程不安全的集合类,具体证明HashSet线程不安全的代码,请参考ArrayList线程不安全的示例. +因为与ArrayList类似,都属于一类问题,也会报ConcurrentModificationException 异常.

+

解决方案

+
    +
  • Collections.synchronizedSet(new HashSet<>())使用集合工具类解决
  • +
  • 使用new CopyOnWriteArraySet<>()来保证集合线程安全
  • +
+

CopyOnWriteArraySet原理

+
   private final CopyOnWriteArrayList<E> al;
+
+    /**
+     * Creates an empty set.
+     */
+    public CopyOnWriteArraySet() {
+        al = new CopyOnWriteArrayList<E>();
+    }
+

底层是CopyOnWriteArrayList

+

HashMap

+

HashMap也是线程不安全的集合类

+

解决方案

+
    +
  • Collections.synchronizedMap(new HashMap<>())使用集合工具类
  • +
  • new ConcurrentHashMap<>()来保证线程安全
  • +
+

ConcurrentHashMap原理

+

参考:CurrentHashMap原理

+

JDK1.7中ConcurrentHashMap采用了数组+Segment+分段锁的方式实现。

+
    +
  • +

    Segment(分段锁) +ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。

    +
  • +
  • +

    内部结构 +ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。如下图是ConcurrentHashMap的内部结构图: +CurrentHashMap

    +
  • +
+

从上面的结构我们可以了解到,ConcurrentHashMap定位一个元素的过程需要进行两次Hash操作。

+

第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部。

+
    +
  • +

    该结构的优劣势

    +
      +
    • 坏处: 这一种结构的带来的副作用是Hash的过程要比普通的HashMap要长
    • +
    • 好处: 写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,这样,在最理想的情况下,ConcurrentHashMap可以最高同时支持Segment数量大小的写操作(刚好这些写操作都非常平均地分布在所有的Segment上).所以,通过这一种结构,ConcurrentHashMap的并发能力可以大大的提高。
    • +
    +
  • +
+

JDK1.8版本的CurrentHashMap的实现原理

+

JDK8中ConcurrentHashMap参考了JDK8 HashMap的实现,采用了数组+链表+红黑树的实现方式来设计,内部大量采用CAS操作.如果不了解CAS请移步CAS原理 +JDK8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想。

+

Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。 +Java8 ConcurrentHashMap结构基本上和Java8的HashMap一样,不过保证线程安全性。

+

在JDK8中ConcurrentHashMap的结构,由于引入了红黑树,使得ConcurrentHashMap的实现非常复杂,我们都知道,红黑树是一种性能非常好的二叉查找树,其查找性能为O(logN),但是其实现过程也非常复杂,而且可读性也非常差,DougLea的思维能力确实不是一般人能比的,早期完全采用链表结构时Map的查找时间复杂度为O(N),JDK8中ConcurrentHashMap在链表的长度大于某个阈值的时候会将链表转换成红黑树进一步提高其查找性能。 +JDK1.8后的currentHashMap +其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树。

+

CurrentHashMapJDK1.7,JDK1.8前后对比

+
    +
  1. 数据结构:取消了Segment分段锁的数据结构,取而代之的是数组+链表+红黑树的结构。
  2. +
  3. 保证线程安全机制:JDK1.7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。
  4. +
  5. 锁的粒度:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。
  6. +
  7. 链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。
  8. +
  9. 查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。
  10. +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/java-transaction/index.html b/blog-site/public/posts/essays/java-transaction/index.html new file mode 100644 index 00000000..8bfe09d4 --- /dev/null +++ b/blog-site/public/posts/essays/java-transaction/index.html @@ -0,0 +1,1599 @@ + + + + + + + + + + + 分布式事务详解 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

分布式事务详解

+ 2021.08.02 +
+

基础概念

+

什么是事务

+

什么是事务?举个例子:你去超市买东西,“一手交钱,一手交货"就是一个事务的例子,交钱和交货必须同时成功,事务才算成功,其中有一个环节失败,事务将会撤销所有已成功的活动。

+

所以事务可以看作是一次重大的活动,它由不同的小活动组成,这些活动要么全部成功,要么全部失败。

+

本地事务

+

在计算机系统中,更多的是通过关系型数据库来控制事务,这是利用数据库本身的事务特性来实现的,因此叫做数据库事务,由于应用程序主要靠关系型数据库来控制事务,而数据库通常和应用在同一个服务器上,所以基于关系型数据库的事务又叫做本地事务。

+

数据库事务的四大特性:ACID:

+
    +
  • A(Atomic):原子性,构成事务的所有操作,要么都执行完,要么全不执行,不可能出现部分成功部分失败的情况。
  • +
  • C(Consistency):一致性,在事务执行前后,数据库的一致性约束没有被破坏。比如:张三向李四转100元,转账前和转账后的数据正确状态这叫一致性,如果出现张三转100元,李四账户没有增加100元这就出现了数据错误,就没有达到一致性。
  • +
  • I(Isolation):隔离性,数据库中事务都是并发的,隔离性是指并发的两个事务的执行互不干扰,一个事务不能看到其他事务运行过程的中间状态。通过配置事务的隔离级别可以避免脏读、重复读等问题。
  • +
  • D(Durability):持久性,事务执行完毕后,该事务对数据的更改会被持久化到数据库,且不会被回滚。
  • +
+

数据库事务在实现时将会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单元中的所有操作要么都成功,要么都失败,只要其中任一操作执行失败,都将会导致事务回滚。

+

分布式事务

+

随着互联网的发展,软件系统由原来的单体应用转变为分布式应用。分布式系统会把一个应用系统拆分为多个可独立部署的程序,因此需要服务与服务之间的远程协作才能完成事务操作,这种分布式系统环境下由不同服务之间通过网络协作完成的事务被称为分布式事务。 +例如:用户注册送积分事务、创建订单减少库存、银行转账事务。

+

本地事务与分布式事务理解:

+

1.本地事务:

+
begin transation;
+// 1.本地数据库操作:张三减少金额
+// 2.本地数据库操作:李四增加金额
+commit transation;
+

2.分布式事务:

+
begin transation;
+// 1.本地数据库操作:张三减少金额
+// 2.远程调用:李四增加金额
+commit transation;
+

2.分布式事务中如果李四增加金额成功,但是由于网络原因,远程调用并没有返回,此时本地事务提交失败就会回滚张三减少金额的操作,此时张三李四的金额就不一致了。

+

因此在分布式架构的基础上,传统数据库事务就无法使用了,张三和李四的账户不在同一个数据库中甚至不在一个系统中,实现转账事务需要通过远程调用,由于网络的问题就会导致分布式事务的问题。

+

分布式事务产生场景

+
    +
  1. +

    最典型的是微服务架构,微服务之间通过远程调用完成事务操作。比如:订单微服务和库存微服务,下单的同时订单微服务请求库存微服务减少库存。简而言之,就是跨JVM进程产生的分布式事务。

    +
  2. +
  3. +

    单体系统访问多个数据库实例,就会产生分布式事务。比如:用户和订单信息分别存储在两个MySQL中,用户管理系统删除用户信息,需要分别删除用户信息及用户的订单信息,由于数据分布在不同的数据库,就需要通过不同的数据库连接去操作数据,此时产生分布式事务。

    +
  4. +
  5. +

    多个微服务访问同一个数据库实例。比如:订单微服务和库存微服务即使访问同一个数据库也会产生分布式事务,原因是跨JVM进程,两个微服务持有了不同的数据库连接进行数据库操作,此时产生分布式事务。

    +
  6. +
+

分布式事务基础理论

+

分布式事务之所以叫做分布式事务,是因为提供服务的各个结点分布在不同的机器上,相互之间通过网络交互。不能因为有网络问题就导致整个系统无法提供服务,网络因素成了分布式事务的考量标准之一。因此,分布式事务需要更进一步的理论支持。

+

CAP理论

+

CAP是Consistency、Availability、Partition tolerance三个词语的缩写,分别表示一致性、可用性、分区容忍性。

+

理解CAP

+

为了方便对CAP理论的理解,结合电商系统中的一些业务场景来理解CAP;如下图是商品信息管理的执行流程:

+

分布式事务-001

+

整体执行流程:

+
    +
  • 商品服务请求主数据库写入商品信息(增、删、改)
  • +
  • 主数据库向商品服务响应写入成功
  • +
  • 商品服务请求从数据库获取商品信息
  • +
+

C-Consistency: +一致性是指写操作后的读操作可以读取到最新的数据状态,当数据分布在多个结点上,从任意结点读取到的数据都是最新状态。

+

上图中,商品的信息的读写要满足一致性就是要实现如下目标:

+
    +
  • 商品服务写入主数据库成功,则向从数据查询新数据也成功
  • +
  • 商品服务写入主数据库失败,则向从数据查询新数据也失败
  • +
+

这里一致性指的就是主从数据库数据的一致性。

+

如何实现一致性:

+
    +
  • 写入主数据库后要将数据同步到从数据库
  • +
  • 写入主数据库后,在向从数据库同步期间要将从数据库数据锁定,待同步完成后在释放锁,以免在新数据写入成功之后,向从数据库查询到旧数据库的数据
  • +
+

分布式系统一致性的特点:

+
    +
  • 由于存在数据库同步的过程中,写操作的响应会有一定的延迟
  • +
  • 为了保证数据一致性会对资源暂时锁定,待数据同步完成后释放锁定的资源
  • +
+

A-Availability: +可用性是指任何事务操作都可以获得响应结果,且不会出现响应超时或响应错误。

+

上图中,商品信息的读取满足可用性就是要实现如下目标:

+
    +
  • 从数据库接收到数据查询的请求则立即能够响应数据的查询结果
  • +
  • 从数据库不允许出现响应超时或响应错误的情况
  • +
+

如何实现可用性:

+
    +
  • 写入主数据库后要将数据数据同步到从数据库
  • +
  • 由于要保证从数据库的可用性,不可将从数据库中的资源进行锁定
  • +
  • 即使数据还没有从主数据库同步过来,从数据库也要返回查询的数据,哪怕是旧数据,如果连旧数据也没有则可以按照约定返回一个默认信息,但不能返回错误或超时
  • +
+

分布式系统可用性的特点:

+
    +
  • 所有请求都要有响应,且不会出现响应超时或响应错误
  • +
+

P-Partition tolerance: +分布式系统的各个结点部署在不同的子网,这就是网络分区,不可避免的会出现由于网络问题而导致结点之间的通信失败,此时仍可以对外提供服务,这就是分区容忍性。

+

上图中,商品信息的读写满足分区容忍性就是要实现如下目标:

+
    +
  • 主数据库向从数据库同步数据失败,不影响读写操作
  • +
  • 其中一个结点挂掉不影响另外一个结点提供服务
  • +
+

如何实现分区容忍性:

+
    +
  • 尽量使用异步取代同步操作,例如使用异步的方法将数据从数据库同步到从数据库,这样的结点之间才能有效的解耦合
  • +
  • 添加从数据库结点,其中一个从数据结点挂掉其他从结点来提供服务
  • +
+

分布式分区容忍性特点:

+
    +
  • 分区容忍性是分布式系统具备的基本的能力
  • +
+

CAP组合方式

+

在所有分布式事务场景中不会同时具备CAP三个特征,因为在具备了P的前提下C和A是不能共存的。

+

例如,下图满足了P分区容忍性

+

分布式事务-001

+

上图分区容忍性含义:

+
    +
  • 主数据库通过网络向从数据库同步数据,可以认为是主从数据库部署在不同的网络分区,通过网络进行交互
  • +
  • 当主数据库和从数据库之间网络出现问题,不影响主数据库和从数据库对外提供服务
  • +
  • 其中一台结点挂掉后,不影响另外一个结点对外提供服务
  • +
+

在分区容忍性存在的前提下,一致性和可用性存在矛盾:

+
    +
  • 如果要实现 (C)一致性 ,则必须保证数据一致性,在数据同步的时候防止向从数据库查询不一致的数据则需要将从数据库数据锁定,待同步完成后解锁,如果同步失败,从数据库必须要返回错误信息或者超时信息。
  • +
  • 如果要实现 (A)可用性 则必须保证数据可用性,即不管任何时候都可以向从数据库查询数据,且不会响应超时信息或错误信息。
  • +
+

在分布式的环境下,一致性和可用性只能存在一种,即AP、CP。

+
    +
  • AP:放弃一致性,追求分区容忍性和可用性。这是很多分布式系统设计时的选择。
  • +
  • CP:放弃可用性,追求一致性和分区容忍性。zookeeper就是一个追求强一致性的例子;例如,跨行转账,一次转账请求需要等待双方银行都完成整个事务才算完成。
  • +
+

如果不在分布式的环境下,一致性和可用性其实是不矛盾的:

+
    +
  • CA:放弃分区容忍性,即不进行分区,不考虑网络结点不通畅或者结点挂掉的情况,则可以实现一致性和可用性。那么系统将不是一个标准的分布式系统,最常用的关系型数据库就满足了CA。
  • +
+

CAP是一个已经被证实的理论:一个分布式系统最多只能同时满足一致性、可用性、分区容忍性中的两种。它可以作为我们进行架构设计、技术选型的考量标准。对于大型互联网应用场景来说,结点众多、部署分散,而且现在的集群规模越来越大,所以结点故障、网络故障是常态,而且要保证服务可用性达到N个9(99.99..%),并要达到良好的响应性能来提高用户的体验,因此一般都会做出以下选择:保证可用性和分区容忍性即AP,舍弃C,强一致性,保证最终一致性。

+

BASE理论

+
+

理解强一致性与最终一致性: +CAP理论说明一个分布式系统最多能同时满足一致性、可用性、分区容忍性这三项中的两项;其中AP在实际应用中比较多,AP舍弃一致性,保证可用性和分区容忍性,但是在实际生产中很多场景都要实现一致性,比如前边的例子,主数据库向从数据库同步数据,即使不要一致性,但是最终也要将数据同步成功来保证数据一致,这种一致性和CAP中的一致性不同,CAP中的一致性要求在任何时间查询每个结点数据都必须保证一致,它强调的是一致性; +但是最终一致性是允许可以在一段时间内每个结点数据不一致,但是经过一段时间后每个结点的数据必须一致,它强调的是数据的最终一致性。

+
+

BASE是Basically Available(基本可用)、Soft state(软状态)、Eventually consistent(最终一致性) 这三个短语的缩写。 +BASE理论是对于CAP中AP的一个扩展,通过牺牲强一致性来获得可用性,当出现故障允许的部分不可用但要保证核心功能可用,允许数据在一段时间内是不一致的,但最终达到一致的状态。满足BASE理论的事务,我们称之为柔性事务。

+
    +
  • 基本可用:分布式系统出现故障时,允许损失部分可用的功能,保证核心功能可用。例如,电商网站交易付款出现问题,商品依然可以正常浏览。
  • +
  • 软状态:由于不要求强一致性,所以BASE允许系统中存在中间状态(软状态),这个状态不影响系统的可用性。例如,订单的"支付中、数据同步中"等状态,待数据最终一致后状态改为"成功"状态。
  • +
  • 最终一致性:最终一致是指经过一段时间后,所有的结点数据将会达到一致。例如,订单的"支付中"状态,最终会变为"支付成功"或"支付失败”,使订单状态与交易结果达成一致,但需要一定的时间等待。
  • +
+

分布式事务解决方案-2PC

+

针对不同的分布式场景常见的解决方案有2PC、TCC、可靠消息最终一致性、最大努力通知这几种。

+

什么是2PC

+

2PC即两阶段提交协议,是将整个事务流程分为两个阶段:准备阶段(Prepare)、提交阶段(Commit),2是指两个阶段,P是准备阶段,C是提交阶段。

+

举例,张三,李四聚餐,饭店老板要求先买单才能出票。张三李四都不愿请客,只能AA。只有张三和李四都付款,老板才能出票安排就餐。

+
    +
  • 准备阶段:老板要求张三付款,张三付款;老板要求李四付款,李四付款。
  • +
  • 提交阶段:老板出票,两人拿票纷纷入座就餐。
  • +
+

例子中形成了一个事务,若张三或李四其中一人拒绝付款,或者钱不够,老板都不会出票,并且把已收的钱退回。

+

整个事务过程由事务管理器和参与者组成,老板就是事务管理器,张三、李四就是事务参与者,事务管理器负责抉择整个分布式事务的提交和回滚,事务参与者负责自己的本地事务提交和回滚。

+

在计算机中部分关系型数据库如oracle、mysql支持两阶段提交协议:

+
    +
  1. 准备阶段:事务管理器给每个参与者发送Prepare消息,每个数据库参与者执行本地事务,并写本地的Undo/Redo日志,此时事务还没有提交。
  2. +
+
+

Undo日志是记录修改前的数据,用于数据库回滚;Redo日志是记录修改后的数据,用于提交事务后写入数据文件。

+
+
    +
  1. 提交阶段:如果事务管理器收到了参与者的执行失败或超时的消息,那么直接给每个参与者发送回滚消息;否则,发送提交消息。参与者根据事务管理器的指令执行提交或回滚的操作,并释放事务处理过程中使用的锁资源。
  2. +
+
+

注意:必须在最后释放锁资源

+
+

成功情况: +分布式事务-002

+

失败情况: +分布式事务-003

+

解决方案

+

2PC具体解决方案.

+

XA方案

+

2PC的传统方案是在数据库层面实现的,如:oracle、mysql都支持2PC协议,为了统一标准减少行业内不必要的对接成本,需要制定标准化的处理模型即接口标准,国际开放标准组织OpenGroup定义了分布式处理模型DTP(Distributed Transaction Processing Reference Model)。

+

DTP模型定义如下角色:

+
    +
  • AP:Application Program即应用程序,可以理解为使用分布式事务的程序。
  • +
  • RM:Resource Manager即资源管理器,可理解为事务的参与者,一般情况是指一个数据库实例,通过资源管理器对该数据库进行控制,资源管理器控制这分支事务。
  • +
  • TM:Transacition Manager即事务管理器,负责协调事务和事务管理,它控制着全局的事务,管理事务的生命周期,并协调各个资源管理器。
  • +
+
+

全局事务是指分布式处理事务环境中,需要操作多个数据库共同完成一个动作,这个工作即是一个全局事务。

+
+

以新用户注册送积分为例: +分布式事务-004

+

执行流程如下:

+
    +
  1. +

    应用程序持有数据库和积分库两个数据源。

    +
  2. +
  3. +

    应用程序通过事务管理器通知用户库的资源管理器新增用户,同时也通知积分库的资源管理器为该用户增加积分,资源管理器此时并未提交事务,此时用户和积分资源锁定。

    +
  4. +
  5. +

    事务管理器收到执行回复,只要有一方失败则分别向其他方发起事务回滚,回滚完毕,资源释放锁;或事务管理器收到执行回复,全部成功,此时向所有资源管理器发起提交事务,提交完毕,资源释放。

    +
  6. +
+

DTP模型定义TM和RM之间通讯的接口规范叫XA,简单理解为数据库提供的2PC接口协议,基于数据库的XA协议来实现2PC又称为XA方案。

+

以上三个角色之间的交互方式如下:

+
    +
  • TM向AP提供程序编程接口,AP通过TM提交或回滚事务
  • +
  • TM交易中间件通过XA接口来通知RM数据库事务的开始、结束以及提交、回滚等
  • +
+

整个2PC的事务管理流程涉及到三个角色:AP、RM、TM。AP指的是使用2PC分布式事务的应用程序;RM指的是资源管理器,它控制这分支事务;TM指的是事务管理器,它控制着全局事务。

+

在准备阶段RM执行实际的业务操作,但是不提交事务,资源锁定;在提交阶段TM会接受RM在准备阶段的执行回复,只要任何一个RM执行失败,TM会通知所有的RM进行回滚操作,否则TM将会通知RM提交事务。提交阶段结束释放资源。

+

XA方案存在的问题:

+
    +
  • 需要本地数据库支持XA协议
  • +
  • 资源锁需要等到准备阶段和提交阶段结束才释放,性能较差
  • +
+

Seata方案

+

Seata是由阿里中间件团队发起的开源项目Fescar,后更名为Seata,它是一个开源的分布式事务框架。

+

传统2PC的问题在Seata中的到了解决,它通过对本地关系型数据库的分支事务的协调来驱动完成全局事务,是工作在应用层的中间件。主要优点是性能较好,且不长时间占用连接资源,它以高效并对业务0入侵的方式解决微服务场景下面临的分布式事务问题,目前提供AT模式(即2PC)和TCC模式的分布式事务解决方案。

+

Seata的设计思想:

+

Seata的设计目标其一是对业务零侵入,因此从业务无侵入的2PC入手,在传统方案2PC的基础上演进,并解决2PC方案面临的问题。

+

Seata把一个分布式的事务理解为一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交事务,要么一起回滚失败。此外,通常分支事务本身就是一个关系型数据库的本地事务。

+

分布式事务-005

+

与传统2PC的模型类似,Seata定义了3个组件来协调分布式事务的处理过程:

+

分布式事务-006

+
    +
  • TM:Transaction Manager事务管理器,需要嵌入(jar包)应用程序中工作,负责开启一个全局事务,并最终向TC发起全局提交或全局回滚的指令。
  • +
  • RM:Resource Manager资源管理器,控制分支事务,负责分支事务注册,状态回报,并接收事务调节器的指令,驱动分支事务的提交和回滚。
  • +
  • TC:Transaction Coordinator事务协调器,它是独立的中间件,需要独立运行部署,它维护全局事务的运行状态,接收TM指令发起全局事务的提交与回滚,负责与RM通信协调各个分支事务的提交回滚。
  • +
+

以新用户注册送积分举例: +分布式事务-007

+

具体执行流程:

+
    +
  1. 用户服务器的事务管理器向事务协调器申请开启一个全局事务,全局事务的创建成功并生成一个全局唯一XID。
  2. +
  3. 用户服务器的资源管理器向事务协调器注册分支事务,该分支事务在用户服务执行新增用户逻辑,并将其纳入XID对应全局事务的管辖。
  4. +
+
+

注意:在执行注册分支事务的时候,这里事务已经提交了,提交后可以释放资源,从而提升程序性能。

+
+
    +
  1. 用户服务器执行分支事务,向用户表插入一条记录。
  2. +
  3. 逻辑执行到远程分布式调用积分服务时,XID在微服务调用链路的上下文中传播。积分服务的资源管理器向事务协调器注册了分支事务,该分支事务执行增加积分的逻辑,并将其纳入XID对应的全局事务的管辖。
  4. +
  5. 积分服务执行分支事务,向积分记录表插入一条记录,执行完毕后,返回用户服务。
  6. +
  7. 用户服务分支事务执行完毕。
  8. +
  9. 事务管理器向事务协调器针对XID的全局提交或者回滚决策。
  10. +
  11. 事务协调器调度XID下管辖的全部分支事务完成提交或回滚的请求。
  12. +
+

Seata实现2PC与传统2PC的差别:

+
    +
  • +

    架构方面,传统2PC方案的RM实际上是在数据库层,RM本质上就是数据库自身,通过XA协议实现,而Seata的RM则是以jar包的形式作为中间件层部署在应用程序这一侧的。

    +
  • +
  • +

    两阶段提交方面,传统2PC无论第二阶段的决策是commit还是rollback,事务性资源的锁都要保持到第二阶段才释放。而Seata的做法是在第一阶段就将本地事务提交,这样可以省去第二阶段持有锁的时间,提高整体效率。

    +
  • +
+

Seata原理简介:

+
+

两阶段提交协议的演变:

+
    +
  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
  • +
  • 二阶段: +
      +
    • 提交异步化,非常快速地完成。
    • +
    • 回滚通过一阶段的回滚日志进行反向补偿。
    • +
    +
  • +
+
+

一阶段:seata会拦截,解析SQL语义,找到SQL要更新的数据,在业务员数据更新前,将其保存为before image,随后执行业务SQL,在业务数据更新后,将其保存为after image,插入UNDO LOG回滚日志;提交前,向 TC 注册分支生成行锁。 +随后本地事务提交,将本地事务提交的结果上报给 TC,由TC协调。

+

分布式事务-008

+

二阶段-提交:执行提交操作,说明SQL执行顺利,因业务SQL在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可。

+

分布式事务-009

+

二阶段-回滚:要执行回滚操作,说明SQL执行不顺利,Seata需要回滚一阶段已经执行的业务SQL还原数据。回滚方式是用before image还原数据,但是在还原前还要校验脏写,对比数据库当前业务数据和after image。 +对比数据库当前业务数据和after image。

+

分布式事务-010

+

分布式事务解决方案-TCC

+

针对不同的分布式场景常见的解决方案有2PC、TCC、可靠消息最终一致性、最大努力通知这几种。

+

什么是TCC

+

TCC是Try、Confirm、Cancel三个词语的缩写,TCC要求每个分支事务实现三个操作:预处理Try、确认Confirm、撤销Cancel。 +Try操作做业务检查及资源预留,Confirm做业务确认操作,Cancel实现一个与Try相反的操作,即回滚操作。TM首先发起所有分支事务的try操作,任何一个分支事务的try操作执行失败,TM将会发起所有分支事务的Cancel操作,若try操作全部成功,TM将发起所有分支事务的Confirm操作,其中Confirm或Cancel操作失败,TM会重试。

+

分布式事务-011

+

分布式事务执行失败情况:

+

分布式事务-012

+

TCC的三个阶段:

+
    +
  • Try阶段是做业务检查一致性及资源预留,此阶段仅是一个初步操作,它和后续的Confirm一起才能真正的构成一个完整的业务逻辑;
  • +
  • Confirm阶段是确认提交,Try阶段所有的分支事务执行成功后开始执行Confirm。通常情况下,采用TCC则认为Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。若Confirm阶段真的出错了,需要引入重试机制或进行人工处理;
  • +
  • Cancel阶段是在业务执行错误需要回滚的状态下,执行分支事务的业务取消了,预留资源释放。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。如果Cancel阶段真的出错了,需要引入重试机制或进行人工处理;
  • +
+

TM事务管理器:

+
    +
  • TM事务管理器可以实现为独立的服务,也可以让全局事务发起方充当TM的角色,TM独立出来是为了成为公用的组件,为了考虑系统结构和软件复用。
  • +
+

TM发起全局事务时生成全局事务记录,全局事务ID贯穿整个分布式事务调用链,用来记录事务上下文,追踪和记录状态,由于Confirm和Cancel失败需要进行重试或人工处理,因此实现为幂等,幂等性是指同一个操作无论请求多少次,其结果都相同。

+

解决方案

+

Hmily是一个高性能分布式事务TCC开源框架。基于Java语言来开发(JDK1.8),支持Dubbo,Spring Cloud等RPC框架进行分布式事务。 +目前支持以下特性:

+
    +
  • 支持嵌套事务(Nested transaction support);
  • +
  • 采用disruptor框架进行事务日志的异步读写,与RPC框架的性能毫无差别;
  • +
  • 支持SpringBoot-starter 项目启动,使用简单;
  • +
  • RPC框架支持 : dubbo,motan,springcloud;
  • +
  • 本地事务存储支持 : redis,mongodb,zookeeper,file,mysql;
  • +
  • 事务日志序列化支持:java,hessian,kryo,protostuff;
  • +
  • 采用Aspect AOP 切面思想与Spring无缝集成,天然支持集群;
  • +
  • RPC事务恢复,超时异常恢复等;
  • +
+

Hmily利用AOP对参与分布式事务的本地方法与远程方法进行拦截处理,通过多方拦截,事务参与者能透明的调用到另一方的Try、Confirm、Cancel方法; +传递事务上下文;并记录事务日志,酌情进行补偿,重试等。

+

Hmily是一个轻量级的TCC事务框架不需要部署独立的事务协调服务,但需要提供一个数据库来进行日志存储。

+

Hmily实现的TCC服务与普通的服务一样,只需要暴露一个接口,也就是它的Try业务。Confirm/Cancel业务逻辑,只是因为全局事务提交/回滚的需要才提供的,因此Confirm/Cancel业务只需要被Hmily TCC事务框架发现即可,不需要被调用它的其他业务服务所感知。

+
+

要实现TCC协议,必须要实现三个方法:Try、Confirm、Cancel,其中最关键的方法是Try方法,Try方法是一个事务的起点。三个方法会由不同的线程来分别调用。

+
+

TCC需要注意三种异常处理:空回滚、幂等、悬挂:

+
    +
  1. +

    空回滚:在没有调用TCC资源Try方法的情况下,调用了二阶段的Cancel方法,Cancel方法需要识别出这是一个空回滚,然后直接返回成功。 +
    +出现原因是当一个分支事务所在的服务器宕机或网络异常,分支事务调用记录为失败,这个时候其实是没有执行Try阶段,当故障恢复后,分布式事务进行回滚则会调用二阶段的Cancel方法,从而形成空回滚。 +
    +解决思路关键就是要标识这个空回滚,就是要知道try阶段是否已经执行了,如果执行了那就是正常回滚;如果没执行,那就是空回滚。可以在try阶段执行完毕后向表里插入一条记录进行标识,如果记录存在则try阶段执行了,如果不存在try阶段则未执行。

    +
  2. +
  3. +

    幂等:为了保证TCC二阶段提交重试机制不会引发数据不一致,要求TCC二阶段Try、Confirm、Cancel接口保证幂等性,这样不会重复使用或示释放资源。如果幂等控制没有做好,很有可能导致数据不一致等严重问题。

    +
  4. +
  5. +

    悬挂:悬挂就是对于一个分布式事务,其二阶段Cancel接口对比Try接口先执行。

    +
  6. +
+

出现原因是在RPC调用分支事务时,先注册分支事务,在执行RPC调用,如果此时RPC调用的网络发生错误,RPC超时后,TM就会通知RM回滚该分布式事务,可能回滚完成后,RPC请求才到达,然后执行,而一个try方法预留的业务资源,只有该分布式事务才能使用,该分布式事务第一阶段预留的业务资源也就在也没有人能够处理,对于这种情况就称为悬挂,即业务资源预留后没办法处理。

+

分布式事务解决方案-可靠消息最终一致性

+

针对不同的分布式场景常见的解决方案有2PC、TCC、可靠消息最终一致性、最大努力通知这几种。

+

什么是可靠消息最终一致性

+

可靠消息最终一致性是指当事务发起方执行完本地事务后并发出一条消息,事务参与方(消息消费者)一定能接收到消息并成功处理事务,此方案强调的是只要消息发给事务参与方最终事务要达到一致。

+

此方案利用消息中间件完成,事务发起方将消息发送给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间,事务参与方和消息中间件之间都是通过网络进行通信,由于网络通信的不稳定会导致分布式事务问题。

+

分布式事务-013

+

因此可靠消息最终一致性方案要解决几个问题:

+
    +
  • 本地事务与消息发送的原子性 即:事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键。
  • +
  • 事务参与方接收消息的可靠性 事务参与方必须能够从消息队列接收消息,如果接收消息失败可以重复接收消息。
  • +
  • 消息重复消费问题 由于网络存在问题,若某一个消费结点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息重复消费问题。要解决消息重复消费问题就要实现事务参与方的方法的幂等性。
  • +
+

解决方案

+

本地消息表方案

+

本地消息表该方案最初是eBay提出,此方案的核心是通过本地事务保证数据业务操作和消息的一致性,然后通过定时任务将消息发送至消息中间件,待确认消息发送给消费方成功后在将消息删除。

+

以用户注册送积分举例:用户服务负责添加用户,积分服务负责添加积分

+

分布式事务-014

+

执行流程:

+
    +
  • 用户注册,用户服务在本地新增用户和增加积分消息日志。用户表和消息表本地事务保持一致,这种情况下本地数据库操作与存储积分消息日志处于同一个事务中,本地数据库操作记录与记录消息日志操作具备原子性。
  • +
+

如何保证将消息发送给消息队列?

+
    +
  • 当消息已经写入到消息日志中去后,可以启动独立的线程,定时对消息日志表中的消息进行扫描并发送至消息中间件,在消息中间件反馈发送成功后删除该消息日志,否则等待定时任务下一周期重试。
  • +
+

如何保证消费者一定能消费到消息?

+
    +
  • 可以使用MQ的ACK机制即消息确认机制,消费这监听MQ,如果消费者接收到消息并且业务处理完成后想MQ发送ACK,此时说明消费者正常消费消息完成,MQ将不再向消费者推送消息,否则生产者会不断重试向消费者发送消息。
  • +
+

由于消息会重复投递,积分服务的增加积分操作功能要实现幂等性。

+

RocketMQ事务消息方案

+

RocketMQ是阿里巴巴的分布式消息中间件,于2012年开源并在2017年正式称为apache的项目。ApacheRocketMQ4.3之后的版本正式支持事务消息,并为分布式事务提供了便利性支持。

+

RocketMQ事务消息设计则主要是为了解决生产者端的消息发送与本地事务执行的原子性问题,RockerMQ设计中broker与生产者端的双向通信能力,使得broker天生可以作为一个事务协调者存在;RockerMQ的高可用机制以及可靠消息设计原则则为事务消息在系统发生异常时依然能够保证达成事务的最终一致性。

+

分布式事务-015

+

执行流程:

+
    +
  1. MQ的发起方会向MQ发送一条消息,MQ发起方会监听MQ是否响应;
  2. +
  3. MQ收到该消息会响应消息发起方,代表发送给MQ成功;但是此时MQ并未发送消息给消费者;
  4. +
  5. MQ发起方开始执行本地事务,执行完本地事务后,给MQ发送commit消息,此时MQ将该消息更改为可消费状态,消费者可以消费该消息;如果消费者执行本地事务失败,给MQ发送rollback消息,MQ会将该消息丢弃;
  6. +
  7. 消费者方利用MQ的ACK机制来保证消息一定被消费到;
  8. +
+

注意:发起方与MQ之间的网络出现问题,但此时MQ里的消息不会一直存着;MQ服务会定回查消费者方本地事务状态(实现MQ事务回查接口),如果事务已经提交了消费者仍然可以接收到该消息,如果没有提交MQ则丢弃该消息。

+

分布式事务解决方案-最大努力通知

+

针对不同的分布式场景常见的解决方案有2PC、TCC、可靠消息最终一致性、最大努力通知这几种。

+

什么是最大努力通知

+

顾名思义,发起通知方通过一定机制最大努力的将业务处理结果通知到接收方。

+

举个例子:

+

分布式事务-016

+

账户系统调用充值系统接口,充值系统完成处理后向账户账户接口发起充值结果通知,如果通知失败则充值充值系统则按策略进行重复通知;账户系统接收到充值系统的通知后修改充值状态。如果账户系统未接受到通知,账户系统会主动调用充值系统接口进行结果查询。

+

最大努力通知特点:

+
    +
  • 有一定的消息重复通知机制。因接收方可能没有收到通知,此时要有一定的机制对消息重复通知。
  • +
  • 消息校对机制。如果发起方尽最大努力也没有通知到接收方,或接收方消费消息后要再次消费,此时可由接收方主动向通知方查询处理结果来满足需求。
  • +
+

最大努力通知与可靠消息一致性不同点

+
    +
  • 解决方案思想不同: +
      +
    • 可靠消息一致性,发起通知方需要保证将消息发送出去,并且将消息发送到接收方,消息的可靠性关键由发起方来保证。
    • +
    • 最大努力通知,发起通知尽最大努力的将业务处理结果通知接收方,但是消息可能接收不到,此时需要接收通知方主动调用发起方的查询业务处理结果接口,通知的可靠性关键在接收方。
    • +
    +
  • +
  • 业务场景不同: +
      +
    • 可靠消息一致性关注的是交易过程事务的一致性,以异步的方式完成交易。
    • +
    • 最大努力通知关注的是交易后的通知事务,即将交易结果可靠的通知出去。
    • +
    +
  • +
  • 技术解决方向不同: +
      +
    • 可靠消息一致性要解决消息从发出到接收的一致性,即发出消息并且被接收到。
    • +
    • 最大努力通知无法保证发出到接收的一致性,只提供消息接收的可靠性机制。消息接收可靠机制指,最大努力的将通知发送给接收方,当消息无法被接收方接收时,由接收方主动查询该消息。
    • +
    +
  • +
+

解决方案

+

通过对最大努力通知的理解,采用MQ的ack机制可以实现最大努力通知。

+

方案1

+

分布式事务-017

+

执行流程:

+
    +
  • 发起通知方将通知发送给MQ;
  • +
  • 接收方监听MQ;
  • +
  • 接收通知方接收消息,业务处理完成回应ack,接收方若没有回应ack则MQ重复通知;
  • +
  • 如果消息没有发送出去可由接收方主动请求发起方查询业务结果接口,通过该接口校对消息的一致性;
  • +
+

方案1中接收通知方MQ接口,即接收通知方监听MQ,此方案主要应用于应用与内部应用之间的通知。

+

方案2

+

分布式事务-018

+

交互流程:

+
    +
  • 发起通知方将消息通知给MQ。使用可靠消息一致方案中的事务消息保证本地事务与消息的原子性,最终将通知先发给MQ;
  • +
  • 通知程序监听MQ,接收MQ消息。通知程序如果没有回应ack则MQ会重复通知;
  • +
  • 通知程序通过互联网接口协议调用接收通知方接口,完成通知。通知程序调用接收方接口成功就表示通知成功,即MQ消费成功,MQ将不再向通知程序投递消息;
  • +
  • 接收通知方可通过消息校对接口来校对消息的一致性;
  • +
+

方案2中由于通知程序与MQ接口,通知程序监听MQ,收到MQ消息后由通知程序通过互联网接口协议调用接收通知方。此方案主要应用于外部应用之间的通知。

+

分布式解决方案对比

+

2PC最大的诟病是一个阻塞协议。RM在执行分支事务后需要等待MT决定,此时服务会堵塞并锁定资源,由于其堵塞机制和最差时间复杂度较高,因此这种设计不能适应随着事务涉及的服务数量增多而扩展的需要,很难用于并发较高以及事务生命周期较长的分布式服务中。

+

如果拿TCC的事务处理流程与2PC做比较,2PC通常都是在跨库的DB层面,而TCC则在应用层面的处理,需要通过业务逻辑来实现。这种分布式事务的实现方式优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量。不足之处是对应用的侵入性非常强,业务逻辑的每个分支都要实现try、confirm、cancel三个操作;此外实现难度也比较大,需要按照网络状态、系统故障的等不同失败原因进行不同回滚策略。

+

可靠消息最终一致性适合执行周期长且实时性要求不高的业务场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免了分布式事务中同步堵塞操作的影响,并实现了两个服务之间的解耦。

+

最大努力通知是分布式事务中要求最低的一种,适用于一些最终一致性时间敏感度低的业务;允许发起通知方处理失败,在接收通知方收到失败通知后积极进行失败处理,无论发起通知方如何处理结果都不会影响到接收方的后续处理;发起通知方需要提供查询执行情况的接口,用于接收通知方校对结果。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
2PCTCC可靠消息最终一致性最大努力通知
一致性强一致性最终一致性最终一致性最终一致性
吞吐量
实现复杂度
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/net-program-java/index.html b/blog-site/public/posts/essays/net-program-java/index.html new file mode 100644 index 00000000..5b8adc17 --- /dev/null +++ b/blog-site/public/posts/essays/net-program-java/index.html @@ -0,0 +1,667 @@ + + + + + + + + + + + 网络编程 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

网络编程

+ 2021.11.19 +
+

网络协议

+

以下内容摘自百度百科:

+ +

网络协议指的是计算机网络中互相通信的对等实体之间交换信息时所必须遵守的规则的集合。

+

为了使不同计算机厂家生产的计算机能够相互通信,以便在更大的范围内建立计算机网络,国际标准化组织(ISO)在1978年提出了“开放系统互联参考模型”,即著名的OSI/RM模型(Open System Interconnection/Reference Model)。

+

网络协议分层

+

国际标准化组织ISO 于1981年正式推荐了一个网络系统结构—-七层参考模型,叫做开放系统互连模型(Open System Interconnection,OSI)。由于这个标准模型的建立,使得各种计算机网络向它靠拢,大大推动了网络通信的发展。

+

它将计算机网络体系结构的通信协议划分为七层,自下而上依次为:

+
    +
  • +

    物理层(Physics Layer):物理层是OSI的第一层,它虽然处于最底层,却是整个开放系统的基础。物理层为设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的环境。以太网 · 调制解调器 · 电力线通信(PLC) · SONET/SDH · G.709 · 光导纤维 · 同轴电缆 · 双绞线等属于物理层;

    +
  • +
  • +

    数据链路层(Data Link Layer):数据链路可以粗略地理解为数据通道。物理层要为终端设备间的数据通信提供传输媒体及其连接.媒体是长期的,连接是有生存期的.在连接生存期内,收发两端可以进行不等的一次或多次数据通信.每次通信都要经过建立通信联络和拆除通信联络两过程.这种建立起来的数据收发关系就叫作数据链路。Wi-Fi(IEEE 802.11) · WiMAX(IEEE 802.16) ·ATM · DTM · 令牌环 · 以太网 ·FDDI · 帧中继 · GPRS · EVDO ·HSPA · HDLC · PPP · L2TP ·PPTP · ISDN·STP · CSMA/CD等;

    +
  • +
  • +

    网络层(Network Layer):这层对端到端的包传输进行定义,它定义了能够标识所有结点的逻辑地址,还定义了路由实现的方式和学习的方式。为了适应最大传输单元长度小于包长度的传输介质,网络层还定义了如何将一个包分解成更小的包的分段方法。IP (IPv4 · IPv6) · ICMP· ICMPv6·IGMP ·IS-IS · IPsec · ARP · RARP · RIP等属于网络层;

    +
  • +
  • +

    传输层(Transport Layer):TCP · UDP · TLS · DCCP · SCTP · RSVP · OSPF 等;

    +
  • +
  • +

    会话层(Session Layer):它定义了如何开始、控制和结束一个会话,包括对多个双向消息的控制和管理,以便在只完成连续消息的一部分时可以通知应用,从而使表示层看到的数据是连续的,在某些情况下,如果表示层收到了所有的数据,则用数据代表表示层。示例:RPC,SQL等;

    +
  • +
  • +

    表示层(Presentation Layer):这一层的主要功能是定义数据格式及加密。例如,FTP允许你选择以二进制或ASCII格式传输。如果选择二进制,那么发送方和接收方不改变文件的内容。如果选择ASCII格式,发送方将把文本从发送方的字符集转换成标准的ASCII后发送数据。在接收方将标准的ASCII转换成接收方计算机的字符集。示例:加密,ASCII等;

    +
  • +
  • +

    应用层(Application Layer):与其它计算机进行通讯的一个应用,它是对应应用程序的通信服务的。DHCP ·DNS · FTP · Gopher · HTTP· IMAP4 · IRC · NNTP · XMPP ·POP3 · SIP · SMTP ·SNMP · SSH ·TELNET · RPC · RTCP · RTP ·RTSP· SDP · SOAP · GTP · STUN · NTP· SSDP · BGP 等属于应用层协议;

    +
  • +
+

网络协议有很多种,具体选择哪一种协议则要看情况而定。Internet上的计算机使用的是TCP/IP协议。

+

TCP/IP协议

+

TCP/IP 协议是一个协议簇,包括很多协议。命名为 TCP/IP 协议的原因是 TCP 和 IP 这两个协议非常重要,应用很广。

+

TCP/IP是因特网的正式网络协议,是一组在许多独立主机系统之间提供互联功能的协议,规范因特网上所有计算机互联时的传输、解释、执行、互操作,解决计算机系统的互联、互通、操作性,是被公认的网络通信协议的国际工业标准。TCP/IP是分组交换协议,信息被分成多个分组在网上传输,到达接收方后再把这些分组重新组合成原来的信息。除TCP/IP外,常用的网络协议还有PPP、SLIP等。

+
+

TCP/IP(Transport Control Protocol/Internet Protocol,传输控制协议/Internet协议)的历史应当追溯到Internet的前身—ARPAnet时代。为了实现不同网络之间的互连,美国国防部于1977年到1979年间制定了TCP/IP体系结构和协议。TCP/IP是由一组具有专业用途的多个子协议组合而成的,这些子协议包括TCP、IP、UDP、ARP、ICMP等。TCP/IP凭借其实现成本低、在多平台间通信安全可靠以及可路由性等优势迅速发展,并成为Internet中的标准协议。在上世纪90年代,TCP/IP已经成为局域网中的首选协议,在最新的操作系统(如Windows7、Windows XP、Windows Server2003等)中已经将TCP/IP作为其默认安装的通信协议。

+
+

网络编程-001

+

TCP 和 UDP 还是HTTP都是 TCP/IP 协议簇里的一员。UDP、TCP处于 OSI 的传输层,而http协议是在tcp/ip协议模型上应用层的一种传输协议。

+

TCP

+

TCP是Transmission Control Protocol的简称,中文名是传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。

+

特点:

+
    +
  • 面向连接,即必须在双方建立可靠连接之后,才会收发数据
  • +
  • 信息包头 20 个字节
  • +
  • 建立可靠连接需要经过3次握手
  • +
  • 断开连接需要经过4次挥手
  • +
  • 需要维护连接状态
  • +
  • 报文头里面的确认序号、累计确认及超时重传机制能保证不丢包、不重复、按序到达
  • +
  • 拥有流量控制及拥塞控制的机制
  • +
+

因为TCP协议是可靠性协议,即接收方收到的数据是完整,有序,无差错的,所以建立连接需要三次握手。

+

UDP

+

UDP 是User Datagram Protocol的简称,中文名是用户数据报协议,是OSI参考模型中一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。

+

特点:

+
    +
  • 不建立可靠连接,无需维护连接状态
  • +
  • 信息包头 8 个字节
  • +
  • 接收端,UDP 把消息段放在队列中,应用程序从队列读消息
  • +
  • 不受拥挤控制算法的调节
  • +
  • 传送数据的速度受应用软件生成数据的速率、传输带宽、源端和终端主机性能的限制
  • +
  • 面向数据报,不保证接收端一定能收到
  • +
+

HTTP

+

HTTP 协议是 Hyper Text Transfer Protocol的缩写,超文本传输协议,是一个基于请求与响应,无状态的,应用层的协议,常基于TCP/IP协议传输数据,互联网上应用最为广泛的一种网络协议,所有的WWW文件都必须遵守这个标准。设计HTTP的初衷是为了提供一种发布和接收HTML页面的方法。

+

特点:

+
    +
  • 基于TCP的可靠通信
  • +
  • 基于客户端与服务端的通信
  • +
  • 无状态:是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快
  • +
  • 无连接:其含义是限制每次连接只处理一个请求。服务器处理完客户端请求,并收到客户端应答后,即断开连接,采用这种方式可以节省传输时间
  • +
+

HTTPS

+

HTTPS 是Hyper Text Transfer Protocol over SecureSocket Layer的简称,是以安全为目标的 HTTP 通道,在HTTP的基础上通过传输加密和身份认证保证了传输过程的安全性。HTTPS 在HTTP 的基础下加入SSL,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL。

+

特点:

+
    +
  • 内容加密:采用混合加密技术,中间者无法直接查看明文内容
  • +
  • 验证身份:通过证书认证客户端访问的是自己的服务器
  • +
  • 保护数据完整性:防止传输的内容被中间人冒充或者篡改
  • +
+

Socket

+

Socket 也称作"套接字",用于描述 IP 地址和端口,是一个通信链的句柄,是应用层与传输层之间的桥梁。网络应用程序位于应用层,TCP 和 UDP 属于传输层协议,在应用层和传输层之间,就可以使用 Socket 来进行连接。 +即Socket 是传输层供给应用层的编程接口。

+

粘包、拆包

+

拆包和粘包是在socket编程中经常出现的情况,在socket通讯过程中,如果通讯的一端一次性连续发送多条数据包,tcp协议会将多个数据包打包成一个tcp报文发送出去,这就是所谓的粘包。而如果通讯的一端发送的数据包超过一次tcp报文所能传输的最大值时,就会将一个数据包拆成多个最大tcp长度的tcp报文分开传输,这就叫做拆包。

+

对于粘包的情况,要对粘在一起的包进行拆包。对于拆包的情况,要对被拆开的包进行粘包,即将一个被拆开的完整应用包再组合成一个完整包。比较通用的做法就是每次发送一个应用数据包前在前面加上四个字节的包长度值,指明这个应用包的真实长度。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/nginx-start/index.html b/blog-site/public/posts/essays/nginx-start/index.html new file mode 100644 index 00000000..fc7fe13b --- /dev/null +++ b/blog-site/public/posts/essays/nginx-start/index.html @@ -0,0 +1,1020 @@ + + + + + + + + + + + Nginx介绍 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Nginx介绍

+ 2021.03.04 +
+

Nginx介绍

+
+

Nginx (“engine x”)是一个高性能的HTTP和反向代理服务器,特点是占有内存少,并发能力强,事实上Nginx的并发能力确实在同类型的网页服务器中表现较好. +Nginx专为性能优化而开发,性能是其最重要的考量,实现上非常注重效率,能经受高负载的考验,有报告表明能支持高达50000个并发连接数。

+
+

与apahche区别联系

+

Nginx 启动特别容易, 并且几乎可以做到7*24不间断运行,即使运行数个月也不需要重新启动. 你还能够不间断服务的情况下进行软件版本的升级。

+

Nginx 静态处理性能比 Apache 高 3倍以上,Apache 对 PHP 支持比较简单, Nginx 需要配合其他后端来使用 ,Apache 的组件比 Nginx 多。

+

Nginx的优势是处理静态请求,cpu内存使用率低,apache适合处理动态请求, 所以现在一般前端用nginx作为反向代理抗住压力,apache作为后端处理动态请求。

+

apache是同步多进程模型,一个连接对应一个进程;nginx是异步的,多个连接(万级别)可以对应一个进程。所以在高连接并发的情况下,Nginx是Apache服务器不错的替代品。

+

反向代理

+

nginx反向代理

+

正向代理: +在客户端(浏览器)配置代理服务器,通过代理服务器进行互联网访问. 例如:在国内想通过网络翻墙,直接访问外网,访问失败, 这个时候就要通过代理服务器,我们访问代理服务器,让代理服务器访问外网,这样就能顺利翻墙。

+

反向代理: +其实客户端对代理是无感知的,因为客户端不需要任何配置就可以访问,我们只需要将请求发送到反向代理服务器,由反向代理服务器去选择目标服务器获取数据后,在返回给客户端,此时反向代理服务器和目标服务器对外就是一个服务器,暴露的是代理服务器地址,隐藏了真实服务器IP地址。

+

负载均衡

+

由于访问量的增加,单个服务器承受不了并发,我们增加服务器的数量,然后将请求分发到各个服务器上, 将原先请求集中到单个服务器上的情况改为将请求分发到多个服务器上,将负载分发到不同的服务器,也就是我们所说的负载均衡。 +nginx负载均衡

+

动静分离

+

为了加快网站的解析速度,降低原来单个服务器的压力,可以把动态页面和静态页面由不同的服务器来解析。

+

nginx动静分离

+

Docker安装nginx

+

如未安装docker移步安装 docker

+

拉取镜像

+

搜寻 nginx 镜像

+
docker search nginx
+

拉取 nginx 镜像

+
docker pull nginx
+

查看本地已经安装的镜像,是否有我们刚拉取的 nginx 镜像

+
docker images
+

配置nginx

+

在本地创建对应 docekr 上 nginx 的配置文件和目录,方便管理。

+

~/Documents/config/nginx是我本地存放 nginx 的配置路径。路径哪里方便放哪里。

+

在上述路径下创建 conf.dwww 文件夹

+
mkdir conf.d www
+

创建 nginx 临时容器,用于拷贝所需配置文件

+
docker run --name tmp-nginx-container -d nginx
+

拷贝 nginx 配置文件

+
docker cp tmp-nginx-container:/etc/nginx/nginx.conf ~/Documents/config/nginx/nginx.conf
+

nginx存放路径

+

拷贝站点配置文件

+
docker cp tmp-nginx-container:/etc/nginx/conf.d/default.conf ~/Documents/config/nginx/conf.d/default.conf
+

删除 nginx 临时容器

+
docker rm -f tmp-nginx-container
+

创建 nginx 容器,并映射 nginx 配置文件、站点配置文件目录和网站根目录;-v 是挂载的意思,将宿主机的文件映射到容器中

+
docker run --name nginx -p 80:80 -v ~/Documents/config/nginx/nginx.conf:/etc/nginx/nginx.conf -v ~/Documents/config/nginx/conf.d:/etc/nginx/conf.d -v ~/Documents/config/nginx/www:/www -d nginx
+

测试nginx

+

拷贝 default.conftest.conf ,并修改test.confserver_name文件

+
server {
+    listen       80;
+    listen  [::]:80;
+    server_name  www.test.com;
+
+    #charset koi8-r;
+    #access_log  /var/log/nginx/host.access.log  main;
+
+    location / {
+        root   /www/test;
+        index  index.html index.htm;
+    }
+
+    #error_page  404              /404.html;
+
+    # redirect server error pages to the static page /50x.html
+    #
+    error_page   500 502 503 504  /50x.html;
+    location = /50x.html {
+        root   /usr/share/nginx/html;
+    }
+    # ...
+}
+

修改本地 /etc/hosts 文件;浏览器解析域名会先从 hosts 文件进行解析,如果没有的话,会从网络上进行解析。

+
127.0.0.1 www.test.com
+

nginx测试修改hosts文件

+

在本地 ~/Documents/config/nginx/www 下新建 test 目录,并编写一个 index.html 测试文件 +nginx本地路径

+
<!DOCTYPE html>
+<html lang="en">
+ 
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+</head>
+ 
+<body>
+    <h1>测试Nginx</h1>
+</body>
+ 
+</html>
+

重启 nginx

+
docker restart nginx
+

浏览器访问http://www.test.com +测试nginx成功

+

Nginx配置详解

+
######Nginx配置文件nginx.conf中文详解#####
+
+#定义Nginx运行的用户和用户组
+user www www;
+
+#nginx进程数,建议设置为等于CPU总核心数。
+worker_processes 8;
+
+#全局错误日志定义类型,[ debug | info | notice | warn | error | crit ]
+error_log /usr/local/nginx/logs/error.log info;
+
+#进程pid文件
+pid /usr/local/nginx/logs/nginx.pid;
+
+#指定进程可以打开的最大描述符:数目
+#工作模式与连接数上限
+#这个指令是指当一个nginx进程打开的最多文件描述符数目,理论值应该是最多打开文件数(ulimit -n)与nginx进程数相除,但是nginx分配请求并不是那么均匀,所以最好与ulimit -n 的值保持一致。
+#现在在linux 2.6内核下开启文件打开数为65535,worker_rlimit_nofile就相应应该填写65535。
+#这是因为nginx调度时分配请求到进程并不是那么的均衡,所以假如填写10240,总并发量达到3-4万时就有进程可能超过10240了,这时会返回502错误。
+worker_rlimit_nofile 65535;
+
+
+events
+{
+    #参考事件模型,use [ kqueue | rtsig | epoll | /dev/poll | select | poll ]; epoll模型
+    #是Linux 2.6以上版本内核中的高性能网络I/O模型,linux建议epoll,如果跑在FreeBSD上面,就用kqueue模型。
+    #补充说明:
+    #与apache相类,nginx针对不同的操作系统,有不同的事件模型
+    #A)标准事件模型
+    #Select、poll属于标准事件模型,如果当前系统不存在更有效的方法,nginx会选择select或poll
+    #B)高效事件模型
+    #Kqueue:使用于FreeBSD 4.1+, OpenBSD 2.9+, NetBSD 2.0 和 MacOS X.使用双处理器的MacOS X系统使用kqueue可能会造成内核崩溃。
+    #Epoll:使用于Linux内核2.6版本及以后的系统。
+    #/dev/poll:使用于Solaris 7 11/99+,HP/UX 11.22+ (eventport),IRIX 6.5.15+ 和 Tru64 UNIX 5.1A+。
+    #Eventport:使用于Solaris 10。 为了防止出现内核崩溃的问题, 有必要安装安全补丁。
+    use epoll;
+
+    #单个进程最大连接数(最大连接数=连接数*进程数)
+    #根据硬件调整,和前面工作进程配合起来用,尽量大,但是别把cpu跑到100%就行。每个进程允许的最多连接数,理论上每台nginx服务器的最大连接数为。
+    worker_connections 65535;
+
+    #keepalive超时时间。
+    keepalive_timeout 60;
+
+    #客户端请求头部的缓冲区大小。这个可以根据你的系统分页大小来设置,一般一个请求头的大小不会超过1k,不过由于一般系统分页都要大于1k,所以这里设置为分页大小。
+    #分页大小可以用命令getconf PAGESIZE 取得。
+    #[root@web001 ~]# getconf PAGESIZE
+    #4096
+    #但也有client_header_buffer_size超过4k的情况,但是client_header_buffer_size该值必须设置为“系统分页大小”的整倍数。
+    client_header_buffer_size 4k;
+
+    #这个将为打开文件指定缓存,默认是没有启用的,max指定缓存数量,建议和打开文件数一致,inactive是指经过多长时间文件没被请求后删除缓存。
+    open_file_cache max=65535 inactive=60s;
+
+    #这个是指多长时间检查一次缓存的有效信息。
+    #语法:open_file_cache_valid time 默认值:open_file_cache_valid 60 使用字段:http, server, location 这个指令指定了何时需要检查open_file_cache中缓存项目的有效信息.
+    open_file_cache_valid 80s;
+
+    #open_file_cache指令中的inactive参数时间内文件的最少使用次数,如果超过这个数字,文件描述符一直是在缓存中打开的,如上例,如果有一个文件在inactive时间内一次没被使用,它将被移除。
+    #语法:open_file_cache_min_uses number 默认值:open_file_cache_min_uses 1 使用字段:http, server, location  这个指令指定了在open_file_cache指令无效的参数中一定的时间范围内可以使用的最小文件数,如果使用更大的值,文件描述符在cache中总是打开状态.
+    open_file_cache_min_uses 1;
+
+    #语法:open_file_cache_errors on | off 默认值:open_file_cache_errors off 使用字段:http, server, location 这个指令指定是否在搜索一个文件是记录cache错误.
+    open_file_cache_errors on;
+}
+
+
+
+#设定http服务器,利用它的反向代理功能提供负载均衡支持
+http
+{
+    #文件扩展名与文件类型映射表
+    include mime.types;
+
+    #默认文件类型
+    default_type application/octet-stream;
+
+    #默认编码
+    #charset utf-8;
+
+    #服务器名字的hash表大小
+    #保存服务器名字的hash表是由指令server_names_hash_max_size 和server_names_hash_bucket_size所控制的。参数hash bucket size总是等于hash表的大小,并且是一路处理器缓存大小的倍数。在减少了在内存中的存取次数后,使在处理器中加速查找hash表键值成为可能。如果hash bucket size等于一路处理器缓存的大小,那么在查找键的时候,最坏的情况下在内存中查找的次数为2。第一次是确定存储单元的地址,第二次是在存储单元中查找键 值。因此,如果Nginx给出需要增大hash max size 或 hash bucket size的提示,那么首要的是增大前一个参数的大小.
+    server_names_hash_bucket_size 128;
+
+    #客户端请求头部的缓冲区大小。这个可以根据你的系统分页大小来设置,一般一个请求的头部大小不会超过1k,不过由于一般系统分页都要大于1k,所以这里设置为分页大小。分页大小可以用命令getconf PAGESIZE取得。
+    client_header_buffer_size 32k;
+
+    #客户请求头缓冲大小。nginx默认会用client_header_buffer_size这个buffer来读取header值,如果header过大,它会使用large_client_header_buffers来读取。
+    large_client_header_buffers 4 64k;
+
+    #设定通过nginx上传文件的大小
+    client_max_body_size 8m;
+
+    #开启高效文件传输模式,sendfile指令指定nginx是否调用sendfile函数来输出文件,对于普通应用设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置为off,以平衡磁盘与网络I/O处理速度,降低系统的负载。注意:如果图片显示不正常把这个改成off。
+    #sendfile指令指定 nginx 是否调用sendfile 函数(zero copy 方式)来输出文件,对于普通应用,必须设为on。如果用来进行下载等应用磁盘IO重负载应用,可设置为off,以平衡磁盘与网络IO处理速度,降低系统uptime。
+    sendfile on;
+
+    #开启目录列表访问,合适下载服务器,默认关闭。
+    autoindex on;
+
+    #此选项允许或禁止使用socke的TCP_CORK的选项,此选项仅在使用sendfile的时候使用
+    tcp_nopush on;
+
+    tcp_nodelay on;
+
+    #长连接超时时间,单位是秒
+    keepalive_timeout 120;
+
+    #FastCGI相关参数是为了改善网站的性能:减少资源占用,提高访问速度。下面参数看字面意思都能理解。
+    fastcgi_connect_timeout 300;
+    fastcgi_send_timeout 300;
+    fastcgi_read_timeout 300;
+    fastcgi_buffer_size 64k;
+    fastcgi_buffers 4 64k;
+    fastcgi_busy_buffers_size 128k;
+    fastcgi_temp_file_write_size 128k;
+
+    #gzip模块设置
+    gzip on; #开启gzip压缩输出
+    gzip_min_length 1k;    #最小压缩文件大小
+    gzip_buffers 4 16k;    #压缩缓冲区
+    gzip_http_version 1.0;    #压缩版本(默认1.1,前端如果是squid2.5请使用1.0)
+    gzip_comp_level 2;    #压缩等级
+    gzip_types text/plain application/x-javascript text/css application/xml;    #压缩类型,默认就已经包含textml,所以下面就不用再写了,写上去也不会有问题,但是会有一个warn。
+    gzip_vary on;
+
+    #开启限制IP连接数的时候需要使用
+    #limit_zone crawler $binary_remote_addr 10m;
+
+
+
+    #负载均衡配置
+    upstream piao.jd.com {
+
+        #upstream的负载均衡,weight是权重,可以根据机器配置定义权重。weigth参数表示权值,权值越高被分配到的几率越大。
+        server 192.168.80.121:80 weight=3;
+        server 192.168.80.122:80 weight=2;
+        server 192.168.80.123:80 weight=3;
+
+        #nginx的upstream目前支持4种方式的分配
+        #1、轮询(默认)
+        #每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
+        #2、weight
+        #指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
+        #例如:
+        #upstream bakend {
+        #    server 192.168.0.14 weight=10;
+        #    server 192.168.0.15 weight=10;
+        #}
+        #2、ip_hash
+        #每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
+        #例如:
+        #upstream bakend {
+        #    ip_hash;
+        #    server 192.168.0.14:88;
+        #    server 192.168.0.15:80;
+        #}
+        #3、fair(第三方)
+        #按后端服务器的响应时间来分配请求,响应时间短的优先分配。
+        #upstream backend {
+        #    server server1;
+        #    server server2;
+        #    fair;
+        #}
+        #4、url_hash(第三方)
+        #按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。
+        #例:在upstream中加入hash语句,server语句中不能写入weight等其他的参数,hash_method是使用的hash算法
+        #upstream backend {
+        #    server squid1:3128;
+        #    server squid2:3128;
+        #    hash $request_uri;
+        #    hash_method crc32;
+        #}
+
+        #tips:
+        #upstream bakend{#定义负载均衡设备的Ip及设备状态}{
+        #    ip_hash;
+        #    server 127.0.0.1:9090 down;
+        #    server 127.0.0.1:8080 weight=2;
+        #    server 127.0.0.1:6060;
+        #    server 127.0.0.1:7070 backup;
+        #}
+        #在需要使用负载均衡的server中增加 proxy_pass http://bakend/;
+
+        #每个设备的状态设置为:
+        #1.down表示单前的server暂时不参与负载
+        #2.weight为weight越大,负载的权重就越大。
+        #3.max_fails:允许请求失败的次数默认为1.当超过最大次数时,返回proxy_next_upstream模块定义的错误
+        #4.fail_timeout:max_fails次失败后,暂停的时间。
+        #5.backup: 其它所有的非backup机器down或者忙的时候,请求backup机器。所以这台机器压力会最轻。
+
+        #nginx支持同时设置多组的负载均衡,用来给不用的server来使用。
+        #client_body_in_file_only设置为On 可以讲client post过来的数据记录到文件中用来做debug
+        #client_body_temp_path设置记录文件的目录 可以设置最多3层目录
+        #location对URL进行匹配.可以进行重定向或者进行新的代理 负载均衡
+    }
+
+
+
+    #虚拟主机的配置
+    server
+    {
+        #监听端口
+        listen 80;
+
+        #域名可以有多个,用空格隔开
+        server_name www.jd.com jd.com;
+        index index.html index.htm index.php;
+        root /data/www/jd;
+
+        #fastcgi解析php
+        location ~ .*.(php|php5)?$
+        {
+#此处有两种方式去和php-fpm交互,一种是9000端口,另一种是使用socket连接
+            fastcgi_pass 127.0.0.1:9000;
+            fastcgi_index index.php;
+            include fastcgi.conf;
+        }
+
+        #图片缓存时间设置
+        location ~ .*.(gif|jpg|jpeg|png|bmp|swf)$
+        {
+            expires 10d;
+        }
+
+        #JS和CSS缓存时间设置
+        location ~ .*.(js|css)?$
+        {
+            expires 1h;
+        }
+
+        #日志格式设定
+        #$remote_addr与$http_x_forwarded_for用以记录客户端的ip地址;
+        #$remote_user:用来记录客户端用户名称;
+        #$time_local: 用来记录访问时间与时区;
+        #$request: 用来记录请求的url与http协议;
+        #$status: 用来记录请求状态;成功是200,
+        #$body_bytes_sent :记录发送给客户端文件主体内容大小;
+        #$http_referer:用来记录从那个页面链接访问过来的;
+        #$http_user_agent:记录客户浏览器的相关信息;
+        #通常web服务器放在反向代理的后面,这样就不能获取到客户的IP地址了,通过$remote_add拿到的IP地址是反向代理服务器的iP地址。反向代理服务器在转发请求的http头信息中,可以增加x_forwarded_for信息,用以记录原有客户端的IP地址和原来客户端的请求的服务器地址。
+        log_format access '$remote_addr - $remote_user [$time_local] "$request" '
+        '$status $body_bytes_sent "$http_referer" '
+        '"$http_user_agent" $http_x_forwarded_for';
+
+        #定义本虚拟主机的访问日志
+        access_log  /usr/local/nginx/logs/host.access.log  main;
+        access_log  /usr/local/nginx/logs/host.access.404.log  log404;
+
+        #对 "/" 启用反向代理
+        location / {
+            proxy_pass http://127.0.0.1:88;
+            proxy_redirect off;
+            proxy_set_header X-Real-IP $remote_addr;
+
+            #后端的Web服务器可以通过X-Forwarded-For获取用户真实IP
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+
+            #以下是一些反向代理的配置,可选。
+            proxy_set_header Host $host;
+
+            #允许客户端请求的最大单文件字节数
+            client_max_body_size 10m;
+
+            #缓冲区代理缓冲用户端请求的最大字节数,
+            #如果把它设置为比较大的数值,例如256k,那么,无论使用firefox还是IE浏览器,来提交任意小于256k的图片,都很正常。如果注释该指令,使用默认的client_body_buffer_size设置,也就是操作系统页面大小的两倍,8k或者16k,问题就出现了。
+            #无论使用firefox4.0还是IE8.0,提交一个比较大,200k左右的图片,都返回500 Internal Server Error错误
+            client_body_buffer_size 128k;
+
+            #表示使nginx阻止HTTP应答代码为400或者更高的应答。
+            proxy_intercept_errors on;
+
+            #后端服务器连接的超时时间_发起握手等候响应超时时间
+            #nginx跟后端服务器连接超时时间(代理连接超时)
+            proxy_connect_timeout 90;
+
+            #后端服务器数据回传时间(代理发送超时)
+            #后端服务器数据回传时间_就是在规定时间之内后端服务器必须传完所有的数据
+            proxy_send_timeout 90;
+
+            #连接成功后,后端服务器响应时间(代理接收超时)
+            #连接成功后_等候后端服务器响应时间_其实已经进入后端的排队之中等候处理(也可以说是后端服务器处理请求的时间)
+            proxy_read_timeout 90;
+
+            #设置代理服务器(nginx)保存用户头信息的缓冲区大小
+            #设置从被代理服务器读取的第一部分应答的缓冲区大小,通常情况下这部分应答中包含一个小的应答头,默认情况下这个值的大小为指令proxy_buffers中指定的一个缓冲区的大小,不过可以将其设置为更小
+            proxy_buffer_size 4k;
+
+            #proxy_buffers缓冲区,网页平均在32k以下的设置
+            #设置用于读取应答(来自被代理服务器)的缓冲区数目和大小,默认情况也为分页大小,根据操作系统的不同可能是4k或者8k
+            proxy_buffers 4 32k;
+
+            #高负荷下缓冲大小(proxy_buffers*2)
+            proxy_busy_buffers_size 64k;
+
+            #设置在写入proxy_temp_path时数据的大小,预防一个工作进程在传递文件时阻塞太长
+            #设定缓存文件夹大小,大于这个值,将从upstream服务器传
+            proxy_temp_file_write_size 64k;
+        }
+
+
+        #设定查看Nginx状态的地址
+        location /NginxStatus {
+            stub_status on;
+            access_log on;
+            auth_basic "NginxStatus";
+            auth_basic_user_file confpasswd;
+            #htpasswd文件的内容可以用apache提供的htpasswd工具来产生。
+        }
+
+        #本地动静分离反向代理配置
+        #所有jsp的页面均交由tomcat或resin处理
+        location ~ .(jsp|jspx|do)?$ {
+            proxy_set_header Host $host;
+            proxy_set_header X-Real-IP $remote_addr;
+            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+            proxy_pass http://127.0.0.1:8080;
+        }
+
+        #所有静态文件由nginx直接读取不经过tomcat或resin
+        location ~ .*.(htm|html|gif|jpg|jpeg|png|bmp|swf|ioc|rar|zip|txt|flv|mid|doc|ppt|
+        pdf|xls|mp3|wma)$
+        {
+            expires 15d; 
+        }
+
+        location ~ .*.(js|css)?$
+        {
+            expires 1h;
+        }
+    }
+}
+######Nginx配置文件nginx.conf中文详解#####
+

参考链接: https://blog.6ag.cn/2918.html

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/pay-code/index.html b/blog-site/public/posts/essays/pay-code/index.html new file mode 100644 index 00000000..45f7d322 --- /dev/null +++ b/blog-site/public/posts/essays/pay-code/index.html @@ -0,0 +1,3053 @@ + + + + + + + + + + + 整合支付功能 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

整合支付功能

+ 2023.08.10 +
+

结构

+

整合支付功能

+

pom.xml

+
<dependencies>
+    <dependency>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-web</artifactId>
+    </dependency>
+
+    <dependency>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-test</artifactId>
+        <scope>test</scope>
+    </dependency>
+
+    <dependency>
+        <groupId>org.projectlombok</groupId>
+        <artifactId>lombok</artifactId>
+        <optional>true</optional>
+    </dependency>
+
+    <dependency>
+        <groupId>cn.hutool</groupId>
+        <artifactId>hutool-all</artifactId>
+        <version>5.8.11</version>
+    </dependency>
+
+    <dependency>
+        <groupId>com.alipay.sdk</groupId>
+        <artifactId>alipay-sdk-java</artifactId>
+        <version>4.9.9</version>
+    </dependency>
+
+    <dependency>
+        <groupId>com.github.binarywang</groupId>
+        <artifactId>weixin-java-pay</artifactId>
+        <version>4.5.0</version>
+    </dependency>
+
+</dependencies>
+

application.yml

+
server:
+  port: 8080
+
+pay:
+  wechat:
+    #微信公众号或者小程序等的appid
+    appId: ""
+    #微信支付商户号
+    mchId: ""
+    #微信支付商户密钥
+    mchKey: ""
+    #服务商模式下的子商户公众账号ID
+    subAppId:
+    #服务商模式下的子商户号
+    subMchId:
+    # p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
+    keyPath:
+  alipay:
+    # 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
+    appId: ""
+    # 商户私钥,您的PKCS8格式RSA2私钥
+    merchantPrivateKey: ""
+    # 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
+    alipayPublicKey: ""
+    # 服务器异步通知页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
+    notifyUrl: "http://localhost:8080/ali/paymentNotify"
+    # 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
+    returnUrl: "http://localhost:8080/ali/paymentNotify"
+    # 签名方式
+    signType: "RSA2"
+    # 字符编码格式
+    charset: "utf-8"
+    # 格式
+    format: "json"
+    # 支付宝网关
+    gatewayUrl: "https://openapi.alipaydev.com/gateway.do"
+

公共部分

+

PayService

+
public interface PayService {
+
+
+    /**
+     * 获取支付平台类型
+     *
+     * @return 支付平台类型
+     */
+    PayPlatformTypeEnum getPayPlatformType();
+
+
+    /**
+     * 支付时可能存在的行为; 支付订单 查询订单 生成支付二维码 退款等
+     *
+     * @param orderInfo 订单信息
+     * @return any
+     */
+    <T extends CommonPayRequest> Object apply(T orderInfo);
+
+}
+

PayRefundAble

+
public interface PayRefundAble extends PayService{
+}
+

PayOrderQueryAble

+
public interface PayOrderQueryAble extends PayService{
+}
+

PayOrderCloseAble

+
public interface PayOrderCloseAble extends PayService{
+}
+

PaymentAble

+
public interface PaymentAble extends PayService{
+}
+

AbstractPayService

+
@Slf4j
+public abstract class AbstractPayService<Q extends CommonPayRequest,S> implements PayService{
+
+
+    @Override
+    @SneakyThrows
+    public Object apply(CommonPayRequest payRequest) {
+        Q orderInfo = (Q) payRequest;
+
+        // 前置处理
+        log.debug("开始执行前置处理.");
+        if (!preprocessing(orderInfo)) {
+            throw new PayException("支付前置处理失败,不能继续执行");
+        }
+        log.debug("前置处理执行完毕");
+
+        // 执行
+        log.info("开始执行,请求参数 {}", JSON.toJSONString(orderInfo));
+        S result = processing(orderInfo);
+        log.info("执行完毕,响应参数 {}", JSON.toJSONString(result));
+
+        // 后置处理
+        log.debug("开始执行后置处理.");
+        postprocessing(orderInfo);
+        log.debug("后置处理执行完毕.");
+        return result;
+    }
+
+
+
+
+    /**
+     * 前置处理
+     *
+     * @param orderInfo 订单信息
+     * @return true 通过 ; false 不通过
+     */
+    protected  boolean preprocessing(Q orderInfo){
+        return true;
+    }
+
+
+    /**
+     * 核心处理方法
+     *
+     * @param orderInfo 订单信息
+     * @return 支付结果
+     */
+    protected abstract S processing(Q orderInfo) throws PayException;
+
+
+    /**
+     * 后置处理
+     *
+     * @param orderInfo 订单信息
+     */
+    protected void postprocessing(Q orderInfo){
+    }
+
+
+}
+

CommonPayRequest

+
public interface CommonPayRequest {
+
+
+    /**
+     * 获取平台类型
+     */
+    PayPlatformTypeEnum getPayTypeEnum();
+
+
+}
+

PayPlatformTypeEnum

+
public enum PayPlatformTypeEnum {
+
+    // 支付平台
+    ALIPAY,
+    WECHAT,
+
+}
+

PayFacade

+
public interface PayFacade {
+
+
+    /**
+     * 支付调用接口(指定操作传入class,传入请求参数)
+     *
+     * @param <T>       支付操作类型 {@linkplain  PayService}
+     * @param orderInfo 订单信息
+     */
+    <T extends PayService> Object execute(Class<T> payClass, CommonPayRequest orderInfo);
+}
+

PayFacadeImpl

+
@Slf4j
+@Component
+public class PayFacadeImpl implements PayFacade {
+
+    
+    @Override
+    public  <T extends PayService> Object execute(Class<T> payClass, CommonPayRequest orderInfo){
+        Optional<T> payItem = new ArrayList<>(SpringUtil.getBeansOfType(payClass).values())
+                .stream()
+                .filter(item -> item.getPayPlatformType().equals(orderInfo.getPayTypeEnum()))
+                .findFirst();
+        log.info("支付行为[{}],调用平台类型[{}]",payClass.getSimpleName(),orderInfo.getPayTypeEnum());
+
+        @SuppressWarnings("unchecked")
+        Optional<PayService> payService = (Optional<PayService>) payItem;
+        if (payService.isPresent()) {
+            return payService.get().apply(orderInfo);
+        }
+        throw new IllegalArgumentException("未获取到支付平台类型");
+    }
+
+
+}
+

PayException

+
public class PayException extends RuntimeException{
+
+
+    private String msg;
+
+    public PayException(String message) {
+        super(message);
+        this.msg = message;
+    }
+
+    public PayException(String message, Throwable cause) {
+        super(message, cause);
+        this.msg = message;
+    }
+
+}
+

支付宝

+

AlipayRefundService

+
@Service
+public class AlipayRefundService extends AbstractPayService<AlipayRefundRequest,AlipayTradeRefundResponse> implements PayRefundAble {
+
+    @Autowired
+    private AlipayClient alipayClient;
+
+    @Override
+    protected AlipayTradeRefundResponse processing(AlipayRefundRequest orderInfo) throws PayException {
+        AlipayTradeRefundRequest alipayRequest = new AlipayTradeRefundRequest ();
+        alipayRequest.setBizContent(JSON.toJSONString(orderInfo));
+        AlipayTradeRefundResponse response = null;
+        try {
+            response = alipayClient.execute(alipayRequest);
+        } catch (AlipayApiException e) {
+            throw new PayException(e.getMessage(),e);
+        }
+        if (response.isSuccess()) {
+            // 退款成功
+            return response;
+        }
+        return response;
+    }
+
+
+    @Override
+    public PayPlatformTypeEnum getPayPlatformType() {
+        return PayPlatformTypeEnum.ALIPAY;
+    }
+
+
+}
+

AlipayPayOrderQueryService

+
@Slf4j
+@Service
+public class AlipayPayOrderQueryService extends AbstractPayService<AlipayQueryOrderRequest,AlipayTradeQueryResponse> implements PayOrderQueryAble {
+
+
+    @Autowired
+    private AlipayClient alipayClient;
+
+
+    @Override
+    protected AlipayTradeQueryResponse processing(AlipayQueryOrderRequest orderInfo) throws PayException {
+        AlipayTradeQueryRequest alipayRequest = new AlipayTradeQueryRequest();
+        alipayRequest.setBizContent(JSON.toJSONString(orderInfo));
+        AlipayTradeQueryResponse response = null;
+        try {
+            response = alipayClient.execute(alipayRequest);
+        } catch (AlipayApiException e) {
+            throw new PayException(e.getMessage(),e);
+        }
+        if (response.isSuccess()) {
+            // 支付成功
+            return response;
+        }
+        return response;
+    }
+
+
+    @Override
+    public PayPlatformTypeEnum getPayPlatformType() {
+        return PayPlatformTypeEnum.ALIPAY;
+    }
+}
+

AlipayPaymentService

+
@Slf4j
+@Service
+public class AlipayPaymentService extends AbstractPayService<AlipayPaymentRequest, AlipayTradePagePayResponse> implements PaymentAble {
+
+
+    @Autowired
+    private AlipayClient alipayClient;
+
+    @Autowired
+    private AlipayProperties aliPayProperties;
+
+
+    protected AlipayTradePagePayRequest buildUnifiedOrderRequest(AlipayPaymentRequest alipayPaymentRequest) {
+        AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
+        alipayRequest.setReturnUrl(aliPayProperties.getReturnUrl());
+        alipayRequest.setNotifyUrl(aliPayProperties.getNotifyUrl());
+        alipayRequest.setBizContent(JSON.toJSONString(alipayPaymentRequest));
+        return alipayRequest;
+    }
+
+
+    @Override
+    protected AlipayTradePagePayResponse processing(AlipayPaymentRequest alipayPaymentRequest) throws PayException {
+        try {
+            return alipayClient.pageExecute(buildUnifiedOrderRequest(alipayPaymentRequest));
+        } catch (AlipayApiException e) {
+            throw new PayException(e.getMessage(), e);
+        }
+    }
+
+
+    @Override
+    public PayPlatformTypeEnum getPayPlatformType() {
+        return PayPlatformTypeEnum.ALIPAY;
+    }
+
+
+}
+

AlipayOrderCloseService

+
@Service
+public class AlipayOrderCloseService extends AbstractPayService<AlipayOrderCloseRequest,AlipayTradeCloseResponse> implements PayOrderCloseAble {
+
+
+    @Autowired
+    private AlipayClient alipayClient;
+
+
+    @Override
+    protected AlipayTradeCloseResponse processing(AlipayOrderCloseRequest orderInfo) throws PayException {
+        AlipayTradeCloseRequest alipayRequest = new AlipayTradeCloseRequest();
+        alipayRequest.setBizContent(JSON.toJSONString(orderInfo));
+        AlipayTradeCloseResponse response = null;
+        try {
+            response = alipayClient.execute(alipayRequest);
+        } catch (AlipayApiException e) {
+            throw new PayException(e.getMessage(),e);
+        }
+        if (response.isSuccess()) {
+            // 关闭成功
+            return response;
+        }
+        return response;
+    }
+
+
+
+    @Override
+    public PayPlatformTypeEnum getPayPlatformType() {
+        return PayPlatformTypeEnum.ALIPAY;
+    }
+
+
+}
+

AliApp

+
@Service
+public class AliApp extends AlipayPaymentService {
+
+    @Override
+    protected AlipayTradePagePayRequest buildUnifiedOrderRequest(AlipayPaymentRequest alipayPaymentRequest) {
+        alipayPaymentRequest.setProduct_code("QUICK_MSECURITY_PAY");
+        return super.buildUnifiedOrderRequest(alipayPaymentRequest);
+    }
+
+
+}
+

AliH5

+
@Service
+public class AliH5 extends AlipayPaymentService {
+
+    @Override
+    protected AlipayTradePagePayRequest buildUnifiedOrderRequest(AlipayPaymentRequest alipayPaymentRequest) {
+        alipayPaymentRequest.setProduct_code("QUICK_WAP_WAY");
+        return super.buildUnifiedOrderRequest(alipayPaymentRequest);
+    }
+
+
+}
+

AliPc

+
@Service
+public class AliPc extends AlipayPaymentService {
+
+    @Override
+    protected AlipayTradePagePayRequest buildUnifiedOrderRequest(AlipayPaymentRequest alipayPaymentRequest) {
+        alipayPaymentRequest.setProduct_code("FAST_INSTANT_TRADE_PAY");
+        return super.buildUnifiedOrderRequest(alipayPaymentRequest);
+    }
+
+}
+

AlipayOrderCloseRequest

+
@Data
+@Accessors(chain = true)
+public class AlipayOrderCloseRequest implements CommonPayRequest {
+
+    /**
+     * 该交易在支付宝系统中的交易流水号
+     */
+    private String trade_no;
+
+    /**
+     * 订单支付时传入的商户订单号,和支付宝交易号不能同时为空。 trade_no,out_trade_no如果同时存在优先取trade_no
+     */
+    private String out_trade_no;
+
+
+    @Override
+    public PayPlatformTypeEnum getPayTypeEnum() {
+        return PayPlatformTypeEnum.ALIPAY;
+
+    }
+
+}
+

AlipayPaymentRequest

+
@Data
+@Accessors(chain = true)
+public class AlipayPaymentRequest implements CommonPayRequest {
+
+    /**
+     * 商户订单号
+     */
+    private String out_trade_no;
+
+    /**
+     * 订单名称
+     */
+    private String subject;
+
+    /**
+     * 付款金额
+     */
+    private String total_amount;
+
+    /**
+     * 商品描述
+     */
+    private String body;
+
+    /**
+     * 超时时间参数
+     */
+    private String timeout_express = "60m";
+
+    /**
+     * 产品编号
+     */
+    private String product_code;
+
+    @Override
+    public PayPlatformTypeEnum getPayTypeEnum() {
+        return PayPlatformTypeEnum.ALIPAY;
+    }
+
+}
+

AlipayQueryOrderRequest

+
@Data
+@Accessors(chain = true)
+public class AlipayQueryOrderRequest implements CommonPayRequest {
+
+
+    /**
+     * 商户订单号;
+     */
+    private String out_trade_no;
+
+
+    @Override
+    public PayPlatformTypeEnum getPayTypeEnum() {
+        return PayPlatformTypeEnum.ALIPAY;
+    }
+
+
+}
+

AlipayRefundRequest

+
@Data
+@Accessors(chain = true)
+public class AlipayRefundRequest implements CommonPayRequest {
+
+    /**
+     * 支付宝交易号
+     */
+    private String trade_no;
+
+    /**
+     * 商户订单号
+     */
+    private String out_trade_no;
+
+    /**
+     * 退款请求号
+     */
+    private String out_request_no;
+
+    /**
+     * 退款金额
+     */
+    private String refund_amount;
+
+
+    @Override
+    public PayPlatformTypeEnum getPayTypeEnum() {
+        return PayPlatformTypeEnum.ALIPAY;
+    }
+
+}
+

AlipayConfiguration

+
@Configuration
+@EnableConfigurationProperties(AlipayProperties.class)
+@AllArgsConstructor
+public class AlipayConfiguration {
+
+    private AlipayProperties aliPayProperties;
+
+
+    @Bean
+    public AlipayClient alipayClient() {
+        return new DefaultAlipayClient(
+                aliPayProperties.getGatewayUrl(),
+                aliPayProperties.getAppId(),
+                aliPayProperties.getMerchantPrivateKey(),
+                aliPayProperties.getFormat(),
+                aliPayProperties.getCharset(),
+                aliPayProperties.getAlipayPublicKey(),
+                aliPayProperties.getSignType()
+        );
+    }
+
+
+}
+

AlipayProperties

+
@Data
+@ConfigurationProperties(prefix = "pay.alipay")
+public class AlipayProperties {
+
+    /**
+     * 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
+     */
+    private String appId;
+
+    /**
+     * 商户私钥,您的PKCS8格式RSA2私钥
+     */
+    private String merchantPrivateKey;
+
+    /**
+     * 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
+     */
+    private String alipayPublicKey;
+
+    /**
+     * 服务器异步通知页面路径  需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
+     */
+    private String notifyUrl;
+
+    /**
+     * 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
+     */
+    private String returnUrl;
+
+    /**
+     * 签名方式
+     */
+    private String signType;
+
+    /**
+     * 字符编码格式
+     */
+    private String charset;
+
+    /**
+     * 格式
+     */
+    private String format;
+
+    /**
+     * 支付宝网关
+     */
+    private String gatewayUrl;
+
+
+}
+

微信

+

WxpayRefundService

+
@Service
+public class WxpayRefundService extends AbstractPayService implements PayRefundAble {
+
+    @Autowired
+    protected WxPayService wxPayService;
+
+
+    @Override
+    protected Object processing(CommonPayRequest orderInfo) throws PayException {
+        WxRefundRequest wxRefundRequest = (WxRefundRequest) orderInfo;
+        WxPayRefundRequest refundRequest = new WxPayRefundRequest();
+        refundRequest.setOutRefundNo(wxRefundRequest.getOutRefundNo());
+        refundRequest.setRefundAccount(wxRefundRequest.getRefundAccount());
+        refundRequest.setRefundDesc(wxRefundRequest.getRefundDesc());
+        refundRequest.setRefundFee(wxRefundRequest.getRefundFee());
+        refundRequest.setNotifyUrl(wxRefundRequest.getNotifyUrl());
+        refundRequest.setRefundFeeType(wxRefundRequest.getRefundFeeType());
+        refundRequest.setTransactionId(wxRefundRequest.getTransactionId());
+        refundRequest.setOutRefundNo(wxRefundRequest.getOutTradeNo());
+        refundRequest.setTotalFee(wxRefundRequest.getTotalFee());
+
+        try {
+            return wxPayService.refund(refundRequest);
+        } catch (WxPayException e) {
+            throw new PayException(e.getMessage(),e);
+        }
+    }
+
+
+
+
+    @Override
+    public PayPlatformTypeEnum getPayPlatformType() {
+        return PayPlatformTypeEnum.WECHAT;
+    }
+
+
+}
+

WxpayPaymentService

+
@Slf4j
+@Service
+public class WxpayPaymentService extends AbstractPayService implements PaymentAble {
+
+
+    @Autowired
+    protected WxPayService wxPayService;
+
+
+    @Override
+    public PayPlatformTypeEnum getPayPlatformType() {
+        return PayPlatformTypeEnum.WECHAT;
+    }
+
+
+    protected WxPayUnifiedOrderRequest buildUnifiedOrderRequest(WxpayPaymentRequest wxpayOrder) {
+        // 微信统一下单请求对象
+        WxPayUnifiedOrderRequest request = new WxPayUnifiedOrderRequest();
+        request.setOutTradeNo(wxpayOrder.getOut_trade_no());
+        request.setBody(wxpayOrder.getBody());
+        request.setFeeType(wxpayOrder.getFee_type());
+        request.setTotalFee(Integer.valueOf(wxpayOrder.getTotal_fee()));
+        request.setSpbillCreateIp(wxpayOrder.getSpbill_create_ip());
+        request.setNotifyUrl(wxpayOrder.getNotify_url());
+        request.setProductId(String.valueOf(System.currentTimeMillis()));
+        request.setTimeExpire(wxpayOrder.getTime_expire());
+        request.setTimeStart(wxpayOrder.getTime_start());
+        request.setProfitSharing("Y");
+        return request;
+    }
+
+
+    @Override
+    protected Object processing(CommonPayRequest orderInfo) throws PayException {
+        try {
+            return wxPayService.unifiedOrder(buildUnifiedOrderRequest((WxpayPaymentRequest) orderInfo));
+        } catch (WxPayException e) {
+            throw new PayException(e.getMessage(),e);
+        }
+    }
+
+
+}
+

WxpayOrderQueryService

+
@Service
+public class WxpayOrderQueryService extends AbstractPayService implements PayOrderQueryAble {
+
+    @Autowired
+    private WxPayService wxPayService;
+
+    @Override
+    public PayPlatformTypeEnum getPayPlatformType() {
+        return PayPlatformTypeEnum.WECHAT;
+    }
+
+
+    @Override
+    protected Object processing(CommonPayRequest orderInfo) throws PayException {
+        WxOrderQueryRequest wxQueryOrder = (WxOrderQueryRequest) orderInfo;
+        WxPayOrderQueryRequest req = new WxPayOrderQueryRequest();
+        req.setSubMchId(wxQueryOrder.getSubMchId());
+        req.setSubAppId(wxQueryOrder.getSubMchAppId());
+        req.setOutTradeNo(wxQueryOrder.getPayOrderId());
+        WxPayOrderQueryResult result;
+        try {
+            result = wxPayService.queryOrder(req);
+        } catch (WxPayException e) {
+            throw new PayException("微信订单查询异常", e);
+        }
+        // 根据状态判断
+        if ("SUCCESS".equals(result.getTradeState())) {
+            // 支付成功
+            return result;
+        } else if ("USERPAYING".equals(result.getTradeState())) {
+            // 支付中,等待用户输入密码
+            return result;
+        } else if ("CLOSED".equals(result.getTradeState()) || "REVOKED".equals(result.getTradeState()) || "PAYERROR".equals(result.getTradeState())) {
+            // CLOSED—已关闭, REVOKED—已撤销(刷卡支付), PAYERROR--支付失败(其他原因,如银行返回失败) 支付失败
+            return result;
+        } else {
+            // unknow
+            return result;
+        }
+    }
+
+
+}
+

WxpayOrderCloseService

+
@Service
+public class WxpayOrderCloseService extends AbstractPayService implements PayOrderCloseAble {
+
+
+    @Autowired
+    private WxPayService wxPayService;
+
+    @Override
+    protected Object processing(CommonPayRequest orderInfo) throws PayException {
+        WxpayOrderCloseRequest wxOrderInfo = (WxpayOrderCloseRequest) orderInfo;
+        WxPayOrderCloseRequest wxPayRequest = new WxPayOrderCloseRequest();
+        wxPayRequest.setOutTradeNo(wxOrderInfo.getOut_trade_no());
+        WxPayOrderCloseResult result = null;
+        try {
+            result = wxPayService.closeOrder(wxPayRequest);
+        } catch (WxPayException e) {
+            throw new PayException(e.getMessage(), e);
+        }
+        return result;
+    }
+
+
+    @Override
+    public PayPlatformTypeEnum getPayPlatformType() {
+        return PayPlatformTypeEnum.WECHAT;
+    }
+
+}
+

WxApp

+
@Service
+public class WxApp extends WxpayPaymentService {
+
+
+    @Override
+    protected WxPayUnifiedOrderRequest buildUnifiedOrderRequest(WxpayPaymentRequest wxpayOrder) {
+        WxPayUnifiedOrderRequest wxPayUnifiedOrderRequest = super.buildUnifiedOrderRequest(wxpayOrder);
+        wxPayUnifiedOrderRequest.setTradeType(WxPayConstants.TradeType.APP);
+        wxPayUnifiedOrderRequest.setOpenid(wxpayOrder.getOpenid());
+        return wxPayUnifiedOrderRequest;
+    }
+
+
+}
+

WxH5

+
@Service
+public class WxH5 extends WxpayPaymentService {
+
+
+
+    @Override
+    protected WxPayUnifiedOrderRequest buildUnifiedOrderRequest(WxpayPaymentRequest wxpayOrder) {
+        WxPayUnifiedOrderRequest wxPayUnifiedOrderRequest = super.buildUnifiedOrderRequest(wxpayOrder);
+        wxPayUnifiedOrderRequest.setTradeType(WxPayConstants.TradeType.MWEB);
+        wxPayUnifiedOrderRequest.setOpenid(wxpayOrder.getOpenid());
+        return wxPayUnifiedOrderRequest;
+    }
+
+
+}
+

WxJsapi

+
@Service
+public class WxJsapi extends WxpayPaymentService {
+
+
+
+    @Override
+    protected WxPayUnifiedOrderRequest buildUnifiedOrderRequest(WxpayPaymentRequest wxpayOrder) {
+        WxPayUnifiedOrderRequest wxPayUnifiedOrderRequest = super.buildUnifiedOrderRequest(wxpayOrder);
+        wxPayUnifiedOrderRequest.setTradeType(WxPayConstants.TradeType.JSAPI);
+        wxPayUnifiedOrderRequest.setOpenid(wxpayOrder.getOpenid());
+        return wxPayUnifiedOrderRequest;
+    }
+
+
+}
+

WxNative

+
@Service
+public class WxNative extends WxpayPaymentService {
+
+    @Override
+    protected WxPayUnifiedOrderRequest buildUnifiedOrderRequest(WxpayPaymentRequest wxpayOrder) {
+        WxPayUnifiedOrderRequest wxPayUnifiedOrderRequest = super.buildUnifiedOrderRequest(wxpayOrder);
+        wxPayUnifiedOrderRequest.setTradeType(WxPayConstants.TradeType.NATIVE);
+        return wxPayUnifiedOrderRequest;
+    }
+
+}
+

WxOrderQueryRequest

+
@Data
+@Accessors(chain = true)
+public class WxOrderQueryRequest implements CommonPayRequest {
+
+    /**
+     * 支付订单号
+     */
+    private String payOrderId;
+
+
+    // 特约商户传入
+    /** 子商户ID **/
+    private String subMchId;
+
+    /** 子账户appID **/
+    private String subMchAppId;
+
+
+    @Override
+    public PayPlatformTypeEnum getPayTypeEnum() {
+        return PayPlatformTypeEnum.WECHAT;
+    }
+
+}
+

WxpayOrderCloseRequest

+
@Data
+@Accessors(chain = true)
+public class WxpayOrderCloseRequest implements CommonPayRequest {
+
+
+    /**
+     * 商户订单号
+     */
+    private String out_trade_no;
+
+
+    @Override
+    public PayPlatformTypeEnum getPayTypeEnum() {
+        return PayPlatformTypeEnum.WECHAT;
+    }
+
+
+}
+

WxpayPaymentRequest

+
@Data
+@Accessors(chain = true)
+public class WxpayPaymentRequest implements CommonPayRequest {
+
+
+    /**
+     * 公众账号appid
+     */
+    private String appid;
+
+
+    /**
+     * 商户号
+     */
+    private String mch_id;
+
+    /**
+     * 商品描述
+     */
+    private String body;
+
+
+    /**
+     * 附加数据; 在查询API和支付通知中原样返回,可作为自定义参数使用。
+     */
+    private String attach;
+
+    /**
+     * 商户订单号; 要求32个字符内(最少6个字符),只能是数字、大小写字母_-|*且在同一个商户号下唯一
+     */
+    private String out_trade_no;
+
+    /**
+     * 总金额(分)
+     */
+    private String total_fee;
+
+    /**
+     * 默认人民币:CNY
+     */
+    private String fee_type;
+
+
+    /**
+     * 交易起始时间 格式为yyyyMMddHHmmss
+     */
+    private String time_start;
+
+
+    /**
+     * 交易结束时间 格式为yyyyMMddHHmmss
+     */
+    private String time_expire;
+
+    /**
+     * 交易类型;
+     * JSAPI -JSAPI支付
+     * NATIVE -Native支付
+     * APP -APP支付
+     */
+    private String trade_type;
+
+
+    /**
+     * 商品ID; trade_type=NATIVE时,此参数必传
+     */
+    private String product_id;
+
+    /**
+     * 用户标识; trade_type=JSAPI时(即JSAPI支付),此参数必传
+     */
+    private String openid;
+
+    /**
+     * 是否分账 不传默认不分账
+     * <p>
+     * Y-是,需要分账
+     * N-否,不分账
+     */
+    private String profit_sharing;
+
+    /**
+     * 回调地址
+     */
+    private String notify_url;
+
+    /**
+     * 支持IPV4和IPV6两种格式的IP地址。用户的客户端IP
+     */
+    private String spbill_create_ip;
+
+    /**
+     * 随机字符串; 长度要求在32位以内
+     */
+    private String nonce_str;
+
+    @Override
+    public PayPlatformTypeEnum getPayTypeEnum() {
+        return PayPlatformTypeEnum.WECHAT;
+    }
+
+
+}
+

WxRefundRequest

+
@Data
+@Accessors(chain = true)
+public class WxRefundRequest implements CommonPayRequest {
+
+    /**
+     * 微信支付订单号
+     */
+    private String transactionId;
+    /**
+     * 商户订单号
+     */
+    private String outTradeNo;
+
+    /**
+     * 商户退款单号
+     */
+    private String outRefundNo;
+    /**
+     * 订单金额
+     */
+    private Integer totalFee;
+    /**
+     * 退款金额
+     */
+    private Integer refundFee;
+    /**
+     * 退款货币种类
+     */
+    private String refundFeeType;
+    /**
+     * 退款资金来源
+     */
+    private String refundAccount;
+    /**
+     * 退款原因
+     */
+    private String refundDesc;
+    /**
+     * 退款结果通知url
+     */
+    private String notifyUrl;
+
+    @Override
+    public PayPlatformTypeEnum getPayTypeEnum() {
+        return PayPlatformTypeEnum.WECHAT;
+    }
+}
+

WxpayConfiguration

+
@Configuration
+@ConditionalOnClass(WxPayService.class)
+@EnableConfigurationProperties(WxpayProperties.class)
+@AllArgsConstructor
+public class WxpayConfiguration {
+
+    private WxpayProperties properties;
+
+    @Bean
+    @ConditionalOnMissingBean
+    public WxPayService wxService() {
+        WxPayConfig payConfig = new WxPayConfig();
+        payConfig.setAppId(StringUtils.trimToNull(this.properties.getAppId()));
+        payConfig.setMchId(StringUtils.trimToNull(this.properties.getMchId()));
+        payConfig.setMchKey(StringUtils.trimToNull(this.properties.getMchKey()));
+        payConfig.setSubAppId(StringUtils.trimToNull(this.properties.getSubAppId()));
+        payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId()));
+        payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath()));
+        // 可以指定是否使用沙箱环境
+        payConfig.setUseSandboxEnv(false);
+        WxPayService wxPayService = new WxPayServiceImpl();
+        wxPayService.setConfig(payConfig);
+        return wxPayService;
+    }
+
+}
+

WxpayProperties

+
@Data
+@ConfigurationProperties(prefix = "pay.wechat")
+public class WxpayProperties {
+  /**
+   * 设置微信公众号或者小程序等的appid
+   */
+  private String appId;
+
+  /**
+   * 微信支付商户号
+   */
+  private String mchId;
+
+  /**
+   * 微信支付商户密钥
+   */
+  private String mchKey;
+
+  /**
+   * 服务商模式下的子商户公众账号ID,普通模式请不要配置,请在配置文件中将对应项删除
+   */
+  private String subAppId;
+
+  /**
+   * 服务商模式下的子商户号,普通模式请不要配置,最好是请在配置文件中将对应项删除
+   */
+  private String subMchId;
+
+  /**
+   * apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定
+   */
+  private String keyPath;
+
+  /**
+   * 支付回调地址
+   */
+  private String payNotifyUrl;
+
+  /**
+   * 退款回调地址
+   */
+  private String refundNotifyUrl;
+
+}
+

调用测试

+

AlipayExampleController

+
@RestController
+@RequestMapping("/example/alipay")
+@AllArgsConstructor
+public class AlipayExampleController {
+
+    private AlipayClient alipayClient;
+
+
+    /**
+     * 统一下单
+     * 接口地址: https://opendocs.alipay.com/open/59da99d0_alipay.trade.page.pay
+     *
+     * @param request 请求对象
+     * @return 返回 {@link com.alipay.api.AlipayResponse}包下的类对象
+     */
+    @SneakyThrows
+    @PostMapping("/unifiedOrder")
+    public AlipayTradePagePayResponse unifiedOrder(@RequestBody AlipayTradePagePayRequest request) {
+        return alipayClient.pageExecute(request);
+    }
+
+
+    /**
+     * 交易查询
+     * 接口地址: https://opendocs.alipay.com/open/bff76748_alipay.trade.query
+     *
+     * @param request 请求对象
+     * @return 返回 {@link com.alipay.api.AlipayResponse}包下的类对象
+     */
+    @SneakyThrows
+    @PostMapping("/queryOrder")
+    public AlipayTradeQueryResponse queryOrder(@RequestBody AlipayTradeQueryRequest request) {
+        return alipayClient.pageExecute(request);
+    }
+
+    /**
+     * 交易退款
+     * 接口地址: https://opendocs.alipay.com/open/357441a2_alipay.trade.fastpay
+     *
+     * @param request 请求对象
+     * @return 返回 {@link com.alipay.api.AlipayResponse}包下的类对象
+     */
+    @SneakyThrows
+    @PostMapping("/refund")
+    public AlipayTradeRefundResponse refund(@RequestBody AlipayTradeRefundRequest request) {
+        return alipayClient.pageExecute(request);
+    }
+
+    /**
+     * 退款查询
+     * 接口地址: https://opendocs.alipay.com/open/357441a2_alipay.trade.fastpay.refund.query
+     *
+     * @param request 请求对象
+     * @return 返回 {@link com.alipay.api.AlipayResponse}包下的类对象
+     */
+    @SneakyThrows
+    @PostMapping("/refundQuery")
+    public AlipayTradeFastpayRefundQueryResponse refundQuery(@RequestBody AlipayTradeFastpayRefundQueryRequest request) {
+        return alipayClient.pageExecute(request);
+    }
+
+
+    /**
+     * 交易关闭
+     * 接口地址: https://opendocs.alipay.com/open/8dc9ebb3_alipay.trade.close
+     *
+     * @param request 请求对象
+     * @return 返回 {@link com.alipay.api.AlipayResponse}包下的类对象
+     */
+    @SneakyThrows
+    @PostMapping("/closeOrder")
+    public AlipayTradeCloseResponse closeOrder(@RequestBody AlipayTradeCloseRequest request) {
+        return alipayClient.pageExecute(request);
+    }
+
+
+}
+

WxpayExampleController

+
@RestController
+@RequestMapping("/example/wxpay")
+@AllArgsConstructor
+public class WxpayExampleController {
+
+    private WxPayService wxService;
+
+    /**
+     * <pre>
+     * 查询订单(详见https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_2)
+     * 该接口提供所有微信支付订单的查询,商户可以通过查询订单接口主动查询订单状态,完成下一步的业务逻辑。
+     * 需要调用查询接口的情况:
+     * ◆ 当商户后台、网络、服务器等出现异常,商户系统最终未接收到支付通知;
+     * ◆ 调用支付接口后,返回系统错误或未知交易状态情况;
+     * ◆ 调用被扫支付API,返回USERPAYING的状态;
+     * ◆ 调用关单或撤销接口API之前,需确认支付状态;
+     * 接口地址:https://api.mch.weixin.qq.com/pay/orderquery
+     * </pre>
+     *
+     * @param transactionId 微信订单号
+     * @param outTradeNo    商户系统内部的订单号,当没提供transactionId时需要传这个。
+     */
+    @GetMapping("/queryOrder")
+    public WxPayOrderQueryResult queryOrder(@RequestParam(required = false) String transactionId,
+                                            @RequestParam(required = false) String outTradeNo)
+            throws WxPayException {
+        return this.wxService.queryOrder(transactionId, outTradeNo);
+    }
+
+    @PostMapping("/queryOrder")
+    public WxPayOrderQueryResult queryOrder(@RequestBody WxPayOrderQueryRequest wxPayOrderQueryRequest) throws WxPayException {
+        return this.wxService.queryOrder(wxPayOrderQueryRequest);
+    }
+
+    /**
+     * <pre>
+     * 关闭订单
+     * 应用场景
+     * 以下情况需要调用关单接口:
+     * 1. 商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;
+     * 2. 系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。
+     * 注意:订单生成后不能马上调用关单接口,最短调用时间间隔为5分钟。
+     * 接口地址:https://api.mch.weixin.qq.com/pay/closeorder
+     * 是否需要证书:   不需要。
+     * </pre>
+     *
+     * @param outTradeNo 商户系统内部的订单号
+     */
+    @GetMapping("/closeOrder/{outTradeNo}")
+    public WxPayOrderCloseResult closeOrder(@PathVariable String outTradeNo) throws WxPayException {
+        return this.wxService.closeOrder(outTradeNo);
+    }
+
+    @PostMapping("/closeOrder")
+    public WxPayOrderCloseResult closeOrder(@RequestBody WxPayOrderCloseRequest wxPayOrderCloseRequest) throws WxPayException {
+        return this.wxService.closeOrder(wxPayOrderCloseRequest);
+    }
+
+    /**
+     * 调用统一下单接口,并组装生成支付所需参数对象.
+     *
+     * @param request 统一下单请求参数
+     * @param <T>     请使用{@link com.github.binarywang.wxpay.bean.order}包下的类
+     * @return 返回 {@link com.github.binarywang.wxpay.bean.order}包下的类对象
+     */
+    @PostMapping("/createOrder")
+    public <T> T createOrder(@RequestBody WxPayUnifiedOrderRequest request) throws WxPayException {
+        return this.wxService.createOrder(request);
+    }
+
+    /**
+     * 统一下单(详见https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1)
+     * 在发起微信支付前,需要调用统一下单接口,获取"预支付交易会话标识"
+     * 接口地址:https://api.mch.weixin.qq.com/pay/unifiedorder
+     *
+     * @param request 请求对象,注意一些参数如appid、mchid等不用设置,方法内会自动从配置对象中获取到(前提是对应配置中已经设置)
+     */
+    @PostMapping("/unifiedOrder")
+    public WxPayUnifiedOrderResult unifiedOrder(@RequestBody WxPayUnifiedOrderRequest request) throws WxPayException {
+        return this.wxService.unifiedOrder(request);
+    }
+
+    /**
+     * <pre>
+     * 微信支付-申请退款
+     * 详见 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_4
+     * 接口链接:https://api.mch.weixin.qq.com/secapi/pay/refund
+     * </pre>
+     *
+     * @param request 请求对象
+     * @return 退款操作结果
+     */
+    @PostMapping("/refund")
+    public WxPayRefundResult refund(@RequestBody WxPayRefundRequest request) throws WxPayException {
+        return this.wxService.refund(request);
+    }
+
+    /**
+     * <pre>
+     * 微信支付-查询退款
+     * 应用场景:
+     *  提交退款申请后,通过调用该接口查询退款状态。退款有一定延时,用零钱支付的退款20分钟内到账,
+     *  银行卡支付的退款3个工作日后重新查询退款状态。
+     * 详见 https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_5
+     * 接口链接:https://api.mch.weixin.qq.com/pay/refundquery
+     * </pre>
+     * 以下四个参数四选一
+     *
+     * @param transactionId 微信订单号
+     * @param outTradeNo    商户订单号
+     * @param outRefundNo   商户退款单号
+     * @param refundId      微信退款单号
+     * @return 退款信息
+     */
+    @GetMapping("/refundQuery")
+    public WxPayRefundQueryResult refundQuery(@RequestParam(required = false) String transactionId,
+                                              @RequestParam(required = false) String outTradeNo,
+                                              @RequestParam(required = false) String outRefundNo,
+                                              @RequestParam(required = false) String refundId)
+            throws WxPayException {
+        return this.wxService.refundQuery(transactionId, outTradeNo, outRefundNo, refundId);
+    }
+
+    /**
+     * 退款查询
+     * @param wxPayRefundQueryRequest 请求对象
+     * @return 退款信息
+     * @throws WxPayException
+     */
+    @PostMapping("/refundQuery")
+    public WxPayRefundQueryResult refundQuery(@RequestBody WxPayRefundQueryRequest wxPayRefundQueryRequest) throws WxPayException {
+        return this.wxService.refundQuery(wxPayRefundQueryRequest);
+    }
+
+    /**
+     * 支付回调通知处理
+     */
+    @PostMapping("/notify/order")
+    public String parseOrderNotifyResult(@RequestBody String xmlData) throws WxPayException {
+        final WxPayOrderNotifyResult notifyResult = this.wxService.parseOrderNotifyResult(xmlData);
+        // TODO 根据自己业务场景需要构造返回对象
+        return WxPayNotifyResponse.success("成功");
+    }
+
+    /**
+     * 退款回调通知处理
+     * @param xmlData
+     */
+    @PostMapping("/notify/refund")
+    public String parseRefundNotifyResult(@RequestBody String xmlData) throws WxPayException {
+        final WxPayRefundNotifyResult result = this.wxService.parseRefundNotifyResult(xmlData);
+        // TODO 根据自己业务场景需要构造返回对象
+        return WxPayNotifyResponse.success("成功");
+    }
+
+    /**
+     * 扫码支付回调通知处理
+     */
+    @PostMapping("/notify/scanpay")
+    public String parseScanPayNotifyResult(String xmlData) throws WxPayException {
+        final WxScanPayNotifyResult result = this.wxService.parseScanPayNotifyResult(xmlData);
+        // TODO 根据自己业务场景需要构造返回对象
+        return WxPayNotifyResponse.success("成功");
+    }
+
+
+    /**
+     * <pre>
+     * 扫码支付模式一生成二维码的方法
+     * 二维码中的内容为链接,形式为:
+     * weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX
+     * 其中XXXXX为商户需要填写的内容,商户将该链接生成二维码,如需要打印发布二维码,需要采用此格式。商户可调用第三方库生成二维码图片。
+     * 文档详见: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4
+     * </pre>
+     *
+     * @param productId  产品Id
+     * @param logoFile   商户logo图片的文件对象,可以为空
+     * @param sideLength 要生成的二维码的边长,如果为空,则取默认值400
+     * @return 生成的二维码的字节数组
+     */
+    public byte[] createScanPayQrcodeMode1(String productId, File logoFile, Integer sideLength) {
+        return this.wxService.createScanPayQrcodeMode1(productId, logoFile, sideLength);
+    }
+
+    /**
+     * <pre>
+     * 扫码支付模式一生成二维码的方法
+     * 二维码中的内容为链接,形式为:
+     * weixin://wxpay/bizpayurl?sign=XXXXX&appid=XXXXX&mch_id=XXXXX&product_id=XXXXXX&time_stamp=XXXXXX&nonce_str=XXXXX
+     * 其中XXXXX为商户需要填写的内容,商户将该链接生成二维码,如需要打印发布二维码,需要采用此格式。商户可调用第三方库生成二维码图片。
+     * 文档详见: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_4
+     * </pre>
+     *
+     * @param productId 产品Id
+     * @return 生成的二维码URL连接
+     */
+

PayTestController

+
@Slf4j
+@RestController
+@RequestMapping("/example/iPay")
+public class PayTestController {
+
+    @Autowired
+    private PayFacade payFacade;
+
+
+    @GetMapping("/alipayUnifiedOrder")
+    public String alipayUnifiedOrder() {
+        payFacade.execute(PaymentAble.class,
+                new AlipayPaymentRequest()
+                        .setBody("商品描述")
+                        .setSubject("支付宝测试商品")
+                        .setTotal_amount("0.1")
+                        .setOut_trade_no("2023009999999")
+        );
+        return "支付宝-支付-交易成功";
+    }
+
+
+    @RequestMapping("/alipayUnifiedOrderNotify")
+    public String alipayUnifiedOrderNotify() {
+        log.info("支付宝-支付回调-交易成功");
+        return "交易成功!";
+    }
+
+
+    @GetMapping("/wxUnifiedOrder")
+    public String wxUnifiedOrder() {
+        payFacade.execute(PaymentAble.class,
+                new WxpayPaymentRequest()
+                        .setBody("微信测试商品")
+                        .setTotal_fee("0.1")
+                        .setOut_trade_no("2023009999999")
+        );
+        return "微信-支付-交易成功";
+    }
+
+
+    @RequestMapping("/wxUnifiedOrderNotify")
+    public String wxUnifiedOrderNotify() {
+        log.info("微信-支付回调-交易成功");
+        return "交易成功!";
+    }
+
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/pipeline-business/index.html b/blog-site/public/posts/essays/pipeline-business/index.html new file mode 100644 index 00000000..a904fd84 --- /dev/null +++ b/blog-site/public/posts/essays/pipeline-business/index.html @@ -0,0 +1,2138 @@ + + + + + + + + + + + 管道流设计模式结合业务 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

管道流设计模式结合业务

+ 2023.06.15 +
+

流程图

+

管道流设计结合业务-01

+

代码实现

+

管道流设计结合业务-02

+

pom

+
<dependencies>
+    <dependency>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-web</artifactId>
+    </dependency>
+    
+    <dependency>
+        <groupId>org.springframework.plugin</groupId>
+        <artifactId>spring-plugin-core</artifactId>
+        <version>${spring.plugin.core.version}</version>
+    </dependency>
+    
+    <dependency>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-test</artifactId>
+        <scope>test</scope>
+    </dependency>
+    
+    <dependency>
+        <groupId>org.projectlombok</groupId>
+        <artifactId>lombok</artifactId>
+        <optional>true</optional>
+    </dependency>
+    
+    <dependency>
+        <groupId>cn.hutool</groupId>
+        <artifactId>hutool-all</artifactId>
+        <version>${hutool.version}</version>
+    </dependency>
+</dependencies>
+

context

+

EventContext

+
public interface EventContext {
+
+
+    /**
+     * 是否继续调用链
+     */
+    boolean continueChain();
+
+
+    /**
+     * 获取当前过滤器选择器
+     */
+    FilterSelector getFilterSelector();
+
+}
+

BizType

+
public interface BizType {
+
+
+    /**
+     * 获取业务类型码值
+     */
+    Integer getCode();
+
+    /**
+     * 业务类型名称
+     *
+     */
+    String getName();
+
+}
+

AbstractEventContext

+
public abstract class AbstractEventContext implements EventContext{
+
+
+    private final BizType businessType;
+    private final FilterSelector filterSelector;
+
+
+    protected AbstractEventContext(BizType businessType, FilterSelector filterSelector) {
+        this.businessType = businessType;
+        this.filterSelector = filterSelector;
+    }
+
+
+    @Override
+    public boolean continueChain() {
+        return true;
+    }
+
+    @Override
+    public FilterSelector getFilterSelector() {
+        return filterSelector;
+    }
+
+}
+

filter

+

EventFilter

+
public interface EventFilter<T extends EventContext> {
+
+    /**
+     * 过滤逻辑封装点
+     *
+     * @param context 上下文对象
+     * @param chain   调用链
+     */
+    void doFilter(T context, EventFilterChain<T> chain);
+
+}
+

AbstractEventFilter

+
public abstract class AbstractEventFilter<T extends EventContext> implements EventFilter<T> {
+
+    @Override
+    public void doFilter(T context, EventFilterChain<T> chain) {
+        // 执行
+        if (context.getFilterSelector().matchFilter(this.getClass().getSimpleName())) {
+            handler(context);
+        }
+
+        // 是否继续执行调用链
+        if (context.continueChain()) {
+            chain.nextHandler(context);
+        }
+    }
+
+
+    /**
+     * 执行器
+     *
+     * @param context 上下文对象
+     */
+    protected abstract void handler(T context);
+
+}
+

EventFilterChain

+
public interface EventFilterChain<T extends EventContext> {
+
+
+    /**
+     * 执行当前过滤器
+     *
+     * @param context 上下文对象
+     */
+    void handler(T context);
+
+
+    /**
+     * 跳过当前过滤器 执行下一个执行过滤器
+     *
+     * @param context 上下文对象
+     */
+    void nextHandler(T context);
+
+}
+

FilterChainPipeline

+
@Slf4j
+@Component
+public class FilterChainPipeline<F extends EventFilter>{
+
+
+    private DefaultEventFilterChain<EventContext> last;
+
+
+    public FilterChainPipeline<F> append(F filter){
+        last = new DefaultEventFilterChain<>(last, filter);
+        return this;
+    }
+
+
+    public FilterChainPipeline<F> append(String description, F filter){
+        log.debug("过滤器调用链管道开始设置 {} 过滤器",description);
+        last = new DefaultEventFilterChain<>(last, filter);
+        return this;
+    }
+
+
+    public DefaultEventFilterChain<EventContext> getFilterChain() {
+        return this.last;
+    }
+
+}
+

DefaultEventFilterChain

+
public class DefaultEventFilterChain<T extends EventContext> implements EventFilterChain<T> {
+
+    private final EventFilterChain<T> next;
+    private final EventFilter<T> filter;
+
+
+    public DefaultEventFilterChain(EventFilterChain<T> next, EventFilter<T> filter) {
+        this.next = next;
+        this.filter = filter;
+    }
+
+
+    @Override
+    public void handler(T context) {
+        filter.doFilter(context,this);
+    }
+
+    @Override
+    public void nextHandler(T context) {
+        if (next != null) {
+            next.handler(context);
+        }
+    }
+
+}
+

selector

+

FilterSelector

+
public interface FilterSelector {
+
+
+    /**
+     * 匹配过滤器
+     *
+     * @param currentFilterName 过滤器名称
+     * @return true 匹配成功
+     */
+    boolean matchFilter(String currentFilterName);
+
+    /**
+     * 获取当前所有过滤器名称
+     *
+     * @return 过滤器名称
+     */
+    List<String> getAllFilterNames();
+}
+

DefaultFilterSelector

+
public class DefaultFilterSelector implements FilterSelector{
+
+    @Setter
+    private  List<String> filterNames = CollUtil.newArrayList();
+
+    @Override
+    public boolean matchFilter(String currentFilterName) {
+        return filterNames.stream().anyMatch(s -> Objects.equals(s,currentFilterName));
+    }
+
+
+    @Override
+    public List<String> getAllFilterNames() {
+        return filterNames;
+    }
+
+}
+

调用代码

+

管道流设计结合业务-03

+

PipelineApplication

+
@SpringBootApplication
+@EnablePluginRegistries(value = {Business1PostPlugin.class, Business2PostPlugin.class})
+public class PipelineApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(PipelineApplication.class, args);
+    }
+}
+

controller

+
@RestController
+@RequestMapping("/pipelineTest")
+public class PipelineController {
+
+    @Autowired
+    private Business1Service business1PipelineTestService;
+
+    @Autowired
+    private Business2Service business2PipelineTestService;
+
+
+    @GetMapping("/business1")
+    public void business1(){
+        PipelineRequestVo pipelineTestRequest = new PipelineRequestVo();
+        pipelineTestRequest.setUuid("business1-1110-1111231afsas-123adss");
+        pipelineTestRequest.setBusinessCode("business1");
+        pipelineTestRequest.setModel2(new Business1Model2());
+        pipelineTestRequest.setModel1(new Business1Model1());
+        business1PipelineTestService.doService(pipelineTestRequest);
+    }
+
+
+    @GetMapping("/business2")
+    public void business2(){
+        PipelineRequestVo pipelineTestRequest = new PipelineRequestVo();
+        pipelineTestRequest.setUuid("business2-1110-1111231afsas-123adss");
+        pipelineTestRequest.setBusinessCode("business2");
+        pipelineTestRequest.setModel3(new Business2Model1());
+        pipelineTestRequest.setModel4(new Business2Model2());
+        business2PipelineTestService.doService(pipelineTestRequest);
+    }
+
+}
+

entity

+
@Data
+public class PipelineRequestVo {
+
+
+    private String uuid;
+
+    private String businessCode;
+
+    /**
+     * 在自定义的filter中处理
+     */
+    @Setter
+    @Getter
+    private Business1Model1 model1;
+
+    /**
+     * 在自定义的filter中处理
+     */
+    @Setter
+    @Getter
+    private Business1Model2 model2;
+
+
+    /**
+     * 在自定义的filter中处理
+     */
+    @Setter
+    @Getter
+    private Business2Model1 model3;
+
+    /**
+     * 在自定义的filter中处理
+     */
+    @Setter
+    @Getter
+    private Business2Model2 model4;
+
+}
+

service

+
@Getter
+@AllArgsConstructor
+public enum BusinessTypeEnum implements BizType {
+
+    BUSINESS_1(1,"业务1"),
+    BUSINESS_2(2,"业务2"),
+    BUSINESS_3(3,"业务3"),
+    ;
+
+
+
+   private Integer code;
+   private String name;
+
+}
+

service.business1

+
public interface Business1Service {
+
+    void doService(PipelineRequestVo pipelineTestRequest);
+}
+
@Slf4j
+@Service
+public class Business1ServiceImpl implements Business1Service {
+
+    @Qualifier("business1PipelineSelectorFactory")
+    @Autowired
+    private  PipelineSelectorFactory business1PipelineSelectorFactory;
+
+    @Autowired
+    private  FilterChainPipeline<Business1PipelineFilter> filterChainPipeline;
+
+    @Autowired
+    private  PluginRegistry<Business1PostPlugin, Business1Model1> business1PostPlugin;
+
+
+
+    @Override
+    public void doService(PipelineRequestVo pipelineTestRequest) {
+        log.info("===============business1开始===============");
+        // 处理器参数
+        log.info("===============开始获取FilterSelector===============");
+        FilterSelector filterSelector = business1PipelineSelectorFactory.getFilterSelector(pipelineTestRequest);
+        Business1Context pipelineEventContext = new Business1Context(BusinessTypeEnum.BUSINESS_1, filterSelector);
+        log.info("获取FilterSelector完成: {}",filterSelector.getAllFilterNames());
+        log.info("===============获取FilterSelector完成===============");
+
+        // 处理
+        log.info("===============开始执行过滤器===============");
+        pipelineEventContext.setPipelineTestRequest(pipelineTestRequest);
+        pipelineEventContext.setModel2(pipelineTestRequest.getModel2());
+        pipelineEventContext.setModel1(pipelineTestRequest.getModel1());
+        filterChainPipeline.getFilterChain().handler(pipelineEventContext);
+        log.info("===============执行过滤器完成===============");
+
+        // 处理后获取值
+        log.info("===============开始执行后置处理器===============");
+        Business1Model2 model2 = pipelineEventContext.getModel2();
+        Business1Model1 model1 = pipelineEventContext.getModel1();
+        PipelineRequestVo pipelineTestRequest1 = pipelineEventContext.getPipelineTestRequest();
+        business1PostPlugin.getPluginsFor(model1)
+                .forEach(handler -> handler.postProcessing(model1));
+        log.info("===============执行后置处理器完成===============");
+
+        log.info("===============business1结束===============");
+
+    }
+
+}
+

service.business1.context

+
public class Business1Context extends AbstractEventContext {
+
+    /**
+     * 在自定义的filter中处理
+     */
+    @Setter
+    @Getter
+    private Business1Model1 model1;
+
+    /**
+     * 在自定义的filter中处理
+     */
+    @Setter
+    @Getter
+    private Business1Model2 model2;
+
+    /**
+     * 在自定义的filter中处理
+     */
+    @Setter
+    @Getter
+    private PipelineRequestVo pipelineTestRequest;
+
+
+    public Business1Context(BizType businessType, FilterSelector filterSelector) {
+        super(businessType, filterSelector);
+    }
+
+    @Override
+    public boolean continueChain() {
+        return true;
+    }
+
+}
+
@Data
+public class Business1Model1 {
+
+    private Integer id;
+
+    private String name1;
+
+    private String name2;
+
+    private String name3;
+
+}
+
@Data
+public class Business1Model2 {
+
+    private Integer id;
+
+    private String name;
+
+    private String desc;
+
+    private String age;
+
+}
+

service.business1.filters

+
public interface Business1PipelineFilter extends EventFilter<Business1Context> {
+
+    int order();
+}
+
@Slf4j
+@Component
+public class Business1Filter1 extends AbstractEventFilter<Business1Context> implements Business1PipelineFilter {
+
+    @Override
+    public void handler(Business1Context context) {
+        // 模拟操作数据库 等业务操作 可以利用门面模式进行解耦
+        Business1Model1 model1 = context.getModel1();
+        model1.setName1("张三");
+        model1.setName2("李四");
+        model1.setName3("王五");
+        model1.setId(1);
+
+        Business1Model2 model2 = context.getModel2();
+        model2.setId(2);
+        model2.setDesc("");
+        model2.setAge("18");
+        model2.setName("小白");
+
+        log.info("Filter1执行完毕...");
+
+        // 存入新的值到上下文对象中 下个处理器继续处理
+        context.setModel1(model1);
+        context.setModel2(model2);
+    }
+
+    @Override
+    public int order() {
+        return 1;
+    }
+}
+
@Slf4j
+@Component
+public class Business1Filter2 extends AbstractEventFilter<Business1Context>  implements Business1PipelineFilter {
+
+    @Override
+    public void handler(Business1Context context) {
+        // 模拟操作数据库 等业务操作 可以利用门面模式进行解耦
+        Business1Model1 model1 = context.getModel1();
+        model1.setName1(model1.getName1() + "-------------");
+        model1.setName2(model1.getName2() + "-------------");
+        model1.setName3(model1.getName3() + "-------------");
+        model1.setId(100);
+
+        log.info("Filter2执行完毕...");
+        // 存入新的值到上下文对象中 下个处理器继续处理
+        context.setModel1(model1);
+        context.setModel2(context.getModel2());
+    }
+
+    @Override
+    public int order() {
+        return 2;
+    }
+}
+

service.business1.plugins

+
public interface Business1PostPlugin extends Plugin<Business1Model1> {
+
+
+    /**
+     * 后置处理
+     *
+     * @param model 处理参数
+     */
+    void postProcessing(Business1Model1 model);
+
+}
+
@Slf4j
+@Component
+public class Business1ServicePluginImpl implements Business1PostPlugin {
+
+
+    @Override
+    public boolean supports(Business1Model1 pipelineEventContext) {
+        return true;
+    }
+
+
+    @Override
+    public void postProcessing(Business1Model1 model) {
+        log.info("===>{}",model.getId());
+    }
+
+}
+
@Slf4j
+@Component
+public class Business1ServicePluginImpl2 implements Business1PostPlugin {
+
+    @Override
+    public boolean supports(Business1Model1 model) {
+        return true;
+    }
+
+
+    @Override
+    public void postProcessing(Business1Model1 model) {
+        log.info("===>{}",model.getId());
+    }
+
+}
+

service.business2

+
public interface Business2Service {
+
+    void doService(PipelineRequestVo pipelineTestRequest);
+}
+
@Slf4j
+@Service
+public class Business2ServiceImpl implements Business2Service {
+
+    @Qualifier("business2PipelineSelectorFactory")
+    @Autowired
+    private PipelineSelectorFactory business2PipelineSelectorFactory;
+
+    @Autowired
+    private FilterChainPipeline<Business2PipelineFilter> filterChainPipeline;
+
+    @Autowired
+    private PluginRegistry<Business2PostPlugin, Business2Model1> business2PostPlugin;
+
+
+
+    @Override
+    public void doService(PipelineRequestVo pipelineTestRequest) {
+        log.info("===============business2开始===============");
+        // 处理器参数
+        log.info("===============开始获取FilterSelector===============");
+        FilterSelector filterSelector = business2PipelineSelectorFactory.getFilterSelector(pipelineTestRequest);
+        Business2Context pipelineEventContext = new Business2Context(BusinessTypeEnum.BUSINESS_2, filterSelector);
+        log.info("获取FilterSelector完成: {}",filterSelector.getAllFilterNames());
+        log.info("===============获取FilterSelector完成===============");
+
+        // 处理
+        log.info("===============开始执行过滤器===============");
+        pipelineEventContext.setPipelineTestRequest(pipelineTestRequest);
+        pipelineEventContext.setModel2(pipelineTestRequest.getModel4());
+        pipelineEventContext.setModel1(pipelineTestRequest.getModel3());
+        filterChainPipeline.getFilterChain().handler(pipelineEventContext);
+        log.info("===============执行过滤器完成===============");
+
+        // 处理后获取值
+        log.info("===============开始执行后置处理器===============");
+        Business2Model2 model2 = pipelineEventContext.getModel2();
+        Business2Model1 model1 = pipelineEventContext.getModel1();
+        PipelineRequestVo pipelineTestRequest1 = pipelineEventContext.getPipelineTestRequest();
+        business2PostPlugin.getPluginsFor(model1)
+                .forEach(handler -> handler.postProcessing(model1));
+        log.info("===============执行后置处理器完成===============");
+        log.info("===============business2结束===============");
+    }
+
+}
+

service.business2.context

+
public class Business2Context extends AbstractEventContext {
+
+    /**
+     * 在自定义的filter中处理
+     */
+    @Setter
+    @Getter
+    private Business2Model1 model1;
+
+    /**
+     * 在自定义的filter中处理
+     */
+    @Setter
+    @Getter
+    private Business2Model2 model2;
+
+    /**
+     * 在自定义的filter中处理
+     */
+    @Setter
+    @Getter
+    private PipelineRequestVo pipelineTestRequest;
+
+
+    public Business2Context(BizType businessType, FilterSelector filterSelector) {
+        super(businessType, filterSelector);
+    }
+
+    @Override
+    public boolean continueChain() {
+        return true;
+    }
+
+}
+
@Data
+public class Business2Model1 {
+
+    private Integer id;
+
+    private String name1;
+
+    private String name2;
+
+    private String name3;
+
+}
+
@Data
+public class Business2Model2 {
+
+    private Integer id;
+
+    private String name;
+
+    private String desc;
+
+    private String age;
+
+}
+

service.business2.filters

+
public interface Business2PipelineFilter extends EventFilter<Business2Context> {
+
+     int order();
+}
+
@Slf4j
+@Component
+public class Business2Filter1 extends AbstractEventFilter<Business2Context> implements Business2PipelineFilter {
+
+    @Override
+    public void handler(Business2Context context) {
+        // 模拟操作数据库 等业务操作 可以利用门面模式进行解耦
+        Business2Model1 model1 = context.getModel1();
+        model1.setName1("张三");
+        model1.setName2("李四");
+        model1.setName3("王五");
+        model1.setId(1);
+
+        Business2Model2 model2 = context.getModel2();
+        model2.setId(2);
+        model2.setDesc("");
+        model2.setAge("18");
+        model2.setName("小白");
+
+        log.info("Filter1执行完毕...");
+
+        // 存入新的值到上下文对象中 下个处理器继续处理
+        context.setModel1(model1);
+        context.setModel2(model2);
+    }
+
+    @Override
+    public int order() {
+        return 1;
+    }
+}
+
@Slf4j
+@Component
+public class Business2Filter2 extends AbstractEventFilter<Business2Context>  implements Business2PipelineFilter {
+
+    @Override
+    public void handler(Business2Context context) {
+        // 模拟操作数据库 等业务操作 可以利用门面模式进行解耦
+        Business2Model1 model1 = context.getModel1();
+        model1.setName1(model1.getName1() + "-------------");
+        model1.setName2(model1.getName2() + "-------------");
+        model1.setName3(model1.getName3() + "-------------");
+        model1.setId(100);
+
+        log.info("Filter2执行完毕...");
+        // 存入新的值到上下文对象中 下个处理器继续处理
+        context.setModel1(model1);
+        context.setModel2(context.getModel2());
+    }
+
+    @Override
+    public int order() {
+        return 2;
+    }
+}
+

service.business2.plugins

+
public interface Business2PostPlugin extends Plugin<Business2Model1> {
+
+
+    /**
+     * 后置处理
+     *
+     * @param model 处理参数
+     */
+    void postProcessing(Business2Model1 model);
+
+}
+
@Slf4j
+@Component
+public class Business2ServicePluginImpl implements Business2PostPlugin {
+
+
+    @Override
+    public boolean supports(Business2Model1 pipelineEventContext) {
+        return true;
+    }
+
+
+    @Override
+    public void postProcessing(Business2Model1 model) {
+        log.info("===>{}",model.getId());
+    }
+
+}
+
@Slf4j
+@Component
+public class Business2ServicePluginImpl2 implements Business2PostPlugin {
+
+
+    @Override
+    public boolean supports(Business2Model1 model) {
+        return true;
+    }
+
+
+    @Override
+    public void postProcessing(Business2Model1 model) {
+        log.info("===>{}",model.getId());
+    }
+
+}
+

service.config

+
@ConfigurationProperties(prefix = "test")
+@Component
+@Data
+public class FilterConfigProperties {
+
+    private Map<String, List<String>> configs;
+
+
+    public Map<String, List<String>> getConfigs() {
+        if (configs == null) {
+            configs = MapUtil.newHashMap(16);
+        }
+        return configs;
+    }
+
+}
+
@Component
+@RequiredArgsConstructor
+public class PipelineFilterConfig {
+
+    private final List<Business1PipelineFilter> business1PipelineFilter;
+    private final FilterChainPipeline<Business1PipelineFilter> business1FilterChainPipeline;
+
+    private final List<Business2PipelineFilter> business2PipelineFilter;
+    private final FilterChainPipeline<Business2PipelineFilter> business2FilterChainPipeline;
+
+    private final FilterConfigProperties filterConfigProperties;
+
+
+    @Bean
+    public FilterChainPipeline<Business1PipelineFilter> business1ChargePipeline() {
+        Map<String, List<String>> configs = filterConfigProperties.getConfigs();
+        if (business1PipelineFilter.isEmpty() || configs.isEmpty()){
+            return business1FilterChainPipeline;
+        }
+
+        Set<Map.Entry<String, List<String>>> filtersName = configs.entrySet();
+        long distinctCount = filtersName.stream().distinct().count();
+        if (distinctCount > business1PipelineFilter.size()) {
+            throw new IllegalArgumentException("设置的过滤器数量大于实际过滤器数量");
+        }
+
+        business1PipelineFilter
+                .stream()
+                .sorted(Comparator.comparing(Business1PipelineFilter::order))
+                .forEach(business1FilterChainPipeline::append)
+        ;
+        return business1FilterChainPipeline;
+    }
+
+
+    @Bean
+    public FilterChainPipeline<Business2PipelineFilter> business2ChargePipeline() {
+        Map<String, List<String>> configs = filterConfigProperties.getConfigs();
+        if (business2PipelineFilter.isEmpty() || configs.isEmpty()){
+            return business2FilterChainPipeline;
+        }
+
+        Set<Map.Entry<String, List<String>>> filtersName = configs.entrySet();
+        long distinctCount = filtersName.stream().distinct().count();
+        if (distinctCount > business2PipelineFilter.size()) {
+            throw new IllegalArgumentException("设置的过滤器数量大于实际过滤器数量");
+        }
+
+        business2PipelineFilter
+                .stream()
+                .sorted(Comparator.comparing(Business2PipelineFilter::order))
+                .forEach(business2FilterChainPipeline::append)
+        ;
+        return business2FilterChainPipeline;
+    }
+
+}
+

service.selector

+
public interface PipelineSelectorFactory {
+    FilterSelector  getFilterSelector(PipelineRequestVo request);
+}
+
@Component("business1PipelineSelectorFactory")
+public class Business1PipelineSelectorFactory implements PipelineSelectorFactory {
+
+    @Autowired
+    private FilterConfigProperties filterConfigProperties;
+
+    @Override
+    public FilterSelector getFilterSelector(PipelineRequestVo request) {
+        String businessCode = request.getBusinessCode();
+        DefaultFilterSelector defaultFilterSelector = new DefaultFilterSelector();
+        if (businessCode.equals("business1")){
+            defaultFilterSelector.setFilterNames(filterConfigProperties.getConfigs().getOrDefault(businessCode, Collections.unmodifiableList(new ArrayList<>())));
+        }
+        return defaultFilterSelector;
+    }
+}
+
@Component("business2PipelineSelectorFactory")
+public class Business2PipelineSelectorFactory implements PipelineSelectorFactory {
+
+    @Autowired
+    private FilterConfigProperties filterConfigProperties;
+
+    @Override
+    public FilterSelector getFilterSelector(PipelineRequestVo request) {
+        String businessCode = request.getBusinessCode();
+        DefaultFilterSelector defaultFilterSelector = new DefaultFilterSelector();
+        if (businessCode.equals("business2")){
+            defaultFilterSelector.setFilterNames(filterConfigProperties.getConfigs().getOrDefault(businessCode, Collections.unmodifiableList(new ArrayList<>())));
+        }
+        return defaultFilterSelector;
+    }
+
+}
+

application.yml

+
server:
+  port: 8080
+
+test:
+  configs:
+    business1:
+      - Business1Filter1
+      - Business1Filter2
+    business2:
+      - Business2Filter1
+      - Business2Filter2
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/scheduled-job/index.html b/blog-site/public/posts/essays/scheduled-job/index.html new file mode 100644 index 00000000..81b46e8f --- /dev/null +++ b/blog-site/public/posts/essays/scheduled-job/index.html @@ -0,0 +1,1840 @@ + + + + + + + + + + + 定时任务可视化管理 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

定时任务可视化管理

+ 2023.09.09 +
+

代码实现

+

代码结构

+

定时任务可视化管理-01

+

pom

+
<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-security</artifactId>
+</dependency>
+
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-web</artifactId>
+</dependency>
+
+<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-quartz</artifactId>
+</dependency>
+
+<dependency>
+    <groupId>org.projectlombok</groupId>
+    <artifactId>lombok</artifactId>
+</dependency>
+
+<dependency>
+    <groupId>cn.hutool</groupId>
+    <artifactId>hutool-all</artifactId>
+</dependency>
+

库表结构

+
-- ----------------------------
+-- 定时任务调度表
+-- ----------------------------
+drop table if exists sys_job;
+create table sys_job (
+  job_id              bigint(20)    not null auto_increment    comment '任务ID',
+  job_name            varchar(64)   default ''                 comment '任务名称',
+  job_group           varchar(64)   default 'DEFAULT'          comment '任务组名',
+  invoke_target       varchar(500)  not null                   comment '调用目标字符串',
+  cron_expression     varchar(255)  default ''                 comment 'cron执行表达式',
+  misfire_policy      varchar(20)   default '3'                comment '计划执行错误策略(1立即执行 2执行一次 3放弃执行)',
+  concurrent          char(1)       default '1'                comment '是否并发执行(0允许 1禁止)',
+  status              char(1)       default '0'                comment '状态(0正常 1暂停)',
+  create_by           varchar(64)   default ''                 comment '创建者',
+  create_time         datetime                                 comment '创建时间',
+  update_by           varchar(64)   default ''                 comment '更新者',
+  update_time         datetime                                 comment '更新时间',
+  remark              varchar(500)  default ''                 comment '备注信息',
+  primary key (job_id, job_name, job_group)
+) engine=innodb auto_increment=100 comment = '定时任务调度表';
+
+
+-- ----------------------------
+-- 定时任务调度日志表
+-- ----------------------------
+drop table if exists sys_job_log;
+create table sys_job_log (
+  job_log_id          bigint(20)     not null auto_increment    comment '任务日志ID',
+  job_name            varchar(64)    not null                   comment '任务名称',
+  job_group           varchar(64)    not null                   comment '任务组名',
+  invoke_target       varchar(500)   not null                   comment '调用目标字符串',
+  job_message         varchar(500)                              comment '日志信息',
+  status              char(1)        default '0'                comment '执行状态(0正常 1失败)',
+  exception_info      varchar(2000)  default ''                 comment '异常信息',
+  create_time         datetime                                  comment '创建时间',
+  primary key (job_log_id)
+) engine=innodb comment = '定时任务调度日志表';
+

JobManagerController

+
/**
+ * @author: whitepure
+ * @date: 2023/7/20 13:35
+ * @description: JobIndexController
+ */
+@Controller
+public class JobManagerController {
+
+    @RequestMapping({"/","/index"})
+    public String index(){
+        return "index.html";
+    }
+
+}
+

SysJobController

+
@RestController
+@RequestMapping("/main")
+public class SysJobController  {
+    @Autowired
+    private ISysJobService jobService;
+
+    /**
+     * 查询定时任务列表
+     */
+    @GetMapping("/list")
+    public R<List<SysJob>> list(SysJob sysJob) {
+        return R.ok(jobService.selectJobList(sysJob));
+    }
+
+    /**
+     * 获取定时任务详细信息
+     */
+    @GetMapping(value = "/getInfo")
+    public R<SysJob> getInfo(Long jobId) {
+        return R.ok(jobService.selectJobById(jobId));
+    }
+
+    /**
+     * 新增定时任务
+     */
+    @PostMapping("/add")
+    public R<Boolean> add(@RequestBody SysJob job) throws SchedulerException, TaskException {
+        if (!CronUtils.isValid(job.getCronExpression())) {
+            return R.failed("新增任务'" + job.getJobName() + "'失败,Cron表达式不正确");
+        } else if (StringUtils.containsIgnoreCase(job.getInvokeTarget(), ScheduleConstants.LOOKUP_RMI)) {
+            return R.failed("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用");
+        } else if (StrUtil.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{ScheduleConstants.LOOKUP_LDAP, ScheduleConstants.LOOKUP_LDAPS})) {
+            return R.failed("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用");
+        } else if (StrUtil.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{ScheduleConstants.HTTP, ScheduleConstants.HTTPS})) {
+            return R.failed("新增任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用");
+        } else if (StrUtil.containsAnyIgnoreCase(job.getInvokeTarget(), ScheduleConstants.JOB_ERROR_STR)) {
+            return R.failed("新增任务'" + job.getJobName() + "'失败,目标字符串存在违规");
+        } else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) {
+            return R.failed("新增任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
+        }
+        return R.ok(jobService.insertJob(job) > 0);
+    }
+
+    /**
+     * 修改定时任务
+     */
+    @PostMapping("/edit")
+    public R<Boolean> edit(@RequestBody SysJob job) throws SchedulerException, TaskException {
+        if (!CronUtils.isValid(job.getCronExpression())) {
+            return R.failed("修改任务'" + job.getJobName() + "'失败,Cron表达式不正确");
+        } else if (StrUtil.containsIgnoreCase(job.getInvokeTarget(), ScheduleConstants.LOOKUP_RMI)) {
+            return R.failed("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'rmi'调用");
+        } else if (StrUtil.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{ScheduleConstants.LOOKUP_LDAP, ScheduleConstants.LOOKUP_LDAPS})) {
+            return R.failed("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'ldap(s)'调用");
+        } else if (StrUtil.containsAnyIgnoreCase(job.getInvokeTarget(), new String[]{ScheduleConstants.HTTP, ScheduleConstants.HTTPS})) {
+            return R.failed("修改任务'" + job.getJobName() + "'失败,目标字符串不允许'http(s)'调用");
+        } else if (StrUtil.containsAnyIgnoreCase(job.getInvokeTarget(), ScheduleConstants.JOB_ERROR_STR)) {
+            return R.failed("修改任务'" + job.getJobName() + "'失败,目标字符串存在违规");
+        } else if (!ScheduleUtils.whiteList(job.getInvokeTarget())) {
+            return R.failed("修改任务'" + job.getJobName() + "'失败,目标字符串不在白名单内");
+        }
+        return R.ok(jobService.updateJob(job) > 0);
+    }
+
+    /**
+     * 定时任务状态修改
+     */
+    @PostMapping("/changeStatus")
+    public R<Boolean> changeStatus(@RequestBody SysJob job) throws SchedulerException {
+        SysJob newJob = jobService.selectJobById(job.getJobId());
+        newJob.setStatus(job.getStatus());
+        return R.ok(jobService.changeStatus(newJob) > 0);
+    }
+
+    /**
+     * 定时任务立即执行一次
+     */
+    @GetMapping("/run")
+    public R<Boolean> run(Long jobId) throws SchedulerException {
+        boolean result = jobService.run(jobId);
+        return result ? R.ok(true): R.failed("任务不存在或已过期!");
+    }
+
+    /**
+     * 删除定时任务
+     */
+    @GetMapping("/remove")
+    public R<Boolean> remove(Long jobId) throws SchedulerException {
+        jobService.deleteJobByIds(new Long[]{jobId});
+        return R.ok(true);
+    }
+}
+

SysJobLogController

+
@RestController
+@RequestMapping("/jobLog")
+public class SysJobLogController {
+    @Autowired
+    private ISysJobLogService jobLogService;
+
+    /**
+     * 查询定时任务调度日志列表
+     */
+    @GetMapping("/list")
+    public R<List<SysJobLog>> list(SysJobLog sysJobLog) {
+        return R.ok(jobLogService.selectJobLogList(sysJobLog));
+    }
+
+
+    /**
+     * 根据调度编号获取详细信息
+     */
+    @GetMapping(value = "/getInfo")
+    public R<SysJobLog> getInfo(Long logId) {
+        return R.ok(jobLogService.selectJobLogById(logId));
+    }
+
+
+    /**
+     * 删除定时任务调度日志
+     */
+    @PostMapping("/{jobLogIds}")
+    public R<Boolean> remove(@PathVariable Long[] jobLogIds) {
+        return R.ok(jobLogService.deleteJobLogByIds(jobLogIds) > 0);
+    }
+
+    /**
+     * 清空定时任务调度日志
+     */
+    @GetMapping("/clean")
+    public R<Boolean> clean() {
+        jobLogService.cleanJobLog();
+        return R.ok(true);
+    }
+
+}
+

JobExceptionHandler

+
@Slf4j
+@RestControllerAdvice("com.chenglian.scheduled")
+public class JobExceptionHandler {
+
+
+    @ExceptionHandler(Exception.class)
+    public R<Boolean> globalHandler(Exception exception) {
+        log.error("定时任务程序发生异常.", exception);
+        return R.failed(exception.getMessage());
+    }
+
+
+}
+

TaskException

+
public class TaskException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    private final Code code;
+
+    public TaskException(String msg, Code code) {
+        this(msg, code, null);
+    }
+
+    public TaskException(String msg, Code code, Exception nestedEx) {
+        super(msg, nestedEx);
+        this.code = code;
+    }
+
+    public Code getCode() {
+        return code;
+    }
+
+    public enum Code {
+        TASK_EXISTS, NO_TASK_EXISTS, TASK_ALREADY_STARTED, UNKNOWN, CONFIG_ERROR, TASK_NODE_NOT_AVAILABLE
+    }
+}
+

IErrorCode

+
public interface IErrorCode {
+    long getCode();
+
+    String getMsg();
+}
+

ApiErrorCode

+
@Getter
+@ToString
+public enum ApiErrorCode implements IErrorCode {
+    FAILED(-1L, "操作失败"),
+    SUCCESS(0L, "执行成功");
+
+    private final long code;
+    private final String msg;
+
+    ApiErrorCode(final long code, final String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+
+    public static ApiErrorCode fromCode(long code) {
+        ApiErrorCode[] ecs = values();
+        ApiErrorCode[] var3 = ecs;
+        int var4 = ecs.length;
+
+        for(int var5 = 0; var5 < var4; ++var5) {
+            ApiErrorCode ec = var3[var5];
+            if (ec.getCode() == code) {
+                return ec;
+            }
+        }
+
+        return SUCCESS;
+    }
+}
+

R

+
@Data
+@EqualsAndHashCode
+public class R<T> implements Serializable  {
+
+    private static final long serialVersionUID = 1L;
+    private long code;
+    private T data;
+    private String msg;
+
+    public R() {
+    }
+
+    public R(IErrorCode errorCode) {
+        errorCode = (IErrorCode) Optional.ofNullable(errorCode).orElse(ApiErrorCode.FAILED);
+        this.code = errorCode.getCode();
+        this.msg = errorCode.getMsg();
+    }
+
+    public static <T> R<T> ok(T data) {
+        ApiErrorCode aec = ApiErrorCode.SUCCESS;
+        if (data instanceof Boolean && Boolean.FALSE.equals(data)) {
+            aec = ApiErrorCode.FAILED;
+        }
+
+        return restResult(data, aec);
+    }
+
+    public static <T> R<T> failed(String msg) {
+        return restResult(null, ApiErrorCode.FAILED.getCode(), msg);
+    }
+
+    public static <T> R<T> failed(IErrorCode errorCode) {
+        return restResult(null, errorCode);
+    }
+
+    public static <T> R<T> restResult(T data, IErrorCode errorCode) {
+        return restResult(data, errorCode.getCode(), errorCode.getMsg());
+    }
+
+    private static <T> R<T> restResult(T data, long code, String msg) {
+        R<T> apiResult = new R();
+        apiResult.setCode(code);
+        apiResult.setData(data);
+        apiResult.setMsg(msg);
+        return apiResult;
+    }
+
+    public boolean ok() {
+        return ApiErrorCode.SUCCESS.getCode() == this.code;
+    }
+}
+

ISysJobLogService

+
public interface ISysJobLogService {
+    /**
+     * 获取quartz调度器日志的计划任务
+     *
+     * @param jobLog 调度日志信息
+     * @return 调度任务日志集合
+     */
+    List<SysJobLog> selectJobLogList(SysJobLog jobLog);
+
+    /**
+     * 通过调度任务日志ID查询调度信息
+     *
+     * @param jobLogId 调度任务日志ID
+     * @return 调度任务日志对象信息
+     */
+    SysJobLog selectJobLogById(Long jobLogId);
+
+    /**
+     * 新增任务日志
+     *
+     * @param jobLog 调度日志信息
+     */
+    void addJobLog(SysJobLog jobLog);
+
+    /**
+     * 批量删除调度日志信息
+     *
+     * @param logIds 需要删除的日志ID
+     * @return 结果
+     */
+    int deleteJobLogByIds(Long[] logIds);
+
+    /**
+     * 删除任务日志
+     *
+     * @param jobId 调度日志ID
+     * @return 结果
+     */
+    int deleteJobLogById(Long jobId);
+
+    /**
+     * 清空任务日志
+     */
+    void cleanJobLog();
+}
+

ISysJobService

+
public interface ISysJobService {
+    /**
+     * 获取quartz调度器的计划任务
+     *
+     * @param job 调度信息
+     * @return 调度任务集合
+     */
+    List<SysJob> selectJobList(SysJob job);
+
+    /**
+     * 通过调度任务ID查询调度信息
+     *
+     * @param jobId 调度任务ID
+     * @return 调度任务对象信息
+     */
+    SysJob selectJobById(Long jobId);
+
+    /**
+     * 暂停任务
+     *
+     * @param job 调度信息
+     * @return 结果
+     */
+    int pauseJob(SysJob job) throws SchedulerException;
+
+    /**
+     * 恢复任务
+     *
+     * @param job 调度信息
+     * @return 结果
+     */
+    int resumeJob(SysJob job) throws SchedulerException;
+
+    /**
+     * 删除任务后,所对应的trigger也将被删除
+     *
+     * @param job 调度信息
+     * @return 结果
+     */
+    int deleteJob(SysJob job) throws SchedulerException;
+
+    /**
+     * 批量删除调度信息
+     *
+     * @param jobIds 需要删除的任务ID
+     * @return 结果
+     */
+    void deleteJobByIds(Long[] jobIds) throws SchedulerException;
+
+    /**
+     * 任务调度状态修改
+     *
+     * @param job 调度信息
+     * @return 结果
+     */
+    int changeStatus(SysJob job) throws SchedulerException;
+
+    /**
+     * 立即运行任务
+     *
+     * @param jobId 调度任务ID
+     * @return 结果
+     */
+    boolean run(Long jobId) throws SchedulerException;
+
+    /**
+     * 新增任务
+     *
+     * @param job 调度信息
+     * @return 结果
+     */
+    int insertJob(SysJob job) throws SchedulerException, TaskException;
+
+    /**
+     * 更新任务
+     *
+     * @param job 调度信息
+     * @return 结果
+     */
+    int updateJob(SysJob job) throws SchedulerException, TaskException, TaskException;
+
+    /**
+     * 校验cron表达式是否有效
+     *
+     * @param cronExpression 表达式
+     * @return 结果
+     */
+    boolean checkCronExpressionIsValid(String cronExpression);
+}
+

AbstractQuartzJob

+
public abstract class AbstractQuartzJob implements Job {
+    private static final Logger log = LoggerFactory.getLogger(AbstractQuartzJob.class);
+
+    /**
+     * 线程本地变量
+     */
+    private static final ThreadLocal<Date> threadLocal = new ThreadLocal<>();
+
+    @Override
+    public void execute(JobExecutionContext context) {
+        SysJob sysJob = new SysJob();
+        BeanUtil.copyProperties(context.getMergedJobDataMap().get(ScheduleConstants.TASK_PROPERTIES),sysJob);
+        try {
+            before(context, sysJob);
+            if (sysJob.getJobId() != null) {
+                doExecute(context, sysJob);
+            }
+            after(context, sysJob, null);
+        } catch (Exception e) {
+            log.error("任务执行异常  - :", e);
+            after(context, sysJob, e);
+        }
+    }
+
+    /**
+     * 执行前
+     *
+     * @param context 工作执行上下文对象
+     * @param sysJob  系统计划任务
+     */
+    protected void before(JobExecutionContext context, SysJob sysJob) {
+        threadLocal.set(new Date());
+    }
+
+    /**
+     * 执行后
+     *
+     * @param context 工作执行上下文对象
+     * @param sysJob  系统计划任务
+     */
+    protected void after(JobExecutionContext context, SysJob sysJob, Exception e) {
+        Date startTime = threadLocal.get();
+        threadLocal.remove();
+
+        final SysJobLog sysJobLog = new SysJobLog();
+        sysJobLog.setJobName(sysJob.getJobName());
+        sysJobLog.setJobGroup(sysJob.getJobGroup());
+        sysJobLog.setInvokeTarget(sysJob.getInvokeTarget());
+        sysJobLog.setStartTime(startTime);
+        sysJobLog.setStopTime(new Date());
+        long runMs = sysJobLog.getStopTime().getTime() - sysJobLog.getStartTime().getTime();
+        sysJobLog.setJobMessage(sysJobLog.getJobName() + " 总共耗时:" + runMs + "毫秒");
+        if (e != null) {
+            sysJobLog.setStatus(ScheduleConstants.FAIL);
+            String errorMsg = StringUtils.substring(getExceptionMessage(e), 0, 2000);
+            sysJobLog.setExceptionInfo(errorMsg);
+        } else {
+            sysJobLog.setStatus(ScheduleConstants.SUCCESS);
+        }
+
+        // 写入数据库当中
+        SpringUtil.getBean(ISysJobLogService.class).addJobLog(sysJobLog);
+    }
+
+
+    public static String getExceptionMessage(Throwable e) {
+        StringWriter sw = new StringWriter();
+        e.printStackTrace(new PrintWriter(sw, true));
+        return sw.toString();
+    }
+
+    /**
+     * 执行方法,由子类重载
+     *
+     * @param context 工作执行上下文对象
+     * @param sysJob  系统计划任务
+     * @throws Exception 执行过程中的异常
+     */
+    protected abstract void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception;
+}
+

CronUtils

+
public class CronUtils {
+    /**
+     * 返回一个布尔值代表一个给定的Cron表达式的有效性
+     *
+     * @param cronExpression Cron表达式
+     * @return boolean 表达式是否有效
+     */
+    public static boolean isValid(String cronExpression) {
+        return CronExpression.isValidExpression(cronExpression);
+    }
+
+    /**
+     * 返回一个字符串值,表示该消息无效Cron表达式给出有效性
+     *
+     * @param cronExpression Cron表达式
+     * @return String 无效时返回表达式错误描述,如果有效返回null
+     */
+    public static String getInvalidMessage(String cronExpression) {
+        try {
+            new CronExpression(cronExpression);
+            return null;
+        } catch (ParseException pe) {
+            return pe.getMessage();
+        }
+    }
+
+    /**
+     * 返回下一个执行时间根据给定的Cron表达式
+     *
+     * @param cronExpression Cron表达式
+     * @return Date 下次Cron表达式执行时间
+     */
+    public static Date getNextExecution(String cronExpression) {
+        try {
+            CronExpression cron = new CronExpression(cronExpression);
+            return cron.getNextValidTimeAfter(new Date(System.currentTimeMillis()));
+        } catch (ParseException e) {
+            throw new IllegalArgumentException(e.getMessage());
+        }
+    }
+}
+

JobInvokeUtil

+
public class JobInvokeUtil {
+    /**
+     * 执行方法
+     *
+     * @param sysJob 系统任务
+     */
+    public static void invokeMethod(SysJob sysJob) throws Exception {
+        String invokeTarget = sysJob.getInvokeTarget();
+        String beanName = getBeanName(invokeTarget);
+        String methodName = getMethodName(invokeTarget);
+        List<Object[]> methodParams = getMethodParams(invokeTarget);
+
+        if (!isValidClassName(beanName)) {
+            Object bean = SpringUtil.getBean(beanName);
+            invokeMethod(bean, methodName, methodParams);
+        } else {
+            Object bean = Class.forName(beanName).getDeclaredConstructor().newInstance();
+            invokeMethod(bean, methodName, methodParams);
+        }
+    }
+
+    /**
+     * 调用任务方法
+     *
+     * @param bean         目标对象
+     * @param methodName   方法名称
+     * @param methodParams 方法参数
+     */
+    private static void invokeMethod(Object bean, String methodName, List<Object[]> methodParams)
+            throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException,
+            InvocationTargetException {
+        if (ObjectUtil.isNotEmpty(methodParams) && !methodParams.isEmpty()) {
+            Method method = bean.getClass().getMethod(methodName, getMethodParamsType(methodParams));
+            method.invoke(bean, getMethodParamsValue(methodParams));
+        } else {
+            Method method = bean.getClass().getMethod(methodName);
+            method.invoke(bean);
+        }
+    }
+
+    /**
+     * 校验是否为为class包名
+     *
+     * @param invokeTarget 名称
+     * @return true是 false否
+     */
+    public static boolean isValidClassName(String invokeTarget) {
+        return StringUtils.countMatches(invokeTarget, ".") > 1;
+    }
+
+    /**
+     * 获取bean名称
+     *
+     * @param invokeTarget 目标字符串
+     * @return bean名称
+     */
+    public static String getBeanName(String invokeTarget) {
+        String beanName = StringUtils.substringBefore(invokeTarget, "(");
+        return StringUtils.substringBeforeLast(beanName, ".");
+    }
+
+    /**
+     * 获取bean方法
+     *
+     * @param invokeTarget 目标字符串
+     * @return method方法
+     */
+    public static String getMethodName(String invokeTarget) {
+        String methodName = StringUtils.substringBefore(invokeTarget, "(");
+        return StringUtils.substringAfterLast(methodName, ".");
+    }
+
+    /**
+     * 获取method方法参数相关列表
+     *
+     * @param invokeTarget 目标字符串
+     * @return method方法相关参数列表
+     */
+    public static List<Object[]> getMethodParams(String invokeTarget) {
+        String methodStr = StringUtils.substringBetween(invokeTarget, "(", ")");
+        if (StringUtils.isEmpty(methodStr)) {
+            return null;
+        }
+        String[] methodParams = methodStr.split(",(?=([^\"']*[\"'][^\"']*[\"'])*[^\"']*$)");
+        List<Object[]> classs = new LinkedList<>();
+        for (int i = 0; i < methodParams.length; i++) {
+            String str = StringUtils.trimToEmpty(methodParams[i]);
+            // String字符串类型,以'或"开头
+            if (StringUtils.startsWithAny(str, "'", "\"")) {
+                classs.add(new Object[]{StringUtils.substring(str, 1, str.length() - 1), String.class});
+            }
+            // boolean布尔类型,等于true或者false
+            else if ("true".equalsIgnoreCase(str) || "false".equalsIgnoreCase(str)) {
+                classs.add(new Object[]{Boolean.valueOf(str), Boolean.class});
+            }
+            // long长整形,以L结尾
+            else if (StringUtils.endsWith(str, "L")) {
+                classs.add(new Object[]{Long.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Long.class});
+            }
+            // double浮点类型,以D结尾
+            else if (StringUtils.endsWith(str, "D")) {
+                classs.add(new Object[]{Double.valueOf(StringUtils.substring(str, 0, str.length() - 1)), Double.class});
+            }
+            // 其他类型归类为整形
+            else {
+                classs.add(new Object[]{Integer.valueOf(str), Integer.class});
+            }
+        }
+        return classs;
+    }
+
+    /**
+     * 获取参数类型
+     *
+     * @param methodParams 参数相关列表
+     * @return 参数类型列表
+     */
+    public static Class<?>[] getMethodParamsType(List<Object[]> methodParams) {
+        Class<?>[] classs = new Class<?>[methodParams.size()];
+        int index = 0;
+        for (Object[] os : methodParams) {
+            classs[index] = (Class<?>) os[1];
+            index++;
+        }
+        return classs;
+    }
+
+    /**
+     * 获取参数值
+     *
+     * @param methodParams 参数相关列表
+     * @return 参数值列表
+     */
+    public static Object[] getMethodParamsValue(List<Object[]> methodParams) {
+        Object[] classs = new Object[methodParams.size()];
+        int index = 0;
+        for (Object[] os : methodParams) {
+            classs[index] = os[0];
+            index++;
+        }
+        return classs;
+    }
+}
+

QuartzDisallowConcurrentExecution

+
@DisallowConcurrentExecution
+public class QuartzDisallowConcurrentExecution extends AbstractQuartzJob {
+    @Override
+    protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception {
+        JobInvokeUtil.invokeMethod(sysJob);
+    }
+}
+

QuartzJobExecution

+
public class QuartzJobExecution extends AbstractQuartzJob {
+    @Override
+    protected void doExecute(JobExecutionContext context, SysJob sysJob) throws Exception {
+        JobInvokeUtil.invokeMethod(sysJob);
+    }
+}
+

ScheduleConstants

+
public class ScheduleConstants {
+    public static final String TASK_CLASS_NAME = "TASK_CLASS_NAME";
+
+    /**
+     * 执行目标key
+     */
+    public static final String TASK_PROPERTIES = "TASK_PROPERTIES";
+
+    /**
+     * 默认
+     */
+    public static final String MISFIRE_DEFAULT = "0";
+
+    /**
+     * 立即触发执行
+     */
+    public static final String MISFIRE_IGNORE_MISFIRES = "1";
+
+    /**
+     * 触发一次执行
+     */
+    public static final String MISFIRE_FIRE_AND_PROCEED = "2";
+
+    /**
+     * 不触发立即执行
+     */
+    public static final String MISFIRE_DO_NOTHING = "3";
+
+    public enum Status {
+        /**
+         * 正常
+         */
+        NORMAL("0"),
+        /**
+         * 暂停
+         */
+        PAUSE("1");
+
+        private final String value;
+
+        Status(String value) {
+            this.value = value;
+        }
+
+        public String getValue() {
+            return value;
+        }
+    }
+
+
+
+
+
+    /**
+     * http请求
+     */
+    public static final String HTTP = "http://";
+
+    /**
+     * https请求
+     */
+    public static final String HTTPS = "https://";
+
+    /**
+     * 通用成功标识
+     */
+    public static final String SUCCESS = "0";
+
+    /**
+     * 通用失败标识
+     */
+    public static final String FAIL = "1";
+
+
+    /**
+     * RMI 远程方法调用
+     */
+    public static final String LOOKUP_RMI = "rmi:";
+
+    /**
+     * LDAP 远程方法调用
+     */
+    public static final String LOOKUP_LDAP = "ldap:";
+
+    /**
+     * LDAPS 远程方法调用
+     */
+    public static final String LOOKUP_LDAPS = "ldaps:";
+
+    /**
+     * 定时任务白名单配置(仅允许访问的包名,如其他需要可以自行添加)
+     */
+    public static final String[] JOB_WHITELIST_STR = {"com.chenglian"};
+
+    /**
+     * 定时任务违规的字符
+     */
+    public static final String[] JOB_ERROR_STR = {"java.net.URL", "javax.naming.InitialContext", "org.yaml.snakeyaml",
+            "org.springframework", "org.apache", "com.chenglian.common.utils.file", "com.chenglian.common.config"};
+
+
+}
+

ScheduleUtils

+
public class ScheduleUtils {
+    /**
+     * 得到quartz任务类
+     *
+     * @param sysJob 执行计划
+     * @return 具体执行任务类
+     */
+    private static Class<? extends Job> getQuartzJobClass(SysJob sysJob) {
+        boolean isConcurrent = "0".equals(sysJob.getConcurrent());
+        return isConcurrent ? QuartzJobExecution.class : QuartzDisallowConcurrentExecution.class;
+    }
+
+    /**
+     * 构建任务触发对象
+     */
+    public static TriggerKey getTriggerKey(Long jobId, String jobGroup) {
+        return TriggerKey.triggerKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
+    }
+
+    /**
+     * 构建任务键对象
+     */
+    public static JobKey getJobKey(Long jobId, String jobGroup) {
+        return JobKey.jobKey(ScheduleConstants.TASK_CLASS_NAME + jobId, jobGroup);
+    }
+
+    /**
+     * 创建定时任务
+     */
+    public static void createScheduleJob(Scheduler scheduler, SysJob job) throws SchedulerException, TaskException {
+        Class<? extends Job> jobClass = getQuartzJobClass(job);
+        // 构建job信息
+        Long jobId = job.getJobId();
+        String jobGroup = job.getJobGroup();
+        JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(getJobKey(jobId, jobGroup)).build();
+
+        // 表达式调度构建器
+        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(job.getCronExpression());
+        cronScheduleBuilder = handleCronScheduleMisfirePolicy(job, cronScheduleBuilder);
+
+        // 按新的cronExpression表达式构建一个新的trigger
+        CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(jobId, jobGroup))
+                .withSchedule(cronScheduleBuilder).build();
+
+        // 放入参数,运行时的方法可以获取
+        jobDetail.getJobDataMap().put(ScheduleConstants.TASK_PROPERTIES, job);
+
+        // 判断是否存在
+        if (scheduler.checkExists(getJobKey(jobId, jobGroup))) {
+            // 防止创建时存在数据问题 先移除,然后在执行创建操作
+            scheduler.deleteJob(getJobKey(jobId, jobGroup));
+        }
+
+        // 判断任务是否过期
+        if (Objects.nonNull(CronUtils.getNextExecution(job.getCronExpression()))) {
+            // 执行调度任务
+            scheduler.scheduleJob(jobDetail, trigger);
+        }
+
+        // 暂停任务
+        if (job.getStatus().equals(ScheduleConstants.Status.PAUSE.getValue())) {
+            scheduler.pauseJob(ScheduleUtils.getJobKey(jobId, jobGroup));
+        }
+    }
+
+    /**
+     * 设置定时任务策略
+     */
+    public static CronScheduleBuilder handleCronScheduleMisfirePolicy(SysJob job, CronScheduleBuilder cb)
+            throws TaskException {
+        switch (job.getMisfirePolicy()) {
+            case ScheduleConstants.MISFIRE_DEFAULT:
+                return cb;
+            case ScheduleConstants.MISFIRE_IGNORE_MISFIRES:
+                return cb.withMisfireHandlingInstructionIgnoreMisfires();
+            case ScheduleConstants.MISFIRE_FIRE_AND_PROCEED:
+                return cb.withMisfireHandlingInstructionFireAndProceed();
+            case ScheduleConstants.MISFIRE_DO_NOTHING:
+                return cb.withMisfireHandlingInstructionDoNothing();
+            default:
+                throw new TaskException("The task misfire policy '" + job.getMisfirePolicy()
+                        + "' cannot be used in cron schedule tasks", TaskException.Code.CONFIG_ERROR);
+        }
+    }
+
+    /**
+     * 检查包名是否为白名单配置
+     *
+     * @param invokeTarget 目标字符串
+     * @return 结果
+     */
+    public static boolean whiteList(String invokeTarget) {
+        String packageName = StringUtils.substringBefore(invokeTarget, "(");
+        int count = StringUtils.countMatches(packageName, ".");
+        if (count > 1) {
+            return CharSequenceUtil.containsAnyIgnoreCase(invokeTarget, ScheduleConstants.JOB_WHITELIST_STR);
+        }
+        Object obj = SpringUtil.getBean(StringUtils.split(invokeTarget, ".")[0]);
+        String beanPackageName = obj.getClass().getPackage().getName();
+        return CharSequenceUtil.containsAnyIgnoreCase(beanPackageName, ScheduleConstants.JOB_WHITELIST_STR)
+                && !CharSequenceUtil.containsAnyIgnoreCase(beanPackageName, ScheduleConstants.JOB_ERROR_STR);
+    }
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/springboot-validator/index.html b/blog-site/public/posts/essays/springboot-validator/index.html new file mode 100644 index 00000000..7af2b139 --- /dev/null +++ b/blog-site/public/posts/essays/springboot-validator/index.html @@ -0,0 +1,1081 @@ + + + + + + + + + + + Validator参数校验 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Validator参数校验

+ 2023.07.01 +
+

常见参数校验

+

在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,最简单就是用if条件语句来判断,但是随着参数越来越多,业务越来越复杂,判断参数代码语句显得尤为冗长.

+

或者有些程序会将if封装起来,例如spring中的assert类,进行判断,但归根结底还是靠代码对接口参数一个一个校验,这样太繁琐了,而且如果参数太多代码可读性比较差.

+
@Slf4j
+@RequestMapping("/Chapter1")
+@RestController
+public class ValidatedCaseController1 {
+
+
+    @PostMapping("/case1")
+    public R<Boolean> case1(UserEntity1 userEntity){
+        String username = userEntity.getUsername();
+        if (CharSequenceUtil.isBlank(username)){
+            return R.failed("用户名不能为空");
+        }
+        return R.ok(true);
+    }
+
+
+    @PostMapping("/case2")
+    public R<Boolean> case2(UserEntity1 userEntity){
+        String username = userEntity.getUsername();
+        Assert.notNull(username,"用户名不能为空");
+        return R.ok(true);
+    }
+
+}
+

Validator框架入门

+

包括引入依赖,创建controller,创建校验实体,错误异常处理.

+

pom依赖

+

从springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入validation和web,而springboot-2.3之前的版本只需要引入 web 依赖就可以了.

+
    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-web</artifactId>
+    </dependency>
+    
+    <dependency>
+      <groupId>org.springframework.boot</groupId>
+      <artifactId>spring-boot-starter-validation</artifactId>
+    </dependency>
+

UserEntity1

+
/**
+ * 常用校验注解
+ * JSR提供的校验注解:
+ * @Null 被注释的元素必须为 null
+ * @NotNull 被注释的元素必须不为 null
+ * @AssertTrue 被注释的元素必须为 true
+ * @AssertFalse 被注释的元素必须为 false
+ * @Min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
+ * @Max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
+ * @DecimalMin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值
+ * @DecimalMax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值
+ * @Size(max=, min=)   被注释的元素的大小必须在指定的范围内
+ * @Digits (integer, fraction)     被注释的元素必须是一个数字,其值必须在可接受的范围内
+ * @Past 被注释的元素必须是一个过去的日期
+ * @Future 被注释的元素必须是一个将来的日期
+ * @Pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式
+ *
+ */
+@Data
+public class UserEntity1 {
+
+    @NotNull(message = "id不能为空")
+    private Integer id;
+
+    @NotBlank(message = "用户名不能为空")
+    private String username;
+
+    @Size(max = 10,min = 7,message = "密码位数必须在7-10位")
+    @NotBlank(message = "密码不能为空")
+    private String password;
+
+    @NotBlank(message = "企业名称不能为空")
+    private String enterpriseName;
+
+    @Null
+    private Integer contactId;
+
+    @Null
+    private BigDecimal money;
+
+    @Email(message = "电子邮箱不能为空")
+    private String email;
+
+    @NotBlank(message = "手机号不能为空")
+    private String mobile;
+
+}
+

ValidatedCaseController1

+
@Slf4j
+@RequestMapping("/Chapter1")
+@RestController
+public class ValidatedCaseController1 {
+
+
+    @PostMapping("/case3")
+    public R<Boolean> case3(@RequestBody @Validated UserEntity1 userEntity) {
+        log.info("userEntity=>{}", userEntity);
+        return R.ok(true);
+    }
+
+}
+

发起请求测试接口

+
POST http://localhost:8080/Chapter1/case3
+Content-Type: application/json
+
+{
+  "id": "1",
+  "username": "111",
+  "password": "123",
+  "enterpriseName": "1112",
+  "contactId": "",
+  "money": "",
+  "email": "",
+  "mobile": "123123"
+}
+

ValidatedExceptionAdvice

+
@Slf4j
+@RestControllerAdvice(basePackages = {"com.whitepure.demo.validated"})
+public class ValidatedExceptionAdvice {
+
+
+    /**
+     * json格式参数校验异常
+     */
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public R<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+        log.error(e.getMessage(), e);
+        return R.failed(Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
+    }
+
+
+    @ExceptionHandler(Exception.class)
+    public R<String> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
+        String requestUri = request.getRequestURI();
+        log.error("请求地址'{}',发生未知异常.", requestUri, e);
+        return R.failed(e.getMessage().length() > 1000 ? "系统错误" : e.getMessage());
+    }
+
+}
+

Validator框架进阶

+

包括自定义校验注解,validated分组校验,对象嵌套校验,表单格式校验.

+

UserEntity2

+
@Data
+public class UserEntity2 {
+
+    @NotNull(message = "id不能为空",groups = ValidGroup.Crud.Update.class)
+    @Null(groups = ValidGroup.Crud.Create.class,message = "id添加时必须为null")
+    private Integer id;
+
+    @NotBlank(message = "用户名不能为空",groups = {ValidGroup.Crud.Update.class, ValidGroup.Crud.Create.class})
+    private String username;
+
+    @Password(groups = {ValidGroup.Crud.Update.class, ValidGroup.Crud.Create.class})
+    private String password;
+
+    @NotBlank(message = "企业名称不能为空")
+    @Chinese(message = "企业名称只能为中文")
+    private String enterpriseName;
+
+    @Null(groups = {ValidGroup.Crud.Update.class, ValidGroup.Crud.Create.class})
+    private Integer contactId;
+
+    @NotNull(message = "日期不能为null")
+    @Past(message = "当前日期必须是过期的一个日期",groups = {ValidGroup.Crud.Update.class, ValidGroup.Crud.Create.class})
+    private Date yesterday;
+
+    @NotNull(message = "展示不能为null")
+    @AssertTrue(message = "展示字段必须为true",groups = {ValidGroup.Crud.Update.class, ValidGroup.Crud.Create.class})
+    private Boolean isShow;
+
+    @Email(message = "电子邮箱不合法",groups = {ValidGroup.Crud.Update.class, ValidGroup.Crud.Create.class})
+    @NotBlank(message = "电子邮箱不能为空")
+    private String email;
+
+    @Mobile(groups = {ValidGroup.Crud.Update.class, ValidGroup.Crud.Create.class})
+    private String mobile;
+
+    @Size(max = 3, min = 1, message = "图片为1-3张")
+    @NotNull(message = "图片不能为空")
+    private List<String> imgs;
+
+    @Valid
+    @NotNull(groups = {ValidGroup.Crud.Create.class, ValidGroup.Crud.Update.class}, message = "工作对象不能为空")
+    private Job job;
+
+    @Data
+    public static class Job {
+
+        @NotNull(groups = {ValidGroup.Crud.Update.class}, message = "工作id不能为空")
+        @Min(value = 1, groups = ValidGroup.Crud.Update.class)
+        private Long jobId;
+
+        @NotNull(groups = {ValidGroup.Crud.Create.class, ValidGroup.Crud.Update.class}, message = "工作名称不能为空")
+        @Length(min = 2, max = 10, groups = {ValidGroup.Crud.Create.class, ValidGroup.Crud.Update.class},message = "工作名称.2-10位之间")
+        private String jobName;
+
+        @NotNull(groups = {ValidGroup.Crud.Create.class, ValidGroup.Crud.Update.class}, message = "工作职务不能为空")
+        @Length(min = 2, max = 10, groups = {ValidGroup.Crud.Create.class, ValidGroup.Crud.Update.class},message = "工作职务.2-10位之间")
+        private String position;
+    }
+
+}
+

自定义校验注解

+
@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD,ElementType.PARAMETER})
+@Constraint(validatedBy = ChineseValidator.class)
+public @interface Chinese {
+
+    String[] value() default {};
+
+    String message() default "请输入中文";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+    String regexp() default ".*";
+
+}
+
+
+public class ChineseValidator implements ConstraintValidator<Chinese, String> {
+
+    @SneakyThrows
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        if (value == null || StringPool.EMPTY.equals(value)){
+            return false;
+        }
+        return Pattern.compile(PatternPool.CHINESE_PATTERN, Pattern.CASE_INSENSITIVE).matcher(value).matches();
+    }
+    
+}
+
@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD,ElementType.PARAMETER})
+@Constraint(validatedBy = MobileValidator.class)
+public @interface Mobile {
+
+    String[] value() default {};
+
+    String message() default "手机号不合法";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+    String regexp() default ".*";
+
+}
+
+
+public class MobileValidator implements ConstraintValidator<Mobile, String> {
+
+    @SneakyThrows
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext context) {
+        if (value == null || StringPool.EMPTY.equals(value)){
+            return false;
+        }
+        return Pattern.compile(PatternPool.MOBILE_PATTERN, Pattern.CASE_INSENSITIVE).matcher(value).matches();
+    }
+
+}
+
@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.FIELD,ElementType.PARAMETER})
+@Constraint(validatedBy = PasswordValidated.class)
+public @interface Password {
+
+    String[] value() default {};
+
+    String message() default "密码不合法.以字母开头,长度在6~18之间,只能包含字符、数字和下划线。";
+
+    Class<?>[] groups() default {};
+
+    Class<? extends Payload>[] payload() default {};
+
+    String regexp() default ".*";
+
+}
+
+public class PasswordValidated implements ConstraintValidator<Password, String> {
+
+    @Override
+    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
+        if (value == null || StringPool.EMPTY.equals(value)){
+            return false;
+        }
+        return Pattern.compile(PatternPool.PASSWORD_PATTERN, Pattern.CASE_INSENSITIVE).matcher(value).matches();
+    }
+
+}
+
public interface PatternPool {
+
+    String MOBILE_PATTERN = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(16[5,6])|(17[0-8])|(18[0-9])|(19[1、5、8、9]))\\d{8}$";
+
+    String CHINESE_PATTERN = "^[\\u4e00-\\u9fa5]{0,}$";
+
+    String PASSWORD_PATTERN = "^[a-zA-Z]\\w{5,17}$";
+}
+

ValidGroup

+
public interface ValidGroup extends Default {
+
+    interface Crud extends ValidGroup{
+        interface Create extends Crud{
+
+        }
+
+        interface Update extends Crud{
+
+        }
+
+        interface Query extends Crud{
+
+        }
+
+        interface Delete extends Crud{
+
+        }
+    }
+}
+

ValidatedCaseController2

+
@Slf4j
+@RequestMapping("/Chapter2")
+@RestController
+public class ValidatedCaseController2 {
+
+    @PostMapping("/case1")
+    public R<Boolean> case1(@RequestBody @Validated(ValidGroup.Crud.Create.class) UserEntity2 userEntity) {
+        log.info("userEntity=>{}", userEntity);
+        return R.ok(true);
+    }
+
+
+    @PostMapping("/case2")
+    public R<Boolean> case2(@RequestBody @Validated(ValidGroup.Crud.Update.class) UserEntity2 userEntity) {
+        log.info("userEntity=>{}", userEntity);
+        return R.ok(true);
+    }
+
+}
+

发起请求测试接口

+
POST http://localhost:8080/Chapter2/case1
+Content-Type: application/json
+
+{
+  "id": "",
+  "username": "111",
+  "password": "a1231123121",
+  "enterpriseName": "企业名称",
+  "contactId": "",
+  "email": "11123@qq.com",
+  "yesterday": "2023-06-30",
+  "isShow": "true",
+  "mobile": "18830281211"
+}
+

ValidatedCaseController3

+

表单格式参数校验

+
@Slf4j
+@RequestMapping("/Chapter3")
+@RestController
+@Validated
+public class ValidatedCaseController3 {
+
+
+    @PostMapping("/case1")
+    public R<Boolean> case1(
+            @NotBlank(message = "用户名称不能为空") String username,
+            @NotBlank(message = "密码不能为空") String password,
+            @NotNull(message = "联系人id不能为空") Integer cid
+    ) {
+        log.info("username=>{}", username);
+        return R.ok(true);
+    }
+
+}
+

发起请求测试接口

+
POST http://localhost:8080/Chapter3/case1
+

ValidatedExceptionAdvice

+
@Slf4j
+@RestControllerAdvice(basePackages = {"com.whitepure.demo.validated"})
+public class ValidatedExceptionAdvice {
+
+    /**
+     * json格式参数校验异常
+     */
+    @ExceptionHandler(MethodArgumentNotValidException.class)
+    public R<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
+        log.error(e.getMessage(), e);
+        return R.failed(Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
+    }
+
+    /**
+     * 表单格式参数验证异常
+     */
+    @ExceptionHandler(ConstraintViolationException.class)
+    public R<String> handleMethodArgumentNotValidException(ConstraintViolationException e) {
+        log.error(e.getMessage(), e);
+        return R.failed(e.getMessage());
+    }
+
+    @ExceptionHandler(Exception.class)
+    public R<String> handleRuntimeException(RuntimeException e, HttpServletRequest request) {
+        String requestUri = request.getRequestURI();
+        log.error("请求地址'{}',发生未知异常.", requestUri, e);
+        return R.failed(e.getMessage().length() > 1000 ? "系统错误" : e.getMessage());
+    }
+
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/sql-select-fast/index.html b/blog-site/public/posts/essays/sql-select-fast/index.html new file mode 100644 index 00000000..881bc77a --- /dev/null +++ b/blog-site/public/posts/essays/sql-select-fast/index.html @@ -0,0 +1,2660 @@ + + + + + + + + + + + MySQL详解 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

MySQL详解

+ 2023.03.13 +
+

逻辑架构

+

主要分为:连接层,服务层,引擎层,存储层。

+

客户端执行一条select命令的流程如下:

+

MySqlSQL优化及锁机制-001

+
    +
  • +

    连接层:最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcplip的通信。主要完成一些类似于连接处理、授权认证、及相关的安全方案。在该层上引入了线程池的概念,为通过认证安全接入的客户端提供线程。同样在该层上可以实现基于SSL的安全链接。服务器也会为安全接入的每个客户端验证它所具有的操作权限。

    +
  • +
  • +

    服务层:第二层架构主要完成大多少的核心服务功能,如SQL接口,并完成缓存的查询,SQL的分析和优化及部分内置函数的执行。所有跨存储引擎的功能也在这一层实现,如过程、函数等。在该层,服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定查询表的顺序是否利用索引等,最后生成相应的执行操作。如果是select语句,服务器还会查询内部的缓存。如果缓存空间足够大,这样在解决大量读操作的环境中能够很好的提升系统的性能。

    +
  • +
  • +

    引擎层:存储引擎层,存储引擎真正的负责了MySQL中数据的存储和提取,服务器通过API与存储引擎进行通信。不同的存储引擎具有的功能不同这样我们可以根据自己的实际需要进行选取,后面介绍MyISAM和InnoDB。

    +
  • +
  • +

    存储层:数据存储层,主要是将数据存储在运行于裸设备的文件系统之上,并完成与存储引擎的交互。

    +
  • +
+

其中比较重要的就要说存储引擎了,MySQL默认使用的存储引擎是InnoDB,可以使用相关命令来查看:

+
 show engines;
+

如果查看当前使用的存储引擎可以使用命令:

+
 show variables like '%storage_engine%';
+

在创建数据库的时候可以指定存储引擎:

+
 CREATE TABLE tbl_emp (
+	id INT(11) NOT NULL AUTO_INCREMENT,
+	NAME VARCHAR(20) DEFAULT NULL,
+	deptId INT(11) DEFAULT NULL,
+	PRIMARY KEY (id)
+ )ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
+

SQL优化

+

参考文章:

+ +

为什么要进行优化?

+
    +
  • SQL语句欠佳、执行过程耗时较长、索引失效;
  • +
  • 单库数据量越来越大,存储出现问题;
  • +
  • 在高并发场景下,大量请求阻塞;
  • +
+

总之一句话,数据库出现性能瓶颈。

+

想要进行SQL优化首先要弄懂SQL编写过程和SQL执行过程是存在区别的:

+
    +
  • SQL编写过程:
  • +
+
 select distinct .. from .. join .. on .. where .. group by .. having .. order by .. limit ..
+
    +
  • SQL执行过程:
  • +
+
 from .. on .. join ..  where .. group by .. having .. select distinct .. order by .. limit ..
+

之所以SQL的编写过程会和执行过程有区别,是因为Mysql中有专门负责优化SELECT语句的优化器模块。

+
+

MySQL Query Optimizer:MySQL查询优化器,主要功能:通过计算分析系统中收集到的统计信息,为客户端请求的Query提供MySQL认为最优的执行计划。

+
+

当客户端向MySQL请求一条Query,命令解析器模块完成请求分类,区别出是SELECT并转发给MySQL Query Optimizer时,MySQL Query Optimizer首先会对整条Query进行优化,处理掉一些常量表达式的预算直接换算成常量值。 +并对Query中的查询条件进行简化和转换,如去掉一些无用或显而易见的条件、结构调整等。然后分析Query 中的 Hint信息(如果有),看显示Hint信息是否可以完全确定该Query的执行计划。如果没有Hint 或Hint信息还不足以完全确定执行计划,则会读取所涉及对象的统计信息,根据Query进行写相应的计算分析,然后再得出最后的执行计划。

+
+

Hint: 简单来说就是在某些特定的场景下人工协助MySQL优化器的工作,使其生成最优的执行计划。一般来说,优化器的执行计划都是最优化的,不过在某些特定场景下,执行计划可能不是最优化。

+
+

优化思路

+

数据库优化方案很多,主要分为两大类:软件层面、硬件层面。

+

软件层面包括:SQL 调优、表结构优化、读写分离、数据库集群、分库分表等;硬件层面主要是增加机器性能。对于后端程序员来说最主要的就是软件层面优化索引和分库分表了。

+
    +
  1. SQL调优主要目的是尽可能的让那些慢 SQL 变快,手段其实也很简单就是让 SQL 执行尽量命中索引:
  2. +
+
    +
  • 避免在 where 子句中对字段进行 null 值判断,创建表默认值是 NULL。尽量使用 NOT NULL,或使用特殊值,如 0、-1
  • +
  • 避免在 where 子句中使用 != 或 <> 操作符, MySQL 只有对以下操作符才使用索引:<、<=、=、>、>=、BETWEEN、IN、非 % 开头的 LIKE
  • +
  • 避免在 where 子句中使用 or 来连接条件,可以使用 UNION 进行连接
  • +
  • 能用 union all 就不用 union,union 过滤重复数据要耗费更多的 CPU 资源
  • +
  • 避免全部 like 查询,如 ‘%ConstXiong%’
  • +
  • 避免在索引列上使用计算、函数
  • +
  • in 和 not in 慎用,能用 between 不要用 in
  • +
  • select 子句中避免使用 *
  • +
  • 根据 where 和 order by 使用比较频繁的字段创建索引,提高查询效率
  • +
  • 索引不宜过多,单表最好不要超过 6 个。索引过多会导致占用存储空间变大;insert、update 变慢
  • +
  • 删除未使用的索引
  • +
+
    +
  1. 单表数据量太大。解决办法:
  2. +
+
    +
  • 分页查询(在索引上完成排序分页操作、借助主键进行关联)
  • +
  • 单表数据过大,进行分库分表
  • +
  • 考虑使用非关系型数据库提高查询效率
  • +
  • 全文索引场景较多,考虑使用 ElasticSearch、solr
  • +
+
    +
  1. 其他优化:
  2. +
+
    +
  • 数据库配置参数调整
  • +
  • 数据库表结构优化
  • +
  • 硬件优化,增加服务器数量,扩大服务器内存,CPU核数
  • +
+

分表

+ +

当数据库存储出现瓶颈的时候,就需要进行分库分表了。

+

分表主要是为了减少单张表的大小,解决单表数据量带来的性能问题。当单表数据增量过快,业界流传是超过500万的数据量就要考虑分表了。当然500万只是一个经验值,大家可以根据实际情况做出决策。

+

分表有几个维度,一是水平切分和垂直切分,二是单库内分表和多库内分表:

+
    +
  • +

    水平拆分:基于数据划分,表结构相同,数据不同。例如,现有一个业务表,新建业务表2,业务表与业务表2中字段一致,将ID为奇数的存储在业务表中,偶数的存放在业务表2中;

    +
  • +
  • +

    垂直拆分:基于表或字段划分,表结构不同。例如,将业务表中不常用的字段name和age存放到业务表2中;

    +
  • +
  • +

    单库内拆:先分库,将不同数据库中的表拆分为了多个子表,多个子表存在于同一数据库中; +MySqlSQL优化及锁机制-013

    +
  • +
  • +

    多库拆分: +MySqlSQL优化及锁机制-014

    +
  • +
+

在一个数据库中将一张表拆分为几个子表在一定程度上可以解决单表查询性能的问题,但是也会遇到一个问题:单数据库存储瓶颈。所以在业界用的更多的还是将子表拆分到多个数据库中。

+

分库分表问题:

+
    +
  • 跨库关联查询
  • +
  • 分布式事务
  • +
  • 排序、分页、函数计算问题
  • +
  • 分布式 ID
  • +
  • 多数据源
  • +
+

索引

+

MySQL官方对索引的定义为:索引(Index)是帮助MySQL高效获取数据的数据结构。可以得到索引的本质:索引是数据结构。相当于一本书的目录或者字典。

+

比如,要查“mysql”这个单词,我们肯定需要定位到m字母,然后从下往下找到y字母,再找到剩下的sql;如果没有索引,那么你可能需要逐个逐个寻找。你可以简单理解为,索引维护的就是一组排好序的数据。

+

我们平常所说的索引,如果没有特别指明,都是指B树结构组织的索引。当然,除了B+树这种类型的索引之外,还有哈稀索引等。

+

总结:索引,这种数据结构就是以空间换取时间。

+

索引的优缺点

+

索引的优势:

+
    +
  • 提高数据检索的效率,降低数据库的IO成本;
  • +
  • 降低数据排序的成本,降低了CPU的消耗;
  • +
+

索引的劣势:

+
    +
  • 索引本身很大,可以放在内存、硬盘中,通常为硬盘;
  • +
  • 索引不是所有情况都适用,所以在建立的时候要考虑: +
      +
    • 数据量很小则不适合建立;
    • +
    • 频繁更新的字段则不适合建立;
    • +
    • 很少使用的字段则不适合建立;
    • +
    • 某个字段包含许多重复的数据则不适合建立;
    • +
    +
  • +
  • 索引虽然提高了查询的效率,但是会降低增删改的效率;
  • +
+

索引分类及操作

+
    +
  • 单值索引:即一个索引只包含单个列,一个表可以有多个单列索引;
  • +
  • 唯一索引:索引列的值必须唯一,但允许有空值;
  • +
  • 复合索引:一个索引包含多个列;
  • +
  • 主键索引:属于唯一索引的一种,但是值不能为null;
  • +
+

创建索引:

+
 CREATE [UNIQUE] INDEX indexName ON mytable(columnName(length));
+ ALTER mytable ADD [UNIQUE] INDEX [indexName] ON (columnName(length));
+

删除索引:

+
 DROP INDEX [indexName] ON [tableName];
+

修改索引:

+
-- 该语句添加一个主键,这意味着索引值必须是唯一的,且不能为NULL
+ ALTER TABLE [tableName] ADD PRIMARY KEY (column_list);
+
+-- 这条语句创建索引的值必须是唯一的(除了NULL外,NULL可能会出现多次)
+ ALTER TABLE [tableName] ADD UNIQUE [indexName] (column_list);
+
+-- 添加普通索引,索引值可出现多次
+ ALTER TABLE [tableName] ADD INDEX [indexName] (column_list);
+
+-- 该语句指定了索引为FULLTEXT,用于全文索引
+ ALTER TABLE [tableName] ADD FULLTEXT [indexName] (column_list);
+

查看索引:

+
 SHOW INDEX FROM [tableName];
+

索引建立原则

+
    +
  • 不要建立太多的索引:因为维护索引是需要耗费空间和性能的,MySQl会为了每一个索引维护一个B树,如果索引过多,这无疑是增加了MySQL的负担;
  • +
  • 频繁增删改的字段不要建立索引:如果某个字段频繁修改,那就意味着也需要修改索引,这必然影响MySQL的性能;
  • +
  • 为频繁查询的字段建立索引:建立索引要为经常作为查询条件的字段建立索引,这样能提高SQL的查询效率;与之相反的不要为很少使用的字段建立索引;
  • +
  • 避免为”大字段”建立索引:就是尽量使用字段长度小的字段作为索引;例如,要为varchar(5)varchar(200)的字段建立索引,则优先考虑为varchar(5)的建立;如果非要为varchar(200)的字段建立索引那么可以这样: +
     -- varchar(200)的字段建立索引,将其长度设置为 20  
    + create index  tbl_001 on dual(address(20));
    +
  • +
  • 选择数据区分大的列建立索引:如果某个字段包含许多重复的数据则不适合建立索引。假设现在有一个”性别”字段,里面存放的数据的值要么是男,要么是女,这样的字段很不适合作为索引。因为如果值出现的几率几乎相等,那么无论搜索哪个值都可能得到一半的数据。在这些情况下,还不如不要索引,因为MySQL他还有一个查询优化器,查询优化器发现某个值出现在表的数据行中的百分比很高的时候,它一般会忽略索引,进行全表扫描。 +
    +

    惯用的百分比界线是”30%”。匹配的数据量超过一定限制的时候查询器会放弃使用索引,这也是索引失效的场景之一。

    +
    +
  • +
+

执行计划

+

使用explain关键字可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理你的SQL语句的。分析你的查询语句或是表结构的性能瓶颈。

+
+

explain不止适用于select也适用于delete、insert、replace和update语句。

+
+

当explain可解释语句一起使用时,MySQL会显示来自优化器的关于语句执行计划的信息。也就是说,MySQL解释了它将如何处理语句,包括有关表如何连接以及按顺序连接的信息。

+

在MySQL中执行任意一条SQL前加上explain关键字,即可看到:

+
 +----+-------------+----------+------+---------------+------+---------+------+------+-------+
+ | id | select_type | table    | type | possible_keys | key  | key_len | ref  | rows | Extra |
+ +----+-------------+----------+------+---------------+------+---------+------+------+-------+
+

id、table

+

table是显示这一行的数据是关于哪张表的。

+

id是select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序。

+
    +
  • id值相同,执行顺序由上至下;
  • +
  • id值不同,id值越大优先级越高,越先被执行;
  • +
+
+

表的执行顺序与表中数据息息相关,举个例子:表A:3条数据 表B:4条数据 表C:6条数据

+

假设在执行某条SQL的情况下表A、B、C的id值相同,假设执行顺序:表A、B、C。在给表B添加4条数据后,再次执行前面的这条SQL发现执行顺序变为:表A、C、B。 +
+这是因为中间结果会影响表的执行顺序: +3 * 4 * 2 = 12 * 2 = 24 +3 * 2 * 4 = 6 * 4 = 24 +虽然最后结果一样,但是中间过程不一样。中间过程越小占用的空间越小,所以在ID值相同的情况下数据小的表优先查询。

+
+

select_type

+

select_type是查询的类型,主要是用于区别普通查询、联合查询、子查询等的复杂查询。

+

查询类型:

+
    +
  • PRIMARY:包含自查询的主查询,最外层部分;
  • +
  • SUBQUERY:查询语句中包含了子查询,非最外层;
  • +
  • SIMPLE:简单查询,即SQL中不包含子查询或者UNION查询;
  • +
  • DERIVED:在查询过程中创建了临时表,例如SQL: +
     explain select name from (select * from table1) t1;
    +
  • +
  • UNION:若第二个SELECT出现在UNION之后,则被标记为UNION;若UNION包含在FROM子句的子查询中外层SELECT将被标记为:DERIVED; +
     explain select name from (select * from table1 union select * from table2) t1;
    +
  • +
  • UNION RESULT:从UNION表中的结果;
  • +
+

type

+

type是索引类型,是较为重要的一个指标,结果值从最好到最坏依次是:

+
 system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index >ALL
+

实际工作中可能用不到这么多,一般只要记住以下就行了:

+
 system > const > eq_ref > ref > range > index > ALL
+

其中system、const只是理想情况,实际能达到ref、range。

+

常见索引类型:

+
    +
  • system:表只有一行记录的系统表,或衍生表只有一行数据的主查询; +
     -- 衍生表:(select * from tbl_001) 只有一条记录;id为主键索引
    + explain select * from (select * from tbl_001) where id = 1;
    +
  • +
  • const:仅仅能查询到一条记录的SQL,并且用于主键索引或者唯一键索引; +
     -- tbl_001表只有一条记录;id为主键索引或唯一键索引
    + explain select * from tbl_001 where id = 1;
    +
  • +
  • eq_ref:唯一性索引;对于每个索引键的查询,返回匹配唯一一行的数据,有且只有一个,不能多也不能为0;常见于主键或唯一索引扫描; +
     -- tbl_001.tid为索引列;且tbl_001与tbl_002表中数据记录一一对应
    + select * from tbl_001,tbl_002 where tbl_001.tid = tbl_002.id
    +
  • +
  • ref:非唯一性索引;对于每个索引键的查询,返回多个匹配的所有行,包括0个; +
     -- name 为索引列;tbl_001 表中 name 值为 zs 的有多个
    + explain select * from tbl_001 where name = 'zs';
    +
  • +
  • range:检索指定范围的行,where后面是一个范围查询常见:between and、in、>、<=; +
     -- tid 为索引列;
    + explain select * from tbl_001 where tid <=3;
    +
  • +
  • index:查询全部索引中的数据; +
     -- name为索引列;
    + explain select name from tbl_001;
    +
  • +
  • all:查询全部表中的数据; +
     -- name不是索引列;
    + explain select name from tbl_001;
    +
  • +
+

possible_keys、key

+

possible_keys是可能用到的索引,是一种预测,不准确;key是实际用到的索引,如果为null则没有用到索引。

+
 -- name为索引列
+ explain select name from tbl_001;
+ +----+-------------+---------+-------+---------------+----------+---------+------+------+-------------+
+ | id | select_type | table   | type  | possible_keys | key      | key_len | ref  | rows | Extra       |
+ +----+-------------+---------+-------+---------------+----------+---------+------+------+-------------+
+ |  1 | SIMPLE      | tbl_001 | index | NULL          | idx_name | 83      | NULL |    4 | Using index |
+ +----+-------------+---------+-------+---------------+----------+---------+------+------+-------------+
+

key_len

+

key_len索引使用的长度,可通过索引列长度,例如:name vachar(20),来判断复合索引到底有没有使用。

+
 -- name为索引列;可以为nullvarchar类型utf8字符集
+ explain select name from tbl_001 name = 'zs';
+ +----+-------------+---------+-------+---------------+----------+---------+------+------+-------------+
+ | id | select_type | table   | type  | possible_keys | key      | key_len | ref  | rows | Extra       |
+ +----+-------------+---------+-------+---------------+----------+---------+------+------+-------------+
+ |  1 | SIMPLE      | tbl_001 | index | NULL          | idx_name | 63      | NULL |    4 | Using index |
+ +----+-------------+---------+-------+---------------+----------+---------+------+------+-------------+
+

其中,如果索引字段可以为null,MySQL会用一个字节进行标识;varchar为可变长度,MySQL用两个字节来标识。

+
+

utf8:一个字符三个字节; +gbk:一个字符两个字节; +latin:一个字符一个字节;

+
+

举例,字段name vachar(20),不能为null,utf8编码,执行执行计划name列使用到了索引那么key_len等于20*3+1+2=63

+

ref

+

ref指明当前表所引用的字段。

+
 -- 其中b.d可以为常量,常量用 const 标识
+ select ... where a.c = b.d;  
+
 -- tid 为索引列
+ explain select * from tbl_001 t1,tbl_002 t2 where t1.tid = t2.id and t1.name = 'zs';
+ +----+-------------+-------+--------+------------------+----------+---------+-----------------+------+-------------+
+ | id | select_type | table | type   | possible_keys    | key      | key_len | ref             | rows | Extra       |
+ +----+-------------+-------+--------+------------------+----------+---------+-----------------+------+-------------+
+ |  1 | SIMPLE      | t1    | ref    | idx_name,idx_tid | idx_name | 83      | const           |    1 | Using where |
+ |  1 | SIMPLE      | t2    | eq_ref | PRIMARY          | PRIMARY  | 4       | sql_demo.t1.tid |    1 | NULL        |
+ +----+-------------+-------+--------+------------------+----------+---------+-----------------+------+-------------+
+

rows

+

rows表示查询行数,实际通过索引查询到的个数。

+
 mysql> select * from tbl_001 t1,tbl_002 t2 where t1.tid = t2.id and t1.name = 'ls';
+ +----+------+------+----+------+-------+-------+
+ | id | name | tid  | id | name | name1 | name2 |
+ +----+------+------+----+------+-------+-------+
+ |  2 | ls   |    2 |  2 | ls   | ls1   | ls2   |
+ |  4 | ls   |    1 |  1 | zs   | zs1   | zs2   |
+ +----+------+------+----+------+-------+-------+
+
+ mysql> explain select * from tbl_001 t1,tbl_002 t2 where t1.tid = t2.id and t1.name = 'ls';
+ +----+-------------+-------+-------+------------------+------------+---------+----------------+------+-------------+
+ | id | select_type | table | type  | possible_keys    | key        | key_len | ref            | rows | Extra       |
+ +----+-------------+-------+-------+------------------+------------+---------+----------------+------+-------------+
+ |  1 | SIMPLE      | t2    | index | PRIMARY          | index_name | 249     | NULL           |    2 | Using index |
+ |  1 | SIMPLE      | t1    | ref   | idx_name,idx_tid | idx_tid    | 5       | sql_demo.t2.id |    1 | Using where |
+ +----+-------------+-------+-------+------------------+------------+---------+----------------+------+-------------+
+

Extra

+

Extra包含不适合在其他列中显示但十分重要的额外信息。

+

常见值:

+
    +
  • Using filesort:文件内排序;出现这个值代表额外的一次排序(查询),性能消耗比较大,需要进行SQL优化。常见order by语句中; +
     -- name 为单值索引
    + explain select * from tbl_001 where name = 'zs' order by tid;
    + +----+-------------+---------+------+---------------+----------+---------+-------+------+----------------+
    + | id | select_type | table   | type | possible_keys | key      | key_len | ref   | rows | Extra          |
    + +----+-------------+---------+------+---------------+----------+---------+-------+------+----------------+
    + |  1 | SIMPLE      | tbl_001 | ref  | idx_name      | idx_name | 83      | const |    1 | Using filesort |
    + +----+-------------+---------+------+---------------+----------+---------+-------+------+----------------+
    +
    对于单值索引来说,如果排序和查找的是同一个字段,则不会出现 Using filesort 反之则出现;一般通过where哪些字段就 orderby 哪些字段来避免。 +
     -- nametid 为复合索引
    + explain select * from tbl_001 where name = 'zs' order by tid;
    + +----+-------------+---------+------+---------------+-----------+---------+-------+------+-------------+
    + | id | select_type | table   | type | possible_keys | key       | key_len | ref   | rows | Extra       |
    + +----+-------------+---------+------+---------------+-----------+---------+-------+------+-------------+
    + |  1 | SIMPLE      | tbl_001 | ref  | idx_trans     | idx_trans | 83      | const |    1 | Using index |
    + +----+-------------+---------+------+---------------+-----------+---------+-------+------+-------------+
    +
    对于复合索引来说要按照建立复合索引的顺序来使用,不要跨列或者无序使用。 +
    +

    跨列使用指,where后边的字段和order by后边的字段拼接起来,看是否满足索引的顺序,如不满足则为跨列,会出现Using filsort。

    +
    +
  • +
  • Using temporary:使了用临时表保存中间结果,MysQL在对查询结果排序时使用临时表。性能消耗也是很大的,一般出现这个词需要进行SQL优化。常见于分组查询group by语句; +
     -- nametidcid 为复合索引
    + explain select name from tbl_001 where name = 'zs' group by cid;
    + +----+-------------+---------+------+---------------+---------+---------+-------+------+------------------------------+
    + | id | select_type | table   | type | possible_keys | key     | key_len | ref   | rows | Extra                        |
    + +----+-------------+---------+------+---------------+---------+---------+-------+------+------------------------------+
    + |  1 | SIMPLE      | tbl_001 | ref  | idx_ntc       | idx_ntc | 83      | const |    2 | Using index; Using temporary |
    + +----+-------------+---------+------+---------------+---------+---------+-------+------+------------------------------+
    +
    Using temporary出现的原因,已经有表了但是不使用,必须再来一张表。我们可以通过查询哪些列,就通过哪些列来分组来避免。
  • +
  • Using index:出现这个值代表性能还是不错的;表示使用到了覆盖索引,不读取源文件,只从索引文件中获取数据,不需要回表查询。 +
    +

    覆盖索引(Covering Index),一说为索引覆盖: 查询的数据列从索引文件中就能够取得,不必读取原数据行,MySQL可以利用索引返回select列表中的字段,而不必根据索引再次读取数据文件,换句话说查询到的列全部都在索引列中就是索引覆盖。 +如果要使用覆盖索引,一定要注意select列表中只取出需要的列,不可select*,因为如果将所有字段一起做索引会导致索引文件过大,查询性能下降。

    +
    +
     -- nametidcid 为复合索引
    + explain select name,tid,cid from tbl_001 where name = 'zs';
    + +----+-------------+---------+------+---------------+---------+---------+-------+------+-------------+
    + | id | select_type | table   | type | possible_keys | key     | key_len | ref   | rows | Extra       |
    + +----+-------------+---------+------+---------------+---------+---------+-------+------+-------------+
    + |  1 | SIMPLE      | tbl_001 | ref  | idx_ntc       | idx_ntc | 83      | const |    2 | Using index |
    + +----+-------------+---------+------+---------------+---------+---------+-------+------+-------------+
    +
    如果用到了索引覆盖时,会对possible_key和key造成影响: +
      +
    • 如果没有使用到where,则索引只出现在key中;
    • +
    • 如果有where,则索引出现在key和possible_key中;
    • +
    +
  • +
  • Using where:出现这个值代表需要回表查询; +
     -- name 为单值索引,此时索引中不包含其他字段,想要查询其他字段就必须回表查询
    + explain select name,tid,cid from tbl_001 where name = 'zs';
    + +----+-------------+---------+------+---------------+---------+---------+-------+------+-------------+
    + | id | select_type | table   | type | possible_keys | key     | key_len | ref   | rows | Extra       |
    + +----+-------------+---------+------+---------------+---------+---------+-------+------+-------------+
    + |  1 | SIMPLE      | tbl_001 | ref  | idx_name      | idx_name | 83     | const |   2  | Using where |
    + +----+-------------+---------+------+---------------+---------+---------+-------+------+-------------+
    +
  • +
  • Impossible WHERE:where语句永远为false,永远不成立; +
     explain select * from tbl_001 where name = 'zs' and name = 'ls';
    + +----+-------------+-------+------+---------------+------+---------+------+------+------------------+
    + | id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra            |
    + +----+-------------+-------+------+---------------+------+---------+------+------+------------------+
    + |  1 | SIMPLE      | NULL  | NULL | NULL          | NULL | NULL    | NULL | NULL | Impossible WHERE |
    + +----+-------------+-------+------+---------------+------+---------+------+------+------------------+
    +
  • +
+

SQL优化案例

+

单表优化

+
 -- 准备测试表、测试数据
+ create table book_2021(
+   bid int(4) primary key,
+   name varchar(20) not null,
+   authorid int(4) not null,
+   publicid int(4) not null,
+   typeid int(4) not null  
+ );
+ insert into book_2021 values(1,'java',1,1,2);
+ insert into book_2021 values(2,'php',4,1,2);
+ insert into book_2021 values(3,'c',1,2,2);
+ insert into book_2021 values(4,'c#',3,1,2);
+ insert into book_2021 values(5,'c++',3,1,2);
+

需要优化的SQL:

+
 -- SQL原型,优化该SQL 
+ select bid from book_2021 where typeid in(2,3) and authorid=1 order by typeid desc;
+

执行计划分析该SQL,发现该SQL存在Using filesort,type为ALL,需要进行SQL优化。

+
 explain select bid from book_2021 where typeid in(2,3) and authorid=1 order by typeid desc;
+ +----+-------------+-----------+------+---------------+------+---------+------+------+-----------------------------+
+ | id | select_type | table     | type | possible_keys | key  | key_len | ref  | rows | Extra                       |
+ +----+-------------+-----------+------+---------------+------+---------+------+------+-----------------------------+
+ |  1 | SIMPLE      | book_2021 | ALL  | NULL          | NULL | NULL    | NULL |    5 | Using where; Using filesort |
+ +----+-------------+-----------+------+---------------+------+---------+------+------+-----------------------------+
+
    +
  1. 根据SQL的执行过程可知,建立索引的顺序为typeid、authorid、bid; +
     -- sql执行顺序
    + from .. on .. join ..  where .. group by .. having .. select distinct .. order by .. limit ..
    +
     -- 建立索引
    + alter table book_2021 add index idx_tab(typeid,authorid,bid);
    +
  2. +
  3. 使用in的时候使索引失效,如果in失效那么后面的索引也将会失效,所以将范围条件放到最后; +
     -- 优化后的SQL
    + select bid from book_2021 where authorid=1 and typeid in(2,3) order by typeid desc;
    +
    将旧索引删除,创建字段顺序对应的索引。 +
    +

    温馨提示:索引一旦升级优化,需要删除索引防止干扰

    +
    +
     -- 删除 idx_tab 索引
    + drop index idx_tab on book_2021;
    +
    + -- 给表 book_2021 添加索引 idx_atb
    + alter table book_2021 add index idx_atb(authorid,typeid,bid);
    +
  4. +
+

最终优化后的SQL执行过程:

+
 explain select bid from book_2021 where authorid=1 and typeid in(2,3) order by typeid desc;
+ +----+-------------+-----------+------------+-------+---------------+---------+---------+------+------+----------+-----------------------------------------------+
+ | id | select_type | table     | partitions | type  | possible_keys | key     | key_len | ref  | rows | filtered | Extra                                         |
+ +----+-------------+-----------+------------+-------+---------------+---------+---------+------+------+----------+-----------------------------------------------+
+ |  1 | SIMPLE      | book_2021 | NULL       | range | idx_atb       | idx_atb | 8       | NULL |    3 |   100.00 | Using where; Backward index scan; Using index |
+ +----+-------------+-----------+------------+-------+---------------+---------+---------+------+------+----------+-----------------------------------------------+
+

单表优化总结:

+
    +
  • 最佳左前缀原则,保持索引的定义和使用顺序一致;
  • +
  • 索引需要逐步优化;
  • +
  • 将包含范围查询的条件放置到最后
  • +
+

两表优化

+
 -- 准备测试表、测试数据
+ create table teacher2(
+   tid int(4) primary key,
+   cid int(4) not null  
+ );
+
+ insert into teacher2 values(1,2);
+ insert into teacher2 values(2,1);
+ insert into teacher2 values(3,3);
+
+ create table course2(
+   cid int(4) not null,
+   cname varchar(20)
+ );
+
+ insert into course2 values(1,'java');
+ insert into course2 values(2,'phython');
+ insert into course2 values(3,'kotlin');
+

需要优化的SQL:

+
 -- SQL原型,优化该SQL 
+ select * from teacher2 t left outer join course2 c on t.cid = c.cid where c.cname = 'java';
+

执行过程分析该SQL,发现进行了全表扫描。

+
 explain select * from teacher2 t left outer join course2 c on t.cid = c.cid where c.cname = 'java';
+ +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
+ | id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                                      |
+ +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
+ |  1 | SIMPLE      | c     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where                                |
+ |  1 | SIMPLE      | t     | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    3 |    33.33 | Using where; Using join buffer (hash join) |
+ +----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+--------------------------------------------+
+

优化思路:

+
    +
  • 建立索引;在哪张表哪个字段建立索引?即小表驱动大表,索引应建立在经常使用的字段上,假设此时teacher表数据量少,那么本题应为t.cid、c.name字段加索引;
  • +
+
+

小表驱动大表:假设 小表10条数据;大表300条数据;让大小表做嵌套循环,无论10是外层循环还是300是外层循环,结果都是3000次,但是为了减少表连接创建的次数,应该将10,即小表放在外层循环这样效率会更高。

+

小表驱动大表原因: +现有两个表A与B ,表A有200条数据,表B有20万条数据;按照循环的概念举个例子: +小表驱动大表 > A驱动表,B被驱动表 + for(200条){ for(20万条){ ... } } +大表驱动小表 > B驱动表,A被驱动表 + for(20万条){ for(200条){ ... } } +总结: +如果小的循环在外层,对于表连接来说就只连接200次 ; +如果大的循环在外层,则需要进行20万次表连接,从而浪费资源,增加消耗 ; +小表驱动大表的主要目的是通过减少表连接创建的次数,加快查询速度。

+
+
 -- 给表teacher2、course2 添加索引
+ alter table teacher2 add index idx_teacher2_cid(cid);
+ alter table course2 add index idx_course2_cname(cname);
+

最终优化后的SQL:

+
 -- 添加索引后
+ explain select * from teacher2 t left outer join course2 c on t.cid = c.cid where c.cname = 'java';
+ +----+-------------+-------+------------+------+-------------------+-------------------+---------+----------------+------+----------+-------------+
+ | id | select_type | table | partitions | type | possible_keys     | key               | key_len | ref            | rows | filtered | Extra       |
+ +----+-------------+-------+------------+------+-------------------+-------------------+---------+----------------+------+----------+-------------+
+ |  1 | SIMPLE      | c     | NULL       | ref  | idx_course2_cname | idx_course2_cname | 83      | const          |    1 |   100.00 | Using where |
+ |  1 | SIMPLE      | t     | NULL       | ref  | idx_teacher2_cid  | idx_teacher2_cid  | 4       | sql_demo.c.cid |    1 |   100.00 | Using index |
+ +----+-------------+-------+------------+------+-------------------+-------------------+---------+----------------+------+----------+-------------+
+

多表优化总结:

+
    +
  • 小表驱动大表;
  • +
  • 索引建立在经常查询的字段上;
  • +
+

避免索引失效的一些原则

+
    +
  • 在使用复合索引时,不要跨列使用或无序使用,即最佳左前缀法则;否则索引失效; +
    +

    跨列使用指,where后边的字段和order by后边的字段拼接起来,看是否满足索引的顺序,如不满足则为跨列,会出现Using filsort。

    +
    +
  • +
  • 不要在索引上进行任何操作,例如:计算、函数运算、类型转换等;否则索引失效; +
     -- age列为索引,但此时SQL语句age列存在计算,age索引列失效;
    + select * from tbl_001 where age*3 = '10';
    +
    + -- name为索引列,此时在该索引列上进行函数计算,name索引列失效;
    + select * from tbl_001 tbl_001 left(name,4)='July'; 
    +
    + -- typeid为索引列,typeid为int类型,此时发生隐式类型转换,typeid索引列失效;
    + select * from tbl_001 c typeid = '1';
    +
  • +
  • 复合索引不能使用不等于(!=、<>)或is null、is not null,否则自身及右侧索引全部失效;
  • +
+
+

大部分情况下如果复合索引使用上面的运算符索引会失效,但是MySQL在服务层存在SQL优化阶段,所以复合索引即使使用了不等于、is null、is not null等操作,还是可能会存在索引生效的情况。

+
+
    +
  • 尽量使用覆盖索引,使用覆盖索引会提高系统性能; +
     -- name、tid、cid 为复合索引
    +  select name,tid,cid from tbl_001 where name = 'zs';
    +
  • +
  • like尽量以常量开头,不要以%开头,否则索引失效; +
     -- name 为索引列,此处索引失效
    + select * from tbl_002 where name like '%x%';
    +
    + -- 如果必须要使用%开头,可以使用覆盖索引挽救一下
    + select name from tbl_002 where name like '%x%';
    +
  • +
  • 不要使用or,否则索引失效; +
     -- authorid、typeid为复合索引,此处or的左右两边索引全部失效
    + select * from book_2021 where authorid = 1 or typeid = 1;
    +
  • +
  • 避免使用*,因为使用之后MySQL还需要计算把替换成对应的列; +
    -- 不要使用*,把*替换成具体需要的列
    + select * from book_2021;
    +
  • +
+

SQL优化方法

+

exist和in选择

+ +

两者可以互相替代,如果主查询的数据集大,使用in的效率高一点;如果子查询的数据集大,使用exist效率高一点;

+
 -- 假设此处主查询数据集较大,使用in
+ select * from tbl_001 where tid in (select id from tbl_001);
+
+  -- 假设此处子查询数据集较大,使用exist
+  select tid from tbl_001 where exists (select * from tbl_001);
+
+

exists语法:将主查询的结果,放到子查询结果中进行判断,看子查询中是否有数据,如果有数据则保留数据并返回;没有则返回空。

+
+

order by优化

+

order by 是比较常用的一个关键字,基本上查询出来的数据都要进行排序,否则就太乱了。使用order by的时候常常伴随着Using filesort的发生,Using filesort在底层有两种算法,根据IO的次数分为:

+
    +
  • 双路排序:MySQL4.1之前默认使用双路排序;具体执行流程,双路排序需要扫描两次磁盘上的字段,首先扫描排序的字段对字段进行排序,然后扫描其他字段;
  • +
  • 单路排序:为了减少IO访问次数在MySQL4.1之后默认使用单路排序;读取一次获取全部的字段,在buffer中进行排序。但是此种方法存在隐患不一定是只读一次数据,如果数据量过大buffer放不下则再需要读取文件,可以通过调节buffer来进行缓解; +
     -- 设置读取文件缓冲区(buffer)的大小,单位字节
    + set max_length_for_sort_data = 1024;
    +
  • +
+

如果需要排序的列的总大小超过了设置的max_length_for_sort_data那么MySQL底层会自动从单路排序切换到双路排序。

+

保证全部索引排序字段的一致性,都是升序或都是降序,不要部分升序部分降序。

+

排查SQL

+

在MySQL中可以通过抓去慢SQL日志来进行SQL排查,慢SQL日志超过响应时间的阈值默认10秒,便会记录该SQL;慢SQL日志默认是关闭的,建议在开发调优时打开,最终部署上线时关闭。

+

慢SQL日志设置

+

通过SQL来查看记录慢SQL日志是否开启:

+
 -- 查看变量 slow_query_log,记录慢查询日志默认关闭
+ show variables like '%slow_query_log%';
+ +---------------------+----------------------------------------------+
+ | Variable_name       | Value                                        |
+ +---------------------+----------------------------------------------+
+ | slow_query_log      | OFF                                          |
+ | slow_query_log_file | /usr/local/mysql/data/MacBook-Pro-2-slow.log |
+ +---------------------+----------------------------------------------+
+

可以通过SQL来进行开启记录慢SQL日志:

+
 -- 在内存中开启慢SQL记录日志 此种方式在 mysql 服务重启后失效
+ set global slow_query_log=1;
+ 
+ -- 查看是否开启慢SQL日志记录
+ show variables like '%slow_query_log%';
+ +---------------------+----------------------------------------------+
+ | Variable_name       | Value                                        |
+ +---------------------+----------------------------------------------+
+ | slow_query_log      | ON                                           |
+ | slow_query_log_file | /usr/local/mysql/data/MacBook-Pro-2-slow.log |
+ +---------------------+----------------------------------------------+
+
 -- 在MySQL my.cnf 配置文件中追加配置;重启MySQL服务后也生效,即永久开启
+ slow_query_log=1
+ slow_query_log_file=/usr/local/mysql/data/MacBook-Pro-2-slow.log
+

查看慢SQL记录日志阈值:

+
 -- 查看慢SQL记录日志阈值,默认10
+ show variables like '%long_query_time%';
+ +-----------------+-----------+
+ | Variable_name   | Value     |
+ +-----------------+-----------+
+ | long_query_time | 10.000000 |
+ +-----------------+-----------+
+

修改慢SQL记录日志阈值:

+
 -- 在内存中修改设置慢查询SQL记录日志阈值为5秒;注意设置后不会立即生效,需要重新登陆mysql客户端才会生效
+ set global long_query_time = 5;
+
+ -- 设置之后重新登陆mysql使用命令查看慢SQL查询日志阈值 
+ show variables like '%long_query_time%';
+ +-----------------+-----------+
+ | Variable_name   | Value     |
+ +-----------------+-----------+
+ | long_query_time | 5.000000 |
+ +-----------------+-----------+
+
 -- 在MySQL配置文件my.cnf中修改慢查询SQL记录日志阈值,设置为3秒
+ long_query_time=3
+

查看慢SQL日志

+
    +
  • 直接查看慢SQL日志; +
      -- 模拟慢查询SQL 
    +  select sleep(5);
    +
    +  -- 查看是否开启慢SQL日志记录
    +  show variables like '%slow_query_log%';
    +  +---------------------+----------------------------------------------+
    +  | Variable_name       | Value                                        |
    +  +---------------------+----------------------------------------------+
    +  | slow_query_log      | ON                                           |
    +  | slow_query_log_file | /usr/local/mysql/data/MacBook-Pro-2-slow.log |
    +  +---------------------+----------------------------------------------+
    +
    查看慢SQL日志记录cat /usr/local/mysql/data/MacBook-Pro-2-slow.log执行命令: +
     MacBook-Pro-2:~ root# cat /usr/local/mysql/data/MacBook-Pro-2-slow.log
    + /usr/local/mysql/bin/mysqld, Version: 8.0.22 (MySQL Community Server - GPL). started with:
    + Tcp port: 3306  Unix socket: /tmp/mysql.sock
    + Time                 Id Command    Argument
    + # Time: 2021-09-17T06:46:20.655775Z
    + # User@Host: root[root] @ localhost []  Id:    15
    + # Query_time: 4.003891  Lock_time: 0.000000 Rows_sent: 1  Rows_examined: 1
    + use sql_demo;
    + SET timestamp=1631861176;
    + select sleep(4);
    +
  • +
  • 通过mysqldumpslow工具来查看,可以通过一些过滤条件快速找到需要定位的慢SQL; +
    +

    语法:mysqldumpslow 各种参数 慢查询日志文件路径 +-s:排序方式 +-r:逆序反转 +-l:锁定时间,不要从总时间中减去锁定时间 +-g:在文件中查找考虑包含此字符串的 +更多指令解析查看 mysqldumpslow –help

    +
    +
     -- 获取返回记录最多的3条SQL
    + mysqldumpslow -s r -t 3 /usr/local/mysql/data/MacBook-Pro-2-slow.log
    +
    + -- 获取访问次数最多的3条SQL
    + mysqldumpslow -s r -t 3 /usr/local/mysql/data/MacBook-Pro-2-slow.log
    +
    + -- 按照时间排序,前10条包含 left join 的查询SQL 
    + mysqldumpslow -s t -t 10 -g "left join" /usr/local/mysql/data/MacBook-Pro-2-slow.log
    +
  • +
+

模拟数据并分析SQL

+
    +
  • +

    模拟数据;

    +
     -- 创建表
    + create database testdata;
    + use testdata;
    +
    + create table dept 
    + (
    + dno int(5) primary key default 0,
    + dname varchar(20) not null default '',
    + loc varchar(30) default ''
    + ) engine=innodb default charset=utf8;
    +
    + create table emp
    + (
    + eid int(5) primary key,
    + ename varchar(20) not null default '',
    + job varchar(20) not null default '',
    + deptno int(5) not null default 0
    + )engine=innodb default charset=utf8;
    +
     -- 创建存储过程:获取随机字符串
    + set global log_bin_trust_function_creators=1;
    + use testdata;
    + delimiter $
    + create function randstring(n int) returns varchar(255)
    + begin 
    +
    +     declare all_str varchar(100) default 'abcdefghijklmnopqrestuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    +     declare return_str varchar(255) default '';
    +     declare i int default 0;
    +     while i<n
    +     do
    +         set return_str=concat(return_str, substring(all_str, FLOOR(1+rand()*52), 1));
    +         set i=i+1;
    +     end while;
    +     return return_str;
    + end $
    +
     -- 创建存储函数:插入随机整数
    + use testdata;
    + create function ran_num() returns int(5)
    + begin 
    +
    + declare i int default 0;
    + set i=floor(rand()*100);
    + return i;
    +
    + end$
    +
     -- 创建存储过程:向emp表插入数据
    + create procedure insert_emp(in eid_start int(10), in data_times int(10))
    + begin 
    + declare i int default 0;
    + set autocommit =0;
    +
    + repeat 
    + insert into emp values(eid_start+i, randstring(5), 'other', ran_num());
    + set i=i+1;
    + until i=data_times
    + end repeat;
    +
    + commit;
    +
    + end $
    +
     -- 创建存储过程:向dept表插入数据
    + create procedure insert_dept(in dno_start int(10), in data_times int(10))
    + begin 
    + declare i int default 0;
    + set autocommit =0;
    +
    + repeat 
    + insert into dept values(dno_start+i, randstring(6), randstring(8));
    + set i=i+1;
    + until i=data_times
    + end repeat;
    +
    + commit;
    +
    + end $
    +
     -- 向emp、dept表中插入数据
    + delimiter ;
    + call insert_emp(1000, 800000);
    + call insert_dept(10, 30);
    +
     -- 验证插入数据量
    + select count(1) from emp;
    +
  • +
  • +

    使用show profile进行sql分析;

    +
     -- 查看 profiling 是否开启,默认是关闭的
    + show variables like 'profiling';
    + +---------------+-------+
    + | Variable_name | Value |
    + +---------------+-------+
    + | profiling     | OFF   |
    + +---------------+-------+
    +
    + -- 如果没开启将其开启
    + set profiling=on;
    +
  • +
  • +

    执行测试SQL,任意执行均可供之后SQL分析;

    +
    select * from emp;
    +
    +select * from emp group by eid order by eid;
    +
    +select * from emp group by eid limit 150000;
    +
    -- 执行命令,查看结果
    + show profiles;
    + +----------+------------+--------------------------------------------------+
    + | Query_ID | Duration   | Query                                            |
    + +----------+------------+--------------------------------------------------+
    + |        2 | 0.00300900 | show tables                                      |
    + |        3 | 0.01485700 | desc emp                                         |
    + |        4 | 0.00191200 | select * from emp group by eid%10 limit 150000   |
    + |        5 | 0.25516300 | select * from emp                                |
    + |        6 | 0.00026400 | select * from emp group by eid%10 limit 150000   |
    + |        7 | 0.00019600 | select eid from emp group by eid%10 limit 150000 |
    + |        8 | 0.03907500 | select eid from emp group by eid limit 150000    |
    + |        9 | 0.06499100 | select * from emp group by eid limit 150000      |
    + |       10 | 0.00031500 | select * from emp group by eid%20 order by 5     |
    + |       11 | 0.00009100 | select * from emp group by eid%20 order by       |
    + |       12 | 0.00012400 | select * from emp group by eid order by          |
    + |       13 | 0.25195300 | select * from emp group by eid order by eid      |
    + |       14 | 0.00196200 | show variables like 'profiling'                  |
    + |       15 | 0.26208900 | select * from emp group by eid order by eid      |
    + +----------+------------+--------------------------------------------------+
    +
  • +
  • +

    使用命令show profile cpu,block io for query 上一步前面执行 show profiles 的 Query_ID ;诊断SQL;

    +
    +

    参数备注,不区分大小写: +all:显示所有的开销信息; +block io:显示块lO相关开销; +context switches:上下文切换相关开销; +cpu:显示CPU相关开销信息; +ipc:显示发送和接收相关开销信息; +memory:显示内存相关开销信息; +page faults:显示页面错误相关开销信息; +source:显示和Source_function,Source_file,Source_line相关的开销信息; +swaps:显示交换次数相关开销的信息;

    +
    +
     show profile cpu,block io for query 3;
    + +----------------------------+----------+----------+------------+--------------+---------------+
    + | Status                     | Duration | CPU_user | CPU_system | Block_ops_in | Block_ops_out |
    + +----------------------------+----------+----------+------------+--------------+---------------+
    + | starting                   | 0.004292 | 0.000268 |   0.000941 |            0 |             0 |
    + | checking permissions       | 0.000026 | 0.000012 |   0.000013 |            0 |             0 |
    + | checking permissions       | 0.000007 | 0.000005 |   0.000003 |            0 |             0 |
    + | Opening tables             | 0.003883 | 0.000865 |   0.000880 |            0 |             0 |
    + | init                       | 0.000022 | 0.000013 |   0.000008 |            0 |             0 |
    + | System lock                | 0.000013 | 0.000012 |   0.000002 |            0 |             0 |
    + | optimizing                 | 0.000994 | 0.000078 |   0.000155 |            0 |             0 |
    + | statistics                 | 0.000233 | 0.000222 |   0.000011 |            0 |             0 |
    + | preparing                  | 0.000046 | 0.000043 |   0.000003 |            0 |             0 |
    + | Creating tmp table         | 0.000075 | 0.000073 |   0.000002 |            0 |             0 |
    + | executing                  | 0.001532 | 0.000141 |   0.000266 |            0 |             0 |
    + | checking permissions       | 0.000050 | 0.000044 |   0.000005 |            0 |             0 |
    + | checking permissions       | 0.000014 | 0.000012 |   0.000002 |            0 |             0 |
    + | checking permissions       | 0.001539 | 0.000102 |   0.000356 |            0 |             0 |
    + | checking permissions       | 0.000044 | 0.000037 |   0.000007 |            0 |             0 |
    + | checking permissions       | 0.000019 | 0.000016 |   0.000002 |            0 |             0 |
    + | checking permissions       | 0.001250 | 0.000089 |   0.000318 |            0 |             0 |
    + | end                        | 0.000015 | 0.000007 |   0.000008 |            0 |             0 |
    + | query end                  | 0.000006 | 0.000004 |   0.000001 |            0 |             0 |
    + | waiting for handler commit | 0.000041 | 0.000040 |   0.000002 |            0 |             0 |
    + | removing tmp table         | 0.000012 | 0.000010 |   0.000001 |            0 |             0 |
    + | waiting for handler commit | 0.000010 | 0.000009 |   0.000002 |            0 |             0 |
    + | closing tables             | 0.000019 | 0.000018 |   0.000001 |            0 |             0 |
    + | freeing items              | 0.000648 | 0.000081 |   0.000152 |            0 |             0 |
    + | cleaning up                | 0.000067 | 0.000047 |   0.000021 |            0 |             0 |
    + +----------------------------+----------+----------+------------+--------------+---------------+
    +
  • +
+

全局查询日志

+

不要在生产环境开启这个功能。

+

在配置MySQL文件my.cnf设置全局查询日志:

+
-- 开启全局查询日志
+general_log=1
+
+-- 记录日志文件的路径
+general_log_file=/path/logfile
+
+-- 设置输出格式为 FILE
+log_output=FILE
+
+-- 通过cat命令直接查看;如果为空则需要造数
+cat /var/lib/mysql/bigdata01.log;
+

在mysql客户端设置全局查询日志:

+
-- 查看是否开启全局查询日志
+show variables like '%general_log%';
+
+-- 开启全局查询日志
+set global general_log=1;
+
+-- 设置输出格式为 TABLE
+set global log_output='TABLE';
+
+-- 可在mysql库里的geneial_log表查看;如果为空则需要造数
+select * from mysql.general_log;
+

MySQL锁机制

+

在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库而言显得尤其重要,也更加复杂。 +简而言之,锁机制是解决因资源共享,而造成的并发问题。

+

MySQL中的锁按照对数据操作的类型分为:读锁即共享锁,写锁即互斥锁;按照操作粒度来分,分为表锁、行锁,页锁。

+
    +
  • 读锁:对于同一个数据,多个读操作可以同时进行,互不干扰。
  • +
  • 写锁:如果当前写操作没有完毕,则无法进行其他的读操作、写操作。
  • +
  • 表锁:一次性对一张表整体加锁。MyISAM存储引擎偏向表锁,开销较小、加锁较快、无死锁、锁的范围较大,但是容易发生发生锁冲突,并发度低。
  • +
  • 行锁:一次性对一条数据加锁。InnoDB存储引擎偏向行锁,开销较大、加锁较慢、容易出现死锁、锁的范围较小,但是不易发生锁冲突,并发度高。
  • +
+

表锁

+

测试数据:

+
 create table mylock (
+     id int not null primary key auto_increment,
+     name varchar(20) default ''
+ ) engine myisam;
+ 
+ insert into mylock(name) values('a');
+ insert into mylock(name) values('b');
+ insert into mylock(name) values('c');
+ insert into mylock(name) values('d');
+ insert into mylock(name) values('e');
+

常用命令:

+
 -- 加锁
+ lock table 表名1 read/write, 表名2 read/write, 其他;
+ 
+ -- 解锁
+ unlock tables;
+
+ -- 查看表上是否加锁;在In_use列,0代表没有加锁,1代表加锁
+ show open tables;
+ 
+ -- 分析表锁
+ -- Table_locks_immediate:产生表级锁定的次数,表示可以立即获取锁的查询次数,每立即获取锁值加1
+ -- Table_locks_waited:出现表级锁定争用而发生等待的次数(不能立即获取锁的次数,每等待一次锁值加1),此值高则说明存在着较严重的表级锁争用情况
+ show status like 'table_locks%';
+

+

给表添加读锁及测试读操作:

+

MySqlSQL优化及锁机制-002

+

MySqlSQL优化及锁机制-004

+

给表添加读锁及测试写操作:

+

MySqlSQL优化及锁机制-003

+

MySqlSQL优化及锁机制-005

+

给某个表添加读锁之后,所有会话都能对这个表进行读操作,但是当前会话不能对该表进行写操作,对其他表进行读、写操作;其他会话能需要等持有锁的会话释放该表的锁后,才能进行写操作,期间将会一直处于等待状态,可以对其他表进行读写操作。

+
+

给表添加写锁及测试读操作:

+

MySqlSQL优化及锁机制-006

+

MySqlSQL优化及锁机制-007

+

给表添加写锁及测试写操作:

+

MySqlSQL优化及锁机制-008

+

MySqlSQL优化及锁机制-009

+

给某个表添加写锁之后,当前会话可以对该表进行写操作、读操作,其他会话要想对该表进行读写操作需要等持有锁的会话释放锁,否则期间将会一直等待该锁释放;但是当前持有锁的会话是不能对其他表进行读写操作,其他会话能够对其他表进行读写操作。

+

对于表锁MyISAM在执行查询语句前,会自动给涉及的所有表加读锁,在执行增删改操作前,会自动给涉及的表加写锁。

+
    +
  • 对MyISAM表的读操作(加读锁),不会阻塞其他进程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
  • +
  • 对MyISAM表的写操作(加写锁),会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作。
  • +
+

简而言之,就是读锁会阻塞写,但是不会堵塞读。而写锁则会把读和写都堵塞。

+

行锁

+

测试数据:

+
 CREATE TABLE test_innodb_lock (a INT(11),b VARCHAR(16))ENGINE=INNODB;
+ 
+ INSERT INTO test_innodb_lock VALUES(1,'b2');
+ INSERT INTO test_innodb_lock VALUES(3,'3');
+ INSERT INTO test_innodb_lock VALUES(4, '4000');
+ INSERT INTO test_innodb_lock VALUES(5,'5000');
+ INSERT INTO test_innodb_lock VALUES(6, '6000');
+ INSERT INTO test_innodb_lock VALUES(7,'7000');
+ INSERT INTO test_innodb_lock VALUES(8, '8000');
+ INSERT INTO test_innodb_lock VALUES(9,'9000');
+ INSERT INTO test_innodb_lock VALUES(1,'b1');
+ 
+ CREATE INDEX test_innodb_a_ind ON test_innodb_lock(a);
+ CREATE INDEX test_innodb_lock_b_ind ON test_innodb_lock(b);
+ 
+

常用命令:

+
 -- 分析行锁定:
+ -- 尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待,然后根据分析结果着手指定优化计划。
+ -- Innodb_row_lock_current_waits:当前正在等待锁定的数量;
+ -- Innodb_row_lock_time:从系统启动到现在锁定总时间长度;
+ -- Innodb_row_lock_time_avg:每次等待所花平均时间;
+ -- Innodb_row_lock_time_max:从系统启动到现在等待最常的一次所花的时间;
+ -- Innodb_row_lock_waits:系统启动后到现在总共等待的次数;
+ -- show status like 'innodb_row_lock%';
+

由于行锁与事务相关,所以在测试之前需要关闭MySQL的自动提交:

+
 -- 关闭自动提交;0关闭,1打开
+ set autocommit = 0;
+

测试行锁读写操作:

+

MySqlSQL优化及锁机制-010

+

MySqlSQL优化及锁机制-011

+

当前会话当关闭MySQL自动提交后,修改表中的某一行数据,未提交前其他会话是不可见该修改的数据的;如果当前会话和修改某一行数据其他会话也修改该行数据则其他会话会一直等待,直到持有锁的会话commit后才会执行。 +如果当前会话和其他会话操作同一张表的不同行数据时,则相互不影响。

+

使用行锁时注意,无索引行或索引失效都会导致行锁变为表锁。

+

间隙锁:

+

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加锁,对于键值在条件范围内但并不存在的记录,叫做“间隙”。 +InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。

+

因为Query执行过程中通过过范围查找的话,他会锁定整个范围内所有的索引键值,即使这个键值并不存在。

+

间隙锁有一个比较致命的弱点,就是当锁定一个范围键值之后,即使某些不存在的键值也会被无辜的锁定,而造成在锁定的时候无法插入锁定键值范围内的任何数据。在某些场景下这可能会对性能造成很大的危害。

+

如何锁定一行?

+

在查询语句使用for update关键字:

+
 select * from test_innodb_lock where a=8 for update;
+

MySqlSQL优化及锁机制-012

+

Innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更高一些,但是在整体并发处理能力方面要远远优于MyISAM的表级锁定的。当系统并发量较高的时候,Innodb的整体性能和MylISAM相比就会有比较明显的优势了。 +但是,Innodb的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让Innodb的整体性能表现不仅不能比MyISAM高,甚至可能会更差。

+

总结

+
    +
  • MySQL锁分为全局锁、表级锁以及行级锁,不同的存储引擎支持锁的粒度有所不同,MyISAM 只支持到表级锁,InnoDB 则可以支持到行级锁,锁的粒度决定了业务的并发度,因此更推荐使用InnoDB
  • +
  • InnoDB默认最小加锁粒度为行级锁,并且锁是加在索引上,如果SQL语句未命中索引,则走聚簇索引的全表扫描,表上每条记录都会上锁,导致并发能力下降,增大死锁的概率,因此需要为表合理的添加索引,线上查询尽量命中索引
  • +
  • 行级锁默认加 next-key lock,而根据不同的索引也有不同的加锁规则,我们可以根据加锁规分析加锁区间
  • +
  • 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
  • +
  • 合理设计索引,尽量缩小锁的范围
  • +
  • 尽可能较少检索条件,避免间隙锁
  • +
  • 尽量控制事务大小,减少锁定资源量和时间长度
  • +
  • 尽可能低级别事务隔离
  • +
+

分库分表

+

分库分表是两回事儿,可别搞混了,可能是光分库不分表,也可能是光分表不分库,都有可能。分库分表一定是为了支撑高并发、数据量大两个问题的。

+

分库分表场景: +假设你现在在一个小公司,注册用户就 20 万,每天活跃用户就 1 万,每天单表数据量就 1000,然后高峰期每秒钟并发请求最多就 10 个。随着公司业务发展,过了几年,注册用户数达到了 5000 万,每天活跃用户数 200 万,每天单表数据量 50 万条,数据库磁盘容量不断消耗掉,高峰期并发达到惊人的 5000~8000,这时候你会发现你得系统已经支撑不住了已经挂掉了。 其实在挂掉之前就应该考虑一个数据量得增长,考虑分库分表。

+

一句话概括,随着业务的发展,数据库会遇到瓶颈,单表得数据量太大,会极大影响你的 sql 执行的性能,到了后面你的 sql 可能就跑的很慢了。所以为了维持功能的正常使用不得不分库分表。

+

分表: +分表就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在 200 万以内。

+

分库: +分库是啥意思?就是你一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。

+

常见分库分表中间件

+

Cobar: +阿里 b2b 团队开发和开源的,属于 proxy 层方案,就是介于应用服务器和数据库服务器之间。应用程序通过 JDBC 驱动访问 Cobar 集群,Cobar 根据 SQL 和分库规则对 SQL 做分解,然后分发到 MySQL 集群不同的数据库实例上执行。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。

+

TDDL: +淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多,因为还依赖淘宝的 diamond 配置管理系统。

+

Atlas: +360 开源的,属于 proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。

+

Sharding-jdbc: +当当开源的,属于 client 层方案,是 ShardingSphere 的 client 层方案, ShardingSphere 还提供 proxy 层的方案 Sharding-Proxy。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且截至 2019.4,已经推出到了 4.0.0-RC1 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也可以选择的方案。

+

Mycat: +基于 Cobar 改造的,属于 proxy 层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 Sharding jdbc 来说,年轻一些,经历的锤炼少一些。

+

综上,现在其实建议考量的,就是 Sharding-jdbc 和 Mycat,这两个都可以去考虑使用。无论分库还是分表,上面说的那些数据库中间件都是可以支持的。就是基本上那些中间件可以做到你分库分表之后,中间件可以根据你指定的某个字段值,比如说 userid,自动路由到对应的库上去,然后再自动路由到对应的表里去。

+

Sharding-jdbc 这种 client 层方案的优点在于不用部署,运维成本低,不需要代理层的二次转发请求,性能很高,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个系统都需要耦合 Sharding-jdbc 的依赖;

+

Mycat 这种 proxy 层方案的缺点在于需要部署,自己运维一套中间件,运维成本高,但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了。

+

通常来说,这两个方案其实都可以选用,但是建议中小型公司选用 Sharding-jdbc,client 层方案轻便,而且维护成本低,不需要额外增派人手,而且中小型公司系统复杂度会低一些,项目也没那么多;但是中大型公司最好还是选用 Mycat 这类 proxy 层方案,因为可能大公司系统和项目非常多,团队很大,人员充足,那么最好是专门弄个人来研究和维护 Mycat,然后大量项目直接透明使用即可。

+

拆分方案

+

拆分方案一般分为两种一种是水平拆分,一种是垂直拆分。

+

水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来扛更高的并发,还有就是用多个库的存储容量来进行扩容。

+

垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表,或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。

+

除了垂直和水平拆分外,还有比较流行的分库分表方式,一种是按照range(范围)来分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了;另一种是按照某个字段的hash值,一下均匀分散。

+

range:好处在于扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。

+

hash:好处在于可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表。

+

迁移方案

+

现在有一个未分库分表的系统,未来要分库分表,如何设计才可以让系统从未分库分表动态切换到分库分表上?

+

方案一:停机迁移方案

+
    +
  1. 网站或者 app 发布公告,0 点到早上 6 点进行运维,无法访问;
  2. +
  3. 到时间将系统停掉,执行导数工具,将单库单表的数据读出来,写到分库分表里面去;
  4. +
  5. 导数完之后,需要改改配置,验证一下数据啥的;
  6. +
+

方案二:双写迁移方案

+
    +
  1. 在线上系统里面,之前所有写库的地方,增删改操作,除了对老库增删改,都加上对新库的增删改;
  2. +
  3. 重新部署系统,新数据库数据差太多,就写一个导数工具,跑起来读老库数据写新库,跑的时候要注意,老数据不要覆盖掉新数据;
  4. +
  5. 假设导完数据了,有可能数据还是存在不一致,那么就程序自动做一轮校验,比对新老库每个表的每条数据,就针对那些不一样的,从老库读数据再次写。反复循环,直到两个库每个表的数据都完全一致为止;
  6. +
+

id主键问题

+

分库分表之后你必然要面对的一个问题,就是 id 咋生成?因为要是分成多个表之后,每个表都是从 1 开始累加,那肯定不对啊,需要一个全局唯一的 id 来支持。

+

分库分表之后需要的id条件: 自增(最好),唯一

+

利用数据库自增id

+

往一个库的一个表里插入一条没什么业务含义的数据,获取一个数据库自增的一个 id,拿到这个 id 之后再往对应的分库分表里去写入。缺点是高并发存在瓶颈;优点是简单方便;适用于并发不高,但是数据量太大导致的分库分表扩容,可能每秒最高并发最多就几百,那么就走单独的一个库和表生成自增主键即可。

+

利用Redis自增

+

基于对数据库自增的改进,原理就是利用redis的incr命令实现ID的原子性自增。不依赖于数据库,灵活方便,且性能优于数据库,但是如果系统中没有Redis,还需要引入新的组件,会增加系统复杂度。

+

利用UUID作为主键

+

UUID一般不会作为主键使用,因为UUID 太长了、占用空间大,作为主键性能太差了,且UUID 不具有有序性,会导致 B+ 树索引在写的时候有过多的随机写操作,总之就是无序的性能开销大;适合于随机生成个什么文件名、编号之类的。

+

利用系统当前时间

+

一般如果用这个方案,是将当前时间跟很多其他的业务字段拼接起来,作为一个 id,如果业务上你觉得可以接受,那么也是可以的。你可以将别的业务字段值跟当前时间拼接起来,组成一个全局唯一的编号。

+

雪花算法

+

算法思路是是把一个 64 位的 long 型的 id,1 个 bit 是不用的,用其中的 41 bits 作为毫秒数,用 10 bits 作为工作机器 id,12 bits 作为序列号。

+
    +
  • 1 bit:不用,为啥呢?因为二进制里第一个 bit 为如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0;
  • +
  • 41 bits:表示的是时间戳,单位是毫秒。41 bits 可以表示的数字多达 2^41 - 1 ,也就是可以标识 2^41 - 1 个毫秒值,换算成年就是表示 69 年的时间;
  • +
  • 10 bits:记录工作机器 id,代表的是这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。但是 10 bits 里 5 个 bits 代表机房 id,5 个 bits 代表机器 id。意思就是最多代表 2^5 个机房(32 个机房),每个机房里可以代表 2^5 个机器(32 台机器);
  • +
  • 12 bits:这个是用来记录同一个毫秒内产生的不同 id,12 bits 可以代表的最大正整数是 2^12 - 1 = 4096 ,也就是说可以用这个 12 bits 代表的数字来区分同一个毫秒内的 4096 个不同的 id;
  • +
+

雪花算法相对来说还是比较靠谱的,毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的,能达到百万计QPS。但是雪花算法强依赖时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

+

为了规避雪花算法的缺点,一些国内的大厂做了改进,像美团的Leaf,百度的uid-generator,都是基于雪花算法来实现的。

+

所以你要真是搞分布式 id 生成,如果是高并发啥的,那么用这个应该性能比较好,一般每秒几万并发的场景,也足够用了。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/thread-state-and-created/index.html b/blog-site/public/posts/essays/thread-state-and-created/index.html new file mode 100644 index 00000000..0b5e594b --- /dev/null +++ b/blog-site/public/posts/essays/thread-state-and-created/index.html @@ -0,0 +1,964 @@ + + + + + + + + + + + 线程状态及创建方式 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

线程状态及创建方式

+ 2020.04.20 +
+

线程状态及转换

+

线程状态共包含6种,6中状态又可以互相的转换。

+

线程状态转换

+
    +
  • 新建状态(New): 创建了线程后尚未启动;
  • +
  • 可运行状态(Runnable): 可能正在运行,也可能正在等待 CPU 时间片。包含了运行中(Running)和 就绪(Ready)状态; +
      +
    • 就绪(Ready):线程对象创建后,其他线程(比如main线程)调用了该对象的 start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中并分配cpu使用权 。
    • +
    • 运行中(Runing):就绪的线程获得了cpu 时间片,开始执行程序代码。
    • +
    +
  • +
  • 阻塞状态(Blocked): 等待获取一个排它锁,如果其线程释放了锁就会结束此状态;
  • +
  • 无限期等待(Wating): 等待其它线程显式地唤醒,否则不会被分配 CPU 时间片;
  • +
  • 限期等待(Timed Wating): 无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒;
  • +
  • 死亡(Terminated): 可以是线程结束任务之后自己结束,或者产生了异常而结束。 +``
  • +
+
+

补充: +睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。

+
    +
  • 调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
  • +
  • 调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
  • +
  • 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁,而等待是主动的,通过调用 Thread.sleep()Object.wait() 等方法进入等待。
  • +
+
+

创建线程

+

在Java中,创建一个线程,有且仅有一种方式: 创建一个Thread类实例,并调用它的start方法。

+

Thread类

+

通过继承Thread类,重写run()方法来创建线程。

+
public class MainTest {
+    public static void main(String[] args) {
+        ThreadDemo thread1 = new ThreadDemo();
+        thread1.start();
+    }
+}
+class ThreadDemo extends Thread {
+    @Override
+    public void run() {
+        System.out.printf("通过继承Thread类的方式创建线程,线程 %s 启动",Thread.currentThread().getName());
+    }
+}
+

Runnable接口

+

实现 Runnale 接口,将它作为 target 参数传递给 Thread 类构造函数的方式创建线程。

+
public class MainTest {
+    public static void main(String[] args) {
+        new Thread(() -> {
+            System.out.printf("通过实现Runnable接口的方式,重写run方法创建线程;线程 %s 启动",Thread.currentThread().getName());
+        }).start();
+    }
+}
+

Callable接口

+

通过实现 Callable接口,来创建一个带有返回值的线程。

+

在Callable执行完之前的这段时间,主线程可以先去做一些其他的事情,事情都做完之后,再获取Callable的返回结果。可以通过isDone()来判断子线程是否执行完。

+
public class MainTest {
+    public static void main(String[] args) throws ExecutionException, InterruptedException {
+        FutureTask<String> futureTask = new FutureTask<>(() -> {
+            System.out.printf("通过实现Callable接口的方式,重写call方法创建线程;线程 %s 启动", Thread.currentThread().getName());
+            System.out.println();
+            Thread.sleep(10000);
+            return "我是call方法返回值";
+        });
+        new Thread(futureTask).start();
+
+        System.out.println("主线程工作中 ...");
+        String callRet = null;
+        while (callRet == null){
+            if(futureTask.isDone()){
+                callRet = futureTask.get();
+            }
+            System.out.println("主线程继续工作 ...");
+        }
+        System.out.println("获取call方法返回值:"+ callRet);
+    }
+}
+

线程池

+

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。

+

它的主要特点为:线程复用,控制最大并发数,管理线程。

+

优点:

+
    +
  • 降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
  • +
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • +
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • +
+

常用方式

+

通过Executors线程池工具类来使用:

+
    +
  • Executors.newSingleThreadExecutor():创建只有一个线程的线程池
  • +
  • Executors.newFixedThreadPool(int):创建固定线程的线程池
  • +
  • Executors.newCachedThreadPool():创建一个可缓存的线程池,线程数量随着处理业务数量变化
  • +
+

这三种常用创建线程池的方式,底层代码都是用ThreadPoolExecutor创建的。

+
SingleThreadExecutor
+
    +
  • 使用Executors.newSingleThreadExecutor()创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
  • +
  • newSingleThreadExecutorcorePoolSizemaximumPoolSize 都设置为1,它使用的 LinkedBlockingQueue
  • +
+

源代码

+
    public static ExecutorService newSingleThreadExecutor() {
+        return new FinalizableDelegatedExecutorService
+            (new ThreadPoolExecutor(1, 1,
+                                    0L, TimeUnit.MILLISECONDS,
+                                    new LinkedBlockingQueue<Runnable>()));
+    }
+

代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        ExecutorService executor1 = null;
+        try {
+            executor1 = Executors.newSingleThreadExecutor();
+            for (int i = 1; i <= 10; i++) {
+                executor1.execute(() -> {
+                    System.out.println(Thread.currentThread().getName() + "执行了");
+                });
+            }
+        } finally {
+            executor1.shutdown();
+        }
+    }
+}
+
FixedThreadPool
+
    +
  • 使用Executors.newFixedThreadPool(int)创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
  • +
  • newFixedThreadPool 创建的线程池 corePoolSizemaximumPoolSize 值是相等的,它使用的 LinkedBlockingQueue
  • +
+

源代码

+
    public static ExecutorService newFixedThreadPool(int nThreads) {
+        return new ThreadPoolExecutor(nThreads, nThreads,
+                                      0L, TimeUnit.MILLISECONDS,
+                                      new LinkedBlockingQueue<Runnable>());
+    }
+

代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        ExecutorService executor1 = null;
+        try {
+            executor1 = Executors.newFixedThreadPool(10);
+            for (int i = 1; i <= 10; i++) {
+                executor1.execute(() -> {
+                    System.out.println(Thread.currentThread().getName() + "执行了");
+                });
+            }
+        } finally {
+            executor1.shutdown();
+        }
+    }
+}
+
CachedThreadPool
+
    +
  • 使用Executors.newCachedThreadPool()创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • +
  • newCachedThreadPoolcorePoolSize 设置为0,将 maximumPoolSize 设置为 Integer.MAX_VALUE,使用的 SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
  • +
+

源代码

+
    public static ExecutorService newCachedThreadPool() {
+        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
+                                      60L, TimeUnit.SECONDS,
+                                      new SynchronousQueue<Runnable>());
+    }
+

代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        ExecutorService executor1 = null;
+        try {
+            executor1 = Executors.newCachedThreadPool();
+            for (int i = 1; i <= 10; i++) {
+                executor1.execute(() -> {
+                    System.out.println(Thread.currentThread().getName() + "执行了");
+                });
+            }
+        } finally {
+            executor1.shutdown();
+        }
+    }
+}
+

线程池参数

+
    public ThreadPoolExecutor(int corePoolSize,
+                              int maximumPoolSize,
+                              long keepAliveTime,
+                              TimeUnit unit,
+                              BlockingQueue<Runnable> workQueue,
+                              ThreadFactory threadFactory,
+                              RejectedExecutionHandler handler) {
+                 // ...
+    }
+
    +
  • corePoolSize: 线程池中的常驻核心线程数,可理解为初始化线程数
  • +
  • maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
  • +
  • threadFactory:线程工厂;表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可
  • +
  • workQueue:任务队列;随着业务量的增多,线程开始慢慢处理不过来,这时候需要放到任务队列中去等待线程处理
  • +
  • rejectedExecutionHandler:拒绝策略;如果业务越来越多,线程池首先会扩容,扩容后发现还是处理不过来,任务队列已经满了,这时候拒绝接收新的请求
  • +
  • keepAliveTime:多余的空闲线程的存活时间;如果线程池扩容后,能处理过来,而且数据量并没有那么大,用最初的线程数量就能处理过来,剩下的线程被叫做空闲线程
  • +
  • unit:多余的空闲线程的存活时间的单位
  • +
+

线程池工作原理

+

线程池工作原理

+

在创建了线程池后,等待提交过来的任务请求; +当调用execute方法添加一个请求任务时,线程池会做如下判断:

+
    +
  1. 如果当前运行的线程数小于corePoolSize,那么马上创建线程运行该任务
  2. +
  3. 如果当前运行的线程数大于等于corePoolSize,那么该任务会被放入任务队列
  4. +
  5. 如果这时候任务队列满了且正在运行的线程数还小于maximumPoolSize,那么要创建非核心线程立刻运行这个任务(扩容)
  6. +
  7. 如果任务队列满了且正在运行的线程数等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
  8. +
  9. 随着时间的推移,业务量越来越少,线程池中出现了空闲线程,当一个线程无事可做超过一定的时间时,线程池会进行判断: +如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后它最终会收缩到 corePoolSize 的大小
  10. +
+

四种拒绝策略

+

在线程池中,如果任务队列满了并且正在运行的线程个数大于等于允许运行的最大线程数,那么线程池会启动拒绝策略来执行,具体分为下列四种:

+
    +
  • AbortPolicy: 默认拒绝策略;直接抛出java.util.concurrent.RejectedExecutionException异常,阻止系统的正常运行;
  • +
  • CallerRunsPolicy:调用这运行,一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量;
  • +
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中;
  • +
  • DiscardPolicy:直接丢弃任务,不给予任何处理也不会抛出异常;如果允许任务丢失,这是一种最好的解决方案;
  • +
+

自定义线程池

+

在实际开发中用哪个线程池?

+

上面的三种一个都不用,我们生产上只能使用自定义的。

+

Executors 中JDK已经给你提供了,为什么不用?

+

以下内容摘自《阿里巴巴开发手册》

+
+

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 +说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 +【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

+
+
+

说明:Executors 返回的线程池对象的弊端如下: +1) FixedThreadPoolSingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 +2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

+
+

自定义线程池代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        ExecutorService executor1 = null;
+        try {
+            executor1 = new ThreadPoolExecutor(
+                    2,
+                    5,
+                    1L,
+                    TimeUnit.SECONDS,
+                    new LinkedBlockingQueue<>(3),
+                    Executors.defaultThreadFactory(),
+                    new ThreadPoolExecutor.CallerRunsPolicy());
+            for (int i = 1; i <= 20; i++) {
+                executor1.execute(() -> {
+                    System.out.println(Thread.currentThread().getName() + "执行了");
+                });
+            }
+        } finally {
+            executor1.shutdown();
+        }
+    }
+}
+

合理配置线程池参数

+

合理配置线程池参数,可以分为以下两种情况

+
    +
  • +

    CPU密集型:CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行; +CPU密集型任务配置尽可能少的线程数量:参考公式:(CPU核数+1)

    +
  • +
  • +

    IO密集型:即该任务需要大量的IO,即大量的阻塞; +在IO密集型任务中使用多线程可以大大的加速程序运行,故需要多配置线程数:参考公式:CPU核数/ (1-阻塞系数) 阻塞系数在0.8~0.9之间

    +
  • +
+

代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        ExecutorService executor1 = null;
+        try {
+            // 获取cpu核心数
+            int coreNum = Runtime.getRuntime().availableProcessors();
+            /*
+             * 1. IO密集型: CPU核数/ (1-阻塞系数) 阻塞系数在0.8~0.9之间
+             * 2. CPU密集型: CPU核数+1
+             */
+//            int maximumPoolSize = coreNum + 1;
+            int maximumPoolSize = (int) (coreNum / (1 - 0.9));
+            System.out.println("当前线程池最大允许存放:" + maximumPoolSize + "个线程");
+            executor1 = new ThreadPoolExecutor(
+                    2,
+                    maximumPoolSize,
+                    1L,
+                    TimeUnit.SECONDS,
+                    new LinkedBlockingQueue<>(3),
+                    Executors.defaultThreadFactory(),
+                    new ThreadPoolExecutor.CallerRunsPolicy());
+            for (int i = 1; i <= 20; i++) {
+                executor1.execute(() -> {
+                    System.out.println(Thread.currentThread().getName() + "执行了");
+                });
+            }
+        } finally {
+            executor1.shutdown();
+        }
+    }
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/uploadfile-code/index.html b/blog-site/public/posts/essays/uploadfile-code/index.html new file mode 100644 index 00000000..c51cc83f --- /dev/null +++ b/blog-site/public/posts/essays/uploadfile-code/index.html @@ -0,0 +1,2136 @@ + + + + + + + + + + + 整合文件上传功能 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

整合文件上传功能

+ 2023.08.11 +
+

结构

+

整合文件上传功能

+

pom.xml

+

fastdfs-client-java-1.27.jar:点击下载

+
    <dependencies>
+
+        <!-- fastdfs -->
+        <dependency>
+            <groupId>org.csource</groupId>
+            <artifactId>fastdfs-client-java</artifactId>
+            <version>1.27</version>
+            <systemPath>${project.basedir}/lib/fastdfs-client-java-1.27.jar</systemPath>
+            <scope>system</scope>
+        </dependency>
+
+        <!--aliyun oss 依赖-->
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <optional>true</optional>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+            <version>5.8.11</version>
+        </dependency>
+
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>2.11.0</version>
+        </dependency>
+
+    </dependencies>
+

application.yml

+
server:
+  port: 80
+

公共部分

+

FileManagement

+
public interface FileManagement {
+
+
+    /**
+     * 设置下一个bean的对象
+     *
+     * @param nextFileManagement 下一个bean对象
+     */
+    void setNext(FileManagement nextFileManagement);
+
+}
+

FileBeanManagement

+
@Component
+public class FileBeanManagement {
+
+
+    @Bean(name = "uploadChain")
+    public AbstractUploadManagement uploadChain() {
+        return buildChain(AbstractUploadManagement.class, true);
+    }
+
+
+    @Bean(name = "downloadChain")
+    public AbstractDownloadManagement downloadChain() {
+        return buildChain(AbstractDownloadManagement.class, true);
+    }
+
+
+    /**
+     * 设置文件管理bean调用链(环状链)
+     *
+     * @param type   文件管理类型
+     * @param isRing 是否形成环形链表
+     * @param <T>    extends FileManagement 指定类型
+     * @return bean
+     */
+    private <T extends FileManagement> T buildChain(Class<T> type, boolean isRing) {
+        List<T> beans = new ArrayList<>(SpringUtil.getBeansOfType(type).values());
+
+        // 设置调用链
+        for (int index = 0; index < beans.size(); index++) {
+            // 判断是否到最后一个bean 如果到了最后一个bean将第一个bean设置为尾部的下一个节点 形成环状结构
+            beans.get(index).setNext(index + 1 >= beans.size() && isRing ? beans.get(0) : beans.get(index + 1));
+        }
+        return beans.get(0);
+    }
+
+
+}
+

AbstractUploadManagement

+
@Slf4j
+public abstract class AbstractUploadManagement implements FileManagement {
+
+    protected AbstractUploadManagement self;
+
+    @Override
+    public void setNext(FileManagement fileManagement) {
+        this.self = (AbstractUploadManagement)fileManagement;
+    }
+
+    protected List<String> allowUploadFiles(ArchetypeFileConfig fileConfig) {
+        List<String> allowFiles = fileConfig.getAllowFiles();
+        return allowFiles.isEmpty() ? Arrays.asList(FileConstant.DEFAULT_ALLOWED_EXTENSION) : allowFiles;
+    }
+
+    /**
+     * 判断是否是当前平台处理
+     *
+     * @param platformType 指定平台类型 {@linkplain ArchetypeFilePlatformType}
+     * @return true 为当前平台处理
+     */
+    protected abstract boolean isCurrentPlatform(ArchetypeFilePlatformType platformType);
+
+    /**
+     * 校验允许上传的文件格式; 默认{@code  FileTypeUtils.DEFAULT_ALLOWED_EXTENSION}
+     *
+     * @param fileName 文件名称
+     * @return true 允许上传
+     */
+    private boolean checkAllowUploadFiles(String fileName,ArchetypeFileConfig fileConfig) {
+        if (fileName == null) {
+            return false;
+        }
+        List<String> allowUploadFiles = allowUploadFiles(fileConfig);
+        for (String allowUploadFile : allowUploadFiles) {
+            if (fileName.toLowerCase().endsWith(allowUploadFile)) {
+                return true;
+            }
+        }
+        log.info("当前上传文件仅支持 [{}] 格式", Arrays.toString(allowUploadFiles.toArray()));
+        return false;
+    }
+
+    /**
+     * 校验允许上传的文件大小
+     *
+     * @param bytes 字节数量
+     * @return true 允许上传
+     */
+    protected  boolean checkSingleAllowUploadSize(byte[] bytes,ArchetypeFileConfig fileConfig){
+        return bytes.length <= fileConfig.getAllowFileSize();
+    }
+
+    /**
+     * 前置处理 预留方法
+     *
+     * @param multipartFile 上传文件对象
+     * @return true 允许上传
+     */
+    protected abstract boolean preProcessing(MultipartFile multipartFile,ArchetypeFileConfig fileConfig);
+
+    /**
+     * 上传文件
+     *
+     * @param multipartFile 上传文件对象
+     * @return 上传成功后的路径
+     */
+    protected abstract String upload(MultipartFile multipartFile,ArchetypeFileConfig fileConfig);
+
+    /**
+     * 后置处理 预留方法
+     *
+     * @param fileUri 上传成功后的路径
+     * @return 文件上传路径
+     */
+    protected abstract String postProcessing(String fileUri);
+
+
+    /**
+     * 多上传文件模板方法
+     *
+     * @param fileConfig    文件上传配置
+     * @param multipartFile 上传文件对象
+     * @return 上传成功后的文件uri集合
+     */
+    @SneakyThrows
+    public final List<String> uploadTemplate(MultipartFile[] multipartFile, ArchetypeFileConfig fileConfig) {
+        if (multipartFile == null || multipartFile.length == 0) {
+            return Collections.emptyList();
+        }
+        ArchetypeFilePlatformType filePlatformType = fileConfig.getFilePlatformType();
+
+        // 检索上传文件平台类型
+        if (!self.isCurrentPlatform(filePlatformType)) {
+            return self.uploadTemplate(multipartFile,fileConfig);
+        }
+
+        // 多文件上传需要加循环
+        List<String> fileUriList = new ArrayList<>(multipartFile.length);
+        for (MultipartFile file : multipartFile) {
+            if (!self.beforeCheck(file,fileConfig)) {
+                break;
+            }
+            fileUriList.add(self.postProcessing(self.upload(file,fileConfig)));
+        }
+        log.info("上传文件=> 平台类型:[{}]\t 上传路径:[{}]", filePlatformType, Arrays.toString(fileUriList.toArray()));
+        return fileUriList;
+    }
+
+
+    /**
+     * 上传文件前置校验
+     *
+     * @param multipartFile 上传的文件
+     * @return true校验成功允许上传
+     */
+    @SneakyThrows
+    private boolean beforeCheck(MultipartFile multipartFile,ArchetypeFileConfig fileConfig) {
+        return !multipartFile.isEmpty()
+                && (multipartFile.getSize() != 0)
+                && self.checkAllowUploadFiles(multipartFile.getOriginalFilename(),fileConfig)
+                && self.checkSingleAllowUploadSize(multipartFile.getBytes(),fileConfig)
+                && self.preProcessing(multipartFile,fileConfig)
+                ;
+    }
+
+}
+

AbstractDownloadManagement

+
@Slf4j
+public abstract class AbstractDownloadManagement implements FileManagement {
+
+    private AbstractDownloadManagement self;
+
+    @Override
+    public void setNext(FileManagement fileManagement) {
+        this.self = (AbstractDownloadManagement)fileManagement;
+    }
+
+    /**
+     * 前置处理
+     *
+     * @param uri          下载文件的地址
+     * @return true-允许执行后置操作
+     */
+    protected abstract boolean preProcessing(String uri);
+
+    /**
+     * 下载文件
+     *
+     * @param uri          下载文件地址
+     * @return 下载到的文件
+     */
+    protected abstract File download(String uri);
+
+    /**
+     * 后置处理
+     *
+     * @param downloadFile 下载的文件
+     * @return 经过后置处理下载后的文件
+     */
+    protected abstract File postProcessing(File downloadFile);
+
+    /**
+     * 判断是否是当前平台处理
+     *
+     * @param platformType 指定平台类型 {@linkplain ArchetypeFilePlatformType}
+     * @return true 为当前平台处理
+     */
+    protected abstract boolean isCurrentPlatform(ArchetypeFilePlatformType platformType);
+
+
+    /**
+     * 文件下载模板方法
+     *
+     * @param platformType 平台类型
+     * @param uri          文件地址,支持本地和http地址
+     * @return file对象
+     */
+    public final File downloadTemplate(ArchetypeFilePlatformType platformType, String uri) {
+        // 检索上传文件平台类型
+        if (!self.isCurrentPlatform(platformType)) {
+            return self.downloadTemplate(platformType, uri);
+        }
+
+        // 前置下载校验
+        if (!self.preProcessing(uri)) {
+            return new File(CharSequenceUtil.EMPTY);
+        }
+        // 下载文件到指定路径
+        return self.postProcessing(self.download(uri));
+    }
+
+
+    @SneakyThrows
+    protected File download(ArchetypeFilePlatformType platformType, String uri, String downloadPath, InputStream inputStream, long fileLength) {
+        log.info("下载文件平台类型: {} 文件大小为:{} MB", platformType, new DecimalFormat("0.00").format(fileLength / (float) (1024 * 1024)));
+        // 获取文件名称
+        String[] split = uri.split(platformType.getFileSeparator());
+        String fullFileName = split[split.length - 1];
+        String[] fileNameWithSuffix = fullFileName.split("\\.");
+
+        // 指定存放位置(有需求可以自定义)
+        String path = String.format("%s%s%s_%s.%s", downloadPath, File.separatorChar, fileNameWithSuffix[0], DateUtil.format(new Date(), "yyyyMMddHHmmss"), fileNameWithSuffix[1]);
+        File file = new File(path);
+        // 校验文件夹目录是否存在,不存在就创建一个目录
+        if (!file.getParentFile().exists()) {
+            file.getParentFile().mkdirs();
+        }
+
+        // 写入文件
+        @Cleanup OutputStream out = Files.newOutputStream(file.toPath());
+        @Cleanup BufferedInputStream bin = new BufferedInputStream(inputStream);
+        int size = 0;
+        int len = 0;
+        byte[] buf = new byte[2048];
+        while ((size = bin.read(buf)) != -1) {
+            len += size;
+            out.write(buf, 0, size);
+            if (log.isDebugEnabled()) {
+                log.debug("下载文件 {} 进度 =>{}%", fullFileName, len * 100L / fileLength);
+            }
+        }
+        log.info("{} 文件下载成功!", path);
+        return file;
+    }
+
+
+
+
+    @SneakyThrows
+    protected File defaultHttpDownload(ArchetypeFilePlatformType platformType, String uri, String downloadPath) {
+        // 建立 http 下载连接 建立链接从请求中获取数据
+        HttpURLConnection httpUrlConnection = (HttpURLConnection) new URL(uri).openConnection();
+        httpUrlConnection.setConnectTimeout(10 * 1000);
+        httpUrlConnection.setRequestMethod("GET");
+        httpUrlConnection.setRequestProperty("Charset", "UTF-8");
+        httpUrlConnection.connect();
+        return download(platformType, uri, downloadPath, httpUrlConnection.getInputStream(), httpUrlConnection.getContentLength());
+    }
+
+
+}
+

ArchetypeFileConfig

+
public interface ArchetypeFileConfig {
+
+
+    /**
+     * 文件平台类型
+     */
+    ArchetypeFilePlatformType getFilePlatformType();
+
+    /**
+     * 允许上传的文件类型
+     */
+    List<String> getAllowFiles();
+
+    /**
+     * 允许上传的文件大小; 单位字节
+     */
+    Long getAllowFileSize();
+
+
+}
+

ArchetypeFilePlatformType

+
@AllArgsConstructor
+public enum ArchetypeFilePlatformType {
+
+    // 上传下载文件平台类型
+    LOCAL("\\\\"),
+    ALIYUN("/"),
+    TENCEN_CLOUD("/"),
+    FAST_DFS("/")
+
+    ;
+
+    /**
+     * 下载文件分隔符
+     */
+    @Getter
+    private String fileSeparator;
+
+
+
+}
+

FileConstant

+
public interface FileConstant {
+
+    String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
+
+    String[] FLASH_EXTENSION = {"swf", "flv"};
+
+    String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
+            "asf", "rm", "rmvb"};
+
+    String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
+
+    String[] DEFAULT_ALLOWED_EXTENSION = {
+            // 图片
+            "bmp", "gif", "jpg", "jpeg", "png",
+            // word excel powerpoint
+            "doc", "docx", "xls", "xlsx", "ppt", "pptx", "html", "htm", "txt",
+            // 压缩文件
+            "rar", "zip", "gz", "bz2",
+            // 视频格式
+            "mp4", "avi", "rmvb",
+            // pdf
+            "pdf"};
+
+}
+

FileFacade

+
public interface FileFacade {
+
+
+    /**
+     * 上传文件
+     *
+     * @param fileConfig    文件上传配置
+     * @param multipartFile 上传的文件
+     * @return 上传后的文件地址
+     */
+    default List<String> upload(MultipartFile[] multipartFile, ArchetypeFileConfig fileConfig) {
+        return upload(multipartFile, fileConfig, new UploadFileExtensions() {
+            @Override
+            public boolean preProcessing(MultipartFile[] multipartFile) {
+                return true;
+            }
+
+            @Override
+            public List<String> postProcessing(MultipartFile[] multipartFile, List<String> uploadUriList) {
+                return uploadUriList;
+            }
+        });
+    }
+
+
+    /**
+     * 上传文件
+     *
+     * @param fileConfig    文件上传配置
+     * @param multipartFile 上传的文件
+     * @return 上传后的文件地址
+     */
+    default List<String> upload(ArchetypeFileConfig fileConfig, MultipartFile[] multipartFile) {
+        return upload(multipartFile, fileConfig);
+    }
+
+
+    /**
+     * 上传文件
+     *
+     * @param fileConfig     文件上传配置
+     * @param multipartFile  上传的文件
+     * @param fileExtensions 文件扩展接口; 可在文件上传之前,上传之后进行操作
+     * @return 上传后的文件地址
+     */
+    List<String> upload(MultipartFile[] multipartFile, ArchetypeFileConfig fileConfig, UploadFileExtensions fileExtensions);
+
+
+    /**
+     * 下载文件
+     *
+     * @param platformType 指定平台类型{@linkplain ArchetypeFilePlatformType}
+     * @param uri          文件地址; 支持本地,http
+     * @return 下载的文件对象
+     */
+    default File download(@NotNull ArchetypeFilePlatformType platformType, @NotNull String uri) {
+        return download(platformType, uri, new DownloadFileExtensions() {
+            @Override
+            public boolean preProcessing(String uri) {
+                return true;
+            }
+
+            @Override
+            public File postProcessing(String uri, File downloadFile) {
+                return downloadFile;
+            }
+        });
+    }
+
+
+    /**
+     * 下载文件
+     *
+     * @param platformType   指定平台类型{@linkplain ArchetypeFilePlatformType}
+     * @param uri            文件地址; 支持本地,http
+     * @param fileExtensions 下载文件扩展接口;可在下载上传之前,下载之后进行操作
+     * @return 下载的文件对象
+     */
+    File download(ArchetypeFilePlatformType platformType, String uri, DownloadFileExtensions fileExtensions);
+
+}
+

FileManagementBridge

+
@Component
+public class FileManagementBridge implements FileFacade {
+
+
+    @Resource(name = "uploadChain")
+    private AbstractUploadManagement uploadChain;
+
+
+    @Resource(name = "downloadChain")
+    private AbstractDownloadManagement downloadChain;
+
+
+    @Override
+    public List<String> upload(MultipartFile[] multipartFile, ArchetypeFileConfig fileConfig, UploadFileExtensions fileExtensions) {
+        if (ObjectUtil.isAllEmpty(fileConfig.getFilePlatformType(), multipartFile)) {
+            throw new IllegalArgumentException(String.format("upload file need param (platformType:[%s] multipartFile:[%s] maybe is empty.", fileConfig.getFilePlatformType(), Arrays.toString(multipartFile)));
+        }
+        if (!fileExtensions.preProcessing(multipartFile)) {
+            return Collections.emptyList();
+        }
+        return fileExtensions.postProcessing(multipartFile, uploadChain.uploadTemplate(multipartFile, fileConfig));
+    }
+
+
+    @Override
+    public File download(ArchetypeFilePlatformType platformType, String uri, DownloadFileExtensions fileExtensions) {
+        if (ObjectUtil.isAllEmpty(platformType, uri)) {
+            throw new IllegalArgumentException(String.format("download file need param ( platformType:[%s] uri:[%s] ) maybe is empty.", platformType, uri));
+        }
+        if (!fileExtensions.preProcessing(uri)) {
+            return new File(CharSequenceUtil.EMPTY);
+        }
+        return fileExtensions.postProcessing(uri, downloadChain.downloadTemplate(platformType, uri));
+    }
+
+
+}
+

DownloadFileExtensions

+
public interface DownloadFileExtensions {
+
+
+    /**
+     * 下载文件-前置处理
+     *
+     * @param uri 下载的uri
+     * @return true-允许下载;false-不允许下载
+     */
+    boolean preProcessing(String uri);
+
+
+    /**
+     * 下载文件-后置处理
+     *
+     * @param uri          下载的uri
+     * @param downloadFile 下载的文件
+     * @return 经过后置处理的下载的文件
+     */
+    File postProcessing(String uri, File downloadFile);
+
+}
+

UploadFileExtensions

+
public interface UploadFileExtensions {
+
+
+    /**
+     * 上传文件-前置处理
+     *
+     * @param multipartFile 上传的文件对象
+     * @return true-能继续处理
+     */
+    boolean preProcessing(MultipartFile[] multipartFile);
+
+
+    /**
+     * 上传文件-后置处理
+     *
+     * @param uploadUriList 上传文件返回的uri地址集合
+     * @return 经过后置处理返回的uri地址集合
+     */
+    List<String> postProcessing(MultipartFile[] multipartFile,List<String> uploadUriList);
+
+}
+

阿里云

+

AliyunFileConfig

+
@Slf4j
+@Data
+@Configuration(value = "aliyunFileConfig")
+@Accessors(chain = true)
+public class AliyunFileConfig implements ArchetypeFileConfig {
+
+
+    /**
+     * 填写Bucket所在地域对应的Endpoint,可在创建好的Bucket概况页查看
+     */
+    private String endpoint;
+
+    /**
+     * 阿里云账号AccessKey里所对应的AccessKey ID
+     */
+    private String accessKeyId;
+
+    /**
+     * 阿里云账号AccessKey里所对应的AccessKey Secret
+     */
+    private String accessKeySecret;
+
+    /**
+     * OSS对象存储空间名
+     */
+    private String bucketName;
+    /**
+     * 允许上传的文件类型
+     */
+    private List<String> allowFiles;
+
+    /**
+     * 允许上传的文件大小; 单位字节
+     */
+    private Long allowFileSize;
+
+
+    @Override
+    public ArchetypeFilePlatformType getFilePlatformType() {
+        return ArchetypeFilePlatformType.ALIYUN;
+    }
+}
+

ArchetypeAliyunDownload

+
@Component("archetypeAliyunDownload")
+@RequiredArgsConstructor
+public class ArchetypeAliyunDownload extends AbstractDownloadManagement {
+
+
+    @Override
+    protected boolean preProcessing(String uri) {
+        return false;
+    }
+
+    @Override
+    protected File download(String uri) {
+        return null;
+    }
+
+    @Override
+    protected File postProcessing(File downloadFile) {
+        return null;
+    }
+
+    @Override
+    protected boolean isCurrentPlatform(ArchetypeFilePlatformType platformType) {
+        return false;
+    }
+}
+

ArchetypeAliyunUpload

+
@Component("archetypeAliyunUpload")
+@RequiredArgsConstructor
+public class ArchetypeAliyunUpload extends AbstractUploadManagement {
+
+
+    @Override
+    protected boolean isCurrentPlatform(ArchetypeFilePlatformType platformType) {
+        return ArchetypeFilePlatformType.ALIYUN.equals(platformType);
+    }
+
+
+    @Override
+    protected boolean preProcessing(MultipartFile multipartFile, ArchetypeFileConfig fileConfig) {
+        return true;
+    }
+
+
+    @SneakyThrows
+    @Override
+    protected String upload(MultipartFile multipartFile,ArchetypeFileConfig fileConfig) {
+        AliyunFileConfig aliyunFileConfig = ((AliyunFileConfig) fileConfig);
+        OSS ossClient = new OSSClientBuilder().build(
+                aliyunFileConfig.getEndpoint(),
+                aliyunFileConfig.getAccessKeyId(),
+                aliyunFileConfig.getAccessKeySecret()
+        );
+        // 上传文件流
+        try (InputStream inputStream = multipartFile.getInputStream()) {
+            String fileName = multipartFile.getOriginalFilename();
+            fileName = UUID.randomUUID().toString().replaceAll("-", "") + fileName;
+
+            // 按照当前日期,创建文件夹,上传到创建文件夹里面 20220315/xx.jpg
+            fileName = DateUtil.format(DateUtil.date(), "yyyyMMdd") + ArchetypeFilePlatformType.ALIYUN.getFileSeparator() + fileName;
+            ossClient.putObject(aliyunFileConfig.getBucketName(), fileName, inputStream);
+            ossClient.shutdown();
+
+            return String.format("https://%s.%s/%s", aliyunFileConfig.getBucketName(), aliyunFileConfig.getEndpoint(), fileName);
+        }
+    }
+
+
+    @Override
+    protected String postProcessing(String fileUri) {
+        return fileUri;
+    }
+}
+

fastdfs

+

FastDfsConfig

+
@Data
+@Accessors(chain = true)
+@Configuration(value = "fastDfsConfig")
+public class FastDfsConfig implements ArchetypeFileConfig {
+
+
+    /**
+     * 编码
+     */
+    private String charset;
+    /**
+     * 连接超时事件
+     */
+    private String connectTimeoutInSecond;
+    /**
+     * 网络超时时间
+     */
+    private String networkTimeoutInSeconds;
+    /**
+     * track端口
+     */
+    private String httpTrackerHttpPort;
+    /**
+     * http令牌
+     */
+    private String httpAntiStealToken;
+    /**
+     * 服务器地址
+     */
+    private String trackerServers;
+
+    private String storageServiceIp;
+    private Integer storageServicePort;
+    private Integer storageServicePathIndex;
+    /**
+     * 允许上传的文件类型
+     */
+    private List<String> allowFiles;
+
+    /**
+     * 允许上传的文件大小; 单位字节
+     */
+    private Long allowFileSize;
+
+    @Override
+    public ArchetypeFilePlatformType getFilePlatformType() {
+        return ArchetypeFilePlatformType.FAST_DFS;
+    }
+
+}
+

ArchetypeFastDfsDownload

+
@Component("archetypeFastDfsDownload")
+@DependsOn({"fastDfsConfig"})
+@RequiredArgsConstructor
+public class ArchetypeFastDfsDownload extends AbstractDownloadManagement {
+    @Override
+    protected boolean preProcessing(String uri) {
+        return false;
+    }
+
+    @Override
+    protected File download(String uri) {
+        return null;
+    }
+
+    @Override
+    protected File postProcessing(File downloadFile) {
+        return null;
+    }
+
+    @Override
+    protected boolean isCurrentPlatform(ArchetypeFilePlatformType platformType) {
+        return false;
+    }
+}
+

ArchetypeFastDfsUpload

+
@Slf4j
+@Component("archetypeFastDfsUpload")
+@RequiredArgsConstructor
+public class ArchetypeFastDfsUpload extends AbstractUploadManagement {
+
+
+    @Override
+    protected boolean isCurrentPlatform(ArchetypeFilePlatformType platformType) {
+        return ArchetypeFilePlatformType.FAST_DFS.equals(platformType);
+    }
+
+
+    @Override
+    protected boolean preProcessing(MultipartFile multipartFile, ArchetypeFileConfig fileConfig) {
+        return true;
+    }
+
+
+    @SneakyThrows
+    @Override
+    protected String upload(MultipartFile multipartFile,ArchetypeFileConfig fileConfig) {
+        FastDfsConfig fastDfsConfig = (FastDfsConfig) fileConfig;
+        init(fastDfsConfig);
+
+        String originalFilename = multipartFile.getOriginalFilename();
+        String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")+1);
+
+        StorageClient storageClient = new StorageClient(null, new StorageServer(fastDfsConfig.getStorageServiceIp(), fastDfsConfig.getStorageServicePort(), fastDfsConfig.getStorageServicePathIndex()));
+        String[] strings = storageClient.upload_file(multipartFile.getBytes(), suffix, null);
+        return String.format("http://%s:%s/%s",fastDfsConfig.getStorageServiceIp(),fastDfsConfig.getHttpTrackerHttpPort(),StrUtil.join("/",strings));
+    }
+
+    @SneakyThrows
+    private void init(FastDfsConfig fastDfsConfig){
+        // 先从容器获取如果没有在加载 fixme
+
+        // 文件服务器客户端初始化
+        Properties properties = new Properties();
+        properties.setProperty("fastdfs.connect_timeout_in_seconds", fastDfsConfig.getConnectTimeoutInSecond());
+        properties.setProperty("fastdfs.network_timeout_in_seconds", fastDfsConfig.getNetworkTimeoutInSeconds());
+        properties.setProperty("fastdfs.charset", fastDfsConfig.getCharset());
+        properties.setProperty("fastdfs.http_tracker_http_port", fastDfsConfig.getHttpTrackerHttpPort());
+        properties.setProperty("fastdfs.http_anti_steal_token", fastDfsConfig.getHttpAntiStealToken());
+        properties.setProperty("fastdfs.tracker_servers", fastDfsConfig.getTrackerServers());
+        ClientGlobal.initByProperties(properties);
+        log.info("fastdfs客户端初始化成功");
+    }
+
+
+
+    @Override
+    protected String postProcessing(String fileUri) {
+        return fileUri;
+    }
+
+}
+

本地

+

LocalFileConfig

+
@Data
+@Configuration(value = "localFileConfig")
+@Accessors(chain = true)
+public class LocalFileConfig implements ArchetypeFileConfig {
+
+    /**
+     * 允许上传的文件类型
+     */
+    private List<String> allowFiles;
+
+    /**
+     * 允许上传的文件大小; 单位字节
+     */
+    private Long allowFileSize;
+
+    /**
+     * 上传的路径
+     */
+    private String uploadPath;
+
+    /**
+     * 下载文件到指定路径
+     */
+    private String downloadPath;
+
+    @Override
+    public ArchetypeFilePlatformType getFilePlatformType() {
+        return ArchetypeFilePlatformType.LOCAL;
+    }
+
+
+}
+

ArchetypeLocalDownload

+
@DependsOn({"localFileConfig"})
+@Component("archetypeLocalDownload")
+@RequiredArgsConstructor
+public class ArchetypeLocalDownload extends AbstractDownloadManagement {
+
+    private final LocalFileConfig localFileConfig;
+
+    @Override
+    protected boolean preProcessing(String uri) {
+        return true;
+    }
+
+    @SneakyThrows
+    @Override
+    protected File download(String uri) {
+        return download(ArchetypeFilePlatformType.LOCAL, uri, localFileConfig.getDownloadPath(), Files.newInputStream(Paths.get(uri)), new File(uri).length());
+    }
+
+    @Override
+    protected File postProcessing(File downloadFile) {
+        return downloadFile;
+    }
+
+    @Override
+    protected boolean isCurrentPlatform(ArchetypeFilePlatformType platformType) {
+        return ArchetypeFilePlatformType.LOCAL.equals(platformType);
+    }
+
+}
+

ArchetypeLocalUpload

+
@Component("archetypeLocalUpload")
+@RequiredArgsConstructor
+public class ArchetypeLocalUpload extends AbstractUploadManagement {
+
+
+    @Override
+    protected boolean isCurrentPlatform(ArchetypeFilePlatformType platformType) {
+        return ArchetypeFilePlatformType.LOCAL.equals(platformType);
+    }
+
+
+    @Override
+    protected boolean preProcessing(MultipartFile multipartFile,ArchetypeFileConfig fileConfig) {
+        return true;
+    }
+
+
+    @SneakyThrows
+    @Override
+    protected String upload(MultipartFile file, ArchetypeFileConfig fileConfig) {
+        LocalFileConfig localFileConfig = (LocalFileConfig) fileConfig;
+        String fileName = extractFilename(file);
+        String absPath = getAbsoluteFile(localFileConfig.getUploadPath(), fileName).getAbsolutePath();
+        file.transferTo(Paths.get(absPath));
+        return String.format("%s/%s", localFileConfig.getUploadPath(), fileName);
+    }
+
+
+    /**
+     * 编码文件名
+     */
+    private String extractFilename(MultipartFile file) {
+        return CharSequenceUtil.format("{}/{}_{}.{}", DateUtil.format(new Date(), "yyyyMMdd"),
+                FilenameUtils.getBaseName(file.getOriginalFilename()), IdUtil.getSnowflakeNextIdStr(), FilenameUtils.getExtension(file.getOriginalFilename()));
+    }
+
+
+    private File getAbsoluteFile(String uploadDir, String fileName) {
+        File desc = new File(uploadDir + File.separator + fileName);
+        if (!desc.exists() && !desc.getParentFile().exists()) {
+            desc.getParentFile().mkdirs();
+        }
+        return desc;
+    }
+
+
+    @Override
+    protected String postProcessing(String fileUri) {
+        return fileUri;
+    }
+
+}
+

调用测试

+

FileTest

+
@Slf4j
+@RunWith(SpringRunner.class)
+@SpringBootTest(classes = FileApplication.class)
+public class FileTest {
+
+
+    @Autowired
+    private FileFacade fileFacade;
+
+
+    /**
+     *     aliyun:
+     *       allowFiles: [ ".png",".txt" ]
+     *       allowFileSize: 10240
+     *       endpoint: oss-cn-shanghai.aliyuncs.com
+     *       accessKeyId: 
+     *       accessKeySecret: 
+     *       bucketName: shanghai-bucketnametest
+     */
+    ArchetypeFileConfig aliyunFileConfig = new AliyunFileConfig()
+            .setAllowFiles(Arrays.asList(".png", ".txt"))
+            .setAllowFileSize(10240L)
+            .setEndpoint("oss-cn-shanghai.aliyuncs.com")
+            .setAccessKeyId("")
+            .setAccessKeySecret("")
+            .setBucketName("shanghai-bucketnametest");
+
+    /**
+     *     local:
+     *       allowFiles: [".png",".txt"]
+     *       allowFileSize: 10240
+     *       uploadPath: "C:/Users/Administrator/Downloads"
+     *       downloadPath: "C:/Users/Administrator/Downloads"
+     */
+    ArchetypeFileConfig localFileConfig = new LocalFileConfig()
+            .setAllowFiles(Arrays.asList(".png", ".txt"))
+            .setAllowFileSize(10240L)
+            .setUploadPath("C:\\Users\\Administrator\\Downloads")
+            .setDownloadPath("C:\\Users\\Administrator\\Downloads");
+
+
+    /**
+     *     fastdfs:
+     *       allowFiles: [ ".png",".txt" ]
+     *       allowFileSize: 10240
+     *       charset: UTF-8
+     *       connectTimeoutInSecond: 10
+     *       networkTimeoutInSeconds: 30
+     *       httpTrackerHttpPort: 8888
+     *       httpAntiStealToken: no
+     *       trackerServers: 172.16.1.199:22122
+     *       fastdfsUrl: http://172.16.1.199:8888
+     *       storageServiceIp: 172.16.1.199
+     *       storageServicePort: 23000
+     *       storageServicePathIndex: 0
+     */
+    ArchetypeFileConfig fastDfsConfig = new FastDfsConfig()
+            .setAllowFiles(Arrays.asList(".png", ".txt"))
+            .setAllowFileSize(10240L)
+            .setCharset("UTF-8")
+            .setConnectTimeoutInSecond("10")
+            .setNetworkTimeoutInSeconds("30")
+            .setHttpTrackerHttpPort("8888")
+            .setHttpAntiStealToken("no")
+            .setTrackerServers("172.16.1.199:22122")
+            .setStorageServiceIp("172.16.1.199")
+            .setStorageServicePort(23000)
+            .setStorageServicePathIndex(0)
+            ;
+
+    @Test
+    public void uploadTest() {
+        MultipartFile file = new MockMultipartFile(
+                "file",
+                "hello11.txt",
+                MediaType.TEXT_PLAIN_VALUE,
+                "Hello, World!111111111111111111111111111".getBytes()
+        );
+        List<String> uriLs = fileFacade.upload(new MultipartFile[]{file}, aliyunFileConfig);
+        System.out.println("=>" + uriLs);
+    }
+
+
+    @Test
+    public void downloadTest() {
+        fileFacade.download(ArchetypeFilePlatformType.LOCAL, "C:\\Users\\Administrator\\Downloads\\20230114\\hello_1614199174685622272.txt");
+    }
+
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/essays/vue2-note/index.html b/blog-site/public/posts/essays/vue2-note/index.html new file mode 100644 index 00000000..398bcae6 --- /dev/null +++ b/blog-site/public/posts/essays/vue2-note/index.html @@ -0,0 +1,3503 @@ + + + + + + + + + + + Vue2.0学习笔记 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Vue2.0学习笔记

+ 2019.05.23 +
+

参考资料

+ +

vue-组件

+

参考资料:

+ +

组件是可复用的 Vue 实例,且带有一个名字. 组件的出现是为了拆分vue实例的代码量,能够让我们以不同的组件,来划分不同的功能模块,将来我们需要什么样的功能,就可以去调用对应的组件即可.

+

vue-路由

+

参考资料:

+ +

路由概念:

+
    +
  • 后端路由:对于普通网站,所有的超链接都是URL地址,所有的URL地址都对应服务器上的资源
  • +
  • 前端路由:对于单页面的应用程序来说,主要通过URL中的hash(#号)来实现不同页面的切换,同时,hash有一个特点,http请求中不包含hash相关的内容;所以单页面程序的跳转主要用hash来实现,这种通过hash改变切换页面的方式,成为前端路由,区别于后端路由
  • +
+

代码案例

+

依赖的库

+

点击下载

+

1.helloWorld.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <script src="./lib/vue.min.js" type="text/javascript"></script>
+    <title>Document</title>
+</head>
+<body>
+
+    <!-- 使用 vue 框架 开发大大简化了开发,提高了开发效率 代码 使得前端程序员只关心页面逻辑  -->
+    <div class="app">
+        <p>{{msg}}</p>
+    </div>
+    <script>
+        var vm = new Vue({
+           el: '.app',
+           data: {
+               msg: 'helloWord'
+           }
+        })
+    </script>
+</body>
+</html>
+

2.vue一些指令.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <style>
+        [v-cloak] {
+            display: none;
+        }
+    </style>
+</head>
+<body>
+    <!--v-cloak 和 v-text 可以避免 由于网速过慢vue 的闪烁问题
+        "v-bind:" : 等同于 ":" 
+        v-html : 转义渲染页面
+        v-on:"事件名字"="函数名字"  绑定事件 
+    -->
+    <div id="app">
+        <div v-cloak>{{msg}}</div>
+        <div v-text="msg"></div>
+        <div v-html="msg2"></div>
+        <input type="button" value="按钮" :title="mytitle" v-on:click="show">
+    </div>
+    <script src="./lib/vue.min.js" type="text/javascript"></script>
+    <script>
+        var vm = new Vue({
+            el: '#app',
+            data: {
+                msg: '123',
+                mytitle: '我是一个按钮!',
+                msg2: '<h1>我是一个h1标签,.....</h1>',
+
+            },
+            methods:{
+                show:function(){
+                    alert("1234");
+                }
+            }
+        })
+    </script>
+</body>
+</html>
+

3.跑马灯效果.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js" type="text/javascript"></script>
+</head>
+<body>
+    <div id="app">
+        <button @click="lang">start</button>
+        <button @click="stop">end</button>
+        <div>{{ msg }}</div>
+    </div>
+    <script>
+        var vm = new Vue({
+            el: '#app',
+            data: {
+                msg: '猥琐发育别浪~~~',
+                time: null,
+            },
+            methods: {
+                //如果调用vue 本身的属性 用this
+                lang(){
+                    //如果定时器不等于null 说明开了定时器 就不能再开一个定时器了
+                   if(this.time != null)return;
+                    //开启定时器
+                     this.time = setInterval(() => {
+                          //截取字符串
+                        var start = this.msg.substring(0,1);
+                        var end = this.msg.substring(1);
+                        //拼接成新的字符串
+                        this.msg = end + start;
+
+                    },400)
+                },
+                stop(){
+                    //清除定时器
+                    clearInterval(this.time);
+                    //把time重新赋值为null
+                    this.time = null;
+                }
+            }
+        })
+    </script>
+</body>
+</html>
+

4.事件修饰符.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js" type="text/javascript"></script>
+    <style>
+        .outDiv{
+            height: 300px;
+            background-color: red;
+        }
+        .innerDiv{
+            height: 100px;
+            background-color: aqua;
+        }
+    </style>
+</head>
+<body>
+    <div id="app">
+        <!-- 1.stop修饰符 组织冒泡事件 
+             2.prevent 修饰符 阻止默认事件
+             3.once 修饰符 once 事件只触发一次
+             4.capture 从外往里执行事件 实现捕获触发事件的机制
+             5.self 只有点击当前元素的时候才触发的处理函数 
+
+             self 和 stop 都会阻止冒泡 区别:
+             .self 只会阻止一层冒泡 stop 阻止所有冒泡
+        -->
+        <div class="outDiv" @click.self.capture="div1Handler">   
+            <div class="innerDiv" @click.self="div2Handler">
+                <button @click.stop="btn">戳我</button>
+                <a href="https:www.baidu.com" @click.prevent="baidu">我是百度</a>
+            </div>
+        </div>
+    </div>
+    <script>
+        var vm = new Vue({
+            el: '#app',
+            data: {
+
+            },
+            methods:{
+                div1Handler(){
+                    console.log("div1Handler事件");
+                },
+                div2Handler(){
+                    console.log("div2Handler事件");
+                },
+                btn(){
+                    console.log("点击了按钮");
+                },
+                baidu(){
+                    console.log("点击了超链接!");
+                }
+            }
+        })
+    </script>
+</body>
+</html>
+

5.v-model指令.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js" type="text/javascript"></script>
+</head>
+<body>
+    <!--  v-model 实现双向数据绑定
+        1.只对表单元素生效 
+        2. 使用 v-model 可以实现 表单元素和model中的数据 双向绑定
+        v-bind 只能从 M -> V 无法实现数据双向绑定
+    -->
+    <div id="app">
+        <div><h1>{{msg}}</h1></div>
+        <input type="text" style="width: 100%;" v-model="msg">
+    </div>
+    <script>
+        var vm = new Vue({
+            el: '#app',
+            data:{
+                msg: 'Hello Word',
+            },
+            methods:{
+                
+            }
+        })
+    </script>
+</body>
+</html>
+

6.双向绑定简易计算器.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head>
+
+<body>
+    <div id="app">
+        <input type="text" v-model="n1">
+        <select v-model="opt">
+            <option value="+">+</option>
+            <option value="-">-</option>
+            <option value="*">*</option>
+            <option value="/">/</option>
+        </select>
+        <input type="text" v-model="n2">
+        <button @click="calcu">=</button>
+        <input type="text" v-model="result">
+    </div>
+    <script>
+        var vm = new Vue({
+            el: '#app',
+            data: {
+                n1: 0,
+                n2: 0,
+                opt: '+',
+                result: 0,
+            },
+            methods: {
+                calcu() {
+                    // switch (this.opt) {
+                    //     case '+': this.result = parseInt(this.n1) + parseInt(this.n2)
+                    //         break;
+                    //     case '-': this.result = parseInt(this.n1) - parseInt(this.n2)
+                    //         break;
+                    //     case '*': this.result = parseInt(this.n1) * parseInt(this.n2)
+                    //         break;
+                    //     case '/': this.result = parseInt(this.n1) / parseInt(this.n2)
+                    //         break;
+                    // }
+                   this.result = eval(parseInt(this.n1) + this.opt + parseInt(this.n2));
+                }
+            }
+        })
+    </script>
+</body>
+
+</html>
+

7.vue中的样式-class.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+    <style>
+        .red{
+            color: red;
+        }
+        .italic{
+            font-style: italic;
+        }
+        .thin{
+            font-weight: 100;
+        }
+    </style>
+</head>
+<body>
+    <div id="app">
+        <div>
+            <!--
+                vue 的class 选择器 可以通过v-band来绑定
+                :class=""  可以放数组 可以放对象
+                可以通过三目运算符 来进行运算
+            -->
+            <h1 :class="[flag?'thin':'red']">这是一个h1标签,大到你无法想象!</h1>
+                <!-- <h1 :class="['red']">这是一个h1标签,大到你无法想象!</h1> -->
+            <!-- <h1 :class="classObj">这是一个h1标签,大到你无法想象!</h1> -->
+        </div>
+    </div>
+    <script>
+        var vm = new Vue({
+            el: '#app',
+            data: {
+                flag: true,
+                classObj:{red: true,italic: true,thin: true},
+            },
+            methods:{
+
+            }
+        })
+    </script>
+</body>
+
+</html>
+

8.vue中的样式-style.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+    <div id="app">
+        <!--
+            vue 中的内联样式
+            :style="" 可以写对象
+            可以写多个对象
+            可以写数组 和 外联样式 class 一样
+        -->
+        <h1 :style="[styleObj,styleObj2]">我是h1, 我为自己代言</h1>
+    </div>
+    <script>
+        var vm = new Vue({
+            el: '#app',
+            data:{
+                //样式名称有横杠 必须用单引号
+                styleObj: {color: 'red','font-weight': 100},
+                styleObj2:{'font-style': 'italic'}
+            },
+            methods:{
+
+            }
+        })
+    </script>
+</body>
+</html>
+

9.v-for迭代数字.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+
+    <div id="app">
+        <!--  v-for 循环索引从 1 开始 -->
+        <p v-for="count in 10">这是第{{count}}次循环</p>
+    </div>
+
+    <script>
+        var vm = new Vue({
+            el:'#app',
+            data:{
+
+            }
+        })
+    </script>
+</body>
+</html>
+

10.v-for遍历对象.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+    
+    <div id="app">
+        <p v-for="(val, key) in user">值:{{val}} 键:{{key}}</p>
+    </div>
+
+    <script>
+        var vm = new Vue({
+            el:'#app',
+            data:{
+                user:{
+                    name: '张三',
+                    sex: '男',
+                    age: '19',
+                }
+            },
+            methods:{
+
+            }
+        })
+    </script>
+</body>
+</html>
+

11.v-for遍历数组.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+
+
+    <div id="app">
+        <p v-for="item in list">list中的元素:{{item}}</p>
+    </div>
+
+    <script>
+        var vm = new Vue({
+            el: '#app',
+            data:{
+                list:[1,2,,3,4,5,6]
+            },
+            methods:{
+
+            }
+        })
+    </script>
+</body>
+</html>
+

12.v-for中key的使用.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+    <div id="app">
+        <label>
+            id:<input type="text" v-model="id">
+        </label>
+        <label>
+            Name: <input type="text" v-model="name">
+        </label>
+        <input type="button" value="添加" @click="add">
+        <!--
+            v-for 循环的时候 key 的值只能为 String或number 不能是对象或者其他的
+            使用key的时候 必须用v-bind: 绑定
+            在组件中 如果使用v-for有问题 但又必须使用 v-for 要指明key的值
+            不指明的话 默认是按索引寻找的 
+        -->
+        <p v-for="item in list" :key="item.id">
+            id:{{item.id}}-----name:{{item.name}}
+        </p>
+    </div>
+    <script>
+        var vm = new Vue({
+            el: '#app',
+            data:{
+                id: '',
+                name: '',
+                list:[
+                    {id:  1, name: '张三'},
+                    {id:  2, name: '李四'},
+                    {id:  3, name: 'abc'},
+                    {id:  4, name: '王五'},
+                    {id:  5, name: '赵六'},
+                ],
+            },
+            methods:{
+                add(){
+                    this.list.push({ id: this.id,name: this.name}); 
+                }
+            }
+        })
+    </script>
+</body>
+</html>
+

13.v-for和v-show使用特点.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+    
+    <div id="app">
+        <input type="button"  value="戳我" @click="show">
+        <h1 v-show="flag">这是用v-show控制的元素.</h1>
+        <h1 v-if="flag">这是用v-if控制的元素.</h1>  
+        <!--
+            v-show  和  v-if  区别:
+            v-show: 是用style:display:none; 来控制元素
+            v-if: 是控制dom 删除或者添加元素 
+        -->
+    </div>
+
+    <script>
+        var vm = new Vue({
+            el: '#app',
+            data:{
+                flag: false,
+            },
+            methods:{
+                show(){
+                    this.flag = !this.flag;
+                }
+            }
+        })
+    </script>
+</body>
+</html>
+

14.品牌管理案例.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+    <link rel="stylesheet" href="./lib/bootstrap.css">
+</head>
+
+<body>
+    <div id="app">
+
+        <div class="panel panel-primary">
+            <div class="panel-heading">
+                <h3 class="panel-title">添加品牌</h3>
+            </div>
+            <div class="panel-body form-inline">
+                <label>
+                    ID
+                    <input type="text" class="form-control" v-model="id">
+                </label>
+                <label>
+                    Name
+                <!--
+                    
+                -->
+                    <input type="text" class="form-control" v-model="name" @keyup.space="add">
+                </label>
+                <label>
+                    <!-- 方法带括号 可以传参数 -->
+                    <input type="button" value="添加" class="btn btn-primary" @click="add()">
+                </label>
+                <label>
+                    搜索名称关键字
+                    <!-- 
+                        v-color=" 'blue' "
+                        如果不加单引号 会认为 blue 是 date里边的对象 会从date里边去找
+                        加了就是字符串
+                     -->
+                    <input type="text" class="form-control" v-model="keywords" v-focus v-color="'blue'">
+                </label>
+            </div>
+        </div>
+
+        <table class="table table-bordered table-hover table-striped">
+            <thead>
+                <tr>
+                    <th>ID</th>
+                    <th>Name</th>
+                    <th>Time</th>
+                    <th>Operation</th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr v-for="item in search(keywords)" :key="item.id">
+                    <td>{{ item.id }}</td>
+                    <td>{{ item.name }}</td>
+                    <td>{{ item.time | dateFormat()}}</td>
+                    <td><a href="" @click.prevent="del(item.id)">删除</a></td>
+                </tr>
+            </tbody>
+        </table>
+    </div>
+    <script>
+        //自定义全局指令 使用该指令 进入该页面 获取焦点
+        //定义指令的时候不用加 v-... 但是调用的时候 要加 v-...
+        Vue.directive('focus', {
+            //钩子函数
+            //在这三个函数中 第一个参数永远都是 el 表示 被绑定了的那个元素
+            //el 为 原声的  DOM 对象
+            bind: function(el){//当指令绑定到元素上的时候 会执行该函数 只执行1次
+                //bind 函数常用来 控制样式 
+            },
+            inserted: function(el){//当元素插入到 DOM 的时候会执行该函数 只执行1次
+                //当控制 js 行为的时候 最好放在inserted中 防止不执行该行为
+                el.focus()
+            },
+            updated: function(el){// 当VVOde 节点更新的时候 会执行该函数 可执行多次
+
+            }
+        });
+
+        Vue.directive('color',{
+            bind(el,binding){
+                el.style.color = binding.value
+            }
+        });
+
+
+        //自定义键盘修饰符
+        Vue.config.keyCodes.space = 32
+        // Vue.config.devtools = true
+        //定义全局过滤器 格式化时间
+        Vue.filter('dateFormat', function (dateStr) {
+            // var date = new Date(dateStr)
+
+            // //padStart 可以在前边补0 第一个参数为总长度 第二个为 补的东西
+            // var year = date.getFullYear()
+            // var month = (date.getMonth() + 1).toString().padStart(2, 0)
+            // var day = date.getDate().toString().padStart(2, 0)
+            // var hour = date.getHours().toString().padStart(2, 0)
+            // var minutes = date.getMinutes().toString().padStart(2, 0)
+            // var seconds = date.getSeconds().toString().padStart(2, 0)
+
+            // return year + '-' + month + '-' + day + ' ' + hour + ':' + minutes + ':' + seconds
+        })
+
+
+        // vue 入口
+        var vm = new Vue({
+            el: "#app",
+            data: {
+                list: [
+                    { id: 1, name: '奔驰', time: new Date() },
+                    { id: 2, name: '奔奔', time: new Date() },
+                    { id: 3, name: '奔奔x', time: new Date() },
+                    { id: 4, name: '宝马', time: new Date() },
+                    { id: 5, name: '宝马x', time: new Date() },
+                    { id: 6, name: '宝宝', time: new Date() },
+                ],
+                id: '',
+                name: '',
+                keywords: ''
+            },
+            methods: {
+                add() {//添加方法
+                    var car = ({
+                        id: this.id,
+                        name: this.name,
+                        time: new Date()
+                    })
+                    this.list.push(car)
+                    this.id = this.name = ''
+                },
+                del(id) {//删除方法
+
+                    //some 循环 根据指定的条件进行判断 如果返回true 则终止循环
+                    this.list.some((item, i) => {
+                        if (item.id == id) {
+                            this.list.splice(i, 1)
+                            return true;
+                        }
+                    });
+
+                    //findIndex 方法直接查找索引 根据索引删除
+
+                    // var index = this.list.findIndex((item) => {
+                    //     if(item.id == id){
+                    //         return true;
+                    //     }
+                    // })
+                    // this.list.splice(index, 1)
+                },
+                search(keywords) {//搜索关键字
+                    // var newList = []
+                    // this.list.forEach(item => {
+                    //     //判断搜索的名称 包不包含 该关键字 包含不等于-1
+                    //     if(item.name.indexOf(keywords) != -1){
+                    //         newList.push(item)
+                    //     }
+                    // });
+                    // return newList
+
+                    //另一种实现方式 es6 新增
+                    return this.list.filter(item => {
+                        if (item.name.includes(keywords)) {
+                            return item
+                        }
+                    });
+                }
+            },
+            filters: {//自定义过滤器 
+                //如果 全局的过滤器 和自定义的过滤器名称相同 则优先调用自定义的过滤器
+                //就近原则
+                dateFormat: function (dateStr) {
+                    var date = new Date(dateStr)
+
+                    //padStart 可以在前边补0 第一个参数为总长度 第二个为 补的东西
+                    var year = date.getFullYear()
+                    var month = (date.getMonth() + 1).toString().padStart(2, 0)
+                    var day = date.getDate().toString().padStart(2, 0)
+                    var hour = date.getHours().toString().padStart(2, 0)
+                    var minutes = date.getMinutes().toString().padStart(2, 0)
+                    var seconds = date.getSeconds().toString().padStart(2, 0)
+
+                    return year + '-' + month + '-' + day + ' ' + hour + ':' + minutes + ':' + seconds
+                }
+            },
+            //定义私有指令
+            //和自定义过滤器相似 就近原则
+            directives:{
+                // 'color':{
+                //     bind:function(el,binding){
+                //         el.style.color = 'pink'
+                //     },
+                //     inserted:function(el,binding){
+
+                //     }
+                // }
+                //简写指令 
+                //等同于 在 bind 和 updated 这两个钩子函数中各写了一份
+                'fontsize':function(el,binding){
+                    el.style.color  = parseInt(binding.value) + 'px'
+                }
+            }
+        })
+    </script>
+</body>
+
+</html>
+

15.过滤器的使用.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+    <div id="app">
+        <!-- 
+            调用过滤器 
+            必须在差值表达式或v-bind中使用
+            调用格式: {{msg | 过滤器名称 | 过滤器名称}}
+            可以调用多个过滤器 可以传多个参数 但注意过滤器的第一个参数是 要处理的数据 function(data)
+        -->
+        <p>{{ msg | msgFormat('昨天') | test() }}</p>
+    </div>
+    <script>
+        //自定义过滤器 
+        Vue.filter( 'msgFormat' , function(msg,arg1){
+            return  msg.replace(/今天/g , arg1)
+        })
+
+        Vue.filter('test' , function(data){
+            return data + "123" 
+        })
+
+        var vm = new Vue({
+            el: '#app',
+            data: {
+                msg: '今天是2019年6月2日,星期日,天气多云,今天和昨天一样,今天.....',
+            },
+            methods:{
+
+            }
+        })
+    </script>
+</body>
+</html>
+

16.vue-recourse基本使用.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+    <!-- vue-resource.js 依赖于 vue.js 导入的时候 注意顺序 -->
+    <script src="./lib/vue-resource.js"></script>
+</head>
+<body>
+   <div id="app">
+        <input value="get方式" @click="getInfo" type="button">
+        <input value="post方式" @click="postInfo" type="button">
+        <input value="jsonp方式" @click="jsonpInfo" type="button">
+        <p>{{message}}</p>
+   </div>
+   <script>
+
+       // 设置全局的根域名
+       // 如果配置了全局的根域名,则在每次单独发起调用http的请求的时候,请求的url应以相对路径开头 前边不能带"/",否则不会启用根路径作拼接
+       Vue.http.options.root = "http://jsonplaceholder.typicode.com"
+
+       //设置全局的 emulateJson 选项
+       Vue.http.options.emulateJSON = true
+
+       var vm = new Vue({
+           el: '#app',
+           data:{
+            message:"这里显示控制台的输出"
+           },
+           methods:{
+            getInfo(){
+                this.$http.get('users').then(function(result){
+                    console.log(result.body)
+                    this.message = result.body
+                })
+            },
+            postInfo(){
+                //post 默认表单格式
+                // 第三个参数为: emulateJSON : true 表单格式 相当于设置  application/x-www-form-urlencoded
+                //第二个参数为: options 可选参数
+                this.$http.post('users',{},{emulateJSON: true}).then(result =>{
+                    console.log(result.body)
+                    this.message = result.body
+                })
+            },
+            jsonpInfo(){
+                this.$http.jsonp('users').then(result => {
+                    console.log(result.body)
+                    this.message = result.body
+                })
+            }
+
+           }
+       })
+   </script>
+</body>
+
+</html>
+

17.todoSthing.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+    <link rel="stylesheet" href="./lib/bootstrap.css">
+</head>
+
+<body>
+    <div id="app">
+        <div class="panel-body form-inline">
+            ID: <input type="text" v-model="id" class="form-control">
+            Name: <input type="text" v-model="name" class="form-control">
+            Age:<input type="text" v-model="age" class="form-control" @keydown="doAdd($event)">
+            <input type="button" value="添加" class="btn btn-primary" @click="add()">
+        </div>
+        <table class="table table-bordered table-hover table-striped">
+            <thead>
+                <tr>
+                    <th>ID</th>
+                    <th>姓名</th>
+                    <th>年龄</th>
+                    <th>时间</th>
+                    <th>操作</th>
+                </tr>
+            </thead>
+            <tbody>
+                <tr v-for="(item,key) in list" :key="item.id">
+                    <td>{{item.id}}</td>
+                    <td>{{ item.name }}</td>
+                    <td>{{ item.age }}</td>
+                    <td>{{ item.time}}</td>
+                    <td><a href="" @click.prevent="del(item.id)">删除</a></td>
+                </tr>
+            </tbody>
+        </table>
+
+    </div>
+    <script>
+
+        // 自定义键盘码
+        Vue.config.keyCodes.f3 = 113
+
+        var vm = new Vue({
+            el: '#app',
+            data: {
+                id: '',
+                name: '',
+                age: '',
+                list: [{
+                    id: '1',
+                    name: '张三',
+                    age: '18',
+                    time: new Date()
+                }]
+            },
+            methods: {
+                add() {
+                    var user = ({
+                        id: this.id,
+                        name: this.name,
+                        age: this.age,
+                        time: new Date()
+                    })
+                    this.list.push(user)
+                    this.id = this.name = this.age = ''
+                },
+                doAdd(e) {
+                    if (e.keyCode == 13)
+                        this.add()
+                },
+                del(id) {
+                    this.list.some(
+                        (item, i) => {
+                            if (item.id == id) {
+                                this.list.splice(i, 1)
+                                return true;
+                            }
+                        }
+                    )
+                },
+            },
+
+        })
+    </script>
+</body>
+
+</html>
+

18.动画animate.css的使用.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+    <link rel="stylesheet" href="./lib/animate.css">
+    <!-- <link rel="stylesheet" href="https://raw.githubusercontent.com/daneden/animate.css/master/animate.css"> -->
+</head>
+
+<body>
+    <div id="app">
+        <!-- 使用 :duration="{enter:300,leave:300}" 来设置出场离场的时间 -->
+        <input type="button" value="animate基本使用" @click="flag = !flag">
+        <transition enter-active-class="bounceIn" leave-active-class="bounceOut" :duration="{enter:300,leave:300}">
+            <h4 v-if="flag" class="animated">animate的基本使用</h4>
+        </transition>
+    </div>
+
+</body>
+
+<script>
+    var vm = new Vue({
+        el: "#app",
+        data: {
+            flag: false
+        }
+    })
+</script>
+
+</html>
+

19.使用钩子函数完成半场动画.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+    <link rel="stylesheet" href="./lib/animate.css">
+    <style>
+        .boll {
+            width: 20px;
+            height: 20px;
+            border-radius: 50%;
+            background-color: red;
+        }
+    </style>
+</head>
+
+<body>
+    <div id="app">
+        <input @click="flag = !flag" type="button" value="加入购物车">
+        <!--  -->
+        <transition @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter">
+            <div class="boll" v-if="flag"></div>
+        </transition>
+    </div>
+</body>
+<script>
+    var vm = new Vue({
+        el: "#app",
+        data: {
+            flag: false
+        },
+        methods: {
+            // 动画执行钩子函数第一个参数: el 表示 JavaScript原生的dom元素 ,可理解为通过 document.getElememtById得到的dom对象   
+            // 动画入场前
+            beforeEnter(el) {
+                // 设置小球的初始位置
+                el.style.transform = "translate(0, 0)"
+            },
+            //表示动画开始之后的样式,这里可以设置小球完成动画之后的结束状态
+            enter(el,done) {
+                // offsetTop没有实际意义 可以理解为offsetTop 强制会刷新动画
+                el.offsetTop
+                // 设置动画过渡时间
+                el.style.transition = "all 1s ease"
+                //设置小球结束位置
+                el.style.transform = "translate(150px,450px)"
+                // done() 相当于afterEnter函数的调用 
+                done()
+            },
+            afterEnter(el) {
+                this.flag = !this.flag
+            }
+        },
+    })
+</script>
+
+</html>
+

20.使用transform-group.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>animate动画效果</title>
+    <script src="./lib/vue.min.js"></script>
+    <link rel="stylesheet" href="./lib/animate.css">
+    <style>
+        li {
+            margin: 10px;
+            border: 1px #ccc solid;
+            font-size: 12px;
+            width: 100%;
+            padding: 10px;
+            list-style: none;
+        }
+
+        li:hover {
+            background-color: hotpink;
+            transition: all .6s ease;
+        }
+
+        .v-enter,
+        .v-leave-to {
+            opacity: 0;
+            transform: translateY(80px);
+        }
+
+        .v-enter-active,
+        .v-leave-active {
+            transition: all 0.6s ease;
+        }
+
+        .v-move {
+            transition: all .6s ease;
+        }
+
+        .v-leave-active {
+            position: absolute;
+        }
+    </style>
+</head>
+
+<body>
+    <div id="app">
+        <div>
+            ID: <input type="text" v-model="id">
+            姓名: <input type="text" v-model="name">
+            <input type="button" value="添加" @click="add()">
+        </div>
+
+        <!-- 在实现列表过渡的时候,如果过渡的元素是通过v-for循环渲染出来的,不能使用transition包裹,需要使用transitionGroup -->
+        <!--使用 transition-group v-for必须指明 :key 属性  -->
+        <!-- appear实现入场效果 tag 外围标签渲染ul标签 如果不指定外围标签渲染为span元素-->
+        <transition-group appear tag="ul">
+            <li v-for="(item,index) in list" :key="item.id" @click="del(index)" title="点我删除哦~">
+                ID:{{item.id}}-----姓名:{{ item.name}}
+            </li>
+        </transition-group>
+
+
+
+    </div>
+</body>
+<script>
+    var vm = new Vue({
+        el: "#app",
+        data: {
+            id: '',
+            name: '',
+            list: [
+                { id: 1, name: "张三" },
+                { id: 2, name: "李四" },
+                { id: 3, name: "王五" },
+                { id: 4, name: "赵六" },
+                { id: 5, name: "孙七" },
+            ]
+        },
+        methods: {
+            //添加功能
+            add() {
+                this.list.push({ id: this.id, name: this.name })
+                this.id = this.name = ''
+            },
+            //删除功能
+            del(index) {
+                this.list.splice(index, 1)
+            }
+        },
+    })
+</script>
+
+</html>
+

21.vue中的组件.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head>
+
+<body>
+    <div id="app">
+        <!-- 组件使用 直接使用标签即可 -->
+        <!-- 第一种方式创建组件使用 -->
+        <test1></test1>
+        <!-- 第二种方式创建组件使用 -->
+        <test2></test2>
+        <!-- 第三种方式创建组件使用 -->
+        <test3></test3>
+        <!-- 私有组件 -->
+        <personalcom></personalcom>
+        <!-- 组件中的data测试 -->
+        <test4></test4>
+    </div>
+
+    <!-- 第三种方式创建组件 详情看JavaScript-->
+    <template id="testTemplate">
+        <div>
+            <h4>这是第三种创建组件的方式</h4>
+        </div>
+    </template>
+
+    <!-- 定义私有组件 -->
+    <template id="personal">
+        <div>
+            <h4>这是定义的私有组件</h4>
+        </div>
+    </template>
+
+    <template id="testData">
+        <div>
+            <h4>组件中data测试---{{msg}}</h4>
+        </div>
+    </template>
+</body>
+<script>
+
+    //第一种方式创建组件 通过 Vue.extend来创建全局的组件模板
+    //给组件起名字 初始化组件 注意组件命名.如果组件名称使用驼峰命名,则在引用组件的时候需要把大写字母改成小写字母,同时两个单词之间用 '-' 连接
+    Vue.component('test1', Vue.extend({
+        // template 指定组件要展示的HTML结构
+        template: "<h4>这是第一种方式创建组件</h4>"
+    }))
+
+    //第二种创建组件的方式 直接通过component创建组件
+    // 注意: 无论通过那种方式创建组件  组件的template 模板内容,必须有且只能有唯一的一个根元素
+    Vue.component('test2', ({
+        template: "<h4>这是创建组件的第二种方式</h4>"
+    }));
+
+    //第三种方式创建组件  在 #app 外 创建
+    Vue.component('test3', {
+        template: '#testTemplate'
+    });
+
+    //组件中data测试
+    // 组件中可以存在data, data必须为一个方法, data必须返回一个对象,组件中data的用法和vue中的用法一样
+    Vue.component('test4', {
+        template: '#testData',
+        data: () => {
+            return {
+                msg: "组件中data的数据"
+            }
+        }
+    })
+
+
+    var vm = new Vue({
+        el: "#app",
+        data: {
+
+        },
+        methods: {
+
+        },
+        //私有组件
+        components: {
+            personalcom: {
+                template: "#personal"
+            }
+        },
+    });
+</script>
+
+</html>
+

22.vue组件切换-方式1.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head>
+
+
+<body>
+    <div id="app">
+        <input @click="flag=true" type="button" value="login">
+        <input @click='flag=false' type="button" value="register">
+        <login v-if='flag'></login>
+        <register v-else='flag'></register>
+    </div>
+
+    <!-- 登录组件测试 -->
+    <template id="login">
+        <div>
+            <h3>登录组件</h3>
+        </div>
+    </template>
+
+    <!-- 注册组件测试 -->
+    <template id="register">
+        <div>
+            <h3>注册组件</h3>
+        </div>
+    </template>
+</body>
+<script>
+    var vm = new Vue({
+        el: "#app",
+        data: {
+            flag:false
+        },
+        components: {
+            login: {
+                template: '#login'
+            },
+            register: {
+                template: '#register'
+            }
+        }
+    })
+</script>
+
+</html>
+

23.vue组件切换-方式2.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head>
+
+
+<body>
+    <div id="app">
+        <a href="" @click.prevent='componentName="login"'>显示登录组件</a>
+        <a href="" @click.prevent='componentName="register"'>显示注册组件</a>
+        <component :is="componentName"></component>
+    </div>
+
+    <!-- 登录组件测试 -->
+    <template id="login">
+        <div>
+            <h3>登录组件</h3>
+        </div>
+    </template>
+
+    <!-- 注册组件测试 -->
+    <template id="register">
+        <div>
+            <h3>注册组件</h3>
+        </div>
+    </template>
+</body>
+<script>
+    var vm = new Vue({
+        el: "#app",
+        data: {
+            componentName:'login'
+        },
+        components: {
+            login: {
+                template: '#login'
+            },
+            register: {
+                template: '#register'
+            }
+        }
+    })
+</script>
+
+</html>
+

24.vue组件中动画的切换.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+    <style>
+
+        .v-enter,
+        .v-leave-to{
+            opacity: 0;
+            transform: translateX(100px);
+        }
+
+        .v-enter-active,
+        .v-leave-active{
+            transition: all 0.5s ease;
+        }
+    </style>
+</head>
+
+
+<body>
+    <div id="app">
+        <a href="" @click.prevent='componentName="login"'>显示登录组件</a>
+        <a href="" @click.prevent='componentName="register"'>显示注册组件</a>
+
+        <!-- 通过mode 属性,设置组件切换时候的模式 -->
+        <transition mode='out-in'>
+                <component :is="componentName"></component>
+        </transition>
+    </div>
+
+    <!-- 登录组件测试 -->
+    <template id="login">
+        <div>
+            <h3>登录组件</h3>
+        </div>
+    </template>
+
+    <!-- 注册组件测试 -->
+    <template id="register">
+        <div>
+            <h3>注册组件</h3>
+        </div>
+    </template>
+</body>
+<script>
+    var vm = new Vue({
+        el: "#app",
+        data: {
+            componentName:'login'
+        },
+        components: {
+            login: {
+                template: '#login'
+            },
+            register: {
+                template: '#register'
+            }
+        }
+    })
+</script>
+
+</html>
+

25.父子组件传值.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head>
+
+<body>
+    <div id="app">
+        <p>父子组件传值测试</p>
+        <!-- 父组件可以在引用子组件的时候,通过v-bind绑定的形式,把需要传递给子组件的数据,传递到子组件的内部,共子组件使用 -->
+        <test v-bind:parentmsg="msg"></test>
+    </div>
+    <template id="test">
+        <div>
+            <h4>这是子组件-----{{parentmsg}}</h4>
+            <input type="button" value="点我修改值" @click="hello">
+        </div>
+    </template>
+</body>
+<script>
+    var vm = new Vue({
+        el: '#app',
+        data: {
+            msg: '父组件的值'
+        },
+        components: {
+            test: {
+                template: '#test',
+                //定义在props里边的数据都是只读数据 不可修改
+                //把父组件传递过来的parentmsg属性,现在props数组里边定义一下,这样才能使用这个数据
+                props: ['parentmsg'],
+                //定义在data里边的数据 并不是通过父组件传递过来的 而是私有的
+                //data里边的数据都是可读可写的
+                data() {
+                    return {
+                        commsg: '这是组件data里边的数据'
+                    }
+                },
+
+                methods: {
+                    hello() {
+                        this.parentmsg = "修改后的值~"
+                    }
+                },
+            },
+        }
+    });
+</script>
+
+</html>
+

26.子组件获取父组件中的方法.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head> 
+<body>
+    <div id="app">
+        <com1 @func='show'></com1>
+    </div>
+    <template id="template1">
+        <div>
+            <h4>这是子组件</h4>
+            <input type="button" value="点我" @click='com1show'>
+        </div>
+    </template>
+</body>
+<script>
+    var com1 = {
+        template: '#template1',
+        data() {
+            return {
+                msg: { name: 'test', age: 22 }
+            }
+        },
+        methods: {
+            com1show() {
+                // emit 触发调用父元素中绑定的方法
+                this.$emit('func', this.msg)
+            }
+        },
+    }
+
+    var vm = new Vue({
+        el: '#app',
+        components: {
+            com1
+        },
+        methods: {
+            show(data) {
+                console.log(data)
+            }
+        },
+    });
+</script>
+
+</html>
+

27.实现用户评论功能.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+    <link href="./lib/bootstrap.css" rel="stylesheet">
+    </script>
+</head>
+
+<body>
+    <div id="app">
+        <comment @func="loadComments"></comment>
+
+        <ul class="list-group">
+            <li class="list-group-item" v-for="item in list" :key="item.id">
+                <span class="badge">评论人:{{ item.name }}</span>
+                {{ item.content }}
+            </li>
+        </ul>
+
+    </div>
+    
+    <template id="templateComment">
+        <div class="">
+            评论人:<input type="text" class="form-control" v-model='name'>
+            评论内容:<textarea class="form-control" v-model='content'></textarea>
+            <input type="button" value="发布评论" class="btn btn-primary" @click="postComment">
+        </div>
+    </template>
+
+</body>
+<script>
+
+    //用户评论组件
+    var cmts = {
+        data() {
+            return {
+                name: '',
+                content: ''
+            }
+        },
+        template: '#templateComment',
+        methods: {
+            postComment() {
+            //创建评论对象
+            var comment = {
+                id: Date.now(),
+                name: this.name,
+                content: this.content
+            }
+
+            //从localstorage获取数据
+            var list =  JSON.parse(localStorage.getItem('cmts') || '[]')
+            //添加到list
+            list.unshift(comment)
+            //存到localstorage 
+            localStorage.setItem('cmts',JSON.stringify(list))
+            //清空input
+            this.name = this.content = ''
+
+            this.$emit('func')
+        }
+        },
+        
+    }
+
+    var vm = new Vue({
+        el: '#app',
+        data: {
+            list: [
+                { id: Date.now(), name: '测试1', content: "这是一条评论!" },
+                { id: Date.now(), name: '测试2', content: "23333333!" },
+                { id: Date.now(), name: '测试3', content: "helloworld!" }
+            ]
+        },
+        created() {
+            //自动刷新评论列表
+            this.loadComments()
+        },
+        methods: {
+            loadComments() {
+                this.list = JSON.parse(localStorage.getItem('cmts') || '[]')
+            }
+        },
+        components: {
+            'comment': cmts
+        }
+    })
+</script>
+
+</html>
+

28.vue中ref的使用.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+    <div id="app">
+        <templ ref="templ"></templ>
+        <input type="button" value="点我" @click="func">
+        <h4 ref="myh4">测试</h4>
+    </div>
+    <template id="templ">
+        <div>
+            <h4>ref获取组件测试,请点击按钮后,按F12检查</h4>
+        </div>
+    </template>
+</body>
+<script>
+
+    var templ = {
+        template:'#templ',
+        data() {
+            return {
+                msg:"这是组件中的msg"
+            }
+        },
+    }
+
+    var vm = new Vue({
+        el:'#app',
+        data:{
+
+        },
+        methods: {
+            func(){
+                console.log(this.$refs.templ.msg)
+                console.log("这是获取页面中的dom==>"+this.$refs.myh4.innerText)
+            }
+        },
+        components:{
+            templ
+        }
+    })
+</script>
+</html>
+

29.路由-路由的基本使用.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+    <script src="./lib/vue-router.js"></script>
+    <style>
+        /* 设置router-link的样式  
+        也可以设置默认的 router-link-exact-active 类的样式
+        */
+        .myActive {
+            color: red;
+            font-size: 30px;
+            font-weight: 500;
+        }
+
+        .v-enter,
+        .v-leave-to {
+            opacity: 0;
+            transform: translateX(150px);
+        }
+
+        .v-enter-active,
+        .v-leave-active {
+            transition: all .5s ease;
+        }
+    </style>
+</head>
+
+<body>
+    <div id="app">
+        <!-- <a href="#/login">登录</a>
+        <a href="#/register">注册</a> -->
+        <!-- 会默认渲染a标签 -->
+        <router-link to="/login">登录[routerlink]</router-link>
+        <router-link to="/register">注册[routerlink]</router-link>
+        <!-- 这是vue-router提供的组件,专门用来当做占位符,将来路由规则匹配到的组件,就会展示到这个router-view中 -->
+        <transition mode='out-in'>
+            <router-view></router-view>
+        </transition>
+
+    </div>
+</body>
+<script>
+
+    var login = {
+        template: '<h2>登录组件</h2>'
+    }
+
+    var register = {
+        template: '<h2>注册组件</h2>'
+    }
+
+
+    //创建路由对象
+    const routerobj = new VueRouter({
+        //路由的匹配规则
+        // 每个路由规则都是一个对象  这个规则对象身上必须有两个属性
+        //属性1: path,表示监听那个路由的连接
+        // 属性2: component: 表示如果路由匹配到的path,则展示对应的component
+        routes: [
+            //重定向 根路径 到登录组件
+            { path: '/', redirect: login },
+            { path: '/login', component: login },
+            { path: '/register', component: register }
+        ],
+
+        //修改router-link样式
+        linkActiveClass: 'myActive'
+    })
+    new Vue({
+        el: '#app',
+        //将路由对象挂载到vue上
+        router: routerobj
+    })
+</script>
+
+</html>
+

30.路由-路由的参数传递.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+    <script src="./lib/vue-router.js"></script>
+    <style>
+        /* 设置router-link的样式  
+        也可以设置默认的 router-link-exact-active 类的样式
+        */
+        .myActive {
+            color: red;
+            font-size: 30px;
+            font-weight: 500;
+        }
+
+        .v-enter,
+        .v-leave-to {
+            opacity: 0;
+            transform: translateX(150px);
+        }
+
+        .v-enter-active,
+        .v-leave-active {
+            transition: all .5s ease;
+        }
+    </style>
+</head>
+
+<body>
+    <div id="app">
+        <!-- <a href="#/login">登录</a>
+        <a href="#/register">注册</a> -->
+        <!-- 会默认渲染a标签 -->
+        <router-link to="/login?id=99&name=zs">登录[routerlink]</router-link>
+        <router-link to="/register/20/ls">注册[routerlink]</router-link>
+        <!-- 这是vue-router提供的组件,专门用来当做占位符,将来路由规则匹配到的组件,就会展示到这个router-view中 -->
+        <transition mode='out-in'>
+            <router-view></router-view>
+        </transition>
+
+    </div>
+</body>
+<script>
+
+    var login = {
+        //第一种方式显示参数
+        template: '<h2>登录组件 id:{{$route.query.id}} 姓名:{{$route.query.name}}</h2>',
+        created() {
+            console.log(this.$route)
+        },
+    }
+
+    var register = {
+        //第二种方式显示参数
+        template: '<h2>注册组件 年龄:{{$route.params.age}} 姓名:{{$route.params.name}}</h2>'
+    }
+
+    var index = {
+        template: '<h2>index首页</h2>'
+    }
+
+    //创建路由对象
+    const routerobj = new VueRouter({
+        //路由的匹配规则
+        // 每个路由规则都是一个对象  这个规则对象身上必须有两个属性
+        //属性1: path,表示监听那个路由的连接
+        // 属性2: component: 表示如果路由匹配到的path,则展示对应的component
+        routes: [
+            //重定向 根路径 到登录组件  注意重定向路径要写字符串
+            { path: '/', redirect:'/index' },
+            { path: '/index', component: index },
+            { path: '/login', component: login },
+            { path: '/register/:age/:name', component: register }
+        ],
+
+        //修改router-link样式
+        linkActiveClass: 'myActive'
+    })
+    new Vue({
+        el: '#app',
+        //将路由对象挂载到vue上
+        router: routerobj
+    })
+</script>
+
+</html>
+

31.路由-嵌套子路由.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+    <script src="./lib/vue-router.js"></script>
+</head>
+
+<body>
+    <div id="app">
+        <router-link to="/account">跳转到account组件</router-link>
+        <router-view></router-view>
+    </div>
+
+    <template id="templ">
+        <div>
+            <h2>account组件</h2>
+            <router-link to="/account/login">登录</router-link>
+            <router-link to="/account/register">注册</router-link>
+
+            <router-view></router-view>
+        </div>
+    </template>
+</body>
+<script>
+
+    var account = {
+        template: '#templ'
+    }
+
+    var login = {
+        template: '<h3>登录组件</h3>'
+    }
+
+    var register = {
+        template: '<h3>注册组件</h3>'
+    }
+
+    const router = new VueRouter({
+        routes: [
+            {
+                path: '/account',
+                component: account,
+                // 子路由的嵌套
+                children: [
+                    {
+
+                        path: 'register', component: register
+                    },
+                    {
+                        path: 'login', component: login,
+                    }
+                ]
+            },
+        ]
+    })
+
+    new Vue({
+        el: '#app',
+        router
+    })
+</script>
+
+</html>
+

32.路由-使用视图实现经典布局.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+    <script src="./lib/vue-router.js"></script>
+    <style>
+        html,body{
+            margin: 0;
+            padding: 0;
+            width: 100%;
+            height: 100%;
+        }
+        h1 {
+            margin: 0;
+            padding: 0;
+        }
+
+        .header {
+            height: 100px;
+            background-color: blanchedalmond;
+        }
+        .container{
+            display: flex;
+            height: 653px;
+        }
+
+        .left {
+            flex: 2;
+            background-color: chocolate;
+        }
+        .main{
+            flex: 8;
+            background-color: darkgoldenrod;
+        }
+    </style>
+</head>
+
+<body>
+    <div id="app">
+        <router-view></router-view>
+        <div class="container">
+            <router-view name='left'></router-view>
+            <router-view name='main'></router-view>
+        </div>
+
+    </div>
+</body>
+<script>
+
+    var header = {
+        template: '<h1 class="header">头部组件<h1>'
+    }
+    var leftBox = {
+        template: '<h1 class="left">左边导航组件<h1>'
+    }
+    var main = {
+        template: '<h1 class="main">中间内容组件<h1>'
+    }
+
+    const router = new VueRouter({
+        routes: [
+            {
+                //设置路由匹配规则
+                path: '/', components: {
+                    'default': header,
+                    'left': leftBox,
+                    'main': main
+                }
+            }
+        ]
+    })
+
+    new Vue({
+        el: '#app',
+        router
+    })
+</script>
+
+</html>
+

33.路由-监控路由watch基本使用.html

+
<!DOCTYPE html>
+<html lang="en">
+
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+    <script src="./lib/vue-router.js"></script>
+</head>
+
+<body>
+    <div id="app">
+        <router-link to="/login">登录[routerlink]</router-link>
+        <router-link to="/register">注册[routerlink]</router-link>
+        <router-view></route-view>
+    </div>
+</body>
+<script>
+
+    var login = {
+        template: '<h3>登录组件</h3>'
+    }
+
+    var register = {
+        template: '<h3>注册组件</h3>'
+    }
+
+    const router = new VueRouter({
+        routes: [
+            {
+                path: '/login', component: login
+            },
+
+            {
+                path: '/register', component: register
+            }
+        ]
+    })
+
+    new Vue({
+        el: "#app",
+        router,
+        //watch 监控
+        watch: {
+            '$route.path': (newVal, oldVal) => {
+                // console.log("路由的newVal==>"+newVal)
+                // console.log("路由的oldVal==>"+oldVal)
+                if(newVal === '/login' )
+                console.log("登录组件")
+                if(newVal === '/register' )
+                console.log("注册组件")
+            }
+        }
+    })
+</script>
+
+</html>
+

34.computed的使用.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta http-equiv="X-UA-Compatible" content="ie=edge">
+    <title>Document</title>
+    <script src="./lib/vue.min.js"></script>
+</head>
+<body>
+    <div id="app">
+        <input type="text" v-model='firstName'>+
+        <input type="text" v-model='lastName'>=
+        <input type="text" v-model='fullName'>
+    </div>
+</body>
+<script>
+    new Vue({
+        el:'#app',
+        data:{
+            firstName:'',
+            lastName:'',
+        },
+        computed: {
+            // 计算的结果会被缓存起来
+            //计算属性在引用的时候一定不要加 () 直接把他当做普通的属性去调用
+            // 只要这个计算属性的function内部的data发生了变化,就会立即重新计算这个属性的值
+            fullName(){
+               return  this.lastName +'----'+ this.firstName
+            }
+        },
+    })
+</script>
+</html>
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/posts/essays/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277/index.html" "b/blog-site/public/posts/essays/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277/index.html" new file mode 100644 index 00000000..4f7d3b90 --- /dev/null +++ "b/blog-site/public/posts/essays/\345\211\215\347\253\257\345\255\246\344\271\240\350\267\257\347\272\277/index.html" @@ -0,0 +1,294 @@ + + + + + + + + + + + | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

+ 0001.01.01 +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/index.html b/blog-site/public/posts/index.html new file mode 100644 index 00000000..7bada0d7 --- /dev/null +++ b/blog-site/public/posts/index.html @@ -0,0 +1,793 @@ + + + + + + + + + + + Posts | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2024
+
+ +
+
+ 前端学习路线 +
02-29
+
+
+ +
+ +
2023
+
+ +
+
+ 2023工作总结 +
12-01
+
+
+ +
+
+ 20230915简历 +
09-15
+
+
+ +
+
+ 定时任务可视化管理 +
09-09
+
+
+ +
+
+ SpringBoot整合nacos +
09-04
+
+
+ +
+
+ 整合文件上传功能 +
08-11
+
+
+ +
+
+ 整合支付功能 +
08-10
+
+
+ +
+
+ Validator参数校验 +
07-01
+
+
+ + + +
+
+ 重构一个程序 +
04-20
+
+
+ +
+
+ SpringMVC与SpringWebFlux +
04-14
+
+
+ +
+
+ MySQL详解 +
03-13
+
+
+ +
+
+ 如何减少及解决bug +
03-10
+
+
+ +
+
+ Elasticsearch详解 +
02-14
+
+
+ +
+
+ 编程常用词汇汇总 +
02-13
+
+
+ +
+ +
2022
+
+ +
+
+ 接口优化 +
12-20
+
+
+ +
+
+ 孙子兵法 +
12-19
+
+
+ + + +
+
+ 整洁的代码 +
09-01
+
+
+ +
+
+ 关于编程的书籍 +
08-08
+
+
+ + + +
+
+ 20220422简历 +
04-22
+
+
+ +
+
+ Java小程序集合 +
04-09
+
+
+ +
+ +
2021
+
+ +
+
+ 数据结构与算法 +
12-10
+
+
+ + + +
+
+ 网络编程 +
11-19
+
+
+ +
+
+ MQ详解 +
10-19
+
+
+ +
+
+ Java集合 +
10-04
+
+
+ +
+
+ Java反射 +
10-02
+
+
+ + + +
+
+ 分布式事务详解 +
08-02
+
+
+ +
+
+ Object类方法 +
07-10
+
+
+ +
+
+ 微服务治理 +
06-21
+
+
+ +
+
+ Redis详解 +
06-17
+
+
+ +
+
+ Spring详解 +
05-13
+
+
+ + + +
+
+ JVM-垃圾回收器 +
05-06
+
+
+ +
+
+ Java多线程 +
05-05
+
+
+ +
+
+ HashMap详解 +
05-03
+
+
+ +
+
+ JVM-相关概念 +
04-27
+
+
+ +
+
+ 面试中常见的问题 +
04-23
+
+
+ +
+
+ JVM-垃圾回收 +
04-21
+
+
+ +
+
+ JVM-执行引擎 +
04-15
+
+
+ +
+
+ JVM-直接内存 +
04-14
+
+
+ +
+
+ JVM-Java对象 +
04-12
+
+
+ +
+
+ Java语法糖 +
04-10
+
+
+ +
+
+ JavaIO +
04-09
+
+
+ +
+
+ JVM-方法区 +
04-08
+
+
+ +
+
+ JVM-堆 +
04-03
+
+
+ +
+
+ JVM-本地方法栈 +
04-02
+
+
+ +
+
+ JVM-本地方法接口 +
04-02
+
+
+ +
+
+ JVM-虚拟机栈 +
03-28
+
+
+ +
+
+ JVM-程序计数寄存器 +
03-27
+
+
+ +
+
+ JVM-JVM介绍 +
03-05
+
+
+ +
+
+ Nginx介绍 +
03-04
+
+
+ +
+
+ 道德经 +
03-03
+
+
+ +
+
+ 面向对象 +
02-15
+
+
+ +
+
+ JVM-Java类加载机制 +
02-05
+
+
+ +
+
+ Java运算 +
01-30
+
+
+ +
+
+ Java数据类型 +
01-20
+
+
+ +
+
+ Java异常 +
01-13
+
+
+ +
+ +
2020
+
+ +
+
+ 20201124简历 +
11-24
+
+
+ +
+
+ SpringBoot整合docker +
08-30
+
+
+ +
+
+ SpringBoot整合kafka +
08-20
+
+
+ +
+
+ 线程状态及创建方式 +
04-20
+
+
+ +
+
+ Java中常用到的锁 +
04-07
+
+
+ +
+
+ Docker介绍 +
04-07
+
+
+ + + +
+
+ CAS原理 +
04-04
+
+
+ +
+
+ SpringBoot整合redis +
03-01
+
+
+ + + +
+ +
2019
+
+ +
+
+ 2019工作总结 +
12-01
+
+
+ +
+
+ Vue2.0学习笔记 +
05-23
+
+
+ +
+ +
2018
+
+ +
+
+ Js雪花飘落 +
12-25
+
+
+ +
+
+ Js下雨特效 +
12-10
+
+
+ +
+
+ Js换肤特效 +
11-14
+
+
+ +
+
+ Js折纸导航栏 +
10-25
+
+
+ +
+
+ Js表白神器 +
10-14
+
+
+ +
+
+ Js懒加载 +
09-21
+
+
+ +
+
+ Js五子棋 +
09-10
+
+
+ +
+
+ Js滑块拖拽 +
09-08
+
+
+ +
+
+ Js生日礼物 +
08-24
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/index.xml b/blog-site/public/posts/index.xml new file mode 100644 index 00000000..a0dbbd88 --- /dev/null +++ b/blog-site/public/posts/index.xml @@ -0,0 +1,586 @@ + + + + Posts on 唯手熟尔 + http://localhost:1313/iblog/posts/ + Recent content in Posts on 唯手熟尔 + Hugo -- gohugo.io + zh + Thu, 29 Feb 2024 00:00:00 +0000 + + + 前端学习路线 + http://localhost:1313/iblog/posts/essays/front-learning-route/ + Thu, 29 Feb 2024 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/front-learning-route/ + 基础知识 网络知识 HTTP DNS 域名 云服务 网络安全 HTTPS CORS 网络渗透 OWASP HTML CSS JavaScript JQuery Ajax ES6-ES11 综合应用 工程化体系 代码规范 CSS预处理器 Less Sass PostCSS Node Promise Axios 工具 包管理工具 Npm Yarn 打包工具 Webpack Parcel 代码格式化工具 ESLint Prettier 调试工具 Chrome IETest Postman 版本管理工具 Git GitLab GitHub 部署发布工具 Jenkins CICD 主流技术 TypeScript Vue React Angular 综合应用 静态 + + + 2023工作总结 + http://localhost:1313/iblog/posts/worksummary/work-summary-2023/ + Fri, 01 Dec 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/worksummary/work-summary-2023/ + 在职期间,我主要负责耐材项目的开发与维护,共迭代171个版本。通过与团队成员的紧密合作,我们按时完成了项目中的需求。在这段时间里,我不断提升自己的专业技能和知识,增强了自己的专业能力。我始终认为团队合作是成功的关键。在工作中,我积极与同事沟 + + + 20230915简历 + http://localhost:1313/iblog/posts/resume/interview-resume-20230915/ + Fri, 15 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-resume-20230915/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和四年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对Jav + + + 定时任务可视化管理 + http://localhost:1313/iblog/posts/essays/scheduled-job/ + Sat, 09 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/scheduled-job/ + 代码实现 代码结构 pom &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-quartz&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;/dependency&gt; 库表结构 -- ---------------------------- -- 定时任务调度表 -- ---------------------------- drop table if exists sys_job; create table sys_job ( job_id bigint(20) not null auto_increment comment &#39;任务ID&#39;, job_name varchar(64) default &#39;&#39; comment &#39;任务名称&#39;, job_group varchar(64) default &#39;DEFAULT&#39; comment &#39;任务组名&#39;, invoke_target varchar(500) not null comment + + + SpringBoot整合nacos + http://localhost:1313/iblog/posts/spring/springboot-nacos/ + Mon, 04 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-nacos/ + nacos nacos下载 下载地址 一键傻瓜试安装即可,官网写的很清楚这里不在赘述 http://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html nacos启动 将模式改为单机模式 启动成功 nacos相关配置 demo-dev.yaml server: port: 8001 config: info: &#34;config info for dev from nacos config center&#34; demo-test.yaml server: port: 3333 config: info: &#34;config info for test from nacos config center&#34; user.yaml user: name: zs1112222 age: 10 address: 测试地址 代码 整合nacos配置中心,注册 + + + 整合文件上传功能 + http://localhost:1313/iblog/posts/essays/uploadfile-code/ + Fri, 11 Aug 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/uploadfile-code/ + 结构 pom.xml fastdfs-client-java-1.27.jar:点击下载 &lt;dependencies&gt; &lt;!-- fastdfs --&gt; &lt;dependency&gt; &lt;groupId&gt;org.csource&lt;/groupId&gt; &lt;artifactId&gt;fastdfs-client-java&lt;/artifactId&gt; &lt;version&gt;1.27&lt;/version&gt; &lt;systemPath&gt;${project.basedir}/lib/fastdfs-client-java-1.27.jar&lt;/systemPath&gt; &lt;scope&gt;system&lt;/scope&gt; &lt;/dependency&gt; &lt;!--aliyun oss 依赖--&gt; &lt;dependency&gt; &lt;groupId&gt;com.aliyun.oss&lt;/groupId&gt; &lt;artifactId&gt;aliyun-sdk-oss&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;5.8.11&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;commons-io&lt;/groupId&gt; &lt;artifactId&gt;commons-io&lt;/artifactId&gt; &lt;version&gt;2.11.0&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; application.yml server: port: 80 公共部分 FileManagement public interface FileManagement { /** * 设置下一个bean的对象 * * @param nextFileManagement 下一个 + + + 整合支付功能 + http://localhost:1313/iblog/posts/essays/pay-code/ + Thu, 10 Aug 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/pay-code/ + 结构 pom.xml &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;5.8.11&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.alipay.sdk&lt;/groupId&gt; &lt;artifactId&gt;alipay-sdk-java&lt;/artifactId&gt; &lt;version&gt;4.9.9&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.github.binarywang&lt;/groupId&gt; &lt;artifactId&gt;weixin-java-pay&lt;/artifactId&gt; &lt;version&gt;4.5.0&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; application.yml server: port: 8080 pay: wechat: #微信公众号或者小程序等的appid appId: &#34;&#34; #微信支付商户号 mchId: &#34;&#34; #微信支付商户密钥 mchKey: &#34;&#34; #服务商模式下的子商户公众账号ID subAppId: #服务商模式下的子商户号 subMchId: # p12证书的位 + + + Validator参数校验 + http://localhost:1313/iblog/posts/essays/springboot-validator/ + Sat, 01 Jul 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/springboot-validator/ + 常见参数校验 在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,最简单就是用if条件语句来判断,但是随着参数越来越多,业务越来越复杂,判断参数代码语句显得尤为冗长. 或者有些程序会将if封装起来,例如spring中 + + + 管道流设计模式结合业务 + http://localhost:1313/iblog/posts/essays/pipeline-business/ + Thu, 15 Jun 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/pipeline-business/ + 流程图 代码实现 pom &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.plugin&lt;/groupId&gt; &lt;artifactId&gt;spring-plugin-core&lt;/artifactId&gt; &lt;version&gt;${spring.plugin.core.version}&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;${hutool.version}&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; context EventContext public interface EventContext { /** * 是否继续调用链 */ boolean continueChain(); /** * 获取当前过滤器选择器 */ FilterSelector getFilterSelector(); } BizType public interface BizType { /** * 获取业务类型码值 */ Integer getCode(); /** * 业务类型名称 * */ String getName(); } AbstractEventContext public abstract class AbstractEventContext implements EventContext{ private final BizType businessType; private final FilterSelector filterSelector; protected AbstractEventContext(BizType businessType, FilterSelector filterSelector) { + + + 重构一个程序 + http://localhost:1313/iblog/posts/essays/java-project-reconstitution/ + Thu, 20 Apr 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-project-reconstitution/ + 什么是重构 摘自《重构:改善既有代码的设计》 重构(名词形式): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。 重构(动词形式): 使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构。 重 + + + SpringMVC与SpringWebFlux + http://localhost:1313/iblog/posts/spring/java-spring-mvc-webflux/ + Fri, 14 Apr 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/java-spring-mvc-webflux/ + Spring MVC Spring Web MVC是建立在Servlet API上的原始Web框架,从一开始就包含在Spring框架中。正式名称 &ldquo;Spring Web MVC &ldquo;来自其源模块的名称(spring-webmvc),但它更常被称为 &ldquo;Spring MVC&rdquo;。 SpringMVC是基于S + + + MySQL详解 + http://localhost:1313/iblog/posts/essays/sql-select-fast/ + Mon, 13 Mar 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/sql-select-fast/ + 逻辑架构 主要分为:连接层,服务层,引擎层,存储层。 客户端执行一条select命令的流程如下: 连接层:最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcplip的通信。主要完成一些类似于连接处理、 + + + 如何减少及解决bug + http://localhost:1313/iblog/posts/essays/java-bugs/ + Fri, 10 Mar 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-bugs/ + bug的起源: 1945年,一只小飞蛾钻进了计算机电路里,导致系统无法工作,一位名叫格蕾丝·赫柏的人把飞蛾拍死在工作日志上,写道:就是这个 bug(虫子),害我们今天的工作无法完成——于是,bug一词成了电脑系统程序的专业术语,形容那些系统中的 + + + Elasticsearch详解 + http://localhost:1313/iblog/posts/essays/elasticsearch/ + Tue, 14 Feb 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/elasticsearch/ + 概览 Elasticsearch,简称为 ES, ES是一个开源的高扩展的分布式全文搜索引擎, 是整个 ElasticStack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。 Elastic Stack, 包括 Elasticsearch、 Ki + + + 编程常用词汇汇总 + http://localhost:1313/iblog/posts/essays/java-dict/ + Mon, 13 Feb 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-dict/ + QPS 即 Queries Per Second的缩写,每秒能处理查询数目。是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。 TPS 即 Transactions Per Second的缩写,每秒处理的事务数目。一个事务是指一个客户机向服务器发送请求然后服务 + + + 接口优化 + http://localhost:1313/iblog/posts/essays/java-improve/ + Tue, 20 Dec 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-improve/ + 接口优化 线上接口很慢,线上生产问题,我们绝对不能马虎放过抱着侥幸心理,必须要找到根本原因及时处理,防止下次留下更大的坑.大致思路要定位接口问题,然后具体问题具体分析,讨论不同解决方案. 定位问题 要快速定位接口哪一个环节比较慢,性能瓶颈在哪里, + + + 孙子兵法 + http://localhost:1313/iblog/posts/books/books-sunzibingfa/ + Mon, 19 Dec 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/books/books-sunzibingfa/ + 始计 兵者,国之大事,死生之地,存亡之道,不可不察也。 故经之以五事,校之以计,而索其情:一曰道,二曰天,三曰地,四曰将,五曰法。道者,令民于上同意,可与之死,可与之生,而不危也;天者,阴阳、寒暑、时制也;地者,远近、险易、广狭、死生也;将者, + + + IDEA常用配置及使用技巧 + http://localhost:1313/iblog/posts/essays/dev-idea/ + Fri, 16 Dec 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/dev-idea/ + 下载 工欲善其事必先利其器,一个好的开发工具,能极大提高开发效率. 新UI很漂亮。IDEA 2022.2.3 官方下载地址: https://www.jetbrains.com/zh-cn/idea/download/other.html 激活工具 百度云下载. 链接:https://pan.baidu.com/s/19sCUTCBXvwXgEQc8vX-SYQ?pwd=gwu + + + 整洁的代码 + http://localhost:1313/iblog/posts/essays/clean-code/ + Thu, 01 Sep 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/clean-code/ + 为什么要写整洁的代码 为什么要写整洁的代码,回答这个问题之前,也许应该想想写糟糕的代码的原因 是想快点完成吗?还是要赶时间吗?有可能.或许你觉得自己要干好所需要的时间不够;假使花时间清理代码,老板就会大发雷霆.或许你只是不耐烦再搞这套程序,期望 + + + 关于编程的书籍 + http://localhost:1313/iblog/posts/books/java-books/ + Mon, 08 Aug 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/books/java-books/ + HeadFirst设计模式 Java数据结构和算法 Java核心技术卷I基础知识 Java编程思想 代码整洁之道 大型网站技术架构 大话数据结构 深入分析JavaWeb技术内幕 疯狂Java讲义 重构:改善既有代码的设计 领域驱动设计 深入理解计算机系统 Java + + + 如何做好程序设计功能 + http://localhost:1313/iblog/posts/essays/java-design/ + Tue, 02 Aug 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-design/ + 产品需求澄清、PN排期及任务分解 开发设计评审 功能设计流程图 与外部系统交互、本系统模块之间流程,比较好用的画圈软件draw .io或在线的process on 数据库设计 从DDD角度界限上下文、ER图、评审表结构设计是否合理,表的关联关系是否合理、是 + + + 20220422简历 + http://localhost:1313/iblog/posts/resume/interview-resume-20220422/ + Fri, 22 Apr 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-resume-20220422/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和三年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对SQL + + + Java小程序集合 + http://localhost:1313/iblog/posts/toy/java-multi-gadget/ + Sat, 09 Apr 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/java-multi-gadget/ + 写在前面 本文中所涉及的程序均为Java开发,如果您想要直接使用这些工具需要提前配置Java环境。所涉及到的程序均提供完整代码,如果您有兴趣可以尝试运行。 使用java -jar命令启动 某些程序功能并不是很完善,但是也可以凑合着用,写这些程序的主 + + + 数据结构与算法 + http://localhost:1313/iblog/posts/essays/data-structures-algorithms/ + Fri, 10 Dec 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/data-structures-algorithms/ + 数据结构 数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好数据结构可以编写出更有效率的代码。数据结构是算法的基础,想要学好算法,就必须把数据结构学到位。 数据结构包括:线性结构、非线性结构。 线性结构作为最常用的数据结构, + + + 规范编写Java代码总结 + http://localhost:1313/iblog/posts/essays/java-code-rule/ + Thu, 25 Nov 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-code-rule/ + 编码规范 我们为什么要遵守规范来编码? 是因为通常在编码过程中我们不只自己进行开发,通常需要一个团队来进行,开发好之后还需要维护,所以编码规范就显的尤为重要。 代码维护时间比较长,那么保证代码可读性就显得很重要。作为一个程序员,咱们得有点追求和信 + + + 网络编程 + http://localhost:1313/iblog/posts/essays/net-program-java/ + Fri, 19 Nov 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/net-program-java/ + 网络协议 以下内容摘自百度百科: https://baike.baidu.com/item/网络协议/328636 https://baike.baidu.com/item/网络七层协议/6056879 网络协议指的是计算机网络中互相通信的对等实体 + + + MQ详解 + http://localhost:1313/iblog/posts/essays/java-mq/ + Tue, 19 Oct 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-mq/ + 概念 MQ 即 messagequeue 消息队列,是分布式系统的重要组件,主要解决异步消息,应用解耦,消峰等问题。从而实现高可用,高性能,可伸缩和最终一致性的架构。使用较多的MQ有:activeMQ,rabbitMQ,kafka,metaMQ。 MQ优点 异步消息处理:可以 + + + Java集合 + http://localhost:1313/iblog/posts/java/rookie-java-container/ + Mon, 04 Oct 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-java-container/ + 概述 Java中的集合主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 如果你看过ArrayList类源码,就知道ArrayList底层是通过数组来存储元素的,所以如果严格来说,数组也算集合的一种 + + + Java反射 + http://localhost:1313/iblog/posts/java/rookie-reflect/ + Sat, 02 Oct 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-reflect/ + 概述 什么是反射 在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。 反射是Java语言的一个特性,它允许程序 + + + 常见故障排查及程序配置 + http://localhost:1313/iblog/posts/essays/eye-beam/ + Wed, 08 Sep 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/eye-beam/ + 故障排查基础 收录Linux常用命令,以下命令来自https://www.bilibili.com/video/BV14A411378a 关机/重启/注销 常用命令 作用 shutdown -h now 即刻关机 shutdown -h 10 10分钟后关机 shutdown -h 11:00 11:00关机 shutdown -h +10 预定时间关机(10 + + + 分布式事务详解 + http://localhost:1313/iblog/posts/essays/java-transaction/ + Mon, 02 Aug 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-transaction/ + 基础概念 什么是事务 什么是事务?举个例子:你去超市买东西,&ldquo;一手交钱,一手交货&quot;就是一个事务的例子,交钱和交货必须同时成功,事务才算成功,其中有一个环节失败,事务将会撤销所有已成功的活动。 所以事务可以看作是一次重大的活动 + + + Object类方法 + http://localhost:1313/iblog/posts/java/rookie-objectclass-methods/ + Sat, 10 Jul 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-objectclass-methods/ + 概览 Object 类位于 java.lang 包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承Object,成为Object的子类。 Object类可以显示继承,也可以隐式继承,效果都是一样的。 class A extends Object{ // to do } class A { // to do } Java Objec + + + 微服务治理 + http://localhost:1313/iblog/posts/essays/java-small-service/ + Mon, 21 Jun 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-small-service/ + 什么是微服务架构 In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API。 These services are built around business capabilities and independently deployable by fully automated deployment machinery。 There is a bare minimum of centralized management of these services, which may be written in different programming + + + Redis详解 + http://localhost:1313/iblog/posts/essays/java-redis/ + Thu, 17 Jun 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-redis/ + Redis概述 参考文章: https://www.runoob.com/redis/redis-intro.html https://www.redis.com.cn/redis-interview-questions.html 什么是Redis Redis(Remote Dictionary Server) Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API 的非关系型数据库。 简而言之,Redis是一个可基于内存亦可持久 + + + Spring详解 + http://localhost:1313/iblog/posts/spring/java-spring/ + Thu, 13 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/java-spring/ + 概览 Spring是一个轻量级的Java开源框架,为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IOC)和面向切面(AOP)。 简单来说,Spring是一个分层的JavaSE/EE 一站式轻量级开源框架。在每一层都提供支持。 + + + 面试Java可能会被问到的问题 + http://localhost:1313/iblog/posts/resume/interview-junior-javaer/ + Tue, 11 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-junior-javaer/ + 面试必问 自我介绍一下 你有什么职业规划 你为什么要离职 说一下你的优缺点 你的期望薪资是多少 你为什么要选择我们公司 你能否接受加班 你有对象了吗 你还有什么问题要问的吗 基础 说一下UDP、TCP及http与https 如何保证线程安全 线程池工作原理 如何避免死 + + + JVM-垃圾回收器 + http://localhost:1313/iblog/posts/jvm/java-garbage-collector/ + Thu, 06 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/java-garbage-collector/ + 垃圾回收器分类 垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。 由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。 Java不同版本新特性学习思路: 语法层面:Lambda表达式、s + + + Java多线程 + http://localhost:1313/iblog/posts/java/rookie-multi-thread/ + Wed, 05 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-multi-thread/ + 相关概念 线程与进程 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。例如,一个正在运行的程序的实例就是一个进程。 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 一条线程指的是进程中一个单一 + + + HashMap详解 + http://localhost:1313/iblog/posts/essays/java-hashmap/ + Mon, 03 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-hashmap/ + 相关概念 capacity: 容量,默认16; loadFactor: 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容; threshold: 阈值;阈值 + + + JVM-相关概念 + http://localhost:1313/iblog/posts/jvm/jvm-about/ + Tue, 27 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-about/ + 内存溢出 内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。 官方文档中对内存溢出的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。 由于GC一 + + + 面试中常见的问题 + http://localhost:1313/iblog/posts/resume/interview-questions-and-answers/ + Fri, 23 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-questions-and-answers/ + 面试常见问题 自我介绍 个人经历可以进行适当包装,但是不能造假,一方面如果一旦被人拆穿,后果就不用我说了吧,另一方面如果你说谎,说了一些你自己不感兴趣的项目,在入职之后可能会被分配到该项目上; 在介绍的时候要说明你对面试的公司有什么用,根据不同类 + + + JVM-垃圾回收 + http://localhost:1313/iblog/posts/jvm/java-garbage-collection/ + Wed, 21 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/java-garbage-collection/ + 垃圾回收 垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。 垃圾收集机制是Java的招牌能力,极大地提高了开发效率。 如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展 + + + JVM-执行引擎 + http://localhost:1313/iblog/posts/jvm/jvm-execute-engine/ + Thu, 15 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-execute-engine/ + 概述 执行引擎是Java虚拟机核心的组成部分之一,属于JVM的下层,里面包括 解释器、及时编译器、垃圾回收器。 “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力, 其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统 + + + JVM-直接内存 + http://localhost:1313/iblog/posts/jvm/jvm-direct-memory/ + Wed, 14 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-direct-memory/ + 直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。 直接内存是在Java堆外的、直接向系统申请的内存区间。 操作直接内存演示代码: public class MainTest { public static void main(String[] args) { ByteBuffer allocate = ByteBuffer.allocate(1024 * 1024 * 1024); System.out.println(&# + + + JVM-Java对象 + http://localhost:1313/iblog/posts/jvm/java-object/ + Mon, 12 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/java-object/ + 对象实例化 对象的创建方式 使用new关键字创建:最常见的方式、单例类中调用getInstance的静态类方法,XXXFactory的静态方法; 使用反射方式创建: 使用Class的newInstance方法:在JDK9里面被标记为过时的方法,因为 + + + Java语法糖 + http://localhost:1313/iblog/posts/essays/java-syntax-sugar/ + Sat, 10 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-syntax-sugar/ + 原文地址:https://www.jianshu.com/p/0f967298a5d7 语法糖 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法, 这种语法对语言的功能并没有 + + + JavaIO + http://localhost:1313/iblog/posts/java/rookie-io/ + Fri, 09 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-io/ + 概念 Java IO通过数据流、序列化和文件系统提供系统输入和输出。 IO,即 in 和 out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。 传统的 IO 是通过流技术来处理的。 流(Stream),是一个抽象的概念, + + + JVM-方法区 + http://localhost:1313/iblog/posts/jvm/jvm-method-area/ + Thu, 08 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-method-area/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-堆 + http://localhost:1313/iblog/posts/jvm/jvm-heap/ + Sat, 03 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-heap/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-本地方法接口 + http://localhost:1313/iblog/posts/jvm/jvm-native-interface/ + Fri, 02 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-native-interface/ + 概念 简单地讲,一个Native Methodt是一个Java调用非Java代码的接囗。 一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。 这个特征并非Java所特有,很多其它的编程语言都有这一机制,比 + + + JVM-本地方法栈 + http://localhost:1313/iblog/posts/jvm/jvm-native-stack/ + Fri, 02 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-native-stack/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-虚拟机栈 + http://localhost:1313/iblog/posts/jvm/jvm-stack/ + Sun, 28 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-stack/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-程序计数寄存器 + http://localhost:1313/iblog/posts/jvm/jvm-pc-register/ + Sat, 27 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-pc-register/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-JVM介绍 + http://localhost:1313/iblog/posts/jvm/jvm-start/ + Fri, 05 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-start/ + 为什么要学习JVM 大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。 一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要, + + + Nginx介绍 + http://localhost:1313/iblog/posts/essays/nginx-start/ + Thu, 04 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/nginx-start/ + Nginx介绍 Nginx (&ldquo;engine x&rdquo;)是一个高性能的HTTP和反向代理服务器,特点是占有内存少,并发能力强,事实上Nginx的并发能力确实在同类型的网页服务器中表现较好. Nginx专为性能优化而开发,性能是其最重要的考量,实现上非常注重效率, + + + 道德经 + http://localhost:1313/iblog/posts/books/books-daodejing/ + Wed, 03 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/books/books-daodejing/ + 第一章 道可道,非常道。名可名,非常名。 无名天地之始﹔有名万物之母。 故常无,欲以观其妙﹔常有,欲以观其徼。 此两者,同出而异名,同谓之玄。 玄之又玄,众妙之门。 第二章 天下皆知美之为美,斯恶已。 皆知善之为善,斯不善已。 有无相生,难易相成,长短相形, + + + 面向对象 + http://localhost:1313/iblog/posts/java/rookie-object-oriented/ + Mon, 15 Feb 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-object-oriented/ + 面向对象是一种编程思想,包括三大特性和六大原则,其中,三大特性指的是封装、继承和多态;六大原则指的是单一职责原则、开放封闭原则、迪米特原则、里氏替换原则、依赖倒置原则以及接口隔离原则,其中,单一职责原则是指一个类应该是一组相关性很高的函数和 + + + JVM-Java类加载机制 + http://localhost:1313/iblog/posts/jvm/jvm-classloader/ + Fri, 05 Feb 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-classloader/ + 类加载过程 在Java中,类加载器把一个类装入JVM中,要经过以下步骤: 加载、验证、准备、解析和初始化。其中验证,准备,解析统称为连接。 这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。 类加载器只负责class文 + + + Java运算 + http://localhost:1313/iblog/posts/java/rookie-operation/ + Sat, 30 Jan 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-operation/ + 运算符与表达式 运算符 运算符指明对操作数的运算方式。组成表达式的Java操作符有很多种。运算符按照其要求的操作数数目来分,可以有单目运算符、双目运算符和三目运算符,它们分别对应于1个、2个、3个操作数。 种类 运算符按其功能来分:有算术运算符、赋 + + + Java数据类型 + http://localhost:1313/iblog/posts/java/rookie-datatype/ + Wed, 20 Jan 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-datatype/ + 基本类型 Java语言提供了八种基本类型。六种数值类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. 俗称4类8种 这里只介绍称4类8种.实际上,JAVA中还存在另外一种基本类型 void,它也有对应的包装类java.lang.Void + + + Java异常 + http://localhost:1313/iblog/posts/java/rookie-exception/ + Wed, 13 Jan 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-exception/ + 异常类型 Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:Error 和 Exception。 其中 Error 用来表示Java程序无法处理的错误;这类错误一般与硬件有关,与程序本身无关,通常由系统进行处理,程序本身无法捕获和处理。是不可控制的。 Exception 分为两种 + + + 20201124简历 + http://localhost:1313/iblog/posts/resume/interview-resume-20201124/ + Tue, 24 Nov 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-resume-20201124/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 专业技能 熟练使用 SSM,SpringBoot等框架技术; 熟练使用HTML,CSS等相关技术; 有Redis,VUE相关使用经验; 有对接第三方系统,调用外系统相关经验; 熟悉 MySQL,ORACLE.基本操作,熟练使 + + + SpringBoot整合docker + http://localhost:1313/iblog/posts/spring/springboot-docker/ + Sun, 30 Aug 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-docker/ + MacOS上安装docker 下载 国内下载网站: http://get.daocloud.io 不推荐下载docker版本太旧了 官网下载: https://docs.docker.com/get-started/#download-and-install-docker 或用homebrew进行下载安装 brew install --cask --appdir=/Applications docker 配置镜像 由于网速原因,可以配置一下国内的镜像加速器 中科大镜像: https://docker.mirrors.ustc.edu.cn 网易: https://hub-mirror.c.163.com 阿里云: https://&lt;你 + + + SpringBoot整合kafka + http://localhost:1313/iblog/posts/spring/springboot-kafka/ + Thu, 20 Aug 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-kafka/ + kafka介绍 kafka官网: http://kafka.apache.org kafka中文官网: https://kafka.apachecn.org Kafka是一种分布式的,基于发布/订阅的消息系统。主要特点如下: 以时间复杂度为O(1)的方式提供消息持久化能力,并保证即使对TB级以上数据也能保证常数时间的访问性能 高吞吐率。即使在非常 + + + 线程状态及创建方式 + http://localhost:1313/iblog/posts/essays/thread-state-and-created/ + Mon, 20 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/thread-state-and-created/ + 线程状态及转换 线程状态共包含6种,6中状态又可以互相的转换。 新建状态(New): 创建了线程后尚未启动; 可运行状态(Runnable): 可能正在运行,也可能正在等待 CPU 时间片。包含了运行中(Running)和 就绪(Ready)状态; 就绪(Rea + + + Docker介绍 + http://localhost:1313/iblog/posts/essays/docker-start/ + Tue, 07 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/docker-start/ + docker是什么 Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前流行的 Linux 容器解决方案。 Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样 + + + Java中常用到的锁 + http://localhost:1313/iblog/posts/essays/java-lock/ + Tue, 07 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-lock/ + 公平锁 指多个线程按照申请锁的顺序来获取锁类似排队打饭 先来后到 优点: 所有的线程都能得到资源,不会饿死在队列中。 缺点: 吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。 非公平锁 指在多线程获取锁的顺序并 + + + Java中集合的线程不安全问题 + http://localhost:1313/iblog/posts/essays/java-thread-collection/ + Sun, 05 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-thread-collection/ + ArrayList ArrayList线程不安全示例: public static void main(String[] args) { ArrayList&lt;String&gt; arrayList = new ArrayList&lt;&gt;(); for(int i=0; i&lt; 3; i++) { new Thread(() -&gt; { arrayList.add(UUID.randomUUID().toString()); System.out.println(arrayList); },String.valueOf(i)).start(); } } // ConcurrentModificationException 同步修改异常 Exception in thread &#34;8&#34; java.util.ConcurrentModificationException [null, 2041b613-8068-4ddd-9d01-305f5680d377] [null, 2041b613-8068-4ddd-9d01-305f5680d377, b3e0296d-e263-4632-a023-4267cdec5e25] [null, 2041b613-8068-4ddd-9d01-305f5680d377] 原因分析: 当某个线程正在执行 add()方法时,被某个线程打断,添加到一半被打断,没有被添加完 解决方案: 使用Vec + + + CAS原理 + http://localhost:1313/iblog/posts/essays/cas-principle/ + Sat, 04 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/cas-principle/ + CAS CAS全称为Compare and Swap被译为比较并交换。是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。 java.util.concurrent.atomic 并发包下的所有原子类都是基于 CAS 来实现的。 以 AtomicInteger 原子整型类为例。 public class MainTest { public static void main(String[] args) { new AtomicInteger().compareAndSet(1,2); } } 以上面的代码为例 + + + SpringBoot整合redis + http://localhost:1313/iblog/posts/spring/springboot-redis/ + Sun, 01 Mar 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-redis/ + Redis介绍 redis是开源的一个高性能的 key-value 数据库。 主要特点 Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用 Redis支持数据的备份,即master-slave模式的数据备份 Redis 可以存储键与5种不同 + + + SpringBoot整合elasticsearch + http://localhost:1313/iblog/posts/spring/springboot-elasticsearch/ + Sun, 09 Feb 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-elasticsearch/ + 安装elasticsearch 要注意导入依赖的版本和安装elasticsearch的版本与springboot的兼容问题 用 docker 安装 elasticsearch 本例用elasticsearch-6.5.3和springboot-2.1.0.RELEASE版本 下载镜像: docker + + + 2019工作总结 + http://localhost:1313/iblog/posts/worksummary/work-summary-2019/ + Sun, 01 Dec 2019 00:00:00 +0000 + http://localhost:1313/iblog/posts/worksummary/work-summary-2019/ + 本人在进入公司起,期间一直对自己要求严谨,遵守公司的相应制度. 在过去的一个月时间里,我参与了贵州银行的电子验印系统的开发,一直努力完成和完善分配给我的任务,在这一个月发现了自身还有很多的不足,所以抱着虚心学习的态度,学习公司的开发流程,了解 + + + Vue2.0学习笔记 + http://localhost:1313/iblog/posts/essays/vue2-note/ + Thu, 23 May 2019 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/vue2-note/ + 参考资料 vue官方文档: https://cn.vuejs.org/v2/guide vue参考视频资料: https://www.bilibili.com/video/av50680998 vue菜鸟教程文档: https://www.runoob.com/vue2/vue-tutorial.html vue-组件 参考资料: https://cn.vuejs.org/v2/guide/components.html#ad 组件是可复用的 Vue 实例,且带有一个名字. 组件的出现是为了拆分vue实例的代码量,能够让我们以不同的组件,来划分不同的功能模块,将来我们需要什么样的功 + + + Js雪花飘落 + http://localhost:1313/iblog/posts/toy/js-snow/ + Tue, 25 Dec 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-snow/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;snow&lt;/title&gt; &lt;/head&gt; &lt;style&gt; html { width: 100%; } body { margin: 0; padding: 0; overflow-y: hidden; width: 100%; } .header { width: 100%; height: 315px; background: url(&#34;images/header-bg.png&#34;) repeat; } .snow { position: relative; height: inherit; width: 960px; background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) no-repeat 0 0;; margin: 0 auto; animation: auto 10s linear infinite; } /* 下雪动画 插入两个背景图片*/ @keyframes auto { from { background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) repeat 0 0; } to { background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) repeat 0 1000px; } } tree, snow { position: absolute; } tree { width: 112px; height: 137px; background: url(&#34;images/tree.png&#34;); + + + Js下雨特效 + http://localhost:1313/iblog/posts/toy/js-rain/ + Mon, 10 Dec 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-rain/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt; rain &lt;/title&gt; &lt;style&gt; html { width: 100%; } body { width: 100%; margin: 0; padding: 0; background-color: #000; } .rain { display: block; } embed { display: block; } &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;!-- 2、使用hidden=&#34;true&#34;表示隐藏音乐播放按钮,相反使用hidden=&#34;false&#34;表示开启音乐播放按钮。 3、使用a + + + Js换肤特效 + http://localhost:1313/iblog/posts/toy/js-trans-skin/ + Wed, 14 Nov 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-trans-skin/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;换肤特效&lt;/title&gt; &lt;style type=&#34;text/css&#34;&gt; body { margin: 0; background-image: url(&#34;images/1.jpg&#34;); background-size: cover; } ul { margin: 0; padding: 0; list-style-type: none; } .bg-list { display: none; margin: 0; width: 100%; height: 200px; background: rgba(0, 0, 0, 0.5); } .img-wrap { height: 200px; display: flex; justify-content: space-around; align-items: center; } .tab-btn { background-image: url(&#34;images/upseek.png&#34;); height: 50px; width: 50px; position: fixed; top: 0; right: 0; } .tab-btn:hover { background-position-y: -63.6px; } &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;div class=&#34;bg-list&#34;&gt; &lt;ul class=&#34;img-wrap&#34;&gt; &lt;li class=&#34;img-item&#34; data-src=&#34;images/1.jpg&#34;&gt; &lt;img src=&#34;images/1-1.jpg&#34; width=&#34;160px&#34;/&gt; &lt;/li&gt; + + + Js折纸导航栏 + http://localhost:1313/iblog/posts/toy/js-paper-folding/ + Thu, 25 Oct 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-paper-folding/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;折纸导航栏&lt;/title&gt; &lt;/head&gt; &lt;style&gt; *{ margin: 0; padding: 0; } .content{ position: relative; width: 400px; height: 30px; margin: 50px auto; /*-webkit-perspective: 1000px; -moz-perspective: 1000px; -ms-perspective: 1000px;*/ perspective: 1000px;/*景深相当于眼睛距离元素的位置距离*/ } .content .open{ transform: rotateX(0); animation: open 1s linear; } @keyframes open { 0%{ transform: rotateX(-90deg); } 20%{ transform:rotateX(30deg); } 40%{ transform:rotateX(-60deg); } 60%{ transform:rotateX(60deg); } 80%{ transform:rotateX(-30deg); + + + Js表白神器 + http://localhost:1313/iblog/posts/toy/js-love-heart/ + Sun, 14 Oct 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-love-heart/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;love&lt;/title&gt; &lt;style&gt; *{ margin: 0; padding: 0; } body{ background-color: #000; background-size: cover; overflow-y: hidden; } .love{ width: 400px; height: 400px; /*background-color: #7c7c7c;*/ margin: 130px auto; animation: move 1s infinite alternate; } @keyframes move { 100%{ transform: scale(1.5); } } .left{ float: left; width: 150px; height: 250px; background-color: #FF0000; border-radius: 75px 75px 0 5px; -webkit-transform: rotate(-45deg); -moz-transform: rotate(-45deg); -ms-transform: rotate(-45deg); -o-transform: rotate(-45deg); transform: rotate(-45deg); margin-left: 85px; box-shadow: 0 0 20px #FF0000; animation: shadow 1s infinite alternate; } @keyframes shadow { 100%{ box-shadow: 0 0 100px #FF0000; } } .right{ float: left; width: 150px; height: 250px; background-color: #FF0000; border-radius: 75px 75px 5px 0; -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -ms-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg); + + + Js懒加载 + http://localhost:1313/iblog/posts/toy/js-loadding-lazy/ + Fri, 21 Sep 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-loadding-lazy/ + index.html &lt;!doctype html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;meta name=&#34;Generator&#34; content=&#34;EditPlus®&#34;&gt; &lt;meta name=&#34;Author&#34; content=&#34;&#34;&gt; &lt;meta name=&#34;Keywords&#34; content=&#34;&#34;&gt; &lt;meta name=&#34;Description&#34; content=&#34;&#34;&gt; &lt;title&gt;懒加载技术&lt;/title&gt; &lt;style&gt; *{ margin: 0; padding:0; } body{ background: rgb(0,0,0); } .box{ overflow: hidden; width: 948px; background-color: #7c7c7c; margin: 50px auto; -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; } .box img{ float: left; display: block; width: 300px; height: 150px; margin: + + + Js五子棋 + http://localhost:1313/iblog/posts/toy/js-gomoku/ + Mon, 10 Sep 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-gomoku/ + index.html &lt;!DOCTYPE html PUBLIC &#34;-//W3C//DTD XHTML 1.0 Transitional//EN&#34; &#34;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&#34;&gt; &lt;html xmlns=&#34;http://www.w3.org/1999/xhtml&#34;&gt; &lt;head&gt; &lt;meta http-equiv=&#34;Content-Type&#34; content=&#34;text/html; charset=UTF-8&#34; /&gt; &lt;title&gt;五子棋&lt;/title&gt; &lt;meta name=&#34;viewport&#34; content=&#34;device-width; initial-scale=1.0;&#34; /&gt; &lt;style&gt; #c1 { display: block; margin: 60px auto; box-shadow: 1px 1px 5px #000000; } &lt;/style&gt; &lt;script src=&#34;js/index.js&#34;&gt;&lt;/script&gt; &lt;/head&gt; &lt;body&gt; &lt;canvas id=&#34;c1&#34; width=&#34;450px&#34; height=&#34;450px&#34;&gt;&lt;/canvas&gt; &lt;/body&gt; &lt;/html&gt; index.js window.onload = function(){ var oC = document.getElementById(&#39;c1&#39;); var oGc = oC.getContext(&#39;2d&#39;); var over = false; oGc.strokeStyle = &#34;#bfbfbf&#34;; //绘制棋盘 for(var i=0;i&lt;15;i++){ oGc.moveTo(15+i*30,15); oGc.lineTo(15+i*30,435); oGc.stroke(); oGc.moveTo(15,15+i*30); oGc.lineTo(435,15+i*30); oGc.stroke(); } /* AI难点解析 赢法 + + + Js滑块拖拽 + http://localhost:1313/iblog/posts/toy/js-box-drag/ + Sat, 08 Sep 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-box-drag/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;滑块拖拽&lt;/title&gt; &lt;/head&gt; &lt;style&gt; body { margin: 0; padding: 0; user-select: none; } .content { position: relative; width: 300px; height: 40px; margin: 50px auto; background-color: #E8E8EB; text-align: center; line-height: 40px; } .rect { position: absolute; width: 100%; height: 100%; } .rect .bg { position: absolute; left: 0; top: 0; z-index: 1; width: 0; height: 100%; background: rgba(122,194,60,.4); } .rect .move { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; width: 45px; height: 40px; position: absolute; top: 0; left: 0; background-color: #fff; border: 1px solid #cccccc; + + + Js生日礼物 + http://localhost:1313/iblog/posts/toy/js-birthday-gift/ + Fri, 24 Aug 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-birthday-gift/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;card&lt;/title&gt; &lt;style&gt; body,html{ width: 100%; height: 100%; } body{ display: flex;/*弹性盒模型*/ justify-content: center;/*水平对齐 盒子位于中心*/ align-items: center;/*竖直对齐 居中对齐*/ background-color: yellow; perspective: 1000px;/*景深:眼到屏幕的距离*/ } body,h1,p{ margin: 0; } .card{ width: 520px; height: 350px; border-radius: 15px; background: linear-gradient(#020333 70%,#fff 75%);/* + + + diff --git a/blog-site/public/posts/java/rookie-datatype/index.html b/blog-site/public/posts/java/rookie-datatype/index.html new file mode 100644 index 00000000..b6f8c2a7 --- /dev/null +++ b/blog-site/public/posts/java/rookie-datatype/index.html @@ -0,0 +1,2773 @@ + + + + + + + + + + + Java数据类型 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Java数据类型

+ 2021.01.20 +
+

基本类型

+

Java语言提供了八种基本类型。六种数值类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. 俗称4类8种

+

这里只介绍称4类8种.实际上,JAVA中还存在另外一种基本类型 void,它也有对应的包装类java.lang.Void,不过我们无法直接对它们进行操作.

+
+

一个Byte(字节)等于8个bit(位),bit是最小的单位,1B(字节)=8bit(位).
+一般情况下,一个汉字是两个字节,英文与数字是一个字节

+
+

数值类型

+

整型

+
    +
  • +

    byte/8

    +
      +
    • byte 数据类型是8位、有符号的,以二进制补码表示的整数;
    • +
    • 最小值是 -128(-2^7)
    • +
    • 最大值是 127(2^7-1)
    • +
    • 默认值是 0
    • +
    • byte 类型用在大型数组中节约空间,主要代替整数,因为byte变量占用的空间只有int类型的四分之一
    • +
    • 例子:byte a = 100,byte b = -50
    • +
    +
  • +
  • +

    short/16

    +
      +
    • short 数据类型是16位、有符号的以二进制补码表示的整数
    • +
    • 最小值是 -32768(-2^15)
    • +
    • 最大值是 32767(2^15 - 1)
    • +
    • Short 数据类型也可以像byte那样节省空间.一个short变量是int型变量所占空间的二分之一
    • +
    • 默认值是 0
    • +
    • 例子:short s = 1000,short r = -20000
    • +
    +
  • +
  • +

    int/32

    +
      +
    • int 数据类型是32位、有符号的以二进制补码表示的整数
    • +
    • 最小值是 -2,147,483,648(-2^31)
    • +
    • 最大值是 2,147,483,647(2^31 - 1)
    • +
    • 一般地整型变量默认为 int 类型
    • +
    • 默认值是 0
    • +
    • 例子:int a = 100000, int b = -200000
    • +
    +
  • +
  • +

    long/64

    +
      +
    • long 数据类型是 64 位、有符号的以二进制补码表示的整数
    • +
    • 最小值是 -9,223,372,036,854,775,808(-2^63)
    • +
    • 最大值是 9,223,372,036,854,775,807(2^63 -1)
    • +
    • 这种类型主要使用在需要比较大整数的系统上
    • +
    • 默认值是 0L
    • +
    • 例子: long a = 100000L,Long b = -200000L
    • +
    +
  • +
+

浮点型

+
    +
  • +

    float/32

    +
      +
    • float 数据类型是单精度32位.符合IEEE 754标准的浮点数
    • +
    • float 在储存大型浮点数组的时候可节省内存空间
    • +
    • 最小值1.4E-45,最大值3.4028235E38
    • +
    • 默认值是 0.0f
    • +
    • 浮点数不能用来表示精确的值,如货币
    • +
    • 例子:float f = 234.5f
    • +
    +
  • +
  • +

    double/64

    +
      +
    • double 数据类型是双精度、64位、符合IEEE754标准的浮点数
    • +
    • 浮点数的默认类型为double类型
    • +
    • double类型同样不能表示精确的值,如货币
    • +
    • 最小值4.9E-324,最大值1.7976931348623157E308
    • +
    • 默认值是 0.0d
    • +
    • 例子:double d = 123.4
    • +
    +
  • +
+

FloatDouble的最小值和最大值都是以科学记数法的形式输出的,结尾的"E+数字"表示E之前的数字要乘以10的多少次方.比如3.14E3就是3.14 × 10^3 =31403.14E-3 就是 3.14 x 10^-3 =0.00314.

+

字符类型

+
    +
  • char/16 +
      +
    • char类型是一个单一的 16 位 Unicode 字符
    • +
    • 最小值是 \u0000(即为 0)
    • +
    • 最大值是 \uffff(即为65、535)
    • +
    • 默认值是 \u0000
    • +
    • char 数据类型可以储存任何字符
    • +
    • 例子:char letter = 'A'
    • +
    +
  • +
+

布尔类型

+
    +
  • boolean/~ +
      +
    • boolean数据类型表示一位的信息
    • +
    • 只有两个取值:true 和 false
    • +
    • 这种类型只作为一种标志来记录 true/false 情况
    • +
    • 默认值是 false
    • +
    • 例子:boolean one = true
    • +
    +
  • +
+

boolean 只有两个值:true、false,可以使用 1 bit 来存储,但是具体大小没有明确规定。JVM 会在编译时期将 boolean 类型的数据转换为 int,使用 1 来表示 true,0 表示 false。JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。

+

类型转换

+

整型、实型(常量)、字符型数据可以混合运算。运算中,不同类型的数据先转化为同一类型,然后进行运算。

+

转换从低级到高级。

+
byte,short,char,int,long,float,double
+

数据类型转换必须满足如下规则:

+
    +
  • +

    不能对boolean类型进行类型转换。

    +
  • +
  • +

    不能把对象类型转换成不相关类的对象。

    +
  • +
  • +

    在把容量大的类型转换为容量小的类型时必须使用强制类型转换。

    +
  • +
  • +

    浮点数到整数的转换是通过舍弃小数得到,而不是四舍五入

    +
    (int)23.7 == 23;        
    +(int)-45.89f == -45
    +
  • +
  • +

    转换过程中可能导致溢出或损失精度,在运算时要避免该问题.例如:

    +
    // 因为 byte 类型是 8 位,最大值为127,所以当 int 强制转换为 byte 类型时,值 128 时候就会导致溢出
    +int i =128;   
    +byte b = (byte)i;
    +
  • +
+

自动类型转换

+

自动类型转换必须满足转换前的数据类型的位数要低于转换后的数据类型. 即可以 long l = 100;而不可以int l = 100L;

+
public class Test{
+        public static void main(String[] args){
+            char c1='a';//定义一个char类型
+            int i1 = c1;//char自动类型转换为int
+            System.out.println("char自动类型转换为int后的值等于"+i1);
+            char c2 = 'A';//定义一个char类型
+            int i2 = c2+1;//char 类型和 int 类型计算
+            System.out.println("char类型和int计算后的值等于"+i2);
+        }
+}
+// 结果:
+// char自动类型转换为int后的值等于97
+// char类型和int计算后的值等于66
+

隐式类型转换

+

Java 不能隐式执行向下转型,因为这会使得精度降低。

+

1.1 字面量属于 double 类型,不能直接将 1.1 直接赋值给 float 变量,因为这是向下转型。

+
float f = 1.1;
+

1.1f 字面量才是 float 类型。

+
float f = 1.1f;
+

因为字面量 1 是 int 类型,它比 short 类型精度要高,因此不能隐式地将 int 类型下转型为 short 类型。

+
short s1 = 1;
+s1 = s1 + 1;
+

但是使用 += 或者 ++ 运算符可以执行隐式类型转换。

+
s1 += 1;
+s1++;
+

上面的语句相当于将 s1 + 1 的计算结果进行了向下转型:

+
//强制类型转换
+s1 = (short) (s1 + 1);
+

简而言之,不能够直接的将精度高的类型 直接的赋值给精度低的类型.如: float f = 1.1; short s = 1; 如果想要赋值可用Java的隐式类型转换 如:float f+=1.1; s += 1;

+

由大到小需要强制转换,由小到大不需要强转. 顺序:byte , short , char , int ,long,float,double

+
byte b=1; int a = b;//由小到大
+int c = 1;
+byte d = (byte) c;//由大到小
+

包装类型

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
基本数据类型包装类
byteByte
booleanBoolean
shortShort
charCharacter
intInteger
longLong
floatFloat
doubleDouble
+

在这八个类名中,除了 IntegerCharacter 类以后,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写即可.

+

Integer、Long、Byte、Double、Float、Short都是抽象类Number的子类.

+

因为 Java 是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,在集合类中,我们是无法将 int 、double 等类型放进去的。因为集合的容器要求元素是 Object 类型.所以才有了对应基本类型分包装类型.

+

拆箱与装箱

+

基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。

+

Integer int 为例

+
Integer x = 2;     // 装箱 调用了 Integer.valueOf(2)
+int y = x;         // 拆箱 调用了 X.intValue()
+

自动拆装箱

+

自动装箱: 就是将基本数据类型自动转换成对应的包装类.

+

自动拆箱:就是将包装类自动转换成对应的基本数据类型.

+
    Integer i = 10;  //自动装箱
+    int b = i;     //自动拆箱
+

反编译得

+
    public static  void main(String[]args){
+        Integer integer=Integer.valueOf(1);
+        int i=integer.intValue();
+    }
+

int的自动装箱都是通过 Integer.valueOf() 方法来实现的,Integer 的自动拆箱都是通过 integer.intValue 来实现的

+
+

自动装箱都是通过包装类的 valueOf() 方法来实现的.自动拆箱都是通过包装类对象的 xxxValue() 来实现的。

+
+

自动拆装箱带来的问题

+
    +
  • +

    比较 +包装对象的数值比较,不能简单的使用==,虽然 -128 到 127 之间的数字可以,但是这个范围之外还是需要使用 equals方法进行比较.

    +
  • +
  • +

    NPE +因为有自动拆箱的机制,如果初始的包装类型对象为null,那么在自动拆箱的时候的就会报NullPointerException,在使用时需要格外注意.

    +
  • +
  • +

    内存浪费 +如果一个 for 循环中有大量拆装箱操作,会浪费很多资源

    +
  • +
+

缓存池

+

案例: new Integer(123) Integer.valueOf(123) 的区别在于:

+
    +
  • new Integer(123) 每次都会新建一个对象;
  • +
  • Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用
  • +
+
Integer x = new Integer(123);
+Integer y = new Integer(123);
+System.out.println(x == y);    // false
+Integer z = Integer.valueOf(123);
+Integer k = Integer.valueOf(123);
+System.out.println(z == k);   // true
+

valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容

+
public static Integer valueOf(int i) {
+    if (i >= IntegerCache.low && i <= IntegerCache.high)
+        return IntegerCache.cache[i + (-IntegerCache.low)];
+    return new Integer(i);
+}
+

在 Java 8 中,Integer 缓存池的大小默认为 -128~127

+
static final int low = -128;
+static final int high;
+static final Integer cache[];
+
+static {
+    // high value may be configured by property
+    int h = 127;
+    String integerCacheHighPropValue =
+        sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
+    if (integerCacheHighPropValue != null) {
+        try {
+            int i = parseInt(integerCacheHighPropValue);
+            i = Math.max(i, 127);
+            // Maximum array size is Integer.MAX_VALUE
+            h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
+        } catch( NumberFormatException nfe) {
+            // If the property cannot be parsed into an int, ignore it.
+        }
+    }
+    high = h;
+
+    cache = new Integer[(high - low) + 1];
+    int j = low;
+    for(int k = 0; k < cache.length; k++)
+        cache[k] = new Integer(j++);
+
+    // range [-128, 127] must be interned (JLS7 5.1.7)
+    assert IntegerCache.high >= 127;
+}
+

编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。

+
Integer m = 123;
+Integer n = 123;
+System.out.println(m == n); // true
+

Integer,int -127~128之前是不会创建新的对象的,即

+
 Integer a = new Integer(12);
+ int b = 12;
+ System.out.println(a==b);//true
+

Integerint自动装箱拆箱会通过valueOf()方法实现,当这个数在-127~128之间直接从缓存里边取,不会重新new对象

+

基本类型对应的缓冲池如下:

+
    +
  • boolean values true and false
  • +
  • all byte values
  • +
  • short values between -128 and 127
  • +
  • int values between -128 and 127
  • +
  • char in the range \u0000 to \u007F
  • +
+

在使用这些基本类型对应的包装类型时,如果该数值范围在缓冲池范围内,就可以直接使用缓冲池中的对象。==两种浮点数类型的包装类Float,Double并没有实现常量池技术。==

+
+

在 JDK 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的:
在启动JVM的时候,通过-XX:AutoBoxCacheMax=<size>来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache初始化的时候就会读取该系统属性来决定上界。

+
+

引用类型

+
    +
  • +
  • 数组类型
  • +
  • 接口类型
  • +
  • 注解类型
  • +
  • 枚举类型
  • +
+

引用类型指向一个对象(类似于C的指针),指向对象的变量是引用变量

+

所有引用类型的默认值都是null

+

与基本类型区别

+
    +
  1. +

    基本数据类型在被创建时,在栈中直接划分一块内存,将数值直接存入栈中,引用数据类型再被创建时,先在堆中开辟内存创建存放值,然后引用到栈中的是在堆中的地址值

    +
  2. +
  3. +

    传递参数的时候不同.基本数据类型是值传递,引用数据类型是引用传递.

    +
  4. +
+

注意事项

+
    +
  • String类是引用类型,不是基本类型
  • +
  • 默认double类型,如果float加后缀F(不区分大小写).如果long类型加后缀L(不区分大小写)
  • +
  • 浮点数可能是个近似值,不是精确值.如果想要表示精确用BigDecimal类型
  • +
+

常量

+

案例: 常量,变量和字面量

+
int a = 10; //a为变量,10为字面量
+final int b = 10;  //b为常量,10为字面量
+static c = "Hello World";  //c为变量,HelloWorld为字面量
+
    +
  • 常量:程序运行中,固定不变的量.
  • +
  • 变量:值可以修改的数据类型。
  • +
  • 字面量:字面量就是数据,是由字母、数字等构成的字符串或者数值,如30(整型)、3.15(浮点型)、“我是字符串”(字符串型)、‘中’(字符型)、true(布尔型)、false(布尔型)等,只能作为右值出现如int a = 10;这里的a是左值,10为右值.
  • +
+

字面量

+
+

在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串;而有很多也对布尔类型和字符类型的值也支持字面量表示;还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。

+
+

Java常量

+
+

JAVA常量就是在程序运行过程中一直不会改变的量的量称为常量.常量在整个程序中只能被赋值一次.

+
+

Java常量简单理解为final修饰的变量

+

定义常量

+

在 Java 中使用final关键字来修饰常量,声明方式和变量类似

+

要声明一个常量,第一需要制定数据类型,第二需要通过final关键字进行限定格式: +final 数据类型 常量名称[=值]

+

常量在程序运行时是不能被修改的(final作用).所以在定义常量时就需要对该常量进行初始化.为了与变量区别,常量取名一般都用大写字符

+
final double PI = 3.1415927;
+

final 关键字表示最终的,它可以修改很多元素,修饰变量就变成了常量.之后会详细说明final关键字

+
public class HelloWorld {
+    // 静态常量
+    public static final double PI = 3.14;
+    // 声明成员常量
+    final int y = 10;
+    public static void main(String[] args) {
+        // 声明局部常量
+        final double x = 3.3;
+    }
+}
+

常量池

+

一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述符信息外, +还包含一项信息就是常量池表(Constant Pool Table),包括各种字面量和对类型、域和方法的符号引用。

+

常量池中有什么?

+
    +
  • 数量值
  • +
  • 字符串值
  • +
  • 类引用
  • +
  • 字段引用
  • +
  • 方法引用
  • +
+

常量池、可以看做是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等类型。

+

在Java中,大致上可以分为三种常量池.分别是字符串常量池、Class常量池(静态常量池)和运行时常量池.

+

我们常说的常量池,就是指方法区中的运行时常量池。

+

Class常量池

+
Class文件
+
+

class文件全名称为Java class文件,主要在平台无关性和网络移动性方面使Java更适合网络。它在平台无关性方面的任务是:为Java程序提供独立于底层主机平台的二进制形式的服务。(会产生字节码)

+
+
+

有了字节码,无论是哪种平台(如Windows、Linux等),只要安装了虚拟机,都可以直接运行字节码。目前Java虚拟机已经可以支持很多除Java语言以外的语言了,如Groovy、JRuby、Jython、Scala等。之所以可以支持,就是因为这些语言也可以被编译成字节码。而虚拟机并不关心字节码是有哪种语言编译而来的。

+
+

将下面代码通过javac命令编译

+
public class HelloWorld {
+    public static void main(String[] args) {
+        String s = "123";
+    }
+}
+

生成.class文件.vi命令查看

+
Êþº¾^@^@^@4^@^Q
+^@^D^@^M^H^@^N^G^@^O^G^@^P^A^@^F<init>^A^@^C()V^A^@^DCode^A^@^OLineNumberTable^A^@^Dmain^A^@^V([Ljava/lang/String;)V^A^@
+SourceFile^A^@^OHelloWorld.java^L^@^E^@^F^A^@^C123^A^@7com/example/springboot/example/security/util/HelloWorld^A^@^Pjava/lang/Object^@!^@^C^@^D^@^@^@^@^@^B^@^A^@^E^@^F^@^A^@^G^@^@^@^]^@^A^@^A^@^@^@^E*·^@^A±^@^@^@^A^@^H^@^@^@^F^@^A^@^@^@^C^@        ^@      ^@
+^@^A^@^G^@^@^@ ^@^A^@^B^@^@^@^D^R^BL±^@^@^@^A^@^H^@^@^@
+^@^B^@^@^@^E^@^C^@^F^@^A^@^K^@^@^@^B^@^L
+
+

使用16进制打开class文件:使用 vim xxx.class ,然后在交互模式下,输入:%!xxd 即可。

+
+
00000000: cafe babe 0000 0034 0011 0a00 0400 0d08  .......4........
+00000010: 000e 0700 0f07 0010 0100 063c 696e 6974  ...........<init
+00000020: 3e01 0003 2829 5601 0004 436f 6465 0100  >...()V...Code..
+00000030: 0f4c 696e 654e 756d 6265 7254 6162 6c65  .LineNumberTable
+00000040: 0100 046d 6169 6e01 0016 285b 4c6a 6176  ...main...([Ljav
+00000050: 612f 6c61 6e67 2f53 7472 696e 673b 2956  a/lang/String;)V
+00000060: 0100 0a53 6f75 7263 6546 696c 6501 000f  ...SourceFile...
+00000070: 4865 6c6c 6f57 6f72 6c64 2e6a 6176 610c  HelloWorld.java.
+00000080: 0005 0006 0100 0331 3233 0100 3763 6f6d  .......123..7com
+00000090: 2f65 7861 6d70 6c65 2f73 7072 696e 6762  /example/springb
+000000a0: 6f6f 742f 6578 616d 706c 652f 7365 6375  oot/example/secu
+000000b0: 7269 7479 2f75 7469 6c2f 4865 6c6c 6f57  rity/util/HelloW
+000000c0: 6f72 6c64 0100 106a 6176 612f 6c61 6e67  orld...java/lang
+
+

HelloWorld.class文件中的前八个字母是cafe babe,这就是Class文件的魔数(Java中的”魔数”)

+
+
cafe babe   0000      0034        0011      0a00 0400 0d08
+   魔数    此版本号   主版本号    常量池计数器      常量池计数区
+
+

我们需要知道的是,在Class文件的4个字节的魔数后面的分别是4个字节的Class文件的版本号(第5、6个字节是次版本号,第7、8个字节是主版本号,我生成的Class文件的版本号是52,这时Java 8对应的版本。也就是说,这个版本的字节码,在JDK 1.8以下的版本中无法运行)在版本号后面的,就是Class常量池入口了.

+
+
存放内容
+

Class常量池中主要存放两大类常量:字面量和符号引用。

+
+

PS 字面量前面已经记录过了,这里来记录下符号引用的概念.

+
+

符号引用

+

符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括了以下三类常量: 类和接口的全限定名 字段的名称和描述符 方法的名称和描述符.

+
+

符号引用 :符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。

+
+

在编译的时候每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替, +而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。

+
+

解析阶段: +Java类从加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括,加载 ,验证 , 准备 , 解析 , 初始化 , 卸载 ,总共七个阶段。其中验证 ,准备 , 解析 统称为连接。 +而在解析阶段会有一步将常量池当中二进制数据当中的符号引用转化为直接引用的过程。

+
+

在Java编译阶段,由.java文件会生成.class文件.Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译器生成的各种字面量和符号引用。

+

由于不同的Class文件中包含的常量的个数是不固定的,所以在Class文件的常量池入口处会设置两个字节的常量池容量计数器,记录了常量池中常量的个数。

+

Class常量池可以理解为是Class文件中的资源仓库。

+

class常量池中保存了各种常量。而这些常量都是开发者定义出来,需要在程序的运行期使用的。

+
+

Java代码在进行Javac编译的时候,在Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

+
+

Class是用来保存常量的一个媒介场所,并且是一个中间场所。在JVM真的运行时,需要把常量池中的常量加载到内存中.

+

运行时常量池

+
+

运行时常量池是每一个类或接口的常量池的运行时表示形式.

+

它包括了若干种不同的常量:从编译期可知的数值字面量到必须运行期解析后才能获得的方法或字段引用。运行时常量池扮演了类似传统语言中符号表的角色,不过它存储数据范围比通常意义上的符号表要更为广泛。

+

每一个运行时常量池都分配在 Java 虚拟机的方法区之中,在类和接口被加载到虚拟机后,对应的运行时常量池就被创建出来。

+
+
    +
  • 保存在方法区中的叫运行时常量池。
  • +
  • 在 class/字节码 文件中的叫class常量池(静态常量池)。
  • +
+

简单说来就是JVM在完成类装载操作后,将class文件中的常量池载入到内存中,并保存在方法区中。 +我们常说的常量池,就是指方法区中的运行时常量池。

+

运行时常量池类似于传统编程语言中的符号表,但是它所包含的数据却比符号表要更加丰富一些。

+

当创建类或接口的运行时常量池时,如果构造运行时常量池所需的内存空间超过了方法区所能提供的最大值,则JVM会抛outOfMemoryError异常。

+
存放位置及存放来源
+

在不同版本的JDK中,运行时常量池所处的位置也不一样.以HotSpot为例: +JDK1.7之前方法区位于永久代.由于一些原因在JDK1.8时彻底祛除了永久代,用元空间代替.

+
+

根据JVM规范,JVM内存共分为虚拟机栈,堆,方法区,程序计数器,本地方法栈五个部分.

+
+

运行时常量池存放在JVM内存模型中的方法区中。

+
+

PS 方法区:

+
    +
  • 被所有方法线程共享的一块内存区域.
  • +
  • 用于存储已经被虚拟机加载的类信息,常量,静态变量等.
  • +
  • 这个区域的内存回收目标主要针对常量池的回收和堆类型的卸载.
  • +
+
+

运行时常量池是方法区的一部分.class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池, +用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后进入方法区的运行时常量池中存放.

+

运行时常量池中包含多种不同的常量,包括编译期就已经明确的数值字面量,也包括到运行期解析后才能够获得的方法或者字段引用。 +此时不再是常量池中的符号地址了,这里换为真实地址。

+

运行时常量池内容包含了Class常量池中的常量和字符串常量池中的内容. +运行时常量池,相对于Class文件常量池的另一重要特征是:具备动态性。

+

字符串常量池

+

在JVM中,为了减少相同的字符串的重复创建,为了达到节省内存的目的。会单独开辟一块内存,用于保存字符串常量,这个内存区域被叫做字符串常量池.

+

字符串常量池保存着所有字符串字面量,这些字面量在编译时期就确定.

+
内存分配
+

不同版本JDK内存分配情况:

+
    +
  • Java 6及以前,字符串常量池存放在永久代
  • +
  • Java 7将字符串常量池的位置调整到Java堆内
  • +
+

字符串常量池为什么要调整位置?

+

JDK7中将字符串常量池放到了堆空间中。因为对永久代的回收效率很低,只有在Full GC的时候才会触发。

+

Full GC 是老年代的空间不足、永久代不足时才会触发。 +这就导致字符串常量池回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。 +所以JDK7之后将字符串常量池放到堆里,能及时回收内存,避免出现OOM错误。

+

简单说来:

+
    +
  • 永久代垃圾回收频率低;而字符串使用频率比较高,不能及时回收字符串,会导致导致永久代内存不足。
  • +
  • 永久代的默认比较小;因字符串大量使用,所以会导致OOM(OutOfMemoryError )错误。
  • +
+

如何证明Java8中的字符串常量池方到了堆中?

+

代码演示:

+
public class MainTest {
+    // 虚拟机参数: -Xmx6m -Xms6m -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
+    public static void main(String[] args) {
+        HashSet<String> hashSet = new HashSet<>();
+        short i = 0;
+        while (true) {
+            hashSet.add(String.valueOf(i++).intern());
+        }
+    }
+}
+

抛出异常证明Java8中的字符串常量池方到了堆中

+
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
+
intern方法
+

intern是一个native方法,调用的是底层C的方法

+
    /**
+     * Returns a canonical representation for the string object.
+     * <p>
+     * A pool of strings, initially empty, is maintained privately by the
+     * class {@code String}.
+     * <p>
+     * When the intern method is invoked, if the pool already contains a
+     * string equal to this {@code String} object as determined by
+     * the {@link #equals(Object)} method, then the string from the pool is
+     * returned. Otherwise, this {@code String} object is added to the
+     * pool and a reference to this {@code String} object is returned.
+     * <p>
+     * It follows that for any two strings {@code s} and {@code t},
+     * {@code s.intern() == t.intern()} is {@code true}
+     * if and only if {@code s.equals(t)} is {@code true}.
+     * <p>
+     * All literal strings and string-valued constant expressions are
+     * interned. String literals are defined in section 3.10.5 of the
+     * <cite>The Java&trade; Language Specification</cite>.
+     */
+    public native String intern();
+

intern方法文档注释大意是:字符串池最初是空的,由String类私有地维护。在调用intern方法时,如果池中已经包含了由equals(object)方法确定的与该字符串对象相等的字符串,则返回池中的字符串。 +否则,该字符串对象将被添加到池中,并返回对该字符串对象的引用。

+

当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals()方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用.

+

下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同字符串,而 s3 和 s4 是通过 s1.intern() 方法取得一个字符串引用。intern() 首先把 s1 引用的字符串放到 String Pool 中,然后返回这个字符串引用。因此 s3 和 s4 引用的是同一个字符串。

+
public class MainTest {
+    public static void main(String[] args) {
+        String s1 = new String("aaa");
+        String s2 = new String("aaa");
+        System.out.println(s1 == s2);           // false
+        String s3 = s1.intern();
+        String s4 = s1.intern();
+        System.out.println(s3 == s4);           // true
+    }
+}
+

上面的示例说明了可以使用 String的intern()方法在程序运行过程中将字符串添加到字符串常量池中。

+

空间效率测试

+

对于程序中大量使用存在的字符串时,尤其存在很多已经重复的字符串时,使用intern()方法能够节省内存空间。

+
public class MainTest {
+    static final int MAX_COUNT = 1000 * 10000;
+    static final String[] arr = new String[MAX_COUNT];
+
+    public static void main(String[] args) {
+        Integer [] data = new Integer[]{1,2,3,4,5,6,7,8,9,10};
+        long start = System.currentTimeMillis();
+        for (int i = 0; i < MAX_COUNT; i++) {
+//            arr[i] = new String(String.valueOf(data[i%data.length])); // 花费的时间为:3074
+            arr[i] = new String(String.valueOf(data[i%data.length])).intern(); // 花费的时间为:1196
+        }
+        long end = System.currentTimeMillis();
+        System.out.println("花费的时间为:" + (end - start));
+
+        try {
+            Thread.sleep(1000000);
+        } catch (Exception e) {
+            e.getStackTrace();
+        }
+        System.gc();
+    }
+}
+

不使用intern方法 +不用intern方法测试

+

使用intern方法后 +使用intern方法测试

+
经典案例
+

String常量池常考的一个问题就是new String("abc")会创建几个对象?

+

答案: 两个字符串对象(前提是 String常量池中还没有 “abc” 字符串对象).

+
    +
  • “abc” 属于字符串字面量,因此编译时期会在 String 常量池 中创建一个字符串对象,指向这个 “abc” 字符串字面量;
  • +
  • 而使用 new 的方式会在堆中创建一个字符串对象。
  • +
+

以下是 JDK8 中 String 构造函数的源码,文档注释大意是:

+
+

初始化新创建的String对象,使其表示与实参相同的字符序列;换句话说,新创建的字符串是实参字符串的副本。 +除非需要显式复制形参的值,否则没有必要使用这个构造函数,因为字符串是不可变的。

+
+
/**
+ * Initializes a newly created {@code String} object so that it represents
+ * the same sequence of characters as the argument; in other words, the
+ * newly created string is a copy of the argument string. Unless an
+ * explicit copy of {@code original} is needed, use of this constructor is
+ * unnecessary since Strings are immutable.
+ *
+ */
+public String(String original) {
+    this.value = original.value;
+    this.hash = original.hash;
+}
+

字节码分析:

+

创建一个测试类,其 main 方法中使用这种方式来创建字符串对象。

+
public class MainTest {
+    public static void main(String[] args) {
+        String s = new String("abc");
+    }
+}
+

使用 javap -verbose 进行反编译,得到以下内容:

+
// ...
+Constant pool:
+// ...
+   #2 = Class              #18            // java/lang/String
+   #3 = String             #19            // abc
+// ...
+  #18 = Utf8               java/lang/String
+  #19 = Utf8               abc
+// ...
+
+  public static void main(java.lang.String[]);
+    descriptor: ([Ljava/lang/String;)V
+    flags: ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=3, locals=2, args_size=1
+         0: new           #2                  // class java/lang/String
+         3: dup
+         4: ldc           #3                  // String abc
+         6: invokespecial #4                  // Method java/lang/String."<init>":(Ljava/lang/String;)V
+         9: astore_1
+// ...
+

Constant Pool 中,#19 存储这字符串字面量 "abc",#3 是 String Pool 的字符串对象,它指向 #19 这个字符串字面量。 +在 main 方法中,0: 行使用 new #2 在堆中创建一个字符串对象,并且使用 ldc #3 将 String Pool 中的字符串对象作为 String 构造函数的参数。 +所以能看到使用new String() 的方式创建字符串是创建两个对象。

+

使用new String("a") + new String("b") 会创建几个对象?

+

答案:会创建6个对象:

+
    +
  • 对象1:因为存在用"+“连接,所以从字节码角度分析会产生new StringBuilder()对象;
  • +
  • 对象2:new String("a")会从堆空间创建一个对象;
  • +
  • 对象3:常量池的 a ;
  • +
  • 对象4:new String("b") 会从堆空间创建一个对象;
  • +
  • 对象5:常量池的 b;
  • +
  • 对象6:从字节码角度分析,通过new StringBuilder().append()添加完字符串后,会通过StringBuilder.toString()中会创建一个 new String("ab"); +但是调用toString()方法,不会在常量池中生成ab字符串;
  • +
+

也可跟上面的代码一样从字节码角度进行分析,这里不做过多分析。

+

扩展:在JDK1.6/JDK1.7中下列代码执行结果不同

+
public class MainTest {
+    public static void main(String[] args) {
+        String s3 = new String("1") + new String("1"); // ==> new String("11"); 并不会在字符串常量池创建 "11"
+        // 在Jdk7及之后,JVM为了节省空间进行优化,在字符串常量池中的对象只保存了堆中对象的地址,而并不是创建了一个真正的对象;
+        // "11"不在字符串常量池中 ,把"11"放入字符串常量池中 因为堆中已经存在对象,JDK6在字符串池中保存的是一个真正的对象 JDK7保存的是一个引用地址
+        s3.intern(); 
+        // 获取字符串常量池的引用
+        String s4 = "11"; 
+        System.out.println(s3 == s4); // JDK1.6: false   JDK1.7: true
+    }
+}
+

原因是因为字符串常量池在JDK1.7的时候被放在了堆中,而JDK1.6被放在了永久区中;

+

JDK1.6创建对象被放在了永久区中的字符串常量池,并非堆中,JVM无法对其进行优化; +在JDK1.7及之后,new String("1") 会在堆中创建一个对象并且会在字符串常量池中创建一个对象; +因为在堆中已经创建了对象,JVM为了节省空间,在字符串常量池中的对象只保留了引用堆中对象的地址,而并不是创建了一个真正的对象;

+

JDK1.6中,将这个字符串对象尝试放入串池。

+
    +
  • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
  • +
  • 如果没有,会把此对象复制一份,放入串池,并返回串池中的对象地址
  • +
+

JDK6字符串常量池案例解析

+

JDK1.7起,将这个字符串对象尝试放入串池。

+
    +
  • 如果串池中有,则并不会放入。返回已有的串池中的对象的地址
  • +
  • 如果没有,则会把对象的引用地址复制一份,放入串池,并返回串池中的引用地址
  • +
+

JDK78字符串常量池案例解析

+
注意
+

字符串常量池是不会存储相同内容的字符串的。

+

字符串常量池其实是一个固定大小的Hashtable; +如果放进字符串常量池的string非常多,就会造成Hash冲突严重,从而导致链表会很长,而链表长了后直接会造成的影响就是当调用string.intern时性能会大幅下降。

+

使用VM参数-XX:StringTablesize可设置字符串常量池的的长度;

+

不同JDK版本字符串常量池的的长度:

+
    +
  • 在JDK6中stringTable是固定的,为1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快。-XX:StringTablesize设置没有要求,可随意设置;
  • +
  • 在JDK中,stringTable的长度默认值是60013;-XX:StringTablesize设置没有要求,可随意设置;
  • +
  • 在JDK8中,StringTable可以设置的最小值为1009;stringTable的长度默认值是60013;
  • +
+

可以配置不同版本的JRE,运行下列代码,然后使用 jsp(获取程序进程ID) -> jinfo -flag StringTableSize 进程ID命令进行查看stringTableSize

+
public class MainTest {
+    public static void main(String[] args) {
+        try {
+            Thread.sleep(1000000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+}
+

为什么需要常量池

+

一个Java源文件中的类、接口,编译后产生一个字节码文件。 +而Java中的字节码需要数据支持,通常这种数据会很大以至于不能直接存到字节码里,换另一种方式,可以存到常量池,这个字节码包含了指向常量池的引用。 +在动态链接的时候会用到运行时常量池。

+

比如:如下的代码:

+
public class SimpleClass {
+    public void sayHello() {
+        System.out.println("hello");
+    }
+}
+

虽然上述代码生成的class文件只有194字节,但是里面却使用了String、System、PrintStream及Object等结构。 +这里的代码量其实很少了,如果代码多的话,引用的结构将会更多,这样就需要用到常量池了。

+

常量池是为了避免频繁的创建和销毁对象而影响系统性能,其实现了对象的共享. +避免了相同内容的字符串的创建,节省了内存,省去了创建相同字符串的时间,同时提升了性能.

+

String 类

+

String是Java中一个比较基础的类.广泛应用 在 Java 编程中,在Java中字符串属于对象,Java 提供了 String 类来创建和操作字符串.

+

String被声明为final,因此它不可被继承(当然Integer等包装类也不能被继承);

+

String实现了Serializable接口:表示字符串是支持序列化的。实现了Comparable接口:表示string可以比较大小;

+

为什么JDK9改变了存储结构

+

在 Java 8 中,String 内部使用 char 数组存储数据。

+
public final class String
+    implements java.io.Serializable, Comparable<String>, CharSequence {
+    /** The value is used for character storage. */
+    private final char value[];
+}
+

在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

+
public final class String
+    implements java.io.Serializable, Comparable<String>, CharSequence {
+    /** The value is used for character storage. */
+    private final byte[] value;
+
+    /** The identifier of the encoding used to encode the bytes in {@code value}. */
+    private final byte coder;
+}
+

那么为什么要改变内存存储结构呢? +官方的解释:

+
+

The current implementation of the String class stores characters in a char array, using two bytes (sixteen bits) for each character. +Data gathered from many different applications indicates that strings are a major component of heap usage and, +moreover, that most String objects contain only Latin-1 characters. Such characters require only one byte of storage, +hence half of the space in the internal char arrays of such String objects is going unused.

+

We propose to change the internal representation of the String class from a UTF-16 char array to a byte array plus an encoding-flag field. +The new String class will store characters encoded either as ISO-8859-1/Latin-1 (one byte per character), +or as UTF-16 (two bytes per character), based upon the contents of the string. +The encoding flag will indicate which encoding is used.

+
+

大意:String类的当前实现将字符存储在char数组中,每个字符使用两个字节(16位)。 +从许多不同的应用程序收集的数据表明,字符串是堆使用的主要组成部分,而且,大多数字符串对象只包含拉丁字符。 +这些字符只需要一个字节的存储空间,因此这些字符串对象的内部char数组中有一半的空间将不会使用。

+

我们建议将String类的内部表示从UTF-16字符数组更改为一个字节数组加上一个编码标志字段。 +新的String类将根据字符串的内容存储编码为ISO-8859-1/Latin-1(每个字符一个字节)或UTF-16(每个字符两个字节)的字符。 +编码标志将指示所使用的编码。

+

简单说来就是用char数组存,有些数据占不了两个字节,所以为了节约资源改成byte数组存放; +如果存放的不是拉丁文则需要占两个字节,所以还需要加上编码标记。

+

基于String的数据结构,StringBufferStringBuilder也同样做了修改

+

不可变性

+

value数组被声明为final,这意味着value数组初始化之后就不能再引用其它数组。 +并且 String 内部没有改变 value 数组的方法,因此可以保证String不可变; +String是Java中一个不可变的类,所以他一旦一个string对象在内存(堆)中被创建出来,他就无法被修改。

+

下面代码说明String是不可变的:

+
public class MainTest {
+    String str = new String("good");
+    char [] ch = {'t','e','s','t'};
+
+    public void change(String str, char ch []) {
+        str = "test ok";
+        System.out.println( "chanage方法里str=" + str); // chanage方法里str=test ok
+        ch[0] = 'b';
+    }
+
+    public static void main(String[] args) {
+        MainTest ex = new MainTest();
+        ex.change(ex.str, ex.ch);
+        System.out.println(ex.str); // good
+        System.out.println(ex.ch);// best
+    }
+}
+

特别要注意的是,String类的所有方法都没有改变字符串本身的值,都是返回了一个新的对象。字符串不可变的根本原因应是处于安全性考虑。

+

不可变性优点:

+
    +
  1. 可以缓存 hash 值
  2. +
+

因为Stringhash值经常被使用,像Set、Map结构中的 key 值也需要用到HashCode来保证唯一性和一致性,因此不可变的 HashCode 才是安全可靠的

+
    +
  1. String常量池的需要
  2. +
+

字符串常量池的基础就是字符串的不可变性,如果字符串是可变的,那想一想,常量池就没必要存在了。 +如果一个 String 对象已经被创建过了,那么就会从String常量池中取得引用。只有String`是不可变的,才可能使用 String常量池。

+
    +
  1. 安全性
  2. +
+

String经常作为参数,String不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果String是可变的,那么在网络连接过程中,String被改变,改变String对象的那一方以为现在连接的是其它主机,而实际情况却不一定是. +实际项目中会用到,比如数据库连接串、账号、密码等字符串,只有不可变的连接串、用户名和密码才能保证安全性.

+
    +
  1. 线程安全
  2. +
+

字符串在 Java 中的使用频率可谓高之又高,那在高并发的情况下不可变性也使得对字符串的读写操作不用考虑多线程竞争的情况. +String不可变性天生具备线程安全,可以在多个线程中安全地使用.

+

使用

+

创建字符串

+
// String 直接创建
+String str1 = "123";
+// String 对象创建
+String str2 = new String("123");
+// 引用创建
+String str3 = str1;
+

格式化创建字符串

+
String str = String.format("浮点型变量: " +
+                   "%f, 整型变量: " +
+                   " %d, 字符串变量: " +
+                   " %s", 1.0f, 1, "1");
+System.out.println(str);
+

通过字面量的方式(区别于new)给一个字符串赋值,存储在String常量池中,而 new 创建的字符串对象在堆上.

+

连接字符串

+

因为String类是不可变的.所以所谓字符串拼接,都是重新生成了一个新的字符串.

+

以下是字符串的几种拼接的方式

+
// 原字符串
+String str1 = "123";
+
+//1. concat 方法连接字符串
+String str2 = "123".concat("456");
+System.out.println(str2);
+
+//2. 用 "+" 连接字符串
+// 在编译时会编译成 String str4 = "123456";
+String str3 = "123" + "456";
+// 会调用StringBuilder.append方法
+String str4 = str3 + "789";
+System.out.println(str3);
+System.out.println(str4);
+
+//3. 用JDK内置处理字符串类 Stringbuilder, StringBuffer
+StringBuilder builder = new StringBuilder();
+StringBuffer buffer = new StringBuffer();
+String str5 = builder.append(str).append("456").toString();
+String str6 = buffer.append(str).append("456").toString();
+System.out.println(str5);
+System.out.println(str6);
+
+//4. Java8新增String.join方法连接字符串
+List<String> list = new ArrayList<>();
+list.add("1");
+list.add("2");
+list.add("3");
+String str7 = String.join("-", list);
+String str8 = String.join("~", new String[]{"1","2","3"}r);
+System.out.println(str7);
+System.out.println(str8);
+
+//5. 用第三方处理字符串工具类. 例如: apache.commons.
+String str9 = StringUtils.join(new String[]{str, "456", "789"});
+
+// Java8提供的String.join方法和`apache.commons`方法相似.
+// 主要作用是将数组或集合以某拼接符拼接到一起形成新的字符串
+
拼接字符串
+

拼接字符串最简单的方式就是直接使用符号+来拼接. + 是Java提供的一个语法糖.

+
+

语法糖:语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性。

+
+
    public static void main(String[] args) {
+        String str = "12" + "3";
+    }
+

两个都为编译期常量,编译器会进行常量折叠变为String str = "123";

+

如果拼接符号的前后出现了变量,则相当于在堆空间中new String()(这里的堆指的是除了字符串常量池外的区域),具体的内容为拼接的结果.

+
    public static void main(String[] args) {
+        String str1 = "123";
+        String str2 = "456";
+        String str3 = str1 + str2;
+        System.out.println(str3);
+    }
+

打开文件所在位置,用javap -verbose命令进行反编译.

+
// ...
+  public static void main(java.lang.String[]);
+    descriptor: ([Ljava/lang/String;)V
+    flags: ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=2, locals=4, args_size=1
+         0: ldc           #2                  // String 123
+         2: astore_1
+         3: ldc           #3                  // String 456
+         5: astore_2
+         6: new           #4                  // class java/lang/StringBuilder
+         9: dup
+        10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
+        13: aload_1
+        14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
+        17: aload_2
+        18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
+        21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
+        24: astore_3
+        25: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
+        28: aload_3
+        29: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
+        32: return
+      // ...
+

可以看到在字符串拼接过程中,是将String转成了StringBuilder后,使用其append方法进行拼接字符串处理的.最后在去调用StringBuildertoString方法进行返回

+
+

在JDK5之后,使用的是StringBuilder,在JDK5之前使用的是StringBuffer

+
+

但是如果使用final修饰String变量时,在VM编译过程中,仍然被编译器优化;

+
public class MainTest {
+    public static void main(String[] args) {
+        final String a = "1";
+        final String b = "2";
+        String ab = a + b; // 在class文件中会优化为: String ab = 12;
+        String ab0 = "12";
+        System.out.println(ab == ab0);
+    }
+}
+

在开发中,能够使用final的时候,建议使用上。

+
concat方法
+

源码

+
    public String concat(String str) {
+        // 获取字符串长度 
+        int otherLen = str.length();
+        // 判空
+        if (otherLen == 0) {
+            return this;
+        }
+        // 获取已有字符串长度
+        int len = value.length;
+        // 创建新的字符数组.
+        // 长度为:已有字符串+待拼接字符长度.
+        // 将两个字符串的值复制到新的字符数组
+        char buf[] = Arrays.copyOf(value, len + otherLen);
+        str.getChars(buf, len);
+        // 创建新的字符串对象
+        return new String(buf, true);
+    }
+
StringBuffer和StringBuilder
+

StringbuilderStringBuffer共同的父类:

+
abstract class AbstractStringBuilder implements Appendable, CharSequence {
+    /**
+     * The value is used for character storage.
+     */
+    char[] value;
+
+    /**
+     * The count is the number of characters used.
+     */
+    int count;
+
+    // ...
+}
+

用于存贮的value并没有用final修饰,说明是可以修改的.还有一个属性字段count,用来保存数组中已经使用的字符个数.

+

append方法

+

StringBuilder.append方法

+
    @Override
+    public StringBuilder append(String str) {
+        super.append(str);
+        return this;
+    }
+

StringBuffer.append方法

+
    @Override
+    public synchronized StringBuffer append(String str) {
+        toStringCache = null;
+        super.append(str);
+        return this;
+    }
+

由此可以看出StringBuilderStringBuffer原理是相似的,最大的区别就是StringBuffer是线程安全的.原因是用了synchronized修饰.

+

append方法原理的在父类中.需要注意的是,如果append方法append(null)会直接拼接字符串"null”

+
    public AbstractStringBuilder append(String str) {
+        // 判空
+        if (str == null)
+            /**
+             * final char[] value = this.value;
+             * value[c++] = 'n';
+             * value[c++] = 'u';
+             * value[c++] = 'l';
+             * value[c++] = 'l';
+             */
+           // 如果是'null' 则返回 字符串 "null"
+            return appendNull();
+        // 获取字符长度
+        int len = str.length();
+        /**  if (count + len - value.length > 0) {
+                // 拷贝字符到内部的字符数组中.如果字符数组长度不够,会进行扩展
+                value = Arrays.copyOf(value,newCapacity(count+len));
+             }
+        **/
+        ensureCapacityInternal(count + len);
+        // 复制数组
+        // System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
+        str.getChars(0, len, value, count);
+        // 拼接字符长度
+        count += len;
+        return this;
+    }
+

与 String 类不同的是,StringBufferStringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。

+

如果你需要一个可修改的字符串,应该使用StringBuffer 或者 StringBuilder。但是会有大量时间浪费在垃圾回收上,因为每次试图修改都有新的String对象被创建出来.

+
比较和使用
+

时间比较(短->长):

+

StringBuilder<StringBuffer<concat<+<StringUtils.join

+

使用注意:

+
    +
  • +

    如果在并发场景中进行字符串拼接的话,要使用StringBuffer来代替StringBuilder.

    +
  • +
  • +

    如果不是在循环体中进行字符串拼接的话,直接使用+就好了,如果在循环体内使用"+“拼接字符串对象会在每一次循环都会创建StringBuilder对象,导致程序效率降低. +如下代码:

    +
  • +
+
    public static void method1(int highLevel) {
+        String src = "";
+        for (int i = 0; i < highLevel; i++) {
+            src += "a"; // 每次循环都会创建一个StringBuilder对象
+        }
+    }
+
+    public static void method2(int highLevel) {
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < highLevel; i++) {
+            sb.append("a");
+        }
+    }
+

通过StringBuilderappend()方式添加字符串的效率,要远远高于String的字符串拼接方法.

+

在实际开发中还可以进行优化,StringBuilder 的空参构造器,默认的字符串容量是16,如果需要存放的数据过多,容量就会进行扩容,我们可以设置默认初始化更大的长度,来减少扩容的次数。

+

如果我们能够确定,前前后后需要添加的字符串不高于某个限定值,那么建议使用构造器创建一个阈值的长度。

+

常用方法

+

基于Java8整理.如果查看其他方法请参照Java8API官方文档 java.lang.String

+
substring
+

对字符串进行截取.返回一个新的字符串,它是此字符串的一个子字符串.

+

源码

+
    public String substring(int beginIndex) {
+        // 判空
+        if (beginIndex < 0) {
+            throw new StringIndexOutOfBoundsException(beginIndex);
+        }
+        // 需要截取的长度不能超过源字符的长度
+        int subLen = value.length - beginIndex;
+        if (subLen < 0) {
+            throw new StringIndexOutOfBoundsException(subLen);
+        }
+        // 如果传入的长度不等于被截字符串的长度 则创建新的字符串
+        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
+    }
+    
+    public String substring(int beginIndex, int endIndex) {
+        if (beginIndex < 0) {
+            throw new StringIndexOutOfBoundsException(beginIndex);
+        }
+        if (endIndex > value.length) {
+            throw new StringIndexOutOfBoundsException(endIndex);
+        }
+        int subLen = endIndex - beginIndex;
+        if (subLen < 0) {
+            throw new StringIndexOutOfBoundsException(subLen);
+        }
+        return ((beginIndex == 0) && (endIndex == value.length)) ? this
+                : new String(value, beginIndex, subLen);
+    }
+

使用

+
    public static void main(String[] args) {
+        String str = "123456";
+        String substring = str.substring(2);
+        // 3456
+        System.out.println(substring);
+        // 索引从1开始截取字符串
+        String substring2 = str.substring(2,4);
+        // 34
+        System.out.println(substring2);
+    }
+
replace
+

替换字符串.返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的.

+

源码

+
 public String replace(char oldChar, char newChar) {
+        // 校验: 需要进行替换新旧字符不能相同
+        if (oldChar != newChar) {
+            // 获取源字符串中的长度
+            int len = value.length;
+            int i = -1;
+            // 获取源字符串
+            char[] val = value; /* avoid getfield opcode */
+            // 判断源字符串是否存在 需要替换的旧字符
+            while (++i < len) {
+                if (val[i] == oldChar) {
+                    break;
+                }
+            }
+            if (i < len) {
+                // 创建新的字符数组 用于替换后保存新字符串 
+                char buf[] = new char[len];
+                for (int j = 0; j < i; j++) {
+                    // 将旧字符存入到新字符数组
+                    buf[j] = val[j];
+                }
+                while (i < len) {
+                    char c = val[i];
+                    // 替换字符
+                    buf[i] = (c == oldChar) ? newChar : c;
+                    i++;
+                }
+                // 创建新字符串对象
+                return new String(buf, true);
+            }
+        }
+        return this;
+    }
+

使用

+
    public static void main(String[] args) {
+        String str = "123456";
+        // 223456
+        System.out.println(str.replace('1', '2'));
+    }
+
replaceAll
+

使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串.

+

源码

+
    public String replaceAll(String regex, String replacement) {
+        return Pattern.compile(regex).matcher(this).replaceAll(replacement);
+    }
+    
+
    // complile 解析正则表达式 获得 Pattern对象
+    public static Pattern compile(String regex) {
+        return new Pattern(regex, 0);
+    }
+    
+    //matcher 获取匹配器对象
+    public Matcher matcher(CharSequence input) {
+        if (!compiled) {
+            synchronized(this) {
+                if (!compiled)
+                    compile();
+            }
+        }
+        Matcher m = new Matcher(this, input);
+        return m;
+    }
+    
+    // replaceAll 进行字符串替换
+    public String replaceAll(String replacement) {
+    // 对当前Matcher类进行重置,即对其中记录匹配结果的开始和结束位置索引,以及分组信息重置
+        reset();
+        // 执行第一次搜索
+        boolean result = find();
+         // 第一次搜索匹配成功
+        if (result) {
+            // 用于记录最终的替换结果字符串
+            StringBuffer sb = new StringBuffer();
+            do {
+            // 重点:用于将从上一次匹配子字符串的下一个索引位置开始,到当前匹配的子字符串的结束索引位置的所有字符 append到字符串sb中
+                appendReplacement(sb, replacement);
+                result = find();
+            } while (result);
+            // 将从最后一次匹配子字符串的下一个索引位置,到字符串的结尾的所有字符append到字符串sb中
+            appendTail(sb);
+            return sb.toString();
+        }
+        return text.toString();
+    }
+

重点看一下replaceAll中调用的appendReplacement方法

+
    public Matcher appendReplacement(StringBuffer sb, String replacement) {
+            
+            // ...
+            
+            // 用于跟踪 replacement 字符串的索引
+            int cursor = 0;
+            
+            // 对当前匹配到子字符串替换后的结果字符串
+            StringBuffer result = new StringBuffer();
+            
+            // 遍历 replacement字符串
+            while (cursor < replacement.length()) {
+                
+                char nextChar = replacement.charAt(cursor);
+                
+                if (nextChar == '\\') {
+                    // 重点1:当字符为'\'时,跳过,并获取其后面的字符,追加到result
+                    cursor++;
+                    nextChar = replacement.charAt(cursor);
+                    result.append(nextChar);
+                    cursor++;
+                } else if (nextChar == '$') {
+                    
+                    // 重点2:当字符为$时,跳过,并获取其后面的数值,并以此如果$后面第一个不为数字则抛异常,
+                    // Skip past $
+                    cursor++;
+                    
+                    // The first number is always a group
+                    int refNum = (int)replacement.charAt(cursor) - '0';
+                    
+                    // 此处代码用于计算$符号后的数值,数值结果赋予 refNum
+                    // ...
+                    
+                    // group(refNum) 用于获取正则表达式第refNum个分组表示的字符串,不详说了
+                    if (group(refNum) != null)
+                         // 追加到result
+                        result.append(group(refNum));
+                } else {
+                    
+                    // 当前字符不为\ 或 $ 则直接追加到result
+                    result.append(nextChar);
+                    cursor++;
+                }
+            }
+            
+            // 将从上一次匹配的子字符串的结尾索引,到当前匹配的第一个字符串索引的字符串追加到sb
+            // lastAppendPosition参数为上一次执行appendReplacement方法最后追加的字符在原始字符串中的索引位置。
+            // first 参数为当前待替换的子字符串的首个字符在原始字符串中的索引位置
+            sb.append(getSubSequence(lastAppendPosition, first));
+            
+            // 将当前配置子字符串替换后的结果字符串追加到sb
+            sb.append(result.toString());
+            
+            // 更新lastAppendPosition,供下一个匹配执行appendReplacement方法使用
+            lastAppendPosition = last;
+            
+            /* sb中追加了当前匹配的子字符串与前一次匹配子字符串中间的字符,以及当前匹配子字符串被替换后的字符串
+             */
+            return this;
+        }
+

replaceAll中第二个参数replacement中,\ 有转义的作用, $ 用于获取分组匹配的当前子字符串 因为引入了 $ 符的分组功能,所以为了解决能输出 $ 字符,故引入 \ 转义功能.

+

使用

+
    public static void main(String[] args) {
+        String str = "111111";
+        // 222222
+        System.out.println(str.replaceAll("1", "2"));
+    }
+
valueOf
+

该方法作用是将对象转成String类型.

+

源码

+
    public static String valueOf(Object obj) {
+        return (obj == null) ? "null" : obj.toString();
+    }
+

使用

+
    public static void main(String[] args) {
+        Integer integer = 11111;
+        String str = String.valueOf(integer);
+        // 11111
+        System.out.println(str);
+    }
+

长度限制

+

翻阅String源码在String源码中发现有定义字符串长度的构造函数

+
    // count 就是 字符串定义长度 
+    public String(char value[], int offset, int count) {
+        if (offset < 0) {
+            throw new StringIndexOutOfBoundsException(offset);
+        }
+        if (count <= 0) {
+            if (count < 0) {
+                throw new StringIndexOutOfBoundsException(count);
+            }
+            if (offset <= value.length) {
+                this.value = "".value;
+                return;
+            }
+        }
+        // Note: offset or count might be near -1>>>1.
+        if (offset > value.length - count) {
+            throw new StringIndexOutOfBoundsException(offset + count);
+        }
+        this.value = Arrays.copyOfRange(value, offset, offset+count);
+    }
+

通过源码可以看到int的最大长度就是String的支持的最大长度.

+
    public static void main(String[] args) {
+        // 2,147,483,648 = 2^31 - 1 
+        System.out.println(Integer.MAX_VALUE);
+    }
+

注意new String(char value[], int offset, int count)是运行时String支持的最大长度.

+

String编译声明期间,用javac编译 长度为2^31 -1的字符串.

+
    public static void main(String[] args) {
+        // 长度: 2^31 -1
+        String str = "1111 ... ";
+        System.out.println(str);
+    }
+
java: 常量字符串过长
+

Gen类中相关报错信息源码

+
private void checkStringConstant(DiagnosticPosition var1, Object var2) {
+    if (this.nerrs == 0 && var2 != null && var2 instanceof String && ((String)var2).length() >= 65535) {
+        this.log.error(var1, "limit.string", new Object[0]);
+        ++this.nerrs;
+    }
+}
+

可以看到源码中如果String长度大于等于65535会导致编译失败

+

在编译期的时候,字面量要进字符串常量池.所以要遵守《Java®虚拟机规范》(Java8)中对String常量池的描述.

+

CONSTANT_String_info 用于表示 java.lang.String 类型的常量对象结构体

+

CONSTANT_String_info格式如下:

+
CONSTANT_String_info {
+    u1 tag;
+    u2 string_index;
+}
+
+

tag
+结构CONSTANT_String_info的标签项的值为CONSTANT_String(8)

+
+
+

string_index
+string_index 项的值必须是对常量池的有效索引,常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示一组 Unicode 码点序列,这组 Unicode 码点序列最终会被初始化为一个 下 Unicode 对象

+
+

CONSTANT_Utf8_info是一个CONSTANT_Utf8类型的常量池数据项,它存储的是一个常量字符串。常量池中的所有字面量几乎都是通过CONSTANT_Utf8_info描述的。CONSTANT_Utf8_info的定义如下:

+
ONSTANT_Utf8_info {
+    u1 tag;
+    u2 length;
+    u1 bytes[length];
+}
+

length则指明了 bytes数组的长度,其类型为u2

+

通过查阅《JVM规范》发现u2表示两个字节的无符号数,那么1个字节有8位,2个字节就有16位。16位无符号数可表示的最大值位2^16 - 1 = 65535。也就是说,Class文件中常量池的格式规定了,其字符串常量的长度不能超过65535。

+

关于编译器字符串最大长度65534的问题

+
+

如果一个方法的Java虚拟机代码长度正好是65535字节,并且以一个1字节长的指令结束,那么该指令不能被异常处理程序保护。编译器作者可以通过将任何方法、实例初始化方法或静态初始化器(任何代码数组的大小)生成的Java虚拟机代码的最大大小限制为65534字节来解决这个问题

+
+

简单来说

+
    +
  • 在程序运行时String最大长度为int最大长度为2^31 -1
  • +
  • 在程序编译期根据StringJVM常量池规范String字符串在声明时最大为 65535,但是为了修复Java的遗留问题改为65534
  • +
+

在程序开发中,需要注意如果你用String变量接收Base64图片或音频视频需要注意不要超过在程序运行时字符串的最大阈值.

+

编码问题

+

因为全世界有很多编程人员,有很多语言,不同的国家使用不同的语言,如果说没有一套统一的编码规则,这么多语言混在一起,很容易出现乱码现象,本着既方便又节约内存的理念大家基本都是用utf-8码来编写程序.

+

Unicode

+
+

Unicode(中文:万国码、国际码、统一码、单一码)是计算机科学领域里的一项业界标准。它对世界上大部分的文字系统进行了整理、编码,使得计算机可以用更为简单的方式来呈现和处理文字.

+
+
+

Unicode伴随着通用字符集的标准而发展,同时也以书本的形式对外发表。Unicode至今仍在不断增修,每个新版本都加入更多新的字符。目前最新的版本为2018年6月5日公布的11.0.0,已经收录超过13万个字符(第十万个字符在2005年获采纳)。Unicode涵盖的数据除了视觉上的字形、编码方法、标准的字符编码外,还包含了字符特性,如大小写字母。

+
+

Unicode是一种编码规范,是为解决全球字符通用编码而设计的,而UTF-8 UTF-16等是这种规范的一种实现.Unicode是字符集而UTF-8是编码规则。

+

Java内部采用Unicode编码规范,也就是支持多语言的,具体采用的UTF-16编码方式.

+

不管程序过程中用到了GBK还是ISO8859-1等格式,在存储与传递的过程中实际传递的都是Unicode编码的数据,要想接收到的值不出现乱码,就要保证传过去的时候用的是X编码,接收的时候也用X编码来转换接收

+

乱码原因

+

编码时格式和解码时格式不一致.

+

string在文件里面底层保存形式是二进制,底层用byte[]数组存储(Java9. Java8是用char数组储存).byte[]数组里面的内容可以按照不同的编码格式存放.在读取字符串的时候,也可以按照不同的解码格式存放.这样就造成了乱码.

+

简单理解为

+

在编码(字符串到字节)的时候是用一种编码;而在解码(从字节到字符串)的时候用另一种编码;所以导致乱码问题.所以想要避免乱码问题最简单的办法就是从始至终,都用同一种字符格式

+

相关方法

+

String类有两种比较常用的操作编码方式

+
    // 注意处理异常
+    public static void main(String[] args) throws UnsupportedEncodingException {
+        // 本地使用的是 utf-8 的编码
+        String str = "你好";
+        byte[] bytes = str.getBytes("utf-8");
+        // 你好
+        System.out.println(new String(bytes));
+        String string = new String(str.getBytes(), "utf-8");
+        // 你好
+        System.out.println(string);
+    }
+
getBytes(String charsetName)
+

该方法会根据指定的decode编码返回某字符串在该编码下的byte数组表示

+

源码

+
    public byte[] getBytes(String charsetName)
+            throws UnsupportedEncodingException {
+        if (charsetName == null) throw new NullPointerException();
+        return StringCoding.encode(charsetName, value, 0, value.length);
+    }
+

StringCoding.encode方法

+
// len: 当前字符串长度
+static byte[] encode(String charsetName, char[] ca, int off, int len)
+        throws UnsupportedEncodingException
+    {
+        StringEncoder se = deref(encoder);
+        // 如果为空 默认ISO-8859-1
+        String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
+        // 
+        if ((se == null) || !(csn.equals(se.requestedCharsetName())
+                              || csn.equals(se.charsetName()))) {
+            se = null;
+            try {
+                // 根据编码获取 Charset对象
+                Charset cs = lookupCharset(csn);
+                if (cs != null)
+                    se = new StringEncoder(cs, csn);
+            } catch (IllegalCharsetNameException x) {}
+            if (se == null)
+                throw new UnsupportedEncodingException (csn);
+            set(encoder, se);
+        }
+        return se.encode(ca, off, len);
+    }
+
new String(byte bytes[], String charsetName)
+

该方法为字节数组构造

+

char[]数组是以unicode码来存储的,Stringchar为内存形式.byte是网络传输或存储的序列化形式.可以通过charset来解码指定的byte数组,将其解码成unicodechar[]数组,构造String.

+

源码

+
    public String(byte bytes[], String charsetName)
+            throws UnsupportedEncodingException {
+        this(bytes, 0, bytes.length, charsetName);
+    }
+
    public String(byte bytes[], int offset, int length, String charsetName)
+            throws UnsupportedEncodingException {
+        if (charsetName == null)
+            throw new NullPointerException("charsetName");
+        checkBounds(bytes, offset, length);
+        this.value = StringCoding.decode(charsetName, bytes, offset, length);
+    }
+
    static char[] decode(String charsetName, byte[] ba, int off, int len)
+        throws UnsupportedEncodingException
+    {
+        StringDecoder sd = deref(decoder);
+        String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
+        if ((sd == null) || !(csn.equals(sd.requestedCharsetName())
+                              || csn.equals(sd.charsetName()))) {
+            sd = null;
+            try {
+                Charset cs = lookupCharset(csn);
+                if (cs != null)
+                    sd = new StringDecoder(cs, csn);
+            } catch (IllegalCharsetNameException x) {}
+            if (sd == null)
+                throw new UnsupportedEncodingException(csn);
+            set(decoder, sd);
+        }
+        return sd.decode(ba, off, len);
+    }
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/java/rookie-exception/index.html b/blog-site/public/posts/java/rookie-exception/index.html new file mode 100644 index 00000000..45f81a5b --- /dev/null +++ b/blog-site/public/posts/java/rookie-exception/index.html @@ -0,0 +1,1043 @@ + + + + + + + + + + + Java异常 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Java异常

+ 2021.01.13 +
+

Java异常分类

+

异常类型

+

Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:ErrorException

+

其中 Error 用来表示Java程序无法处理的错误;这类错误一般与硬件有关,与程序本身无关,通常由系统进行处理,程序本身无法捕获和处理。是不可控制的。

+

Exception 分为两种:运行时异常和检查型异常。

+
    +
  • 受检异常 :需要用try...catch...语句捕获并进行处理,并且可以从异常中恢复; +
    public void test() throws MyException{}
    +
    Java编译器对检查性异常会要求我们进行catch,必须得进行捕获,否则编译不过去。Java认为检查性异常都可以被处理,所以必须显示的处理 checked 异常。 +常见的检查性异常有IOExceptionSqlException。 +当我们希望我们的⽅法调⽤者, 明确的处理⼀些特殊情况的时候, 就应该使⽤受检异常。
  • +
  • 非受检异常 :是程序运行时错误。例如:除 0 会引发 ArithmeticException ,此时程序崩溃并且无法恢复。 +
    public void test() {
    +    int a = 1;
    +    int b = a/0;
    +}
    +
    这种异常⼀般可以理解为是代码原因导致的。 +⽐如发⽣空指针、 数组越界等。 所以, 只要代码写的没问题, 这些异常都是可以避免的。 +也就不需要我们显⽰的进⾏处理。
  • +
+

Exception 表⽰程序需要捕捉、 需要处理的常,是由与程序设计的不完善⽽出现的问题,程序必须处理的问题。

+
+

异常和错误的区别:异常(Exception)能被程序本身可以处理,错误(Error)是无法处理。

+
+

自定义异常

+

在 Java 中可以自定义异常。编写自己的异常类时需要注意:

+
    +
  • 所有异常都必须是 Throwable 的子类。
  • +
  • 如果希望写一个检查性异常类,则需要继承 Exception 类。
  • +
  • 如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。
  • +
+

大多数情况下都会继承RuntimeException,自定义运行时异常。

+
public class MainTest {
+
+    void context() throws TestException {
+        throw new TestException();
+    }
+    
+    void  context2() {
+        throw new Test2Exception();
+    }
+}
+// TestException 为受检异常 程序必须要处理否则编译不通过
+class TestException extends Exception {
+}
+
+// Test2Exception为运行时异常 可以不进行处理
+class Test2Exception extends RuntimeException{
+}
+

异常链

+
+

异常链是Java中⾮常流⾏的异常处理概念, 是指在进⾏⼀个异常处理时抛出了另外⼀个异常, 由此产⽣了⼀个异常链条。

+
+

如果因为因为异常你决定抛出⼀个新的异常, 你⼀定要包含原有的异常, 这样, 处理程序才可以通过getCause()initCause()⽅法来访问异常最终的根源。

+
public class MainTest {
+    public static void main(String[] args) {
+        try {
+            int a = 1;
+            int b = a / 0;
+        } catch (ArithmeticException e) {
+            throw new RuntimeException("除以零异常", e);
+        }
+    }
+}
+

在此示例中,当捕获到ArithmeticException时,将创建一个新的RuntimeException异常,并附加原始的异常原因,并将异常链抛出到下一个更高级别的异常处理程序。

+

处理异常

+

异常的处理⽅式有两种:

+
    +
  1. ⾃⼰处理。
  2. +
  3. 向上抛, 交给调⽤者处理。
  4. +
+

不要丢弃异常,捕获异常后需要进行相关处理。如果用户觉得不能很好地处理该异常,就让它继续传播,传到别的地方去处理,或者把一个低级的异常转换成应用级的异常,重新抛出。 +千万不能捕获了之后什么也不做。 或者只是使⽤e.printStacktrace。如果是练习这样写也就算了,但是在正式的环境上不能这样做。正式环境请使用日志记录。

+
+

写完代码后请一定要检查下,代码中千万不要有printStackTrace()。因为printStackTrace()只会在控制台上输出错误的堆栈信息,他只适合于用来代码调试。

+
+

catch语句应该指定具体的异常类型。不能把不该捕获的异常也捕获了;如果finally里面也会抛出异常,也一样需要使用try..catch处理。

+

try..catch..

+

在Java中如果需要处理异常,必须先对异常通过try..catch..进行捕获,然后再对异常情况进行处理。

+
try{
+   // 程序代码
+}catch(异常类型1 异常的变量名1){
+  // 程序代码
+}
+

一个 try 对应多个 catch,进行多重捕获。

+

可以在 try 语句后面添加任意数量的 catch 块。如果发生异常,异常被抛给第一个 catch 块。如果不匹配,它会被传递给第二个 catch 块。 +如此,直到异常被捕获或者通过所有的 catch 块。

+
try{
+   // 程序代码
+}catch(异常类型1 异常的变量名1){
+  // 程序代码
+}catch(异常类型2 异常的变量名2){
+  // 程序代码
+}catch(异常类型3 异常的变量名3){
+  // 程序代码
+}
+

在JDK7之后,可以将 catch 语句块折叠,这意味着可以将多个不同类型的异常合并处理。

+
try{
+  // 程序代码
+} catch (异常类型1 | 异常类型2 | 异常类型3 e) {
+  // 程序代码
+}
+

finally

+

try..catch..通常连用用来捕获异常;try..catch..finally..也可以连用。

+
try{
+  // 程序代码
+  return a;
+}catch(异常类型2 异常的变量名2){
+  // 程序代码
+  return b;
+}finally{
+  // 程序代码
+  return c;
+}
+

对于try..catch..finally..语句块中执行顺序的解释

+

根据JVM规范:

+
    +
  • 如果 try 语句块里边有返回值则返回 try 语句块里边的;
  • +
  • 如果 try 语句块和 finally 语句块都有 return,则忽略 try 语句块里边的使用 finally 语句块里边的 return;
  • +
  • finally 语句块是在 try 语句块或者 catch 语句块中的 return 语句之前执行的;
  • +
  • 无论是否发生异常,finally 代码块中的代码总会被执行;
  • +
+

如果方法有返回值,切忌不要再 finally 中使用 return,这样会使得程序结构变得混乱。

+
+

finally语句块什么时候不执行 ?

+

如果当一个线程在执行 try 语句块或者 catch 语句块时被打断(interrupted)或者被终止(killed)或退出虚拟机(System.exit(0)),与其相对应的 finally 语句块可能不会执行。 +还有更极端的情况,就是在线程运行 try 语句块或者 catch 语句块时,突然死机或者断电,finally 语句块肯定不会执行了。

+
+

JVM先会把try或者 catch代码块中的返回值保留,再来执行 finally 代码块中的语句,等到 finally 代码块执行完毕之后,在把之前保留的返回值给返回出去。 +这条规则(保留返回值),只适用于 returnthrow 语句,不适用于 breakcontinue 语句,因为它们根本就没有返回值。

+
public class MyTest {
+ 
+	public static void main(String[] args) {
+        // main 代码块中的执行结果为:1
+		System.out.println("main 代码块中的执行结果为:" + myMethod());
+	}
+ 
+	public static int myMethod() {
+		int i = 1;
+		try {
+			System.out.println("try 代码块被执行!");
+			return i;
+		} finally {
+			++i;
+			System.out.println("finally 代码块被执行!");
+			System.out.println("finally 代码块中的i = " + i);
+		}
+ 
+	}
+ 
+}
+

try 语句块不止可以与 catch 连用,也可以与 finally 连用,但是 catch 不能与 finally 连用。

+
try{
+  // 程序代码
+} finally{
+  // 程序代码
+}
+

try-with-resources

+

由于..finally..语句块中的代码一般情况下一定会执行,所以经常用来关闭资源。

+

代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        InputStream in = null;
+        try {
+            in = new FileInputStream("awsl");
+            in.read();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            if (in != null) {
+                try {
+                    in.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+}
+

自从JDK7之后,支持try-with-resources的写法,这种写法对比之前更清晰、明了:

+
public class MainTest {
+    public static void main(String[] args) {
+        try (InputStream in = new FileInputStream("awsl")) {
+            in.read();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+}
+

这种写法其实是Java语法糖。

+
+

Java语法糖

+

1.什么是语法糖? +语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·兰丁发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。

+

2.能够带来的好处 +语法糖让程序更加简洁,有更高的可读性

+

3.有哪些语法糖? +1.自动拆箱、装箱 +2.泛型擦除 +3.不定长参数 +4.迭代器 +5.枚举 +6.switch支持枚举和字符串 +7.内部类 +8.try-with-resources +9.lambda

+
+

throws、throw

+

处理异常的方法,除了可以捕获异常,也可以将其丢给调用者进行处理。

+
    +
  • throws 用在方法上声明异常,子类继承的时候要继承该异常或者该异常的子类,不处理异常,谁调用该方法谁处理异常; +throws抛出异常时,它的调用者也要申明抛出异常或者捕获,不然编译报错。 +
    public static void main(String[] args) throws Exception {}
    +
  • +
  • throw用于方法内部,抛出的是异常对象。调用者可以不申明或不捕获(这是非常不负责任的方式)但编译器不会报错。 +
    throw new RuntimeException("这是运行中的异常");
    +
  • +
+

throws表示出现异常的一种可能性,告诉调用者这个方法是危险的,并不一定会发生这些异常;throw则是抛出了异常,执行throw则一定抛出了某种异常对象。 +两者都是消极处理异常的方式(这里的消极并不是说这种方式不好),只是抛出或者可能抛出异常,但是不会由方法去处理异常,真正的处理异常由此方法的上层调用处理。

+

经验总结

+

参考文章:

+ +

不要滥用异常

+

要谨慎地使用异常,异常捕获的代价非常高昂,异常使用过多会严重影响程序的性能。 +如果在程序中能够用if语句和boolean变量来进行逻辑判断,那么尽量减少异常的使用,从而避免不必要的异常捕获和处理。

+

空catch

+

千万不要使用空的catch块:

+
try{
+ // ...
+}catch(IOException e){
+  // ...
+}
+

在捕获了异常之后什么都不做,相当于忽略了这个异常。空的catch块意味着你在程序中隐藏了错误和异常,并且很可能导致程序出现不可控的执行结果。 +如果你非常肯定捕获到的异常不会以任何方式对程序造成影响,最好用日志将该异常进行记录,以便日后方便更新和维护。

+

吞掉异常

+

请不要在catch块中吞掉异常:

+
catch (NoSuchMethodException e) {
+   return null;
+}
+

不要不处理异常,而返回null,这样异常就会被吞掉,无法获取到任何失败信息,会给日后的问题排查带来巨大困难。

+

精确处理异常

+
public void foo() throws Exception { //错误做法
+}
+

一定要尽量避免上面的代码,因为他的调用者完全不知道错误的原因到底是什么。

+

在方法声明中,可以由方法抛出一些特定受检异常。如果有多个,那就分别抛出多个,这样这个方法的使用者才会分别针对每个异常做特定的处理,从而避免发生故障。

+
public void foo() throws SpecificException1, SpecificException2 { 
+//正确做法
+}
+

同样的在捕获异常时,也要注意,尽量捕获特定的子类,而不是直接捕获Exception类。

+
try {
+    someMethod();
+} 
+catch (Exception e) {
+    log.error("method has failed", e);
+}
+

上面代码,最大的问题就是,如果someMethod()的开发者在里面新增了一个特定的异常,并且预期是调用方能够特殊的对他进行处理。

+

但是调用者直接catch了Exception类,就会导致永远无法知道someMethod的具体变化细节。这久可能导致在运行的过程中在某一个时间点程序崩溃。

+

更不要去捕获Throwable类。因为Java中的Error也可以是Throwable的子类。但是Error是Java虚拟机本身无法控制的。Java虚拟机甚至可能不会在出现任何错误时请求用户的catch子句。

+
try {
+    someMethod();
+} 
+catch (Throwable e) {
+    log.error("method has failed", e);
+}
+

OutOfMemoryErrorStackOverflowError便是典型的例子,它们都是由于一些超出应用处理范围的情况导致的。

+

抛出异常

+

通常情况下,在捕获异常的时候抛出异常,需要注意的是,要始终在自定义异常中,覆盖原有的异常,从而构成一条异常链,这样堆栈跟踪就不会丢失:

+
catch (NoSuchMethodException e) {
+    throw new MyServiceException("Some information: " + e.getMessage());  //错误做法
+}
+

上面的命令可能会丢失掉主异常的堆栈跟踪。正确的方法是:

+
catch (NoSuchMethodException e) {
+     throw new MyServiceException("Some information: " , e);  //正确做法
+}
+

需要注意的是,可以记录异常或抛出异常,但不要同时做:

+
catch (NoSuchMethodException e) {
+   log.error("Some information", e);
+   throw e;
+}
+

抛出和日志记录可能会在日志文件中产生多个日志消息,这就会导致同一个问题,却在日志中有很多不同的错误信息,使得开发人员陷入混乱。

+

选择异常

+

一旦你决定抛出异常,你就要决定抛出抛出检查异常还是非检查异常。

+

检查异常导致了太多的try…catch代码,可能有很多检查异常对开发人员来说是无法合理地进行处理的,比如:SQLException,而开发人员却不得不去进行try…catch,这样就会导致经常出现这样一种情况:逻辑代码只有很少的几行,而进行异常捕获和处理的代码却有很多行。 +这样不仅导致逻辑代码阅读起来晦涩难懂,而且降低了程序的性能。

+

建议尽量避免检查异常的使用,如果确实该异常情况出现很的普遍,需要提醒调用者注意处理的话,就使用检查异常;否则使用非检查异常。 +因此,在一般情况下,尽量将检查异常转变为非检查异常交给上层处理。

+

不要在finally中抛异常

+
try {
+  someMethod();  //抛出 exceptionOne
+}finally{
+  cleanUp();    //如果在这里再抛出一个异常,那么try中的exception将会丢失
+}
+

在上面的例子中,如果someMethod()抛出一个异常,并且在finally块中,cleanUp()也抛出一个异常,那么初始的exception(正确的错误异常)将永远丢失。

+

但是,如果你不想处理someMethod()中的异常,但是仍然需要做一些清理工作,那么在finally块中进行清理。不要使用catch块。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/java/rookie-io/index.html b/blog-site/public/posts/java/rookie-io/index.html new file mode 100644 index 00000000..e2062309 --- /dev/null +++ b/blog-site/public/posts/java/rookie-io/index.html @@ -0,0 +1,4042 @@ + + + + + + + + + + + JavaIO | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JavaIO

+ 2021.04.09 +
+

概念

+

Java IO通过数据流、序列化和文件系统提供系统输入和输出。

+
+

IO,即 in 和 out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。

+
+

传统的 IO 是通过流技术来处理的。

+
+

流(Stream),是一个抽象的概念,是指一连串的数据(字符或字节),是以先进先出的方式发送信息的通道。 +代表任何有能力产出数据的数据源对象或者是有能力接受数据的接收端对象。

+
+

流的作用就是为数据源和目的地建立一个输送通道

+

一般来说关于流的特性有下面几点:

+
    +
  • 先进先出:最先写入输出流的数据最先被输入流读取到。
  • +
  • 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。(RandomAccessFile除外)
  • +
  • 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。 +在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流。
  • +
+

流的分类

+

JavaIO流分类

+

根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。

+

主要的分类方式有以下3种:

+
    +
  • 按数据流的方向:输入流、输出流
  • +
  • 按处理数据单位:字节流、字符流
  • +
  • 按功能:节点流、处理流
  • +
+

输入流与输出流

+

此输入、输出是相对于我们写的代码程序而言。

+
    +
  • 输入流:从别的地方获取资源 输入到 我们的程序中
  • +
  • 输出流:从我们的程序中输出到别的地方;例如:将一个字符串保存到本地文件中,就需要使用输出流。
  • +
+

字节流与字符流

+

字节流和字符流的用法几乎完成全一样,区别在于字节流和字符流所操作的数据单元不同,字节流操作的单元是数据单元是8位的字节,字符流操作的是数据单元为16位的字符。

+
+
    +
  • 字符流的由来
  • +
+

Java中字符是采用Unicode标准,一个字符是16位,即一个字符使用两个字节来表示。 +为此,JAVA中引入了处理字符的流。因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。

+
    +
  • 为什么要有字符流?
  • +
+

Java中字符是采用Unicode标准,Unicode 编码中,一个英文为一个字节,一个中文为两个字节。 +如果使用字节流处理中文,如果一次读写一个字符对应的字节数就不会有问题,一旦将一个字符对应的字节分裂开来,就会出现乱码了。

+
+
    +
  • 字节流:每次读取(写出)一个字节,当传输的资源文件有中文时,就会出现乱码,
  • +
  • 字符流:每次读取(写出)两个字节,有中文时,使用该流就可以正确传输显示中文。
  • +
+
+
    +
  • 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件。 +用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本文件。
  • +
  • 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了。
  • +
+
+

节点流与处理流

+

按功能不同分为 节点流、处理流:

+
    +
  • 节点流:以从或向一个特定的地方读写数据。如FileInputStream .
  • +
  • 处理流:是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader。 +处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,
  • +
+

处理流是对节点流的封装,最终的数据处理还是由节点流完成的。

+

使用流

+

看上面的几个分类,可能会感觉到有些混乱,那什么时候用字节流,什么时候该用输出流呢?

+
+

1、首先自己要知道是选择输入流还是输出流,这就要根据自己的情况而定,如果你想从程序写东西到别的地方,那么就选择输出流,反之用输入流 +2、然后考虑你传输数据时,是选择使用字节流传输还是字符流,也就是每次传1个字节还是2个字节,有中文肯定就选择字符流了。 +3、前面两步就可以选出一个合适的节点流了,比如字节输入流inputStream,如果要在此基础上增强功能,那么就在处理流中选择一个合适的即可。

+
+

File类

+

如果我们想要操作流首先,那不得不首先说一下File类。

+

Java中的File类以抽象的方式代表文件名和目录路径名。该类主要用于文件和目录的创建、文件的查找和文件的删除等。

+

File对象代表磁盘中实际存在的文件和目录。

+

使用 File 类,查找文件、删除文件、创建文件的代码演示

+
 public static void main(String args[]) throws IOException {
+        String dirname = "/home/dir";
+        File file = new File(dirname);
+
+        // 判断目录或文件存在
+        if (!file.exists()) {
+            System.out.println(dirname + " 该路径或文件不存在");
+            System.out.println("开始创建该文件或目录....");
+            // 不存在则创建
+            if (file.createNewFile()) {
+                System.out.println(dirname + " 创建成功");
+            }
+        }
+
+        // 判断是否为目录
+        if (file.isDirectory()) {
+            System.out.println("目录: " + dirname);
+            String s[] = file.list();
+            for (int i = 0; i < s.length; i++) {
+                File f = new File(dirname + "/" + s[i]);
+                if (f.isDirectory()) {
+                    System.out.println(s[i] + " 是文件夹");
+                } else {
+                    System.out.println(s[i] + " 是文件");
+                }
+            }
+        } else {
+            System.out.println(dirname + " 不是一个目录");
+            System.out.println("开始删除文件 ...");
+            // 删除文件
+            if (file.delete()) {
+                System.out.println(dirname + "删除成功"); 
+            }
+        }
+
+    }
+

操作字节流

+

字节流

+

操作byte类型数据,主要操作类是OutputStream、InputStream的子类;不用缓冲区,直接对文件本身操作。

+

以下代码就是用FileInputStream、FileOutputStream操作字节流

+
public static void main(String[] args) throws IOException {
+        File file = new File("./test.txt");
+        write(file);
+        System.out.println(read(file));
+    }
+
+    // 用字节流写入
+    public static void write(File file) throws IOException {
+        OutputStream os = new FileOutputStream(file, true);
+        // 要写入的字符串
+        String string = "awslawslawslawslawslawslawsl";
+        // 写入文件
+        os.write(string.getBytes());
+        // 关闭流
+        os.close();
+    }
+
+    // 用字节流读取
+    public static String read(File file) throws IOException {
+        InputStream in = new FileInputStream(file);
+        // 一次性取多少个字节
+        byte[] bytes = new byte[1024];
+        // 用来接收读取的字节数组
+        StringBuilder sb = new StringBuilder();
+        // 读取到的字节数组长度,为-1时表示没有数据
+        int length = 0;
+        // 循环取数据
+        while ((length = in.read(bytes)) != -1) {
+            // 将读取的内容转换成字符串
+            sb.append(new String(bytes, 0, length));
+        }
+        // 关闭流
+        in.close();
+        return sb.toString();
+    }
+

缓冲字节流是为高效率而设计的,真正的读写操作还是靠FileOutputStreamFileInputStream

+
public static void main(String[] args) throws IOException {
+        File file = new File("test.txt");
+        write(file);
+        System.out.println(read(file));
+    }
+
+    public static void write(File file) throws IOException {
+        // 缓冲字节流,提高了效率
+        BufferedOutputStream bis = new BufferedOutputStream(new FileOutputStream(file, true));
+        // 要写入的字符串
+        String string = "awslawslawslawslawslawslawsl";
+        // 写入文件
+        bis.write(string.getBytes());
+        // 关闭流
+        bis.close();
+    }
+
+    public static String read(File file) throws IOException {
+        BufferedInputStream fis = new BufferedInputStream(new FileInputStream(file));
+        // 一次性取多少个字节
+        byte[] bytes = new byte[1024];
+        // 用来接收读取的字节数组
+        StringBuilder sb = new StringBuilder();
+        // 读取到的字节数组长度,为-1时表示没有数据
+        int length = 0;
+        // 循环取数据
+        while ((length = fis.read(bytes)) != -1) {
+            // 将读取的内容转换成字符串
+            sb.append(new String(bytes, 0, length));
+        }
+        // 关闭流
+        fis.close();
+        return sb.toString();
+    }
+

操作字符流

+

字符流

+

操作字符类型数据,主要操作类是Reader、Writer的子类;使用缓冲区缓冲字符,不关闭流就不会输出任何内容。

+

字符流适用于文本文件的读写,OutputStreamWriter 类其实也是借助 FileOutputStream 类实现的

+

以下代码就是用InputStreamReader、OutputStreamWriter操作字节流.

+
public static void main(String[] args) throws IOException {
+        File file = new File("test.txt");
+        write(file);
+        System.out.println(read(file));
+    }
+
+    public static void write(File file) throws IOException {
+        // OutputStreamWriter可以显示指定字符集,否则使用默认字符集
+        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(file, true), "UTF-8");
+        // 要写入的字符串
+        String string = "awslawslawslawslawslawslawslawsl";
+        osw.write(string);
+        osw.close();
+    }
+
+    public static String read(File file) throws IOException {
+        InputStreamReader isr = new InputStreamReader(new FileInputStream(file), "UTF-8");
+        // 字符数组:一次读取多少个字符
+        char[] chars = new char[1024];
+        // 每次读取的字符数组先append到StringBuilder中
+        StringBuilder sb = new StringBuilder();
+        // 读取到的字符数组长度,为-1时表示没有数据
+        int length;
+        // 循环取数据
+        while ((length = isr.read(chars)) != -1) {
+            // 将读取的内容转换成字符串
+            sb.append(chars, 0, length);
+        }
+        // 关闭流
+        isr.close();
+        return sb.toString();
+    }
+

字符缓冲流

+
public static void main(String[] args) throws IOException {
+        File file = new File("test.txt");
+        write(file);
+        System.out.println(read(file));
+    }
+
+    public static void write(File file) throws IOException {
+        // FileWriter可以大幅度简化代码
+        BufferedWriter bw = new BufferedWriter(new FileWriter(file, true));
+        // 要写入的字符串
+        String string = "awslawslawslawslawslawslawslawsl";
+        bw.write(string);
+        bw.close();
+    }
+
+    public static String read(File file) throws IOException {
+        BufferedReader br = new BufferedReader(new FileReader(file));
+        // 用来接收读取的字节数组
+        StringBuilder sb = new StringBuilder();
+        // 按行读数据
+        String line;
+        // 循环取数据
+        while ((line = br.readLine()) != null) {
+            // 将读取的内容转换成字符串
+            sb.append(line);
+        }
+        // 关闭流
+        br.close();
+        return sb.toString();
+    }
+

字节流和字符流间的转换

+
    +
  • OutputStreamWriter 是字符流通向字节流的桥梁
  • +
+
    public static void main(String[] args) throws IOException {
+        File f = new File("test.txt");
+
+        // OutputStreamWriter 是字符流通向字节流的桥梁,创建了一个字符流通向字节流的对象
+        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(f),"UTF-8");
+
+        osw.write("我是字符流转换成字节流输出的");
+        osw.close();
+
+    }
+
    +
  • InputStreamReader 是字节流通向字符流的桥梁
  • +
+
  public static void main(String[] args) throws IOException {
+        
+        File f = new File("test.txt");
+        
+        InputStreamReader inr = new InputStreamReader(new FileInputStream(f),"UTF-8");
+        
+        char[] buf = new char[1024];
+        
+        int len = inr.read(buf);
+        System.out.println(new String(buf,0,len));
+        
+        inr.close();
+
+    }
+

Java序列化、反序列化

+

序列化是将对象的状态信息转换为可存储或传输的形式的过程(一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型)。 +是一种数据的持久化手段。一般广泛应用于网络传输,RMI和RPC等场景中。 +一般是以字节码或XML格式传输。而字节码或XML编码格式可以还原为完全相等的对象。

+

将序列化对象写入文件之后,可以从文件中读取出来,这个相反的过程称为反序列化。

+

序列化作用

+

序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。 +序列化机制使得对象可以脱离程序的运行而独立存在。

+

对象序列化机制是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组, +并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。对象序列化可以很容易的在JVM中的活动对象和字节流之间进行转换。

+

由于序列化整个过程都是 Java 虚拟机独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。

+

在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。

+

使用序列化

+

使用Java对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。 +必须注意地是,对象序列化保存的是对象的"状态",即它的成员变量。所以,对象序列化不会关注类中的静态变量。

+

如果需要将某个对象保存到磁盘上或者通过网络传输,那么这个类应该实现Serializable接口或者Externalizable接口。

+

Serializable

+
+

类的可序列化性是通过实现 java.io.Serializable 接口的类来启用的。没有实现此接口的类的任何状态都不会被序列化或反序列化。 +可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,只用于标识可序列化的语义。

+

为了允许非序列化类的子类型被序列化,子类型可以承担保存和恢复超类型的公共、受保护和(如果可以访问)包字段的状态的责任。 +只有当它所继承的类有一个可访问的无参数构造函数来初始化类的状态时,子类型才可以承担这种责任。如果不是这种情况,则声明一个类可序列化是错误的。 +该错误将在运行时检测到。

+

当试图对一个对象进行序列化的时候,如果遇到不支持 Serializable 接口的对象。在此情况下,将抛出 NotSerializableException。并标识非serializable对象的类。

+
+

实现Serializable序列化反序列对象化代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+//       serialUser();
+        System.out.println("----------反序列化对象----------");
+        unSerialUser();
+    }
+
+    private static void serialUser (){
+        User user = new User();
+        user.setName("Jane");
+        user.setAge("100");
+        System.out.println(user);
+        try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./user.txt"));) {
+            oos.writeObject(user);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static void unSerialUser() {
+        File file = new File("./user.txt");
+        try(ObjectInputStream ois  = new ObjectInputStream(new FileInputStream(file))) {
+            User newUser = (User) ois.readObject();
+            System.out.println(newUser);
+        } catch (IOException | ClassNotFoundException e) {
+            e.printStackTrace();
+        }
+    }
+
+}
+
+class User implements Serializable {
+    private String name;
+    private String age;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getAge() {
+        return age;
+    }
+
+    public void setAge(String age) {
+        this.age = age;
+    }
+
+    @Override
+    public String toString() {
+        return "User{" +
+                "name='" + name + '\'' +
+                ", age='" + age + '\'' +
+                '}';
+    }
+}
+

Externalizable

+
+

Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。 +当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()readExternal()方法。 +由于上面的代码中,并没有在这两个方法中定义序列化实现细节,所以输出的内容为空。

+

还有一点值得注意:在使用Externalizable进行序列化的时候,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。 +所以,实现Externalizable接口的类必须要提供一个public的无参的构造器。

+
+

如果User类中没有无参数的构造函数,在反序列化时会抛出异常: +java.io.InvalidClassException: content.posts.rookie.User; no valid constructor

+

实现Externalizable序列化反序列对象化代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+//       serialUser();
+        System.out.println("----------反序列化对象----------");
+        unSerialUser();
+    }
+
+    private static void serialUser ()  {
+        User user = new User();
+        user.setName("Jane");
+        user.setAge("100");
+        System.out.println(user);
+        // /将对象序列化到文件
+        try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./user.txt"));) {
+            oos.writeObject(user);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static void unSerialUser() {
+        File file = new File("./user.txt");
+        try(ObjectInputStream ois  = new ObjectInputStream(new FileInputStream(file))) {
+            User newUser = (User) ois.readObject();
+            System.out.println(newUser);
+        } catch (IOException | ClassNotFoundException e) {
+            e.printStackTrace();
+        }
+    }
+
+}
+
+class User implements Externalizable {
+
+    public User() {
+    }
+
+    private String name;
+    private String age;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getAge() {
+        return age;
+    }
+
+    public void setAge(String age) {
+        this.age = age;
+    }
+
+    @Override
+    public String toString() {
+        return "User{" +
+                "name='" + name + '\'' +
+                ", age='" + age + '\'' +
+                '}';
+    }
+
+    @Override
+    public void writeExternal(ObjectOutput out) throws IOException {
+        out.writeObject(name);
+        out.writeObject(age);
+    }
+
+    @Override
+    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+        name = (String) in.readObject();
+        age = (String) in.readObject();
+    }
+}
+

transient

+

对于一个类中的某些字段如果不需要序列化,就需要加上transient关键字。

+
+

transient修饰的成员变量,在序列化的时候其值会被忽略,在被反序列化后, transient 变量的值被设为初始值, +如 int 型的是 0,对象型的是 null。

+
+
private transient String name;
+

此时name字段将不会被序列化;当然如果一个变量被static修饰,他也不会被序列化。

+

serialVersionUID

+

虚拟机是否允许反序列化, 不仅取决于类路径和功能代码是否⼀致, ⼀个⾮常重要的⼀点是两个类的序列化 ID 是否⼀致, 即serialVersionUID要求⼀致。

+

因为⽂件存储中的内容可能被篡改,为了保证数据的安全: 在进⾏反序列化时, JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进⾏⽐较, 如果相同就认为是⼀致的, 可以进⾏反序列化; +否则就会出现序列化版本不⼀致的异常, 即是InvalidCastException

+

以下内容来自Serializable接口注释

+
+

If a serializable class does not explicitly declare a serialVersionUID, +then the serialization runtime will calculate a default +serialVersionUID value for that class based on various aspects of the class, +as described in the Java(TM) Object Serialization Specification. +However, it is strongly recommended that all serializable classes explicitly declare serialVersionUID values, +since the default serialVersionUID computation is highly sensitive to class details that may vary depending on compiler implementations, +and can thus result in unexpectedInvalidClassExceptions during deserialization.

+
+

当实现java.io.Serializable接口的类没有显式地定义⼀个serialVersionUID变量时候,Java序列化机制会根据编译的Class⾃动⽣成⼀个serialVersionUID作序列化版本⽐较⽤, +这种情况下,如果Class⽂件没有发⽣变化,就算再编译多次, serialVersionUID也不会变化的。 +但是,如果发⽣了变化,那么这个⽂件对应的serialVersionUID也就会发⽣变化。

+

Java强烈建议用户自定义一个serialVersionUID,因为默认的serialVersinUID对于class的细节非常敏感, +反序列化时可能会导致InvalidClassException这个异常。

+

代码演示序列化、反序列化加上serialVersionUID

+
private static final long serialVersionUID = 1L;
+
public class MainTest {
+    public static void main(String[] args) {
+        System.out.println("----------序列化对象----------");
+        serialUser();
+        System.out.println("----------反序列化对象----------");
+        unSerialUser();
+    }
+
+    private static void serialUser (){
+        User user = new User();
+        user.setName("Jane");
+        user.setAge("100");
+        System.out.println(user);
+        try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("./user.txt"));) {
+            oos.writeObject(user);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+    private static void unSerialUser() {
+        File file = new File("./user.txt");
+        try(ObjectInputStream ois  = new ObjectInputStream(new FileInputStream(file))) {
+            User newUser = (User) ois.readObject();
+            System.out.println(newUser);
+        } catch (IOException | ClassNotFoundException e) {
+            e.printStackTrace();
+        }
+    }
+
+}
+
+class User implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String name;
+    private String age;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getAge() {
+        return age;
+    }
+
+    public void setAge(String age) {
+        this.age = age;
+    }
+
+    @Override
+    public String toString() {
+        return "User{" +
+                "name='" + name + '\'' +
+                ", age='" + age + '\'' +
+                '}';
+    }
+}
+
java.io.InvalidClassException: co.test.User; local class incompatible: stream classdesc serialVersionUID = -1643371274357194431, local class serialVersionUID = 1
+

代码调用链:

+
ObjectInputStream.readObject -> readObject0 -> readOrdinaryObject -> readClassDesc -> readNonProxyDesc -> ObjectStreamClass.initNonProxy
+

initNonProxy中 ,关键代码如下:

+
 void initNonProxy(ObjectStreamClass model,
+                      Class<?> cl,
+                      ClassNotFoundException resolveEx,
+                      ObjectStreamClass superDesc)
+        throws InvalidClassException
+    {
+        long suid = Long.valueOf(model.getSerialVersionUID());
+        ObjectStreamClass osc = null;
+        if (cl != null) {
+            osc = lookup(cl, true);
+            if (osc.isProxy) {
+                throw new InvalidClassException(
+                        "cannot bind non-proxy descriptor to a proxy class");
+            }
+            if (model.isEnum != osc.isEnum) {
+                throw new InvalidClassException(model.isEnum ?
+                        "cannot bind enum descriptor to a non-enum class" :
+                        "cannot bind non-enum descriptor to an enum class");
+            }
+
+            // ========== 判断反序列化 serializableUID 是否一致 ========== start//
+            if (model.serializable == osc.serializable &&
+                    !cl.isArray() &&
+                    suid != osc.getSerialVersionUID()) {
+                throw new InvalidClassException(osc.name,
+                        "local class incompatible: " +
+                                "stream classdesc serialVersionUID = " + suid +
+                                ", local class serialVersionUID = " +
+                                osc.getSerialVersionUID());
+            }
+            // ========== 判断反序列化 serializableUID 是否一致 ========== end//
+
+            if (!classNamesEqual(model.name, osc.name)) {
+                throw new InvalidClassException(osc.name,
+                        "local class name incompatible with stream class " +
+                                "name \"" + model.name + "\"");
+            }
+            
+            // ...
+   
+

getSerialVersionUID方法:

+
public long getSerialVersionUID() {
+    // REMIND: synchronize instead of relying on volatile?
+    if (suid == null) {
+        suid = AccessController.doPrivileged(
+            new PrivilegedAction<Long>() {
+                public Long run() {
+                    return computeDefaultSUID(cl);
+                }
+            }
+        );
+    }
+    return suid.longValue();
+}
+

在没有定义serialVersionUID的时候,会调用computeDefaultSUID方法,生成一个默认的serialVersionUID

+

serialVersionUID有两种显示的生成方式:

+
    +
  • 默认的1L,比如:private static final long serialVersionUID = 1L;
  • +
  • 根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如: private static final long serialVersionUID = xxxxL;
  • +
+

第二种方式可通过编译器进行配置: +idea检查serialVersionUID

+

idea自动生成serialVersionUID

+

序列化底层原理

+

如何自定义的序列化和反序列化策略?

+

通过在被序列化的类中增加 writeObject 和 readObject 方法来实现。

+

java.util.ArrayList中我们能找到答案:

+
public class ArrayList<E> extends AbstractList<E>
+        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
+{
+    private static final long serialVersionUID = 8683452581122892189L;
+    transient Object[] elementData; // non-private to simplify nested class access
+    private int size;
+}
+

ArrayList实现了java.io.Serializable接口,那么我们就可以对它进行序列化及反序列化。 +因为elementDatatransient 的,所以这个成员变量不会被序列化而保留下来.

+

ArrayList底层是通过数组实现的。 +那么数组elementData其实就是用来保存列表中的元素的。通过该属性的声明方式我们知道,他是无法通过序列化持久化下来的。 +那么为什么却通过序列化和反序列化把List中的元素保留下来了呢?

+
public static void main(String[] args) throws IOException, ClassNotFoundException {
+        List<String> stringList = new ArrayList<String>();
+        stringList.add("hello");
+        stringList.add("world");
+        stringList.add("hollis");
+        stringList.add("chuang");
+        System.out.println("init StringList" + stringList);
+        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("stringlist"));
+        objectOutputStream.writeObject(stringList);
+
+        IOUtils.close(objectOutputStream);
+        File file = new File("stringlist");
+        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
+        List<String> newStringList = (List<String>)objectInputStream.readObject();
+        IOUtils.close(objectInputStream);
+        if(file.exists()){
+            file.delete();
+        }
+        System.out.println("new StringList" + newStringList);
+    }
+//init StringList[hello, world, hollis, chuang]
+//new StringList[hello, world, hollis, chuang]
+
+

在序列化过程中,如果被序列化的类中定义了writeObjectreadObject 方法,虚拟机会试图调用对象类里的 writeObjectreadObject 方法,进行用户自定义的序列化和反序列化。

+

如果没有这样的方法,则默认调用是 ObjectOutputStreamdefaultWriteObject 方法以及 ObjectInputStreamdefaultReadObject 方法。

+

用户自定义的 writeObjectreadObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。 +对象的序列化过程通过 ObjectOutputStreamObjectInputputStream 来实现的.

+
+

ArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100, +而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化, +ArrayList把元素数组设置为transient。

+

为了防止一个包含大量空对象的数组被序列化,为了优化存储,所以,ArrayList使用transient来声明elementData。 +但是,作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化下来, +所以,通过重写writeObject 和 readObject方法的方式把其中的元素保留下来。

+
    +
  • writeObject 方法把 elementData 数组中的元素遍历的保存到输出流(ObjectOutputStream)中。
  • +
  • readObject 方法从输入流(ObjectInputStream)中读出对象并保存赋值到 elementData 数组中。
  • +
+

在一个类中定义了 writeObjectreadObject 方法,那么这两个方法是怎么被调用的呢?

+

在使用 ObjectOutputStreamwriteObject 方法和 ObjectInputStreamreadObject 方法时,会通过反射的方式调用。

+

ObjectOutputStream中writeObject的调用栈:

+
+

writeObject —> writeObject0 —>writeOrdinaryObject—>writeSerialData—>invokeWriteObject

+
+
+

调用表示的 serializable 类的 writeObject 方法。 +如果类描述符不与类相关联,或者该类是可外部化、不可序列化的,或者没有定义 writeObject, +则抛出 UnsupportedOperationException

+

类定义的writeObject方法,如果没有则为null

+
+

invokeWriteObject方法

+
    /**
+     * Invokes the writeObject method of the represented serializable class.
+     * Throws UnsupportedOperationException if this class descriptor is not
+     * associated with a class, or if the class is externalizable,
+     * non-serializable or does not define writeObject.
+     */
+    void invokeWriteObject(Object obj, ObjectOutputStream out)
+        throws IOException, UnsupportedOperationException
+    {
+        requireInitialized();
+        if (writeObjectMethod != null) {
+            try {
+
+                // ========== 调用writeObject 方法 start========== //
+                writeObjectMethod.invoke(obj, new Object[]{ out });
+                // ========== 调用writeObject 方法 end========== //
+
+            } catch (InvocationTargetException ex) {
+                Throwable th = ex.getTargetException();
+                if (th instanceof IOException) {
+                    throw (IOException) th;
+                } else {
+                    throwMiscException(th);
+                }
+            } catch (IllegalAccessException ex) {
+                // should not occur, as access checks have been suppressed
+                throw new InternalError(ex);
+            }
+        } else {
+            throw new UnsupportedOperationException();
+        }
+    }
+
    /** class-defined writeObject method, or null if none */
+    private Method writeObjectMethod;
+

为什么实现了Serializable接口就能保证对象序列化?

+

ObjectOutputStream中writeObject的调用栈:

+
+

writeObject —> writeObject0 —>writeOrdinaryObject—>writeSerialData—>invokeWriteObject

+
+

writeObject0方法

+
 /**
+     * Underlying writeObject/writeUnshared implementation.
+     */
+    private void writeObject0(Object obj, boolean unshared)
+        throws IOException
+    {
+        boolean oldMode = bout.setBlockDataMode(false);
+        depth++;
+        try {
+           // ... 
+
+            // remaining cases
+            if (obj instanceof String) {
+                writeString((String) obj, unshared);
+            } else if (cl.isArray()) {
+                writeArray(obj, desc, unshared);
+            } else if (obj instanceof Enum) {
+                writeEnum((Enum<?>) obj, desc, unshared);
+            // =============================
+            } else if (obj instanceof Serializable) {
+                writeOrdinaryObject(obj, desc, unshared);
+            } else {
+                if (extendedDebugInfo) {
+                    throw new NotSerializableException(
+                        cl.getName() + "\n" + debugInfoStack.toString());
+                } else {
+                    throw new NotSerializableException(cl.getName());
+                }
+            }
+            // =============================
+        } finally {
+            // ... 
+        }
+    }
+

在进行序列化操作时,会判断要被序列化的类是否是 String、Enum、ArraySerializable 类型, +如果不是则直接抛出 NotSerializableException

+

序列化与单例模式

+

序列化破坏单例

+

为什么序列化可以破坏单例了?

+

序列化会通过反射调用无参数的构造方法创建一个新的对象。

+
public class MainTest {
+    public static void main(String[] args) throws Exception {
+        String path = "/Users/whitepure/github/iblog/blog-site/content/posts/rookie/singleton.txt";
+
+        //Write Obj to file
+        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(path));
+        oos.writeObject(Singleton.getSingleton());
+
+        //Read Obj from file
+        File file = new File(path);
+        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
+        Singleton newInstance = (Singleton) ois.readObject();
+
+        //判断是否是同一个对象
+        System.out.println(newInstance == Singleton.getSingleton());
+    }
+
+}
+
+class Singleton implements Serializable {
+    private static final long serialVersionUID = 6377402142849822126L;
+
+    private volatile static Singleton singleton;
+
+    private Singleton() {
+    }
+
+    public static Singleton getSingleton() {
+        if (singleton == null) {
+            synchronized (MainTest.class) {
+                if (singleton == null) {
+                    singleton = new Singleton();
+                }
+            }
+        }
+        return singleton;
+    }
+}
+

输出结果为false,对Singleton 的序列化与反序列化得到的对象是一个新的对象,这就破坏了 Singleton 的单例性。

+

分析原因

+

对象的序列化过程通过 ObjectOutputStreamObjectInputputStream 来实现的

+

ObjectInputStreamreadObject 的调用栈:

+
+

readObject —> readObject0 —> readOrdinary —> checkResolve

+
+

readOrdinaryObject 方法

+
+

读取并返回"ordinary"(即,不是字符串,类,ObjectStreamClass,数组,或枚举常量)对象,如果对象的类是不可解析的,则为null(在这种情况下,ClassNotFoundException将与对象的句柄相关联)。 +设置passHandle为对象的赋值句柄。

+
+
    /**
+     * Reads and returns "ordinary" (i.e., not a String, Class,
+     * ObjectStreamClass, array, or enum constant) object, or null if object's
+     * class is unresolvable (in which case a ClassNotFoundException will be
+     * associated with object's handle).  Sets passHandle to object's assigned
+     * handle.
+     */
+    private Object readOrdinaryObject(boolean unshared)
+        throws IOException
+    {
+
+        // ...
+
+        Object obj;
+        try {
+            // `desc.isInstantiable()`: 如果一个 `serializable/externalizable` 的类可以在运行时被实例化,那么该方法就返回true
+            // `desc.newInstance`:该方法通过反射的方式调用无参构造方法新建一个对象
+            obj = desc.isInstantiable() ? desc.newInstance() : null;
+        } catch (Exception ex) {
+            throw (IOException) new InvalidClassException(
+                desc.forClass().getName(),
+                "unable to create instance").initCause(ex);
+        }
+
+        // ...
+
+        // hasReadResolveMethod:如果实现了serializable 或者 externalizable接口的类中包含readResolve则返回true
+        if (obj != null &&
+            handles.lookupException(passHandle) == null &&
+            desc.hasReadResolveMethod())
+        {
+            // invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve方法
+            Object rep = desc.invokeReadResolve(obj);
+            if (unshared && rep.getClass().isArray()) {
+                rep = cloneArray(rep);
+            }
+            // ...
+        }
+
+        return obj;
+    }
+

解决

+

Singleton 中定义 readResolve 方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

+
class Singleton implements Serializable {
+    private static final long serialVersionUID = 6377402142849822126L;
+
+    private volatile static Singleton singleton;
+
+    private Singleton() {
+    }
+
+    public static Singleton getSingleton() {
+        if (singleton == null) {
+            synchronized (MainTest.class) {
+                if (singleton == null) {
+                    singleton = new Singleton();
+                }
+            }
+        }
+        return singleton;
+    }
+
+    public Object readResolve() {
+        return singleton;
+    }
+}
+

IO模型

+

IO模型共有5种:阻塞IO、非阻塞IO、信号驱动IO、IO多路转接、异步IO。其中,前四个被称为同步IO。

+

阻塞式IO模型

+

BIO(Blocking IO):最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。

+

当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。 +当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。

+

特点:

+
    +
  • 进程阻塞挂起不消耗CPU资源,及时响应每个操作;
  • +
  • 适用并发量小的网络应用开发;
  • +
+

因为一个请求IO会阻塞进程,不能充分利用cpu资源,所以,得为每请求分配一个处理进程(线程)以及时响应,系统开销大;不适用并发量大的应用。

+

非阻塞IO模型

+

NIO(NoBlocking IO):当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。 +如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。 +一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。

+

特点:

+
    +
  • 进程轮询(重复)调用,消耗CPU的资源;
  • +
  • 适用并发量较小、且不需要及时响应的网络应用开发;
  • +
+

在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。

+

IO复用模型

+

IO复用模型: 一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。 +在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。

+

Java NIO实际上就是多路复用IO。 +通过selector.select()查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。所以,多路复用IO比较适合连接数比较多的情况。 +IO多路复用模型

+

特点:

+
    +
  • 专一进程解决多个进程IO的阻塞问题,性能好;
  • +
  • 适用高并发服务应用开发:一个进程响应多个请求;
  • +
+
+

多路复用IO为何比非阻塞IO模型的效率高? +因为在非阻塞IO中,不断地询问socket状态是通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。

+
+

多路复用IO模型是通过轮询的方式来检测是否有事件到达,并且对到达的事件逐一进行响应。 +因此对于多路复用IO模型来说,一旦事件响应体很大,那么就会导致后续的事件迟迟得不到处理,并且会影响新的事件轮询。

+

信号驱动IO模型

+

当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行不阻塞, +当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。

+

进程预先告知内核,使得当某个socket有事件发生时,系统内核使用信号通知相关进程。

+

特点:

+
    +
  • 回调机制,实现、开发应用难度较大;
  • +
+

异步IO模型

+

AIO(Async IO):当用户线程发起IO操作后,立刻就可以开始去做其它的事。 +另一方面,从内核的角度,当它收到一个IO请求之后,它会立刻返回给用户线程,说明IO请求已经成功发起了,因此不会对用户线程产生任何阻塞。 +然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它IO操作完成了。

+

用户线程完全不需要知道实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。

+

在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。

+

用户线程中不需要再次调用IO函数进行具体的读写。 +这点是和信号驱动模型有所不同的,在信号驱动模型中,当用户线程接收到信号表示数据已经就绪,然后需要用户线程调用IO函数进行实际的读写操作; +而在异步IO模型中,收到信号表示IO操作已经完成,不需要再在用户线程中调用IO函数进行实际的读写操作。

+

特点:

+
    +
  • 不阻塞,数据一步到位;
  • +
  • 需要操作系统的底层支持,linux 2.5 版本内核首现,2.6 版本产品的内核标准特性;
  • +
  • 实现、开发应用难度大,需要开发者合理控制;
  • +
  • 非常适合高性能高并发应用;
  • +
+

NIO

+

Java NIO 解释为: New IONon Blocking IO 是从J ava 1.4 版本开始引入的一个新的IO API,可以替代标准的Java IO API。 +NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。

+

与传统IO的区别:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
区别IONIO
传输方式面向流,通过流传输面向缓冲区,通过缓冲区传输
是否阻塞阻塞IO非阻塞IO
其他选择器,可以解决阻塞问题
+

IO

+

NIO

+

通道与缓冲区

+

通道负责传输,缓冲区负责存储。

+

若需要使用 NIO ,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。

+

缓冲区

+

缓冲区:在java NIO 中负者数据的存储。缓冲区底层实现是数组。用于存储不同类型的数据。 +根据数据类型的不同(boolean 除外),有以下 Buffer 常用子类:

+
    +
  • ByteBuffer
  • +
  • CharBuffer
  • +
  • ShortBuffer
  • +
  • IntBuffer
  • +
  • LongBuffer
  • +
  • FloatBuffer
  • +
  • DoubleBuffer
  • +
+
常用API及属性解析
+

在父类抽象类Buffer中存在四个核心属性:

+
    +
  • capacity:容量,表示缓冲区中最大存储数据的容量。一旦声明不能改变。
  • +
  • limit:界限,表示缓冲区中可以操作数据的大小。(limit后数据不能进行读写)
  • +
  • position:位置,表示缓冲区中正在操作数据的位置。
  • +
  • mark:标记,表示记录当前position位置。可以通过reset()恢复到mark的位置。
  • +
+

大小关系:0<=mark<=position<=limit<=capacity

+

存储数据:

+
    +
  • put():存入数据到缓冲区中
  • +
  • put(byte b):将给定单个字节写入缓冲区的当前位置
  • +
  • put(byte[] src):将 src 中的字节写入缓冲区的当前位置
  • +
  • put(int index, byte b):将指定字节写入缓冲区的索引位置
  • +
+

flip(): 切换为读取数据模式

+

读取数据:

+
    +
  • get():获取缓存区中的数据
  • +
  • get() :读取单个字节
  • +
  • get(byte[] dst):批量读取多个字节到 dst 中
  • +
  • get(int index):读取指定索引位置的字节
  • +
+

clear(): 清空缓冲区;但是缓冲区中的数据依然存在,只是将position、limit 的值回归到初始值

+

position、limit 数值变化:

+
    +
  • 使用 put() 存储数据时,把 position 向前移动,移动长度为要存储数据的长度;
  • +
  • 使用 filp() 切换为只读模式时,把 position 的值赋值给 limit,在将 position 的值归零;
  • +
  • 使用 get() 读取数据时,在把 position 移动,移动的长度为想要读取的长度,但是要小于等于 limit 的位置;
  • +
  • 使用 rewind() 切换为重读模式时,将 position、limit 恢复到使用 filp() 方法时的值;
  • +
  • 使用 clear() 清空缓冲区,将position、limit 的值回归到初始值;
  • +
+

代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        String str = "abcde";
+        ByteBuffer allocate = ByteBuffer.allocate(1024);
+        allocate.put(str.getBytes());
+        System.out.println("========向ByteBuffer添加数据========");
+        System.out.println(str);
+        System.out.println("capacity: "+ allocate.capacity());
+        System.out.println("limit: " + allocate.limit());
+        System.out.println("position: " + allocate.position());
+
+        allocate.flip();
+        System.out.println("========切换为读取数据模式========");
+        System.out.println("capacity: "+ allocate.capacity());
+        System.out.println("limit: " + allocate.limit());
+        System.out.println("position: " + allocate.position());
+
+        byte[] bytes = str.getBytes();
+        allocate.get(bytes);
+        System.out.println("========从ByteBuffer取出数据========");
+        System.out.println(new String(bytes, 0, bytes.length));
+        System.out.println("capacity: "+ allocate.capacity());
+        System.out.println("limit: " + allocate.limit());
+        System.out.println("position: " + allocate.position());
+
+        allocate.rewind();
+        System.out.println("========切换重新读取数据模式========");
+        System.out.println("capacity: "+ allocate.capacity());
+        System.out.println("limit: " + allocate.limit());
+        System.out.println("position: " + allocate.position());
+
+        allocate.clear();
+        System.out.println("========清空缓冲区========");
+        System.out.println("capacity: "+ allocate.capacity());
+        System.out.println("limit: " + allocate.limit());
+        System.out.println("position: " + allocate.position());
+        System.out.println("再来读取数据:" + (char)allocate.get());
+    }
+}
+

mark方法: 记录当前position位置。可以通过 reset() 恢复到 mark 的位置。

+

代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        String str = "abcde";
+        ByteBuffer allocate = ByteBuffer.allocate(1024);
+
+        allocate.put(str.getBytes());
+
+        allocate.flip();
+
+        byte[] bytes = str.getBytes();
+        allocate.get(bytes, 0, 2);
+        System.out.println("========从ByteBuffer取出数据========");
+        System.out.println(new String(bytes, 0, 2));
+        System.out.println("capacity: " + allocate.capacity());
+        System.out.println("limit: " + allocate.limit());
+        System.out.println("position: " + allocate.position());
+
+        allocate.mark();
+        System.out.println("========记录当前 `position` 的位置========");
+        System.out.println("capacity: " + allocate.capacity());
+        System.out.println("limit: " + allocate.limit());
+        System.out.println("position: " + allocate.position());
+
+        allocate.get(bytes, 2, 2);
+        System.out.println("========从ByteBuffer再次取出数据========");
+        System.out.println(new String(bytes, 2, 2));
+        System.out.println("capacity: " + allocate.capacity());
+        System.out.println("limit: " + allocate.limit());
+        System.out.println("position: " + allocate.position());
+
+        allocate.reset();
+        System.out.println("========恢复之前被标记的位置========");
+        System.out.println("capacity: " + allocate.capacity());
+        System.out.println("limit: " + allocate.limit());
+        System.out.println("position: " + allocate.position());
+    }
+}
+
非直接缓冲区与直接缓冲区
+

直接缓冲区:通过 allocateDirect() 方法分配直接缓冲区,将缓冲区建立在物理内存中。可以提高读写效率。

+

直接缓冲区

+

非直接缓冲区:通过 allocate() 方法分配缓冲区,将缓冲区建立在JVM的内存中。

+

allocate()方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区 。 +直接缓冲区的内容可以驻留在常规的垃圾回收堆之外.

+

非直接缓冲区

+
public class MainTest {
+    public static void main(String[] args) {
+        ByteBuffer allocate = ByteBuffer.allocate(1024);
+        ByteBuffer direct = ByteBuffer.allocateDirect(1024);
+
+        if (direct.isDirect()){
+            System.out.println("allocateDirect 是直接缓冲区");
+        }
+        if (!allocate.isDirect()){
+            System.out.println("allocate 是非直接缓冲区");
+        }
+    }
+}
+

通道

+

通道(Channel):用于源节点与目标节点的连接。在 java NIO 中负责缓冲区中数据的传输。Channel本身不存储数据,需要配合缓冲区进行数据传输。

+

在操作系统中,通道是一种通过执行通道程序管理I/O操作的控制器,它使主机(CPU和内存)与I/O操作之间达到更高的并行程度。 +需要进行I/O操作时,CPU只需启动通道,然后可以继续执行自身程序,通道则执行通道程序,管理与实现I/O操作。

+
操作通道
+

通道的主要实现类:

+
    +
  • FileChannel:用于读取、写入、映射和操作文件的通道。
  • +
  • SocketChannel:通过 TCP 读写网络中的数据。
  • +
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel
  • +
  • DatagramChannel:通过 UDP 读写网络中的数据通道。
  • +
+

Java 针对支持通道的类提供了 getChannel() 方法

+

使用 非直接缓冲区 完成对文件的读写

+
public class MainTest {
+    /**
+     * 使用非直接缓冲区完成读写操作
+     * @param args args
+     */
+    public static void main(String[] args) {
+        long start = System.currentTimeMillis();
+        try (
+                // 获取通道
+                FileChannel inChannel = new FileInputStream("1.jpg").getChannel();
+                FileChannel outChannel = new FileOutputStream("2.jpg").getChannel();
+        ) {
+            // 分配指定大小的缓冲区
+            ByteBuffer buf = ByteBuffer.allocate(1024);
+
+            // 将通道中的数据存入缓冲区中
+            while (inChannel.read(buf) != -1) {
+
+                // 切换读取数据的模式
+                buf.flip();
+
+                // 将缓冲区中的数据写入通道中
+                outChannel.write(buf);
+
+                // 清空缓冲区
+                buf.clear();
+            }
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            long end = System.currentTimeMillis();
+            System.out.println("耗费的时间为:" + (end - start));
+        }
+    }
+}
+

使用 直接缓冲区 完成对文件的读写

+
public class MainTest {
+    /**
+     * 使用直接缓冲区完成文件的读写
+     *
+     * @param args args
+     */
+    public static void main(String[] args) {
+        long start = System.currentTimeMillis();
+        try (
+                FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
+                FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
+        ) {
+
+            //内存映射文件
+            MappedByteBuffer inMappedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
+            MappedByteBuffer outMappedBuf = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
+
+            //直接对缓冲区进行数据的读写操作
+            byte[] dst = new byte[inMappedBuf.limit()];
+            inMappedBuf.get(dst);
+            outMappedBuf.put(dst);
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            long end = System.currentTimeMillis();
+            System.out.println("耗费的时间为:" + (end - start));
+        }
+    }
+}
+

使用 通道 完成对文件的读写

+
public class MainTest {
+    /**
+     * 使用通道完成读写操作
+     *
+     * @param args args
+     */
+    public static void main(String[] args) {
+        long start = System.currentTimeMillis();
+        try (
+                // 获取通道
+                FileChannel inChannel = FileChannel.open(Paths.get("1.jpg"), StandardOpenOption.READ);
+                FileChannel outChannel = FileChannel.open(Paths.get("2.jpg"), StandardOpenOption.WRITE, StandardOpenOption.READ, StandardOpenOption.CREATE);
+        ) {
+
+            //内存映射文件
+            MappedByteBuffer inMappedBuf = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());
+            MappedByteBuffer outMappedBuf = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, inChannel.size());
+            
+            //直接对缓冲区进行数据的读写操作
+            byte[] dst = new byte[inMappedBuf.limit()];
+            inMappedBuf.get(dst);
+            outMappedBuf.put(dst);
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            long end = System.currentTimeMillis();
+            System.out.println("耗费的时间为:" + (end - start));
+        }
+    }
+}
+

分散读取和聚集写入:

+
    +
  • 分散读取(Scattering Reads):将通道中的数据分散到多个缓冲区中
  • +
  • 聚集写入(Gathering Writes):将多个缓冲区中的数据聚集到通道中
  • +
+
public class MainTest {
+    /**
+     * 分散和聚集
+     *
+     * @param args args
+     */
+    public static void main(String[] args) {
+        long start = System.currentTimeMillis();
+        try (
+            // 分散读取通道
+            FileChannel channel1 = new RandomAccessFile("1.txt", "rw").getChannel();
+            // 聚集写入通道
+            FileChannel channel2 = new RandomAccessFile("2.txt", "rw").getChannel();
+        ) {
+            // 分配指定大小的缓冲区
+            ByteBuffer buf1 = ByteBuffer.allocate(100);
+            ByteBuffer buf2 = ByteBuffer.allocate(1024);
+
+            // 分散读取
+            ByteBuffer[] bufs = {buf1, buf2};
+            channel1.read(bufs);
+
+            for (ByteBuffer byteBuffer : bufs) {
+                byteBuffer.flip();
+            }
+
+            System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
+            System.out.println("--------------------");
+            System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
+
+            // 聚集写入
+            channel2.write(bufs);
+
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            long end = System.currentTimeMillis();
+            System.out.println("耗费的时间为:" + (end - start));
+        }
+    }
+}
+

解码与编码:

+
    +
  • 编码:字符串转化为字符数组的过程
  • +
  • 解码:字符数组转化为字符串的过程
  • +
+
public class MainTest {
+    /**
+     * 编码与解码
+     *
+     * @param args args
+     */
+    public static void main(String[] args) {
+        Charset cs1 = Charset.forName("GBK");
+
+        //获取编码器
+        CharsetEncoder ce = cs1.newEncoder();
+
+        //获取解码器
+        CharsetDecoder cd = cs1.newDecoder();
+
+        CharBuffer cBuf = CharBuffer.allocate(1024);
+        cBuf.put("阿伟死了");
+        cBuf.flip();
+
+        ByteBuffer bBuf;
+        try {
+            //编码
+            bBuf = ce.encode(cBuf);
+            for (int i = 0; i < 8; i++) {
+                System.out.println(bBuf.get());
+            }
+            bBuf.flip();
+
+            //解码
+            CharBuffer cBuf2 = cd.decode(bBuf);
+            System.out.println(cBuf2.toString());
+
+        } catch (CharacterCodingException e) {
+            e.printStackTrace();
+        }
+    }
+}
+

阻塞与非阻塞网络通信

+

阻塞

+

传统的 IO 流都是阻塞式的。 +就是说,当一个线程调用 read()write() 时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。 +因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。

+

阻塞式IO,代码演示

+
class Client {
+    public static void main(String[] args) throws IOException {
+        System.out.println("启动客户端 ...");
+        client();
+    }
+
+    /**
+     * 阻塞NIO 客户端
+     */
+    public static void client() throws IOException {
+        SocketChannel sChannel=SocketChannel.open(new InetSocketAddress("127.0.0.1",9898));
+        FileChannel inChannel=FileChannel.open(Paths.get("/Users/whitepure/Desktop/1.txt"), StandardOpenOption.READ);
+        ByteBuffer buf=ByteBuffer.allocate(1024);
+
+        while(inChannel.read(buf)!=-1){
+            buf.flip();
+            sChannel.write(buf);
+            buf.clear();
+        }
+
+        //关闭发送通道,表明发送完毕
+        sChannel.shutdownOutput();
+
+        //接收服务端的反馈
+        int len=0;
+        while((len=sChannel.read(buf))!=-1){
+            buf.flip();
+            System.out.println(new String(buf.array(),0,len));
+            buf.clear();
+        }
+        inChannel.close();
+        sChannel.close();
+    }
+}
+
+class Server {
+    public static void main(String[] args) throws IOException {
+        System.out.println("启动服务端 ...");
+        server();
+    }
+    /**
+     * 阻塞IO 服务器方
+     */
+    public static void server() throws IOException{
+        ServerSocketChannel ssChannel=ServerSocketChannel.open();
+        FileChannel outChannel=FileChannel.open(Paths.get("/Users/whitepure/Desktop/2.txt"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
+        ssChannel.bind(new InetSocketAddress(9898));
+        SocketChannel sChannel=ssChannel.accept();
+        ByteBuffer buf=ByteBuffer.allocate(1024);
+        
+        while(sChannel.read(buf)!=-1){
+            buf.flip();
+            outChannel.write(buf);
+            buf.clear();
+        }
+
+        //发送反馈给客户端
+        buf.put("服务端接收数据成功".getBytes());
+        buf.flip();
+        sChannel.write(buf);
+
+        sChannel.close();
+        outChannel.close();
+        ssChannel.close();
+    }
+}
+

非阻塞

+

Java NIO 是非阻塞模式的。 +当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。 +因此,NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。

+

非阻塞式IO,代码演示

+
class Client {
+    public static void main(String[] args) throws IOException {
+        System.out.println("启动客户端 ... 等待输入 ...");
+        client();
+    }
+
+    /**
+     * 非阻塞NIO 客户端
+     */
+    public static void client() throws IOException {
+        SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898));
+        ByteBuffer buf = ByteBuffer.allocate(1024);
+
+        Scanner scanner = new Scanner(System.in);
+
+        // 发送数据到服务方
+        while (scanner.hasNextLine()) {
+            buf.put((LocalDate.now() + "\n" + scanner.next()).getBytes());
+            buf.flip();
+            sChannel.write(buf);
+            buf.clear();
+        }
+        sChannel.close();
+    }
+}
+
+class Server {
+    public static void main(String[] args) throws IOException {
+        System.out.println("启动服务端 ... 等待客户端请求 ...");
+        server();
+    }
+
+    /**
+     * 非阻塞IO 服务器方
+     */
+    public static void server() throws IOException {
+        ServerSocketChannel ssChannel = ServerSocketChannel.open();
+
+        // 切换为非阻塞模式
+        ssChannel.configureBlocking(false);
+        // 绑定链接
+        ssChannel.bind(new InetSocketAddress(9898));
+
+        // 获取选择器
+        Selector selector = Selector.open();
+
+        /**
+         * 将通道注册到选择器上,并且指定“监听接收事件”
+         * 使用 SelectionKey 的四个常量 表示
+         *
+         * 读 : SelectionKey.OP_READ (1)
+         * 写 : SelectionKey.OP_WRITE (4)
+         * 连接 : SelectionKey.OP_CONNECT (8)
+         * 接收 : SelectionKey.OP_ACCEPT (16)
+         *
+         * 若注册时不止监听一个事件,则可以使用“位或”操作符连接。
+         */
+        SelectionKey selectionKey = ssChannel.register(selector, SelectionKey.OP_ACCEPT);
+
+        // 轮巡获取 注册器上的接收事件
+        while (selector.select() > 0) {
+            // 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
+            Iterator<SelectionKey> it = selector.selectedKeys().iterator();
+
+            while (it.hasNext()) {
+                // 获取准备“就绪”的事件
+                SelectionKey sk = it.next();
+
+                // 判断具体是什么时间准备就绪
+                if (sk.isAcceptable()) {
+                    // 若“接收就绪”,获取客户端连接
+                    SocketChannel sChannel = ssChannel.accept();
+
+                    // 切换非阻塞模式
+                    sChannel.configureBlocking(false);
+
+                    // 将该通道注册到选择器上
+                    sChannel.register(selector, SelectionKey.OP_READ);
+                } else if (sk.isReadable()) {
+                    // 获取当前选择器上“读就绪”状态的通道
+                    SocketChannel sChannel = (SocketChannel) sk.channel();
+                    // 读取数据
+                    ByteBuffer buf = ByteBuffer.allocate(1024);
+                    int len = 0;
+                    while ((len = sChannel.read(buf)) > 0) {
+                        buf.flip();
+                        System.out.println(new String(buf.array(), 0, len));
+                        buf.clear();
+                    }
+                }
+
+                // 移除注册的选择键,否则会一直轮巡获取
+                it.remove();
+            }
+        }
+    }
+}
+

通道

+

Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。

+

代码演示

+
public class MainTest {
+    public static void main(String[] args) throws IOException {
+        //1.获取管道
+        Pipe pipe= Pipe.open();
+
+        //2.将缓冲区中的数据写入管道
+        ByteBuffer buf= ByteBuffer.allocate(1024);
+        Pipe.SinkChannel sinkChannel=pipe.sink();
+        buf.put("通过单向管道发送数据".getBytes());
+        buf.flip();
+        sinkChannel.write(buf);
+
+        //3.读取缓冲区中的数据
+        Pipe.SourceChannel sourceChannel=pipe.source();
+        buf.flip();
+        int len=sourceChannel.read(buf);
+        System.out.println(new String(buf.array(),0,len));
+
+        sourceChannel.close();
+        sinkChannel.close();
+    }
+}
+

Netty

+

概述

+
+

Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

+
+
    +
  1. Netty 是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络 IO 程序。
  2. +
  3. Netty 主要针对在 TCP 协议下,面向 Client 端的高并发应用,或者 Peer-to-Peer 场景下的大量数据持续传输的应用。
  4. +
  5. Netty 本质是一个 NIO 框架,适用于服务器通讯相关的多种应用场景。
  6. +
  7. Netty 是由 JBoss 提供的一个 Java 开源框架,现为 Github 上的独立项目。
  8. +
+

线程模型演变

+

目前存在的线程模型有:

+
    +
  • 传统阻塞 I/O 服务模型
  • +
  • Reactor 模式 +
      +
    • 单 Reactor 单线程
    • +
    • 单 Reactor 多线程
    • +
    • 主从 Reactor多线程
    • +
    +
  • +
+

Netty 主要基于主从 Reactor 多线程模型做了一定的改进。

+

传统 IO 模型

+

传统IO模型

+

采用阻塞 IO 模式获取输入的数据,每个连接都需要独立的线程完成数据的输入,业务处理,数据返回。 +当并发数很大,就会创建大量的线程,占用很大系统资源,连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在 read 操作,造成线程资源浪费。

+

Reactor 模型

+

基于 I/O 复用模型,多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。 当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。

+

Reactor模式

+

为了避免浪费可以创建一个线程池,当客户端发起请求时,通过DispatcherHandler进行分发请求处理到线程池,线程池中在使用具体的线程进行事件处理。 服务器端程序处理传入的多个请求,并将它们同步分派到相应的处理线程,因此 Reactor 模式也叫 Dispatcher 模式。

+

Reactor 模式使用 IO 复用监听事件,收到事件后,分发给某个线程(进程),这点就是网络服务器高并发处理关键。

+
单 Reactor 单线程
+

单Reactor单线程

+

步骤:

+
    +
  • Reactor 对象通过 Select 监控客户端请求事件,收到事件后通过 Dispatch 进行分发
  • +
  • 如果是建立连接请求事件,则由 Acceptor 通过 Accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后的后续业务处理
  • +
  • 如果不是建立连接事件,则 Reactor 会分发调用连接对应的 Handler 来响应
  • +
  • Handler 会完成 Read → 业务处理 → Send 的完整业务流程
  • +
+

缺点:

+
    +
  • 性能问题,只有一个线程,无法完全发挥多核 CPU 的性能
  • +
  • 如果线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息
  • +
+

优点:

+
    +
  • 模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成
  • +
+

使用场景:

+
    +
  • 客户端的数量有限,业务处理非常快速的情况
  • +
+
单 Reactor 多线程
+

单Reactor多线程

+

步骤:

+
    +
  • Reactor 对象通过对 select 监听请求事件,收到请求事件后交给 dispath 进行转发
  • +
  • 如果是建立连接请求,则通过 accept 处理连接请求,然后创建一个handler对象处理完成连接后的事件
  • +
  • 如果不是建立连接请求,则直接交给 handler 对象
  • +
  • handler 只负责响应事件,不做具体的业务处理,通过 read 读取完后,分发给下面的 worker线程池中某个线程处理
  • +
  • worker 线程负责处理具体业务,处理完成后会将具体的结果返回给 handler
  • +
  • handler 线程通过send方法返回给客户端
  • +
+

缺点:

+
    +
  • 多线程访问比较复杂,需要处理线程之间的竞争,资源共享
  • +
  • Reactor 对象在处理所有事件的监听和响应都是单线程的,在高并发场景容易出现性能瓶颈
  • +
+

优点:

+
    +
  • 可以充分利用CPU资源
  • +
+
主从 Reactor 多线程
+

主从Reactor线程

+

Reactor 主线程可以对应多个 Reactor 子线程,即 MainRecator 可以关联多个 SubReactor,从而解决了Reactor 在单线程中运行,高并发场景下容易成为性能瓶颈。 +步骤:

+
    +
  • Reactor 主线程 MainReactor 对象通过 select 监听连接事件,收到事件后,通过 Acceptor 处理连接事件
  • +
  • 当 Acceptor 处理连接事件后,MainReactor 将连接分配给 SubReactor
  • +
  • SubReactor 将连接加入到连接队列进行监听,并创建 handler 进行各种事件处理
  • +
  • 当有新事件发生时,SubReactor 就会调用对应的 handler 处理
  • +
  • handler 只负责响应事件,不做具体的业务处理,通过 read 读取完后,分发给下面的 worker线程池中某个线程处理
  • +
  • worker 线程负责处理具体业务,处理完成后会将具体的结果返回给 handler
  • +
  • handler 线程通过send方法返回给客户端
  • +
+

优点:

+
    +
  • 父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据
  • +
  • 父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理
  • +
+

缺点:

+
    +
  • 编程复杂度较高
  • +
+

这种模型在许多项目中广泛使用,包括 Nginx 主从 Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持。

+

Netty 模型

+

Netty模型

+

netty结构

+
    +
  • Netty 抽象出两组线程池 BossGroup 专门负责接收客户端的连接,WorkerGroup 专门负责网络的读写;BossGroupWorkerGroup 类型都是 NioEventLoopGroup
  • +
  • NioEventLoopGroup 相当于一个事件循环组,这个组中含有多个事件循环,每一个事件循环是 NioEventLoop,每个 NioEventLoop 都有一个 Selector,用于监听绑定在其上的 socket 的网络通讯
  • +
  • NioEventLoopGroup 可指定多个 NioEventLoop
  • +
  • 每个 BossNioEventLoop 循环执行的步骤 +
      +
    • 轮询 accept 事件
    • +
    • 处理 accept 事件,与 client 建立连接,生成 NioSocketChannel,并将其注册到某个 workerNioEventLoop 上的 Selector
    • +
    • 处理任务队列的任务,即 runAllTasks
    • +
    +
  • +
  • 每个 WorkerNioEventLoop 循环执行的步骤 +
      +
    • 轮询 read,write 事件
    • +
    • 处理 I/O 事件,即 read,write 事件,在对应 NioSocketChannel 处理
    • +
    • 处理任务队列的任务,即 runAllTasks
    • +
    +
  • +
  • 每个 WorkerNioEventLoop 处理业务时,会使用 pipeline(管道),pipeline 中包含了 channel(通道),即通过 pipeline 可以获取到对应通道,管道中维护了很多的处理器
  • +
+

简单使用

+

1.导入依赖

+
<dependency>
+    <groupId>io.netty</groupId>
+    <artifactId>netty-all</artifactId>
+    <version>4.1.36.Final</version>
+</dependency>
+<dependency>
+    <groupId>org.projectlombok</groupId>
+    <artifactId>lombok</artifactId>
+</dependency>
+

2.服务器端代码演示

+
/**
+ * netty 服务端测试
+ */
+public class MainTestServer {
+    public static void main(String[] args) {
+        // 启动器, 负责组装netty组件 启动服务器
+        new ServerBootstrap()
+                // BossEventLoop WorkEventLoop 每个 EventLoop 就是 一个选择器 + 一个线程
+                .group(new NioEventLoopGroup())
+                // 选择服务器 ServerSocketChannel 具体实现
+                .channel(NioServerSocketChannel.class)
+                // 决定了 workEventLoop 能做那些操作
+                .childHandler(
+                        // 建立连接后会被调用; 作用: 初始化 + 添加其他的 handler
+                        new ChannelInitializer<NioSocketChannel>() {
+                    // 当客户端请求发过来时 才会调用
+                    @Override 
+                    protected void initChannel(NioSocketChannel channel) throws Exception {
+                        channel.pipeline().addLast(new StringDecoder());
+                        // 自定义handler 
+                        channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
+                            @Override
+                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+                                System.out.println("服务器端接收数据:" + msg);
+                            }
+                        });
+                    }
+                })
+                // 绑定监听端口
+                .bind(8090);
+    }
+}
+

3.客户端代码演示

+
/**
+ * netty 客户端测试
+ */
+public class MainTestClient {
+    public static void main(String[] args) throws InterruptedException {
+        new Bootstrap()
+                .group(new NioEventLoopGroup())
+                .channel(NioSocketChannel.class)
+                .handler(new ChannelInitializer<NioSocketChannel>() {
+                    // 初始化 在与服务器建立链接的时候 调用
+                    @Override
+                    protected void initChannel(NioSocketChannel channel) throws Exception {
+                        // 添加编码器 只有当向服务端发送请求数据时 才会执行
+                        channel.pipeline().addLast(new StringEncoder());
+                    }
+                })
+                .connect(new InetSocketAddress("127.0.0.1", 8090))
+                //阻塞方法,直到与服务器端连接建立
+                .sync()
+                .channel()
+                // 向服务器端发送数据
+                .writeAndFlush("hello word");
+    }
+}
+

任务队列

+
    +
  • 用户程序自定义的普通任务
  • +
  • 用户自定义定时任务
  • +
  • 非当前 Reactor 线程调用 Channel 的各种方法 例如在推送系统的业务线程里面,根据用户的标识,找到对应的 Channel 引用,然后调用 Write 类方法向该用户推送消息,就会进入到这种场景。 +最终的 Write 会提交到任务队列中后被异步消费
  • +
+

客户端

+
/**
+ * netty 客户端测试
+ */
+public class MainTestClient {
+    public static void main(String[] args) throws InterruptedException {
+        new Bootstrap()
+                .group(new NioEventLoopGroup())
+                .channel(NioSocketChannel.class)
+                .handler(new ChannelInitializer<NioSocketChannel>() {
+
+                    // 初始化 在与服务器建立链接的时候 调用
+                    @Override
+                    protected void initChannel(NioSocketChannel channel) throws Exception {
+                        // 添加编码器 只有当向服务端发送请求数据时 才会执行
+                        channel.pipeline().addLast("decoder", new StringDecoder());
+                        channel.pipeline().addLast("encoder", new StringEncoder());
+                        channel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
+                            @Override
+                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+                                System.out.println("服务端响应数据:" + msg);
+                            }
+
+                            @Override
+                            public void channelActive(ChannelHandlerContext ctx) throws Exception {
+                                System.out.println("客户端Active .....");
+                            }
+
+                            /**
+                             * 客户端异常时触发
+                             */
+                            @Override
+                            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+                                cause.printStackTrace();
+                                ctx.close();
+                            }
+
+                        });
+                    }
+
+                })
+                .connect(new InetSocketAddress("127.0.0.1", 8090))
+                //阻塞方法,直到与服务器端连接建立
+                .sync()
+                .channel()
+                // 向服务器端发送数据
+                .writeAndFlush("hello word");
+    }
+}
+

服务端

+
/**
+ * netty 服务端测试
+ */
+public class MainTestServer {
+    public static void main(String[] args) {
+        //new 一个主线程组
+        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
+        //new 一个工作线程组
+        EventLoopGroup workGroup = new NioEventLoopGroup(200);
+        // 启动器, 负责组装netty组件 启动服务器
+        try {
+            ServerBootstrap serverBootstrap = new ServerBootstrap()
+                    // BossEventLoop WorkEventLoop 每个 EventLoop 就是 一个选择器 + 一个线程
+                    .group(bossGroup, workGroup)
+                    // 选择服务器 ServerSocketChannel 具体实现
+                    .channel(NioServerSocketChannel.class)
+                    //设置队列大小
+                    .option(ChannelOption.SO_BACKLOG, 1024)
+                    // 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
+                    .childOption(ChannelOption.SO_KEEPALIVE, true)
+                    // 决定了 workEventLoop 能做那些操作
+                    .childHandler(
+                            // 建立连接后会被调用; 作用: 初始化 + 添加其他的 handler
+                            new ChannelInitializer<NioSocketChannel>() {
+                                // 当客户端请求发过来时 才会调用
+                                @Override
+                                protected void initChannel(NioSocketChannel channel) throws Exception {
+                                    channel.pipeline().addLast("decoder", new StringDecoder(CharsetUtil.UTF_8));
+                                    channel.pipeline().addLast("encoder", new StringEncoder(CharsetUtil.UTF_8));
+                                    // 自定义handler
+                                    channel.pipeline().addLast(new ChannelInboundHandlerAdapter() {
+
+                                        /**
+                                         * 客户端连接会触发
+                                         */
+                                        @Override
+                                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
+                                            System.out.println("服务端 Active......");
+                                        }
+
+                                        /**
+                                         * 客户端发消息会触发
+                                         */
+                                        @Override
+                                        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+                                            System.out.println("服务器收到消息: " + msg.toString());
+                                            // 比如这里我们有一个非常耗时长的业务-> 应该一步执行 -> 提交该对应的channel
+                                            // 将任务放在 taskQueue 中
+                                            ctx.channel().eventLoop().execute(() -> {
+                                                try {
+                                                    Thread.sleep(10 * 1000);
+                                                    ctx.writeAndFlush("业务1处理完成");
+                                                } catch (InterruptedException e) {
+                                                    e.printStackTrace();
+                                                }
+                                            });
+                                            // 放在TaskQueue 中的任务是由一个线程来进行处理的 所以执行完上一个任务才会执行下一个任务 时间是累加的
+                                            ctx.channel().eventLoop().execute(() -> {
+                                                try {
+                                                    Thread.sleep(20 * 1000);
+                                                    ctx.writeAndFlush("业务2处理完成");
+                                                } catch (InterruptedException e) {
+                                                    e.printStackTrace();
+                                                }
+                                            });
+                                             // 用户自定义定时任务 -》 该任务是提交到 scheduleTaskQueue中
+                                        
+                                            ctx.channel().eventLoop().schedule(new Runnable() {
+                                                @Override
+                                                public void run() {
+                                    
+                                                    try {
+                                                        Thread.sleep(5 * 1000);
+                                                        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵4", CharsetUtil.UTF_8));
+                                                        System.out.println("channel code=" + ctx.channel().hashCode());
+                                                    } catch (Exception ex) {
+                                                        System.out.println("发生异常" + ex.getMessage());
+                                                    }
+                                                }
+                                            }, 5, TimeUnit.SECONDS);
+                                        }
+
+                                        /**
+                                         * 给客户端发送消息
+                                         */
+                                        @Override
+                                        public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
+                                            ctx.writeAndFlush("over");
+                                        }
+
+                                        /**
+                                         * 发生异常触发
+                                         */
+                                        @Override
+                                        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+                                            cause.printStackTrace();
+                                            ctx.close();
+                                        }
+
+
+                                    });
+                                }
+                            });
+                    // 绑定监听端口
+            ChannelFuture future = serverBootstrap.bind(8090).sync();
+            // 对关闭通道进行监听
+            future.channel().closeFuture().sync();
+        } catch (InterruptedException e) {
+
+        } finally {
+            //关闭主线程组
+            bossGroup.shutdownGracefully();
+            //关闭工作线程组
+            workGroup.shutdownGracefully();
+        }
+    }
+}
+

异步模型

+

异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态、通知和回调来通知调用者。

+

Netty 中的 I/O 操作是异步的,包括 Bind、Write、Connect 等操作会简单的返回一个 ChannelFuture。 调用者并不能立刻获得结果,而是通过 Future-Listener 机制,用户可以方便的主动获取或者通过通知机制获得 IO 操作结果。

+

Future 对象刚刚创建时,处于非完成状态,调用者可以通过返回的 ChannelFuture 来获取操作执行的状态,注册监听函数来执行完成后的操作。

+

常见有如下操作:

+
    +
  • 通过 isDone 方法来判断当前操作是否完成;
  • +
  • 通过 isSuccess 方法来判断已完成的当前操作是否成功;
  • +
  • 通过 getCause 方法来获取已完成的当前操作失败的原因;
  • +
  • 通过 isCancelled 方法来判断已完成的当前操作是否被取消;
  • +
  • 通过 addListener 方法来注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果 Future 对象已完成,则通知指定的监听器
  • +
+
//绑定一个端口并且同步,生成了一个ChannelFuture对象
+//启动服务器(并绑定端口)
+ChannelFuture cf = bootstrap.bind(6668).sync();
+//给cf注册监听器,监控我们关心的事件
+cf.addListener(new ChannelFutureListener() {
+   @Override
+   public void operationComplete (ChannelFuture future) throws Exception {
+      if (cf.isSuccess()) {
+         System.out.println("监听端口6668成功");
+      } else {
+         System.out.println("监听端口6668失败");
+      }
+   }
+});
+

Netty搭建Http服务

+

服务端

+
public class MainTestServer {
+    public static void main(String[] args) {
+        //new 一个主线程组
+        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
+        //new 一个工作线程组
+        EventLoopGroup workGroup = new NioEventLoopGroup(200);
+        // 启动器, 负责组装netty组件 启动服务器
+        try {
+            ServerBootstrap serverBootstrap = new ServerBootstrap()
+                    // BossEventLoop WorkEventLoop 每个 EventLoop 就是 一个选择器 + 一个线程
+                    .group(bossGroup, workGroup)
+                    // 选择服务器 ServerSocketChannel 具体实现
+                    .channel(NioServerSocketChannel.class)
+                    //设置队列大小
+                    .option(ChannelOption.SO_BACKLOG, 1024)
+                    // 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
+                    .childOption(ChannelOption.SO_KEEPALIVE, true)
+                    // 决定了 workEventLoop 能做那些操作
+                    .childHandler(
+                            // 建立连接后会被调用; 作用: 初始化 + 添加其他的 handler
+                            new ChannelInitializer<NioSocketChannel>() {
+                                // 当客户端请求发过来时 才会调用
+                                @Override
+                                protected void initChannel(NioSocketChannel channel) throws Exception {
+                                    channel.pipeline().addLast("MyHttpServerCodec", new HttpServerCodec());
+                                    // 自定义handler
+                                    channel.pipeline().addLast(new SimpleChannelInboundHandler<HttpObject>() {
+                                        @Override
+                                        protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
+                                            System.out.println("对应的channel=" + ctx.channel() + " pipeline=" + ctx.pipeline() + " 通过pipeline获取channel" + ctx.pipeline().channel());
+
+                                            System.out.println("当前ctx的handler=" + ctx.handler());
+
+                                            //判断 msg 是不是 httprequest请求
+                                            if (msg instanceof HttpRequest) {
+
+                                                System.out.println("ctx 类型=" + ctx.getClass());
+
+                                                System.out.println("pipeline hashcode" + ctx.pipeline().hashCode() + " TestHttpServerHandler hash=" + this.hashCode());
+
+                                                System.out.println("msg 类型=" + msg.getClass());
+                                                System.out.println("客户端地址" + ctx.channel().remoteAddress());
+
+                                                //获取到
+                                                HttpRequest httpRequest = (HttpRequest) msg;
+                                                //获取uri, 过滤指定的资源
+                                                URI uri = new URI(httpRequest.uri());
+                                                if ("/favicon.ico".equals(uri.getPath())) {
+                                                    System.out.println("请求了 favicon.ico, 不做响应");
+                                                    return;
+                                                }
+                                                //回复信息给浏览器 [http协议]
+
+                                                ByteBuf content = Unpooled.copiedBuffer("hello, 我是服务器", CharsetUtil.UTF_8);
+
+                                                //构造一个http的相应,即 httpresponse
+                                                FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
+
+                                                response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
+                                                response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
+
+                                                //将构建好 response返回
+                                                ctx.writeAndFlush(response);
+                                            }
+                                        }
+                                    });
+                                }
+                            });
+                    // 绑定监听端口
+            ChannelFuture future = serverBootstrap.bind(8090).sync();
+            future.addListener( future1 -> {
+                if (future.isSuccess()) {
+                    System.out.println("监听端口8090成功");
+                }else{
+                    System.out.println("监听端口8090失败");
+                }
+            });
+            // 对关闭通道进行监听
+            future.channel().closeFuture().sync();
+        } catch (InterruptedException e) {
+
+        } finally {
+            //关闭主线程组
+            bossGroup.shutdownGracefully();
+            //关闭工作线程组
+            workGroup.shutdownGracefully();
+        }
+    }
+}
+

TCP粘包、拆包及解决方案

+

TCP 是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的 socket,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle 算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,由于 TCP 无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题。

+

拆包和粘包是在socket编程中经常出现的情况,在socket通讯过程中,如果通讯的一端一次性连续发送多条数据包,tcp协议会将多个数据包打包成一个tcp报文发送出去,这就是所谓的粘包。而如果通讯的一端发送的数据包超过一次tcp报文所能传输的最大值时,就会将一个数据包拆成多个最大tcp长度的tcp报文分开传输,这就叫做拆包。

+

对于粘包的情况,要对粘在一起的包进行拆包。对于拆包的情况,要对被拆开的包进行粘包,即将一个被拆开的完整应用包再组合成一个完整包。比较通用的做法就是每次发送一个应用数据包前在前面加上四个字节的包长度值,指明这个应用包的真实长度。

+

使用netty解决拆包、粘包问题代码示例:

+

客户端代码

+
@SpringBootApplication
+public class NettyClientApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(NettyClientApplication.class, args);
+    }
+}
+
+@Slf4j
+@Component
+public class StartNetty implements CommandLineRunner {
+
+    private final NettyClient nettyClient;
+
+    public StartNetty(NettyClient nettyClient) {
+        this.nettyClient = nettyClient;
+    }
+
+    @Override
+    public void run(String... args) throws Exception {
+        log.info("启动netty客户端 ...");
+        nettyClient.start();
+    }
+}
+
+@Slf4j
+@Component
+public class NettyClient {
+    /**
+     * Netty客户端启动
+     */
+    public void start() {
+        EventLoopGroup group = new NioEventLoopGroup();
+        Bootstrap bootstrap = new Bootstrap()
+                .group(group)
+                //该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输
+                .option(ChannelOption.TCP_NODELAY, true)
+                .channel(NioSocketChannel.class)
+                .handler(new NettyClientInitializer());
+        try {
+            ChannelFuture future = bootstrap.connect("127.0.0.1", 9000).sync();
+            log.info("客户端成功....");
+            //发送消息
+            future.channel().writeAndFlush("客户端请求数据");
+            // 等待连接被关闭
+            future.channel().closeFuture().sync();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        } finally {
+            group.shutdownGracefully();
+        }
+    }
+}
+
+
+
+public class NettyClientInitializer extends ChannelInitializer<SocketChannel> {
+    @Override
+    protected void initChannel(SocketChannel socketChannel) throws Exception {
+        socketChannel.pipeline().addLast("decoder", new MyMessageDecoder());
+        socketChannel.pipeline().addLast("encoder", new MyMessageEncoder());
+        socketChannel.pipeline().addLast(new NettyClientHandler());
+    }
+}
+
+
+@Slf4j
+public class NettyClientHandler extends ChannelInboundHandlerAdapter {
+    @Override
+    public void channelActive(ChannelHandlerContext ctx) throws Exception {
+        log.info("客户端Active .....");
+        // 模拟tcp粘包
+        for (int i = 0; i < 5; i++) {
+            String mes = "今天天气冷,吃火锅";
+            byte[] content = mes.getBytes(Charset.forName("utf-8"));
+            int length = mes.getBytes(Charset.forName("utf-8")).length;
+
+            //  解决tcp粘包问题
+            MessageProtocol messageProtocol = new MessageProtocol();
+            messageProtocol.setLen(length);
+            messageProtocol.setContent(content);
+            ctx.writeAndFlush(messageProtocol);
+        }
+    }
+
+    /**
+     * 收到服务端的消息
+     */
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+        log.info("客户端收到消息: {}", msg.toString());
+        MessageProtocol mp = (MessageProtocol)msg;
+        int len = mp.getLen();
+        byte[] content = mp.getContent();
+
+        System.out.println("客户端接收到消息如下");
+        System.out.println("长度=" + len);
+        System.out.println("内容=" + new String(content, StandardCharsets.UTF_8));
+
+
+    }
+
+    /**
+     * 客户端异常时触发
+     */
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        cause.printStackTrace();
+        ctx.close();
+    }
+}
+

服务端代码

+
@SpringBootApplication
+public class NettyServerApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(NettyServerApplication.class, args);
+    }
+}
+
+@Slf4j
+@Component
+public class StartNetty implements CommandLineRunner {
+
+    private final NettyServer nettyServer;
+
+    public StartNetty(NettyServer nettyServer) {
+        this.nettyServer = nettyServer;
+    }
+
+    /**
+     * 启动netty netty随着项目一起启动
+     */
+    @Override
+    public void run(String... args) throws Exception {
+        log.info("netty 服务端启动 ...");
+        nettyServer.start(new InetSocketAddress("127.0.0.1", 9000));
+    }
+}
+
+@Slf4j
+@Component
+public class NettyServer  {
+
+    /**
+     * Netty服务启动
+     */
+    public void start(InetSocketAddress socketAddress) {
+        //new 一个主线程组
+        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
+        //new 一个工作线程组
+        EventLoopGroup workGroup = new NioEventLoopGroup(200);
+
+        ServerBootstrap bootstrap = new ServerBootstrap()
+                .group(bossGroup, workGroup)
+                .channel(NioServerSocketChannel.class)
+                .childHandler(new ServerChannelInitializer())
+                .localAddress(socketAddress)
+                //设置队列大小
+                .option(ChannelOption.SO_BACKLOG, 1024)
+                // 两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文
+                .childOption(ChannelOption.SO_KEEPALIVE, true);
+        //绑定端口,开始接收进来的连接
+        try {
+            // 绑定端口 生成一个ChannelFuture 对象 启动服务器
+            ChannelFuture future = bootstrap.bind(socketAddress).sync();
+            log.info("服务器启动开始监听端口: {}", socketAddress.getPort());
+            // 对关闭通道进行监听
+            future.channel().closeFuture().sync();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        } finally {
+            //关闭主线程组
+            bossGroup.shutdownGracefully();
+            //关闭工作线程组
+            workGroup.shutdownGracefully();
+        }
+    }
+}
+
+public class ServerChannelInitializer extends ChannelInitializer<SocketChannel> {
+
+    @Override
+    protected void initChannel(SocketChannel socketChannel) throws Exception {
+        socketChannel.pipeline().addLast("decoder", new MyMessageDecoder());
+        socketChannel.pipeline().addLast("encoder", new MyMessageEncoder());
+        socketChannel.pipeline().addLast(new NettyServerHandler());
+    }
+}
+
+@Slf4j
+public class NettyServerHandler extends ChannelInboundHandlerAdapter {
+    /**
+     * 客户端连接会触发
+     */
+    @Override
+    public void channelActive(ChannelHandlerContext ctx) throws Exception {
+        log.info("服务端 Active......");
+    }
+
+    /**
+     * 客户端发消息会触发
+     */
+    @Override
+    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
+        log.info("服务器收到消息: {}", msg.toString());
+        MessageProtocol mp = (MessageProtocol)msg;
+        int len = mp.getLen();
+        byte[] content = mp.getContent();
+
+        System.out.println();
+        System.out.println();
+        System.out.println();
+        System.out.println("服务器接收到信息如下");
+        System.out.println("长度=" + len);
+        System.out.println("内容=" + new String(content, Charset.forName("utf-8")));
+
+        //回复消息
+        String responseContent = UUID.randomUUID().toString();
+        int responseLen = responseContent.getBytes("utf-8").length;
+        byte[] responseContent2 = responseContent.getBytes("utf-8");
+        //构建一个协议包
+        MessageProtocol messageProtocol = new MessageProtocol();
+        messageProtocol.setLen(responseLen);
+        messageProtocol.setContent(responseContent2);
+
+        ctx.writeAndFlush(messageProtocol);
+    }
+
+    /**
+     * 给客户端发送消息
+     */
+    @Override
+    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
+        ctx.writeAndFlush("hello client");
+    }
+
+    /**
+     * 发生异常触发
+     */
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
+        cause.printStackTrace();
+        ctx.close();
+    }
+}
+

公共代码部分

+
public class MessageProtocol {
+
+    private int len;
+
+    private byte[] content;
+
+    public int getLen() {
+        return len;
+    }
+
+    public void setLen(int len) {
+        this.len = len;
+    }
+
+    public byte[] getContent() {
+        return content;
+    }
+
+    public void setContent(byte[] content) {
+        this.content = content;
+    }
+
+    @Override
+    public String toString() {
+        return "MessageProtocol{" +
+                "len=" + len +
+                ", content=" + new String(content) +
+                '}';
+    }
+}
+
+public class MyMessageDecoder extends ReplayingDecoder<Void> {
+
+    @Override
+    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
+        //需要将得到二进制字节码-> MessageProtocol 数据包(对象)
+        int length = in.readInt();
+        byte[] content = new byte[length];
+        in.readBytes(content);
+
+        //封装成 MessageProtocol 对象,放入 out 传递下一个handler业务处理
+        MessageProtocol messageProtocol = new MessageProtocol();
+        messageProtocol.setLen(length);
+        messageProtocol.setContent(content);
+        out.add(messageProtocol);
+    }
+}
+
+public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
+
+    @Override
+    protected void encode(ChannelHandlerContext ctx, MessageProtocol msg, ByteBuf out) throws Exception {
+        System.out.println("MyMessageEncoder encode 方法被调用");
+        out.writeInt(msg.getLen());
+        out.writeBytes(msg.getContent());
+    }
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/java/rookie-java-container/index.html b/blog-site/public/posts/java/rookie-java-container/index.html new file mode 100644 index 00000000..2f1bab91 --- /dev/null +++ b/blog-site/public/posts/java/rookie-java-container/index.html @@ -0,0 +1,2152 @@ + + + + + + + + + + + Java集合 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Java集合

+ 2021.10.04 +
+

概述

+

Java中的集合主要包括 CollectionMap 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。

+

Java中的集合-01

+

如果你看过ArrayList类源码,就知道ArrayList底层是通过数组来存储元素的,所以如果严格来说,数组也算集合的一种。

+

数组

+

Java中提供的数组是用来存储固定大小的同类型元素,所以Java数组就是同类数据元素的集合。

+

数组是引用数据类型,如果使用了没有开辟空间的数组,则一定会出现NullPointerException异常信息。所以数组本质上也是Java对象,能够向下或者向上转型,能使用instanceof关键字。

+
// 数组的父类也是Object,可以将a向上转型到Object  
+int[] a = new int[8];  
+Object obj = a ; 
+
+// 可以进行向下转型 
+int[] b = (int[])obj;  
+
+// 可以用instanceof关键字进行类型判定 
+if(obj instanceof int[]){ 
+}
+
+

void method_name(int ... value)方法中变参就是当数组处理的,参数为定参的编译后就是数组。一个方法只能有一个变参,即使是不同的类型也不行,变参参数只能在形参列表的末尾,如果传入的是数组,则只能传一个。

+
+

优缺点

+

数组优点:

+
    +
  • 数组元素的内存地址是连续分配的,所以通过下标访问元素的效率很高,可以快速找到指定下标为n的元素的地址;
  • +
+

数组缺点:

+
    +
  • 数组一旦初始化之后长度是固定的不能变的;
  • +
  • 数组进行元素的删除和插入操作的时候,效率比较低,需要移动大量的元素;
  • +
  • 数组元素的类型只能是一种;
  • +
  • 数组元素的内存地址是连续分配的,对内存要求高一些;相对于链表结构比较,链表的内存是连续不连续都可以;
  • +
+

数组的优点是效率高,但为此,所付出的代价就是数组对象的大小被固定。这也使得在工作中,数组并不实用。所以我们应该优选容器,而不是数组。只有在已证明性能成为问题的时候,并且确定切换到数组对性能提高有帮助时,才应该将项目重构为使用数组。

+

操作数组

+

由于数组没有提供任何的封装,所有对元素的操作,都是通过自定义的方法实现的,对数组元素的操作比较麻烦,好在Java自带了一些API供开发者调用。

+

定义数组

+
 int[] array1 = { 1,2,3,4,5 }; 
+ int[] array2 = new int[10];
+ int[] array3 = new int[]{ 1,2,3,4,5 };
+

需要注意的是[],写在数组名称的前后都可以,但是推荐第一种写法:

+
  int[] array1 = { 1,2,3,4,5 };
+  int array2[] = { 1,2,3,4,5 };
+

遍历数组

+
 for (int i = 0; i < array1.length; i++) {
+       System.out.println(array1[i]);
+ }
+

数组去重

+
// 最简单方法,利用 hashSet 集合去重
+Set<Integer> set2 = new HashSet<Integer>();
+for (int i = 0; i < arr11.length; i++) {
+    set2.add(arr11[i]);
+}
+

数组与集合转换

+
// 数组转成set集合
+Set<String> set = new HashSet<String>(Arrays.asList(array2));
+
+// 数组转list 
+List<String > list2 = Arrays.asList(array);
+

数组排序

+
 // 原生方法 或 8种排序算法
+ Arrays.sort(arr);
+

复制数组

+
// 待复制的数组
+int[] arr = {1, 2, 3, 4};
+
+// 指定新数组的长度
+int[] arr2 = Arrays.copyOf(arr, 10);
+
+// 只复制从索引[1]到索引[3]之间的元素(不包括索引[3]的元素)
+int[] arr3 = Arrays.copyOfRange(arr, 1, 3);
+

ArrayList

+

在List接口实现类中,最常用的就是ArrayList,ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,可以添加或删除元素。

+

ArrayList 继承了 AbstractList ,并实现了 List、RandomAccess, Cloneable 接口:

+
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess,Cloneable,Serializable
+

RandomAccess

+

Random是随机的意思,Access是访问的意思,合起来就是随机访问的意思。

+

RandomAccess接口是一个标记接口,用来标记实现的List集合具备快速随机访问的能力。所有的List实现都支持随机访问的,只是基于基本结构的不同,实现的速度不同罢了。

+

当一个List拥有快速访问功能时,其遍历方法采用随机访问速度最快,而没有快速随机访问的List采用顺序访问的速度最快。如果集合中的数据量过大需要遍历时,此时需要格外注意,因为不同的遍历方式会影响很大,可以使用instanceof关键字来判断该类有没有RandomAccess标记:

+
// 假设 list 数据量非常大,推荐写法
+List<Object> list = ...;
+
+if(list instanceof RandomAccess){
+    // 随机访问
+    for (int i = 0;i< list.size();i++) {
+        System.out.println(list.get(i));
+    }
+}else {
+    // 顺序访问
+    for(Object obj: list) {
+        System.out.println(obj);
+    }
+}
+

在List中ArrayList被RandomAccess接口标记,而LinkedList没有被RandomAccess接口标记,所以ArrayList适合随机访问,LinkedList适合顺序访问。

+

Cloneable

+

Cloneable接口是Java开发中常用的一个接口之一,它是一个标记接口。

+

如果一个想要拷贝一个对象,就需要重写Object中的clone方法并让其实现Cloneable接口,如果只重写clone方法,不实现Cloneable接口就会报CloneNotSupportedException异常。

+

JDK中clone方法源码:

+
protected native Object clone() throws CloneNotSupportedException;
+

应当注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException

+

换言之,clone方法规定了想要拷贝对象,就需要实现Cloneable方法,clone方法让Cloneable接口变得有意义。

+

浅拷贝与深拷贝

+
    +
  • 浅拷贝:被复制对象的所有值属性都含有与原来对象的相同,而所有的对象引用属性仍然指向原来的对象。
  • +
  • 深拷贝:在浅拷贝的基础上,所有引用其他对象的变量也进行了clone,并指向被复制过的新对象。
  • +
+

如果一个被复制的属性都是基本类型,那么只需要实现当前类的cloneable机制就可以了,此为浅拷贝。

+

如果被复制对象的属性包含其他实体类对象引用,那么这些实体类对象都需要实现cloneable接口并覆盖clone()方法。

+

浅拷贝:

+
public class ShallowCloneExample implements Cloneable {
+
+    private int[] arr;
+
+    public ShallowCloneExample() {
+        arr = new int[10];
+        for (int i = 0; i < arr.length; i++) {
+            arr[i] = i;
+        }
+    }
+
+    public void set(int index, int value) {
+        arr[index] = value;
+    }
+
+    public int get(int index) {
+        return arr[index];
+    }
+
+    @Override
+    protected ShallowCloneExample clone() throws CloneNotSupportedException {
+        return (ShallowCloneExample) super.clone();
+    }
+}
+
ShallowCloneExample e1 = new ShallowCloneExample();
+ShallowCloneExample e2 = null;
+try {
+    e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+    e.printStackTrace();
+}
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 222
+

深拷贝:

+
public class DeepCloneExample implements Cloneable {
+
+    private int[] arr;
+
+    public DeepCloneExample() {
+        arr = new int[10];
+        for (int i = 0; i < arr.length; i++) {
+            arr[i] = i;
+        }
+    }
+
+    public void set(int index, int value) {
+        arr[index] = value;
+    }
+
+    public int get(int index) {
+        return arr[index];
+    }
+
+    @Override
+    protected DeepCloneExample clone() throws CloneNotSupportedException {
+        DeepCloneExample result = (DeepCloneExample) super.clone();
+        result.arr = new int[arr.length];
+        for (int i = 0; i < arr.length; i++) {
+            result.arr[i] = arr[i];
+        }
+        return result;
+    }
+}
+
DeepCloneExample e1 = new DeepCloneExample();
+DeepCloneExample e2 = null;
+try {
+    e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+    e.printStackTrace();
+}
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 2
+

ArrayList中clone方法

+

clone方法调用栈:

+
clone 
+    -> Object.clone
+    -> Arrays.copyOf(T[] original, int newLength)
+    -> Arrays.copyOf(U[] original, int newLength, Class<? extends T[]> newType)
+

文档注释大意:返回这个ArrayList实例的浅拷贝(元素本身不会被复制)。

+
public class ArrayList implements Cloneable {
+
+    transient Object[] elementData;     
+
+    /**
+     * Returns a shallow copy of this <tt>ArrayList</tt> instance.  (The
+     * elements themselves are not copied.)
+     *
+     * @return a clone of this <tt>ArrayList</tt> instance
+     */
+    public Object clone() {
+        try {
+            // 调用Object类的clone方法
+            ArrayList<?> v = (ArrayList<?>) super.clone();
+            
+            // 将集合中的元素进行拷贝
+            v.elementData = Arrays.copyOf(elementData, size);
+            v.modCount = 0;
+            return v;
+        } catch (CloneNotSupportedException e) {
+            // this shouldn't happen, since we are Cloneable
+            throw new InternalError(e);
+        }
+    }
+}
+
public class Arrays{
+   public static <T> T[] copyOf(T[] original, int newLength) {
+        return (T[]) copyOf(original, newLength, original.getClass());
+    }
+    
+    public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
+        @SuppressWarnings("unchecked")
+        T[] copy = ((Object)newType == (Object)Object[].class)
+            ? (T[]) new Object[newLength]
+            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
+        System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength));
+        return copy;
+    }
+}
+

ArrayList中clone方法底层是调用父类的clone方法,父类没有重写clone方法所以调用的是Object类的clone方法。

+

在ArrayList中核心方法最终调用Arrays.copyOf方法,不论怎样都会创建一个Object数组。

+
+

Arrays.newInstance(Class<?> componentType,int length)方法作用,创建具有指定组件类型和长度的新数组。

+
+

最终使用System.arraycopy方法将之前的旧数组中的元素拷贝到新创建的数组中,然后赋值给ArrayList.elementData对象并返回。

+

ArrayList扩容

+

因为ArrayList底层使用数组保存数据的,而数组一旦被创建就不能改变大小,但是ArrayList的长度是可以改变的,所以可以通过ArrayList类中的add方法找到数组扩容方法。

+

add方法调用栈:

+
add 
+    -> ensureCapacityInternal()
+    -> calculateCapacity()
+    -> ensureExplicitCapacity()
+    -> grow()
+
    private void grow(int minCapacity) {
+        // overflow-conscious code
+        int oldCapacity = elementData.length;
+        int newCapacity = oldCapacity + (oldCapacity >> 1);
+        if (newCapacity - minCapacity < 0)
+            newCapacity = minCapacity;
+        if (newCapacity - MAX_ARRAY_SIZE > 0)
+            newCapacity = hugeCapacity(minCapacity);
+        // minCapacity is usually close to size, so this is a win:
+        elementData = Arrays.copyOf(elementData, newCapacity);
+    }
+

ArrayList容量:如果没有指定容量创建数组,默认会创建一个长度为10的数组用来保存元素,之后通过:

+
 int newCapacity = oldCapacity + (oldCapacity >> 1);
+

每次扩容都是原容量的1.5倍。

+
+

>>,右移几位就是相当于除以2的几次幂 +<<,左移几位就是相当于乘以2的几次幂

+
+

最后通过Arrays.copyOf方法将之前的数组中元素,全部移到新创建的数组上。

+

由于频繁的扩容数组会对性能产生影响,如果在ArrayList中要存储很大的数据,就需要在ArrayList的有参构造中指定数组的长度:

+
List<String> list = new ArrayList(1000000);
+

需要注意的是创建指定长度的ArrayList,在没有add之前ArrayList中的数组已经初始化了,但是List的大小没变,因为List的大小是由size决定的。

+

ArrayList与LinkedList

+

ArrayList与LinkedList性能比较是一道经典的面试题,ArrayList查找快,增删慢;而LinkedList增删快,查找慢。

+

造成这种原因是因为底层的数据结构不一样,ArrayList底层是数组,而数组的中的元素内存分配都是连续的,并且数组中的元素只能存放一种,这就造成了数组中的元素地址是有规律的,数组中查找元素快速的原因正是利用了这一特点。

+
+

查询方式为: 首地址+(元素长度*下标) +例如:new int arr[5]; arr数组的地址假设为0x1000,arr[0] ~ arr[5] 地址可看作为 0x1000 + i * 4。

+
+

而LinkedList在Java中的底层结构是对象,每一个对象结点中都保存了下一个结点的位置形成的链表结构,由于LinkedList元素的地址是不连续的,所以没办法按照数组那样去查找,所以就比较慢。

+

由于数组一旦分配了大小就不能改变,所以ArrayList在进行添加操作时会创建新的数组,如果要添加到ArrayList中的指定的位置,是通过System.arraycopy方法将数组进行复制,新的数组会将待插入的指定位置空余出来,最后在将元素添加到集合中。

+

在进行删除操作时是通过System.arraycopy方法,将待删除元素后面剩余元素复制到待删除元素的位置。当ArrayList里有大量数据时,这时候去频繁插入或删除元素会触发底层数组频繁拷贝,效率不高,还会造成内存空间的浪费。

+

LinkedList在进行添加,删除操作时,会用二分查找法找到将要添加或删除的元素,之后再设置对象的下一个结点来进行添加或删除操作。

+
+

二分查找法:也称为折半查找法,是一种适用于大量数据查找的方法,但是要求数据必须的排好序的,每次以中间的值进行比较,根据比较的结果可以直接舍去一半的值,直至全部找完(可能会找不到)或者找到数据为止。

+

此处LinkedList会比较查找的元素是距离头结点比较近,还是尾结点比较近,距离哪边较近则从哪边开始查找。

+
+

ArrayList,获取元素效率非常的高,时间复杂度是O(1),而查找,插入和删除元素效率似乎不太高,时间复杂度为O(n)。

+

LinkedList,正与ArrayList相反,获取第几个元素依次遍历复杂度O(n),添加到末尾复杂度O(1),添加到指定位置复杂度O(n),删除元素,直接指针指向操作复杂度O(1)。

+

注意,ArrayList的增删不一定比LinkedList效率低,但是ArrayList查找效率一定比LinkedList高,如果在List靠近末尾的地方插入,那么ArrayList只需要移动较少的数据,而LinkedList则需要一直查找到列表尾部,反而耗费较多时间,这时ArrayList就比LinkedList要快。

+

使用场景:

+
    +
  • 如果应用程序对数据有较多的随机访问,ArrayList要优于LinkedList;
  • +
  • 如果应用程序有更多的插入或者删除操作,较少的随机访问,LinkedList要优于ArrayList;
  • +
+

线程安全问题

+

众所周知,ArrayList是线程不安全的:

+
public class MainTest {
+    // 如果没有报错,需要多试几次
+    public static void main(String[] args) {
+        ArrayList<String> arrayList = new ArrayList<>();
+        for(int i=0; i< 10; i++) {
+            new Thread(() -> {
+                arrayList.add(UUID.randomUUID().toString());
+                System.out.println(arrayList);
+            },String.valueOf(i)).start();
+        }
+    }
+}
+

为避免偶然事件,请重复多试几次上面的代码,很大情况会出现ConcurrentModificationException“同步修改异常”:

+
java.util.ConcurrentModificationException
+

出现该异常的原因是,当某个线程正在执行 add()方法时,被某个线程打断,添加到一半被打断,没有被添加完。

+

保证ArrayList线程安全有以下几种方法:

+
    +
  • 可以使用 Vector 来代替 ArrayList,Vector 是线程安全的 ArrayList,但是由于底层是加了synchronized,性能略差不推荐使用; +
    List list = new Vector();
    +list.add(UUID.randomUUID().toString());
    +
  • +
  • 使用Collections.synchronizedArrayList() 来创建 ArrayList;使用 Collections 工具类来创建 ArrayList 的思路是,在 ArrayList 的外边套了一个synchronized外壳,来使 ArrayList 线程安全; +
    List list = Collections.synchronizedArrayList();
    +list.add(UUID.randomUUID().toString());
    +
  • +
  • 使用CopyOnWriteArrayList()来保证 ArrayList 线程安全;CopyWriteArrayList字面意思就是在写的时候复制,主要思想就是读写分离的思想。CopyWriteArrayList之所以线程安全的原因是在源码里面使用ReentrantLock,所以保证了某个线程在写的时候不会被打断; +
    CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
    +list.add(UUID.randomUUID().toString());
    +
  • +
+

Set

+
    +
  • TreeSet:基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSet,HashSet 查找的时间复杂度为 O(1),TreeSet 则为 O(logN);
  • +
  • HashSet:基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。HashSet的value作为hashmap的key,来保证不重复;
  • +
  • LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序;
  • +
+

Queue

+

队列是一种经常使用的集合。Queue实际上是实现了一个先进先出(FIFO:First In First Out)的有序列表。它和List的区别在于,List可以在任意位置添加和删除元素,而Queue只有两个操作:

+
    +
  • 把元素添加到队列末尾;
  • +
  • 从队列头部取出元素;
  • +
+

常见实现:

+
    +
  • LinkedList:可以用它来实现双向队列;
  • +
  • PriorityQueue:基于堆结构实现,可以用它来实现优先队列;
  • +
+

Queue实现通常不允许插入null元素,尽管一些实现,如LinkedList,不禁止插入null元素。即使在允许它的实现中,null也不应插入Queue中,因为poll方法也使用null作为特殊返回值,用来表示队列不包含任何元素。

+
+

poll(): 检索并删除此队列的头部,如果此队列为空,则返回null +peek(): 检索但不删除此队列的头部,如果此队列为空,则返回null

+
+

HashMap

+
    +
  • HashMap 是一个散列表,它存储的内容是键值对(key-value)映射;
  • +
  • HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步;
  • +
  • HashMap 是无序的,即不会记录插入的顺序;
  • +
+

相关操作:

+
    +
  • 存贮: 通过key的hashcode方法找到在hashMap 存贮的位置,如果该位置有元素,则通过equals方法进行比较,equals返回值为true,则覆盖value,equals返回值为false则,在该数组元素的头部追加该元素,形成一个链表结构;
  • +
  • 读取:通过key的hashcode方法获取元素存在该数组的位置,然后通过equals拿到该值;
  • +
  • 结构: hashMap是一个散列数据结构,HashMap底层就是一个数组结构,数组中的每一项又是一个链表;
  • +
+

总的来说hashMap底层将key-value(键值对)当成一个整体来处理,hashMap底层采用一个 Entry 数组保存所有的键值对,当存储一个entry对象时,会根据key的hash算法来决定存放在数组中的位置,在根据equals方法来确定在链表中的位置,读取一个entry对象,先根据hash算法确定在数组中的位置,再根据equals来获取该值,equals和equals在hashMap中就像一个坐标一样,来确定hashMap中的值。

+

相关概念

+
    +
  • capacity: 容量,默认16;
  • +
  • loadFactor: 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容;
  • +
  • threshold: 阈值;阈值 = 容量 * 负载因子。默认12;
  • +
  • hash碰撞:即hash冲突,两个不同的输入值,根据同一散列函数计算出的散列值相同的现象叫做碰撞。hash碰撞就是用同一hash散列函数计算出相同的散列值;当插入hashmap中元素的key出现重复时,这个时候就发生了hash碰撞;
  • +
+

结构

+

HashMap结构

+
    +
  • JDK1.7:数组 + 单向链表;
  • +
  • JDK1.8: 数组 + 单向链表/红黑树;
  • +
+

在JDK1.8时,如果存储Map中数组元素对应的索引的每个链表超过8,就将单向链表转化为红黑树;当红黑树的节点少于6个的时候又开始使用链表。

+

为什么要使用红黑树

+

当有发生大量的hash冲突时,因为链表遍历效率很慢,为了提升查询的效率,所以使用了红黑树的数据结构。

+

为什么不一开始就用红黑树代替链表结构

+

JDK文档注释:

+
+

Because TreeNodes are about twice the size of regular nodes, we use them only when bins contain enough nodes to warrant use (see TREEIFY_THRESHOLD). +And when they become too small (due to removal or resizing) they are converted back to plain bins.

+
+

单个 TreeNode 需要占用的空间大约是普通 Node 的两倍,所以只有当包含足够多的 Nodes 时才会转成 TreeNodes,而是否足够多就是由 TREEIFY_THRESHOLD 的值(默认值8)决定的。而当桶中节点数由于移除或者 resize 变少后,又会变回普通的链表的形式,以便节省空间,这个阈值是 UNTREEIFY_THRESHOLD(默认值6)。

+

为什么树化阈值为8

+

JDK1.8HashMap文档注释:

+
+

如果 hashCode 分布良好,也就是 hash 计算的结果离散好的话,那么红黑树这种形式是很少会被用到的,因为各个值都均匀分布,很少出现链表很长的情况。 +在理想情况下,链表长度符合泊松分布,各个长度的命中概率依次递减,当长度为 8 的时候,概率仅为 0.00000006。这是一个小于千万分之一的概率,通常我们的 Map 里面是不会存储这么多的数据的,所以通常情况下,并不会发生从链表向红黑树的转换。

+
+

HashMap是通过hash算法,来判断对象应该放在哪个桶里面的;JDK 并不能阻止我们用户实现自己的哈希算法,如果我们故意把哈希算法变得不均匀,那么每次存放对象很容易造成hash冲突。

+

链表长度超过 8 就转为红黑树的设计,更多的是为了防止用户自己实现了不好的哈希算法时导致链表过长,从而导致查询效率低,而此时转为红黑树更多的是一种保底策略,用来保证极端情况下查询的效率。红黑树的引入保证了在大量hash冲突的情况下,HashMap还具有良好的查询性能。

+

为什么树化阈值和链表阈值不设置成一样

+

为了防止出现节点个数频繁在一个相同的数值来回切换。

+

举个极端例子,现在单链表的节点个数是9,开始变成红黑树,然后红黑树节点个数又变成8,就又得变成单链表,然后节点个数又变成9,就又得变成红黑树,这样的情况消耗严重浪费。因此干脆错开两个阈值的大小,使得变成红黑树后“不那么容易”就需要变回单链表,同样,使得变成单链表后,“不那么容易”就需要变回红黑树。

+

引入红黑树后,如果单链表节点个数超过8个是否一定会树化

+

不一定,在进行树化之前会进行判断(n = tab.length) < MIN_TREEIFY_CAPACITY)是否需要扩容,如果表中数组元素小于这个阈值(默认是64),就会进行扩容。 因为扩容不仅能增加表中的容量,还能缩短单链表的节点数,从而更长远的解决链表遍历慢问题。

+
    /**
+     * Replaces all linked nodes in bin at index for given hash unless
+     * table is too small, in which case resizes instead.
+     */
+    final void treeifyBin(Node<K,V>[] tab, int hash) {
+        int n, index; Node<K,V> e;
+        if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
+            resize();
+        else if ((e = tab[index = (n - 1) & hash]) != null) {
+            TreeNode<K,V> hd = null, tl = null;
+            do {
+                TreeNode<K,V> p = replacementTreeNode(e, null);
+                if (tl == null)
+                    hd = p;
+                else {
+                    p.prev = tl;
+                    tl.next = p;
+                }
+                tl = p;
+            } while ((e = e.next) != null);
+            if ((tab[index] = hd) != null)
+                hd.treeify(tab);
+        }
+    }
+

容量

+

为什么负载因子默认是0.75

+

HashMap中的负载因子这个值现在在JDK的源码中默认是0.75:

+
/**
+ * The load factor used when none specified in constructor.
+ */
+static final float DEFAULT_LOAD_FACTOR = 0.75f;
+

JDK的官方文档中解释如下:

+
+

As a general rule, the default load factor (.75) offers a good tradeoff between time and space costs. +Higher values decrease the space overhead but increase the lookup cost (reflected in most of the operations of the HashMap class, including get and put). +The expected number of entries in the map and its load factor should be taken into account when setting its initial capacity, so as to minimize the number of rehash operations. +If the initial capacity is greater than the maximum number of entries divided by the load factor, no rehash operations will ever occur.

+
+

大意:一般来说,默认的负载因子(0.75)在时间和空间成本之间提供了很好的权衡。更高的值减少了空间开销,但增加了查找成本(反映在HashMap类的大多数操作中,包括get和put)。在设置映射的初始容量时,应该考虑映射中预期的条目数及其负载因子,以便最小化重哈希操作的数量。如果初始容量大于最大条目数除以负载因子,则不会发生重新散列操作。

+

负载因子和hashmap中的扩容有关,当hashmap中的元素大于临界值(threshold = loadFactor * capacity)就会扩容。所以负载因子的大小决定了什么时机扩容,扩容又影响到了hash碰撞的频率。所以设置一个合理的负载因子可以有效的避免hash碰撞。

+

设置为0.75的其他解释:

+
    +
  • 根据数学公式推算。这个值在log(2)的时候比较合理;
  • +
  • 为了提升扩容效率,HashMap的容量有一个固定的要求,那就是一定是2的幂。如果负载因子是3/4的话,那么和容量的乘积结果就可以是一个整数;
  • +
+

如果指定容量大小为10,那么实际大小是多少

+

实际大小是16。其容量为不小于指定容量的2的幂数。

+

为什么容量始终是2的N次方?

+

为了减少Hash碰撞,尽量使Hash算法的结果均匀分布。

+

当使用put方法时,到底存入HashMap中的那个数组中?这时是通过hash算法决定的,如果某一个数组中的链表过长旧会影响查询的效率;那么为了避免出现hash碰撞,让hash尽可能的散列分布,就需要在hash算法上做文章。

+

JDK1.7通过逻辑与运算,来判断这个元素该进入哪个数组;在下面的代码中length的长度始终为不小于指定容量的2的幂数。

+
static int indexFor(int h, int length) {
+    return h & (length - 1);
+}
+

为了更好的理解举个例子:假设h=2或h=3,length=15,进行与运算,最终逻辑与运算后的结果是一致的,因为最终结果是一致的所以就发生了hash碰撞,这种问题多了以后会造成容器中的元素分布不均匀,都分配在同一个数组上,在查询的时候就减慢了查询的效率,另一方面也造成空间的浪费。

+
-- 2转换为2进制与15-1进行&运算
+  0000 0010
+& 0000 1110 
+———————————— 
+  0000 1110
+
+-- 3转换为2进制与15-1进行&运算
+  0000 0011
+& 0000 1110 
+————————————
+  0000 1110
+

为了避免上面length=15这类问题出现,所以集合的容量采用必须是2的N次幂这种方式,因为2的N次幂的结果减一转换为二进制后都是以...1111结尾的,所以在进行逻辑与运算时碰撞几率小。

+

在JDK1.8中,在putVal()方法中通过i = (n - 1) & hash来计算key的散列地址:

+
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
+                   boolean evict) {
+        // 此处省略了代码
+        // i = (n - 1) & hash]
+        if ((p = tab[i = (n - 1) & hash]) == null)
+            
+            tab[i] = newNode(hash, key, value, null);
+        
+ 
+        else {
+            // 省略了代码
+        }
+}
+
+

这里的 “&” 等同于 %",但是"%“运算的速度并没有”&“的操作速度快;”&“操作能代替”%“运算,必须满足一定的条件,也就是a%b=a&(b-1)仅当b是2的n次方的时候方能成立。

+
+

容器容量怎么保持始终为2的N次方?

+

HashMaptableSizeFor()方法做了处理,能保证n永远都是2次幂。

+

如果用户制定了初始容量,那么HashMap会计算出比该数大的第一个2的幂作为初始容量;另外就是在扩容的时候,也是进行成倍的扩容,即4变成8,8变成16。

+
/**
+ * Returns a power of two size for the given target capacity.
+ */
+static final int tableSizeFor(int cap) {
+
+    // 假设n=17
+    // n = 00010001 - 00010000 = 00010000 = 16
+    int n = cap - 1;
+
+    // n = (00010000 | 00001000) = 00011000 = 24
+    n |= n >>> 1;
+
+    // n = (00011000 | 00000110) = 00011110 = 30
+    n |= n >>> 2;
+
+    // n = (00011110 | 00000001) = 00011111 = 31
+    n |= n >>> 4;
+
+    // n = (00011111 | 00000000) = 00011111 = 31
+    n |= n >>> 8;
+
+    // n = (00011111 | 00000000) = 00011111 = 31
+    n |= n >>> 16;
+
+    // n = 00011111 = 31,MAXIMUM_CAPACITY:Integer的最大长度
+    // (31 < 0) ? 1 : (31 >= Integer的最大长度) ? Integer的最大长度 : 31 + 1 ;
+    // 即最终返回 32 = 2 的 (n=5)次方
+    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
+}
+

发现上面在进行>>>操作时会将cap的二进制值变为最高位后边全是1,00010001 -> 00011111这个算法就导致了任意传入一个数值,会将该数字变为它的2倍减1,因为任何尾数全为1的在加1都为2的倍数。

+

至于开头减1,是因为如果给定的n已经是2的次幂,但是不进行减1操作的话,那么得到的值就是大于给定值的最小2的次幂值,例如传入4就会返回8。

+

为什么最大右移到16位,因为可以得到的最大值是32个1,这个是int类型存储变量的最大值,在往后就没意义了。

+

默认初始化容量为什么是16

+

没有找到相关解释,推断这应该就是个经验值,既然一定要设置一个默认的2^n 作为初始值,那么就需要在效率和内存使用上做一个权衡。这个值既不能太小,也不能太大。太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。所以,16就作为一个经验值被采用了。

+

关于默认容量的定义:

+
/**
+ * The default initial capacity - MUST be a power of two.
+ */
+static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
+

故意把16写成1 << 4这种形式,就是提醒开发者,这个地方要是2的次幂。

+

初始化容量设置多少合适

+

当我们使用HashMap(int initialCapacity)来初始化容量的时候,HashMap并不会使用我们传进来的initialCapacity直接作为初始容量。JDK会默认帮我们计算一个相对合理的值当做初始容量。所谓合理值,其实是找到第一个比用户传入的值大的2的幂。

+

如果创建hashMap初始化容量设置为7,那么JDK通过计算会创建一个初始化为8的hashMap。当hashMap中的元素到0.75 * 8 = 6就会进行扩容,这明显是我们不希望看到的。

+

参考JDK8中putAll方法中的实现:

+
(int) ((float) expectedSize / 0.75F + 1.0F);
+

通过expectedSize / 0.75F + 1.0F计算,7/0.75 + 1 = 10 ,10经过JDK处理之后,会被设置成16,这就大大的减少了扩容的几率。

+

当我们明确知道HashMap中元素的个数的时候,把默认容量设置成expectedSize / 0.75F + 1.0F 是一个在性能上相对好的选择,但是,同时也会牺牲些内存。

+

这个算法在guava中有实现,开发的时候,可以直接通过Maps类创建一个HashMap:

+
Map<String, String> map = Maps.newHashMapWithExpectedSize(7);
+
public static <K, V> HashMap<K, V> newHashMapWithExpectedSize(int expectedSize) {
+    return new HashMap(capacity(expectedSize));
+}
+
+static int capacity(int expectedSize) {
+    if (expectedSize < 3) {
+        CollectPreconditions.checkNonnegative(expectedSize, "expectedSize");
+        return expectedSize + 1;
+    } else {
+        return expectedSize < 1073741824 ? (int)((float)expectedSize / 0.75F + 1.0F) : 2147483647;
+    }
+}
+

扩容

+
    +
  • JDK1.7: 先扩容在添加元素;
  • +
  • JDK1.8: 先添加元素在扩容;
  • +
+

为什么要进行扩容

+

随着HashMap中的元素增加,Hash碰撞导致获取元素方法的效率就会越来越低,为了保证获取元素方法的效率,所以针对HashMap中的数组进行扩容。扩容数组的方式只能再去开辟一个新的数组,并把之前的元素转移到新数组上。

+
+

PS 如何能避免哈希碰撞?

+
    +
  • 容量太小。容量小,碰撞的概率就高了。狼多肉少,就会发生争抢。
  • +
  • hash算法不够好。算法不合理,就可能都分到同一个或几个桶中。分配不均,也会发生争抢。
  • +
+
+

什么时机进行扩容

+

HashMap的扩容条件就是当HashMap中的元素个数(size)超过临界值(threshold)时就会自动扩容。在HashMap中,threshold = loadFactor * capacity。默认情况下负载因子为0.75,理解为当容器中元素到容器的3/4时就会扩容。

+
 if (++size > threshold)
+    resize();
+

HashMap的容量是有上限的,必须小于1<<30,即1073741824。如果容量超出了这个数,则不再增长,且阈值会被设置为Integer.MAX_VALUE

+
// Java8
+if (oldCap >= MAXIMUM_CAPACITY) {
+    threshold = Integer.MAX_VALUE;
+    return oldTab;
+}
+// Java7
+if (oldCapacity == MAXIMUM_CAPACITY) { 
+    threshold = Integer.MAX_VALUE;
+    return;
+}
+

1.7扩容

+
    +
  • 新容量 = 旧容量 * 2
  • +
  • 新阈值 = 新容量 * 负载因子
  • +
+
void addEntry(int hash, K key, V value, int bucketIndex) {  
+    //sizeThe number of key-value mappings contained in this map.  
+    //thresholdThe next size value at which to resize (capacity * load factor)  
+    //数组扩容条件:1.已经存在的key-value mappings的个数大于等于阈值  
+    //             2.底层数组的bucketIndex坐标处不等于null  
+    if ((size >= threshold) && (null != table[bucketIndex])) {  
+        resize(2 * table.length);//扩容之后,数组长度变了  
+        hash = (null != key) ? hash(key) : 0;//为什么要再次计算一下hash值呢  
+        bucketIndex = indexFor(hash, table.length);//扩容之后,数组长度变了,在数组的下标跟数组长度有关,得重算。  
+    }  
+    createEntry(hash, key, value, bucketIndex);  
+} 
+
void resize(int newCapacity) {   //传入新的容量
+    Entry[] oldTable = table;    //引用扩容前的Entry数组
+    int oldCapacity = oldTable.length;
+    if (oldCapacity == MAXIMUM_CAPACITY) {  //扩容前的数组大小如果已经达到最大(2^30)
+        threshold = Integer.MAX_VALUE; //修改阈值为int的最大值(2^31-1),这样以后就不会扩容了
+        return;
+    }
+
+    Entry[] newTable = new Entry[newCapacity];  //初始化一个新的Entry数组
+    transfer(newTable);                         //!!将数据转移到新的Entry数组里
+    table = newTable;                           //HashMap的table属性引用新的Entry数组
+    threshold = (int) (newCapacity * loadFactor);//修改阈值
+}
+

通过transfer方法将旧数组上的元素转移到扩容后的新数组上

+
void transfer(Entry[] newTable) {
+    Entry[] src = table;                   //src引用了旧的Entry数组
+    int newCapacity = newTable.length;
+    for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
+        Entry<K, V> e = src[j];             //取得旧Entry数组的每个元素
+        if (e != null) {
+            src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
+            do {
+                Entry<K, V> next = e.next;
+                int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
+                e.next = newTable[i]; //标记[1]
+                newTable[i] = e;      //将元素放在数组上
+                e = next;             //访问下一个Entry链上的元素
+            } while (e != null);
+        }
+    }
+}
+

1.8扩容

+

容量变为原来的2倍,阈值也变为原来的2倍。容量和阈值都变为原来的2倍时,负载因子还是不变。

+

在1.8时做了一些优化,文档注释写的很清楚:“元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置”。也就是对比1.7的迁移到新的数组上省去了重新计算hash值的时间。

+

这里的"2次幂的位置"是指长度为原来数组元素的两倍的位置;举个例子,现在容量为16,要扩容到32,要将之前的元素迁移过去,要根据hash值来判断迁移过去的位置;假设元素A:hash值:0101 0101;根据代码h & (length - 1)可得元素A & 15元素A & 31

+
扩容之前的位置:
+  0101 0101
+& 0000 1111
+————————————
+  0000 0101
+
+扩容之后的位置:
+  0101 0101
+& 0001 1111
+————————————
+  0001 0101
+

发现规律:扩容前的hash值和扩容后的hash值,如果元素A二进制形式第三位如果是0,扩容之后就还是原来的位置,如果是1扩容后就是原来的位置加16,而16就是扩容的大小。

+
 /**
+     * Initializes or doubles table size.  If null, allocates in
+     * accord with initial capacity target held in field threshold.
+     * Otherwise, because we are using power-of-two expansion, the
+     * elements from each bin must either stay at same index, or move
+     * with a power of two offset in the new table.
+     *
+     * @return the table
+     */
+    final Node<K,V>[] resize() {
+        Node<K,V>[] oldTab = table;
+        int oldCap = (oldTab == null) ? 0 : oldTab.length;
+        int oldThr = threshold;
+        int newCap, newThr = 0;
+        if (oldCap > 0) {
+            if (oldCap >= MAXIMUM_CAPACITY) {
+                threshold = Integer.MAX_VALUE;
+                return oldTab;
+            }
+            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
+                     oldCap >= DEFAULT_INITIAL_CAPACITY)
+                newThr = oldThr << 1; // double threshold
+        }
+        else if (oldThr > 0) // initial capacity was placed in threshold
+            newCap = oldThr;
+        else {               // zero initial threshold signifies using defaults
+            newCap = DEFAULT_INITIAL_CAPACITY;
+            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
+        }
+        if (newThr == 0) {
+            float ft = (float)newCap * loadFactor;
+            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
+                      (int)ft : Integer.MAX_VALUE);
+        }
+        threshold = newThr;
+        @SuppressWarnings({"rawtypes","unchecked"})
+        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
+        table = newTab;
+        if (oldTab != null) {
+            for (int j = 0; j < oldCap; ++j) {
+                Node<K,V> e;
+                if ((e = oldTab[j]) != null) {
+                    oldTab[j] = null;
+                    if (e.next == null)
+                        newTab[e.hash & (newCap - 1)] = e;
+                    else if (e instanceof TreeNode)
+                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
+                    else { // preserve order
+                        Node<K,V> loHead = null, loTail = null;
+                        Node<K,V> hiHead = null, hiTail = null;
+                        Node<K,V> next;
+                        do {
+                            next = e.next;
+                            if ((e.hash & oldCap) == 0) {
+                                if (loTail == null)
+                                    loHead = e;
+                                else
+                                    loTail.next = e;
+                                loTail = e;
+                            }
+                            else {
+                                if (hiTail == null)
+                                    hiHead = e;
+                                else
+                                    hiTail.next = e;
+                                hiTail = e;
+                            }
+                        } while ((e = next) != null);
+                        if (loTail != null) {
+                            loTail.next = null;
+                            newTab[j] = loHead;
+                        }
+                        if (hiTail != null) {
+                            hiTail.next = null;
+                            newTab[j + oldCap] = hiHead;
+                        }
+                    }
+                }
+            }
+        }
+        return newTab;
+    }
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/java/rookie-multi-thread/index.html b/blog-site/public/posts/java/rookie-multi-thread/index.html new file mode 100644 index 00000000..a36b392c --- /dev/null +++ b/blog-site/public/posts/java/rookie-multi-thread/index.html @@ -0,0 +1,5963 @@ + + + + + + + + + + + Java多线程 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Java多线程

+ 2021.05.05 +
+

相关概念

+

线程与进程

+
+

进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。例如,一个正在运行的程序的实例就是一个进程。

+
+
+

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 +一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

+
+

进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位。 +一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

+

Java 程序是多线程程序,每启动一个Java程序至少我们知道的都会包含一个主线程和一个垃圾回收线程。 +而且启动的时候,每条线程可以并行执行不同的任务。

+

线程与进程的关系

+

并行、并发、串行

+
    +
  • 并行:前提是在多核CPU或多个CPU条件下,多个线程同时被多个CPU执行,同时执行的线程并不会抢占CPU资源。
  • +
  • 串行:前提是在单核CPU条件下,单线程程序执行,不能同时执行,也不能去切换执行。也就是在同一时间段只能做一件事。
  • +
  • 并发:前提是多线程条件下,多个线程抢占一个CPU资源,多个线程被交替执行。因为CPU运算速度很快,所以用户感觉不到线程切换的卡顿。
  • +
+ + + + + + + + + + + + + + + + + + + + +
单线程多线程
单CPU串行并发
多CPU串行并行
+

无论并行、并发,都可以有多个线程执行,如果是多个线程抢占一个CPU就成了并发,多个CPU同时执行多个线程就是并行。

+

对于单CPU的计算机来说,同一时间是只能干一件事儿的,如果是单线程就是串行;如果是多个线程就是并发。 +而对于多CPU的计算机说,同一时间能干多个事,如果多个CPU同时执行多个线程就是并行;如果一个CPU同时执行多个线程就是并行。

+

并行与并发区别图解 +并行与并发区别

+

并发是两个队列交替使用一台咖啡机,并行是两个队列同时使用两台咖啡机; +如果串行,一个队列使用一台咖啡机,那么哪怕前面那个人便秘了去厕所呆半天,后面的人也只能死等着他回来才能去接咖啡,这效率无疑是最低的。

+

同步与异步

+

同步、异步一般是相对与方法来说的。

+
    +
  • 同步方法调用一旦开始,调用者必须等到方法调用返回后,才能继续后续的行为;
  • +
  • 异步方法调用更像一个消息传递,一旦开始,方法调用就会立即返回,调用者就可以继续后续的操作; +异步方法通常会在另外一个线程中执行着。整个过程,不会阻碍调用者的后续工作。
  • +
+

只有多线程环境下才会异步调用方法,换言之异步调用方法则需要单独创建一个线程。

+ + + + + + + + + + + + + + + + + + + + +
单线程多线程
同步只能同步可以同步
异步不能异步可以异步
+
+

PS 线程中的同步(synchronized)机制 +在多线程环境下,一旦一个方法或一段代码被synchronized修饰,也就意味着被同步了; +在synchronized修饰的作用域中,某一段时间内只允许一个线程进行操作数据,如果有多个线程需要操作则要排队等待。

+
+

守护线程

+
+

守护线程是指为其他线程服务的线程。在JVM中,所有非守护线程都执行完毕后,无论有没有守护线程,虚拟机都会自动退出。

+
+

守护线程也称“服务线程”,在没有用户线程可服务时会自动离开。因为主要是服务其他线程所以在程序中的优先级比较低。

+

举例:垃圾回收线程就是一个经典的守护线程,当我们的程序中不再有任何运行的线程,程序就不会再产生垃圾,垃圾回收器也就无事可做; +所以当垃圾回收线程是JVM上仅剩的线程时,垃圾回收线程会自动离开。它始终在低级别的状态中运行,用于实时监控和管理系统中的可回收资源。

+

守护线程在程序中的操作演示

+
public class MainTest {
+    public static void main(String[] args) {
+        int i = 0;
+        while (true) {
+            Thread daemon = new Thread(() -> {
+                System.out.println("启动线程--->" + Thread.currentThread().getName());
+            });
+            daemon.setDaemon(i==3);
+            daemon.start();
+            boolean isDaemon = daemon.isDaemon();
+            System.out.println("当前线程是否是守护线程:" + isDaemon);
+            if (isDaemon) {
+                break;
+            }
+            i++;
+        }
+
+    }
+}
+

线程的状态

+

线程状态共包含6种,6中状态又可以互相的转换。 +线程状态转换

+
    +
  • 新建状态(New): 创建了线程后尚未启动;
  • +
  • 可运行状态(Runnable): 可能正在运行,也可能正在等待 CPU 时间片。包含了运行中(Running)和 就绪(Ready)状态; +
      +
    • 就绪(Ready):线程对象创建后,其他线程(比如main线程)调用了该对象的 start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中并分配cpu使用权 。
    • +
    • 运行中(Runing):就绪的线程获得了cpu 时间片,开始执行程序代码。
    • +
    +
  • +
  • 阻塞状态(Blocked): 等待获取一个排它锁,如果其线程释放了锁就会结束此状态;
  • +
  • 无限期等待(Wating): 等待其它线程显式地唤醒,否则不会被分配 CPU 时间片;
  • +
  • 限期等待(Timed Wating): 无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒;
  • +
  • 死亡(Terminated): 可以是线程结束任务之后自己结束,或者产生了异常而结束。
  • +
+

线程中的常用方法

+
+

睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。

+
    +
  • 调用 Thread.sleep() 方法使线程进入限期等待状态时,常常用“使一个线程睡眠”进行描述。
  • +
  • 调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。
  • +
  • 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁,而等待是主动的,通过调用 Thread.sleep()Object.wait() 等方法进入等待。
  • +
+
+

sleepwait方法区别:

+
    +
  1. sleep()属于Thread类,wait()属于Object类;
  2. +
  3. sleep()wait()都会抛出InterruptedException异常,这个异常属于checkedException不可避免;
  4. +
  5. 两者比较的共同之处是,都是使程序等待多长时间。不同的是调用sleep()不会释放锁,会使线程堵塞,而调用wait()会释放锁,让线程进入等待状态,用 notify()、notifyall()可以唤醒,或者等待时间到了; +这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
  6. +
  7. wait()必须在同步synchronized块里使用,sleep()可以在任意地方使用;
  8. +
+

其他方法:

+
    +
  • join()使当前线程停下来等待,直至另一个调用join的线程终止,值得注意的是:线程的在被激活后不一定马上就运行.而是进入到可运行线程的队列中;
  • +
  • yield()是停止当前线程,让同等优先权的线程运行。如果没有同等优先权的线程,那么yield()将不会起作用;
  • +
  • notify()、notifyall()需要搭配wait方法使用,前提条件必须要在synchronized代码块里面,因为需要依赖monitor对象;
  • +
+

创建线程

+

在Java中,创建一个线程,有且仅有一种方式: 创建一个Thread类实例,并调用它的start方法。

+

Thread类

+

通过继承Thread类,重写run()方法来创建线程。

+
public class MainTest {
+    public static void main(String[] args) {
+        ThreadDemo thread1 = new ThreadDemo();
+        thread1.start();
+    }
+}
+class ThreadDemo extends Thread {
+    @Override
+    public void run() {
+        System.out.printf("通过继承Thread类的方式创建线程,线程 %s 启动",Thread.currentThread().getName());
+    }
+}
+

Runnable接口

+

实现 Runnale 接口,将它作为 target 参数传递给 Thread 类构造函数的方式创建线程。

+
public class MainTest {
+    public static void main(String[] args) {
+        new Thread(() -> {
+            System.out.printf("通过实现Runnable接口的方式,重写run方法创建线程;线程 %s 启动",Thread.currentThread().getName());
+        }).start();
+    }
+}
+

Callable接口

+

通过实现 Callable接口,来创建一个带有返回值的线程。

+

在Callable 执行完之前的这段时间,主线程可以先去做一些其他的事情,事情都做完之后,再获取 Callable 的返回结果。可以通过isDone()来判断子线程是否执行完。

+
public class MainTest {
+    public static void main(String[] args) throws ExecutionException, InterruptedException {
+        FutureTask<String> futureTask = new FutureTask<>(() -> {
+            System.out.printf("通过实现Callable接口的方式,重写call方法创建线程;线程 %s 启动", Thread.currentThread().getName());
+            System.out.println();
+            Thread.sleep(10000);
+            return "我是call方法返回值";
+        });
+        new Thread(futureTask).start();
+
+        System.out.println("主线程工作中 ...");
+        String callRet = null;
+        while (callRet == null){
+            if(futureTask.isDone()){
+                callRet = futureTask.get();
+            }
+            System.out.println("主线程继续工作 ...");
+        }
+        System.out.println("获取call方法返回值:"+ callRet);
+    }
+}
+

线程池

+

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。

+

它的主要特点为:线程复用,控制最大并发数,管理线程。

+

优点:

+
    +
  • 降低资源消耗。通过重复利用己创建的线程降低线程创建和销毁造成的消耗。
  • +
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • +
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
  • +
+

常用方式

+

通过Executors线程池工具类来使用:

+
    +
  • Executors.newSingleThreadExecutor():创建只有一个线程的线程池
  • +
  • Executors.newFixedThreadPool(int):创建固定线程的线程池
  • +
  • Executors.newCachedThreadPool():创建一个可缓存的线程池,线程数量随着处理业务数量变化
  • +
+

这三种常用创建线程池的方式,底层代码都是用ThreadPoolExecutor创建的。

+
SingleThreadExecutor
+
    +
  • 使用Executors.newSingleThreadExecutor()创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序执行。
  • +
  • newSingleThreadExecutorcorePoolSizemaximumPoolSize 都设置为1,它使用的 LinkedBlockingQueue
  • +
+

源代码

+
    public static ExecutorService newSingleThreadExecutor() {
+        return new FinalizableDelegatedExecutorService
+            (new ThreadPoolExecutor(1, 1,
+                                    0L, TimeUnit.MILLISECONDS,
+                                    new LinkedBlockingQueue<Runnable>()));
+    }
+

代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        ExecutorService executor1 = null;
+        try {
+            executor1 = Executors.newSingleThreadExecutor();
+            for (int i = 1; i <= 10; i++) {
+                executor1.execute(() -> {
+                    System.out.println(Thread.currentThread().getName() + "执行了");
+                });
+            }
+        } finally {
+            executor1.shutdown();
+        }
+    }
+}
+
FixedThreadPool
+
    +
  • 使用Executors.newFixedThreadPool(int)创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
  • +
  • newFixedThreadPool 创建的线程池 corePoolSizemaximumPoolSize 值是相等的,它使用的 LinkedBlockingQueue
  • +
+

源代码

+
    public static ExecutorService newFixedThreadPool(int nThreads) {
+        return new ThreadPoolExecutor(nThreads, nThreads,
+                                      0L, TimeUnit.MILLISECONDS,
+                                      new LinkedBlockingQueue<Runnable>());
+    }
+

代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        ExecutorService executor1 = null;
+        try {
+            executor1 = Executors.newFixedThreadPool(10);
+            for (int i = 1; i <= 10; i++) {
+                executor1.execute(() -> {
+                    System.out.println(Thread.currentThread().getName() + "执行了");
+                });
+            }
+        } finally {
+            executor1.shutdown();
+        }
+    }
+}
+
CachedThreadPool
+
    +
  • 使用Executors.newCachedThreadPool()创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
  • +
  • newCachedThreadPoolcorePoolSize 设置为0,将 maximumPoolSize 设置为 Integer.MAX_VALUE,使用的 SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。
  • +
+

源代码

+
    public static ExecutorService newCachedThreadPool() {
+        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
+                                      60L, TimeUnit.SECONDS,
+                                      new SynchronousQueue<Runnable>());
+    }
+

代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        ExecutorService executor1 = null;
+        try {
+            executor1 = Executors.newCachedThreadPool();
+            for (int i = 1; i <= 10; i++) {
+                executor1.execute(() -> {
+                    System.out.println(Thread.currentThread().getName() + "执行了");
+                });
+            }
+        } finally {
+            executor1.shutdown();
+        }
+    }
+}
+

阻塞队列

+

阻塞队列,顾名思义,首先它是一个队列:

+

阻塞队列

+

一个阻塞队列在数据结构中所起的作用:

+
    +
  • 当阻塞队列是空时,从队列中获取元素的操作将会被阻塞。
  • +
  • 当阻塞队列是满时,往队列里添加元素的操作将会被阻塞。
  • +
+

blockQueue作为线程容器、阻塞队列,多用于生产者、消费者的关系模式中,保障并发编程线程同步,线程池中被用于当作存储任务的队列,还可以保证线程执行的有序性.fifo先进先出

+

在多线程领域:所谓阻塞,在某些情况下会挂起线程(即线程阻塞),一旦条件满足,被挂起的线程优惠被自动唤醒

+
为什么使用阻塞队列
+

我们不需要关心什么时候需要阻塞线程,什么时候需要唤醒线程,因为BlockingQueue都一手给你包办好了 +在concurrent包 发布以前,在多线程环境下,我们每个程序员都必须自己去控制这些细节,尤其还要兼顾效率和线程安全,而这会给我们的程序带来不小的复杂度.

+
阻塞队列种类
+
    +
  • ArrayBlockingQueue: 由数组结构组成的有界阻塞队列.
  • +
  • LinkedBlockingDeque: 由链表结构组成的有界(但大小默认值Integer>MAX_VALUE大约21亿)阻塞队列.
  • +
  • PriorityBlockingQueue:支持优先级排序的无界阻塞队列.
  • +
  • DelayQueue: 使用优先级队列实现的延迟无界阻塞队列.
  • +
  • SynchronousQueue:不存储元素的阻塞队列,也即是单个元素的队列.
  • +
  • LinkedTransferQueue:由链表结构组成的无界阻塞队列.
  • +
  • LinkedBlockingDeque:由了解结构组成的双向阻塞队列.
  • +
+
ArrayListBlockingQueue
+
    +
  • add() :相对列里边添加元素,返回值了类型boolean,当超出队列大小时会抛出异常java.lang.IllegalStateException: Queue full
  • +
  • remove:清除元素,默认清除队列最上边的元素,可指定元素进行清除,如果清除一个不存在的元素会报异常java.util.NoSuchElementException
  • +
  • element:查看队首元素,检查队列为不为空
  • +
+
public static void arrayBlockDemo() {
+        // 与ArrayList类似,但需要设置队列大小
+        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
+        System.out.println(queue.add("c"));
+        System.out.println(queue.add("b"));
+        System.out.println(queue.add("a"));
+        // 当add第四个元素到队列时会抛异常
+        queue.add("f");
+        //查看队首元素,检查队列为不为空
+        System.out.println(queue.element());
+        System.out.println(queue.remove());
+        System.out.println(queue.remove());
+        System.out.println(queue.remove());
+        // 如果多清除一个不存在的元素会报异常
+        System.out.println(queue.remove());
+    }
+
    +
  • offer:与add()类似,但如果添加失败,不会报异常.会返回false
  • +
  • poll:与remove类似,如果没有元素可取,不会报异常,会返回null
  • +
  • peek:与element类似
  • +
+
public static void arrayBlockDemo2(){
+        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
+        System.out.println(queue.offer("1"));
+        System.out.println(queue.offer("2"));
+        System.out.println(queue.offer("3"));
+        // 不会抛异常
+        System.out.println(queue.offer("4"));
+        System.out.println(queue.peek());
+        System.out.println(queue.poll());
+        System.out.println(queue.poll());
+        System.out.println(queue.poll());
+        System.out.println(queue.poll());
+    }
+
    +
  • put:当阻塞队列满时,生产者继续往队列里面put元素,队列会一直阻塞直到put数据or响应中断退出
  • +
  • take:获取并移除此队列头元素,若没有元素则一直阻塞.当阻塞队列空时,消费者试图从队列take元素,队列会一直阻塞消费者线程直到队列可用.当阻塞队列满时,队列会阻塞生产者线程一定时间,超过后限时后生产者线程就会退出
  • +
+
SynchronousQueue
+

SynchronousQueue,实际上它不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移出队列.

+

SynchronousQueue支持支持生产者和消费者等待的公平性策略。默认情况下,不能保证生产消费的顺序。 +如果是公平锁的话可以保证当前第一个队首的线程是等待时间最长的线程,这时可以视SynchronousQueue为一个FIFO队列

+
public class SynchronousQueueDemo {
+
+    public static void main(String[] args) {
+        SynchronousQueue<Integer> synchronousQueue = new SynchronousQueue<>();
+        new Thread(() -> {
+            try {
+                synchronousQueue.put(1);
+                Thread.sleep(3000);
+                synchronousQueue.put(2);
+                Thread.sleep(3000);
+                synchronousQueue.put(3);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }).start();
+
+        new Thread(() -> {
+            try {
+                Integer val = synchronousQueue.take();
+                System.out.println(val);
+                Integer val2 = synchronousQueue.take();
+                System.out.println(val2);
+                Integer val3 = synchronousQueue.take();
+                System.out.println(val3);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }).start();
+    }
+}
+

使用场景:

+
    +
  • 生产者消费者模式
  • +
  • 线程池
  • +
  • 消息中间件
  • +
+

线程池参数

+
    public ThreadPoolExecutor(int corePoolSize,
+                              int maximumPoolSize,
+                              long keepAliveTime,
+                              TimeUnit unit,
+                              BlockingQueue<Runnable> workQueue,
+                              ThreadFactory threadFactory,
+                              RejectedExecutionHandler handler) {
+                 // ...
+    }
+
    +
  • corePoolSize: 线程池中的常驻核心线程数,可理解为初始化线程数
  • +
  • maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
  • +
  • threadFactory:线程工厂;表示生成线程池中工作线程的线程工厂,用于创建线程,一般用默认的即可
  • +
  • workQueue:任务队列;随着业务量的增多,线程开始慢慢处理不过来,这时候需要放到任务队列中去等待线程处理
  • +
  • rejectedExecutionHandler:拒绝策略;如果业务越来越多,线程池首先会扩容,扩容后发现还是处理不过来,任务队列已经满了,这时候拒绝接收新的请求
  • +
  • keepAliveTime:多余的空闲线程的存活时间;如果线程池扩容后,能处理过来,而且数据量并没有那么大,用最初的线程数量就能处理过来,剩下的线程被叫做空闲线程
  • +
  • unit:多余的空闲线程的存活时间的单位
  • +
+

线程池工作原理

+

线程池工作原理

+

在创建了线程池后,等待提交过来的任务请求; +当调用execute方法添加一个请求任务时,线程池会做如下判断:

+
    +
  1. 如果当前运行的线程数小于corePoolSize,那么马上创建线程运行该任务
  2. +
  3. 如果当前运行的线程数大于等于corePoolSize,那么该任务会被放入任务队列
  4. +
  5. 如果这时候任务队列满了且正在运行的线程数还小于maximumPoolSize,那么要创建非核心线程立刻运行这个任务(扩容)
  6. +
  7. 如果任务队列满了且正在运行的线程数等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行
  8. +
  9. 随着时间的推移,业务量越来越少,线程池中出现了空闲线程,当一个线程无事可做超过一定的时间时,线程池会进行判断: +如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉,所以线程池的所有任务完成后它最终会收缩到 corePoolSize 的大小
  10. +
+

四种拒绝策略

+

在线程池中,如果任务队列满了并且正在运行的线程个数大于等于允许运行的最大线程数,那么线程池会启动拒绝策略来执行,具体分为下列四种:

+
    +
  • AbortPolicy: 默认拒绝策略;直接抛出java.util.concurrent.RejectedExecutionException异常,阻止系统的正常运行;
  • +
  • CallerRunsPolicy:调用这运行,一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量;
  • +
  • DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中;
  • +
  • DiscardPolicy:直接丢弃任务,不给予任何处理也不会抛出异常;如果允许任务丢失,这是一种最好的解决方案;
  • +
+

自定义线程池

+

在实际开发中用哪个线程池?

+

上面的三种一个都不用,我们生产上只能使用自定义的。

+

Executors 中JDK已经给你提供了,为什么不用?

+

以下内容摘自《阿里巴巴开发手册》

+
+

【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 +说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 +【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

+
+
+

说明:Executors 返回的线程池对象的弊端如下: +1) FixedThreadPoolSingleThreadPool: 允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。 +2) CachedThreadPool: 允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

+
+

自定义线程池代码演示:

+
public class MainTest {
+    public static void main(String[] args) {
+        ExecutorService executor1 = null;
+        try {
+            executor1 = new ThreadPoolExecutor(
+                    2,
+                    5,
+                    1L,
+                    TimeUnit.SECONDS,
+                    new LinkedBlockingQueue<>(3),
+                    Executors.defaultThreadFactory(),
+                    new ThreadPoolExecutor.CallerRunsPolicy());
+            for (int i = 1; i <= 20; i++) {
+                executor1.execute(() -> {
+                    System.out.println(Thread.currentThread().getName() + "执行了");
+                });
+            }
+        } finally {
+            executor1.shutdown();
+        }
+    }
+}
+

SpringBoot异步配置,自定义线程池代码演示:

+
@EnableAsync
+@Configuration
+public class AsyncConfig {
+
+    /**
+     * 线程空闲存活的时间 单位: TimeUnit.SECONDS
+     */
+    public static final int KEEP_ALIVE_TIME = 60 * 60;
+    /**
+     * CPU 核心数量
+     */
+    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
+    /**
+     * 核心线程数量
+     */
+    public static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
+    /**
+     * 线程池最大容纳线程数量
+     * IO密集型:即存在大量堵塞; 公式: CPU核心数量 / 1- 阻塞系数 (阻塞系统在 0.8~0.9 之间)
+     * CPU密集型: 需要大量运算,没有堵塞或很少有; 公式:CPU核心数量 + 1
+     */
+    public static final int IO_MAXIMUM_POOL_SIZE = (int) (CPU_COUNT / (1 - 0.9));
+    public static final int CPU_MAXIMUM_POOL_SIZE = CPU_COUNT + 2;
+
+    /**
+     * 执行写入请求时的线程池
+     *
+     * @return 线程池
+     */
+    @Bean(name = "iSaveTaskThreadPool")
+    public Executor iSaveTaskThreadPool() {
+        return getThreadPoolTaskExecutor("iSaveTaskThreadPool-",IO_MAXIMUM_POOL_SIZE,100000,new ThreadPoolExecutor.CallerRunsPolicy());
+    }
+
+    /**
+     * 执行读请求时的线程池
+     *
+     * @return 线程池
+     */
+    @Bean(name = "iQueryThreadPool")
+    public Executor iQueryThreadPool() {
+        return getThreadPoolTaskExecutor("iQueryThreadPool-",CPU_MAXIMUM_POOL_SIZE,10000,new ThreadPoolExecutor.CallerRunsPolicy());
+    }
+
+    /**
+     * 创建一个线程池对象
+     * @param threadNamePrefix 线程名称
+     * @param queueCapacity 堵塞队列长度
+     * @param refusePolicy 拒绝策略
+     */
+    private ThreadPoolTaskExecutor getThreadPoolTaskExecutor(String threadNamePrefix,int maxPoolSize,int queueCapacity,ThreadPoolExecutor.CallerRunsPolicy refusePolicy) {
+        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
+        taskExecutor.setCorePoolSize(CORE_POOL_SIZE);
+        taskExecutor.setMaxPoolSize(maxPoolSize);
+        taskExecutor.setKeepAliveSeconds(KEEP_ALIVE_TIME);
+        taskExecutor.setThreadNamePrefix(threadNamePrefix);
+        // 拒绝策略; 既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量
+        taskExecutor.setRejectedExecutionHandler(refusePolicy);
+        // 阻塞队列 长度
+        taskExecutor.setQueueCapacity(queueCapacity);
+        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
+        taskExecutor.setAwaitTerminationSeconds(60);
+        taskExecutor.initialize();
+        return taskExecutor;
+    }
+
+}
+

合理配置线程池参数

+

合理配置线程池参数,可以分为以下两种情况

+
    +
  • +

    CPU密集型:CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行; +CPU密集型任务配置尽可能少的线程数量:参考公式:(CPU核数+1)

    +
  • +
  • +

    IO密集型:即该任务需要大量的IO,即大量的阻塞; +在IO密集型任务中使用多线程可以大大的加速程序运行,故需要多配置线程数:参考公式:CPU核数/ (1-阻塞系数) 阻塞系数在0.8~0.9之间

    +
  • +
+

代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        ExecutorService executor1 = null;
+        try {
+            // 获取cpu核心数
+            int coreNum = Runtime.getRuntime().availableProcessors();
+            /*
+             * 1. IO密集型: CPU核数/ (1-阻塞系数) 阻塞系数在0.8~0.9之间
+             * 2. CPU密集型: CPU核数+1
+             */
+//            int maximumPoolSize = coreNum + 1;
+            int maximumPoolSize = (int) (coreNum / (1 - 0.9));
+            System.out.println("当前线程池最大允许存放:" + maximumPoolSize + "个线程");
+            executor1 = new ThreadPoolExecutor(
+                    2,
+                    maximumPoolSize,
+                    1L,
+                    TimeUnit.SECONDS,
+                    new LinkedBlockingQueue<>(3),
+                    Executors.defaultThreadFactory(),
+                    new ThreadPoolExecutor.CallerRunsPolicy());
+            for (int i = 1; i <= 20; i++) {
+                executor1.execute(() -> {
+                    System.out.println(Thread.currentThread().getName() + "执行了");
+                });
+            }
+        } finally {
+            executor1.shutdown();
+        }
+    }
+}
+

+

参考文章:

+ +

在Java中根据锁的特性来区分可以分为很多,在程序中"锁"的作用无非就是保证线程安全,线程安全的目的就是保证程序正常执行。

+

在Java中具体"锁"的实现,无非就三种:使用synchronized关键字、调用juc.locks包下相关接口、使用CAS思想。

+

公平锁与非公平锁

+

公平锁:多个线程按照申请锁的顺序来获取锁,线程直接进入队列中排队,队列中的第一个线程才能获得锁。

+
    +
  • 优点是等待锁的线程不会饿死;
  • +
  • 缺点是整体吞吐效率相对非公平锁要低,等待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非公平锁大;
  • +
+

非公平锁:多个线程加锁时直接尝试获取锁,获取不到才会到等待队列的队尾等待。但如果此时锁刚好可用,那么这个线程可以无需阻塞直接获取到锁,所以非公平锁有可能出现后申请锁的线程先获取锁的场景。

+
    +
  • 优点是可以减少唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞直接获得锁,CPU不必唤醒所有线程。
  • +
  • 缺点是处于等待队列中的线程可能会饿死,或者等很久才会获得锁。
  • +
+

相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。 +当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。

+

在Java中公平锁和非公平锁的实现为ReentrantLocksynchronized。 +其中synchronized是非公平锁;ReentrantLock默认是非公平锁,但是可以指定ReentrantLock的构造函数创建公平锁。

+
/**
+ * Creates an instance of {@code ReentrantLock}.
+ * This is equivalent to using {@code ReentrantLock(false)}.
+ */
+public ReentrantLock() {
+    sync = new NonfairSync();
+}
+
+/**
+ * Creates an instance of {@code ReentrantLock} with the
+ * given fairness policy.
+ *
+ * @param fair {@code true} if this lock should use a fair ordering policy
+ */
+public ReentrantLock(boolean fair) {
+    sync = fair ? new FairSync() : new NonfairSync();
+}
+

可重入锁与不可重入锁

+

可重入锁:在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class)不会因为之前已经获取过还没释放而阻塞。 +所以可重入锁又叫做递归锁,就是因为能获取到加锁方法里面的加锁方法的锁。

+
    +
  • 可重入锁最大的作用就是避免死锁
  • +
+

不可重入锁:所谓不可重入锁,就是与可冲入锁作用相悖;即当前线程执行某个方法已经获取了该锁,那么在该方法中尝试再次获取加锁的方法时,就会获取不到被阻塞。

+
+

举个栗子: 当你进入你家时门外会有锁,进入房间后厨房卫生间都可以随便进出,这个叫可重入锁; +当你进入房间时,发现厨房,卫生间都有上锁.这个叫不可重入锁。

+
+

在Java中ReentrantLocksynchronized都是可重入锁。

+

共享锁与独占锁

+
    +
  • 独占锁: 又称排它锁,指该锁一次只能被一个线程独占;
  • +
  • 共享锁:指该锁可被多个线程所持有;
  • +
+

在Java中,对于 ReentrantLocksynchronized 都是独占锁;对与 ReentrantReadWriteLock 其读锁是共享锁而写锁是独占锁。 +读锁的共享可保证并发读是非常高效的。

+

悲观锁与乐观锁

+

乐观锁与悲观锁是一种广义上的概念,可以理解为一种标准类似于Java中的接口。

+

对于多线程并发操作,加了悲观锁的线程认为每一次修改数据时都会有其他线程来跟它一起修改数据,所以在修改数据之前先会加锁,确保其他线程不会修改该数据。 +由于悲观锁在修改数据前先加锁的特性,能保证写操作时数据正确,所以悲观锁更适合写多读少的场景。

+

乐观锁则与悲观锁相反,每一次修改数据时,都认为没有其他线程来跟它一起修改,所以在修改数据之前不会去添加锁,如果这个数据没有被更新,当前线程将自己修改的数据成功写入。如果数据已经被其他线程更新,则根据不同的实现方式执行不同的操作。 +由于乐观锁是一种无锁操作,所以在使用乐观锁的场景中读的性能会大幅度提升,适合读多写少。

+

悲观锁与乐观锁

+

在Java中悲观锁的实现有:synchronizedLock实现类,乐观锁的实现有CAS

+

自旋锁、适应性自旋锁

+

自旋锁与非自旋锁

+

自旋锁

+

当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态,直到获取到某个锁。

+

自旋锁本身是有缺点的,它不能代替阻塞。如果锁被占用的时间很长,那么自旋的线程只会白白浪费处理器资源,带来性能上的浪费。

+

自旋锁的实现原理是CAS,例如AtomicIntegergetAndUpdate()方法

+
    public final int getAndUpdate(IntUnaryOperator updateFunction) {
+        int prev, next;
+        do {
+            prev = get();
+            next = updateFunction.applyAsInt(prev);
+        } while (!compareAndSet(prev, next));
+        return prev;
+    }
+

源码中的do-while循环就是一个自旋操作,如果修改数值失败则通过循环来执行自旋,直至修改成功。

+

为什么要使用自旋锁?

+

在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,线程挂起和恢复现场的花费可能会让系统得不偿失。 +简单来说就是,避免切换线程带来的开销。

+

自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。如果锁被占用的时间很短,自旋等待的效果就会非常好。 +反之,如果锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。 +所以,自旋等待的时间必须要有一定的限度,如果自旋超过了限定次数(默认是10次,可以使用-XX:PreBlockSpin来更改)没有成功获得锁,就应当挂起线程。

+

自旋锁在JDK 1.4中引入,默认关闭,但是可以使用-XX:+UseSpinning开开启;在JDK1.6中默认开启,同时自旋的默认次数为10次,可以通过参数-XX:PreBlockSpin来调整。

+

如果通过参数-XX:PreBlockSpin来调整自旋锁的自旋次数,会带来诸多不便。 +假如将参数调整为10,但是系统很多线程都是等你刚刚退出的时候就释放了锁(假如多自旋一两次就可以获取锁)。于是JDK1.6引入适应性自旋锁

+

适应性自旋锁

+

适应性自旋锁是对自旋的升级、优化,自旋的时间不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态决定。

+

如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。 +如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

+

偏向锁

+

引入偏向锁的目的是在没有多线程竞争的前提下,进一步减少线程同步的性能消耗。

+
+

《深入理解Java虚拟机》对偏向锁的解释: +Hotspot 的作者经过以往的研究发现大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入 +了偏向锁。当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID,以后该线程在进入和退出同步块时不需要花费 +CAS操作来加锁和解锁,而只需简单的测试一下对象头的 MarkWord 里是否存储着指向当前线程的偏向锁,如果测试成功,表示线程已经获得了锁, +如果测试失败,则需要再测试下 MarkWord 中偏向锁的标识是否设置成 1(表示当前是偏向锁),如果没有设置,则使用 CAS 竞争锁,如果设置了, +则尝试使用 CAS 将对象头的偏向锁指向当前线程。

+
+

之所以叫偏向锁是因为偏向于第一个获取到他的线程,如果在程序执行中该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。 +但是如果线程间存在锁竞争,会带来额外的锁撤销(CAS)的消耗。

+

个人理解,偏向锁就是对锁的标志位做了一个缓存,在没有多线程竞争的前提下,这样做会大幅度提升程序性能。

+

轻量级锁与重量级锁

+

重量级锁:传统的重量级锁,使用的是系统互斥量实现的;重量级锁会导致线程堵塞; +轻量级锁:相对于重量级锁而言的;他的出现并不是代替重量级锁,而是在没有多线程竞争的前提下,减少系统互斥量操作产生的性能消耗;是重量级锁的优化。

+

在Java中轻量级锁的经典实现是CAS中的自旋锁,所以优点缺点就很明显了。

+
    +
  • 优点:竞争的线程不会阻塞,提高了程序的响应速度;
  • +
  • 缺点:如果始终得不到锁竞争的线程,使用自旋会消耗CPU;
  • +
+

所以适合,追求响应时间,同步块执行速度非常快的场景。

+

重量级锁优缺点:

+
    +
  • 优点:线程竞争不使用自旋,不会消耗CPU;
  • +
  • 缺点:线程阻塞,响应时间慢;
  • +
+

适合追求吞吐量、同步块执行时间较长也就是线程竞争激烈的场景。

+

轻量级锁不是在任何情况下都比重量级锁快的,要看同步块执行期间有没有多个线程抢占资源的情况。 +如果有,那么轻量级线程要承担 CAS + 互斥锁的性能消耗,就会比重量锁执行的更慢。

+

可中断锁

+

顾名思义,就是可以中断的锁。

+

如果某一线程A正在执行锁中的代码,另一线程B正在等待获取该锁,可能由于等待时间过长,线程B不想等待了,想先处理其他事情,我们可以让它中断自己或者在别的线程中中断它,这种就是可中断锁。

+

在Java中synchronized就是不可中断锁,Lock是可中断锁。

+

互斥锁

+
+

在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。 +每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。

+
+

互斥锁:在访问共享资源之前对对象进行加锁操作,在访问完成之后进行解锁操作。 +加锁后,任何其他试图再次加锁的线程会被阻塞,直到当前线程解锁其他线程才能访问公共资源。

+

如果解锁时有一个以上的线程阻塞,那么所有该锁上的线程都变为就绪状态,第一个变为就绪状态的线程又执行加锁操作,其他的线程又会进入等待。 +在这种方式下,只有一个线程能够访问被互斥锁保护的资源。

+

在Java里最基本的互斥手段就是使用synchronized关键字、ReentrantLock

+
+

中提供一把互斥锁mutex也称之为互斥量。每个线程在对资源操作前都尝试先加锁,成功加锁才能操作,操作结束解锁。 +要注意,同一时刻,只能有一个线程持有该锁。 +所以,互斥锁实质上是操作系统提供的一把“建议锁”(又称“协同锁”),建议程序中有多线程访问共享资源的时候使用该机制。但,并没有强制限定。 +因此,即使有了mutex,如果有线程不按规则来访问数据,依然会造成数据混乱。

+
+

死锁

+

死锁并不是Java程序中的"锁",而是程序中出现的一种问题。之所以放到这个标题下,是为了方便类比,就类似谐音梗吧。

+
+

死锁通常被定义为:如果一个进程集合中的每个进程都在等待只能由此集合中的其他进程才能引发的事件,而无限期陷入僵持的局面称为死锁。

+
+

举个例子,当线程A持有锁a并尝试获取锁b,线程B持有锁b并尝试获取锁a时,就会出现死锁。简单来说,死锁问题的产生是由两个或者以上线程并行执行的时候,争夺资源而互相等待造成的。

+
 public class MainTest {
+ 
+     public static void main(String[] args) {
+          String lockA = "lockA";
+          String lockB = "lockB";
+         new Thread(new ThreadHolderLock(lockA,lockB),"线程AAA").start();
+         new Thread(new ThreadHolderLock(lockB,lockA),"线程BBB").start();
+     }
+ }
+ 
+ class ThreadHolderLock implements Runnable{
+ 
+     private String lockA;
+     private String lockB;
+ 
+     public ThreadHolderLock(String lockA, String lockB){
+         this.lockA = lockA;
+         this.lockB = lockB;
+     }
+ 
+     @Override
+     public void run() {
+         synchronized (lockA){
+             System.out.println(Thread.currentThread().getName() + "\t 持有锁 "+ lockA+", 尝试获得"+ lockB);
+ 
+             try {
+                 Thread.sleep(1000);
+             } catch (InterruptedException e) {
+                 e.printStackTrace();
+             }
+ 
+             synchronized (lockB){
+                 System.out.println(Thread.currentThread().getName() + "\t 持有锁 "+ lockB+", 尝试获得"+ lockA);
+             }
+         }
+     }
+ }
+

如何避免死锁

+

参考文章:

+ +

想要如何避免死锁,就要弄清楚死锁出现的原因,造成死锁必须达成的4个条件:

+
    +
  • 互斥条件:一个资源每次只能被一个线程使用。例如,如果线程 A 已经持有的资源,不能再同时被线程 B 持有,如果线程 B 请求获取线程 A 已经占用的资源,那线程 B 只能等待,直到线程 A 释放了资源。
  • +
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。例如,当线程 A 已经持有了资源 1,又想申请资源 2,而资源 2 已经被线程 C 持有了,所以线程 A 就会处于等待状态,但是线程 A 在等待资源 2 的同时并不会释放自己已经持有的资源 1。
  • +
  • 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。例如,当线程A已经持有了资源 ,在自己使用完之前不能被其他线程获取,线程 B 如果也想使用此资源,则只能在线程 A 使用完并释放后才能获取。
  • +
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。比如,线程 A 已经持有资源 2,而想请求资源 1, 线程 B 已经获取了资源 1,而想请求资源 2,这就形成资源请求等待的环。
  • +
+

避免死锁的产生就只需要破环其中一个条件就可以,最常见的并且可行的就是使用资源有序分配法,来破循环等待条件。

+
+

资源有序分配法: 线程 A 和 线程 B 获取资源的顺序要一样,当线程 A 先尝试获取资源 A,然后尝试获取资源 B 的时候,线程 B 同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A 和 线程 B 总是以相同的顺序申请自己想要的资源。

+
+

也可以通过银行家算法来动态避免死锁问题。 +银行家算法

+
+

银行家算法:一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。 +
+在银行中,客户申请贷款的数量是有限的,每个客户在第一次申请贷款时要声明完成该项目所需的最大资金量,在满足所有贷款要求时,客户应及时归还。银行家在客户申请的贷款数量不超过自己拥有的最大值时,都应尽量满足客户的需要。通过判断借贷是否安全,然后决定借不借。 +
+举例,现有公司B、公司A、公司T,想要从银行分别贷款70亿、40亿、50亿,假设银行只有100亿供放贷,如果借不到企业最大需求的钱,钱将不会归还,怎么才能合理的放贷?

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
公司最大需求已借走最多还借
B702050
A401030
T503020
+

此时公司B、A、T已经从银行借走60亿,银行还剩40亿。此时银行可放贷金额组合:

+
    +
  • 借给公司B10亿、公司A10亿、公司T20亿,等待公司T还钱再将10亿借给公司A,等待公司A还钱,再将钱借给公司B;
  • +
  • 借给公司T20亿,等公司T还钱再将钱借给公司A,等待公司A还钱再将钱借给公司B;
  • +
  • 借给公司A10亿,等待公司A还钱再将钱借给公司T,公司T还钱再将钱借给公司B;
  • +
+
+

线程安全

+

线程不安全,在多线程并发的环境中,多个线程共同操作同一个数据,如果最后数据的值和期待值不一样,这时候就出现了线程不安全问题。

+

线程安全,就是在多线程并发中,多个线程共同操作同一个数据,在Java中最常用的就是加锁; +当一个线程修改某个数据的时候,其他线程不能进行访问直到该线程操作该数据结束释放锁,其他线程才可以继续操作该数据。

+

一个对象是否安全取决于它是否被多个线程访问,要使对象线程安全,那么需要采用同步的机制来协同对对象可变状态的访问。

+

三大特性

+

在保证线程安全之前要先弄明白线程安全的三大特性,即原子性、可见性、有序性。

+
    +
  • 原子性,即不可分割,完整性.当某个线程正在做某个业务时,中间不能被打断、加塞,不能被分割.需要整体完整。要么同时成功,要么同时失败.与数据库中的原子性类似。
  • +
  • 可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即获取到修改的值。
  • +
  • 有序性即程序执行的顺序按照代码的先后顺序执行。
  • +
+
+

关于有序性肯定会有人有疑问,程序执行的顺序难道不是从上到下按照顺序来执行吗?

+

在多线程环境下,Java语句可能会不按照顺序执行,所以要注意数据的依赖性。计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排,一把分为以下两种:

+
    +
  • 单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。处理器在进行重新排序是必须要考虑指令之间的数据依赖;
  • +
  • 多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测;
  • +
+
+

多线程并发出问题的根本原因:

+
    +
  • CPU切换线程执导致的原子性问题;
  • +
  • 由于CPU、内存、IO 设备读写速度差异巨大,为了减少CPU等待IO的时间,在CPU和IO之间建立高速缓存,在多核CPU的情况下,会导致高速缓存与主内存之间的数据不一致,即缓存可见性问题;
  • +
  • 指令优化重排序问题;
  • +
+

内存模型

+
+

为了保证并发编程中可以满足原子性、可见性及有序性。内存模型定义了共享内存系统中多线程程序读写操作行为的规范。 +通过这些规则来规范对内存的读写操作,从而保证指令执行的正确性。它与处理器有关、与缓存有关、与并发有关、与编译器也有关。 +他解决了CPU多级缓存、处理器优化、指令重排等导致的内存访问问题,保证了并发场景下的一致性、原子性和有序性。

+
+

内存模型解决并发问题主要采用两种方式:限制处理器优化和使用内存屏障。

+

Java内存模型

+

Java内存模型,即JMM(Java Memory Model)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范通过规范定制了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。 +屏蔽了各种硬件和操作系统的访问差异的,保证了Java程序在各种平台下对内存的访问都能保证效果一致的机制及规范。

+

JMM就作用于工作内存和主存之间数据同步过程。他规定了如何做数据同步以及什么时候做数据同步。

+

Java内存模型

+

那么在Java中是如何保证原子性、可见性及有序性的呢?

+
    +
  • +

    原子性:在Java中,为了保证原子性,提供了两个高级的字节码指令 monitorentermonitorexit。对应的就是Java中的关键字 synchronized,在Java中只要被synchronized修饰就能保证原子性。

    +
  • +
  • +

    可见性:在Java中,为了保证线程间的可见性,可以使用volatilesynchronizedfinal来修饰。

    +
  • +
  • +

    有序性:在Java中,可以使用 synchronizedvolatile 来保证多线程之间操作的有序性。其中,volatile 关键字会禁止编译器指令重排,来保证;synchronized 关键字保证同一时刻只允许一条线程操作,而不能禁止指令重排,指令重排并不会影响单线程的顺序,它影响的是多线程并发执行的顺序性,从而保证了有序性。

    +
  • +
+

volatile

+

volatile通常被比喻成轻量级的锁,也是Java并发编程中比较重要的一个关键字。

+

volatile特点:

+
    +
  • 保证线程之间的可见性;
  • +
  • 禁止指令重排;
  • +
  • 不保证原子性,也就是线程不安全;
  • +
+

使用案例

+

在Java中volatile 是一个变量修饰符,只能用来修饰变量。

+

volatile 典型的使用就是单例模式中的DCL双重检查锁。

+
/**
+多线程下的单例模式 DCL(double check lock)
+**/
+class SingletonDemo {
+
+    // volatile 此处作用 禁止指令重排
+    public static volatile SingletonDemo singleton = null;
+
+    private SingletonDemo() {
+    }
+
+    public static SingletonDemo getInstance() {
+        if (singleton == null) {
+            synchronized (SingletonDemo.class) {
+                if (singleton == null) {
+                    singleton = new SingletonDemo();
+                }
+            }
+        }
+        return singleton;
+    }
+
+}
+

为什么在此处要使用volatile修饰singleton

+

多线程下的DCL单例模式,如果不加 volatile 修饰不是绝对安全的,因为在创建对象的时候JVM底层会进行三个步骤:

+
    +
  1. 分配对象的内存空间
  2. +
  3. 初始化对象
  4. +
  5. 设置对象指向刚刚分配的内存地址
  6. +
+

其中步骤2和步骤3是没有数据依赖关系的,而且无论重排前还是重排后的程序执行结果在单线程中并没有改变,因此这种重排优化是允许的。 +所以有可能先执行步骤3在执行步骤2,导致分配的对象不为 null,但对象没有被初始化;

+

所以当一个线程获取对象不为 null 时,由于对象未必已经完成初始化,会存在线程不安全的风险。

+

原理

+

《深入理解JVM》中对 volatile 的描述:

+
+

一旦一个共享变量(类的成员变量、类的静态成员变量)被 volatile 修饰之后,那么就具备了两层语义:

+
    +
  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的;
  • +
  • 禁止进行指令重排序,即有序性;
  • +
+

volatile只提供了保证访问该变量时,每次都是从内存中读取最新值,并不会使用寄存器缓存该值——每次都会从内存中读取。 +而对该变量的修改,volatile 并不提供原子性(线程不安全)的保证;由于及时更新,很可能导致另一线程访问最新变量值,无法跳出循环的情况,多线程下计数器必须使用锁保护.

+
+

上面的代码javap -v SingletonDemo.class >test.txt命令执行,将反编译后的字节码指令写入到test文件中,可以看到ACC_VOLATILE

+
  public static volatile content.posts.rookie.SingletonDemo singleton;
+    descriptor: Lcontent/posts/rookie/SingletonDemo;
+    flags: ACC_PUBLIC, ACC_STATIC, ACC_VOLATILE
+

volatile 在字节码层面,就是使用访问标志:ACC_VOLATILE 来表示,供后续操作此变量时判断访问标志是否为 ACC_VOLATILE,来决定是否遵循 volatile 的语义处理。

+

可以从openjdk8中找到对应的源码文件

+
路径:openjdk8/hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp
+

volitile字节码

+

重点是cache->is_volatile()方法,调用栈

+
bytecodeInterpreter.cpp>is_volatile() 
+==> accessFlags.hpp>is_volatile 
+==> bytecodeInterpreter.cpprelease_byte_field_put
+==> oop.inline.hpp>(oopDesc::byte_field_acquire、oopDesc::release_byte_field_put)
+==> orderAccess.hpp
+>> orderAccess_linux_x86.inline.hpp.OrderAccess::release_store
+

最终调用了OrderAccess::release_store

+
inline void     OrderAccess::release_store(volatile jbyte*   p, jbyte   v) { *p = v; }
+inline void     OrderAccess::release_store(volatile jshort*  p, jshort  v) { *p = v; }
+

可以从上面看到,到C++的实现层面,又使用C++中的 volatile 关键字,用来修饰变量,通常用于建立语言级别的内存屏障memory barrier。 +在《C++ Programming Language》一书中对 volatile 修饰词的解释:

+
+

A volatile specifier is a hint to a compiler that an object may change its value in ways not specified by the language so that aggressive optimizations must be avoided.

+
+
    +
  • volatile 修饰的类型变量表示可以被某些编译器未知的因素更改;
  • +
  • 使用 volatile 变量时,避免激进的优化; 系统总是重新从内存读取数据,即使它前面的指令刚从内存中读取被缓存,防止出现未知更改和主内存中不一致。
  • +
+

其在64位系统的实现orderAccess_linux_x86.inline.hpp.OrderAccess::release_store

+
inline void OrderAccess::fence() {
+  if (os::is_MP()) {
+    // always use locked addl since mfence is sometimes expensive
+#ifdef AMD64
+    __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
+#else
+    __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
+#endif
+  }
+}
+

代码lock; addl $0,0(%%rsp)就是lock前缀;

+
+

lock前缀,会保证某个处理器对共享内存的独占使用。 +它将本处理器缓存写入内存,该写入操作会引起其他处理器或内核对应的缓存失效。 +通过独占内存、使其他处理器缓存失效,达到了“指令重排序无法越过内存屏障”的作用。

+
+

对于 volatile修饰的变量,当对 volatile 修饰的变量进行写操作的时候,JVM会向处理器发送一条带有 lock 前缀的指令,将这个缓存中的变量回写到系统主存中。 +但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现 缓存一致性协议

+
+

缓存一致性协议: 每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

+
+

为了提高CPU处理器的执行速度,在处理器和内存之间增加了多级缓存来提升。但是由于引入了多级缓存,就存在缓存数据不一致问题。

+

CPU多级缓存

+

所以,如果一个变量被 volatile 所修饰的话,在每次数据变化之后,其值都会被强制刷入主存。而其他处理器的缓存由于遵守了缓存一致性协议,也会把这个变量的值从主存加载到自己的缓存中。这就保证了一个 volatile 在并发编程中,其值在多个缓存中是可见的。

+

volatile与可见性

+

各个线程对主内存中共享变量的操作,都是各个线程各自拷贝到自己的工作内存操作后再写回主内存中的。 +这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时 ,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享比那里X对线程B来说并不不可见。 +这种工作内存与主内存同步延迟现象就造成了可见性问题。

+

这种变量的可见性问题可以用volatile来解决。volatile的作用简单来说就是当一个线程修改了数据,并且写回主物理内存,其他线程都会得到通知获取最新的数据。

+

volatile可见性,代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        A a = new A();
+        // thread1
+        new Thread(() -> {
+            System.out.println(Thread.currentThread().getName() + " is come in");
+            try {
+                // 模拟执行其他业务
+                Thread.sleep(3);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            // 用该线程改变A类中 number 变量的值
+            a.numberTo100();
+        }, "thread1").start();
+        
+        // 如果number 等于0,则其他线程会一直等待 则证明 volatile 没有保证变量的可见性;相反则保证了变量的可见性
+        while (a.number == 0) {
+        }
+        System.out.println(Thread.currentThread().getName() + " thread is over");
+    }
+}
+class A {
+    // 注意: 此时变量要加 volatile 关键字修饰; 可以去掉 volatile 来进行对比测试
+    volatile int number = 0;
+
+    public void numberTo100() {
+        System.out.println(Thread.currentThread().getName() + " update number");
+        this.number = 100;
+    }
+}
+

volatile与原子性

+

volatile原子性,代码演示

+
public class MainTest {
+
+    public static void main(String[] args) {
+        A a = new A();
+        /**
+         * 创建20个线程 每个线程让 number++ 1000次;
+         * number 变量用 volatile 修饰
+         * 如果 volatile 保证变量的原子性,则最后结果为 20 * 1000,反之则不保证。
+         * 当然不排除偶然事件,建议反复多试几次。
+         */
+        for (int i = 0; i < 20; i++) {
+            new Thread(() -> {
+                for (int j = 0; j < 1000; j++) {
+                    a.addPlusplus();
+                }
+            }, String.valueOf(i)).start();
+        }
+        // 如果当前存活线程大于 2 个(包括main线程) 礼让线程继续执行上边的线程
+        while (Thread.activeCount() > 2) {
+            Thread.yield();
+        }
+        System.out.println(Thread.currentThread().getName() + " Thread is over\t" + a.number);
+
+    }
+
+}
+
+class A {
+    volatile int number = 0;
+
+    public void addPlusplus() {
+        this.number++;
+    }
+}
+

不保证原子性的原因

+

由于各个线程之间都是复制主内存的数据到自己的工作空间里边修改数据,CPU的轮询反复切换线程,会导致数据丢失。 +即某个线程修改了数据,准备回主内存,此时CPU切换到另一个线程修改了数据,并且写回到了主内存,此时其他的线程不知道主内存的数据已经被更改,还会执行将之前从主内存复制的数据修改后的,写到主内存,这就导致了数据被覆盖,丢失。

+

解决

+

如果要解决原子性的问题,在Java中只能控制线程,在修改的时候不能被中断,即加锁。 +上面的例子可以使用CAS的实现AtomicInteger来解决。

+
public class MainTest {
+
+    public static void main(String[] args) {
+        A a = new A();
+        /**
+         * 创建20个线程 每个线程让 number++ 1000次;
+         * number 变量用 volatile 修饰
+         * 如果 volatile 保证变量的原子性,则最后结果为 20 * 1000,反之则不保证。
+         * 当然不排除偶然事件,建议反复多试几次。
+         */
+        for (int i = 0; i < 20; i++) {
+            new Thread(() -> {
+                for (int j = 0; j < 1000; j++) {
+                    a.addPlusplus();
+                }
+            }, String.valueOf(i)).start();
+        }
+        // 如果当前存活线程大于 2 个(包括main线程) 礼让线程继续执行上边的线程
+        while (Thread.activeCount() > 2) {
+            Thread.yield();
+        }
+        System.out.println(Thread.currentThread().getName() + " Thread is over\t" + a.number);
+
+    }
+
+}
+
+class A {
+
+    int number = 0;
+
+    /**
+     * 如果要解决原子性的问题可以用synchronized 关键字(这种太浪费性能)
+     * 可用JUC下的 AtomicInteger 来解决
+     **/
+    AtomicInteger atomicInteger = new AtomicInteger(number);
+
+    public void addPlusplus() {
+        number = atomicInteger.incrementAndGet();
+    }
+}
+

对于AtomicInteger.incrementAndGet方法来说,原理就是volatile + do...while() + CAS;

+

AtomicInteger.incrementAndGet源码

+
public final int incrementAndGet() {
+    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
+}
+//=========================
+public final int getAndAddInt(Object var1, long var2, int var4) {
+    int var5;
+    do {
+        var5 = this.getIntVolatile(var1, var2);
+    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
+
+    return var5;
+}
+

volatile修饰该变量,保证该变量被某个线程修改时,保证其他线程中的这个变量的可见性; +在多线程环境下,CPU轮流切换线程执行,有可能某个线程修改了数据,准备回主内存,此时CPU切换到另一个线程修改了数据,并且写回到了主内存,此时就导致数据的不准确; +do...while() + CAS的作用就是,当某个线程工作内存中的值与主内存中的值,如果不相同就会一直while循环下去,之所以用do..while是考虑到做自增操作。

+

volatile与有序性

+

有序性,指的就是代码按照顺序执行,这个就是对比指令重排来说的;计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排。

+

在上面的使用案例中的代码,DCL就是一个使用禁止指令重排的案例。

+

volatile 禁止指令重排原因

+

由于编译器和处理器都能执行指令重排的优化,如果在指令键加入一条内存屏障(Memory barrier),就会告诉编译器和CPU不管什么指令都不能和这条加入Memory barrier指令键重新排序,也就是说通过内存屏障禁止在内存屏障前后的指令重新排序优化。 +内存屏障的另一个作用就是强制刷出各种CPU缓存数据,因此任何CPU上的线程都能读取到这些数据的最新值,即可见性。

+

CAS

+

CAS全称为Compare and Swap被译为比较并交换。是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。 +java.util.concurrent.atomic 并发包下的所有原子类都是基于 CAS 来实现的。

+

原理

+

AtomicInteger 原子整型类为例,来看一下CAS实现原理。

+
public class MainTest {
+    public static void main(String[] args) {
+        new AtomicInteger().compareAndSet(1,2);
+    }
+}
+

以上面的代码为例,调用栈如下:

+
compareAndSet --> unsafe.compareAndSwapInt ---> unsafe.compareAndSwapInt --> (C++) cmpxchg
+

AtomicInteger 内部方法都是基于 Unsafe 类实现的。

+
public final boolean compareAndSet(int expect, int update) {
+    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
+}
+

参数:

+
    +
  • this: Unsafe 对象本身,需要通过这个类来获取 value 的内存偏移地址;
  • +
  • valueOffset: value 变量的内存偏移地址;
  • +
  • expect: 期望更新的值;
  • +
  • update: 要更新的最新值;
  • +
+

偏移量valueOffset

+
// setup to use Unsafe.compareAndSwapInt for updates
+    private static final Unsafe unsafe = Unsafe.getUnsafe();
+    private static final long valueOffset;
+
+    static {
+        try {
+            valueOffset = unsafe.objectFieldOffset
+                (AtomicInteger.class.getDeclaredField("value"));
+        } catch (Exception ex) { throw new Error(ex); }
+    }
+
+    private volatile int value;
+
    +
  1. Unsafe 是CAS的核心类,Java无法直接访问底层操作系统,而是通过 native 方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类 Unsafe,它提供了硬件级别的原子操作。
  2. +
  3. valueOffset 表示的是变量值在内存中的偏移地址,因为 Unsafe 就是根据内存偏移地址获取数据的原值的。
  4. +
  5. value 是用 volatile 修饰的,保证了多线程之间看到的 value 值是同一份。
  6. +
+

继续向底层深入,就会看到Unsafe类中的一些方法,同时也是CAS的核心方法:

+
public final class Unsafe {
+
+    // ...
+
+    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
+
+    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
+
+    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
+    
+    // ...
+}
+

上面的三个方法的原理,可以对应去查看 openjdkhotspot源码:src/share/vm/prims/unsafe.cpp

+
#define FN_PTR(f) CAST_FROM_FN_PTR(void*, &f)
+
+{CC"compareAndSwapObject", CC"("OBJ"J"OBJ""OBJ")Z",  FN_PTR(Unsafe_CompareAndSwapObject)},
+
+{CC"compareAndSwapInt",  CC"("OBJ"J""I""I"")Z",      FN_PTR(Unsafe_CompareAndSwapInt)},
+
+{CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z",      FN_PTR(Unsafe_CompareAndSwapLong)},
+

最终在 hotspot 源码实现中都会调用统一的 cmpxchg 函数,/src/share/vm/runtime/Atomic.cpp

+
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte*dest, jbyte compare_value) {
+         assert (sizeof(jbyte) == 1,"assumption.");
+         uintptr_t dest_addr = (uintptr_t) dest;
+         uintptr_t offset = dest_addr % sizeof(jint);
+         volatile jint*dest_int = ( volatile jint*)(dest_addr - offset);
+         // 对象当前值
+         jint cur = *dest_int;
+         // 当前值cur的地址
+         jbyte * cur_as_bytes = (jbyte *) ( & cur);
+         // new_val地址
+         jint new_val = cur;
+         jbyte * new_val_as_bytes = (jbyte *) ( & new_val);
+          // new_val存exchange_value,后面修改则直接从new_val中取值
+         new_val_as_bytes[offset] = exchange_value;
+         // 比较当前值与期望值,如果相同则更新,不同则直接返回
+         while (cur_as_bytes[offset] == compare_value) {
+          // 调用汇编指令cmpxchg执行CAS操作,期望值为cur,更新值为new_val
+             jint res = cmpxchg(new_val, dest_int, cur);
+             if (res == cur) break;
+             cur = res;
+             new_val = cur;
+             new_val_as_bytes[offset] = exchange_value;
+         }
+         // 返回当前值
+         return cur_as_bytes[offset];
+}
+

从上述源码可以看出CAS的原理就是调用了汇编指令 cmpxchg ,最终其实也就调用了CPU的某些指令。

+

CAS作用也一目了然,在多线程环境中,就是比较当前线程工作内存中的值和主内存中的值,如果相同则执行规定操作,否则继续比较,直到主内存和当前线程工作内存中的值一致为止。

+

例如代码:

+
public final int getAndAddInt(Object var1, long var2, int var4) {
+        int var5;
+        do {
+            var5 = this.getIntVolatile(var1, var2);
+        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
+        return var5;
+}
+

如何保证数据一致性

+

从源码可以看出,CAS是通过Unsafe调用CPU指令,当CPU中某个处理器对缓存中的共享变量进行了操作,其他处理器会有个嗅探机制,将其他处理器的该共享变量的缓存失效,待其他线程读取时会重新从主内存中读取最新的数据,基于 MESI 缓存一致性协议来实现的。

+

简述,就是通过CPU的缓存一致性协议来保证线程之间的数据一致性的。

+
+

CPU 处理器速度远远大于在主内存中的,为了解决速度差异,在他们之间架设了多级缓存,如 L1、L2、L3 级别的缓存,这些缓存离CPU越近就越快,将频繁操作的数据缓存到这里,加快访问速度。

+
+

CPU多级缓存

+

CAS与Unsafe关系

+

CAS的作用是比较并交换,就是先拿这个期望值,与主内存的值比较,判断主内存中该位置是否存在期望值, +如果存在,则改为新的值,这个修改的过程是具有原子性的. +因为CAS是cpu并发源语,并发源语体现在Java sun.misc.Unsafa类上. +调用Unsafe类中的CAS方法,JVM会帮我们实现CAS汇编指令。这是一种完全依赖于硬件的功能,通过他实现了原子操作。 +由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成数据不一致问题。

+
+

PS Unsafe类 +CAS其实是调用了 Unsafe 类的方法 Unsafa 类是CAS核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe 相当于一个后门,基于该类可以直接操作特定内存数据。 +Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针(内存地址)一样直接操作内存,因此Java中CAS操作的执行依赖于Unsafe类的方法。 +Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

+
+

缺点

+
循环时间长开销
+

因为是采用自旋锁的方式来实现所以,自然有自旋锁的缺点,循环时间长开销大,例如:getAndAddInt 方法执行,有个do while循环,如果CAS失败,一直会进行尝试,如果CAS长时间不成功,可能会给CPU带来很大的开销。

+
public final int getAndAddInt(Object var1, long var2, int var4) {
+        int var5;
+        do {
+            var5 = this.getIntVolatile(var1, var2);
+        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
+        return var5;
+}
+
多个变量原子性
+

只能保证一个共享变量的原子操作,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。 +但是Java从1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,可以把多个变量放在一个对象里来进行CAS操作。

+
ABA问题
+

ABA问题示例代码:

+
public class MainTest {
+    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
+    public static void main(String[] args) {
+        new Thread(() -> {
+            // 先改到101在改回来,CAS会认为value没有被修改过
+            atomicReference.compareAndSet(100, 101);
+            atomicReference.compareAndSet(101, 100);
+        }, "Thread 1").start();
+
+        new Thread(() -> {
+            try {
+                //保证线程1完成一次ABA操作
+                TimeUnit.SECONDS.sleep(1);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
+        }, "Thread 2").start();
+        try {
+            TimeUnit.SECONDS.sleep(2);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+}
+

CAS算法实现一个重要前提是,需要去除内存中某个时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

+

比如,线程1从内存位置V取出A,线程2同时也从内存取出A,并且线程2进行一些操作将值改为B,然后线程2又将V位置数据改成A,这时候线程1进行CAS操作发现内存中的值依然时A,然后线程1操作成功。 +尽管线程1的CAS操作成功,但是不代表这个过程没有问题。

+

简单说,如果一个线程改了一个值,最后又改回到初始值了,这时候CAS会认为它没有被修改过。简而言之就是只比较结果,不比较过程。

+

ABA问题解决

+

利用 AtomicReference 类进行原子引用

+
public class AtomicRefrenceDemo {
+    public static void main(String[] args) {
+        User z3 = new User("张三", 22);
+        User l4 = new User("李四", 23);
+        AtomicReference<User> atomicReference = new AtomicReference<>();
+        atomicReference.set(z3);
+        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
+        System.out.println(atomicReference.compareAndSet(z3, l4) + "\t" + atomicReference.get().toString());
+    }
+}
+
+@Getter
+@ToString
+@AllArgsConstructor
+class User {
+    String userName;
+    int age;
+}
+
// 输出结果
+true	User(userName=李四, age=23)
+false	User(userName=李四, age=23)
+

使用时间戳的原子引用AtomicStampedReference修改版本号。主要是在对象中额外再增加一个标记来标识对象是否有过变更

+
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
+
+public static void main(String[] args) {
+    new Thread(() -> {
+            int stamp = atomicStampedReference.getStamp();
+            System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
+            try {
+                TimeUnit.SECONDS.sleep(2);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
+            System.out.println(Thread.currentThread().getName() + "\t第2次版本号" + atomicStampedReference.getStamp());
+            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
+            System.out.println(Thread.currentThread().getName() + "\t第3次版本号" + atomicStampedReference.getStamp());
+        }, "Thread 3").start();
+
+        new Thread(() -> {
+            int stamp = atomicStampedReference.getStamp();
+            System.out.println(Thread.currentThread().getName() + "\t第1次版本号" + stamp);
+            try {
+                TimeUnit.SECONDS.sleep(4);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+            boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
+
+            System.out.println(Thread.currentThread().getName() + "\t修改是否成功" + result + "\t当前最新实际版本号:" + atomicStampedReference.getStamp());
+            System.out.println(Thread.currentThread().getName() + "\t当前最新实际值:" + atomicStampedReference.getReference());
+        }, "Thread 4").start();
+}
+
Thread 3	第1次版本号1
+Thread 4	第1次版本号1
+Thread 3	第2次版本号2
+Thread 3	第3次版本号3
+Thread 4	修改是否成功false	当前最新实际版本号:3
+Thread 4	当前最新实际值:100
+

J.U.C.

+

参考文章:

+ +

java.util.concurrent.locks包下常用的类与接口是jdk1.5后新增的。lock的出现是为了弥补synchronized关键字解决不了的一些问题。

+

例如:当一个代码块被synchronized修饰了,一个线程获取了对应的锁,并执行该代码块时,其他线程只能一直等待,等待获取锁的线程释放锁,如果这个线程因为某些原因被堵塞了,没有释放锁,那么其他线程只能一直等待下去。导致效率很低。

+

因此就需要有一种机制可以不让等待的线程一直无期限地等待下去(比如只等待一定的时间或者能够响应中断),通过Lock就可以办到。

+

locksynchronized最大的区别就是lock能够手动控制锁,而synchronized是JVM控制的。所以lock更加灵活。lock锁的粒度要优于synchronized。 +在实际使用中,自然是能够替代synchronized关键字的。

+

使用

+

在实际使用过程中,lock也是比较简单的。 +LockReadWriteLock是两大锁的根接口,Lock代表实现类是ReentrantLock(可重入锁),ReadWriteLock(读写锁)的代表实现类是ReentrantReadWriteLock

+

juc.locks

+
Lock
+

lock()

+

lock()方法是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。 +如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。 +因此,一般来说,使用Lock必须在try…catch…块中进行,并且将释放锁的操作放在finally块中进行,以保证锁一定被被释放,防止死锁的发生。

+
  Lock l = ...;
+  l.lock();
+  try {
+    // access the resource protected by this lock
+  } finally {
+    l.unlock();
+  }
+

trylock()

+

尝试获取锁,如果锁可用则返回true,不可用则返回false。也就是说,这个方法无论如何都会立即返回,在拿不到锁时不会一直在那等待。

+
+

tryLock(long time, TimeUnit unit)方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false,同时可以响应中断。 +如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。

+
+
  Lock lock = ...;
+  if (lock.tryLock()) {
+    try {
+      // manipulate protected state
+    } finally {
+      lock.unlock();
+    }
+  } else {
+    // perform alternative actions
+  }
+

lockInterruptibly()

+

当通过这个方法去获取锁时,如果线程正在等待获取锁,则这个线程能够响应中断,即中断线程的等待状态。 +例如,当两个线程同时通过lock.lockInterruptibly()想获取某个锁时,假若此时线程A获取到了锁,而线程B只有在等待,那么对线程B调用threadB.interrupt()能够中断线程B的等待过程。

+

注意,当一个线程获取了锁之后,是不会被interrupt()方法中断的。因为interrupt()方法只能中断阻塞过程中的线程而不能中断正在运行过程中的线程。因此,当通过 lockInterruptibly() 方法获取某个锁时,如果不能获取到,那么只有进行等待的情况下,才可以响应中断的。 +与 synchronized 相比,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。

+

由于lockInterruptibly()的声明中抛出了异常,所以lock.lockInterruptibly()必须放在try块中或者在调用lockInterruptibly()的方法外声明抛出 InterruptedException

+
public void method() throws InterruptedException {
+    lock.lockInterruptibly();
+    try {  
+     //.....
+    }
+    finally {
+        lock.unlock();
+    }  
+}
+

newCondition

+

Lock接口提供了方法Condition newCondition();Condition也是一个接口,可以理解为synchronized锁的监视器的概念; +对于synchronized是借助于锁与监视器,从而进行线程的同步与通信协作;而Lock接口也提供了synchronized的语意,对于监视器的概念,则借助于Condition

+

lock中可以定义多个Condition,也就是一个锁,可以对应多个监视器,可以更加细粒度的进行同步协作的处理。

+
ReadWriteLock
+

该接口有两个方法:

+
//返回用于读取操作的锁    
+Lock readLock()   
+//返回用于写入操作的锁  
+Lock writeLock() 
+

ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁。共享锁与独占锁的实现是读写锁。 +Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并添加了可重入的特性。

+

对于ReetrantReadWriteLock其读锁是共享锁而写锁是独占锁,读锁的共享可保证并发读是非常高效的; +需要注意的是,读写、写读和写写的过程是互斥的,只有读读不是互斥的

+

读写锁使用示例

+
public class MainTest {
+
+    public static void main(String[] args) {
+        MyCache myCache = new MyCache();
+        for (int i = 0; i < 10; i++) {
+            int finalI = i;
+            new Thread(() -> {
+                myCache.put(finalI + "", finalI + "");
+            }, String.valueOf(i)).start();
+        }
+
+        System.out.println("---------------");
+
+        for (int i = 0; i < 10; i++) {
+            int finalI = i;
+            new Thread(() -> {
+                myCache.get(finalI + "");
+            }, String.valueOf(i)).start();
+        }
+    }
+}
+
+class MyCache {
+
+    private volatile Map<String, Object> map = new HashMap<>();
+
+    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
+
+    // 写操作
+    public void put(String key, Object value) {
+        rwLock.writeLock().lock();
+        try {
+            System.out.println("开始 写入 ..." + key);
+            map.put(key, value);
+            System.out.println("写入完成 ...");
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            rwLock.writeLock().unlock();
+        }
+
+    }
+
+    // 读操作
+    public Object get(String key) {
+        Object obj = null;
+        rwLock.readLock().lock();
+        try {
+            System.out.println("开始读取 ..." + key);
+            obj = map.get(key);
+            System.out.println("读取完成 ..." + obj);
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+            rwLock.readLock().unlock();
+        }
+        return obj;
+    }
+
+}
+

LockSupport

+

LockSupportjava.util.concurrent.locks包下的一个工具类。 +LockSupport类使用了一种名为Permit(许可)的概念来做到阻塞和唤醒线程的功能,每个线程都有一个许可(permit),permit只有两个值1和零,默认是零。

+

官网解释:LockSupport是用来创建锁和其他同步类的基本线程阻塞原语;

+

其中有两个重要的方法,通过park()unpark()方法来实现阻塞和唤醒线程的操作;可以理解为wait()notify的加强版。

+

传统等待唤醒机制:

+
    +
  1. 使用Object中的wait()方法让线程等待,使用Object中的notify方法唤醒线程
  2. +
  3. 使用JUC包中Conditionawait()方法让线程等待,使用signal()方法唤醒线程
  4. +
+

传统等待唤醒机制的弊端:

+
    +
  • waitnotify/await()signal()方法必须要在同步块或同步方法里且成对出现使用,如果没有在synchronized代码块使用则抛出java.lang.IllegalMonitorStateException;
  • +
  • 必须先wait/await()notify/signal(),如果先notifywait会出现另一个线程一直处于等待状态;
  • +
+

LockSupport对比传统等待唤醒机制,能够解决上面的弊端:

+
public class MainTest {
+    public static void main(String[] args) {
+
+        Thread t1=new Thread(()->{
+//            try { TimeUnit.SECONDS.sleep(3);  } catch (InterruptedException e) {e.printStackTrace();}
+            System.out.println(Thread.currentThread().getName()+"\t"+"coming....");
+            LockSupport.park();
+            /*
+            如果这里有两个LockSupport.park(),因为permit的值为1,上一行已经使用了permit
+            所以下一行被注释的打开会导致程序处于一直等待的状态
+            * */
+            //LockSupport.park();
+            System.out.println(Thread.currentThread().getName()+"\t"+"被B唤醒了");
+        },"A");
+        t1.start();
+
+        Thread t2=new Thread(()->{
+            System.out.println(Thread.currentThread().getName()+"\t"+"唤醒A线程");
+            //有两个LockSupport.unpark(t1),由于permit的值最大为1,所以只能给park一个通行证
+            LockSupport.unpark(t1);
+            //LockSupport.unpark(t1);
+        },"B");
+        t2.start();
+    }
+}
+

LockSupport原理是调用的Unsafe中的native代码。以unpark、park为例:

+
    public static void unpark(Thread thread) {
+        if (thread != null)
+            UNSAFE.unpark(thread);
+    }
+
+    public static void park(Object blocker) {
+        Thread t = Thread.currentThread();
+        setBlocker(t, blocker);
+        UNSAFE.park(false, 0L);
+        setBlocker(t, null);
+    }
+

理解:

+
    +
  • 线程堵塞需要消耗凭证,这个凭证最多只有一个。
  • +
  • 当调用park方法时,如果有凭证则会直接消耗这张凭证然后退出;如果没有凭证就必须堵塞等待凭证可用;
  • +
  • unpark方法则相反,调用该方法会增加一个凭证,连续调用两次unpark()和调用一次一样,只会增加一个凭证。
  • +
+

为什么可以先唤醒线程后阻塞线程?

+

因为unpark获得了一个凭证,之后再调用park方法,就可以名正言顺的凭证消费,所以不会阻塞。

+

为什么唤醒两次后阻塞两次,但最终结果还会阻塞线程?

+

因为凭证的数量最多为1,连续调用两次unpark和调用一次unpark效果一样,只会增加一个凭证; +而调用两次park却需要消费两个凭证,证不够,不能放行。

+

AQS

+

AQS是指java.util.concurrent.locks包下的一个抽象类AbstractQueuedSynchronizer译为:抽象的队列同步器。

+

在JUC包下,能够看到有许多类都继承了AQS,例如,ReentrantLock 、CountDownLatch 、 ReentrantReadWriteLock 、 Semaphore ; +所以AQS是JUC内容中重要的基础。

+

JUC.locks包UML

+
+

同步、同步器? +同步,面向锁的使用者,定义了程序员和锁交互的使用层API; +同步器,面向锁的实现者,统一规范,实现锁 自定义等待唤醒机制等等;

+
+

AQS是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,通过内置的CLH(FIFO)队列的变种来完成资源获取线程的排队工作; +将每条将要去抢占资源的线程封装成一个Node节点来实现锁的分配,有一个int类变量表示持有锁的状态,通过CAS完成对status值的修改。

+

AQS

+

在多线程并发环境下,使用lock加锁,当处在加锁与解锁之间的代码,只能有一个线程来执行;这时候其他线程不能够获取锁,如果不处理线程就会造成了堵塞; +在AQS框架中,会将暂时获取不到锁的线程加入到队列里,这个队列就是AQS的抽象表现。它会将这些线程封装成队列的结点,通过CAS、自旋以及LockSuport.park()的方式,维护state变量的状态,使并发达到同步的效果。

+

AQS中的队列,是指CLH队列(Craig, Landin, and Hagerste[三个人名组成])锁队列的变体,是一个双向队列。 +队列中的元素即Node结点,每个Node中包含:头结点、尾结点、等待状态、存放的线程等;Node遵循从尾部入队,从头部出队的规则,即先进先出原则。

+
+

详细可查看 java.util.concurrent.locks; 包下 AbstractQueuedSynchronizer 类。

+
+

AQS可以理解为一个框架,因为它定义了一些JUC包下常用"锁"的标准。 +AQS简单来说,包含一个status和一个队列;status保存线程持有锁的状态,用于判断该线程获没获取到锁,没获取到锁就去队列中排队。 +队列是由Node结点构成,每个Node结点里面主要包含一个waitStatus和保存的线程。

+

AQS简单理解

+

ReentrantLock原理

+

ReentrantLock译为,可重入锁,它的原理用到了AQS。

+
+

AQS里面有个变量叫State,它的值有3种状态:没占用是0,占用了是1,大于1是可重入锁 +如果A、B两个线程进来了以后,请问这个总共有多少个Node节点?答案是3个,其中队列的第一个是傀儡节点(哨兵节点)

+
+

ReentrantLock原理说简单一点,就是加锁解锁的过程。

+

在多线程并发环境下,某个线程持有锁,将state由0设置为1,如果在有其他线程再次进入,线程则会经过一系列判断,然后构建Node结点,最终形成双向链表结构。 +最后在执行LockSupport.park()方法,将等待的线程挂起,如果当前持有锁的线程释放了锁,则将state变量设置为0,调用LockSpoort.unpark()方法指定唤醒等待队列中的某个线程。

+

ReentrantLock使用

+
public class AQSDemo {
+    public static void main(String[] args) {
+        ReentrantLock lock = new ReentrantLock();
+        new Thread(() -> {
+                lock.lock();
+                try{
+                    System.out.println("-----A thread come in");
+
+                    try { TimeUnit.MINUTES.sleep(20); }catch (Exception e) {e.printStackTrace();}
+                }finally {
+                    lock.unlock();
+                }
+        },"A").start();
+
+        new Thread(() -> {
+            lock.lock();
+            try{
+                System.out.println("-----B thread come in");
+            }finally {
+                lock.unlock();
+            }
+        },"B").start();
+
+        new Thread(() -> {
+            lock.lock();
+            try{
+                System.out.println("-----C thread come in");
+            }finally {
+                lock.unlock();
+            }
+        },"C").start();
+    }
+}
+

ReentrantLock加锁

+

ReentrantLock原理用到了AQS,而AQS包括一个线程队列和一个state变量;所以ReentrantLock加锁过程,可以简单理解为state变量的变化。 +如果在多线程并发的环境下,还要有其他线程被保存到AQS的队列中。 +加锁过程,如图所示: +reentrantLock加锁

+

ReentrantLock加锁,有两种形式,默认是非公平锁,但可以通过构造方法来指定为公平锁

+
    public static void main(String[] args) {
+        ReentrantLock reentrantLock = new ReentrantLock(true);
+    }
+    //⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇
+    /**
+     * Creates an instance of {@code ReentrantLock} with the
+     * given fairness policy.
+     *
+     * @param fair {@code true} if this lock should use a fair ordering policy
+     */
+    public ReentrantLock(boolean fair) {
+        sync = fair ? new FairSync() : new NonfairSync();
+    }
+

无论是公平锁还是非公平锁,由于用到了AQS框架,所以底层实现的逻辑大致是差不多的,ReentrantLock加锁方法调用栈:

+
lock() --> acquire() --> tryAcquire() --> addWaiter() --> acquireQueued() --> selfInterrupt()
+

虽然大致逻辑差不多,但是区别总是有的,总的来说非公平锁比非公平锁在代码里面多了几行判断;

+
// ===========重写 lock 方法对比===========
+    // 公平锁
+    final void lock() {
+        acquire(1);
+    }
+
+    // 非公平锁
+    final void lock() {
+        if (compareAndSetState(0, 1))
+            setExclusiveOwnerThread(Thread.currentThread());
+        else
+            acquire(1);
+    }
+
// ===========重写 tryAcquire 方法对比===========
+
+    // 公平锁
+    protected final boolean tryAcquire(int acquires) {
+        final Thread current = Thread.currentThread();
+        int c = getState();
+        if (c == 0) {
+            if (!hasQueuedPredecessors() &&
+                compareAndSetState(0, acquires)) {
+                setExclusiveOwnerThread(current);
+                return true;
+            }
+        }
+        else if (current == getExclusiveOwnerThread()) {
+            int nextc = c + acquires;
+            if (nextc < 0)
+                throw new Error("Maximum lock count exceeded");
+            setState(nextc);
+            return true;
+        }
+        return false;
+    }
+
+    // 非公平锁
+    protected final boolean tryAcquire(int acquires) {
+        return nonfairTryAcquire(acquires);
+    }
+
+    final boolean nonfairTryAcquire(int acquires) {
+        final Thread current = Thread.currentThread();
+        int c = getState();
+        if (c == 0) {
+            if (compareAndSetState(0, acquires)) {
+                setExclusiveOwnerThread(current);
+                return true;
+            }
+        }
+        else if (current == getExclusiveOwnerThread()) {
+            int nextc = c + acquires;
+            if (nextc < 0) // overflow
+                throw new Error("Maximum lock count exceeded");
+            setState(nextc);
+            return true;
+        }
+        return false;
+    }
+

在重写的tryAcquire方法里,公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors() ;

+

该方法作用:保证等待队列中的线程按照从头到尾的顺序排队获取锁。 +举个例子,目前队列中有两个线程A、B,线程A,在线程B的前面;在当前线程释放锁的时候,线程B获取到了锁,该方法会判断当前头结点的下一个结点中存放的线程跟当前线程相不相同;

+

在这里头结点的下一个结点存放的线程是傀儡结点线程为null,而当前线程是线程B,所以返回true,回到上一个方法true取反就是false,则获取锁失败。

+
    public final boolean hasQueuedPredecessors() {
+        // The correctness of this depends on head being initialized
+        // before tail and on head.next being accurate if the current
+        // thread is first in queue.
+        Node t = tail; // Read fields in reverse initialization order
+        Node h = head;
+        Node s;
+        return h != t &&
+            ((s = h.next) == null || s.thread != Thread.currentThread());
+    }
+

在执行完tryAcquire方法之后就会执行addWaiter方法。

+

addWaiter方法作用;当第一次将等待的线程添加到队列时,先会调用enq方法;如果不是第一次调用,即尾结点不为空,队列中已经有了其他线程结点,则会直接将当前线程的前结点指向尾结点,即队列中最后一个线程结点; +然后用CAS将前一个结点的下一个结点指向当前结点,最后返回添加到队列中的结点。

+
    private Node addWaiter(Node mode) {
+        Node node = new Node(Thread.currentThread(), mode);
+        // Try the fast path of enq; backup to full enq on failure
+        Node pred = tail;
+        if (pred != null) {
+            node.prev = pred;
+            if (compareAndSetTail(pred, node)) {
+                pred.next = node;
+                return node;
+            }
+        }
+        enq(node);
+        return node;
+    }
+

enq方法作用是,将等待获取锁的线程封装成Node结点,并将Node结点串联起来,形成双向链表结构;简而言之就是将线程添加到等待队列中去。

+

该方法运用自旋机制,如果添加的结点为第一个结点,则会在第一个实际结点之前,先生成一个“傀儡结点”; +头结点指向指向傀儡结点,傀儡结点的后结点则指向添加的第一个结点;添加的第一个结点的前结点指向傀儡结点,尾结点指向实际结点。然后将处理好的实际结点返回。

+
    private Node enq(final Node node) {
+        for (;;) {
+            Node t = tail;
+            if (t == null) { // Must initialize
+                if (compareAndSetHead(new Node()))
+                    tail = head;
+            } else {
+                node.prev = t;
+                if (compareAndSetTail(t, node)) {
+                    t.next = node;
+                    return t;
+                }
+            }
+        }
+    }
+

之后在执行acquireQueued方法。该方法用到了自旋机制;首先先判断当前结点是否为头结点,如果是头结点,就让头结点中的线程尝试获取锁,之后执行异常结点的操作。 +如果不是头结点,就会尝试让当前线程挂起,直到持有锁的线程释放锁,唤醒等待的线程,之后再去尝试获取锁。

+
    final boolean acquireQueued(final Node node, int arg) {
+        boolean failed = true;
+        try {
+            boolean interrupted = false;
+            for (;;) {
+                final Node p = node.predecessor();
+                if (p == head && tryAcquire(arg)) {
+                    setHead(node);
+                    p.next = null; // help GC
+                    failed = false;
+                    return interrupted;
+                }
+                if (shouldParkAfterFailedAcquire(p, node) &&
+                    parkAndCheckInterrupt())
+                    interrupted = true;
+            }
+        } finally {
+            if (failed)
+                cancelAcquire(node);
+        }
+    }
+

如果不是头结点,则会执行shouldParkAfterFailedAcquire方法:

+
    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
+        int ws = pred.waitStatus;
+        if (ws == Node.SIGNAL)
+            return true;
+        if (ws > 0) {
+            do {
+                node.prev = pred = pred.prev;
+            } while (pred.waitStatus > 0);
+            pred.next = node;
+        } else {
+            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
+        }
+        return false;
+    }
+

执行该方法,首先判断上一个结点的waitStatus; +如果该队列只有一个结点,则上一个结点为头结点,此时头结点的waitStatus=0,经过该方法会将上一个结点的waitStatus通过CAS,设置为-1; +因为最外部是一个自旋机制,会一直循环,当第二次进入该方法,则会直接返回true。返回true,则意味着当前线程将进入堵塞状态,会执行parkAndCheckInterrupt()方法。

+
    private final boolean parkAndCheckInterrupt() {
+        LockSupport.park(this);
+        return Thread.interrupted();
+    }
+

调用LockSupport.park()方法让线程挂起,直到持有锁的线程将它们唤醒。

+

ReentrantLock解锁 +reentrantLock解锁

+

ReentrantLock释放锁调用栈:

+
unlock() --> release() --> tryRelease() --> unparkSuccessor()
+

release方法,如果tryRelease方法返回true,则判队列中的头结点中的waitStatus,如果不等于0则,执行unparkSuccessor方法,按唤醒队列中等待的线程。

+

核心就是调用tryRelease方法和unparkSuccessor方法:

+
    public final boolean release(int arg) {
+        if (tryRelease(arg)) {
+            Node h = head;
+            if (h != null && h.waitStatus != 0)
+                unparkSuccessor(h);
+            return true;
+        }
+        return false;
+    }
+

tryRelease方法作用是尝试释放锁;首先获取当前持有锁线程的state,并使其减1; +如果减一后的state值等于0,则认为该线程马上要释放锁,将当前持有锁的线程为null,将0设置为state的新值,返回true。

+
    protected final boolean tryRelease(int releases) {
+        int c = getState() - releases;
+        if (Thread.currentThread() != getExclusiveOwnerThread())
+            throw new IllegalMonitorStateException();
+        boolean free = false;
+        if (c == 0) {
+            free = true;
+            setExclusiveOwnerThread(null);
+        }
+        setState(c);
+        return free;
+    }
+

由于之前加锁等待队列中是自旋机制,由于持有锁的线程唤醒队列中排队的线程,队列中的线程则会尝试再次获取锁。

+

首先,将头结点从前向后移动一个结点,随后清空该结点的线程对象、该结点的前结点、后结点,即将该结点设置成新的傀儡结点(哨兵结点),最后结束循环。

+
private void unparkSuccessor(Node node) {
+    int ws = node.waitStatus;
+    if (ws < 0)
+        compareAndSetWaitStatus(node, ws, 0);
+
+    Node s = node.next;
+    if (s == null || s.waitStatus > 0) {
+        s = null;
+        for (Node t = tail; t != null && t != node; t = t.prev)
+            if (t.waitStatus <= 0)
+                s = t;
+    }
+    if (s != null)
+        LockSupport.unpark(s.thread);
+}
+

总结

+

ReentrantLock 在采用非公平锁构造时,首先检查锁状态,如果锁可用,直接通过CAS设置成持有状态,且把当前线程设置为锁的拥有者。 +如果当前锁已经被持有,那么接下来进行可重入检查,如果可重入,需要为锁状态加上请求数。如果不属于上面两种情况,那么说明锁是被其他线程持有, +当前线程应该放入等待队列。

+

在放入等待队列的过程中,首先要检查队列是否为空队列,如果为空队列,需要创建虚拟的头节点,然后把对当前线程封装的节点加入到队列尾部。 +由于设置尾部节点采用了CAS,为了保证尾节点能够设置成功,这里采用了无限循环的方式,直到设置成功为止。

+

在完成放入等待队列任务后,则需要维护节点的状态,以及及时清除处于Cancel状态的节点,以帮助垃圾收集器及时回收。 +如果当前节点之前的节点的等待状态小于1,说明当前节点之前的线程处于等待状态(挂起),那么当前节点的线程也应处于等待状态(挂起)。 +挂起的工作是由 LockSupport 类支持的,LockSupport 通过JNI调用本地操作系统来完成挂起的任务。 +在当前等待的线程,被唤起后,检查中断状态,如果处于中断状态,那么需要中断当前线程。

+

CountDownLatch

+

count down latch直译为:倒计时门闩,也可以叫做闭锁。

+
+

门闩,汉语词汇。拼音:mén shuān 释义:指门关上后,插在门内使门推不开的滑动插销。

+
+

CountDownLatchJDK文档注释:

+
+

A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.

+
+

大意:一种同步辅助工具,允许一个或多个线程等待,直到在其他线程中执行的一组操作完成。

+

举个例子,晚上教室关门,要同学都离开之后,再关门:

+
public class MainTest {
+    public static void main(String[] args) throws InterruptedException {
+        CountDownLatch countDownLatch = new CountDownLatch(7);
+        for (int i = 0; i < 7; i++){
+            new Thread(() -> {
+                System.out.println("同学"+Thread.currentThread().getName() + "\t 离开");
+                countDownLatch.countDown();
+            },String.valueOf(i)).start();
+        }
+        countDownLatch.await();
+        System.out.println("关门...");
+    }
+}
+

再比如,跑步比赛,裁判的发令枪一响,参赛者就开始跑步:

+
public class MainTest {
+    public static void main(String[] args) throws InterruptedException {
+        CountDownLatch countDownLatch = new CountDownLatch(1);
+        for (int i = 0; i < 5; i++) {
+            new Thread(() -> {
+                try {
+                    //准备完毕……运动员都阻塞在这,等待号令
+                    countDownLatch.await();
+                    String parter = "【" + Thread.currentThread().getName() + "】";
+                    System.out.println(parter + "开始执行……");
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }).start();
+        }
+        Thread.sleep(2000);// 裁判准备发令
+        countDownLatch.countDown();// 发令枪:执行发令
+    }
+}
+

CountDownLatch是通过一个计数器来实现的,计数器的初始值为线程的数量; +可以通过CountDownLatch的构造函数,可以指定,不能小于0:

+
    public CountDownLatch(int count) {
+        if (count < 0) throw new IllegalArgumentException("count < 0");
+        this.sync = new Sync(count);
+    }
+

每次调用countDown()方法可以让计数器减1,底层是AQS框架,这里就不写了。 +调用了await()进行阻塞等待的线程,当计数器减到0后,再执行await()之后的代码。

+

CyclicBarrier

+

参考文章:

+ +

Cyclic Barrier直译为:循环屏障,是Java中关于线程的计数器,也可以叫它栅栏。

+

它与CountDownLatch的作用是相反的,CountDownLatch是定义一个次数,然后减,直到减到0,在去执行一些任务; +而CyclicBarrier是定义一个上限次数,然后从零开始加,直到加到定义的上限次数,在去执行一些任务。

+
+

CyclicBarrier与CountDownLatch作用是相反的,CountDownLatch的计数器只能使用一次,而CyclicBarrier的计数器可以使用reset()方法重置,可以使用多次,所以CyclicBarrier能够处理更为复杂的场景。

+
+

CyclicBarrierJDK文档注释:

+
+

A synchronization aid that allows a set of threads to all wait for each other to reach a common barrier point. CyclicBarriers are useful in programs involving a fixed sized party of threads that must occasionally wait for each other. The barrier is called cyclic because it can be re-used after the waiting threads are released.

+
+

大意:一种同步辅助工具,允许一组线程相互等待到达一个共同的障碍点。cyclicbarrier在包含固定大小的线程组的程序中非常有用,这些线程必须偶尔相互等待。 +这个屏障被称为cyclic·,因为它可以在等待的线程被释放后被重用。

+

它要做的事情是,让一组线程达到一个屏障(同步点)时被阻塞,直到最后一个线程达到屏障时,所有被屏障拦截的线程才会继续干活线程进入屏障通过CyclicBarrier.await()方法。

+

简单说就是让一组线程相互等待,当达到一个共同点时,所有之前等待的线程再继续执行,且 CyclicBarrier 功能可重复使用。

+

CyclicBarrier

+

例如,凑齐七颗龙珠召唤神龙:

+
public class MainTest {
+    public static void main(String[] args) {
+        CyclicBarrier cyclicBarrier = new CyclicBarrier(7,() -> {
+            System.out.println("凑齐七颗龙珠,召唤神龙!");
+        });
+        for (int i = 1; i <= 7;i++){
+            new Thread(() -> {
+                System.out.println("拿到"+Thread.currentThread().getName() + "星龙珠");
+                try {
+                    cyclicBarrier.await();
+                } catch (InterruptedException | BrokenBarrierException e) {
+                    e.printStackTrace();
+                }
+            },String.valueOf(i)).start();
+        }
+    }
+}
+

CyclicBarrier原理简单说明:

+

CyclicBarrier 是基于 ReentrantLock 实现的,其底层也是基于 AQS 的。

+

CyclicBarrier 类的内部有一个计数器 count,当 count 不为 0 时,每个线程在到达屏障点会先调用 await 方法将自己阻塞,此时计数器会减 1,直到计数器减为 0 的时候,所有因调用 await 方法而被阻塞的线程就会被唤醒继续执行。 +当 count 计数器变成 0 之后,就会进入下一轮阻塞,此时 parties(parties 是在 new CyclicBarrier(parties) 时设置的值)会将它的值赋值给 count 从而实现复用。

+

CyclicBarrier内部使用了ReentrantLockCondition两个类。它有两个构造函数:

+
public CyclicBarrier(int parties) {
+    this(parties, null);
+}
+ 
+public CyclicBarrier(int parties, Runnable barrierAction) {
+    if (parties <= 0) throw new IllegalArgumentException();
+    this.parties = parties;
+    this.count = parties;
+    this.barrierCommand = barrierAction;
+}
+

调用await方法的线程告诉CyclicBarrier已经到达同步点,然后当前线程被阻塞。 +直到达到定义上限个数的线程都到达了屏障;

+

参与线程调用了await方法,CyclicBarrier同样提供带超时时间的await和不带超时时间的await方法:

+
public int await() throws InterruptedException, BrokenBarrierException {
+    try {
+        // 不超时等待
+        return dowait(false, 0L);
+    } catch (TimeoutException toe) {
+        throw new Error(toe); // cannot happen
+    }
+}
+
+public int await(long timeout, TimeUnit unit)
+    throws InterruptedException,
+            BrokenBarrierException,
+            TimeoutException {
+    return dowait(true, unit.toNanos(timeout));
+}
+

这两个方法最终都会调用dowait(boolean, long)方法,它也是CyclicBarrier的核心方法:

+
private int dowait(boolean timed, long nanos)
+    throws InterruptedException, BrokenBarrierException,
+            TimeoutException {
+    // 获取独占锁
+    final ReentrantLock lock = this.lock;
+    lock.lock();
+    try {
+        // 当前代
+        final Generation g = generation;
+        // 如果这代损坏了,抛出异常
+        if (g.broken)
+            throw new BrokenBarrierException();
+ 
+        // 如果线程中断了,抛出异常
+        if (Thread.interrupted()) {
+            // 将损坏状态设置为true
+            // 并通知其他阻塞在此栅栏上的线程
+            breakBarrier();
+            throw new InterruptedException();
+        }
+ 
+        // 获取下标
+        int index = --count;
+        // 如果是 0,说明最后一个线程调用了该方法
+        if (index == 0) {  // tripped
+            boolean ranAction = false;
+            try {
+                final Runnable command = barrierCommand;
+                // 执行栅栏任务
+                if (command != null)
+                    command.run();
+                ranAction = true;
+                // 更新一代,将count重置,将generation重置
+                // 唤醒之前等待的线程
+                nextGeneration();
+                return 0;
+            } finally {
+                // 如果执行栅栏任务的时候失败了,就将损坏状态设置为true
+                if (!ranAction)
+                    breakBarrier();
+            }
+        }
+ 
+        // loop until tripped, broken, interrupted, or timed out
+        for (;;) {
+            try {
+                 // 如果没有时间限制,则直接等待,直到被唤醒
+                if (!timed)
+                    trip.await();
+                // 如果有时间限制,则等待指定时间
+                else if (nanos > 0L)
+                    nanos = trip.awaitNanos(nanos);
+            } catch (InterruptedException ie) {
+                // 当前代没有损坏
+                if (g == generation && ! g.broken) {
+                    // 让栅栏失效
+                    breakBarrier();
+                    throw ie;
+                } else {
+                    // 上面条件不满足,说明这个线程不是这代的
+                    // 就不会影响当前这代栅栏的执行,所以,就打个中断标记
+                    Thread.currentThread().interrupt();
+                }
+            }
+ 
+            // 当有任何一个线程中断了,就会调用breakBarrier方法
+            // 就会唤醒其他的线程,其他线程醒来后,也要抛出异常
+            if (g.broken)
+                throw new BrokenBarrierException();
+ 
+            // g != generation表示正常换代了,返回当前线程所在栅栏的下标
+            // 如果 g == generation,说明还没有换代,那为什么会醒了?
+            // 因为一个线程可以使用多个栅栏,当别的栅栏唤醒了这个线程,就会走到这里,所以需要判断是否是当前代。
+            // 正是因为这个原因,才需要generation来保证正确。
+            if (g != generation)
+                return index;
+            
+            // 如果有时间限制,且时间小于等于0,销毁栅栏并抛出异常
+            if (timed && nanos <= 0L) {
+                breakBarrier();
+                throw new TimeoutException();
+            }
+        }
+    } finally {
+        // 释放独占锁
+        lock.unlock();
+    }
+}
+

dowait方法作用,如果该线程不是最后一个调用await方法的线程,则它会一直处于等待状态,除非发生以下情况:

+
    +
  • 最后一个线程到达,即index == 0;
  • +
  • 某个参与线程等待超时;
  • +
  • 某个参与线程被中断;
  • +
  • 调用了CyclicBarrier的reset()方法。该方法会将屏障重置为初始状态;
  • +
+

Semaphore

+

参考文章:

+ +

Semaphore译为信号量,有时被称为信号灯。可以用来控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

+
+

信号量主要用于两个目的,一个是用于多个共享资源的互斥使用,另一个用于并发线程数量的控制。

+
+

SemaphoreJDK文档注释:

+
+

A counting semaphore. Conceptually, a semaphore maintains a set of permits. Each {@link #acquire} blocks if necessary until a permit is available, and then takes it. Each {@link #release} adds a permit, potentially releasing a blocking acquirer.

+
+

大意:计数信号量。从概念上讲,信号量维护一组许可。如果需要,每个{@link #acquire}块,直到有一个许可可用,然后获取它。 +每个{@link #release}添加一个许可,可能释放一个阻塞的获取者。 +但是,没有实际的permit对象被使用;{@code Semaphore}只保留可用数量的计数,并相应地执行操作。

+

简单理解,使用acquire方法获取一个令牌(许可),进入堵塞状态,使用release方法则释放一个令牌(许可)唤醒一个堵塞的线程。

+

举个例子,抢车位,九辆车抢三个车位,车位满了之后只有等里面的车离开停车场外面的车才可以进入:

+
public class MainTest {
+    public static void main(String[] args) {
+        
+        Semaphore semaphore = new Semaphore(3);
+        
+        for (int i = 1; i <= 9; i++) {
+            new Thread(() -> {
+                try {
+                    semaphore.acquire();
+                    System.out.println("第" + Thread.currentThread().getName() + "辆车,抢到车位");
+                    Thread.sleep(2000);
+                    System.out.println("停车结束.");
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }finally {
+                    semaphore.release();
+                }
+            }, String.valueOf(i)).start();
+        }
+        
+    }
+}
+

Semaphore有两个构造方法,可以通过其中一个构造方法来指定锁的类型,是公平锁还是非公平锁:

+
    // 设置令牌(许可)数量
+    public Semaphore(int permits) {
+        sync = new NonfairSync(permits);
+    }
+
+    // 可以设置锁的类型,是否是公平锁
+    public Semaphore(int permits, boolean fair) {
+        sync = fair ? new FairSync(permits) : new NonfairSync(permits);
+    }
+

Semaphore的底层也用到了AQS

+

Semaphore 是用来保护一个或者多个共享资源的访问,Semaphore 内部维护了一个计数器,其值为可以访问的共享资源的个数。 +一个线程要访问共享资源,先获得信号量,如果信号量的计数器值大于1,意味着有共享资源可以访问,则使其计数器值减去1,再访问共享资源。

+

如果计数器值为0,线程进入休眠。当某个线程使用完共享资源后,释放信号量,并将信号量内部的计数器加1,之前进入休眠的线程将被唤醒并再次试图获得信号量。

+

当调用semaphore.acquire()方法时:

+
    +
  • 当前线程会尝试去同步队列获取一个令牌,获取令牌的过程也就是使用原子操作去修改同步队列的state ,获取一个令牌则修改为state=state-1;
  • +
  • 当计算出来的state<0,则代表令牌数量不足,此时会创建一个Node节点加入阻塞队列,挂起当前线程;
  • +
  • 当计算出来的state>=0,则代表获取令牌成功;
  • +
+

当调用semaphore.release()方法时:

+
    +
  • 线程会尝试释放一个令牌,释放令牌的过程也就是把同步队列的state修改为state=state+1的过程;
  • +
  • 释放令牌成功之后,同时会唤醒同步队列中的一个线程;
  • +
  • 被唤醒的节点会重新尝试去修改state=state-1的操作,如果state>=0则获取令牌成功,否则重新进入阻塞队列,挂起线程;
  • +
+

synchronized

+

synchronized是Java提供的关键字,可译为同步。可用来给对象、方法或者代码块加锁,当它锁定一个方法或者一个代码块的时候,同一时刻最多只有一个线程执行这段代码。

+
+

synchronized关键字在需要原子性、可见性和有序性这三种特性的时候都可以作为其中一种解决方案,看起来是“万能”的。的确,大部分并发控制操作都能使用synchronized来完成。

+
+

使用

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
修饰的对象作用范围作用对象
同步一个实例方法整个实例方法调用此方法的对象
同步一个静态方法整个静态方法此类的所有对象
同步代码块-对象整个代码块调用此代码块的对象
同步代码块-类整个代码块此类的所有对象
+
同步一个方法
+

代码演示

+
public class MainTest {
+
+    //共享资源
+    static int i = 0;
+
+    public static void main(String[] args) throws InterruptedException {
+        MainTest mainTest = new MainTest();
+        Thread thread1 = new Thread(() -> {
+            for (int j = 0; j < 1000000; j++) {
+                mainTest.increase();
+            }
+        }, "线程1");
+        Thread thread2 = new Thread(() -> {
+            for (int j = 0; j < 1000000; j++) {
+                mainTest.increase();
+            }
+        }, "线程2");
+
+        thread1.start();
+        thread2.start();
+
+        // join方法的作用是调用线程等待该线程完成后,才能继续用下运行。
+        thread1.join();
+        thread2.join();
+        System.out.println(i);
+    }
+
+    public synchronized void increase() {
+        i++;
+    }
+
+    // 通过是否使用synchronized来体会
+//    public  void increase() {
+//        i++;
+//    }
+}
+

对于上面的代码如果加上synchronized最后输出的结果为2000000; +如果没有加,最后的结果很大程度上是小于2000000的,当然不排除偶然情况,所以这里不是肯定句。

+

由此可见,当某个线程运行到这个方法时,都要检查有没有其它线程正在用这个方法(或者该类的其他同步方法),有的话要等待正在使用 synchronized 方法的线程运行完这个方法后再运行此线程,没有的话,锁定调用者,然后直接运行。

+
同步一个静态方法
+

synchronized 作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。

+

需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象; +因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

+

代码演示

+
public class MainTest {
+
+    //共享资源
+    static int i = 0;
+
+    public static void main(String[] args) throws InterruptedException {
+        MainTest mainTest = new MainTest();
+        Thread thread1 = new Thread(() -> {
+            for (int j = 0; j < 1000000; j++) {
+//                increase();
+                mainTest.increaseNoneStatic();
+            }
+        }, "线程1");
+        Thread thread2 = new Thread(() -> {
+            for (int j = 0; j < 1000000; j++) {
+//                increase();
+//                mainTest.increaseNoneStatic();
+            }
+        }, "线程2");
+
+        thread1.start();
+        thread2.start();
+
+        // join方法的作用是调用线程等待该线程完成后,才能继续用下运行。
+        thread1.join();
+        thread2.join();
+        System.out.println(i);
+    }
+
+    // static修饰 锁住的是类对象
+    public static synchronized void increase() {
+        i++;
+    }
+
+    // 无static修饰 锁住的是调用该方法的 当前对象
+    public synchronized void increaseNoneStatic() {
+        i++;
+    }
+}
+

同步一个静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(static表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。 +所以如果一个线程A调用一个实例对象的非静态synchronized方法,而线程B需要调用这个实例对象所属类的静态synchronized方法,是允许的,不会发生互斥现象,因为访问静态synchronized方法占用的锁是当前类的锁,而访问非静态synchronized方法占用的锁是当前实例对象锁。

+
同步代码块
+

在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,这样做就有点浪费; +此时我们可以使用同步代码块的方式对需要同步的代码进行包裹。

+

代码演示

+
public class MainTest {
+
+    //共享资源
+    static int i = 0;
+
+    public static void main(String[] args) throws InterruptedException {
+        MainTest mainTest = new MainTest();
+        Thread thread1 = new Thread(() -> {
+            for (int j = 0; j < 1000000; j++) {
+                mainTest.increase();
+            }
+        }, "线程1");
+        Thread thread2 = new Thread(() -> {
+            for (int j = 0; j < 1000000; j++) {
+                mainTest.increase();
+            }
+        }, "线程2");
+
+        thread1.start();
+        thread2.start();
+
+        // join方法的作用是调用线程等待该线程完成后,才能继续用下运行。
+        thread1.join();
+        thread2.join();
+        System.out.println(i);
+    }
+
+    public  void increase() {
+        synchronized (this){
+            i++;
+        }
+    }
+
+//    public  void increase() {
+//        i++;
+//    }
+
+}
+

除了使用synchronized (this)锁定,当然静态方法是没有this对象的;也可以使用class对象,和程序中创建的一些对象来做为锁。

+
// class类对象锁
+synchronized(MainTest.class){
+    // ...
+}
+
+// 
+

当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的对象来充当锁;

+
private byte[] lock = new byte[0];
+public void method(){
+  synchronized(lock) {
+     // .....
+  }
+}
+

零长度的byte数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的byte[]对象只需3条操作码,而Object lock = new Object()则需要7行操作码。

+

当一个线程访问对象的一个synchronized(this)同步代码块时,另一个线程仍然可以访问该对象中的非synchronized(this)同步代码块。

+
public class MainTest {
+    public static void main(String[] args) {
+        Counter counter = new Counter();
+        Thread thread1 = new Thread(counter, "A");
+        Thread thread2 = new Thread(counter, "B");
+        thread1.start();
+        thread2.start();
+    }
+}
+
+class Counter implements Runnable{
+    private int count;
+
+    public Counter() {
+        count = 0;
+    }
+
+    public void countAdd() {
+        synchronized(this) {
+            for (int i = 0; i < 5; i ++) {
+                try {
+                    System.out.println(Thread.currentThread().getName() + ":" + (count++));
+                    Thread.sleep(100);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+
+    //非synchronized代码块,未对count进行读写操作,所以可以不用synchronized
+    public void printCount() {
+        for (int i = 0; i < 5; i ++) {
+            try {
+                System.out.println(Thread.currentThread().getName() + " count:" + count);
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    @Override
+    public void run() {
+        String threadName = Thread.currentThread().getName();
+        if (threadName.equals("A")) {
+            countAdd();
+        } else if (threadName.equals("B")) {
+            printCount();
+        }
+    }
+}
+

原理

+

参考文章:

+ +

阅读前建议先了解Java对象头。 +如果你对对象头有了解,你就知道在Java中synchronized锁对象时,其实就是改变对象中的对象头的markword的锁的标志位来实现的。

+

通过上面的使用,可以体会到被synchronized修饰的代码块及方法,在同一时间,只能被单个线程访问。

+

javap -v MainTest.class 命令反编译下面代码,我们就能了解到JVM对synchronized是怎么处理的了。

+
public class MainTest {
+
+    synchronized void demo01() {
+        System.out.println("demo 01");
+    }
+
+    void demo02() {
+        synchronized (MainTest.class) {
+            System.out.println("demo 02");
+        }
+    }
+
+}
+
  synchronized void demo01();
+    descriptor: ()V
+    flags: ACC_SYNCHRONIZED
+    Code:
+      stack=2, locals=1, args_size=1
+         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
+         3: ldc           #3                  // String demo 01
+         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
+         8: return
+// ...
+void demo02();
+    descriptor: ()V
+    flags:
+    Code:
+      stack=2, locals=3, args_size=1
+         0: ldc           #5                  // class content/posts/rookie/MainTest
+         2: dup
+         3: astore_1
+         4: monitorenter
+         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
+         8: ldc           #6                  // String demo 02
+        10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
+        13: aload_1
+        14: monitorexit
+        15: goto          23
+        18: astore_2
+        19: aload_1
+        20: monitorexit
+        21: aload_2
+        22: athrow
+        23: return
+// ...
+

通过反编译后代码可以看出:

+
    +
  • 对于同步方法,JVM采用ACC_SYNCHRONIZED标记符来实现同步;
  • +
  • 对于同步代码块,JVM采用monitorentermonitorexit两个指令来实现同步;
  • +
+

其中同步代码块,有两个monitorexit指令的原因是,为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。

+

官方文档中关于同步方法和同步代码块的实现原理描述

+
+

方法级的同步是隐式的。同步方法的常量池中会有一个 ACC_SYNCHRONIZED 标志。当某个线程要访问某个方法的时候,会检查是否有 ACC_SYNCHRONIZED,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行之后再释放监视器锁。这时如果其他线程来请求执行方法,会因为无法获得监视器锁而被阻断住。值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

+
+
+

同步代码块使用 monitorentermonitorexit 两个指令实现。可以把执行 monitorenter 指令理解为加锁,执行 monitorexit 理解为释放锁。 每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为0,当一个线程获得锁(执行 monitorenter)后,该计数器自增变为 1 ,当同一个线程再次获得该对象的锁的时候,计数器再次自增。当同一个线程释放锁(执行 monitorexit 指令)的时候,计数器再自减。当计数器为0的时候。锁将被释放,其他线程便可以获得锁。

+
+

其实无论是ACC_SYNCHRONIZED还是monitorentermonitorexit都是基于Monitor实现的,每一个锁都对应一个monitor对象; +在Java虚拟机(HotSpot)中,Monitor 是基于C++实现的,由ObjectMonitor实现。

+

/hotspot/src/share/vm/runtime/objectMonitor.hpp中有ObjectMonitor的实现

+
// initialize the monitor, exception the semaphore, all other fields
+// are simple integers or pointers
+ObjectMonitor() {
+    _header       = NULL;
+    _count        = 0; //记录个数
+    _waiters      = 0,
+    _recursions   = 0;
+    _object       = NULL;
+    _owner        = NULL;
+    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
+    _WaitSetLock  = 0 ;
+    _Responsible  = NULL ;
+    _succ         = NULL ;
+    _cxq          = NULL ;
+    FreeNext      = NULL ;
+    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
+    _SpinFreq     = 0 ;
+    _SpinClock    = 0 ;
+    OwnerIsThread = 0 ;
+  }
+
    +
  • _owner:指向持有ObjectMonitor对象的线程
  • +
  • _WaitSet:存放处于wait状态的线程队列
  • +
  • _EntryList:存放处于等待锁block状态的线程队列
  • +
  • _recursions:锁的重入次数
  • +
  • _count:用来记录该线程获取锁的次数
  • +
+

当多个线程同时访问一段同步代码时,首先会进入_EntryList队列中,当某个线程获取到对象的monitor后进入_Owner区域并把monitor中的_owner变量设置为当前线程,同时monitor中的计数器_count加1。即获得对象锁。

+

synchronized原理

+

若此时持有monitor的线程调用wait()方法,将释放当前对象持有的monitor_owner变量恢复为null_count自减1,同时该线程进入_WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor并复位变量的值,以便其他线程进入获取monitor

+

由此看来,monitor对象存在于每个Java对象的对象头中(存储的是指针),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因。

+

ObjectMonitor中其他方法:

+
  bool      try_enter (TRAPS) ;
+  void      enter(TRAPS);
+  void      exit(bool not_suspended, TRAPS);
+  void      wait(jlong millis, bool interruptable, TRAPS);
+  void      notify(TRAPS);
+  void      notifyAll(TRAPS);
+

sychronized加锁的时候,会调用objectMonitorenter方法,解锁的时候会调用exit方法。 +在JDK1.6之前,synchronized 的实现才会直接调用 ObjectMonitorenterexit,这种锁被称之为重量级锁。

+
+

早期的synchronized效率低的原因: +Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,监视器锁monitor是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态。因此状态转换需要花费很多的处理器时间。 +对于代码简单的同步块(如被synchronized修饰的getset方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说synchronized是java语言中一个重量级的操作。也是为什么早期的synchronized效率低的原因。

+
+

所以,在JDK1.6中出现对锁进行了很多的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化。

+

锁的升级

+

参考文章:

+ +

在JDK1.6之前,使用synchronized被称作重量级锁,重量级锁的实现是基于底层操作系统的mutex互斥原语的,这个开销是很大的。所以在JDK1.6时JVM对synchronized做了优化。

+

对象头中markword锁状态的表示:

+
+
    +
  • biased_lock :0 lock: 01: 表示无锁状态
  • +
  • biased_lock :1 lock: 01: 表示偏向锁状态
  • +
  • lock: 00: 表示轻量级锁状态
  • +
  • lock: 10: 表示重量级锁状态
  • +
  • lock: 11: 表示被垃圾回收器标记的状态
  • +
+
+

对象的锁状态,可以分为4种,级别从低到高依次是:无锁状态、偏向锁状态轻量级锁状态重量级锁状态。 +其中这几个锁只有重量级锁是需要使用操作系统底层mutex互斥原语来实现,其他的锁都是使用对象头来实现的。 +随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁。

+

锁升级过程:

+
    +
  • 无锁状态:markword锁的标志位0,偏向锁的标志位为1;例如:刚被创建出来的对象;
  • +
  • 偏向锁:如果一个线程获取了锁,此时markword的结构变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,直接可以获取锁。 +省去了大量有关锁申请的操作,从而也就提供程序的性能。
  • +
  • 轻量级锁:当锁是偏向锁的时候,被另外的线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,从而提高性能;
  • +
  • 重量级锁:升级为重量级锁时,锁标志的状态值变为“10”,此时MarkWord中存储的是指向重量级锁的指针,此时等待锁的线程都会进入阻塞状态。所以开销是很大;
  • +
+
    +
  1. +

    无锁状态升级为偏向锁: +一个对象刚开始实例化的时候,没有任何线程来访问它的时候。它是可偏向的,意味着,它现在认为只可能有一个线程来访问它,所以当第一个线程来访问它的时候,它会偏向这个线程,此时,对象持有偏向锁。 +偏向第一个线程,这个线程在修改对象头成为偏向锁的时候使用CAS操作,并将对象头中的 ThreadID 改成自己的ID,之后再次访问这个对象时,只需要对比ID,不需要再使用CAS在进行操作。

    +
  2. +
  3. +

    偏向锁升级为轻量级锁: +一旦有第二个线程访问这个对象,因为偏向锁不会主动释放,所以第二个线程可以看到对象的偏向状态; +这时表明在这个对象上已经存在竞争了,JVM会检查原来持有该对象锁的线程是否依然存活,如果不存活,则可以将对象变为无锁状态,然后重新偏向新的线程,如果原来的线程依然存活,则马上执行那个线程的操作栈,检查该对象的使用情况,如果仍然需要持有偏向锁,则偏向锁升级为轻量级锁。 +如果不存在使用了,则可以将对象回复成无锁状态,然后重新偏向。

    +
  4. +
  5. +

    轻量级锁升级为重量级锁: +轻量级锁认为竞争存在,但是竞争的程度很轻,一般两个线程对于同一个锁的操作都会错开,或者说稍微等待一下(自旋),另一个线程就会释放锁。 +但是当自旋超过一定的次数,或者一个线程在持有锁,一个在自旋,又有第三个来访时,轻量级锁膨胀为重量级锁,重量级锁使除了拥有锁的线程以外的线程都阻塞,防止CPU空转。

    +
  6. +
+

在所有的锁都启用的情况下线程进入临界区时会先去获取偏向锁,如果已经存在偏向锁了,则会尝试获取轻量级锁,启用自旋锁,如果自旋也没有获取到锁,则使用重量级锁,将没有获取到锁的线程阻塞挂起,直到持有锁的线程执行完同步块唤醒他们;

+

偏向锁是在无锁争用的情况下使用的,也就是同步代码块在当前线程没有执行完之前,没有其它线程会执行该同步块,一旦有了第二个线程的争用,偏向锁就会升级为轻量级锁,如果轻量级锁自旋到达阈值后,没有获取到锁,就会升级为重量级锁;

+

锁可以升级,但是不可以降级。

+
+

PS:有的观点认为 Java 不会进行锁降级。 +实际上,锁降级确实是会发生的,当 JVM 进入安全点SafePoint)的时候,会检查是否有闲置的 Monitor,然后试图进行降级。

+
+

HotSpot 虚拟机中是有锁降级的,但是仅仅只发生在 STW 的时候,只有垃圾回收线程能够观测到它,也就是说,在我们正常使用的过程中是不会发生锁降级的,只有在 GC 的时候才会降级。

+

synchronized与可见性

+

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

+

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。 +不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。所以,就可能出现线程1改了某个变量的值,但是线程2不可见的情况。 +被synchronized修饰的代码,在开始执行时会加锁,执行完成后会进行解锁。而为了保证可见性,有一条规则是这样的:对一个变量解锁之前,必须先把此变量同步回主存中。这样解锁后,后续线程就可以访问到被修改后的值。

+

所以,synchronized关键字锁住的对象,其值是具有可见性的.

+

synchronized与原子性

+

原子性是指一个操作是不可中断的,要全部执行完成,要不就都不执行。

+

线程是CPU调度的基本单位。CPU有时间片的概念,会根据不同的调度算法进行线程调度。当一个线程获得时间片之后开始执行,在时间片耗尽之后,就会失去CPU使用权。所以在多线程场景下,由于时间片在线程间轮换,就会发生原子性问题。

+

在Java中,为了保证原子性,提供了两个高级的字节码指令monitorentermonitorexit。 +这两个字节码指令,在Java中对应的关键字就是synchronized

+

通过下monitorexitmonitorexit指令,可以保证被synchronized修饰的代码在同一时间只能被一个线程访问,在锁未释放之前,无法被其他线程访问到。因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。

+
+

例如: 线程1在执行monitorenter指令的时候,会对Monitor进行加锁,加锁后其他线程无法获得锁,除非线程1主动解锁。 +即使在执行过程中,由于某种原因,比如CPU时间片用完,线程1放弃了CPU,但是,他并没有进行解锁。 +而由于synchronized的锁是可重入的,下一个时间片还是只能被他自己获取到,还是会继续执行代码。直到所有代码执行完。这就保证了原子性。

+
+

synchronized与有序性

+

有序性即程序执行的顺序按照代码的先后顺序执行。

+

除了引入了时间片以外,由于处理器优化和指令重排等,CPU还可能对输入代码进行乱序执行,比如load->add->save 有可能被优化成load->save->add这就是可能存在有序性问题。

+

这里需要注意的是,synchronized是无法禁止指令重排和处理器优化的。也就是说,synchronized无法避免上述提到的问题。

+

那么,为什么还说synchronized也提供了有序性保证呢?

+
+

如果在本线程内观察,所有操作都是天然有序的。如果在一个线程中观察另一个线程,所有操作都是无序的。

+
+

以上这句话也是,但是怎么理解呢?简单扩展一下,这其实和as-if-serial语义有关。

+

as-if-serial语义的意思指:不管怎么重排序(编译器和处理器为了提高并行度),单线程程序的执行结果都不能被改变。 +编译器和处理器无论如何优化,都必须遵守as-if-serial语义。

+

这里不对as-if-serial语义详细展开了,简单说就是as-if-serial语义保证了单线程中,指令重排是有一定的限制的,而只要编译器和处理器都遵守了这个语义,那么就可以认为单线程程序是按照顺序执行的。 +当然,实际上还是有重排的,只不过我们无须关心这种重排的干扰。

+

所以呢,由于synchronized修饰的代码,同一时间只能被同一线程访问。那么也就是单线程执行的。所以,可以保证其有序性。

+

synchronized与ReentrantLock

+

Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问; +第一个是 JVM 实现的 synchronized,而另一个是 JDK 实现的 ReentrantLock

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
比较synchronizedReentrantLock
锁的实现JVM 实现,监视器模式JDK实现,依赖AQS
性能新版本 Java 对 synchronized 进行锁的升级synchronized 与 ReentrantLock 大致相同
等待可中断不可中断可中断
公平锁非公平锁默认非公平锁,也可以是公平锁
锁绑定多个条件不能绑定可以同时绑定多个 Condition 对象
可重入可重入锁可重入锁
释放锁自动释放锁调用 unlock() 释放锁
等待唤醒搭配wait()、notify或notifyAll()使用搭配await()/singal()使用
+

synchronizedReentrantLock最直观的区别就是,在使用ReentrantLock的时候需要调用unlock方法释放锁,所以为了保证一定释放,通常都是和 try~finally 配合使用的。

+

除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。 +这是因为 synchronized 是 JVM 实现的一种锁机制,JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。 +并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。

+

ThreadLocal

+

参考文章:

+ +

ThreadLocal文档注释:

+
+

This class provides thread-local variables. These variables differ from +their normal counterparts in that each thread that accesses one (via its +{@code get} or {@code set} method) has its own, independently initialized +copy of the variable.

+
+

大意:这个类提供线程局部变量。这些变量与普通变量的不同之处在于,每个访问它们的线程(通过其get方法或set方法)都有自己的独立初始化的变量副本。

+

如文档注释所说,ThraedLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

+
+

从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。

+
+

说白了ThreadLocal就是存放线程的局部变量的。

+

使用

+

在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()

+
+

关于Object和T的区别:Object是个基类,是个真实存在的类;T是个占位符,表示某个具体的类,仅在编译器有效,最终会被擦除用Object代替。

+
+

主要方法:

+
// 返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。
+// 如果有人心急则吃不了热豆腐,在还没有set的情况下,调用get则返回null。
+protected T initialValue()
+
+// 该方法返回当前线程所对应的线程局部变量
+public T get()
+
+// 设置当前线程的线程局部变量的值
+public void set(T value)
+
+// 将当前线程局部变量的值删除,目的是为了减少内存的占用
+public void remove()
+

ThreadLocal里设置的值,只有当前线程自己看得见:

+
public class MainTest {
+
+    private static ThreadLocal<Integer> localInt = new ThreadLocal<>();
+
+    public static void main(String[] args) {
+        localInt.set(100);
+
+        new Thread(() -> {
+            localInt.set(200);
+            System.out.println("-----thead1-----");
+            System.out.println(context0());
+            System.out.println(context1());
+            System.out.println(context2());
+        },"thread1").start();
+
+        System.out.println("-----main-----");
+        System.out.println(context0());
+        System.out.println(context1());
+        System.out.println(context2());
+    }
+
+
+    static int context0() {
+        return localInt.get();
+    }
+
+    static int context1(){
+        return localInt.get();
+    }
+
+    static int context2(){
+        return localInt.get();
+    }
+}
+

由于ThreadLocal里设置的值,只有当前线程自己看得见,这意味着你不可能通过其他线程为它初始化值。 +为了弥补这一点,ThreadLocal提供了一个withInitial()方法统一初始化所有线程的ThreadLocal的值:

+
public class MainTest {
+
+    private static final ThreadLocal<Integer> localInt = ThreadLocal.withInitial(() -> 300);
+
+    public static void main(String[] args) {
+
+        new Thread(() -> {
+            System.out.println("-----thead1-----");
+            System.out.println(context0());
+            System.out.println(context1());
+            System.out.println(context2());
+        },"thread1").start();
+
+        System.out.println("-----main-----");
+        System.out.println(context0());
+        System.out.println(context1());
+        System.out.println(context2());
+    }
+
+
+    static int context0() {
+        return localInt.get();
+    }
+
+    static int context1(){
+        return localInt.get();
+    }
+
+    static int context2(){
+        return localInt.get();
+    }
+
+}
+

通过上面的代码,可以发现ThreadLocal是跨越几个方法的。为了在几个函数之间共用一个变量,所以才出现:线程变量,这种变量在Java中就是ThreadLocal变量。

+

ThreadLocal是跨函数的,虽然全局变量也是跨函数的,但是跨所有的函数,而且不是动态的。跨哪些函数是由线程来定的,所以更灵活。

+

总之,ThreadLocal类是修饰变量的,是在控制它的作用域,是为了增加变量的种类而已,这才是ThreadLocal类诞生的初衷,它的初衷可不是解决线程冲突的。

+

与同步机制

+

ThreadLocal类是修饰变量的,重点是在控制变量的作用域,初衷可不是为了解决线程并发和线程冲突的,而是为了让变量的种类变的更多更丰富,方便人们使用罢了。 +很多开发语言在语言级别都提供这种作用域的变量类型。

+
+

要保证线程安全,并不一定就是要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段。 +如果一个方法本来就不涉及共享数据,那它自然就无需任何同步措施去保证正确性。

+
+

总之,线程安全,并不一定就是要进行同步,ThreadLocal目的是线程安全,但不是同步手段。

+

ThreadLocal和线程同步机制都可以解决多线程中共享变量的访问冲突问题。

+

在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

+

ThreadLocal 则从另一个角度来解决多线程的并发访问。ThreadLocal 会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。 +ThreadLocal 提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进 ThreadLocal

+

虽然ThreadLocal能够保证多线程访问数据安全,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用 ThreadLocal 要大。

+

对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。 +前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

+

原理

+
    public T get() {
+        // 获取当前线程
+        Thread t = Thread.currentThread();
+        // 每个线程 都有一个自己的ThreadLocalMap,
+        // ThreadLocalMap里就保存着所有的ThreadLocal变量
+        ThreadLocalMap map = getMap(t);
+        if (map != null) {
+            //ThreadLocalMap的key就是当前ThreadLocal对象实例,
+            //多个ThreadLocal变量都是放在这个map中的
+            ThreadLocalMap.Entry e = map.getEntry(this);
+            if (e != null) {
+                @SuppressWarnings("unchecked")
+                //从map里取出来的值就是我们需要的这个ThreadLocal变量
+                T result = (T)e.value;
+                return result;
+            }
+        }
+        // 如果map没有初始化,那么在这里初始化一下
+        return setInitialValue();
+    }
+
+
+    public void set(T value) {
+        // 获取当前线程
+        Thread t = Thread.currentThread();
+        // 每个线程 都有一个自己的ThreadLocalMap
+        // ThreadLocalMap 里就保存着所有的ThreadLocal变量
+        ThreadLocalMap map = getMap(t);
+        if (map != null)
+            // 向map里添加值
+            map.set(this, value);
+        else
+            // map为null,创建一个 ThreadLocalMap
+            createMap(t, value);
+    }
+
+
+    // 全局定义的localMap
+   ThreadLocal.ThreadLocalMap threadLocals = null;
+
+    // 获取当前线程所持有的localMap
+    ThreadLocalMap getMap(Thread t) {
+        return t.threadLocals;
+    }
+    
+    // 创建,初始化 localMap 
+    void createMap(Thread t, T firstValue) {
+        t.threadLocals = new ThreadLocalMap(this, firstValue);
+    }
+

ThreadLocal,get()、set()源码中可以看出,所谓的ThreadLocal变量就是保存在每个线程的map中的。这个map就是Thread对象中的threadLocals字段。

+
ThreadLocal.ThreadLocalMap threadLocals = null;
+

首先,在每个线程 Thread 内部有一个 ThreadLocal.ThreadLocalMap 类型的成员变量 threadLocals,这个 threadLocals 就是用来存储实际的变量副本的,键值为当前 ThreadLocal 变量,value为变量副本,即T类型的变量。

+

初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLoca变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals

+

ThreadLocal.ThreadLocalMap是一个比较特殊的Map,它的每个Entry的key都是一个弱引用:

+
static class Entry extends WeakReference<ThreadLocal<?>> {
+    /** The value associated with this ThreadLocal. */
+    Object value;
+    //key就是一个弱引用
+    Entry(ThreadLocal<?> k, Object v) {
+        super(k);
+        value = v;
+    }
+}
+

这样设计的好处是,如果这个变量不再被其他对象使用时,可以自动回收这个ThreadLocal对象,避免可能的内存泄露

+

内存泄漏问题

+

虽然ThreadLocalMap中的key是弱引用,当不存在外部强引用的时候,就会自动被回收,但是Entry中的value依然是强引用。这个value的引用链条如下:

+
Thrad --> ThreadLocalMap --> Entry --> value
+

只有当Thread被回收时,这个value才有被回收的机会,否则,只要线程不退出,value总是会存在一个强引用。 +但是,要求每个Thread都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value对象出现泄漏的可能。 +处理的方法是,在ThreadLocalMap进行set(),get(),remove()的时候,都会进行清理:

+

remove()方法为例:

+
// public remove
+ public void remove() {
+     ThreadLocalMap m = getMap(Thread.currentThread());
+     if (m != null)
+         m.remove(this);
+ }
+
+// private remove
+private void remove(ThreadLocal<?> key) {
+    Entry[] tab = table;
+    int len = tab.length;
+    int i = key.threadLocalHashCode & (len-1);
+    for (Entry e = tab[i];
+         e != null;
+         e = tab[i = nextIndex(i, len)]) {
+        if (e.get() == key) {
+            e.clear();
+            expungeStaleEntry(i);
+            return;
+        }
+    }
+}
+// 核心方法
+private int expungeStaleEntry(int staleSlot) {
+    Entry[] tab = table;
+    int len = tab.length;
+
+    // expunge entry at staleSlot
+    tab[staleSlot].value = null;
+    tab[staleSlot] = null;
+    size--;
+
+    // Rehash until we encounter null
+    Entry e;
+    int i;
+    for (i = nextIndex(staleSlot, len);
+         (e = tab[i]) != null;
+         i = nextIndex(i, len)) {
+        ThreadLocal<?> k = e.get();
+        if (k == null) {
+            // 将 value 赋值为 null; help gc
+            e.value = null;
+            tab[i] = null;
+            size--;
+        } else {
+            int h = k.threadLocalHashCode & (len - 1);
+            if (h != i) {
+                tab[i] = null;
+
+                // Unlike Knuth 6.4 Algorithm R, we must scan until
+                // null because multiple entries could have been stale.
+                while (tab[h] != null)
+                    h = nextIndex(h, len);
+                tab[h] = e;
+            }
+        }
+    }
+    return i;
+}
+

虽然ThreadLocal为了避免内存泄露,花了一番大心思,但是并不能100%保证不发生内存泄漏。

+

比如,你的get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行,如果你没有机会调用set()remove(),那么这个内存泄漏依然会发生。 +所以,当你不需要这个ThreadLoca变量时,主动调用remove(),这样是能够避免内存泄漏的。

+

常用的线程安全的集合

+ + + + + + + + + + + + + + + + + + + + + +
线程不安全线程不安全解决方案
ArrayList使用Vector、Collections.synchronizedArrayList、CopyOnWriteArrayList
HashSet使用Collections.synchronizedSet、CopyOnWriteArraySet
HashMap使用HashTable、Collections.synchronizedMap、ConcurrentHashMap
+

ArrayList线程不安全

+

ArrayList线程不安全代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        ArrayList<String> arrayList = new ArrayList<>();
+        for(int i=0; i< 10; i++) {
+            new Thread(() -> {
+                arrayList.add(UUID.randomUUID().toString());
+                System.out.println(arrayList);
+            },String.valueOf(i)).start();
+        }
+    }
+}
+

为避免偶然事件,请重复多试几次上面的代码,很大情况会出现ConcurrentModificationException“同步修改异常”

+
java.util.ConcurrentModificationException
+

出现该异常的原因是,当某个线程正在执行 add()方法时,被某个线程打断,添加到一半被打断,没有被添加完。

+

解决ArrayList线程不安全问题

+
    +
  • 可以使用 Vector 来代替 ArrayList,Vector 是线程安全的 ArrayList,但是由于,并发量太小,被淘汰;
  • +
  • 使用 Collections.synchronizedArrayList() 来创建 ArrayList;使用 Collections 工具类来创建 ArrayList 的思路是,在 ArrayList 的外边套了一个synchronized外壳,来使 ArrayList 线程安全;
  • +
  • 使用 CopyOnWriteArrayList()来保证 ArrayList 线程安全;
  • +
+

下面详细说明CopyOnWriteArrayList();使用CopyOnWriteArrayList演示代码

+
public class MainTest {
+    public static void main(String[] args) {
+        CopyOnWriteArrayList<String> arrayList = new CopyOnWriteArrayList<>();
+        for(int i=0; i< 10; i++) {
+            new Thread(() -> {
+                arrayList.add(UUID.randomUUID().toString());
+                System.out.println(arrayList);
+            },String.valueOf(i)).start();
+        }
+    }
+}
+

CopyWriteArrayList原理

+

CopyWriteArrayList 字面意思就是在写的时候复制,思想就是读写分离的思想。以下是 CopyOnWriteArrayListadd() 方法源码

+
/** The array, accessed only via getArray/setArray. */
+    private transient volatile Object[] array;
+
+/** The lock protecting all mutators */
+    final transient ReentrantLock lock = new ReentrantLock();
+
+  /**
+     * Gets the array.  Non-private so as to also be accessible
+     * from CopyOnWriteArraySet class.
+     */
+    final Object[] getArray() {
+        return array;
+    }
+
+/**
+     * 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();
+        }
+    }
+

CopyWriteArrayList之所以线程安全的原因是在源码里面使用 ReentrantLock,所以保证了某个线程在写的时候不会被打断; +可以看到源码开始先是复制了一份数组(因为同一时刻只有一个线程写,其余的线程会读),在复制的数组上边进行写操作,写好以后在返回 true。 +这样写的就把读写进行了分离.写好以后因为 array 加了 volatile 关键字,所以该数组是对于其他的线程是可见的,就会读取到最新的值.

+

HashSet

+

HashSetArrayList 类似,也是线程不安全的集合类。代码演示线程不安全示例,与ArrayList类似

+
public class MainTest {
+    public static void main(String[] args) {
+        HashSet<String> set = new HashSet<>();
+        for(int i=0; i< 10; i++) {
+            new Thread(() -> {
+                set.add(UUID.randomUUID().toString());
+                System.out.println(set);
+            },String.valueOf(i)).start();
+        }
+    }
+}
+

也会报 java.util.ConcurrentModificationException 异常。

+

参照ArrayList解决方案,HashSet有两种解决方案:

+
    +
  • Collections.synchronizedSet()使用集合工具类解决;
  • +
  • 使用 CopyOnWriteArraySet()来保证集合线程安全;
  • +
+

使用 CopyOnWriteArraySet()代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        CopyOnWriteArraySet<String> set = new CopyOnWriteArraySet<>();
+        for(int i=0; i< 10; i++) {
+            new Thread(() -> {
+                set.add(UUID.randomUUID().toString());
+                System.out.println(set);
+            },String.valueOf(i)).start();
+        }
+    }
+}
+

CopyOnWriteArraySet底层调用的就是CopyOnWriteArrayList

+
private final CopyOnWriteArrayList<E> al;
+/**
+ * Creates an empty set.
+ */
+public CopyOnWriteArraySet() {
+    al = new CopyOnWriteArrayList<E>();
+}
+

参照CopyWriteArrayList原理

+

HashMap

+

HashMap 也是线程不安全的集合类; +在多线程环境下使用同样会出现java.util.ConcurrentModificationException

+
public class MainTest {
+    public static void main(String[] args) {
+        HashMap<String,Object> map = new HashMap<>();
+        for(int i=0; i< 10; i++) {
+            new Thread(() -> {
+                map.put(UUID.randomUUID().toString(),Thread.currentThread().getName());
+                System.out.println(map);
+            },String.valueOf(i)).start();
+        }
+    }
+}
+

再多线程环境下HashMap不仅会出现ConcurrentModificationException问题; +更严重的是,当多个线程中的 HashMap 同时扩容时,再使用put方法添加元素,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,CPU飙升到100%。

+

解决方案:

+
    +
  • 使用 HashTable来保证线程安全;
  • +
  • Collections.synchronizedMap() 使用集合工具类;
  • +
  • ConcurrentHashMap<>() 来保证线程安全;
  • +
+

上面的HashTableCollections.synchronizedMap()因为性能的原因,在多线程环境下很少使用,一般都会使用ConcurrentHashMap<>()

+

HashTable性能低的原因,就是直接加了synchronized修饰; +当使用put方法时,通过hash算法判断应该分配到哪一个数组上,如果分配到同一个数组上,即发生hash冲突,这个时候加锁是没问题的;但是一旦不发生hash冲突,再去加锁,性能就不太好了。

+

可理解为HashTable性能不好的原因就是锁的粒度太粗了。

+

HashTableput方法源码

+
public synchronized V put(K key, V value) {
+        // Make sure the value is not null
+        if (value == null) {
+            throw new NullPointerException();
+        }
+
+        // Makes sure the key is not already in the hashtable.
+        Entry<?,?> tab[] = table;
+        int hash = key.hashCode();
+        int index = (hash & 0x7FFFFFFF) % tab.length;
+        @SuppressWarnings("unchecked")
+        Entry<K,V> entry = (Entry<K,V>)tab[index];
+        for(; entry != null ; entry = entry.next) {
+            if ((entry.hash == hash) && entry.key.equals(key)) {
+                V old = entry.value;
+                entry.value = value;
+                return old;
+            }
+        }
+
+        addEntry(hash, key, value, index);
+        return null;
+    }
+

ConcurrentHashMap原理

+

ConcurrentHashMap原理简单理解为:HashMap + 分段锁。

+

因为HashMap在jdk1.7与jdk1.8结构上做了调整,所以ConcurrentHashMap在jdk1.7与jdk1.8结构上也有所不同。

+

在阅读之前建议掌握HashMap基本原理、CAS、synchronized、lock以及对多线程并发有一定了解。

+
jdk1.7ConcurrentHashMap
+

JDK1.7采用segment的分段锁机制实现线程安全,其中segment类继承自ReentrantLock。用ReentrantLock、CAS来保证线程安全。

+

jdk1.7ConcurrentHashMap

+

jdk1.7的ConcurrentHashMap结构:

+
    +
  • segment: 每一个segment数组就相当于一个HashMap
  • +
  • HashEntry: 等同于HashMapEntry,用于存放K,V键值对;
  • +
  • 节点:每个节点对应ConcurrentHashMap存放的值;
  • +
+

jdk1.7ConcurrentHashMap之所以能够保证线程安全,主要原因是在每个segment数组上加了锁,俗称分段锁,细化了锁的粒度。

+

jdk1.7ConcurrentHashMap.put方法源码

+
    public V put(K key, V value) {
+        Segment<K,V> s;
+        if (value == null)
+            throw new NullPointerException();
+        int hash = hash(key.hashCode());
+        int j = (hash >>> segmentShift) & segmentMask;
+        if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
+             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
+            s = ensureSegment(j);
+        return s.put(key, hash, value, false);
+    }
+

首先判空,计算hash值,计算put进来的元素分配到哪个segment数组上,判断当前segments数组上的元素是否为空,如果为空就会使用ensureSegment方法创建segment对象; +最后调用Segment.put方法,存放到对应的节点中。

+

Segment.ensureSegment方法源码

+
/**
+ * Returns the segment for the given index, creating it and
+ * recording in segment table (via CAS) if not already present.
+ *
+ * @param k the index
+ * @return the segment
+ */
+private Segment<K,V> ensureSegment(int k) {
+        final Segment<K,V>[] ss = this.segments;
+        long u = (k << SSHIFT) + SBASE; // raw offset
+        Segment<K,V> seg;
+        if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
+            Segment<K,V> proto = ss[0]; // use segment 0 as prototype
+            int cap = proto.table.length;
+            float lf = proto.loadFactor;
+            int threshold = (int)(cap * lf);
+            HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
+            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
+                == null) { // recheck
+                Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
+                while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
+                       == null) {
+                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
+                        break;
+                }
+            }
+        }
+        return seg;
+    }
+

通过文档注释可以看到ensureSegment方法作用

+
+

返回指定索引的segment对象,通过CAS判断,如果还没有则创建它并记录在segment表中。

+
+

当多个线程同时执行该方法,同时通过ensureSegment方法创建segment对象时,只有一个线程能够创建成功; +其中创建的新segment对象中的加载因子、存放位置、扩容阈值与segment[0]元素保持一致。这样做性能更高,因为不用在计算了。

+

为了保证线程安全,在ensureSegment方法中用Unsafe类中的一些方法做了三次判断,其中最后一次也就是该方法保证线程安全的关键,用到了CAS操作;

+

当多个线程并发执行下面的代码,先执行CAS的线程,判断segment数组中某个位置是空的,然后就把这个线程自己创建的segment数组赋值给seg,即seg = s;然后break跳出循环; +后执行的线程会再次判断seg是否为空,因先执行的线程已经seg = s不为空了,所以循环条件不成立,也就不再执行了。

+
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
+       == null) {
+    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
+        break;
+}
+

Segment.put方法源码;为了保证线程安全,执行put方法要保证要加到锁,如果没加到锁就会执行scanAndLockForPut方法; +这个方法就会保证一定要加到锁;

+
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
+    HashEntry<K,V> node = tryLock() ? null :
+        scanAndLockForPut(key, hash, value);
+    // ... 插入节点操作 最后释放锁
+}
+

scanAndLockForPut方法的主要作用就是加锁,如果没有获取锁,就会一致遍历segment数组,直到遍历到最后一个元素; +每次遍历完都会尝试获取锁,如果还是获取不到锁,就会重试,最大次数为MAX_SCAN_RETRIES在CPU多核下为64次,如果大于64次就会强制加锁。

+
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
+    HashEntry<K,V> first = entryForHash(this, hash);
+    HashEntry<K,V> e = first;
+    HashEntry<K,V> node = null;
+    int retries = -1; // negative while locating node
+    while (!tryLock()) {
+        HashEntry<K,V> f; // to recheck first below
+        if (retries < 0) {
+            if (e == null) {
+                if (node == null) // speculatively create node
+                    node = new HashEntry<K,V>(hash, key, value, null);
+                retries = 0;
+            }
+            else if (key.equals(e.key))
+                retries = 0;
+            else
+                e = e.next;
+        }
+        else if (++retries > MAX_SCAN_RETRIES) {
+            lock();
+            break;
+        }
+        else if ((retries & 1) == 0 &&
+                 (f = entryForHash(this, hash)) != first) {
+            e = first = f; // re-traverse if entry changed
+            retries = -1;
+        }
+    }
+    return node;
+}
+
+static final int MAX_SCAN_RETRIES =
+            Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
+
jdk1.8ConcurrentHashMap
+

JDK1.8的实现已经摒弃了 Segment 的概念,而是直接用 Node数组+链表/红黑树的数据结构来实现,并发控制使用 synchronized 和CAS来操作,整个看起来就像是优化过且线程安全的HashMap; +虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。

+

JDK1.8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想; +JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap 只是增加了同步操作来控制并发。

+

jdk1.8ConcurrentHashMap

+

相关概念:

+
    +
  • sizeCtl :默认为0,用来控制table的初始化和扩容操作;用volatile修饰,保证了其可见性;
  • +
+

JDK1.8ConcurrentHashMap.put方法源码;

+
final V putVal(K key, V value, boolean onlyIfAbsent) {
+    if (key == null || value == null) throw new NullPointerException();
+    int hash = spread(key.hashCode());
+    int binCount = 0;
+    for (Node<K,V>[] tab = table;;) {
+        Node<K,V> f; int n, i, fh;
+        if (tab == null || (n = tab.length) == 0)
+            tab = initTable();
+        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
+            if (casTabAt(tab, i, null,
+                         new Node<K,V>(hash, key, value, null)))
+                break;                   // no lock when adding to empty bin
+        }
+        else if ((fh = f.hash) == MOVED)
+            tab = helpTransfer(tab, f);
+        else {
+            V oldVal = null;
+            synchronized (f) {
+                if (tabAt(tab, i) == f) {
+                    if (fh >= 0) {
+                        binCount = 1;
+                        for (Node<K,V> e = f;; ++binCount) {
+                            K ek;
+                            if (e.hash == hash &&
+                                ((ek = e.key) == key ||
+                                 (ek != null && key.equals(ek)))) {
+                                oldVal = e.val;
+                                if (!onlyIfAbsent)
+                                    e.val = value;
+                                break;
+                            }
+                            Node<K,V> pred = e;
+                            if ((e = e.next) == null) {
+                                pred.next = new Node<K,V>(hash, key,
+                                                          value, null);
+                                break;
+                            }
+                        }
+                    }
+                    else if (f instanceof TreeBin) {
+                        Node<K,V> p;
+                        binCount = 2;
+                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
+                                                       value)) != null) {
+                            oldVal = p.val;
+                            if (!onlyIfAbsent)
+                                p.val = value;
+                        }
+                    }
+                }
+            }
+            if (binCount != 0) {
+                if (binCount >= TREEIFY_THRESHOLD)
+                    treeifyBin(tab, i);
+                if (oldVal != null)
+                    return oldVal;
+                break;
+            }
+        }
+    }
+    addCount(1L, binCount);
+    return null;
+}
+

首先调用Node.initTable()方法,初始化table;sizeCtl 默认为0,如果ConcurrentHashMap实例化时有传参数,sizeCtl 会是一个2的幂次方的值。 +所以执行第一次put方法时操作的线程会执行Unsafe.compareAndSwapInt方法修改sizeCtl=-1,只有一个线程能够修改成功,其它线程通过Thread.yield()礼让线程让出CPU时间片,等待table初始化完成。

+
private final Node<K,V>[] initTable() {
+    Node<K,V>[] tab; int sc;
+    while ((tab = table) == null || tab.length == 0) {
+        if ((sc = sizeCtl) < 0)
+            Thread.yield(); // lost initialization race; just spin
+        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
+            try {
+                if ((tab = table) == null || tab.length == 0) {
+                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
+                    @SuppressWarnings("unchecked")
+                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
+                    table = tab = nt;
+                    sc = n - (n >>> 2);
+                }
+            } finally {
+                sizeCtl = sc;
+            }
+            break;
+        }
+    }
+    return tab;
+}
+

调用put方法,通过hash算法计算,将要存放数组中的位置(n - 1) & hash,如果该节点为空就通过CAS判断,创建一个Node放到该位置上。

+
int hash = spread(key.hashCode());
+
+// hash算法,计算存放在map中的位置;要保证尽可能的均匀分散,避免hash冲突
+static final int HASH_BITS = 0x7fffffff;
+static final int spread(int h) {
+    // 等同于: key.hashCode() ^ (key.hashCode() >>> 16) & 0x7fffffff
+    return (h ^ (h >>> 16)) & HASH_BITS;
+}
+

如果该位置不为空就会继续判断当前线程的ConcurrentHashMap是否进行扩容

+
// MOVED = -1
+if ((fh = f.hash) == MOVED)
+tab = helpTransfer(tab, f);
+

插入之前,再次利用tabAt(tab, i) == f判断,防止被其它线程修改; +之后就会对这个将要添加到该位置的元素加锁,判断是链表还是树节点,做不同的操作;

+
    +
  • 如果f.hash >= 0,说明f是链表结构的头结点,遍历链表,如果找到对应的node节点,则修改value,否则在链表尾部加入节点。
  • +
  • 如果f是TreeBin类型节点,说明f是红黑树根节点,则在树结构上遍历元素,更新或增加节点。
  • +
  • 如果链表中节点数binCount >= TREEIFY_THRESHOLD(默认是8),则把链表转化为红黑树结构。
  • +
+
V oldVal = null;
+synchronized (f) {
+    if (tabAt(tab, i) == f) {
+        if (fh >= 0) {
+            binCount = 1;
+            for (Node<K,V> e = f;; ++binCount) {
+                K ek;
+                if (e.hash == hash &&
+                    ((ek = e.key) == key ||
+                     (ek != null && key.equals(ek)))) {
+                    oldVal = e.val;
+                    if (!onlyIfAbsent)
+                        e.val = value;
+                    break;
+                }
+                Node<K,V> pred = e;
+                if ((e = e.next) == null) {
+                    pred.next = new Node<K,V>(hash, key,
+                                              value, null);
+                    break;
+                }
+            }
+        }
+        else if (f instanceof TreeBin) {
+            Node<K,V> p;
+            binCount = 2;
+            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
+                                           value)) != null) {
+                oldVal = p.val;
+                if (!onlyIfAbsent)
+                    p.val = value;
+            }
+        }
+    }
+}
+if (binCount != 0) {
+    if (binCount >= TREEIFY_THRESHOLD)
+        treeifyBin(tab, i);
+    if (oldVal != null)
+        return oldVal;
+    break;
+}
+

最后则进行扩容操作

+
//相当于size++
+addCount(1L, binCount);
+
private final void addCount(long x, int check) {
+    CounterCell[] as; long b, s;
+    if ((as = counterCells) != null ||
+        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
+        CounterCell a; long v; int m;
+        boolean uncontended = true;
+        if (as == null || (m = as.length - 1) < 0 ||
+            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
+            !(uncontended =
+              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
+            fullAddCount(x, uncontended);
+            return;
+        }
+        if (check <= 1)
+            return;
+        s = sumCount();
+    }
+    if (check >= 0) {
+        Node<K,V>[] tab, nt; int n, sc;
+        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
+               (n = tab.length) < MAXIMUM_CAPACITY) {
+            int rs = resizeStamp(n);
+            if (sc < 0) {
+                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
+                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
+                    transferIndex <= 0)
+                    break;
+                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
+                    transfer(tab, nt);
+            }
+            else if (U.compareAndSwapInt(this, SIZECTL, sc,
+                                         (rs << RESIZE_STAMP_SHIFT) + 2))
+                transfer(tab, null);
+            s = sumCount();
+        }
+    }
+}
+

节点从table移动到nextTable,大体思想是遍历、复制的过程。 +通过Unsafe.compareAndSwapInt修改sizeCtl值,保证只有一个线程能够初始化nextTable,扩容后的数组长度为原来的两倍,但是容量是原来的1.5。

+
    +
  • 首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素f,初始化一个forwardNode实例fwd。
  • +
  • 如果f == null,则在table中的i位置放入fwd,这个过程是采用Unsafe.compareAndSwapObjectf方法实现的,实现了节点的并发移动。
  • +
  • 如果f是链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上,移动完成,采用Unsafe.putObjectVolatile方法给table原位置赋值fwd。
  • +
  • 如果f是TreeBin节点,也做一个反序处理,并判断是否需要untreeify,把处理的结果分别放在nextTable的i和i+n的位置上,移动完成,同样采用Unsafe.putObjectVolatile方法给table原位置赋值fwd。
  • +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/java/rookie-object-oriented/index.html b/blog-site/public/posts/java/rookie-object-oriented/index.html new file mode 100644 index 00000000..7bf14868 --- /dev/null +++ b/blog-site/public/posts/java/rookie-object-oriented/index.html @@ -0,0 +1,6759 @@ + + + + + + + + + + + 面向对象 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

面向对象

+ 2021.02.15 +
+
+

面向对象是一种编程思想,包括三大特性和六大原则,其中,三大特性指的是封装、继承和多态;六大原则指的是单一职责原则开放封闭原则迪米特原则里氏替换原则依赖倒置原则以及接口隔离原则,其中,单一职责原则是指一个类应该是一组相关性很高的函数和数据的封装,这是为了提高程序的内聚性,而其他五个原则是通过抽象来实现的,目的是为了降低程序的耦合性以及提高可扩展性。

+
+

面向对象简称OO(object-oriented)是相对面向过程(procedure-oriented)来说的,是一种编程思想.Java就是一门面向对象的语言.

+

面向对象编程简称OOP(Object-oriented programming),是将事务高度抽象化的编程模式. +面向对象编程是以功能来划分问题的,将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对应对象,通过不同对象之间的调用,组合成某个功能解决问题.

+

对比面向过程

+
+

PS: 面向过程编程简称POP(Procedural oriented programming),面向过程是以过程为中心的编程思想.是自顶而下的编程.

+
+

举个栗子: 下五子棋

+

+面向过程 {
+
+  1.开始游戏
+
+  2.黑子先走
+
+  3.绘制画面
+
+  4.判断输赢
+
+  5.轮到白子
+
+  6.绘制画面
+
+  7.判断输赢
+
+  8.返回到 黑子先走
+ 
+}
+
面向对象 {
+
+    1.创建黑棋,白棋
+    
+    2.创建棋盘
+    
+    3.创建规则
+    
+    4.赋予每个对象相关属性和指定行为
+    
+    5.各个功能之间互相调用
+}
+

面向对象是模型化的,你只需抽象出几个类,进行封装成各个功能,通过不同对象之间的调用来解决问题.而面向过程需要把问题分解为几个步骤,每个步骤用对应的函数调用即可.面向过程是具体化的,流程化的,解决一个问题,需要你一步一步的分析,一步一步的实现.

+

面向对象的底层其实还是面向过程,把面向过程抽象成类,然后进行封装,方便我们我们使用,就是面向对象了.

+

简而言之,用面向过程的方法写出来的程序是一份蛋炒饭,而用面向对象写出来的程序是一份盖浇饭(就是在一碗白米饭上面浇上一份盖菜,你喜欢什么菜,你就浇上什么菜). +通过例子可以看出面向对象更重视不重复造轮子,即创建一次,重复使用.

+

面向对象

+
+
    +
  • +

    优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护。

    +
  • +
  • +

    缺点:性能比面向过程低

    +
  • +
+
+

面向过程

+
+
    +
  • 优点:流程化使得编程任务明确,在开发之前基本考虑了实现方式和最终结果,具体步骤清楚,便于节点分析; 效率高,面向过程强调代码的短小精悍,善于结合数据结构来开发高效率的程序。
  • +
  • 缺点:没有面向对象易维护、易复用、易扩展
  • +
+
+

抽象会使复杂的问题更加简单化,面向对象更符合人类的思维,而面向过程则是机器的思想.

+

软件设计原则

+

设计原则的目的是为了让程序达到高内聚、低耦合,提高可扩展性的目的,其实现手段是面向对象的三大特性:封装、继承以及多态。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
设计原则名称核心思想
单一职责原则一个类只负责一个功能领域中的相应职责
开放封闭原则软件实体应对扩展开放,而对修改关闭
依赖倒转原则抽象不应该依赖于细节,细节应该依赖于抽象
里氏替换原则所有引用基类对象的地方能够透明地使用其子类的对象
接口隔离原则使用多个专门的接口,而不使用单一的总接口
合成复用原则尽量使用对象组合,而不是继承来达到复用的目的
迪米特法则一个软件实体应当尽可能少地与其他实体发生相互作用
+

单一职责原则

+
+

其核心思想为:一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可降低类的复杂度,提高代码可读性,可维护性,降低变更风险. 单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。职责过多,可能引起它变化的原因就越多,这将导致职责依赖,相互之间就产生影响,从而大大损伤其内聚性和耦合度。通常意义下的单一职责,就是指只有一种单一功能,不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。 专注,是一个人优良的品质;同样的,单一也是一个类的优良设计。交杂不清的职责将使得代码看起来特别别扭牵一发而动全身,有失美感和必然导致丑陋的系统错误风险。

+
+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        Vehicle vehicle = new Vehicle();
+        vehicle.running("汽车");
+        // 飞机不是在路上行驶
+        vehicle.running("飞机");
+    }
+}
+
+/**
+ * 在run方法中违反了单一职责原则
+ * 解决方法根据不同的交通工具,分解成不同的类即可
+ */
+class Vehicle{
+    public void running(String name) {
+        System.out.println(name + "在路上行驶 ....");
+    }
+}
+
// 解决
+public class MainTest {
+    public static void main(String[] args) {
+        Driving driving = new Driving();
+        driving.running("汽车");
+        Flight flight = new Flight();
+        flight.running("飞机");
+    }
+}
+
+class Driving {
+    public void running(String name) {
+        System.out.println(name + "在路上行驶 ....");
+    }
+}
+
+class Flight {
+    public void running(String name) {
+        System.out.println(name + "在空中飞行 ....");
+    }
+}
+

通常情况下,我们应当遵循单一职责原则,只要逻辑足够简单,才可以在代码里边违反单一职责原则;只要类中方法数量足够少,可以在方法级别保持单一职责原则.

+
public class MainTest {
+    public static void main(String[] args) {
+        Vehicle2 vehicle2 = new Vehicle2();
+        vehicle2.driving("汽车");
+        vehicle2.flight("飞机");
+    }
+}
+/*
+ * 改进
+ *↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
+ */
+
+class Vehicle2 {
+    public void driving(String name) {
+        System.out.println(name + "在路上行驶 ....");
+    }
+    public void flight(String name) {
+        System.out.println(name + "在空中飞行 ....");
+    }
+}
+

开放封闭原则

+
+

软件实体应该是可扩展的,而不可修改的。也就是,对(提供方)扩展开放,对(使用方)修改封闭的。 开放封闭原则主要体现在两个方面

+
    +
  • 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
  • +
  • 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何尝试的修改。
  • +
+

实现开放封闭原则的核心思想就是对抽象编程,而不对具体编程,因为抽象相对稳定。让类依赖于固定的抽象,所以修改就是封闭的;而通过面向对象的继承和多态机制,又可以实现对抽象类的继承,通过覆写其方法来改变固有行为,实现新的拓展方法,所以就是开放的。 “需求总是变化”没有不变的软件,所以就需要用封闭开放原则来封闭变化满足需求,同时还能保持软件内部的封装体系稳定,不被需求的变化影响。编程中遵循其他原则,以及使用其他设计模式的目的就是为了遵循开闭原则.

+
+

当软件需要变化时,尽量使用扩展的软件实体的方式行为来实现变化,而不是通过修改已有的代码来实现变化.

+

代码实现

+

+public class MainTest {
+    public static void main(String[] args) {
+        Mother mother = new Mother();
+
+        Son son = new Son();
+        Daughter daughter = new Daughter();
+
+        // 注入子类对象 如果扩展需要其他类 换成其他对象即可
+        mother.setAbstractFather(son);
+        mother.display();
+    }
+}
+
+abstract class AbstractFather {
+
+    protected abstract void display();
+
+}
+class Son  extends AbstractFather{
+    @Override
+    protected void display() {
+        System.out.println("son class ...");
+    }
+}
+class Daughter  extends AbstractFather{
+
+    @Override
+    protected void display() {
+        System.out.println("daughter class ...");
+    }
+}
+
+class Mother {
+
+    private AbstractFather abstractFather;
+
+    public void setAbstractFather(AbstractFather abstractFather) {
+        this.abstractFather = abstractFather;
+    }
+
+    public void display() {
+        abstractFather.display();
+    }
+
+}
+

依赖倒置原则

+
+

该原则依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。 我们知道,依赖一定会存在于类与类、模块与模块之间。当两个模块之间存在紧密的耦合关系时,最好的方法就是分离接口和实现:在依赖之间定义一个抽象的接口使得高层模块调用接口,而底层模块实现接口的定义,以此来有效控制耦合关系,达到依赖于抽象的设计目标。 抽象的稳定性决定了系统的稳定性,因为抽象是不变的,依赖于抽象是面向对象设计的精髓,也是依赖倒置原则的核心。 依赖于抽象是一个通用的原则,而某些时候依赖于细节则是在所难免的,必须权衡在抽象和具体之间的取舍,方法不是一层不变的。依赖于抽象,就是对接口编程,不要对实现编程。

+
+

代码实现

+

+public class MainTest {
+
+    public static void main(String[] args) {
+        Computer computer = new Computer();
+        // 对接口编程,不要对实现编程
+        // 如果没有接口 则代码很难实现扩展
+        Disk disk = new CustomDisk();
+        Memory memory = new CustomMemory();
+
+        computer.setDisk(disk);
+        computer.setMemory(memory);
+        computer.run();
+    }
+
+}
+
+interface Disk{
+    void diskMethod();
+}
+
+interface Memory{
+    void memoryMethod();
+}
+
+class CustomDisk implements Disk{
+
+    @Override
+    public void diskMethod() {
+        System.out.println("i am disk ...");
+    }
+}
+
+class  CustomMemory implements Memory{
+
+    @Override
+    public void memoryMethod() {
+        System.out.println("i am memory ...");
+    }
+}
+
+class Computer {
+
+    private Memory memory;
+
+    private Disk disk;
+
+    public void setDisk(Disk disk) {
+        this.disk = disk;
+    }
+
+    public void setMemory(Memory memory) {
+        this.memory = memory;
+    }
+
+    public Disk getDisk() {
+        return disk;
+    }
+
+    public Memory getMemory() {
+        return memory;
+    }
+    public void run() {
+        System.out.println(" computer is running ...");
+        memory.memoryMethod();
+        disk.diskMethod();
+    }
+}
+

接口隔离原则

+
+

使用多个小的专门的接口,而不要使用一个大的总接口。 具体而言,接口隔离原则体现在:接口应该是内聚的,应该避免“胖”接口。一个类对另外一个类的依赖应该建立在最小的接口上,不要强迫依赖不用的方法,这是一种接口污染。 接口有效地将细节和抽象隔离,体现了对抽象编程的一切好处,接口隔离强调接口的单一性。而胖接口存在明显的弊端,会导致实现的类型必须完全实现接口的所有方法、属性等;而某些时候,实现类型并非需要所有的接口定义,在设计上这是“浪费”,而且在实施上这会带来潜在的问题,对胖接口的修改将导致一连串的客户端程序需要修改,有时候这是一种灾难。在这种情况下,将胖接口分解为多个特点的定制化方法,使得客户端仅仅依赖于它们的实际调用的方法,从而解除了客户端不会依赖于它们不用的方法。

+

分离的手段主要有以下两种:

+
    +
  • 委托分离,通过增加一个新的类型来委托客户的请求,隔离客户和接口的直接依赖,但是会增加系统的开销。
  • +
  • 多重继承分离,通过接口多继承来实现客户的需求,这种方式是较好的。
  • +
+
+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        FuncImpl func = new FuncImpl();
+        func.func1();
+        func.func2();
+        func.func3();
+    }
+}
+
+interface Function1{
+    void func1();
+    // 如果将接口中的方法都写在一个接口就会造成实现该接口就要重写该接口所有方法。
+    // 当然Java 8 接口可以有实现,降低了维护成本,解了决该问题;
+    // 但是我们还是应当遵循该原则,使得接口看起来更加清晰
+    // void func2();
+    // void func3();
+}
+interface Function2 {
+    void func2();
+}
+interface Function3 {
+    void func3();
+}
+
+class FuncImpl implements Function1,Function2,Function3{
+
+    @Override
+    public void func1() {
+        System.out.println("i am function1 impl");
+    }
+
+    @Override
+    public void func2() {
+        System.out.println("i am function2 impl");
+    }
+
+    @Override
+    public void func3() {
+        System.out.println("i am function3 impl");
+    }
+}
+

里氏替换原则

+
+

子类必须能够替换其基类。 这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。在父类和子类的具体行为中,必须严格把握继承层次中的关系和特征,将基类替换为子类,程序的行为不会发生任何变化。同时,这一约束反过来则是不成立的,子类可以替换基类,但是基类不一定能替换子类。 里氏替换原则,主要着眼于对抽象和多态建立在继承的基础上,因此只有遵循了里氏替换原则,才能保证继承复用是可靠地。

+

实现的方法是面向接口编程:将公共部分抽象为基类接口或抽象类,通过Extract Abstract Class,在子类中通过覆写父类的方法实现新的方式支持同样的职责。 里氏替换原则是关于继承机制的设计原则,违反了里氏替换原则就必然导致违反开放封闭原则。 里氏替换原则能够保证系统具有良好的拓展性,同时实现基于多态的抽象机制,能够减少代码冗余,避免运行期的类型判别。

+
+

简单来说就是子类可以扩展父类的功能,但是尽量不要重写父类的功能.如果通过重写父类方法来完成新的功能,这样写起来虽然简单,但整个体系的可复用性会非常差,特别是运用多态比较频繁时,程序运行出错的概率会非常大.

+

代码实现

+

+public class MainTest {
+    public static void main(String[] args) {
+        Rectangle rectangle = new Rectangle();
+        rectangle.setWidth(20);
+        rectangle.setHeight(10);
+        resize(rectangle);
+        print(rectangle);
+        System.out.println("=======================");
+        // 因为 Square类 重写了父类set的方法导致调用时出错
+        Rectangle square = new Square();
+        square.setWidth(10);
+        resize(square);
+        print(square);
+    }
+    public static void resize(Rectangle rectangle){
+        while (rectangle.getWidth() >= rectangle.getHeight()){
+            rectangle.setHeight(rectangle.getHeight() + 1);
+        }
+    }
+
+    public static void print(Rectangle rectangle){
+        System.out.println(rectangle.getWidth());
+        System.out.println(rectangle.getHeight());
+    }
+}
+
+class Square  extends Rectangle{
+    private Integer width;
+    private Integer height;
+
+    @Override
+    public void setWidth(Integer width) {
+        super.setWidth(width);
+        super.setHeight(width);
+    }
+
+    @Override
+    public void setHeight(Integer height) {
+        super.setWidth(height);
+        super.setHeight(height);
+    }
+}
+class Rectangle {
+    private Integer width;
+    private Integer height;
+
+    public void setWidth(Integer width) {
+        this.width = width;
+    }
+
+    public void setHeight(Integer height) {
+        this.height = height;
+    }
+
+    public Integer getWidth() {
+        return width;
+    }
+
+    public Integer getHeight() {
+        return height;
+    }
+}
+

合成复用原则

+
+

尽量使用对象组合,而不是继承来达到复用的目的。 在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现,即通过组合/聚合关系或通过继承,但首先应该考虑使用组合/聚合,组合/聚合可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少;其次才考虑继承,在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。

+
+

代码实现

+

详见继承与组合

+

迪米特法则

+
+

迪米特法则又叫最少知识原则,就是说一个对象应当对其他对象有尽可能少的了解。 +其核心思想是: 降低类之间的耦合.如果类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大.一个对象应该对其他对象有最少的了解。 通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)的内部是如何复杂都和我没关系,那是你的事情,我就知道你提供的public方法,我就调用这么多,其他的一概不关心.迪米特法则其根本思想,是强调了类之间的松耦合。类之间的耦合越弱,越有利于复用,一个处在弱耦合的类被修改,不会对有关系的类造成搏击,也就是说,信息的隐藏促进了软件的复用。

+
+

迪米特法则还有个更简单的定义:只与直接的朋友通信

+

朋友定义:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。 耦合的方式很多,依赖,关联,组合,聚合等。其中,我们称出现成员变量,方法参数,方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。也就是说,陌生的类最好不要以局部变量的形式出现在类的内部。

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        //创建了一个 SchoolManager 对象
+        SchoolManager schoolManager = new SchoolManager();
+        // SchoolManager直接朋友: CollegeManager (方法参数) Employee(返回值)
+        // CollegeEmployee以局部变量的形式出现在SchoolManager类中 所以违反了迪米特法则
+        schoolManager.printAllEmployee(new CollegeManager());
+    }
+}
+
+
+class Employee {
+    private String id;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+}
+
+
+class CollegeEmployee {
+    private String id;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+}
+
+class CollegeManager {
+    public List<CollegeEmployee> getAllEmployee() {
+        List<CollegeEmployee> list = new ArrayList<>();
+        for (int i = 0; i < 10; i++) {
+            CollegeEmployee emp = new CollegeEmployee();
+            emp.setId("学院员工 id= " + i);
+            list.add(emp);
+        }
+        return list;
+    }
+}
+
+class SchoolManager {
+
+    public List<Employee> getAllEmployee() {
+        List<Employee> list = new ArrayList<>();
+
+        for (int i = 0; i < 5; i++) {
+            //这里我们增加了 5 个员工到
+            Employee emp = new Employee();
+            emp.setId("学校总部员工 id= " + i);
+            list.add(emp);
+        }
+        return list;
+    }
+
+    void printAllEmployee(CollegeManager sub) {
+        //获取到学院员工
+        List<CollegeEmployee> list1 = sub.getAllEmployee();
+        System.out.println("------------学院员工------------");
+        for (CollegeEmployee e : list1) {
+            System.out.println(e.getId());
+        }
+        //获取到学校总部员工
+        List<Employee> list2 = this.getAllEmployee();
+        System.out.println("------------学校总部员工------------");
+        for (Employee e : list2) {
+            System.out.println(e.getId());
+        }
+    }
+}
+

+class CollegeManager {
+    public List<CollegeEmployee> getAllEmployee() {
+        List<CollegeEmployee> list = new ArrayList<>();
+        for (int i = 0; i < 10; i++) {
+            CollegeEmployee emp = new CollegeEmployee();
+            emp.setId("学院员工 id= " + i);
+            list.add(emp);
+        }
+        return list;
+    }
+    // 修改后
+    public void printAllEmployee() {
+        List<CollegeEmployee> list1 = this.getAllEmployee();
+        System.out.println("------------学院员工------------");
+        for (CollegeEmployee e : list1) {
+            System.out.println(e.getId());
+        }
+    }
+}
+
+class SchoolManager {
+
+    public List<Employee> getAllEmployee() {
+        List<Employee> list = new ArrayList<>();
+
+        for (int i = 0; i < 5; i++) {
+            //这里我们增加了 5 个员工到
+            Employee emp = new Employee();
+            emp.setId("学校总部员工 id= " + i);
+            list.add(emp);
+        }
+        return list;
+    }
+
+    void printAllEmployee(CollegeManager sub) {
+        //获取到学院员工
+        // 修改后
+        sub.printAllEmployee();
+
+        //获取到学校总部员工
+        List<Employee> list2 = this.getAllEmployee();
+        System.out.println("------------学校总部员工------------");
+        for (Employee e : list2) {
+            System.out.println(e.getId());
+        }
+    }
+}
+

三大特性

+

封装

+

封装是面向对象方法的重要原则,就是把对象的属性和操作(或服务)结合为一个独立的整体,并尽可能隐藏对象的内部实现细节.简单的说,一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。

+

封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是要通过外部接口,以特定的访问权限来使用类的成员.

+

优点

+
    +
  • 良好的封装能够减少耦合;提高了可维护性和灵活性以及可重用性;
  • +
  • 类内部的结构可以自由修改;
  • +
  • 可以对成员变量进行更精确的控制;
  • +
  • 隐藏信息,实现细节;
  • +
+

访问权限

+

Java的封装可以通过修改属性的可见性限制对属性的访问来体现.

+

Java 中有三个访问权限修饰符:private、protected 以及 public,如果不加访问修饰符,表示包级可见(default)。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
修饰符当前类同一包下其他包的子类不同包的子类其他包
publicYYYYY
protectedYYYY/NN
defaultYYYNN
privateYNNNN
+

这四种访问权限的控制符能够控制类中成员的可见性.当然需要满足在不使用Java反射的情况下.

+

注意

+
    +
  • protected用于修饰成员,表示在继承体系中成员对于子类可见.如果不存在继承关系则不能访问protected修饰的实例.
  • +
  • 类可见表示其它类可以用这个类创建实例对象;
  • +
  • 成员可见表示其它类可以用这个类的实例对象访问到该成员;
  • +
+

设计良好的模块会隐藏所有的实现细节,把它的 API 与它的实现清晰地隔离开来。模块之间只通过它们的 API 进行通信,一个模块不需要知道其他模块的内部工作情况,这个概念被称为信息隐藏或封装。因此访问权限应当尽可能地使每个类或者成员不被外界访问

+

如果子类的方法重写了父类的方法,那么子类中该方法的访问级别不允许低于父类的访问级别。这是为了确保可以使用父类实例的地方都可以使用子类实例,也就是确保满足里氏替换原则

+

某个类的字段决不能是公有的,因为这么做的话就失去了对这个字段修改行为的控制,其他类可以对其随意修改。 +例如下面的例子中,AccessExample拥有id公有字段,如果在某个时刻,我们想要使用int存储id字段,那么就需要修改所有类中的代码。

+
public class AccessExample {
+    public String id;
+    // public int id;
+}
+

可以使用公有的 gettersetter 方法来替换公有字段,这样的话就可以控制对字段的修改行为。实现了封装

+
public class AccessExample {
+
+    private int id;
+
+    public String getId() {
+        return id + "";
+    }
+
+    public void setId(String id) {
+        this.id = Integer.valueOf(id);
+    }
+}
+

但是也有例外,如果是包级私有的类或者私有的嵌套类,那么直接暴露成员不会有特别大的影响。

+
public class AccessWithInnerClassExample {
+
+    private class InnerClass {
+        int x;
+    }
+
+    private InnerClass innerClass;
+
+    public AccessWithInnerClassExample() {
+        innerClass = new InnerClass();
+    }
+
+    public int getValue() {
+        return innerClass.x;  // 直接访问
+    }
+}
+

继承

+

继承可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程.要实现继承,可以通过“继承”(Inheritance)和“组合”(Composition)来实现. 继承概念的实现方式有两类:实现继承与接口继承。实现继承是指直接使用基类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力.

+
+

继承: 如果多个类的某个部分的功能相同,那么可以抽象出一个类来,把相同的部分放到父类中,让他们继承这个类

+

实现:如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,也就是一个标准,让他们的实现这个接口,各自实现自己具体的处理方法来处理那个目标

+
+

继承的根本原因是因为要复用,而实现的根本原因是需要定义一个标准.

+

继承与组合

+

继承是实现复用代码的重要手段,但是继承会破坏封装.组合也是代码复用的重要方式,可以提供良好的封装性.

+
实现继承
+
// 继承
+
+public class MainTest {
+
+    public static void main(String[] args) {
+        B b = new B();
+        b.test();
+    }
+}
+
+class A {
+
+    protected int i;
+
+    protected void test() {
+        System.out.println("I am super class ... ");
+    }
+
+}
+
+class B  extends A{
+
+    // 调用父类成员 
+    public void t() {
+        System.out.println(i);
+    }
+
+}
+

通过以上代码,可以发现,子类可以访问父类的成员变量方法,并且通过重写可以改变父类方法实现.从而破坏了封装性.

+
+

在继承结构中,父类的内部细节对于子类是可见的。所以我们通常也可以说通过继承的代码复用是一种白盒式代码复用。(如果基类的实现发生改变,那么派生类的实现也将随之改变。这样就导致了子类行为的不可预知性;)

+
+

为了保证父类有良好的封装性,不会对子类随意更改,设计父类时应遵循以下原则:

+
+
    +
  • 尽量隐藏父类的内部数据.尽量把所有父类的所有成员变量都用private修饰,不要让子类直接访问父类的成员.
  • +
  • 不要让子类随意的修改访问父类的方法.父类中那些仅为辅助其他的工具方法,应该使用private修饰,让子类无法访问该方法;如果父类中的方法需要被外部类调用,则需以public修饰,但又不希望重写父类方法可以使用final来修饰方法;但如果希望父类某个方法被重写,但又不希望其他类访问自由,可以使用protected修饰.
  • +
  • 尽量不要在父类构造器中调用将要被子类重写的方法.
  • +
+
+

继承是类与类或者接口与接口之间最常见的关系,继承是一种is-a关系。

+
实现组合
+

组合是把旧类对象作为新类对象的成员变量组合进来,用以实现新类的功能.

+

+public class MainTest {
+
+    public static void main(String[] args) {
+        B b = new B(new A());
+        b.test();
+    }
+}
+
+class A {
+
+    protected int i;
+
+    protected void test() {
+        System.out.println("I am super class ... ");
+    }
+
+}
+
+class B {
+
+    private final A a;
+
+    public B(A a) {
+        this.a = a;
+    }
+    public void test() {
+        // 复用 A 类提供的 test 方法
+        a.test();
+    }
+}
+
+

组合是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。因为在对象之间,各自的内部细节是不可见的,所以我们也说这种方式的代码复用是黑盒式代码复用。(因为组合中一般都定义一个类型,所以在编译期根本不知道具体会调用哪个实现类的方法)

+
+

组合强调的是整体与部分、拥有的关系,即has-a的关系.

+
比较
+
+

继承,在写代码的时候就要指名具体继承哪个类,所以,在编译期就确定了关系。(从基类继承来的实现是无法在运行期动态改变的,因此降低了应用的灵活性。)

+
+
+

组合,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在运行期确定。

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
组合继承
优点:不破坏封装,整体类与局部类之间松耦合,彼此相对独立缺点:破坏封装,子类与父类之间紧密耦合,子类依赖于父类的实现,子类缺乏独立性
优点:具有较好的可扩展性缺点:支持扩展,但是往往以增加系统结构的复杂度为代价
优点:支持动态组合。在运行时,整体对象可以选择不同类型的局部对象缺点:不支持动态继承。在运行时,子类无法选择不同的父类
优点:整体类可以对局部类进行包装,封装局部类的接口,提供新的接口缺点:子类不能改变父类的接口
缺点:整体类不能自动获得和局部类同样的接口优点:子类能自动继承父类的接口
缺点:创建整体类的对象时,需要创建所有局部类的对象优点:创建子类的对象时,无须创建父类的对象
+
使用选择
+

经过以上比较,可以得出结论: 组合比继承更加灵活.所以在写代码如果这个功能组合和继承都能够完成,那么应该优先选择组合. +但是继承在一些场景还是要优先于组合的.

+
+
    +
  • 继承要慎用,其使用场合仅限于你确信使用该技术有效的情况。一个判断方法是,问一问自己是否需要从新类向基类进行向上转型。如果是必须的,则继承是必要的。反之则应该好好考虑是否需要继承。
  • +
  • 只有当子类真正是超类的子类型时,才适合用继承。换句话说,对于两个类A和B,只有当两者之间确实存在is-a关系的时候,类B才应该继承类A。
  • +
+
+

super

+
    +
  • 访问父类的构造函数:可以使用super()函数访问父类的构造函数,从而委托父类完成一些初始化的工作。应该注意到,子类一定会调用父类的构造函数来完成初始化工作,一般是调用父类的默认构造函数如果子类需要调用父类其它构造函数,那么就可以使用super函数。
  • +
  • 访问父类的成员:如果子类重写了父类的某个方法,可以通过使用 super 关键字来引用父类的方法实现。
  • +
+
public class SuperExample {
+
+    protected int x;
+    protected int y;
+
+    public SuperExample(int x, int y) {
+        this.x = x;
+        this.y = y;
+    }
+
+    public void func() {
+        System.out.println("SuperExample.func()");
+    }
+}
+public class SuperExtendExample extends SuperExample {
+
+    private int z;
+
+    public SuperExtendExample(int x, int y, int z) {
+        super(x, y);
+        this.z = z;
+    }
+
+    @Override
+    public void func() {
+        super.func();
+        System.out.println("SuperExtendExample.func()");
+    }
+}
+SuperExample e = new SuperExtendExample(1, 2, 3);
+e.func();
+SuperExample.func()
+SuperExtendExample.func()
+

抽象类与接口

+

抽象类和接口也是Java继承体系中的重要组成部分.抽象类是用来捕捉子类的通用特性的,而接口则是抽象方法的集合;抽象类不能被实例化,只能被用作子类的超类,是被用来创建继承层级里子类的模板,而接口只是一种形式,接口自身不能做任何事情。

+
抽象类
+

抽象类和抽象方法都使用abstract关键字进行声明。如果一个类中包含抽象方法,那么这个类必须声明为抽象类。

+

抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。

+
public abstract class AbstractClassExample {
+
+    protected int x;
+    private int y;
+
+    public abstract void func1();
+
+    public void func2() {
+        System.out.println("func2");
+    }
+}
+public class AbstractExtendClassExample extends AbstractClassExample {
+    @Override
+    public void func1() {
+        System.out.println("func1");
+    }
+}
+
+// 实例化抽象类
+// AbstractClassExample ac1 = new AbstractClassExample(); 
+// 实例化抽象类子类
+// AbstractClassExample ac2 = new AbstractExtendClassExample();
+// ac2.func1();
+
接口
+

接口是抽象类的延伸,在 Java 8 之前,它可以看成是一个完全抽象的类,也就是说它不能有任何的方法实现。

+

从 Java 8 开始接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类,现在不用修改所有实现该接口的类.

+

接口的成员(字段 + 方法)默认都是public的,并且不允许定义为private或者 protected

+

接口的字段默认都是用staticfinal修饰的.

+
public interface InterfaceExample {
+
+    void func1();
+
+    default void func2(){
+        System.out.println("func2");
+    }
+
+    int x = 123;
+    // int y;               // Variable 'y' might not have been initialized
+    public int z = 0;       // Modifier 'public' is redundant for interface fields
+    // private int k = 0;   // Modifier 'private' not allowed here
+    // protected int l = 0; // Modifier 'protected' not allowed here
+    // private void fun3(); // Modifier 'private' not allowed here
+}
+public class InterfaceImplementExample implements InterfaceExample {
+    @Override
+    public void func1() {
+        System.out.println("func1");
+    }
+}
+
+// InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated
+InterfaceExample ie2 = new InterfaceImplementExample();
+ie2.func1();//func1
+System.out.println(InterfaceExample.x);//123
+

Java的接口可以多继承

+
interface Action extends Serializable,AutoCloseable {
+	// to do ...
+}
+
比较
+

从设计层面上看,抽象类提供了一种is-a关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 like-a 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 is-a 关系。 +抽象类是一种自下而上的思想,而接口是一种自上而下的思想。抽象类是将多个类的公共特点聚合到同一个类中,然后实现父类的方法;接口更像是对类的一种约束,其他类调用实现某接口的类。

+

从语法角度来看,一个类可以实现多个接口,但是不能继承多个抽象类;接口的字段只能是staticfinal类型的,而抽象类的字段没有这种限制,接口的成员只能是public的,而抽象类的成员可以有多种访问权限。

+
使用选择
+

使用接口

+
    +
  • 需要让不相关的类都实现一个方法,例如:不相关的类都可以实现 Compareable 接口中的 compareTo() 方法;
  • +
  • 需要使用多重继承。
  • +
+

使用抽象类

+
    +
  • 需要在几个相关的类中共享代码。
  • +
  • 需要能控制继承来的成员的访问权限,而不是都为public
  • +
  • 需要继承非静态和非常量字段。
  • +
+

在很多情况下,接口优先于抽象类。因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。

+

多态

+

多态即多种表现形态;同一个行为具有多个不同表现形式或形态的能力.

+

多态存在的前提

+
    +
  • 有类继承或者接口实现
  • +
  • 子类要重写父类的方法
  • +
  • 父类的引用指向子类的对象;例如:Parent p = new Child();
  • +
+

简单说来: 父类引用指向子类对象,调用方法时会调用子类的实现,而不是父类的实现,称为多态.

+
class Parent {
+
+    void contextLoads(){
+        System.out.println("i am Parent ... ");
+    }
+}
+class Child  extends Parent {
+    @Override
+    void contextLoads(){
+        System.out.println("i am Child ... ");
+    }
+}
+class mainTest{
+    public static void main(String[] args) {
+        Parent child = new Child();
+        // i am Child ... 
+        child.contextLoads();
+    }
+}
+

优缺点

+

优点

+
    +
  • 消除类型之间的耦合关系
  • +
  • 可替换性
  • +
  • 可扩充性
  • +
  • 接口性
  • +
  • 灵活性
  • +
  • 简化性
  • +
+

缺点

+

不能使用子类特有的方法和属性.在编写代码期间使用多态调用方法或属性时,编译工具首先会检查父类中是否有该方法和属性,如果没有,则会编译报错。

+
class Parent {
+    void contextLoads(){
+        System.out.println("i am Parent ... ");
+    }
+}
+class Child  extends Parent {
+
+    String  c = "child";
+
+    @Override
+    void contextLoads(){
+        System.out.println("i am Child ... ");
+    }
+    void test() {
+        System.out.println("i am test method ...");
+    }
+}
+class mainTest{
+    public static void main(String[] args) {
+        Parent child = new Child();
+        // 编译报错: 无法解析 'Parent' 中的方法 'test'
+        child.test();
+        // 编译报错: 不能解决符号 'c'
+        child.c;
+    }
+}
+

重写与重载

+
重写
+

重写(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法。

+
+

为了满足里式替换原则,重写有以下三个限制:

+
    +
  • 子类方法的访问权限必须大于等于父类方法;
  • +
  • 子类方法的返回类型必须是父类方法返回类型或为其子类型。
  • +
  • 子类方法抛出的异常类型必须是父类抛出异常类型或为其子类型。
  • +
+
+

使用@Override注解,可以让编译器帮忙检查是否满足上面的三个限制条件。

+

下面的示例中,SubClass SuperClass 的子类,SubClass 重写了 SuperClassfunc() 方法。其中:

+
    +
  • 子类方法访问权限为public,大于父类的protected
  • +
  • 子类的返回类型为ArrayList,是父类返回类型List的子类。
  • +
  • 子类抛出的异常类型为 Exception,是父类抛出异常 Throwable 的子类。
  • +
  • 子类重写方法使用@Override注解,从而让编译器自动检查是否满足限制条件。
  • +
+
class SuperClass {
+    protected List<Integer> func() throws Throwable {
+        return new ArrayList<>();
+    }
+}
+
+class SubClass extends SuperClass {
+    @Override
+    public ArrayList<Integer> func() throws Exception {
+        return new ArrayList<>();
+    }
+}
+

在调用一个方法时,先从本类中查找看是否有对应的方法,如果没有查找到再到父类中查看,看是否有继承来的方法。否则就要对参数进行转型,转成父类之后看是否有对应的方法。总的来说,方法调用的优先级为:

+
    +
  • this.func(this)
  • +
  • super.func(this)
  • +
  • this.func(super)
  • +
  • super.func(super)
  • +
+
/*继承关系
+    A
+    |
+    B
+    |
+    C
+    |
+    D
+ */
+
+
+class A {
+
+    public void show(A obj) {
+        System.out.println("A.show(A)");
+    }
+
+    public void show(C obj) {
+        System.out.println("A.show(C)");
+    }
+}
+
+class B extends A {
+
+    @Override
+    public void show(A obj) {
+        System.out.println("B.show(A)");
+    }
+}
+
+class C extends B {
+}
+
+class D extends C {
+}
+class  mainTest{
+    public static void main(String[] args) {
+
+        A a = new A();
+        B b = new B();
+        C c = new C();
+        D d = new D();
+
+        //  A 中存在 show(A obj),直接调用
+        a.show(a); // A.show(A)
+        //  A 中不存在 show(B obj),将 B 转型成其父类 A
+        a.show(b); // A.show(A)
+        //  B 中存在从 A 继承来的 show(C obj),直接调用
+        b.show(c); // A.show(C)
+        //  B 中不存在 show(D obj),但是存在从 A 继承来的 show(C obj),将 D 转型成其父类 C
+        b.show(d); // A.show(C)
+
+        // 引用的还是 B 对象,所以 ba  b 的调用结果一样
+        A ba = new B();
+        ba.show(c); // A.show(C)
+        ba.show(d); // A.show(C)
+    }
+}
+
重载
+

重载(overload)是在一个类里面,方法名字相同,但是参数类型、个数、顺序至少有一个不同。返回类型可以相同也可以不同。应该注意的是,返回值不同,其它都相同不算是重载。

+

每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。 最常用的地方就是构造器的重载。

+
+

重载规则:

+
    +
  • 被重载的方法必须改变参数列表(参数个数或类型不一样);
  • +
  • 被重载的方法可以改变返回类型;
  • +
  • 被重载的方法可以改变访问修饰符;
  • +
  • 被重载的方法可以声明新的或更广的检查异常;
  • +
  • 方法能够在同一个类中或者在一个子类中被重载。
  • +
  • 无法以返回值类型作为重载函数的区分标准。
  • +
+
+
public class Overloading {
+    public int test(){
+        System.out.println("test1");
+        return 1;
+    }
+ 
+    public void test(int a){
+        System.out.println("test2");
+    }   
+ 
+    //以下两个参数类型顺序不同
+    public String test(int a,String s){
+        System.out.println("test3");
+        return "returntest3";
+    }   
+ 
+    public String test(String s,int a){
+        System.out.println("test4");
+        return "returntest4";
+    }   
+ 
+    public static void main(String[] args){
+        Overloading o = new Overloading();
+        System.out.println(o.test());
+        o.test(1);
+        System.out.println(o.test(1,"test3"));
+        System.out.println(o.test("test4",1));
+    }
+}
+

方法的重写(Override)和重载(Overload)是Java多态性的不同表现,重写是父类与子类之间多态性的一种表现,重载可以理解成多态的具体表现形式

+
+
    +
  • 方法重载是一个类中定义了多个方法名相同,而他们的参数的数量不同或数量相同而类型和次序不同,则称为方法的重载(Overloading)。
  • +
  • 方法重写是在子类存在方法与父类的方法的名字相同,而且参数的个数与类型一样,返回值也一样的方法,就称为重写(Overriding)。
  • +
  • 方法重载是一个类的多态性表现,而方法重写是子类与父类的一种多态性表现。
  • +
+
+

设计模式

+
+

设计模式是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结,使用设计模式是为了可重用代码、让代码更容易被他人理解并且保证代码可靠性。

+

虽然GoF设计模式只有23个,但是它们各具特色,每个模式都为某一个可重复的设计问题提供了一套解决方案。根据它们的用途,设计模式可分为创建型,结构型和行为型三种,其中创建型模式主要用于描述如何创建对象,结构型模式主要用于描述如何实现类或对象的组合,行为型模式主要用于描述类或对象怎样交互以及怎样分配职责;在GoF23种设计模式中包含5种创建型设计模式、7种结构型设计模式和11种行为型设计模式。此外,根据某个模式主要是用于处理类之间的关系还是对象之间的关系,设计模式还可以分为类模式和对象模式。

+
+

设计模式是一套被反复使用的、多数人知晓的、经过分类编目、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性、高内聚低耦合。

+
+

设计模式练习网站: https://java-design-patterns.com/patterns

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
设计模式类型设计模式名称介绍学习难度使用频率
创建型模式(6种)单例模式保证一个类仅有一个对象,并提供一个访问它的全局访问点。★☆☆☆☆★★★★☆
简单工厂模式定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。★★☆☆☆★★★★★
工厂方法模式定义一个用于创建对象的接口,让子类决定将哪一个类实例化。★★☆☆☆★★★★★
抽象工厂模式提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们的具体类。★★★★☆★★★★★
原型模式使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。★★★☆☆★★★☆☆
建造者模式将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。★★★★☆★★☆☆☆
结构型模式(7种)适配器模式将一个类的接口转换成客户希望的另一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。★★☆☆☆★★★★☆
桥接模式将抽象部分与它的实现部分分离,使他们都可以独立地变化。★★★☆☆★★★☆☆
组合模式组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象和组合对象的使用具有一致性。★★★☆☆★★★★☆
装饰模式动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。★★★☆☆★★★☆☆
外观模式为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。★☆☆☆☆★★★★★
享元模式运用共享技术有效地支持大量细粒度的对象。★★★★☆★☆☆☆☆
代理模式为其他对象提供一个代理以控制对这个对象的访问。★★★☆☆★★★★☆
行为模式(11种) + 职责链模式为解除请求的发送者和接收者之间的耦合,而使多个对象都有机会处理这个请求。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它。★★★☆☆★★☆☆☆
命令模式将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可取消的操作。★★★☆☆★★★★☆
解释器模式定义一个语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。★★★★★★☆☆☆☆
迭代器模式提供一种方法顺序访问一个聚合对象中各个元素,而又不需暴露该对象的内部表示。★★★☆☆★★★★★
中介者模式用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。★★★☆☆★★☆☆☆
备忘录模式在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保持该状态,这样以后就可以将该对象恢复到保存的状态。★★☆☆☆★★☆☆☆
观察者模式定义对象间的一种一对多的依赖关系,以便当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并自动刷新。★★★☆☆★★★★★
状态模式允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它所属的类。★★★☆☆★★★☆☆
策略模式定义一系列的算法,把它们一个个封装起来,并且使他们可相互替换。本模式使得算法的变化可以独立于使用它的客户。★☆☆☆☆★★★★☆
模板方法模式定义一个操作中的算法的骨架,而将一些步骤延迟到子类。★★☆☆☆★★★☆☆
访问者模式表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类别的前提下定义作用于这些元素的新操作。★★★★☆★☆☆☆☆
+

单例模式

+
+

单例模式:确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。

+
+

单例模式设计就是采用一定的方法保证在整个程序中,对某个类只能存在一个对象的实例,并且该类只提供一个取得其对象实例的方法.

+
+

单例模式作用:

+
    +
  • 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如网站首页页面缓存).
  • +
  • 避免对资源的多重占用(比如写文件操作).
  • +
+
+

单例模式使用的场景:需要频繁的进行创建和销毁的对象、创建对象时耗时过多或耗费资源过多(即:重量级对象),但又经常用到的对象、工具类对象、频繁访问数据库或文件的对象(比如数据源、session 工厂等)

+

单例模式的6种写法.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
写法名称优点缺点
饿汉式线程安全,写法简单不懒加载,可能造成浪费
懒汉式(线程不安全)懒加载线程不安全
懒汉式(线程安全)线程安全,懒加载效率很低,反序列化破坏单例
双重校验锁线程安全,懒加载反序列化破坏单例
静态内部类式线程安全,懒加载反序列化破坏单例
枚举式防止反射攻击,反序列化创建对象,写法简单不能传参,继承其他类
+

饿汉式

+
class Singleton {
+
+    private Singleton() {}
+
+    private static final Singleton instance = new Singleton();
+
+    public static Singleton getInstance() {
+        return instance;
+    }
+}
+

这种写法比较简单,就是在类加载的时候就完成实例化。避免了线程同步问题。但是在类装载的时候就完成实例化,没有达到懒加载的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费.

+

线程不安全的懒汉式

+
class Singleton {
+    private Singleton() {
+    }
+
+    private static Singleton instance;
+
+    public static Singleton getInstance() {
+        if (instance == null) {
+            instance = new Singleton();
+        }
+        return instance;
+    }
+}
+

起到了懒加载的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式.

+

线程安全的懒汉式

+
class Singleton {
+
+    private static Singleton instance;
+
+    private Singleton() {
+    }
+
+    public static synchronized Singleton getInstance() {
+        if (instance == null) {
+            instance = new Singleton();
+        }
+        return instance;
+    }
+}
+

虽然解决了线程安全问题但是效率太低了,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return就行了。

+

双重校验锁

+
class Singleton {
+
+    /**
+     * volatile在这作用: 禁止JVM指令重排
+     */
+    private static volatile Singleton instance;
+
+    private Singleton() {
+    }
+
+    public static Singleton getInstance() {
+        if (instance == null) {
+            synchronized (Singleton.class) {
+                if (instance == null) {
+                    instance = new Singleton();
+                }
+            }
+        }
+        return instance;
+    }
+}
+

Double-Check概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。这样,实例化代码只用执行一次,后面再次访问时,判断if (singleton == null),直接return实例化对象,也避免的反复进行方法同步.

+

静态内部类式

+
class Singleton {
+
+    private Singleton() {}
+
+    private static class InnerClass {
+        private static final Singleton instance = new Singleton();
+    }
+    public static Singleton getInstance() {
+        return InnerClass.instance;
+    }
+}
+
+

这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟饿汉式不同的是(很细微的差别):饿汉式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比饿汉式更加合理。

+
+

枚举式

+
enum Singleton {
+    INSTAMCE;
+
+    Singleton() {
+    }
+}
+

这种方式是《Effective Java》作者Josh Bloch提倡的方式.借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。

+

单例与序列化

+

上述单例模式6种写法除了,枚举式单例外其他5种写法都存在序列化问题.序列化可以破坏单例.原因是序列化会通过反射调用无参数的构造方法创建一个新的对象.

+

要想防止序列化对单例的破坏,只要在单例类中定义readResolve方法就可以解决该问题.原因是反序列化时,会通过反射的方式调用要被反序列化的类的readResolve方法 +主要在Singleton类中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

+

DCL为例,在该单例类中插入readResolve方法。

+
public class Singleton implements Serializable{
+
+    private volatile static Singleton singleton;
+    
+    private Singleton (){}
+    
+    public static Singleton getSingleton() {
+        if (singleton == null) {
+            synchronized (Singleton.class) {
+                if (singleton == null) {
+                    singleton = new Singleton();
+                }
+            }
+        }
+        return singleton;
+    }
+
+    private Object readResolve() {
+        return singleton;
+    }
+}
+

工厂模式

+

工厂模式是将实例化的对象代码提取出来,放到一个类中统一管理和维护,达到和主项目的依赖关系的解耦,从而提高项目的扩展和维护性.创建对象实例时,不要直接new类而是把这个new类的动作放在一个工厂的方法中,并返回.不要让类继承具体的类,而是继承抽象类或者实现接口.

+

详情查看以下三种工厂设计模式.

+

简单工厂模式

+
+

简单工厂模式:定义一个工厂类,它可以根据参数的不同返回不同类的实例,被创建的实例通常都具有共同的父类。因为在简单工厂模式中用于创建实例的方法是静态方法,因此简单工厂模式又被称为静态工厂方法模式,它属于类创建型模式。

+
+
public class SimpleFactoryDemo {
+    public static void main(String[] args) {
+        SimpleFactory.getTest(1);
+    }
+}
+
+class SimpleFactoryImpl1 implements SimpleFactoryInterface {
+
+    @Override
+    public void test() {
+        System.out.println("i am  simpleFactory 1 ...");
+    }
+}
+class SimpleFactoryImpl2 implements SimpleFactoryInterface {
+
+    @Override
+    public void test() {
+        System.out.println("i am  simpleFactory 2 ...");
+    }
+}
+
+class SimpleFactory {
+    public static void getTest(int n) {
+        switch (n) {
+            case 1:
+                SimpleFactoryImpl1 simpleFactory = new SimpleFactoryImpl1();
+                simpleFactory.test();
+                break;
+            case 2:
+                SimpleFactoryImpl2 simpleFactoryImpl2 = new SimpleFactoryImpl2();
+                simpleFactoryImpl2.test();
+                break;
+            default:
+        }
+    }
+}
+

简单工厂模式总结:

+
    +
  • 优点 实现了对象创建和使用的分离,代码解耦.符合面向接口(指对外暴露的接口或类)编程思想
  • +
  • 缺点 如果代码逻辑,职责过多,则简单工厂会变的十分臃肿. +系统代码扩展困难.一旦添加新产品就不得不修改工厂逻辑,在产品类型较多时,有可能造成工厂逻辑过于复杂,不利于系统的扩展和维护.
  • +
+

简单工厂模式适用于创建的对象比较少,业务逻辑不太复杂的情景

+

工厂方法模式

+
+

工厂方法模式:定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式,又可称作虚拟构造器模式或多态工厂模式。工厂方法模式是一种类创建型模式。

+
+
public class FactoryMethodDemo {
+    public static void main(String[] args) {
+        FoodFactory f = new ColdRiceNoodleFactory();
+        f.getFood().eat();
+        // 扩展需要增加产品及相应产品工厂并实现相关接口
+    }
+}
+
+interface Food {
+    void eat();
+}
+interface FoodFactory {
+    Food getFood();
+}
+
+class RiceNoodle implements Food{
+
+    @Override
+    public void eat() {
+        System.out.println("eat rice noodle ...");
+    }
+}
+class RiceNoodleFactory implements FoodFactory{
+
+    @Override
+    public Food getFood() {
+        return new RiceNoodle();
+    }
+}
+
+class ColdRiceNoodle implements Food {
+
+    @Override
+    public void eat() {
+        System.out.println("eat cold rice noodle ...");
+    }
+}
+class ColdRiceNoodleFactory implements FoodFactory {
+
+    @Override
+    public Food getFood() {
+        return new ColdRiceNoodle();
+    }
+}
+

工厂方法模式是简单工厂模式的延伸,它继承了简单工厂模式的优点,同时还弥补了简单工厂模式的不足. +使用工厂方法模式扩展时,无须修改抽象工厂和抽象产品提供的接口,无须修改客户端,也无须修改其他的具体工厂和具体产品,而只要添加一个具体工厂和具体产品就可以了,这样,系统的可扩展性也就变得非常好,完全符合“开闭原则”。

+

但是在添加新产品时,需要编写新的具体产品类,而且还要提供与之对应的具体工厂类,系统中类的个数将成对增加,在一定程度上增加了系统的复杂度,有更多的类需要编译和运行,会给系统带来一些额外的开销。

+

抽象工厂模式

+
+

抽象工厂模式:提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,它是一种对象创建型模式。

+
+
interface Food {
+    void eat();
+}
+
+interface FoodFactory {
+    ColdRiceNoodle getColdRiceNoodle();
+    RiceNoodle getRiceNoodle();
+}
+class RiceNoodle implements Food{
+
+    @Override
+    public void eat() {
+        System.out.println("eating rice noodle");
+    }
+}
+class ColdRiceNoodle implements Food{
+
+    @Override
+    public void eat() {
+        System.out.println("eating cold rice noodle");
+    }
+}
+class RiceNoodleFactory implements FoodFactory {
+
+
+    @Override
+    public ColdRiceNoodle getColdRiceNoodle() {
+        return new ColdRiceNoodle();
+    }
+
+    @Override
+    public RiceNoodle getRiceNoodle() {
+        return new RiceNoodle();
+    }
+}
+
+class ColdRiceNoodleFactory implements FoodFactory {
+
+    @Override
+    public ColdRiceNoodle getColdRiceNoodle() {
+        return new ColdRiceNoodle();
+    }
+
+    @Override
+    public RiceNoodle getRiceNoodle() {
+        return new RiceNoodle();
+    }
+}
+

抽象工厂模式是工厂方法模式的进一步延伸,仍然具有工厂方法和简单工厂的优点.抽象工厂模式隔离了具体类的生成,使得客户并不需要知道什么被创建。由于这种隔离,更换一个具体工厂就变得相对容易,所有的具体工厂都实现了抽象工厂中定义的那些公共接口,因此只需改变具体工厂的实例,就可以在某种程度上改变整个软件系统的行为.

+

但是抽象工厂也存在一些缺点,增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了“开闭原则”。

+

原型模式

+

在Java中通过new关键字创建的对象是非常繁琐的,在我们需要大量对象的情况下,原型模式就是我们可以考虑实现的方式.

+
+

原型模式:使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式

+
+

原型模式我们也称为克隆模式,即一个某个对象为原型克隆出来一个一模一样的对象,该对象的属性和原型对象一模一样。而且对于原型对象没有任何影响。原型模式的克隆方式有两种:浅克隆和深度克隆;浅克隆和深克隆的主要区别在于是否支持引用类型的成员变量的复制.

+

浅克隆

+

在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制.

+

代码实现

+
public class ShallowClone {
+    public static void main(String[] args) {
+        CloneHuman cloneHuman = new CloneHuman("黑色","大眼睛","高鼻梁","大嘴巴",new Date(123231231231L));
+        for (int i = 0; i < 20; i++) {
+            try {
+                CloneHuman clone = (CloneHuman)cloneHuman.clone();
+                System.out.printf("头发:%s,眼睛:%s,鼻子:%s,嘴巴:%s,生日:%s",clone.getHair(),clone.getEye(),clone.getNodes(),clone.getMouse(),clone.getBirth());
+                System.out.println();
+                System.out.println("浅克隆,引用类型地址比较:" + (cloneHuman.getBirth() == clone.getBirth()));
+            } catch (CloneNotSupportedException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+}
+
+class CloneHuman  implements Cloneable {
+
+    private String hair;
+
+    private String eye;
+
+    private String nodes;
+
+    private String mouse;
+
+    private Date birth;
+
+    public String getHair() {
+        return hair;
+    }
+
+    public void setHair(String hair) {
+        this.hair = hair;
+    }
+
+    public String getEye() {
+        return eye;
+    }
+
+    public void setEye(String eye) {
+        this.eye = eye;
+    }
+
+    public String getNodes() {
+        return nodes;
+    }
+
+    public void setNodes(String nodes) {
+        this.nodes = nodes;
+    }
+
+    public String getMouse() {
+        return mouse;
+    }
+
+    public void setMouse(String mouse) {
+        this.mouse = mouse;
+    }
+
+    public CloneHuman(String hair, String eye, String nodes, String mouse,Date brith) {
+        this.hair = hair;
+        this.eye = eye;
+        this.nodes = nodes;
+        this.mouse = mouse;
+        this.birth = brith;
+    }
+
+    @Override
+    protected Object clone() throws CloneNotSupportedException {
+        return super.clone();
+    }
+
+    public Date getBirth() {
+        return birth;
+    }
+
+    public void setBirth(Date birth) {
+        this.birth = birth;
+    }
+}
+
+

PS Java语言提供的Cloneable接口和Serializable接口的代码非常简单,它们都是空接口,这种空接口也称为标识接口,标识接口中没有任何方法的定义,其作用是告诉JRE这些接口的实现类是否具有某个功能,如是否支持克隆、是否支持序列化等。

+
+

应该注意的是,clone()方法并不是Cloneable接口的方法,而是Object的一个protected方法。Cloneable接口只是规定,如果一个类没有实现Cloneable接口又调用了clone()方法,就会抛出 CloneNotSupportedException

+

深克隆

+

在深克隆中,除了对象本身被复制外,对象所包含的所有成员变量也将复制. +深克隆有两种实现方式,第一种是在浅克隆的基础上实现,第二种是通过序列化和反序列化实现

+

在浅克隆的基础上实现

+
public class DeepClone {
+    public static void main(String[] args) {
+        CloneHuman cloneHuman = new CloneHuman("黑色","大眼睛","高鼻梁","大嘴巴",new Date(123231231231L));
+        for (int i = 0; i < 20; i++) {
+            try {
+                CloneHuman clone = (CloneHuman)cloneHuman.clone();
+                System.out.printf("头发:%s,眼睛:%s,鼻子:%s,嘴巴:%s,生日:%s",clone.getHair(),clone.getEye(),clone.getNodes(),clone.getMouse(),clone.getBirth());
+                System.out.println();
+                System.out.println("深克隆,引用类型地址比较:" + (cloneHuman.getBirth() == clone.getBirth()));
+            } catch (CloneNotSupportedException e) {
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+}
+class CloneHuman  implements Cloneable {
+
+    private String hair;
+
+    private String eye;
+
+    private String nodes;
+
+    private String mouse;
+
+    private Date birth;
+
+    public String getHair() {
+        return hair;
+    }
+
+    public void setHair(String hair) {
+        this.hair = hair;
+    }
+
+    public String getEye() {
+        return eye;
+    }
+
+    public void setEye(String eye) {
+        this.eye = eye;
+    }
+
+    public String getNodes() {
+        return nodes;
+    }
+
+    public void setNodes(String nodes) {
+        this.nodes = nodes;
+    }
+
+    public String getMouse() {
+        return mouse;
+    }
+
+    public void setMouse(String mouse) {
+        this.mouse = mouse;
+    }
+
+    public CloneHuman(String hair, String eye, String nodes, String mouse,Date brith) {
+        this.hair = hair;
+        this.eye = eye;
+        this.nodes = nodes;
+        this.mouse = mouse;
+        this.birth = brith;
+    }
+
+    @Override
+    protected Object clone() throws CloneNotSupportedException {
+        CloneHuman human = (CloneHuman)super.clone();
+        human.birth = (Date)this.birth.clone();
+        return human;
+    }
+
+    public Date getBirth() {
+        return birth;
+    }
+
+    public void setBirth(Date birth) {
+        this.birth = birth;
+    }
+}
+

序列化反序列化实现深克隆

+
public class DeepClone2 {
+    public static void main(String[] args) throws IOException, ClassNotFoundException {
+        CloneHuman2 cloneHuman1 = new CloneHuman2("黑色","大眼睛","高鼻梁","大嘴巴",new Date(123231231231L));
+
+        // 使用序列化和反序列化实现深克隆
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ObjectOutputStream oos = new ObjectOutputStream(bos);
+        oos.writeObject(cloneHuman1);
+        byte[] bytes = bos.toByteArray();
+
+        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
+        ObjectInputStream ois = new ObjectInputStream(bis);
+
+        // 克隆好的对象
+        CloneHuman2 cloneHuman2 = (CloneHuman2) ois.readObject();
+        System.out.println("深克隆,引用类型地址比较:" + (cloneHuman1.getBirth() == cloneHuman2.getBirth()));
+
+    }
+}
+class CloneHuman2  implements Cloneable, Serializable {
+
+    private String hair;
+
+    private String eye;
+
+    private String nodes;
+
+    private String mouse;
+
+    private Date birth;
+
+    public String getHair() {
+        return hair;
+    }
+
+    public void setHair(String hair) {
+        this.hair = hair;
+    }
+
+    public String getEye() {
+        return eye;
+    }
+
+    public void setEye(String eye) {
+        this.eye = eye;
+    }
+
+    public String getNodes() {
+        return nodes;
+    }
+
+    public void setNodes(String nodes) {
+        this.nodes = nodes;
+    }
+
+    public String getMouse() {
+        return mouse;
+    }
+
+    public void setMouse(String mouse) {
+        this.mouse = mouse;
+    }
+
+    public CloneHuman2(String hair, String eye, String nodes, String mouse,Date brith) {
+        this.hair = hair;
+        this.eye = eye;
+        this.nodes = nodes;
+        this.mouse = mouse;
+        this.birth = brith;
+    }
+
+    @Override
+    protected Object clone() throws CloneNotSupportedException {
+        CloneHuman2 human = (CloneHuman2)super.clone();
+        human.birth = (Date)this.birth.clone();
+        return human;
+    }
+
+    public Date getBirth() {
+        return birth;
+    }
+
+    public void setBirth(Date birth) {
+        this.birth = birth;
+    }
+}
+

总结

+

原型模式作为一种快速创建大量相同或相似对象的方式,在软件开发中应用较为广泛,很多软件提供的复制和粘贴操作就是原型模式的典型应用.

+

通过clone的方式在获取大量对象的时候性能开销基本没有什么影响,而new的方式随着实例的对象越来越多,性能会急剧下降,所以原型模式是一种比较重要的获取实例的方式.

+

优点

+
    +
  • 当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,提高创建对象的效率。
  • +
  • 可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,可辅助实现撤销操作。
  • +
  • 扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。
  • +
+

缺点

+
    +
  • 需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。
  • +
  • 在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。
  • +
+

使用场景

+

原型模式很少单独出现,一般是和工厂方法模式一起出现,通过clone的方法创建一个对象,然后由工厂方法提供给调用者。 +spring中bean的创建实际就是两种:单例模式和原型模式。

+
    +
  • 创建新对象成本较大,新的对象可以通过原型模式对已有对象进行复制来获得。
  • +
  • 系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。
  • +
+

建造者模式

+
+

建造者模式:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一种对象创建型模式。

+
+

建造者模式是较为复杂的创建型模式,它将客户端与包含多个组成部分(或部件)的复杂对象的创建过程分离,客户端无须知道复杂对象的内部组成部分与装配方式,只需要知道所需建造者的类型即可。它关注如何一步一步创建一个的复杂对象,不同的具体建造者定义了不同的创建过程,且具体建造者相互独立,增加新的建造者非常方便,无须修改已有代码,系统具有较好的扩展性。

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+
+        Director director = new Director();
+        Builder commonBuilder = new CommonRole();
+
+        director.construct(commonBuilder);
+        Role commonRole = commonBuilder.getRole();
+        System.out.println(commonRole);
+    }
+}
+
+class Role {
+    private String head;
+    private String body;
+    private String foot;
+    private String sp;
+    private String hp;
+    private String name;
+
+    public void setSp(String sp) {
+        this.sp = sp;
+    }
+
+    public String getSp() {
+        return sp;
+    }
+
+    public void setHp(String hp) {
+        this.hp = hp;
+    }
+
+    public String getHp() {
+        return hp;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    @Override
+    public String toString() {
+        return "Role{" +
+                "head='" + head + '\'' +
+                ", body='" + body + '\'' +
+                ", foot='" + foot + '\'' +
+                ", sp='" + sp + '\'' +
+                ", hp='" + hp + '\'' +
+                ", name='" + name + '\'' +
+                '}';
+    }
+}
+
+abstract class Builder {
+
+    public abstract void builderHead();
+
+    public abstract void builderBody();
+
+    public abstract void builderFoot();
+
+    public abstract void builderSp();
+
+    public abstract void builderHp();
+
+    public abstract void builderName();
+
+    public Role getRole() {
+        return new Role();
+    }
+}
+
+class CommonRole extends Builder {
+
+    private Role role = new Role();
+
+    @Override
+    public void builderHead() {
+        System.out.println("building head .....");
+    }
+
+    @Override
+    public void builderBody() {
+        System.out.println("building body .....");
+    }
+
+    @Override
+    public void builderFoot() {
+        System.out.println("building foot .....");
+    }
+
+    @Override
+    public void builderSp() {
+        role.setSp("100");
+    }
+
+    @Override
+    public void builderHp() {
+        role.setHp("100");
+    }
+
+    @Override
+    public void builderName() {
+        role.setName("lucy");
+    }
+
+    @Override
+    public Role getRole() {
+        return role;
+    }
+}
+class Director {
+
+    public void construct(Builder builder) {
+        builder.builderHead();
+        builder.builderBody();
+        builder.builderFoot();
+        builder.builderHp();
+        builder.builderSp();
+        builder.builderName();
+    }
+}
+

优点

+
    +
  • 客户端不必知道产品内部组成的细节,将产品本身与产品的创建过程解耦,使得相同的创建过程可以创建不同的产品对象。
  • +
  • 建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成,基本上不用修改之前已经测试通过的代码,因此也就不会对原有功能引入风险。符合开放封闭原则
  • +
+

缺点

+
    +
  • 若产品内部发生变化,建造者都要修改,成本较大;若内部变化复杂,会有很多的建造类。
  • +
  • 产品必须有共同点,使用范围有限。建造者模式创造出来的产品,其组成部分基本相同。如果产品之间的差异较大,则不适用这个模式。
  • +
+

使用场景

+
    +
  • 需要生成的产品对象有复杂的内部结构,这些产品对象通常包含多个成员属性。
  • +
  • 需要生成的产品对象的属性相互依赖,需要指定其生成顺序。
  • +
  • 对象的创建过程独立于创建该对象的类。在建造者模式中引入了指挥者类,将创建过程封装在指挥者类中,而不在建造者类中。
  • +
  • 隔离复杂对象的创建和使用,并使得相同的创建过程可以创建不同的产品。
  • +
+

适配器模式

+
+

适配器模式:将一个接口转换成客户希望的另一个接口(指广义的接口,它可以表示一个方法或者方法的集合),使接口不兼容的那些类可以一起工作,其别名为包装器。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

+
+

在适配器模式中,我们通过增加一个新的适配器类来解决接口不兼容的问题,使得原本没有任何关系的类可以协同工作。根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适配器两种,在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。

+

对象适配器

+

由于在Java中不支持多重继承,而且有破坏封装之嫌。所以提倡多用组合少用继承,在实际开发中推荐使用对象适配器模式。

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        new RedHat(new Linux()).useInputMethod();
+        System.out.println("==============");
+
+        new Win10(new Windows()).useInputMethod();
+        System.out.println("==============");
+
+        Adapter adapter = new Adapter(new Windows());
+        new RedHat(adapter).useInputMethod();
+    }
+}
+
+interface LinuxSoftware {
+    void inputMethod();
+}
+
+interface WindowsSoftware {
+    void inputMethod();
+}
+
+class Linux implements LinuxSoftware {
+
+    @Override
+    public void inputMethod() {
+        System.out.println("linux 系统输入法 ...");
+    }
+}
+
+class Windows implements WindowsSoftware {
+
+    @Override
+    public void inputMethod() {
+        System.out.println("windows 系统输入法 ...");
+    }
+}
+
+class RedHat {
+    private LinuxSoftware linuxSoftware;
+
+    public RedHat(LinuxSoftware linuxSoftware) {
+        this.linuxSoftware = linuxSoftware;
+    }
+
+    public void useInputMethod() {
+        System.out.println("开始使用 redHat 系统输入法 ...");
+        linuxSoftware.inputMethod();
+        System.out.println("结束使用 redHat 系统输入法 ...");
+    }
+}
+
+class Win10 {
+    private WindowsSoftware windowsSoftware;
+
+    public Win10(WindowsSoftware windowsSoftware) {
+        this.windowsSoftware = windowsSoftware;
+    }
+
+    public void useInputMethod() {
+        System.out.println("开始使用 win10 系统输入法 ...");
+        windowsSoftware.inputMethod();
+        System.out.println("结束使用 win10 系统输入法 ...");
+    }
+}
+
+// 在 Linux 系统上使用 windows 输入法
+class Adapter implements LinuxSoftware{
+
+    private WindowsSoftware windowsSoftware;
+
+    public Adapter(WindowsSoftware windowsSoftware) {
+        this.windowsSoftware = windowsSoftware;
+    }
+
+    @Override
+    public void inputMethod() {
+        windowsSoftware.inputMethod();
+    }
+}
+

类适配器

+

类适配器模式和对象适配器模式最大的区别在于适配器和适配者之间的关系不同,对象适配器模式中适配器和适配者之间是关联关系,而类适配器模式中适配器和适配者是继承关系。

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        new RedHat(new Linux()).useInputMethod();
+        System.out.println("==============");
+
+        new Win10(new Windows()).useInputMethod();
+        System.out.println("==============");
+
+        Adapter adapter = new Adapter();
+        new RedHat(adapter).useInputMethod();
+    }
+}
+
+interface LinuxSoftware {
+    void inputMethod();
+}
+
+interface WindowsSoftware {
+    void inputMethod();
+}
+
+class Linux implements LinuxSoftware {
+
+    @Override
+    public void inputMethod() {
+        System.out.println("linux 系统输入法 ...");
+    }
+}
+
+class Windows implements WindowsSoftware {
+
+    @Override
+    public void inputMethod() {
+        System.out.println("windows 系统输入法 ...");
+    }
+}
+
+class RedHat {
+    private LinuxSoftware linuxSoftware;
+
+    public RedHat(LinuxSoftware linuxSoftware) {
+        this.linuxSoftware = linuxSoftware;
+    }
+
+    public void useInputMethod() {
+        System.out.println("开始使用 redHat 系统输入法 ...");
+        linuxSoftware.inputMethod();
+        System.out.println("结束使用 redHat 系统输入法 ...");
+    }
+}
+
+class Win10 {
+    private WindowsSoftware windowsSoftware;
+
+    public Win10(WindowsSoftware windowsSoftware) {
+        this.windowsSoftware = windowsSoftware;
+    }
+
+    public void useInputMethod() {
+        System.out.println("开始使用 win10 系统输入法 ...");
+        windowsSoftware.inputMethod();
+        System.out.println("结束使用 win10 系统输入法 ...");
+    }
+}
+
+//  Linux 系统上使用 windows 输入法
+class Adapter extends Windows implements LinuxSoftware{
+    @Override
+    public void inputMethod() {
+        super.inputMethod();
+    }
+}
+

总结

+

适配器模式将现有接口转化为客户类所期望的接口,实现了对现有类的复用,它是一种使用频率非常高的设计模式,在软件开发中得以广泛应用,在Spring等开源框架、驱动程序设计(如JDBC中的数据库驱动程序)中也使用了适配器模式。

+

优点

+
    +
  • 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。
  • +
  • 增加了类的透明性和复用性,同时系统的灵活性和扩展性都非常好,更换适配器或者增加新的适配器都非常方便。可以在不修改原有代码的基础上增加新的适配器类,完全符合开放封闭原则
  • +
+

缺点

+
    +
  • 过多地使用适配器,会让系统非常零乱,不易整体进行把握。比如,明明看到调用的是 A 接口,其实内部被适配成了 B 接口的实现,一个系统如果太多出现这种情况,无异于一场灾难。因此如果不是很有必要,可以不使用适配器。
  • +
  • 对象适配器模式的缺点是很难置换适配者类的方法。
  • +
  • 类适配器模式中的目标抽象类只能为接口,不能为类,其使用有一定的局限性。
  • +
+

使用场景

+

系统需要使用现有的类,而这些类的接口不符合系统的需要;想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类一起工作。

+
    +
  • 系统需要使用现有的类,而此类的接口不符合系统的需要。
  • +
  • 想要建立一个可以重复使用的类,用于与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作,这些源类不一定有一致的接口。
  • +
  • 通过接口转换,将一个类插入另一个类系中。(比如老虎和飞禽,现在多了一个飞虎,在不增加实体的需求下,增加一个适配器,在里面包容一个虎对象,实现飞的接口。)
  • +
+

桥接模式

+
+

桥接模式:将实现与抽象放在两个不同的类层次中,使两个层次可以独立改变。它是一种对象结构型模式,又称为柄体模式或接口模式。

+
+

桥接模式是一种很实用的结构型设计模式,如果软件系统中某个类存在两个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。

+

例如像手机制造:内存是一个公司生产,芯片是另一个公司生产,而品牌又是另一个公司。我们需要什么样子的手机就把相应的芯片、内存组装起来。桥接模式就是把两个不同维度的东西桥接起来。

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        Phone_A phone_a = new Phone_A();
+        phone_a.setAbstractChip(new Chip_A());
+        phone_a.setAbstractMemory(new Memory_A());
+        phone_a.finished();
+        System.out.println("=================");
+        Phone_B phone_b = new Phone_B();
+        phone_b.setAbstractChip(new Chip_B());
+        phone_b.setAbstractMemory(new Memory_A());
+        phone_b.finished();
+    }
+}
+abstract class AbstractMemory {
+    protected abstract void size();
+}
+
+abstract class AbstractChip {
+    protected abstract void type();
+}
+abstract class  AbstractPhone {
+    protected AbstractMemory abstractMemory;
+    protected AbstractChip abstractChip;
+
+    public void setAbstractChip(AbstractChip abstractChip) {
+        this.abstractChip = abstractChip;
+    }
+
+    public void setAbstractMemory(AbstractMemory abstractMemory) {
+        this.abstractMemory = abstractMemory;
+    }
+
+    protected abstract void finished();
+}
+
+class Memory_A  extends AbstractMemory{
+
+    @Override
+    protected void size() {
+        System.out.println("create 6G of memory ...");
+    }
+}
+class Memory_B  extends AbstractMemory{
+
+    @Override
+    protected void size() {
+        System.out.println("create 8G of memory ...");
+    }
+}
+
+class Chip_A extends AbstractChip {
+
+    @Override
+    protected void type() {
+        System.out.println("snapdragon 888 chip ...");
+    }
+}
+
+class Chip_B extends AbstractChip {
+
+    @Override
+    protected void type() {
+        System.out.println("A14 chip ...");
+    }
+}
+
+class Phone_A  extends AbstractPhone{
+
+    @Override
+    protected void finished() {
+        abstractMemory.size();
+        abstractChip.type();
+        System.out.println("phone_a assembly completed ...");
+    }
+}
+
+class Phone_B  extends AbstractPhone{
+
+    @Override
+    protected void finished() {
+        abstractMemory.size();
+        abstractChip.type();
+        System.out.println("phone_b assembly completed ...");
+    }
+}
+

桥接模式是设计Java虚拟机和实现JDBC等驱动程序的核心模式之一,应用较为广泛。在软件开发中如果一个类或一个系统有多个变化维度时,都可以尝试使用桥接模式对其进行设计。桥接模式为多维度变化的系统提供了一套完整的解决方案,并且降低了系统的复杂度。

+

优点

+
    +
  • 实现了抽象和实现部分的分离,从而极大的提供了系统的灵活性,让抽象部分和实现部分独立开来,这有助于系统进行分层设计,从而产生更好的结构化系统。
  • +
  • 优秀的扩展能力。在两个变化维度中任意扩展一个维度,都不需要修改原有系统,符合开放封闭原则
  • +
  • 桥接模式替代多层继承方案,可以减少子类的个数,降低系统的管理和维护成本。
  • +
+

缺点

+
    +
  • 桥接模式的引入会增加系统的理解与设计难度,由于聚合关联关系建立在抽象层,要求开发者针对抽象进行设计与编程。
  • +
  • 桥接模式要求正确识别出系统中两个独立变化的维度(抽象、和实现),因此其使用范围有一定的局限性,即需要有这样的应用场景,如何正确识别两个独立维度也需要一定的经验积累。
  • +
+

使用场景

+
    +
  • 桥接模式主要解决在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。对于两个独立变化的维度,使用桥接模式再适合不过了。
  • +
  • 如果一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性,避免在两个层次之间建立静态的继承联系,通过桥接模式可以使它们在抽象层建立一个关联关系。
  • +
+

组合模式

+
+

组合模式:组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象和组合对象的使用具有一致性,组合模式又可以称为“整体—部分”模式,它是一种对象结构型模式。

+
+

组合模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        Tree tree = new Tree();
+        tree.add(new LeftNode());
+        tree.add(new RightNode());
+        tree.implmethod();
+    }
+}
+
+abstract class AbstractTree {
+    protected abstract void add(AbstractTree node);
+
+    protected abstract void remove(AbstractTree node);
+
+    protected abstract void implmethod();
+}
+
+class LeftNode extends AbstractTree {
+
+    @Override
+    protected void add(AbstractTree node) {
+        System.out.println("Exception: the method is not supported. ");
+    }
+
+    @Override
+    protected void remove(AbstractTree node) {
+        System.out.println("Exception: the method is not supported. ");
+    }
+
+    @Override
+    protected void implmethod() {
+        System.out.println("left node method ...");
+    }
+}
+
+class RightNode extends AbstractTree {
+
+    @Override
+    protected void add(AbstractTree node) {
+        System.out.println("Exception: the method is not supported. ");
+    }
+
+    @Override
+    protected void remove(AbstractTree node) {
+        System.out.println("Exception: the method is not supported. ");
+    }
+
+    @Override
+    protected void implmethod() {
+        System.out.println("right node method ...");
+    }
+}
+
+class Tree extends AbstractTree {
+
+    private ArrayList<AbstractTree> treeList = new ArrayList<>();
+
+
+    @Override
+    protected void add(AbstractTree node) {
+        treeList.add(node);
+    }
+
+    @Override
+    protected void remove(AbstractTree node) {
+        treeList.remove(node);
+    }
+
+    @Override
+    protected void implmethod() {
+        System.out.println("tree node");
+        for (AbstractTree node : treeList) {
+            node.implmethod();
+        }
+    }
+}
+

组合模式使用面向对象的思想来实现树形结构的构建与处理,描述了如何将容器对象和叶子对象进行递归组合,实现简单,灵活性好。由于在软件开发中存在大量的树形结构,因此组合模式是一种使用频率较高的结构型设计模式。在XML解析、组织结构树处理、文件系统设计等领域,组合模式都得到了广泛应用。

+

优点

+
    +
  • 高层模块调用简单。客户端可以一致地使用一个组合结构或其中单个对象,不必关心处理的是单个对象还是整个组合结构。
  • +
  • 在组合模式中节点增加自由,无须对现有类库进行任何修改,符合开闭原则
  • +
+

缺点

+
    +
  • 在使用组合模式时,其叶子和树的声明都是实现类,而不是接口,违反了依赖倒置原则
  • +
+

使用场景

+
    +
  • 部分、整体场景,如树形菜单,文件、文件夹的管理。
  • +
  • 希望用户忽略组合对象与单个对象的不同,用户将统一地使用组合结构中的所有对象。
  • +
+

装饰模式

+
+

装饰模式:动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。

+
+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        Drink coffee = new ShortBlock();
+        System.out.println("订单价格:"+ coffee.cost());
+        System.out.println("订单描述:" + coffee.getDesc());
+        System.out.println("=====================");
+        coffee = new Chocolate(coffee);
+        System.out.println("订单价格:"+ coffee.cost());
+        System.out.println("订单描述:" + coffee.getDesc());
+        System.out.println("=====================");
+        coffee = new Milk(coffee);
+        System.out.println("订单价格:"+ coffee.cost());
+        System.out.println("订单描述:" + coffee.getDesc());
+    }
+}
+
+abstract class Drink {
+
+    public String desc;
+
+    private double price = 0.0;
+
+    public void setDesc(String desc) {
+        this.desc = desc;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    public void setPrice(double price) {
+        this.price = price;
+    }
+
+    public double getPrice() {
+        return price;
+    }
+
+    protected abstract double cost();
+}
+
+class ShortBlock extends Drink{
+
+    public ShortBlock() {
+        super.setPrice(3.0);
+        super.setDesc("ShortBlock");
+    }
+
+    @Override
+    protected double cost() {
+        return super.getPrice();
+    }
+}
+
+class LongBlock extends Drink {
+
+    public LongBlock() {
+        super.setPrice(5.0);
+        super.setDesc("LongBlock");
+    }
+
+    @Override
+    protected double cost() {
+        return super.getPrice();
+    }
+}
+
+class CoffeeShop extends Drink {
+
+    private final Drink coffee;
+
+    protected CoffeeShop(Drink coffee) {
+        this.coffee = coffee;
+    }
+
+    @Override
+    protected double cost() {
+        return super.getPrice() + coffee.cost();
+    }
+
+    @Override
+    public String getDesc() {
+        return desc + " " + getPrice() +" && "+  coffee.getDesc();
+    }
+}
+
+class Milk extends CoffeeShop {
+
+    public Milk(Drink seasoning) {
+        super(seasoning);
+        super.setDesc("Milk");
+        super.setPrice(6);
+    }
+
+}
+
+class Chocolate extends CoffeeShop {
+
+    public Chocolate(Drink seasoning) {
+        super(seasoning);
+        super.setDesc("Chocolate");
+        super.setPrice(4);
+    }
+
+}
+

装饰模式降低了系统的耦合度,可以动态增加或删除对象的职责,并使得需要装饰的具体构件类和具体装饰类可以独立变化,以便增加新的具体构件类和具体装饰类。在软件开发中,装饰模式应用较为广泛,例如在JavaIO中的输入流和输出流的设计、javax.swing包中一些图形界面构件功能的增强等地方都运用了装饰模式。

+

**装饰者模式主要解决:**一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。

+

优点

+
    +
  • 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。
  • +
  • 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。
  • +
+

缺点

+
    +
  • 多层装饰比较复杂。
  • +
  • 使用装饰模式进行系统设计时将产生很多小对象,这些对象的区别在于它们之间相互连接的方式有所不同,而不是它们的类或者属性值有所不同,大量小对象的产生势必会占用更多的系统资源,在一定程序上影响程序的性能。
  • +
+

使用场景

+

这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。可代替继承。

+
    +
  • 扩展一个类的功能。
  • +
  • 动态增加功能,动态撤销。
  • +
  • 当不能采用继承的方式对系统进行扩展或者采用继承不利于系统扩展和维护时可以使用装饰模式。不能采用继承的情况主要有两类: +
      +
    • 第一类是系统中存在大量独立的扩展,为支持每一种扩展或者扩展之间的组合将产生大量的子类,使得子类数目呈爆炸性增长
    • +
    • 第二类是因为类已定义为不能被继承
    • +
    +
  • +
+

外观模式

+
+

外观模式:为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

+
+

外观模式又称为门面模式,它是一种对象结构型模式。外观模式是迪米特法则的一种具体实现,通过引入一个新的外观角色可以降低原有系统的复杂度,同时降低客户类与子系统的耦合度。

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        Facade facade = new Facade();
+        facade.aTest();
+    }
+}
+class Facade {
+    private final A a;
+    private final B b;
+    private final C c;
+    private final D d;
+
+    public Facade() {
+        this.a = A.getInstance();
+        this.b = B.getInstance();
+        this.c = C.getInstance();
+        this.d = D.getInstance();
+    }
+
+    public void aTest() {
+        a.aTest();
+        b.aTest();
+        c.aTest();
+        d.aTest();
+    }
+}
+
+/**
+ * A调用B类,A调用C类,B调用C类,D调用B类,D调用C类
+ */
+class A {
+   private final static A instance = new A();
+   private A(){}
+   public static A getInstance() {
+       return instance;
+   }
+   public  void aTest() {
+       B.getInstance().aTest();
+       C.getInstance().aTest();
+       System.out.println("A class test method ...");
+   }
+}
+
+class B {
+    private final static B instance = new B();
+    private B(){}
+    public static B getInstance() {
+        return instance;
+    }
+    public  void aTest() {
+        C.getInstance().aTest();
+        System.out.println("B class test method ...");
+    }
+}
+
+class C {
+    private final static C instance = new C();
+    private C(){}
+    public  static C getInstance() {
+        return instance;
+    }
+    public  void aTest() {
+        System.out.println("C class test method ...");
+    }
+}
+
+class D {
+    private final static D instance = new D();
+    private D(){}
+    public static D getInstance() {
+        return instance;
+    }
+    public  void aTest() {
+        B.getInstance().aTest();
+        C.getInstance().aTest();
+        System.out.println("D class test method ...");
+    }
+}
+

外观模式的主要目的在于降低系统的复杂程度,在面向对象软件系统中,类与类之间的关系越多,不能表示系统设计得越好,反而表示系统中类之间的耦合度太大,这样的系统在维护和修改时都缺乏灵活性,因为一个类的改动会导致多个类发生变化,而外观模式的引入在很大程度上降低了类与类之间的耦合关系。引入外观模式之后,增加新的子系统或者移除子系统都非常方便,客户类无须进行修改(或者极少的修改),只需要在外观类中增加或移除对子系统的引用即可。从这一点来说,外观模式在一定程度上并不符合开放封闭原则,增加新的子系统需要对原有系统进行一定的修改,虽然这个修改工作量不大。

+

优点

+
    +
  • 减少关联关系;对客户端屏蔽了具体的实现,使得子系统使用起来更加容易;客户端代码将变得很简单,与之关联的对象也很少。
  • +
  • 解耦合;具体实现有改变不会影响到调用的客户端,只需要调整外观类即可。
  • +
+

缺点

+
    +
  • 如果设计不当,扩展时增加新的实现可能需要修改外观类的源代码,违背了开放封闭原则.
  • +
  • 不能很好地限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活性。
  • +
+

使用场景

+
    +
  • 当要为访问一系列复杂的子系统提供一个简单入口时可以使用外观模式。
  • +
  • 客户端程序与多个子系统之间存在很大的依赖性。引入外观类可以将子系统与客户端解耦,从而提高子系统的独立性和可移植性。
  • +
  • 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系,而通过外观类建立联系,降低层之间的耦合度。
  • +
+

享元模式

+
+

享元模式:运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。

+
+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        WebSiteFactory webSiteFactory = new WebSiteFactory();
+        
+        webSiteFactory.getWebSite("新闻").use(new User("lucy"));
+
+        webSiteFactory.getWebSite("新闻").use(new User("jane"));
+
+        webSiteFactory.getWebSite("博客").use(new User("jack"));
+
+        webSiteFactory.getWebSite("博客").use(new User("maik"));
+
+        webSiteFactory.getWebSite("博客").use(new User("seven"));
+
+        System.out.println("网站类型共:" + webSiteFactory.countWebSiteType());
+    }
+}
+
+abstract class WebSite{
+
+    private String name;
+
+    public abstract void use(User user);
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+}
+
+class ConcreateWebSite extends WebSite {
+
+    public ConcreateWebSite(String name) {
+        super.setName(name);
+    }
+
+    @Override
+    public void use(User user) {
+        System.out.println("网站类型名称:" + super.getName() +"\t 网站用户:" + user.getUsername());
+    }
+}
+
+class User {
+
+    private String username;
+
+    public User(String username) {
+        this.username = username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+}
+
+class WebSiteFactory{
+
+    private HashMap<String, WebSite> pool = new HashMap<>();
+
+    public WebSite getWebSite(String webSiteName){
+        if (!pool.containsKey(webSiteName)) {
+            pool.put(webSiteName, new ConcreateWebSite(webSiteName));
+        }
+        return pool.get(webSiteName);
+    }
+
+    public Integer countWebSiteType() {
+        return pool.size();
+    }
+
+}
+

当系统中存在大量相同或者相似的对象时,享元模式是一种较好的解决方案,它通过共享技术实现相同或相似的细粒度对象的复用,从而节约了内存空间,提高了系统性能。相比其他结构型设计模式,享元模式的使用频率并不算太高,但是作为一种以“节约内存,提高性能”为出发点的设计模式,它在软件开发中还是得到了一定程度的应用。例如:String、Java的池技术等。

+

享元模式主要解决在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。

+

优点

+
    +
  • 大大减少对象的创建,降低系统的内存,使效率提高。
  • +
+

缺点

+
    +
  • 提高了系统的复杂度,需要分离出外部状态和内部状态,而且外部状态具有固有化的性质,不应该随着内部状态的变化而变化,否则会造成系统的混乱。
  • +
+

使用场景

+

​ 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。

+
    +
  • 系统有大量相似对象。
  • +
  • 需要缓冲池的场景。
  • +
+

代理模式

+
+

代理模式:给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。

+
+

代理模式是为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象.这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象

+

代理模式有不同的形式, 主要有三种 静态代理、动态代理 (JDK代理、Cglib代理)。

+

静态代理

+
public class MainTest {
+    public static void main(String[] args) {
+        ProxyImpl proxy = new ProxyImpl();
+        StaticProxy staticProxy = new StaticProxy(proxy);
+        staticProxy.test();
+    }
+}
+
+interface ProxyInterface {
+    void test();
+}
+
+class ProxyImpl implements ProxyInterface{
+
+    @Override
+    public void test() {
+        System.out.println("helloWorld");
+    }
+
+}
+
+
+class StaticProxy implements ProxyInterface{
+
+    private ProxyImpl target;
+
+    public StaticProxy (ProxyImpl target){
+        this.target=target;
+    }
+
+
+    @Override
+    public void test() {
+        System.out.println("静态代理之前...");
+        target.test();
+        System.out.println("静态代理之后...");
+    }
+}
+

静态代理能在不修改目标对象的功能前提下,能通过代理对象对目标进行扩展。但是因为代理对象需要与目标对象实现相同的接口,所以会有很多代理类一旦接口增加方法后,目标对象与代理对象都需要维护

+

动态代理

+

动态代理,代理对象不需要实现接口,但是目标对象需要实现接口,否则不能实现动态代理。代理对象的生产是利用JDK的API,动态的在内存中构建代理对象。

+
JDK动态代理
+
public class MainTest {
+    public static void main(String[] args) {
+        ProxyImpl proxy = new ProxyImpl();
+        ProxyInterface jdkProxyInterface = (ProxyInterface)new JDKProxy().bind(proxy);
+        jdkProxyInterface.test();
+    }
+}
+interface ProxyInterface {
+    void test();
+}
+
+class ProxyImpl implements ProxyInterface{
+
+    @Override
+    public void test() {
+        System.out.println("helloWorld");
+    }
+
+}
+
+class JDKProxy {
+    //通用类型,表示被代理的真实对象
+    private Object target;
+
+    public Object bind(Object target){
+        this.target=target;
+        //生成代理类(与被代理对象实现相同接口的兄弟类)
+        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) -> {
+            Object res;
+            System.out.println("JDK动态代理前");
+            res=method.invoke(target, args);
+            System.out.println("JDK动态代理后");
+            return res;
+        });
+    }
+
+}
+
CGlib动态代理
+

Cglib代理也叫作子类代理,它是在内存中构建一个子类对象从而实现对目标对象功能扩展。 +Cglib是一个强大的高性能的代码生成包,它可以在运行期扩展 java 类与实现 java 接口.它广泛的被许多 AOP 的框架使用,例如 Spring AOP,实现方法拦截。

+

以下测试代码需要导入Cglib和asm相关jar包。

+
asm-3.3.1.jar
+cglib-2.2.jar
+
PS:使用`CGLib`实现动态代理时出现了下面这个异常
+Exception in thread "main" java.lang.IncompatibleClassChangeError: 
+class net.sf.cglib.core.DebuggingClassWriter has interface org.objectweb.asm.ClassVisitor as super class
+
+
    +
  • +

    原因: +cglib.jar包含asm.jar包。报错内容是ClassVisitor的父类不相容。详细:原因分析

    +
  • +
  • +

    解决: +测试时用cglib2.2.jarasm3.3.1.jar版本,解决jar包冲突问题。

    +
  • +
+
+
public class MainTest {
+    public static void main(String[] args) {
+        Target target = new Target();
+        Target bind = (Target)new CGLibProxy().bind(target);
+        bind.test();
+    }
+}
+
+
+class Target{
+
+    public void test() {
+        System.out.println("helloWorld");
+    }
+
+}
+
+class CGLibProxy implements MethodInterceptor {
+
+    private Object target;
+
+    public  Object bind(Object target) {
+        this.target = target;
+        Enhancer enhancer = new Enhancer();
+        enhancer.setSuperclass(target.getClass());
+        enhancer.setCallback(this);
+        return enhancer.create();
+    }
+
+    @Override
+    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
+        Object res;
+        System.out.println("CGLib动态代理前");
+        res=method.invoke(target, args);
+        System.out.println("CGLib动态代理后");
+        return res;
+    }
+
+}
+

总结

+

代理模式是常用的结构型设计模式之一,它为对象的间接访问提供了一个解决方案,可以对对象的访问进行控制。代理模式主要解决:在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。

+

优点

+
    +
  • 职责清晰。能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。
  • +
  • 高扩展性。增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性。
  • +
+

缺点

+
    +
  • 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢。
  • +
  • 实现代理模式需要额外的工作,有些代理模式的实现非常复杂
  • +
+

使用场景

+
    +
  • 想在访问一个类时做一些控制。
  • +
+

职责链模式

+
+

职责链模式:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式。

+
+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+         // 产品
+        RequestEntity test = new RequestEntity("test", 2000);
+
+        // 指定职责链
+        BeforeHandler before = new BeforeHandler("before");
+        AfterHandler after = new AfterHandler("after");
+        PostHandler post = new PostHandler("post");
+
+        // 形成链状闭环 要确保会被责任链中的组件处理 否则会一直循环下去 ,当然也可以选择不闭环
+        before.setHandler(after);
+        after.setHandler(post);
+        post.setHandler(before);
+
+        after.handleRequest(test);
+    }
+
+}
+
+class RequestEntity {
+
+    public String name;
+
+    public Integer grade;
+
+    public RequestEntity(String name,Integer grade) {
+        this.name = name;
+        this.grade = grade;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public float getGrade() {
+        return grade;
+    }
+}
+
+abstract class Handler {
+
+    // 下一个引用
+    protected Handler handler;
+
+    protected String name;
+
+    public Handler(String name) {
+        this.name = name;
+    }
+
+    public void setHandler(Handler handler) {
+        this.handler = handler;
+    }
+
+    public abstract void handleRequest(RequestEntity requestEntity);
+
+}
+
+class BeforeHandler extends Handler {
+
+    public BeforeHandler(String name){
+        super(name);
+    }
+
+
+    @Override
+    public void handleRequest(RequestEntity requestEntity) {
+        if (requestEntity.getGrade() < 1000) {
+            System.out.println("分数为:" + requestEntity.getGrade() + ",被" + this.name + "处理 ");
+        }else {
+            handler.handleRequest(requestEntity);
+        }
+    }
+}
+
+class AfterHandler extends Handler {
+
+    public AfterHandler(String name){
+        super(name);
+    }
+
+    @Override
+    public void handleRequest(RequestEntity requestEntity) {
+        if (requestEntity.getGrade() <= 2000) {
+            System.out.println("分数为:" + requestEntity.getGrade() + ",被" + this.name + "处理 ");
+        }else {
+            handler.handleRequest(requestEntity);
+        }
+    }
+}
+
+class PostHandler extends Handler {
+
+    public PostHandler(String name){
+        super(name);
+    }
+
+    @Override
+    public void handleRequest(RequestEntity requestEntity) {
+        if (requestEntity.getGrade() > 3000) {
+            System.out.println("分数为:" + requestEntity.getGrade() + ",被" + this.name + "处理 ");
+        }else {
+            handler.handleRequest(requestEntity);
+        }
+    }
+}
+

职责链模式通过建立一条链来组织请求的处理者,请求将沿着链进行传递,请求发送者无须知道请求在何时、何处以及如何被处理,实现了请求发送者与处理者的解耦。在软件开发中,如果遇到有多个对象可以处理同一请求时可以应用职责链模式,例如在Web应用开发中创建一个过滤器(Filter)链来对请求数据进行过滤,在工作流系统中实现公文的分级审批等等,使用职责链模式可以较好地解决此类问题。

+

优点

+
    +
  • 增加新的请求处理类很方便,只需要在客户端重新建链即可,从这一点来看是符合开闭原则的。
  • +
  • 为请求创建了一个接收者对象的链式结构。对请求的发送者和接收者进行解耦。
  • +
  • 简化了对象。使得对象不需要知道链的结构。
  • +
  • 增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次序,允许动态地新增或者删除责任。
  • +
+

缺点

+
    +
  • 系统性能将受到一定影响,可能会造成循环调用。特别是在链比较长的时候,因此需控制链中最大节点数量,一般通过在 Handler 中设置一个最大节点数量,在 setNext()方法中判断是否已经超过阀值,超过则不允许该链建立,避免出现超长链无意识地破坏系统性能。
  • +
  • 调试不方便。采用了类似递归的方式,调试时逻辑可能比较复杂。可能不容易观察运行时的特征,有碍于除错。
  • +
+

使用场景

+
    +
  • 在处理消息的时候以过滤很多通道。
  • +
  • 有多个对象可以处理同一个请求,具体哪个对象处理该请求由运行时刻自动确定。
  • +
  • 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求。
  • +
  • 可动态指定一组对象处理请求。
  • +
+

命令模式

+
+

命令模式:将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作模式或事务模式。

+
+

命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        LightReceiver lightReceiver = new LightReceiver();
+
+        LightOnCommand lightOnCommand = new LightOnCommand(lightReceiver);
+        LightOffCommand lightOffCommand = new LightOffCommand(lightReceiver);
+        RemoteCommand remoteCommand = new RemoteCommand();
+        // 测试
+        remoteCommand.setCommand(0,lightOnCommand,lightOffCommand);
+        // 按下开灯按钮
+        System.out.println("按下开灯按钮 -------");
+        remoteCommand.onButtonPushed(0);
+        // 按下关灯按钮
+        System.out.println("按下关灯按钮 -------");
+        remoteCommand.offButtonPushed(0);
+        // 按下撤销按钮
+        System.out.println("按下撤销按钮 ---------");
+        remoteCommand.undoButtonPushed(0);
+    }
+}
+
+interface Command{
+
+    /**
+     * 执行命令
+     */
+    void exec();
+
+    /**
+     * 撤销命令
+     */
+    void undo();
+
+}
+
+class LightReceiver{
+
+    public void on(){
+        System.out.println("开灯 ...");
+    }
+
+    public void off() {
+        System.out.println("关灯 ...");
+    }
+
+
+}
+
+class LightOnCommand implements Command{
+
+    private LightReceiver lightReceiver;
+
+    public LightOnCommand(LightReceiver lightReceiver) {
+        super();
+        this.lightReceiver = lightReceiver;
+    }
+
+    @Override
+    public void exec() {
+        lightReceiver.on();
+    }
+
+    @Override
+    public void undo() {
+        lightReceiver.off();
+    }
+}
+
+class LightOffCommand implements Command {
+
+    private LightReceiver lightReceiver;
+
+    public LightOffCommand(LightReceiver lightReceiver) {
+        super();
+        this.lightReceiver = lightReceiver;
+    }
+
+    @Override
+    public void exec() {
+        lightReceiver.off();
+    }
+
+    @Override
+    public void undo() {
+        lightReceiver.on();
+    }
+}
+
+/**
+ * 空执行 默认命令实现类
+ */
+class NoCommand implements Command {
+
+    @Override
+    public void exec() {
+        System.out.println("默认命令执行");
+    }
+
+    @Override
+    public void undo() {
+        System.out.println("默认撤销方法");
+    }
+}
+
+class RemoteCommand {
+
+    // 存放开关命令
+    private Command onCommands[];
+
+    private Command offCommands[];
+
+    // 存放撤销命令
+    private Command undoCommands[];
+
+    public RemoteCommand() {
+        undoCommands = new Command[5];
+        onCommands = new Command[5];
+        offCommands = new Command[5];
+
+        // 默认空命令
+        for (int i = 0; i < 5; i++) {
+            onCommands[i] = new NoCommand();
+            offCommands[i] = new NoCommand();
+        }
+    }
+
+    // 设置命令
+    public  void  setCommand(int no, Command onCommand, Command offCommand) {
+        onCommands[no] = onCommand;
+        offCommands[no] = offCommand;
+    }
+
+    // 按下开的按钮
+    public void onButtonPushed(int no) {
+        onCommands[no].exec();
+        // 记录撤销操作
+        undoCommands[no] = onCommands[no];
+    }
+
+    // 按下关闭的按钮
+    public void offButtonPushed(int no) {
+        offCommands[no].exec();
+        // 记录撤销操作
+        undoCommands[no] = offCommands[no];
+    }
+
+    // 按下撤销按钮
+    public void undoButtonPushed(int no) {
+        undoCommands[no].undo();
+    }
+
+
+}
+

命令模式是一种使用频率非常高的设计模式,它可以将请求发送者与接收者解耦,请求发送者通过命令对象来间接引用请求接收者,使得系统具有更好的灵活性和可扩展性。在基于GUI的软件开发,无论是在电脑桌面应用还是在移动应用中,命令模式都得到了广泛的应用。

+

在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。

+

优点

+
    +
  • 降低了系统耦合度。
  • +
  • 新的命令可以很容易地加入到系统中。由于增加新的具体命令类不会影响到其他类,因此增加新的具体命令类很容易,无须修改原有系统源代码,甚至客户类代码,满足开闭原则的要求。
  • +
+

缺点

+
    +
  • 使用命令模式可能会导致某些系统有过多的具体命令类。因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用。
  • +
+

使用场景

+
    +
  • 请求调用者需要与请求接收者解耦时,命令模式可以使调用者和接收者不直接交互。
  • +
  • 当系统需要支持命令的撤销操作和恢复操作时。
  • +
  • 系统需要在不同的时间指定请求、将请求排队和执行请求。一个命令对象和请求的初始调用者可以有不同的生命期,换言之,最初的请求发出者可能已经不在了,而命令对象本身仍然是活动的,可以通过该命令对象去调用请求接收者,而无须关心请求调用者的存在性,可以通过请求日志文件等机制来具体实现。
  • +
+

解释器模式

+
+

解释器模式:定义一个语言的文法,并且建立一个解释器来解释该语言中的句子,这里的“语言”是指使用规定格式和语法的代码。解释器模式是一种类行为型模式。

+
+

代码实现

+
public class MainTest {
+    public static void main(String[] args) throws IOException {
+        String expStr = getExpStr();
+        HashMap<String, Integer> var = getValue(expStr);
+        Calculator calculator = new Calculator(expStr);
+        System.out.println("运算结果:" + expStr + "=" + calculator.run(var));
+    }
+
+    // 获得表达式
+    public static String getExpStr() throws IOException {
+        System.out.print("请输入表达式:");
+        return (new BufferedReader(new InputStreamReader(System.in))).readLine();
+    }
+
+    // 获得值映射
+    public static HashMap<String, Integer> getValue(String expStr) throws IOException {
+        HashMap<String, Integer> map = new HashMap<>();
+
+        for (char ch : expStr.toCharArray()) {
+            if (ch != '+' && ch != '-') {
+                if (!map.containsKey(String.valueOf(ch))) {
+                    System.out.print("请输入" + String.valueOf(ch) + "的值:");
+                    String in = (new BufferedReader(new InputStreamReader(System.in))).readLine();
+                    map.put(String.valueOf(ch), Integer.valueOf(in));
+                }
+            }
+        }
+        return map;
+    }
+
+}
+
+abstract class AbstractExpression {
+
+    /**
+     * 表达式解释器
+     */
+    public abstract int interpreter(HashMap<String, Integer> var);
+
+}
+
+/**
+ * 终结表达式
+ */
+class VarExpression extends AbstractExpression {
+
+    private String key;
+
+    public VarExpression(String key) {
+        this.key = key;
+    }
+
+    @Override
+    public int interpreter(HashMap<String, Integer> map) {
+        return map.get(key);
+    }
+}
+
+/**
+ * 非终结表达式
+ */
+class SymbolExpression extends AbstractExpression {
+
+    protected AbstractExpression left;
+
+    protected AbstractExpression right;
+
+    SymbolExpression(AbstractExpression left, AbstractExpression right) {
+        this.left = left;
+        this.right = right;
+    }
+
+
+    // 因为 SymbolExpression  是让其子类来实现,因此 interpreter 是一个默认实现
+    @Override
+    public int interpreter(HashMap<String, Integer> var) {
+        return 0;
+    }
+
+}
+
+/**
+ * 减法
+ */
+class SubExpression extends SymbolExpression {
+
+    public SubExpression(AbstractExpression left, AbstractExpression right) {
+        super(left, right);
+    }
+
+    @Override
+    public int interpreter(HashMap<String, Integer> var) {
+        return super.left.interpreter(var) - super.right.interpreter(var);
+    }
+
+}
+
+/**
+ * 加法
+ */
+class AddExpression extends SymbolExpression {
+
+    public AddExpression(AbstractExpression left, AbstractExpression right) {
+        super(left, right);
+    }
+
+    @Override
+    public int interpreter(HashMap<String, Integer> var) {
+        return super.left.interpreter(var) + super.right.interpreter(var);
+    }
+
+}
+
+/**
+ * 计算器 调用加减法
+ */
+class Calculator {
+
+    private AbstractExpression expression;
+
+    public  Calculator(AbstractExpression expression) {
+        this.expression = expression;
+    }
+
+    public Calculator(String expStr) {
+        // 安排运算先后顺序
+        Stack<AbstractExpression> stack = new Stack<>();
+        // 表达式拆分成字符数组
+        char[] charArray = expStr.toCharArray();
+
+
+        AbstractExpression left = null;
+        AbstractExpression right = null;
+        //遍历我们的字符数组,  即遍历	[a, +, b]
+        //针对不同的情况,做处理
+        for (int i = 0; i < charArray.length; i++) {
+            switch (charArray[i]) {
+                case '+':
+                    //  stack   left => "a"
+                    left = stack.pop();
+                    // 取出右表达式 "b"
+                    right = new VarExpression(String.valueOf(charArray[++i]));
+                    stack.push(new AddExpression(left, right));
+                    // 然后根据得到 left  right 构建 AddExpresson 加入 stack
+                    break;
+                case '-':
+                    left = stack.pop();
+                    right = new VarExpression(String.valueOf(charArray[++i]));
+                    stack.push(new SubExpression(left, right));
+                    break;
+                default:
+                    //如果是一个 Var 就创建要给 VarExpression 对象,并 push  stack
+                    stack.push(new VarExpression(String.valueOf(charArray[i])));
+
+                    break;
+            }
+        }
+        //当遍历完整个 charArray  数组后,stack 就得到最后
+        this.expression = stack.pop();
+    }
+
+
+    public int run(HashMap<String, Integer> var) {
+        //最后将表达式 a+b  var = {a=10,b=20}
+        //然后传递给 expression  interpreter 进行解释执行
+        return this.expression.interpreter(var);
+    }
+
+}
+

解释器模式为自定义语言的设计和实现提供了一种解决方案,它用于定义一组文法规则并通过这组文法规则来解释语言中的句子。虽然解释器模式的使用频率不是特别高,但是它在正则表达式、XML文档解释等领域还是得到了广泛使用。与解释器模式类似,目前还诞生了很多基于抽象语法树的源代码处理工具,例如Eclipse中的Eclipse AST,它可以用于表示Java语言的语法结构,用户可以通过扩展其功能,创建自己的文法规则。

+

优点

+
    +
  • 易于改变和扩展文法。增加新的解释表达式较为方便。如果用户需要增加新的解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式类代码无须修改,符合“开闭原则”。
  • +
  • 易于实现简单文法。
  • +
+

缺点

+
    +
  • 可利用场景比较少。
  • +
  • 对于复杂的文法比较难维护。解释器模式会引起类膨胀。如果一个语言包含太多文法规则,类的个数将会急剧增加,导致系统难以管理和维护。
  • +
  • 解释器模式采用递归调用方法。
  • +
+

使用场景

+
    +
  • 对于一些固定文法构建一个解释句子的解释器。
  • +
  • 可以将一个需要解释执行的语言中的句子表示为一个抽象语法树
  • +
  • 一些重复出现的问题可以用一种简单的语言来进行表达。
  • +
  • 一个简单语法需要解释的场景。
  • +
+

迭代器模式

+
+

迭代器模式:提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标。迭代器模式是一种对象行为型模式。

+
+

迭代器模式的重要用途就是帮助我们遍历容器。迭代器模式,提供一种遍历集合元素的统一接口,用一致的方法遍历集合元素,不需要知道集合对象的底层表示,即:不暴露其内部的结构。在迭代器模式结构中包含聚合和迭代器两个层次结构,考虑到系统的灵活性和可扩展性,在迭代器模式中应用了工厂方法模式.

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        Bread bread = new Bread();
+        bread.add("面粉");
+        bread.add("黄油");
+        bread.add("白糖");
+        bread.add("鸡蛋");
+        Iterator iterator = bread.getIterator();
+        while (iterator.hasNext()) {
+            System.out.println(iterator.next());
+        }
+    }
+}
+
+class FoodIterator implements Iterator {
+
+    String[] foods;
+    int      position = 0;
+
+    public FoodIterator(String[] foods) {
+        this.foods = foods;
+    }
+
+    @Override
+    public boolean hasNext() {
+        return position != foods.length;
+    }
+
+    @Override
+    public Object next() {
+        String food = foods[position];
+        position += 1;
+        return food;
+    }
+
+}
+
+interface Food {
+
+    void add(String name);
+
+    Iterator getIterator();
+}
+
+class Bread implements Food{
+    private String[] foods    = new String[4];
+    private int      position = 0;
+
+    @Override
+    public void add(String name) {
+        foods[position] = name;
+        position += 1;
+    }
+
+    @Override
+    public Iterator getIterator() {
+        return new FoodIterator(this.foods);
+    }
+}
+
+

迭代器模式是一种使用频率非常高的设计模式,通过引入迭代器可以将数据的遍历功能从聚合对象中分离出来,聚合对象只负责存储数据,而遍历数据由迭代器来完成。由于很多编程语言的类库都已经实现了迭代器模式,因此在实际开发中,我们只需要直接使用Java、C#等语言已定义好的迭代器即可,迭代器已经成为我们操作聚合对象的基本工具之一。

+
+

迭代器的使用现在非常广泛,因为Java中提供了java.util.Iterator。而且Java中的很多容器(Collection、Set)也都提供了对迭代器的支持。

+

优点

+
    +
  • 提供一个统一的方法遍历对象,客户不用再考虑聚合的类型,使用一种方法就可以遍历对象了。
  • +
  • 隐藏了聚合的内部结构,客户端要遍历聚合的时候只能取到迭代器,而不会知道聚合的具体组成。
  • +
  • 提供了一种设计思想,就是一个类应该只有一个引起变化的原因(单一职责原则)。在聚合类中,我们把迭代器分开,就是要把管理对象集合和遍历对象集合的责任分开,这样一来集合改变的话,只影响到聚合对象。而如果遍历方式改变的话,只影响到了迭代器。
  • +
  • 当要展示一组相似对象,或者遍历一组相同对象时使用, 适合使用迭代器模式
  • +
+

缺点

+
    +
  • 由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加,这在一定程度上增加了系统的复杂性。
  • +
+

使用场景

+
    +
  • 需要为一个聚合对象提供多种遍历方式。
  • +
  • 使用迭代器模式可以为遍历不同的聚合结构提供一个统一的接口,接口的实现类中为不同的聚合结构提供不同的遍历方式,而客户端可以一致性地操作该接口。
  • +
+

中介者模式

+
+

中介者模式:用一个中介对象(中介者)来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式,它是一种对象行为型模式。

+
+

如果在一个系统中对象之间存在多对多的相互关系,我们可以将对象之间的一些交互行为从各个对象中分离出来,并集中封装在一个中介者对象中,并由该中介者进行统一协调,这样对象之间多对多的复杂关系就转化为相对简单的一对多关系。通过引入中介者来简化对象之间的复杂交互,中介者模式是“迪米特法则”的一个典型应用。

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        //创建一个中介者对象
+        Mediator mediator = new ConcreteMediator();
+
+        //创建 Alarm  并且加入到ConcreteMediator 对象的 HashMap
+        Alarm alarm = new Alarm(mediator, "alarm");
+
+        //创建了 CoffeeMachine 对象,并且加入到	ConcreteMediator 对象的 HashMap
+        CoffeeMachine coffeeMachine = new CoffeeMachine(mediator, "coffeeMachine");
+
+        //创建  tV , 	且加入到	ConcreteMediator 对象的 HashMap
+        TV tV = new TV(mediator, "TV");
+
+        //让闹钟发出消息 依次调用
+        alarm.sendAlarm(0);
+        coffeeMachine.finishCoffee();
+        alarm.sendAlarm(1);
+        tV.startTv();
+    }
+
+}
+
+/**
+ * 中介者
+ */
+abstract class Mediator {
+
+    public abstract void register(String colleagueName, Colleague colleague);
+
+    public abstract void getMessage(int stateChange, String name);
+}
+
+class ConcreteMediator extends Mediator {
+
+    /**
+     * 集合,放入所有的同事对象
+     */
+    private HashMap<String, Colleague> colleagueMap;
+    private HashMap<String, String> interMap;
+
+    public ConcreteMediator() {
+        colleagueMap = new HashMap<>();
+        interMap = new HashMap<>();
+    }
+
+
+    @Override
+    public void register(String colleagueName, Colleague colleague) {
+        if (colleague instanceof Alarm) {
+            interMap.put("Alarm", colleagueName);
+        } else if (colleague instanceof CoffeeMachine) {
+            interMap.put("CoffeeMachine", colleagueName);
+        } else {
+            System.out.println("........");
+        }
+    }
+
+    @Override
+    public void getMessage(int stateChange, String colleagueName) {
+        if (colleagueMap.get(colleagueName) instanceof Alarm) {
+            if (stateChange == 0) {
+                ((CoffeeMachine) (colleagueMap.get(interMap
+                        .get("CoffeeMachine")))).startCoffee();
+                ((TV) (colleagueMap.get(interMap.get("TV")))).startTv();
+            } else if (stateChange == 1) {
+                ((TV) (colleagueMap.get(interMap.get("TV")))).stopTv();
+            }
+
+        } else if (colleagueMap.get(colleagueName) instanceof TV) {
+            //如果 TV 发现消息
+        }
+    }
+}
+
+/**
+ * 抽象同事类
+ */
+abstract class Colleague {
+
+    private final Mediator mediator;
+    public String name;
+
+    public Colleague(Mediator mediator, String name) {
+
+
+        this.mediator = mediator;
+        this.name = name;
+
+    }
+
+
+    public Mediator getMediator() {
+        return this.mediator;
+    }
+
+
+    public abstract void sendMessage(int stateChange);
+}
+
+class Alarm extends Colleague {
+
+    public Alarm(Mediator mediator, String name) {
+        super(mediator, name);
+        //在创建 Alarm 同事对象时,将自己放入到 ConcreteMediator 对象中[集合]
+        mediator.register(name, this);
+    }
+
+    public void sendAlarm(int stateChange) {
+        this.sendMessage(stateChange);
+    }
+
+    @Override
+    public void sendMessage(int stateChange) {
+        // 调用的中介者对象的 getMessage 方法
+        this.getMediator().getMessage(stateChange, this.name);
+    }
+
+}
+
+class TV extends Colleague {
+
+
+    public TV(Mediator mediator, String name) {
+        super(mediator, name);
+        mediator.register(name, this);
+    }
+
+
+    @Override
+    public void sendMessage(int stateChange) {
+        this.getMediator().getMessage(stateChange, this.name);
+    }
+
+
+    public void startTv() {
+        System.out.println("It's time to StartTv!");
+    }
+
+
+    public void stopTv() {
+        System.out.println("StopTv!");
+    }
+}
+
+
+class CoffeeMachine extends Colleague {
+
+    public CoffeeMachine(Mediator mediator, String name) {
+        super(mediator, name);
+        mediator.register(name, this);
+    }
+
+
+    @Override
+    public void sendMessage(int stateChange) {
+        this.getMediator().getMessage(stateChange, this.name);
+    }
+
+
+    public void startCoffee() {
+        System.out.println("It's time to startcoffee!");
+    }
+
+
+    public void finishCoffee() {
+        System.out.println("After 5 minutes!");
+        System.out.println("Coffee is ok!");
+        sendMessage(0);
+    }
+}
+

中介者模式将一个网状的系统结构变成一个以中介者对象为中心的星形结构,在这个星型结构中,使用中介者对象与其他对象的一对多关系来取代原有对象之间的多对多关系。中介者模式在事件驱动类软件中应用较为广泛,特别是基于GUI(图形用户界面)的应用软件,此外,在类与类之间存在错综复杂的关联关系的系统中,中介者模式都能得到较好的应用。

+

优点

+
    +
  • 减少类间依赖,降低了耦合,符合迪米特法则
  • +
  • 简化了对象之间的交互,它用中介者和同事的一对多交互代替了原来同事之间的多对多交互,一对多关系更容易理解、维护和扩展,将原本难以理解的网状结构转换成相对简单的星型结构。
  • +
  • 降低了类的复杂度,将一对多转化成了一对一。
  • +
+

缺点

+
    +
  • 中介者承担了较多的责任,一旦中介者出现了问题,整个系统就会受到影响。
  • +
  • 如果设计不当,可能会导致具体中介者类非常复杂,使得系统难以维护,在实际使用过程中要特别注意。
  • +
+

使用场景

+

主要解决:对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。

+

应当注意:不应当在职责混乱的时候使用。

+
    +
  • 系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。
  • +
  • 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。
  • +
+

备忘录模式

+
+

备忘录模式:在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。它是一种对象行为型模式,其别名为Token。

+
+

在设计备忘录类时需要考虑其封装性,除了Originator类,不允许其他类来调用备忘录类Memento的构造函数与相关方法,如果不考虑封装性,允许其他类调用构造方法,将导致在备忘录中保存的历史状态发生改变,通过撤销操作所恢复的状态就不再是真实的历史状态,备忘录模式也就失去了本身的意义。

+

所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        Originator originator = new Originator();
+        CareTaker careTaker = new CareTaker();
+        // 保存状态
+        careTaker.saveMemento(originator.saveState(" 状态#1 "));
+        careTaker.saveMemento(originator.saveState(" 状态#2 "));
+        careTaker.saveMemento(originator.saveState(" 状态#3 "));
+
+        System.out.println("目前保存的状态为:" + originator.getState());
+
+        System.out.println("开始恢复以前的状态 ....");
+        originator.recover(careTaker.recover(0));
+
+        System.out.println("恢复之后的状态为:" + originator.getState());
+
+    }
+}
+
+
+class Originator{
+
+    private String state;
+
+
+    public String getState() {
+        return state;
+    }
+
+    public Memento saveState(String state) {
+        this.state = state;
+        return new Memento(state);
+    }
+
+    public void recover(Memento memento) {
+        this.state =  memento.getState();
+    }
+}
+
+/**
+ * 备忘录对象 保存对象信息
+ */
+class Memento{
+
+    /**
+     * 需要保存状态
+     */
+    private final String state;
+
+    public Memento(String state) {
+        this.state = state;
+    }
+
+    public String getState() {
+        return state;
+    }
+
+}
+
+/**
+ * 管理备忘录对象
+ */
+class CareTaker {
+
+    public ArrayList<Memento> mementos = new ArrayList<>();
+
+    public Memento recover(int index) {
+       return mementos.get(index);
+    }
+
+    public void saveMemento(Memento memento) {
+        mementos.add(memento);
+    }
+
+}
+

备忘录模式在很多软件的使用过程中普遍存在,但是在应用软件开发中,它的使用频率并不太高,因为现在很多基于窗体和浏览器的应用软件并没有提供撤销操作。如果需要为软件提供撤销功能,备忘录模式无疑是一种很好的解决方案。在一些字处理软件、图像编辑软件、数据库管理系统等软件中备忘录模式都得到了很好的应用。

+

为了符合迪米特原则,还要增加一个管理备忘录的类(CareTaker);为了节约内存,可使用原型模式+备忘录模式

+

优点

+
    +
  • 给用户提供了一种可以恢复状态的机制,可以使用户能够比较方便地回到某个历史的状态。
  • +
  • 实现了信息的封装,使得用户不需要关心状态的保存细节。
  • +
+

缺点

+
    +
  • 消耗资源。如果类的成员变量过多,势必会占用比较大的资源,而且每一次保存都会消耗一定的内存。每保存一次对象的状态都需要消耗一定的系统资源。
  • +
+

使用场景

+

很多时候我们总是需要记录一个对象的内部状态,这样做的目的就是为了允许用户取消不确定或者错误的操作,能够恢复到他原先的状态,使得他有"后悔药"可吃。

+
    +
  • 需要保存/恢复数据的相关状态场景。
  • +
  • 提供一个可回滚的操作。
  • +
+

观察者模式

+
+

观察者模式:定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式的别名包括发布-订阅模式、模型-视图模式、源-监听器模式或从属者模式。观察者模式是一种对象行为型模式。

+
+

观察者模式描述了如何建立对象与对象之间的依赖关系,以及如何构造满足这种需求的系统。观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。作为对这个通知的响应,每个观察者都将监视观察目标的状态以使其状态与目标状态同步,这种交互也称为发布-订阅(Publish-Subscribe)。观察目标是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅它并接收通知。

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        WeatherData weatherData = new WeatherData();
+        CurrentCondition currentCondition = new CurrentCondition();
+        BaiduSite baiduSite = new BaiduSite();
+
+        // 注册观察者
+        weatherData.registerObserver(currentCondition);
+        weatherData.registerObserver(baiduSite);
+        // 设置数据 一旦数据变化 所有的观察者都会变化
+        weatherData.setWeatherData(10f, 20f);
+
+        // 移除注册观察者
+        weatherData.removeObserver(baiduSite);
+        
+        // 唤醒所有已经注册的观察者
+        weatherData.notifyObservers();
+    }
+}
+
+interface Subject {
+
+    void registerObserver(Observer observer);
+
+    void removeObserver(Observer observer);
+
+    void notifyObservers();
+}
+
+/**
+ * 观察者
+ */
+interface Observer {
+
+    void update(float  temperature, float humidity);
+}
+
+class WeatherData implements Subject{
+
+    private ArrayList<Observer> observers = new ArrayList<>();
+
+    private float temperature;
+
+    private float humidity;
+
+    public void setWeatherData(float humidity, float temperature) {
+        this.humidity = humidity;
+        this.temperature = temperature;
+    }
+
+
+    @Override
+    public void registerObserver(Observer observer) {
+        observers.add(observer);
+    }
+
+    @Override
+    public void removeObserver(Observer observer) {
+        if (observers.contains(observer)) {
+            observers.remove(observer);
+        }
+    }
+
+    @Override
+    public void notifyObservers() {
+        // 唤醒所有的观察者
+        for (Observer observer : observers) {
+            observer.update(temperature,humidity);
+        }
+    }
+}
+
+class CurrentCondition implements Observer{
+
+    private float temperature;
+    private float humidity;
+
+    @Override
+    public void update(float temperature, float humidity) {
+        this.temperature = temperature;
+        this.humidity = humidity;
+        displayed();
+    }
+
+    void displayed() {
+        System.out.println("===当前天气情况===");
+        System.out.println("当前湿度:" + this.temperature);
+        System.out.println("当前温度:" + this.humidity);
+    }
+}
+
+class BaiduSite implements Observer{
+
+    private float temperature;
+    private float humidity;
+
+    @Override
+    public void update(float temperature, float humidity) {
+        this.temperature = temperature;
+        this.humidity = humidity;
+        displayed();
+    }
+
+    void displayed() {
+        System.out.println("===当前百度网站天气情况===");
+        System.out.println("当前湿度:" + this.temperature);
+        System.out.println("当前温度:" + this.humidity);
+    }
+}
+

观察者模式是一种使用频率非常高的设计模式,无论是移动应用、Web应用或者桌面应用,观察者模式几乎无处不在,它为实现对象之间的联动提供了一套完整的解决方案,凡是涉及到一对一或者一对多的对象交互场景都可以使用观察者模式。观察者模式广泛应用于各种编程语言的GUI事件处理的实现,在基于事件的XML解析技术(如SAX2)以及Web事件处理中也都使用了观察者模式。

+

优点

+
    +
  • 观察者和被观察者是抽象耦合的。观察目标只需要维持一个抽象观察者的集合,无须了解其具体观察者。由于观察目标和观察者没有紧密地耦合在一起,因此它们可以属于不同的抽象化层次。
  • +
  • 观察者模式满足“开闭原则”的要求,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便。
  • +
  • 观察者模式可以实现表示层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层充当具体观察者角色。
  • +
+

缺点

+
    +
  • 如果一个被观察者对象有很多的直接和间接的观察者的话,将所有的观察者都通知到会花费很多时间。
  • +
  • 如果在观察者和观察目标之间有循环依赖的话,观察目标会触发它们之间进行循环调用,可能导致系统崩溃。
  • +
  • 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
  • +
+

使用场景

+

一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。

+
    +
  • 一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这些方面封装在独立的对象中使它们可以各自独立地改变和复用。
  • +
  • 一个对象的改变将导致其他一个或多个对象也发生改变,而不知道具体有多少对象将发生改变,可以降低对象之间的耦合度。
  • +
  • 一个对象必须通知其他对象,而并不知道这些对象是谁。
  • +
  • 需要在系统中创建一个触发链,A对象的行为将影响B对象,B对象的行为将影响C对象……,可以使用观察者模式创建一种链式触发机制。
  • +
+

注意事项:

+
    +
  • JAVA 中已经有了对观察者模式的支持类。
  • +
  • 避免循环引用。
  • +
  • 如果顺序执行,某一观察者错误会导致系统卡壳,一般采用异步方式。
  • +
+

状态模式

+
+

状态模式:允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。其别名为状态对象,状态模式是一种对象行为型模式。

+
+

状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中某个对象存在多个状态,这些状态之间可以进行转换,而且对象在不同状态下行为不相同时可以使用状态模式。状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化,对于客户端而言,无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,客户端都可以一致处理。

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        // 创建活动对象,奖品有 1 个奖品
+        RaffleActivity activity = new RaffleActivity(1);
+
+        // 我们连续抽 300 次奖
+        for (int i = 0; i < 30; i++) {
+            System.out.println("--------第" + (i + 1) + "次抽奖----------");
+            // 参加抽奖,第一步点击扣除积分
+            activity.debuctMoney();
+
+            // 第二步抽奖
+            activity.raffle();
+        }
+    }
+}
+
+
+abstract class State {
+
+
+    // 扣除积分 - 50
+    public abstract void deductMoney();
+
+    // 是否抽中奖品
+    public abstract boolean raffle();
+
+    // 发放奖品
+    public abstract void dispensePrize();
+}
+
+
+class RaffleActivity {
+
+    // state 表示活动当前的状态,是变化
+    State state = null;
+
+    // 奖品数量
+    int count = 0;
+
+    // 四个属性,表示四种状态
+    State noRafflleState = new NoRaffleState(this);
+    State canRaffleState = new CanRaffleState(this);
+
+    State dispenseState = new DispenseState(this);
+    State dispensOutState = new DispenseOutState(this);
+
+    //构造器
+    //1. 初始化当前的状态为 noRafflleState(即不能抽奖的状态)
+    //2. 初始化奖品的数量
+    public RaffleActivity(int count) {
+        this.state = getNoRafflleState();
+        this.count = count;
+    }
+
+    //扣分, 调用当前状态的 deductMoney
+    public void debuctMoney() {
+        state.deductMoney();
+    }
+
+    //抽奖
+    public void raffle() {
+        // 如果当前的状态是抽奖成功
+        if (state.raffle()) {
+            //领取奖品
+            state.dispensePrize();
+        }
+
+
+    }
+
+
+    public State getState() {
+        return state;
+    }
+
+
+    public void setState(State state) {
+        this.state = state;
+    }
+
+    //这里请大家注意,每领取一次奖品,count--
+    public int getCount() {
+        int curCount = count;
+        count--;
+        return curCount;
+    }
+
+
+    public void setCount(int count) {
+        this.count = count;
+    }
+
+    public State getNoRafflleState() {
+        return noRafflleState;
+    }
+
+
+    public void setNoRafflleState(State noRafflleState) {
+        this.noRafflleState = noRafflleState;
+    }
+
+
+    public State getCanRaffleState() {
+        return canRaffleState;
+    }
+
+
+    public void setCanRaffleState(State canRaffleState) {
+        this.canRaffleState = canRaffleState;
+    }
+
+
+    public State getDispenseState() {
+        return dispenseState;
+    }
+
+
+    public void setDispenseState(State dispenseState) {
+        this.dispenseState = dispenseState;
+    }
+
+    public State getDispensOutState() {
+        return dispensOutState;
+
+    }
+
+
+    public void setDispensOutState(State dispensOutState) {
+        this.dispensOutState = dispensOutState;
+    }
+}
+
+
+class DispenseOutState extends State {
+
+    // 初始化时传入活动引用
+    RaffleActivity activity;
+
+
+    public DispenseOutState(RaffleActivity activity) {
+        this.activity = activity;
+    }
+
+    @Override
+    public void deductMoney() {
+        System.out.println("奖品发送完了,请下次再参加");
+    }
+
+
+    @Override
+    public boolean raffle() {
+        System.out.println("奖品发送完了,请下次再参加");
+        return false;
+    }
+
+
+    @Override
+    public void dispensePrize() {
+        System.out.println("奖品发送完了,请下次再参加");
+    }
+}
+
+class DispenseState extends State {
+
+    // 初始化时传入活动引用,发放奖品后改变其状态
+    RaffleActivity activity;
+
+
+    public DispenseState(RaffleActivity activity) {
+        this.activity = activity;
+    }
+
+
+    @Override
+    public void deductMoney() {
+        System.out.println("不能扣除积分");
+    }
+
+
+    @Override
+    public boolean raffle() {
+        System.out.println("不能抽奖");
+        return false;
+    }
+
+    //发放奖品
+    @Override
+    public void dispensePrize() {
+        if (activity.getCount() > 0) {
+            System.out.println("恭喜中奖了");
+            // 改变状态为不能抽奖
+            activity.setState(activity.getNoRafflleState());
+        } else {
+            System.out.println("很遗憾,奖品发送完了");
+            // 改变状态为奖品发送完毕, 后面我们就不可以抽奖
+            activity.setState(activity.getDispensOutState());
+            //System.out.println("抽奖活动结束");
+            //System.exit(0);
+        }
+
+    }
+}
+
+class NoRaffleState extends State {
+
+    // 初始化时传入活动引用,扣除积分后改变其状态
+    RaffleActivity activity;
+
+
+    public NoRaffleState(RaffleActivity activity) {
+        this.activity = activity;
+    }
+
+    // 当前状态可以扣积分 , 扣除后,将状态设置成可以抽奖状态
+    @Override
+    public void deductMoney() {
+        System.out.println("扣除 50 积分成功,您可以抽奖了");
+        activity.setState(activity.getCanRaffleState());
+    }
+
+    // 当前状态不能抽奖
+    @Override
+    public boolean raffle() {
+        System.out.println("扣了积分才能抽奖喔!");
+        return false;
+    }
+
+    // 当前状态不能发奖品
+    @Override
+    public void dispensePrize() {
+        System.out.println("不能发放奖品");
+    }
+}
+
+
+class CanRaffleState extends State {
+
+    RaffleActivity activity;
+
+    public CanRaffleState(RaffleActivity activity) {
+        this.activity = activity;
+    }
+
+    @Override
+    public void deductMoney() {
+        System.out.println("已经扣取过了积分");
+    }
+
+    //可以抽奖, 抽完奖后,根据实际情况,改成新的状态
+    @Override
+    public boolean raffle() {
+        System.out.println("正在抽奖,请稍等!");
+        Random r = new Random();
+        int num = r.nextInt(10);
+        // 10%中奖机会
+        if (num == 0) {
+            // 改变活动状态为发放奖品
+            activity.setState(activity.getDispenseState());
+            return true;
+        } else {
+            System.out.println("很遗憾没有抽中奖品!");
+            // 改变状态为不能抽奖
+            activity.setState(activity.getNoRafflleState());
+            return false;
+        }
+    }
+
+    // 不能发放奖品
+    @Override
+    public void dispensePrize() {
+        System.out.println("没中奖,不能发放奖品");
+    }
+}
+

状态模式将一个对象在不同状态下的不同行为封装在一个个状态类中,通过设置不同的状态对象可以让环境对象拥有不同的行为,而状态转换的细节对于客户端而言是透明的,方便了客户端的使用。在实际开发中,状态模式具有较高的使用频率,在工作流和游戏开发中状态模式都得到了广泛的应用,例如公文状态的转换、游戏中角色的升级等。

+

优点

+
    +
  • 封装了状态的转换规则,在状态模式中可以将状态的转换代码封装在环境类或者具体状态类中,可以对状态转换代码进行集中管理,而不是分散在一个个业务方法中。
  • +
  • 将所有与某个状态有关的行为放到一个类中,只需要注入一个不同的状态对象即可使环境对象拥有不同的行为。
  • +
  • 可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。
  • +
  • 允许状态转换逻辑与状态对象合成一体,而不是提供一个巨大的条件语句块,状态模式可以让我们避免使用庞大的条件语句来将业务方法和状态转换代码交织在一起。
  • +
+

缺点

+
    +
  • 状态模式的使用必然会增加系统类和对象的个数。
  • +
  • 状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。
  • +
  • 状态模式对“开闭原则”的支持并不太好,增加新的状态类需要修改那些负责状态转换的源代码,否则无法转换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。
  • +
+

使用场景

+

状态模式主要解决对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。在代码中包含大量与对象状态有关的条件语句时应该考虑使用状态模式。应当注意的是,在行为受状态约束的时候使用状态模式,状态应该不超过 5 个,太多则导致程序结构、代码混乱。

+
    +
  • 行为随状态改变而改变的场景。
  • +
  • 条件、分支语句的代替者。
  • +
+

策略模式

+
+

策略模式:定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化,也称为政策模式。策略模式是一种对象行为型模式。

+
+

策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,将算法的定义放在专门的策略类中,每一个策略类封装了一种实现算法,使用算法的环境类针对抽象策略类进行编程,符合“依赖倒转原则”。在出现新的算法时,只需要增加一个新的实现了抽象策略类的具体策略类即可。

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        Bird bird = new Bird();
+        bird.fly();
+
+        Duck duck = new Duck();
+        duck.fly();
+
+        Dog dog = new Dog();
+        dog.fly();
+
+        System.out.println("变为会飞 :");
+        dog.setFlyStrategy(new GoodFlyStrategy());
+        dog.fly();
+    }
+}
+
+abstract class AbstractStrategy {
+
+    protected FlyStrategy flyStrategy;
+
+    public void setFlyStrategy(FlyStrategy flyStrategy) {
+        this.flyStrategy = flyStrategy;
+    }
+
+    public abstract void fly();
+
+}
+
+class Bird  extends AbstractStrategy{
+
+    public Bird() {
+        System.out.print("小鸟");
+        flyStrategy = new GoodFlyStrategy();
+    }
+
+    @Override
+    public void fly() {
+        flyStrategy.fly();
+    }
+}
+
+class Duck extends AbstractStrategy {
+
+    public Duck() {
+        System.out.print("鸭子");
+        flyStrategy = new BadFlyStrategy();
+    }
+
+    @Override
+    public void fly() {
+        flyStrategy.fly();
+    }
+}
+
+class Dog extends AbstractStrategy {
+
+    public Dog() {
+        System.out.print("狗");
+        flyStrategy = new NoFlyStrategy();
+    }
+
+    @Override
+    public void fly() {
+        flyStrategy.fly();
+    }
+}
+
+
+
+interface FlyStrategy {
+
+    void fly();
+}
+
+class GoodFlyStrategy implements FlyStrategy {
+
+    @Override
+    public void fly() {
+        System.out.println("擅长飞翔 ...");
+    }
+}
+
+class BadFlyStrategy implements FlyStrategy {
+
+    @Override
+    public void fly() {
+        System.out.println("不擅长飞翔 ...");
+    }
+}
+
+class NoFlyStrategy implements FlyStrategy {
+    @Override
+    public void fly() {
+        System.out.println("不会飞 ...");
+    }
+}
+

策略模式用于算法的自由切换和扩展,它是应用较为广泛的设计模式之一。策略模式对应于解决某一问题的一个算法族,允许用户从该算法族中任选一个算法来解决某一问题,同时可以方便地更换算法或者增加新的算法。只要涉及到算法的封装、复用和切换都可以考虑使用策略模式。

+

优点

+
    +
  • 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
  • +
  • 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族,恰当使用继承可以把公共的代码移到抽象策略类中,从而避免重复的代码。
  • +
  • 策略模式提供了一种算法的复用机制,由于将算法单独提取出来封装在策略类中,因此不同的环境类可以方便地复用这些策略类。
  • +
+

缺点

+
    +
  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。
  • +
  • 所有策略类都需要对外暴露。
  • +
+

如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。

+

使用场景

+
    +
  • +

    一个系统有许多许多类,而区分它们的只是他们直接的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。

    +
  • +
  • +

    如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。

    +
  • +
+

模板方法模式

+
+

模板方法模式:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。模板方法模式是一种基于继承的代码复用技术,它是一种类行为型模式。

+
+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        System.out.println("=====红豆豆浆=====");
+        RedBean redBean = new RedBean();
+        redBean.template();
+
+        System.out.println("=====花生豆浆=====");
+        Peanut peanut = new Peanut();
+        peanut.template();
+
+        System.out.println("=====豆浆=====");
+        None none = new None();
+        none.template();
+    }
+}
+
+abstract class SoyaMilk {
+
+    final void template(){
+        filterMaterial();
+        soak();
+        if (isAppended()) {
+            add();
+        }
+        over();
+    }
+
+    void filterMaterial() {
+        System.out.println("第一步:筛选材料");
+    }
+
+    void soak() {
+        System.out.println("第二步:浸泡");
+    }
+
+    abstract void add();
+
+    void over(){
+        System.out.println("第四步:打豆浆");
+    }
+
+    /**
+     * 钩子方法
+     * @return
+     */
+    boolean isAppended(){
+        return true;
+    }
+
+}
+
+class Peanut extends SoyaMilk{
+
+
+    @Override
+    void add() {
+        System.out.println("第三步:加入花生");
+    }
+}
+
+class RedBean extends SoyaMilk {
+
+
+    @Override
+    void add() {
+        System.out.println("第三步:加入红豆");
+    }
+}
+
+class None extends SoyaMilk {
+
+    @Override
+    void add() {
+
+    }
+
+    @Override
+    boolean isAppended() {
+        return false;
+    }
+
+}
+

模板方法模式是基于继承的代码复用技术,它体现了面向对象的诸多重要思想,是一种使用较为频繁的模式。模板方法模式广泛应用于框架设计中,以确保通过父类来控制处理流程的逻辑顺序(如框架的初始化,测试流程的设置等)。

+

优点

+
    +
  • 它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
  • +
  • 它在父类中提取了公共的部分代码,便于代码复用。
  • +
  • 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则
  • +
+

缺点

+
    +
  • 需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象,间接地增加了系统实现的复杂度。此时,可结合桥接模式来进行设计。
  • +
  • 由于继承关系自身的缺点,如果父类添加新的抽象方法,则所有子类都要改一遍。
  • +
+

使用场景

+
    +
  • 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
  • +
  • 当多个子类存在公共的行为时,可以将其提取出来并集中到一个公共父类中以避免代码重复。首先,要识别现有代码中的不同之处,并且将不同之处分离为新的操作。最后,用一个调用这些新的操作的模板方法来替换这些不同的代码。
  • +
  • 当需要控制子类的扩展时,模板方法只在特定点调用钩子操作,这样就只允许在这些点进行扩展。
  • +
+

访问者模式

+
+

访问者模式:提供一个作用于某对象结构中的各元素的操作表示,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。

+
+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        ObjectStructure objectStructure = new ObjectStructure();
+        objectStructure.attach(new Man());
+        objectStructure.attach(new WoMan());
+        objectStructure.attach(new Man());
+        objectStructure.attach(new WoMan());
+
+        // 显示成功的评价
+        Success success = new Success();
+        objectStructure.display(success);
+
+        System.out.println("==================");
+
+        // 显示失败的评价
+        Fail fail = new Fail();
+        objectStructure.display(fail);
+    }
+}
+
+abstract class Action {
+
+    protected abstract void getManResult(Man man);
+
+    protected abstract void getWomanResult(WoMan woman );
+
+}
+
+class Success  extends Action{
+
+    @Override
+    protected void getManResult(Man man) {
+        System.out.println("男人觉得很赞~");
+    }
+
+    @Override
+    protected void getWomanResult(WoMan woman) {
+        System.out.println("女人觉得很赞~");
+    }
+}
+
+class Fail  extends Action{
+
+    @Override
+    protected void getManResult(Man man) {
+        System.out.println("男人觉得很失败~");
+    }
+
+    @Override
+    protected void getWomanResult(WoMan woman) {
+        System.out.println("女人觉得很失败~");
+    }
+}
+
+
+abstract class  Person {
+    abstract void accpet(Action action);
+}
+
+class WoMan extends Person{
+
+    @Override
+    void accpet(Action action) {
+         action.getWomanResult(this);
+    }
+}
+
+class Man  extends Person{
+
+    @Override
+    void accpet(Action action) {
+        action.getManResult(this);
+    }
+}
+
+class ObjectStructure {
+
+    ArrayList<Person> people =  new ArrayList<>();
+
+    public void attach(Person person) {
+        people.add(person);
+    }
+
+    public void detach(Person person) {
+        people.remove(person);
+    }
+
+    public void display(Action acion) {
+        people.forEach(item -> {
+            item.accpet(acion);
+        });
+    }
+
+}
+

由于访问者模式的使用条件较为苛刻,本身结构也较为复杂,因此在实际应用中使用频率不是特别高。当系统中存在一个较为复杂的对象结构,且不同访问者对其所采取的操作也不相同时,可以考虑使用访问者模式进行设计。在XML文档解析、编译器的设计、复杂集合对象的处理等领域访问者模式得到了一定的应用。

+

优点

+
    +
  • 扩展性好。增加新的访问操作很方便。使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无须修改源代码,符合“开闭原则”。
  • +
  • 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
  • +
  • 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
  • +
  • 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。
  • +
+

缺点

+
    +
  • 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。具体元素对访问者公布细节,违反了迪米特法则
  • +
  • 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。
  • +
+

使用场景

+

需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,使用访问者模式将这些封装到类中。访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。

+
    +
  • 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。
  • +
  • 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。
  • +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/java/rookie-objectclass-methods/index.html b/blog-site/public/posts/java/rookie-objectclass-methods/index.html new file mode 100644 index 00000000..36d871e0 --- /dev/null +++ b/blog-site/public/posts/java/rookie-objectclass-methods/index.html @@ -0,0 +1,1471 @@ + + + + + + + + + + + Object类方法 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Object类方法

+ 2021.07.10 +
+

概览

+

Object 类位于 java.lang 包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承Object,成为Object的子类。

+

Object类可以显示继承,也可以隐式继承,效果都是一样的。

+
class A extends Object{
+    // to do
+}
+
+class A {
+    // to do
+}
+

Java Object类是所有类的父类,也就是说 Java 的所有类都继承了Object,子类可以使用Object的所有方法。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
方法名称方法作用
equals比较两个对象是否相同
hashCode获取对象的哈希值
toString返回对象的字符串表示形式
clone创建并返回一个对象的拷贝
finalize当垃圾收集确定不再有对对象的引用时,由垃圾收集器在对象上调用
getClass获取对象运行时的类
notify唤醒在该对象上等待的某个线程
notifyAll唤醒在该对象上等待的所有线程
wait让当前线程进入等待(阻塞)状态。直到其他线程调用此对象的notify()方法或notifyAll()方法。
+

equals

+

Object类中的equals()方法作用是比较两个对象,是判断两个对象引用指向的是同一个对象,即比较两个对象的内存地址是否相等。

+

Object类中的equals()源码如下

+
    public boolean equals(Object obj) {
+        return (this == obj);
+    }
+

等价关系

+

在Java规范中,equals()方法的使用存在如下特性:

+
    +
  • 自反性:x.equals(x); // true
  • +
  • 对称性:x.equals(y) == y.equals(x); // true
  • +
  • 传递性: if (x.equals(y) && y.equals(z)) => x.equals(z); // true;
  • +
  • 一致性:x.equals(y) == x.equals(y); // true 多次调用equals()方法结果不变
  • +
  • null的比较:x.equals(null); // false; 对任何不是null的对象x调用 x.equals(null) 结果都为false
  • +
+

与双等号

+
    +
  • 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
  • +
  • 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals()判断引用的对象是否等价。
  • +
+
Integer x = new Integer(1);
+Integer y = new Integer(1);
+System.out.println(x.equals(y)); // true
+System.out.println(x == y);      // false
+

equals()作用是判断两个对象是否相等,但一般有两种情况:

+
    +
  1. 类没有覆盖equals方法,则相当于通过 ==来比较这两个对象的地址;
  2. +
  3. 类覆盖equals方法,一般我们通过equals()来比较两个对象的内容是否相等,相等则返回true;
  4. +
+

equals()在不重写的情况下与 == 作用一样都是比较的内存中的地址.但是equals()可以重写。

+

重写equals方法

+

重写equals方法一般思路:

+
    +
  • 检查是否为同一个对象的引用,如果是直接返回 true;
  • +
  • 检查是否是同一个类型,如果不是,直接返回 false;
  • +
  • Object对象进行转型;
  • +
  • 判断每个关键域是否相等;
  • +
+
public class EqualExample {
+
+    private int x;
+    private int y;
+    private int z;
+
+    public EqualExample(int x, int y, int z) {
+        this.x = x;
+        this.y = y;
+        this.z = z;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        EqualExample that = (EqualExample) o;
+
+        if (x != that.x) return false;
+        if (y != that.y) return false;
+        return z == that.z;
+    }
+}
+

hashCode

+

在Java中hashcode方法是Object类的native方法,返回值为int类型,根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为hash值(散列值)。

+
+

hashCode通用约定:

+
    +
  • x.equals(y)返回true ,则x.hashCode()==y.hashCode(),其逆命题不一定成立。
  • +
  • 尽量使 hashCode 方法返回的散列码总体上呈均匀分布,可以提高哈希表的性能。
  • +
  • 程序运行时,若对象的equals方法中使用的字段没有改变,则在程序结束前,多次调用hashCode方法都应返回相同的散列码;程序结束后再执行时则没有此要求。
  • +
+
+

hashCode方法源码:

+
public native int hashCode();
+

根据这个方法的声明可知,该方法返回一个int类型的数值,并且是本地方法,因此在Object类中并没有给出具体的实现。

+

使用场景

+

对于包含容器类型的程序设计语言来说,基本上都会涉及到hashCode。在Java中也一样,hashCode方法的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable

+

在集合中已经存在上万条数据或者更多的数据场景下向集合中插入对象时,如何判别在集合中是否已经存在该对象了?

+

如果采用equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的优点就体现出来了。因为两个不同的对象可能会有相同的hashCode值,所有不能通过hashCode值来判断两个对象是否相等,但是可以直接根据hashcode值判断两个对象不等,如果两个对象的hashCode值不等,则必定是两个不同的对象。 +当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,如果存放的hash值中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就去存。

+
+

需要额外注意的是: +设计hashCode()时最重要的因素就是,无论何时,对同一个对象调用hashCode()都应该产生同样的值。

+

如果在将一个对象用put()添加进HashMap时产生一个hashCdoe值,而用get()取出时却产生了另一个hashCode值,那么就无法获取该对象了。 +所以如果你的hashCode方法依赖于对象中易变的数据,就要当心了,因为此数据发生变化时,hashCode()方法就会生成一个不同的散列码,从而获取不到该对象。

+

所以在重写hashCode方法和equals方法的时候,如果对象中的数据易变,则最好在equals方法和hashCode方法中不要依赖于该字段。

+
+

如下代码

+
public class MainTest {
+    public static void main(String[] args) {
+        Person p1 = new Person("lucy", 22);
+        // 85134311
+        System.out.println(p1.hashCode());
+        HashMap<Person, Integer> hashMap = new HashMap<>();
+        hashMap.put(p1, 1);
+        p1.setAge(13);
+        // null
+        System.out.println(hashMap.get(p1));
+    }
+}
+
+class Person {
+    private String name;
+    private int age;
+
+    public Person(String name, int age) {
+        this.name = name;
+        this.age = age;
+    }
+
+    public void setAge(int age) {
+        this.age = age;
+    }
+
+    @Override
+    public int hashCode() {
+        return name.hashCode() * 37 + age;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return this.name.equals(((Person) obj).name) && this.age == ((Person) obj).age;
+    }
+}
+

hashCode与equals

+

hashCode()返回散列值,而equals()是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。

+

equals()地址比较是通过对象的哈希值来比较的。hash值是由hashCode方法产生的,hashCode属于Object类的本地方法,默认使用==比较两个对象,如果equals()相等,hashcode一定相等,如果hashcode相等,equals不一定相等。

+

所以在覆盖 equals() 方法时应当总是覆盖hashCode()方法,保证等价的两个对象散列值也相等。

+

下面的代码中,新建了两个等价的对象,并将它们添加到HashSet中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为EqualExample没有实现hashCode()方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。

+
public class MainTest {
+    public static void main(String[] args) {
+        EqualExample e1 = new EqualExample(1, 1, 1);
+        EqualExample e2 = new EqualExample(1, 1, 1);
+        // true
+        System.out.println(e1.equals(e2));
+        HashSet<EqualExample> set = new HashSet<>();
+        set.add(e1);
+        set.add(e2);
+        // 2
+        System.out.println(set.size());
+    }
+}
+

所以在覆盖 equals()方法时应当总是覆盖hashCode()方法,保证等价的两个对象散列值也相等。

+

重写hashCode方法

+

重写hashCode方法规则:

+
    +
  • 把某个非零的常数值,保存在一个名为result的int类型的常量中
  • +
  • 字段值哈希码的计算 +
      +
    • 如果是boolean类型,true为1,false则为0
    • +
    • 如果是byte、char、shortint类型,需要强制转为int的值
    • +
    • 如果是long类型,计算(int)(f^(f>>32))
    • +
    • 如果是float类型,计算Float.floatToIntBits(f)
    • +
    • 如果是double类型,计算Double.doubleToLongBits(f),再按照long的方法进行计算
    • +
    • 如果是引用类型,则调用其hashCode方法(假设其hashCode满足你的需求)
    • +
    +
  • +
  • 代入公式result = result * 31 + c,返回result
  • +
+

《Effective Java》的作者推荐使用基于17和31的散列码的算法:

+
@Override
+public int hashCode() {
+    int result = 17;
+    result = 31 * result + x;
+    result = 31 * result + y;
+    result = 31 * result + z;
+    return result;
+}
+

Java 7新增的Objects类提供了计算hashCode的通用方法,可以很简洁实现hashCode方法:

+
@Override
+public int hashCode() {
+    return Objects.hash(name,age);
+}
+

toString

+

toString方法是Object类里定义的,返回只类型是String默认返回类名和它的引用地址:ToStringExample@4554617c这种形式,其中@后面的数值为散列码的无符号十六进制表示。

+

ObjecttoString源代码如下:

+
    public String toString() {
+        return getClass().getName() + "@" + Integer.toHexString(hashCode());
+    }
+

重写toString方法

+

当我们打印一个对象的引用时,实际是默认调用这个对象的toString()方法,当打印的对象所在类没有重写Object中的toString()方法时,默认调用的是Object类中toString()方法.返回此对象所在的类及对应的堆空间对象实体的首地址值。

+
public class MainTest {
+    public static void main(String[] args) {
+        // object.ToStringDemo@511d50c0
+        System.out.println(new ToStringDemo());
+    }
+}
+class ToStringDemo {
+    private String name;
+}
+

当我们打印对象所在类重写了toString(),调用的就是已经重写了的toString()方法,一般重写是将类对象的属性信息返回。

+
public class MainTest {
+    public static void main(String[] args) {
+        ToStringDemo toStringDemo = new ToStringDemo();
+        toStringDemo.setName("lucy");
+        // ToStringDemo{name='lucy'}
+        System.out.println(toStringDemo);
+    }
+}
+class ToStringDemo {
+    private String name;
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return "ToStringDemo{" +
+                "name='" + name + '\'' +
+                '}';
+    }
+}
+

使用

+

在进行String类与其他类型的连接操作时,自动调用toString()方法:

+
public class MainTest {
+    public static void main(String[] args) {
+        Date time = new Date();
+        System.out.println("time = " + time);//相当于下一行代码
+        System.out.println("time = " + time.toString());
+    }
+}
+

在实际应用中,可以根据需要在用户自定义类型中重写toString()方法:

+
public class MainTest {
+    public static void main(String[] args) {
+        // null 没有toString()方法 *会报错*
+        Object var0 = null;
+        System.out.println(var0.toString());
+
+        // 布尔型数据true和false返回对应的'true''false'
+        Boolean var1 = false;
+        Boolean var2 = true;
+        System.out.println(var1.toString());
+        System.out.println(var2.toString());
+
+        // 字符串类型原值返回
+        String var3 = "string";
+        System.out.println(var3.toString());
+
+        // 正浮点数及NaNInfinity加引号返回
+        Double var4 = 1.23d;
+        System.out.println(var4.toString());
+        Double nan = Double.NaN;
+        System.out.println(nan.toString());
+        Double negativeInfinity = Double.NEGATIVE_INFINITY;
+        Double positiveInfinity = Double.POSITIVE_INFINITY;
+        System.out.println(negativeInfinity.toString());
+        System.out.println(positiveInfinity.toString());
+
+        // 负浮点数或加'+'号的正浮点数直接跟上.toString(),相当于先运行toString()方法,再添加正负号,转换为数字
+        Double var5 = -1.23d;
+        Double var6 = +1.23d;
+        System.out.println(var5.toString());
+        System.out.println(var6.toString());
+    }
+}
+

基本数据类型转换为String类型就是调用了对应包装类的toString()方法:

+
int i = 10;
+System.out.println("i=" + i);
+

clone

+

在Java中可以使用clone方法来创建对象:

+
 protected native Object clone() throws CloneNotSupportedException;
+

如何对对象进行克隆:

+
    +
  • 实现Cloneable接口,这是一个标记接口,自身没有方法
  • +
  • 覆盖clone()方法,可见性提升为public
  • +
+

Cloneable接口

+

clone()Objectprotected 方法,它不是被 public修饰;一个类不显式的去重写clone(),其它类就不能直接去调用该类实例的 clone()方法:

+
public class CloneExample {
+    private int a;
+    private int b;
+}
+CloneExample e1 = new CloneExample();
+// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'
+

重写clone()方法得到以下实现:

+
public class CloneExample {
+    private int a;
+    private int b;
+
+    @Override
+    public CloneExample clone() throws CloneNotSupportedException {
+        return (CloneExample)super.clone();
+    }
+}
+
CloneExample e1 = new CloneExample();
+try {
+    CloneExample e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+    e.printStackTrace();
+}
+

以上抛出了 java.lang.CloneNotSupportedException: CloneExample,这是因为 CloneExample 没有实现 Cloneable 接口。

+

应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。

+

Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException

+
public class CloneExample implements Cloneable {
+    private int a;
+    private int b;
+
+    @Override
+    public Object clone() throws CloneNotSupportedException {
+        return super.clone();
+    }
+}
+

浅拷贝与深拷贝

+
    +
  • 浅拷贝:被复制对象的所有值属性都含有与原来对象的相同,而所有的对象引用属性仍然指向原来的对象。
  • +
  • 深拷贝:在浅拷贝的基础上,所有引用其他对象的变量也进行了clone,并指向被复制过的新对象。
  • +
+

如果一个被复制的属性都是基本类型,那么只需要实现当前类的cloneable机制就可以了,此为浅拷贝。

+

如果被复制对象的属性包含其他实体类对象引用,那么这些实体类对象都需要实现cloneable接口并覆盖clone()方法。

+

浅拷贝

+
public class ShallowCloneExample implements Cloneable {
+
+    private int[] arr;
+
+    public ShallowCloneExample() {
+        arr = new int[10];
+        for (int i = 0; i < arr.length; i++) {
+            arr[i] = i;
+        }
+    }
+
+    public void set(int index, int value) {
+        arr[index] = value;
+    }
+
+    public int get(int index) {
+        return arr[index];
+    }
+
+    @Override
+    protected ShallowCloneExample clone() throws CloneNotSupportedException {
+        return (ShallowCloneExample) super.clone();
+    }
+}
+
ShallowCloneExample e1 = new ShallowCloneExample();
+ShallowCloneExample e2 = null;
+try {
+    e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+    e.printStackTrace();
+}
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 222
+

深拷贝

+
public class DeepCloneExample implements Cloneable {
+
+    private int[] arr;
+
+    public DeepCloneExample() {
+        arr = new int[10];
+        for (int i = 0; i < arr.length; i++) {
+            arr[i] = i;
+        }
+    }
+
+    public void set(int index, int value) {
+        arr[index] = value;
+    }
+
+    public int get(int index) {
+        return arr[index];
+    }
+
+    @Override
+    protected DeepCloneExample clone() throws CloneNotSupportedException {
+        DeepCloneExample result = (DeepCloneExample) super.clone();
+        result.arr = new int[arr.length];
+        for (int i = 0; i < arr.length; i++) {
+            result.arr[i] = arr[i];
+        }
+        return result;
+    }
+}
+
DeepCloneExample e1 = new DeepCloneExample();
+DeepCloneExample e2 = null;
+try {
+    e2 = e1.clone();
+} catch (CloneNotSupportedException e) {
+    e.printStackTrace();
+}
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 2
+

clone的替代

+

使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。《Effective Java》 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。

+
public class CloneConstructorExample {
+
+    private int[] arr;
+
+    public CloneConstructorExample() {
+        arr = new int[10];
+        for (int i = 0; i < arr.length; i++) {
+            arr[i] = i;
+        }
+    }
+
+    public CloneConstructorExample(CloneConstructorExample original) {
+        arr = new int[original.arr.length];
+        for (int i = 0; i < original.arr.length; i++) {
+            arr[i] = original.arr[i];
+        }
+    }
+
+    public void set(int index, int value) {
+        arr[index] = value;
+    }
+
+    public int get(int index) {
+        return arr[index];
+    }
+}
+
CloneConstructorExample e1 = new CloneConstructorExample();
+CloneConstructorExample e2 = new CloneConstructorExample(e1);
+e1.set(2, 222);
+System.out.println(e2.get(2)); // 2
+

finalize

+

finalize()方法是Java提供的对象终止机制,允许开发人员提供对象被销毁之前的自定义处理逻辑。当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。

+

finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。

+

文档注释大意:当GC确定不再有对对象的引用时,由垃圾收集器在对象上调用。子类重写finalize方法来释放系统资源或执行其他清理。

+
   /**
+     * Called by the garbage collector on an object when garbage collection
+     * determines that there are no more references to the object.
+     * A subclass overrides the {@code finalize} method to dispose of
+     * system resources or to perform other cleanup.
+     */
+    protected void finalize() throws Throwable { }
+

简而言之,finalize方法是与Java中的垃圾回收器有关系。即:当一个对象变成一个垃圾对象的时候,如果此对象的内存被回收,那么就会调用该类中定义的finalize方法。

+

当一个对象可被回收时,就需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。

+

永远不要主动调用某个对象的finalize方法应该交给垃圾回收机制调用的原因:

+
    +
  • 在调用finalize方法时时可能会导致对象复活;
  • +
  • finalize方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize方法将没有执行机会;因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收;
  • +
  • 一个糟糕的finalize方法会严重影响GC的性能;
  • +
+

由于finalize方法的存在,虚拟机中的对象一般可能处于三种状态:

+

如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,虚拟机中定义了的对象可能的三种状态:

+
    +
  • 可触及的:从根节点开始,可以到达这个对象;对象存活被使用;
  • +
  • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize中复活;对象被复活,对象在finalize方法中被重新使用;
  • +
  • 不可触及的:对象的finalize方法被调用,并且没有复活,那么就会进入不可触及状态;对象死亡,对象没有被使用;
  • +
+

只有在对象不可触及时才可以被回收。不可触及的对象不可能被复活,因为finalize()只会被调用一次。

+

finalize对象终止机制判定一个对象能否被回收过程:

+

判定一个对象是否可回收,至少要经历两次标记过程:

+
    +
  • 如果对象没有没有引用链,则进行第一次标记
  • +
  • 进行筛选,判断此对象是否有必要执行finalize方法 +
      +
    1. 如果对象没有重写finalize方法,或者finalize方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,对象被判定为不可触及的。
    2. +
    3. 如果对象重写了finalize方法,且还未执行过,那么会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize方法执行。
    4. +
    5. finalize方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果对象在finalize方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,该对象会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。
    6. +
    +
  • +
+

代码演示对象能否被回收:

+
public class MainTest {
+
+    public static MainTest var;
+
+    /**
+     * 此方法只能被调用一次
+     * 可对该方法进行注释,来测试finalize方法是否能复活对象
+     */
+    @Override
+    protected void finalize() throws Throwable {
+        System.out.println("调用当前类重写的finalize()方法");
+        // 复活对象 让当前带回收对象重新与引用链中的对象建立联系
+        var = this;
+    }
+
+    public static void main(String[] args) throws InterruptedException {
+        var = new MainTest();
+        var = null;
+        System.gc();
+        System.out.println("-----------------第一次gc操作------------");
+        // 因为Finalizer线程的优先级比较低,暂停2秒,以等待它
+        Thread.sleep(2000);
+        if (var == null) {
+            System.out.println("对象已经死了");
+            // 如果第一次对象就死亡了 就终止
+            return;
+        } else {
+            System.out.println("对象还活着");
+        }
+
+        System.out.println("-----------------第二次gc操作------------");
+        var = null;
+        System.gc();
+        // 下面代码和上面代码是一样的,但是 对象却自救失败了
+        Thread.sleep(2000);
+        if (var == null) {
+            System.out.println("对象已经死了");
+        } else {
+            System.out.println("对象还活着");
+        }
+    }
+
+}
+

getClass

+
    /**
+     * Returns the runtime class of this {@code Object}. The returned
+     * {@code Class} object is the object that is locked by {@code
+     * static synchronized} methods of the represented class.
+     */
+     public final native Class<?> getClass();
+

大意:返回这个对象的运行时类。返回的Class对象是被表示类的static synchronized方法锁定的对象。

+

getClass方法返回对象运行时的类。返回的类型是Class类型的对象。可以通过这个Class对象来创建调用这个方法的对象和执行一些其他操作,这便是反射的入口。

+

除了可以使用getClass来获取反射入口外,还有一种方法与getClass()方法极为相似:获取对象的.class属性。

+

二者区别:

+
    +
  • .class其实是在java运行时就加载进去的,可以说是编译时期就决定好的
  • +
  • getClass()是运行程序时动态加载的
  • +
+

wait、notify、notifyAll

+

之所以把这三个方法放在一起,是因为他们是搭配使用的。

+
    +
  • wait方法的作用是让当前对象上的线程进入等待状态,同时wait()也会让当前线程释放它所持有的锁。直到其他线程调用此对象的notify()方法或 notifyAll() 方法,当前对象上线程被唤醒进入就绪状态。
  • +
  • notify()notifyAll()的作用,则是唤醒当前对象上的等待线程;notify()是(随机)唤醒当前对象上单个线程,而notifyAll()是唤醒当前对象上所有的线程。
  • +
  • wait(long timeout)方法让当前对象上线程处于等待(阻塞)状态,直到其他线程调用此对象的notify()方法或notifyAll()方法,或者超过指定的时间量,当前线程被唤醒进入就绪状态。
  • +
+

需要注意的是wait方法与sleep方法,很多人分不清他俩。

+

sleepwait方法异同点:

+
    +
  • sleep()属于Thread类,wait()属于Object类;
  • +
  • sleep()wait()都会抛出InterruptedException异常,这个异常属于checkedException不可避免;
  • +
  • 两者比较的共同之处是,都是使程序等待多长时间。不同的是调用sleep()不会释放锁,会使线程堵塞,而调用wait()会释放锁,让线程进入等待状态,用 notify()、notifyall()可以唤醒,或者等待时间到了; 这是因为,如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 notify() 或者 notifyAll() 来唤醒挂起的线程,造成死锁。
  • +
  • wait()必须在同步synchronized块里使用,sleep()可以在任意地方使用;
  • +
+

其中"wait()必须在同步synchronized块里使用",使其不止wait方法,notify、notifyAll也和wait方法一样,必须在synchronized块里使用,为什么呢?

+
    +
  • 是为了避免丢失唤醒问题。假设没有synchronized修饰,使用了wait方法而没有设置等待时间,也没有调用唤醒方法或者唤醒方法调用的时机不对,这个线程将会永远的堵塞下去。
  • +
  • wait()、notify、notifyAll方法调用的时候要释放锁,你都没给它加锁,他怎么释放锁,所以如果没在synchronized块中调用wait()、notify、notifyAll方法是肯定抛异常的。
  • +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/java/rookie-operation/index.html b/blog-site/public/posts/java/rookie-operation/index.html new file mode 100644 index 00000000..2a67d89b --- /dev/null +++ b/blog-site/public/posts/java/rookie-operation/index.html @@ -0,0 +1,2665 @@ + + + + + + + + + + + Java运算 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Java运算

+ 2021.01.30 +
+

运算符与表达式

+

运算符

+

运算符指明对操作数的运算方式。组成表达式的Java操作符有很多种。运算符按照其要求的操作数数目来分,可以有单目运算符、双目运算符和三目运算符,它们分别对应于1个、2个、3个操作数。

+

种类

+

运算符按其功能来分:有算术运算符、赋值运算符、关系运算符、逻辑运算符、位运算符和其他运算符.

+
算术运算符
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
运算符名称用法描述备注
+加法相加运算符两侧的值
-减法左操作数减去右操作数
*乘法相乘操作符两侧的值
/除法左操作数除以右操作数右操作数不能为0
%取余(模)(左操作数除以右操作数)的余数
++自增操作数的值增加1
--自减操作数的值减少1
+

基本的+、-、*、/就不写了.记录一下剩下的算数运算符。

+
取余
+

基本用法

+
    public static void main(String[] args) {
+        System.out.println(0%5); // 0
+        System.out.println(1%5); // 1
+        System.out.println(2%5); // 2
+        System.out.println(3%5); // 3
+        System.out.println(4%5); // 4
+        System.out.println(5%5); // 0
+        System.out.println("------------");
+        System.out.println(6%5); // 1
+        System.out.println(7%5); // 2
+        System.out.println(8%5); // 3
+        System.out.println(9%5); // 4
+        System.out.println(9%1); // 0
+    }
+

如果被除数小于除数,那取模的结果就是被除数.如果被除数等于除数,结果是0,如果除数是1,结果是0.

+

取余应用

+

当使用随机数生成器产生的结果时,取余运算(%)可将结果限制在上限为操作数最大值减1的范围.

+

例如:n是随机数,那么n%10就是0~9中的一个数.无论n是多大的数,n%10只能是0~9之间的一个数,其中10就是操作数.可以利用这一特性,可以在数据库分库,分表等.

+
自增和自减
+

基本用法

+
    public static void main(String[] args) {
+        int i = 1;
+        System.out.println(i++); // 1
+        System.out.println(i--); // 2
+    }
+

需要特别留意的是++--运算符可以前置、后置,都是合法的语句,如a++++a都是合法的,上面这两种写法其最终的结果都是是变量a的值加1了,但是它们之间也是有区别的,其区别是:表达是++a会先将a的值自增1,然后在使用变量a。而表达式a++是先使用了a的值,然后再让a的值自增1。也就是说在一些表达式,使用a++和使用++a得到的结果时不一样的。

+
    public static void main(String[] args) {
+        int i = 1;
+        System.out.println(i++);// 1
+        System.out.println(++i);// 3
+    }
+

自增(++)和自减(--)两个运算符只能作用于变量,而不能作用于表达式.

+
    public static void main(String[] args) {
+        int j = 0, i = 1;
+        // 编译报错
+        System.out.println((j+i)++);
+    }
+
赋值运算符
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
运算符名称用法描述
=赋值将右操作数的值赋给左侧操作数
+=加等于把左操作数和右操作数相加赋值给左操作数
-=减等于把左操作数和右操作数相减赋值给左操作数
*=乘等于把左操作数和右操作数相乘赋值给左操作数
/=除等于把左操作数和右操作数相除赋值给左操作数
%=模等于把左操作数和右操作数取模后赋值给左操作数
<<=左位移等于把左操作数和右操作数进行左移运算后赋值给左操作数
>>=右位移等于把左操作数和右操作数进行右移运算后赋值给左操作数
&=按位与等于把左操作数和右操作数进行按位与运算后赋值给左操作数
|=按位或等于把左操作数和右操作数进行按位或运算后赋值给左操作数
^=异或等于把左操作数和右操作数进行按位异或运算后赋值给左操作数
+
关系运算符
+

关系运算符也称为比较运算符. +用于测试两个操作数之间的关系.使用关系运算符表达式的最终结果为boolean型,也就是其结果只有两个truefalse.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
运算符名称用法描述
==双等号检查两个操作数的值是否相等,如果相等则条件为真.
!=不等号检查两个操作数的值是否相等,如果值不相等则条件为真.
>大于检查左操作数的值是否大于右操作数的值,如果是那么条件为真.
<小于检查左操作数的值是否小于右操作数的值,如果是那么条件为真.
>=大于等于检查左操作数的值是否大于或等于右操作数的值,如果是那么条件为真.
<=小于等于检查左操作数的值是否小于或等于右操作数的值,如果是那么条件为真.
+
逻辑运算符
+ + + + + + + + + + + + + + + + + + + + + + + + + +
运算符名称用法描述
&&逻辑与当且仅当两个操作数都为真,条件才为真.
||逻辑或如果两个操作数任何一个为真,条件为真.
!逻辑非用来反转操作数的逻辑状态.如果条件为true,通过逻辑非将得到false.
+
短路运算符
+

&&运算符,运算顺序是从左到右计算,运算规则是如果两个操作数都是真,则返回true,否则返回false.但是当判定到第一个操作数为false时,其结果必定为false,这时候就不再会判定第二个操作数了.

+
    public static void main(String[] args) {
+        int i = 1, j = 2;
+        boolean flag = (i++ == 2) && (++j == 3);
+        // flag的值: false,i的值:2,j的值:2
+        System.out.printf("flag的值: %s,i的值:%s,j的值:%s", flag, i, j);
+    }
+
位运算符
+

位运算符在追求代码效率和编写底层应用时,使用的比较多;在企业Java开发一般用到的较少.

+

因为位运算符是以bit运算单位的.所以想要要弄明白位运算符,就要先弄明白2进制的表示方法.

+

位运算符只能对整数型(int,long,short,byte)和字符型数据(char)进行操作.

+
+

>>,右移几位就是相当于除以2的几次幂 +<<,左移几位就是相当于乘以2的几次幂 +%,当b为2的n次方时,有如下替换公式:a % b = a & (b-1)

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
运算符名称用法描述
&按位与如果相对应位都是1,则结果为1,否则为0
|按位或如果相对应位都是 0,则结果为 0,否则为 1
^按位异或如果相对应位值相同,则结果为0,否则为1
~按位取反翻转操作数的每一位,即0变成1,1变成0.
<<左移左操作数按位左移右操作数指定的位数.
>>右移左操作数按位右移右操作数指定的位数.
>>>无符号右移左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充.
+

基本用法

+
    public static void main(String[] args) {
+        int i = 1, j = 2;
+        System.out.println(i&j);//0
+        System.out.println(i|j);//3
+        System.out.println(i^j);//3
+        System.out.println(~(j+i));//-4
+        System.out.println(i<<j);//4
+        System.out.println(i>>j);//0
+        System.out.println(i>>>j);//0
+    }
+

解析:

+

以下示例用十进制的1和2进行运算.1用二进制表示为0000 0001,2用二进制表示为0000 001010进制2进制互相转换怎么转就不讲了.不懂的小伙伴可自行查看链接.

+

&:如果相对应位都是1,则结果为1,否则为0

+
0000 0001
+0000 0010
+——————————
+0000 0000
+

|:如果相对应位都是 0,则结果为 0,否则为 1

+
0000 0001
+0000 0010
+——————————
+0000 0011
+

^:如果相对应位值相同,则结果为0,否则为1

+
0000 0001
+0000 0010
+——————————
+0000 0011
+

~:翻转操作数的每一位,即0变成1,1变成0.

+
+

十进制负数转换为二进制,就是将其相反数(正数)的补码的每一位变反(1变0,0变1)最后将变完了的数值加1,就完成了负数的补码运算.这样就变成了二进制.二进制转十进制负数相反.

+
+
0000 0011
+——————————
+1111 1100
+

<<:左操作数按位左移右操作数指定的位数.

+

1左移两位

+
0000 0001
+——————————
+0000 0100
+

>>:左操作数按位右移右操作数指定的位数.

+

正数右移高位补0,负数右移高位补1

+

1右移两位

+
0000 0001
+——————————
+0000 0000
+

负一右移两位

+
1111 1111
+——————————
+1111 1111
+

>>>:左操作数的值按右操作数指定的位数右移,移动得到的空位以零填充.

+

无符号右移,无论是正数还是负数,高位通通补0

+

1无符号右移两位

+
0000 0001
+——————————
+0000 0000
+

负一无符号右移两位

+
1111 1111
+——————————
+0011 1111
+
其他运算符
+
三目运算符
+

该运算符有3个操作数,其功能是对表达式条件进行判断,根据判断结果是true或者false两种情况赋予赋予不同的返回值. +该运算符的主要是决定哪个值应该赋值给变量.

+

在某些情况下,可以发现三目运算符和if...else作用是相同的.

+
    public static void main(String[] args) {
+        int var1 = 1;
+        int var2 = 0;
+        var1 = var2 > 1 ?  1: 2;
+        
+        // 等价于if...else以下代码
+        if (var2 > 1) {
+            var1 = 1;
+        }else {
+            var1 = 2;
+        }
+    }
+

三目运算符的原理到底是什么,与if...else到底哪有啥不同?要探究这个原因,需要看JVM是对三目运算符怎么处理的.

+

javap -verbose命令,分别把if...else和三目运算符相关代码进行反编译

+

三目运算符

+
    public static void main(String[] args) {
+        int var1 = 1;
+        int var2 = 0;
+        // 如果var2大于1 var1等于1 否则等于2
+        var1 = var2 > 1 ?  1: 2;
+    }
+
  // ...
+{
+ // ...
+  public static void main(java.lang.String[]);
+    descriptor: ([Ljava/lang/String;)V
+    flags: ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=2, locals=3, args_size=1
+         0: iconst_1 // 向栈里添加int类型 1
+         1: istore_1 // int 1 存储到局部变量
+         2: iconst_0 // 向栈里添加int类型 0
+         3: istore_2 // int 2 存储到局部变量
+         4: iload_2  // 从局部变量加载int 2
+         5: iconst_1
+         6: if_icmple     13  // 1,3步骤栈中的变量弹出 进行比较
+         9: iconst_1 // 比较成功 存储该值到栈中
+        10: goto          14 // 改变地址
+        13: iconst_2
+        14: istore_1
+        15: return
+      LineNumberTable:
+        line 6: 0
+        line 7: 2
+        line 8: 4
+        line 9: 15
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0      16     0  args   [Ljava/lang/String;
+            2      14     1  var1   I
+            4      12     2  var2   I
+      StackMapTable: number_of_entries = 2
+        frame_type = 253 /* append */
+          offset_delta = 13
+          locals = [ int, int ]
+        frame_type = 64 /* same_locals_1_stack_item */
+          stack = [ int ]
+}
+// ...
+

if...else

+
    public static void main(String[] args) {
+        int var1 = 1;
+        int var2 = 0;
+        if (var2 > 1){
+            var1 = 1;
+        } else {
+            var1 = 2;
+        }
+    }
+
  // ...
+  public static void main(java.lang.String[]);
+    descriptor: ([Ljava/lang/String;)V
+    flags: ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=2, locals=3, args_size=1
+         0: iconst_1
+         1: istore_1
+         2: iconst_0
+         3: istore_2
+         4: iload_2
+         5: iconst_1
+         6: if_icmple     14
+         9: iconst_1
+        10: istore_1
+        11: goto          16
+        14: iconst_2
+        15: istore_1
+        16: return
+      LineNumberTable:
+        line 6: 0
+        line 7: 2
+        line 8: 4
+        line 9: 9
+        line 11: 14
+        line 13: 16
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0      17     0  args   [Ljava/lang/String;
+            2      15     1  var1   I
+            4      13     2  var2   I
+      StackMapTable: number_of_entries = 2
+        frame_type = 253 /* append */
+          offset_delta = 14
+          locals = [ int, int ]
+        frame_type = 1 /* same */
+}
+// ...
+

通过查阅《Java虚拟机指令集》,可以看到JVM对上边的代码进行了什么处理.

+

通过比较两次反编译后的代码可以得出一个结论:三目运算符,省去了一步赋值操作 所以实际开发中三目运算符的运算效率略高于if..else. +但是对于实际开发来说三目运算符的可读性相对不如if...else代码,所以在常见的编码中,三目运算符更倾向于简单的if...else语句的替代.

+
instanceof
+

interfaceof是一个双目运算符,该关键字的作用是判断左边的对象是不是右边类的实例,并返回一个boolean

+

基本用法

+
    public static void main(String[] args) {
+        String str1 = "123";
+        String str = (str1 instanceof Object) ? "123" : "456";
+    }
+

运算顺序

+

Java 语言中大部分运算符也是从左向右结合的,只有单目运算符、赋值运算符和三目运算符例外,是从右向左运算的.

+

Java 语言中运算符的优先级共分为 14 级,其中 1 级最高,14 级最低.在同一个表达式中运算符优先级高的先执行.

+
+

算术运算符>比较运算符>赋值运算符>逻辑运算符>三元运算符

+可使用以下口诀记住: +单目乘除为关系,逻辑三目后赋值.

+
+

运算符优先级表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
优先级运算符结合性
1()、[]从左向右
2!、+、-、~、++、–从右向左
3*、/、%从左向右
4+、-从左向右
5«、»、»>从左向
6<、<=、>、>=、instanceof从左向右
7==、!=从左向右
8&从左向右
10|从左向右
11&&从左向右
12||从左向右
13?:从右向左
14=、+=、-=、*=、/=、&=、|=、^=、~=、«=、»=、»>=从右向左
+

建议

+
    +
  • 不要把一个表达式写得过于复杂,如果一个表达式过于复杂,则会影响代码可读性,建议把它分成几步来完成.
  • +
  • 不要过多地依赖运算符的优先级来控制表达式的执行顺序,这样可读性太差,尽量使用小括号来控制表达式的执行顺序.
  • +
+

表达式

+

表达式是由运算符和运算对象组成的,单独的一个运算对象(常量/变量)也可以叫做表达式.

+

变量的赋值与计算都离不开表达式,表达式的运算依赖于变量、常量和运算符.

+

在Java中表达式通常是以分号结尾的一段代码.

+
float f1 = 1.1f;
+float f2 = 1.2f;
+float f3 = f1 + f2;
+

Java正则表达式

+
+

正则表达式,又称规则表达式。(英语:Regular Expression,在代码中常简写为regex、regexp或RE),计算机科学的一个概念。正则表达式通常被用来检索、替换那些符合某个模式(规则)的文本。在众多语言中都可以支持正则表达式,如Perl、PHP、Java、Python、Ruby等.

+
+

正则表达式并不仅限于某一种语言,但是在每种语言中有细微的差别.

+

例如:

+
+

在其他语言中,\\ 表示:我想要在正则表达式中插入一个普通的(字面上的)反斜杠.
+在 Java 中,\\ 表示:我要插入一个正则表达式的反斜线,所以其后的字符具有特殊的意义。
+所以,在其他的语言中,一个反斜杠\就足以具有转义的作用,而在 Java 中正则表达式中则需要有两个反斜杠才能被解析为其他语言中的转义作用。也可以简单的理解在 Java 的正则表达式中,两个 \\ 代表其他语言中的一个 \,这也就是为什么表示一位数字的正则表达式是 \\d,而表示一个普通的反斜杠是 \\\\

+
+

正则表达式是由普通字符(如英文字母)以及特殊字符(也称为元字符)组成的文字模式.

+

例如:

+
String str = "abc^123/?[1,2]";
+

在Java语言中主要是使用正则表达式处理字符串.Java从jdk1.4开始提供了一个包java.util.regex来处理正则表达式.

+

在Java开发中主要使用java.util.regex包中Pattern类,Matcher类来处理字符串. +详情参照文档《Java™平台 +标准Ed. 8》,《菜鸟教程》Java正则表达式 ,《JavaSchool》Java正则表达式 这些文档里面有详细的教程,所以这里不作过多介绍了.

+

使用正则表达式示例

+
    public static void main(String[] args) {
+        // 表示匹配以字母a为开头的单词
+        String regx = "\\ba\\w*\\b";
+        // 将给定的正则表达式编译到具有给定标志的模式中
+        Pattern pattern = Pattern.compile(regx);
+        // 创建匹配给定输入与此模式的匹配器
+        Matcher matcher = pattern.matcher("abcdab cccabcd aaacd");
+        int index = 0;
+        // 循环 查找与上边正则匹配的字符序列
+        while (matcher.find()) {
+            // 返回 由以前匹配操作 所匹配的 输入子序列
+            String res = matcher.group();
+            System.out.println(index + ":" + res);
+            index++;
+        }
+    }
+
0:abcdab
+1:aaacd
+

总的来说正则表达式是对字符串操作的一种逻辑公式,用事先定义好的一些特定字符、及这些特定字符的组合,组成一个"规则字符串",这个"规则字符串"用来表达对字符串的一种过滤逻辑.

+

正则表达式的灵活性、逻辑性和功能性非常的强.可以迅速地用极简单的方式达到字符串的复杂控制.但是对于刚接触的人来说,比较晦涩难懂.

+
常用正则
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
描述正则表达式
是否为数字^[0-9]*$
是否为n位数数字^\d{n}$
是否为m-n位的数字^\d{m,n}$
是否输入至少n位的数字^/d{n,}$"
是否为整数^-?/d+$
是否为负整数^-[0-9]*[1-9][0-9]*$
是否为正整数^[0-9]*[1-9][0-9]*$
是否为汉字^[\u4e00-\u9fa5]{0,}$
是否为邮箱^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$
是否为域名[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?
是否为URL^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=]*)?$
是否为手机号码`^(13[0-9]
是否为固话号码`^($$\d{3,4}-)
是否为身份证号码`^([0-9]){7,18}(x
是否以字母开头,长度在6~18之间,只能包含字母、数字和下划线^[a-zA-Z]\w{5,17}$
是否必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,10}$
是否为腾讯QQ号码[1-9][0-9]{4,} (QQ号从10000开始)
是否为中国邮政编码[1-9]\d{5}(?!\d)
是否为ip地址`((25[0-5]
+

控制流语句

+

if-else语句

+

if语句是最基本的控制语句,它只有在If(exception)true的时候才会执行特定的代码.

+
    public static void main(String[] args) {
+        boolean var1 = true;
+        if (var1){
+            System.out.println("hello world ...");
+        }
+    }
+

if语句后面可以跟else语句.当If(exception)false时,else语句体将被执行.

+
    public static void main(String[] args) {
+        boolean var1 = false;
+        if (var1){
+            System.out.println("if ...");
+        }else{
+            System.out.println("else ...");
+        }
+    }
+

if语句后面可以跟 else if…else 语句

+
public class HelloWorld {
+    public static void main(String[] args) {
+        String var = "123";
+        if ("123".equals(var)){
+            System.out.println("123 ...");
+        }else if("234".equals(var)){
+            System.out.println("234 ...");
+        }else if ("345".equals(var)){
+            System.out.println("345 ...");
+        }else{
+            System.out.println("...");
+        }
+    }
+
+}
+

switch语句

+

switch语句是在项目开发中是比较常用的.可以和if...else起到相同的作用,但是用switch代码可读性更高.

+

switch case语句判断一个变量与一系列值中某个值是否相等,每个值称为一个分支.

+

switch语句可以包含一个default分支. default 在没有case语句的值和变量值相等的时候执行.default 分支不需要break语句。

+
    public static void main(String[] args) {
+        String s = "123";
+        switch (s) {
+            case "123":
+                System.out.println("123");
+                break;
+            case "456":
+                System.out.println("456");
+                break;
+             default :
+                System.out.println("over ...");
+        }
+    }   
+

switch语句中的变量类型可以是: byte,short,int,char 或者 enum从 Java 7 开始,可以在switch条件判断语句中使用String对象.

+

switch 不支持 long,是因为 switch 的设计初衷是对那些只有少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。

+
long x = 111;
+ switch (x) { 
+     case 111:
+         System.out.println(111);
+         break;
+     case 222:
+         System.out.println(222);
+         break;
+}
+

当变量的值与case语句的值相等时,那么case语句之后的语句开始执行,直到break语句出现才会跳出switch语句.如果没有break语句出现,程序会继续执行下一条case语句,直到出现break语句.

+
public static void main(String[] args) {
+        int i = 100;
+        switch (i){
+            case 100:
+                System.out.println("first case ...");
+            case 200:
+                System.out.println("second case ...");
+                break;
+        }
+    }
+

for语句

+

for循环执行的次数是在执行前就确定的.

+

for语句提供了一种紧凑的方法来遍历一系列值。程序员经常将其称为“for循环”,因为它反复循环直到满足特定条件。for语句的一般形式可以表示为:

+
    public static void main(String[] args) {
+        for (int i = 0; i < 100; i++) {
+            System.out.println(i);
+        }
+    }
+

for循环的三个表达式是可选的;可以这样创建一个无限循环:

+
/ /无限循环
+for(;;){
+
+    // to do ...
+}
+

foreach语句是java5的新特征之一,在遍历数组、集合方面,为开发人员提供了极大的方便.

+

foreach循环的效率大概是普通for循环效率的一半 ,但在项目开发中如果只是少量的循环,可以忽略foreach带来的影响.

+
    public static void main(String[] args) {
+        String[] arr = {"1", "2", "3"};
+        ArrayList<String> list = new ArrayList<>(Arrays.asList(arr));
+        for (String s : list) {
+            System.out.println(s);
+        }
+        for (String s : arr) {
+            System.out.println(s);
+        }
+    }
+

分支语句break,continuereturn

+

continue语句用来结束当前循环,并进入下一次循环,即仅仅这一次循环结束了,不是所有循环结束了,后边的循环依旧进行.

+
    public static void main(String[] args) {
+        String[] arr = {"1", "2", "3"};
+        for (String s : arr) {
+            // 如果s等于1,则结束本次循环 执行下次循环
+            if ("1".equals(s)) {
+                continue;
+            }
+            System.out.println("...");
+        }
+    }
+

break语句作用是跳出循环.break主要用在循环语句或者switch语句中

+
public class HelloWorld {
+    public static void main(String[] args) {
+        String[] arr = {"1", "2", "3"};
+        for (String s : arr) {
+            // s等于2,结束for循环
+            if ("2".equals(s)) {
+                break;
+            }
+            System.out.println("...");
+        }
+    }
+
+}
+

如果存在多层循环,要注意break只能跳出一层循环.

+
    public static void main(String[] args) {
+        String[] arr = {"1", "2", "3"};
+        for (int i = 0; i < 10; i++) {
+            for (String s : arr) {
+                if ("2".equals(s)) {
+                    break;
+                }
+                System.out.println("...");
+            }
+            System.out.println("外层 for ...");
+        }
+    }
+

如果存在多层循环,可以用以下方式,当然也可以用break跳两次循环

+
    public static void main(String[] args) {
+        String[] arr = {"1", "2", "3"};
+        
+        test:
+        for (int i = 0; i < 10; i++) {
+            for (String s : arr) {
+                if ("2".equals(s)) {
+                    break test;
+                }
+                System.out.println("...");
+            }
+            System.out.println("外层 for ...");
+        }
+    }
+

continue也可以用这种方式

+
    public static void main(String[] args) {
+        String[] arr = {"1", "2", "3"};
+
+        test:
+        for (int i = 0; i < 10; i++) {
+            for (String s : arr) {
+                if ("1".equals(s)) {
+                    continue test;
+                }
+            }
+            System.out.println("外层 for ...");
+        }
+    }
+

return语句表示从当前方法退出,控制流返回到调用方法的地方。

+

上面continue的示例,用return也能达到一样的效果

+
    public static void main(String[] args) {
+        String[] arr = {"1", "2", "3"};
+
+        for (int i = 0; i < 10; i++) {
+            for (String s : arr) {
+                if ("1".equals(s)) {
+                    return;
+                }
+            }
+            System.out.println("外层 for ...");
+        }
+    }
+

return语句有两种形式:一种返回值,另一种不返回值。要返回一个值,只需将值(或计算该值的表达式)放在return关键字之后.

+

像这样

+
return ++count;
+

do-while和while语句

+

while语句对表达式进行计算,表达式必须返回一个布尔值。如果表达式的计算结果为true,while语句将执行while块中的语句。while语句继续测试表达式并执行其块,直到表达式的计算结果为false.

+
    public static void main(String[] args) {
+        int i = 0;
+       while(++i >= 1){
+           i--;
+        }
+    }
+

死循环

+
    public static void main(String[] args) {
+        while (true) {
+        }
+    }
+

do…while循环和while循环相似,不同的是do…while循环至少会执行一次.

+
    public static void main(String[] args) {
+        int i = 0;
+        do{
+            i++;
+            System.out.println(" ... ");
+        }while (--i == 1);
+    }
+

注意:布尔表达式在循环体的后面,所以语句块在检测布尔表达式之前已经执行了.如果布尔表达式的值为true,则语句块一直执行,直到布尔表达式的值为false

+

参数传递

+

实参与形参

+
+

形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。

+
+
+

实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。

+
+

举个例子:

+
public class HelloWorld {
+    public static void main(String[] args) {
+        // 实参 
+        test("world");
+    }
+    
+    // 形参
+    public static void test(String param){
+        System.out.println("hello " + param);
+    }
+}
+

值传递与引用传递

+
+

值传递: 是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

+
+
+

引用传递: 是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

+
+ + + + + + + + + + + + + + + + + + + + +
值传递引用传递
区别传参时会创建副本传参数不创建副本
描述方法中无法修改原始对象方法中可以修改原始对象
+

Java传递对象参数

+

很多人理解Java中,传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递.这个理解是错误的.

+

举个例子:

+

如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。

+
class PassByValueExample {
+    public static void main(String[] args) {
+        Dog dog = new Dog("A");
+        func(dog);
+        System.out.println(dog.getName());          // B
+    }
+
+    private static void func(Dog dog) {
+        dog.setName("B");//dog.name="B";
+    }
+}
+

然后有人就说Java传递对象类型参数是引用传递.这一点在官方《Java™教程》中有相关的描述.

+
+

Passing Reference Data Type Arguments. +Reference data type parameters, such as objects, are also passed into methods by value. This means that when the method returns, the passed-in reference still references the same object as before. However, the values of the object’s fields can be changed in the method, if they have the proper access level.

+
+

传递引用数据类型参数。 +引用数据类型参数(如对象)也按值传递给方法。这意味着,当方法返回时,传入的引用仍然引用与之前相同的对象。但是,如果对象的字段具有适当的访问级别,则可以在方法中更改它们的值。

+
+

例如,考虑任意类中的一个移动Circle对象的方法:

+
+
public void moveCircle(Circle circle, int deltaX, int deltaY) {
+    // code to move origin of circle to x+deltaX, y+deltaY
+    circle.setX(circle.getX() + deltaX);
+    circle.setY(circle.getY() + deltaY);
+        
+    // code to assign a new reference to circle
+    circle = new Circle(0, 0);
+}
+
+

使用以下参数调用该方法:

+
+
moveCircle(myCircle, 23, 56)
+
+

在这个方法中,circle最初指的是myCircle。该方法将circle引用的对象(即myCircle)的x坐标和y坐标分别更改了23和56。当方法返回时,这些更改将持续存在。然后circle被赋予一个新的circle对象x = y = 0的引用。但是,这种重新分配不具有持久性,因为引用是按值传入的,不能更改。在方法中,circle指向的对象已经改变,但是,当方法返回时,myCircle仍然引用与方法调用之前相同的circle对象。

+
+

值传递和引用传递最大的区别是传递的过程中有没有复制出一个副本来,如果是传递副本,那就是值传递,否则就是引用传递。 +在Java中,其实是通过值传递实现的参数传递,只不过对于Java对象的传递,传递的内容是对象的引用。 +所以说 Java的参数是以值传递的形式传入方法中,而不是引用传递.

+

Java对象的传递,是通过复制的方式把引用关系传递了,如果我们没有改引用关系,而是找到引用的地址,把里面的内容改了,是会对调用方有影响的,因为大家指向的是同一个共享对象。

+

以下代码中Dog类中的dog是一个指针,存储的是对象的地址.

+
public class Dog {
+
+    String name;
+
+    Dog(String name) {
+        this.name = name;
+    }
+
+    String getName() {
+        return this.name;
+    }
+
+    void setName(String name) {
+        this.name = name;
+    }
+
+    String getObjectAddress() {
+        return super.toString();
+    }
+}
+public class PassByValueExample {
+    public static void main(String[] args) {
+        Dog dog = new Dog("A");
+        // Dog@4554617c
+        System.out.println(dog.getObjectAddress()); 
+        func(dog);
+        // Dog@4554617c
+        System.out.println(dog.getObjectAddress()); 
+        // A
+        System.out.println(dog.getName());          
+    }
+
+    private static void func(Dog dog) {
+        // Dog@4554617c
+        System.out.println(dog.getObjectAddress()); 
+        //重新new生成新的对象
+        dog = new Dog("B");
+        // Dog@74a14482
+        System.out.println(dog.getObjectAddress()); 
+        // B
+        System.out.println(dog.getName());          
+    }
+}
+

Java运算

+

精度丢失

+

在Java中,使用浮点类型进行计算会造成精度丢失

+

例如:

+
    public static void main(String[] args) {
+        // 0.20000005
+        System.out.println(1.2f - 1);
+        // 0.19999999999999996
+        System.out.println(1.2d - 1);
+    }
+

那么为什么会浮点类型会存在精度精度丢失问题呢?

+

因为Java的浮点类型在计算机中是用二进制来存储的,也就是小数在转二进制的时候出现了精度丢失.

+

PS: 小数如何转二进制

+
+

将该数字乘以2,取出整数部分作为二进制表示的第1位;然后再将小数部分乘以2,将得到的整数部分作为二进制表示的第2位;以此类推,直到小数部分为0.
+特殊情况: 小数部分出现循环,无法停止,则用有限的二进制位无法准确表示一个小数,这也是在编程语言中表示小数会出现误差的原因.

+
+

例如: 0.1 转二进制

+
    0.1 转2进制
+    
+    0.1 x 2 = 0.2  取整数位 0
+    0.2 x 2 = 0.4  取整数位 0
+    0.4 x 2 = 0.8  取整数位 0
+    0.8 x 2 = 1.6  取整数位 1
+    0.6 x 2 = 1.2  取整数位 1
+    0.2 x 2 = 0.4  取整数位 0
+    0.4 x 2 = 0.8  取整数位 0
+    0.8 x 2 = 1.6  取整数位 1
+    0.6 x 2 = 1.2  取整数位 1
+    
+    ........无限循环
+

因为计算机中存储一个浮点类型所用的位数是有限的,所以遇到无限循环的小数,只能选择在某个精度进行保存.

+

由于计算机中保存的小数其实是十进制的小数的近似值,并不是准确值,所以,千万不要在代码中使用浮点数来表示金额等重要的指标。

+

BigDecimal

+
+

Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数。在实际应用中,需要对更大或者更小的数进行运算和处理。floatdouble只能用来做科学计算或者是工程计算,在商业计算中要用java.math.BigDecimal BigDecimal所创建的是对象,我们不能使用传统的+、-、*、/等算术运算符直接对其对象进行数学运算,而必须调用其相对应的方法。方法中的参数也必须是BigDecimal的对象。构造器是类的特殊方法,专门用来创建对象,特别是带有参数的对象.

+
+

Java中提供了大数字(超过16位有效位)的操作类,即java.math.BinInteger类和java.math.BigDecimal类,用于高精度计算.

+

其中BigInteger类是针对大整数的处理类,而BigDecimal类则是针对大小数的处理类.BigDecimal类的实现用到了 BigDecimalBigInteger类,不同的是BigDecimal加入了小数的概念.

+

之所以要用BigDecimal,是因为十进制的小数在转化成二进制浮点数时会精度丢失.

+

使用

+
基本运算
+

BigDecimal类创建的是对象,不能使用传统的+、-、*、/等算术运算符直接对其进行数学运算,而必须调用其对应的方法.方法的参数也必须是BigDecimal类型的对象.

+
    public static void main(String[] args) {
+        BigDecimal num1 = new BigDecimal("2");
+        BigDecimal num2 = new BigDecimal("1");
+        BigDecimal num3;
+
+        num3 = num1.add(num2);
+        System.out.printf("num1 + num2 = %s\n",num3);
+
+        num3 = num1.subtract(num2);
+        System.out.printf("num1 - num2 = %s\n",num3);
+
+        num3 = num1.multiply(num2);
+        System.out.printf("num1 * num2 = %s\n",num3);
+
+        num3 = num1.divide(num2);
+        System.out.printf("num1 / num2 = %s\n",num3);
+        
+        // 绝对值
+        System.out.printf("|num1 / num2| = %s\n",num3.abs());
+    }
+

BigDecimal基本用法如上所示,重点记录一下除法.

+

在使用除法的时候如果两个数字,除不尽.而又没有设置精确小数位和舍入模式,就会报错.

+
    public static void main(String[] args) {
+        BigDecimal num1 = new BigDecimal("1");
+        BigDecimal num2 = new BigDecimal("3");
+        BigDecimal num3;
+
+        num3 = num1.divide(num2);
+        System.out.printf("num1 / num2 = %s\n",num3);
+    }
+
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
+

为了防止报错,我们可以这样写

+
    public static void main(String[] args) {
+        BigDecimal num1 = new BigDecimal("1");
+        BigDecimal num2 = new BigDecimal("3");
+        BigDecimal num3;
+
+        num3 = num1.divide(num2,3, BigDecimal.ROUND_UP);
+        // 打印结果: num1 / num2 = 0.334
+        System.out.printf("num1 / num2 = %s\n",num3);
+    }
+

BigDecimaldivide方法

+
// divisor: 除数; scale: 精确小数位; roundingMode: 舍入模式
+public BigDecimal divide(BigDecimal divisor, int scale, int roundingMode)
+

舍入模式,如果不指定默认采用四舍五入方式.

+
/**
+     * Rounding mode to round away from zero.  Always increments the
+     * digit prior to a nonzero discarded fraction.  Note that this rounding
+     * mode never decreases the magnitude of the calculated value.
+     * 向远离0的方向舍入 如2.35 --> 2.4
+     */
+    public final static int ROUND_UP =           0;
+
+    /**
+     * Rounding mode to round towards zero.  Never increments the digit
+     * prior to a discarded fraction (i.e., truncates).  Note that this
+     * rounding mode never increases the magnitude of the calculated value.
+     * 向零方向舍入 直接删除多余小数位 如2.35 --> 2.3
+     */
+    public final static int ROUND_DOWN =         1;
+
+    /**
+     * Rounding mode to round towards positive infinity.  If the
+     * {@code BigDecimal} is positive, behaves as for
+     * {@code ROUND_UP}; if negative, behaves as for
+     * {@code ROUND_DOWN}.  Note that this rounding mode never
+     * decreases the calculated value.
+     * 向正无穷方向舍入
+     */
+    public final static int ROUND_CEILING =      2;
+
+    /**
+     * Rounding mode to round towards negative infinity.  If the
+     * {@code BigDecimal} is positive, behave as for
+     * {@code ROUND_DOWN}; if negative, behave as for
+     * {@code ROUND_UP}.  Note that this rounding mode never
+     * increases the calculated value.
+     * 向负无穷方向舍入
+     */
+    public final static int ROUND_FLOOR =        3;
+
+    /**
+     * Rounding mode to round towards {@literal "nearest neighbor"}
+     * unless both neighbors are equidistant, in which case round up.
+     * Behaves as for {@code ROUND_UP} if the discarded fraction is
+     * &ge; 0.5; otherwise, behaves as for {@code ROUND_DOWN}.  Note
+     * that this is the rounding mode that most of us were taught in
+     * grade school.
+     * 向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,向上舍入, 1.55保留一位小数结果为1.6
+     */
+    public final static int ROUND_HALF_UP =      4;
+
+    /**
+     * Rounding mode to round towards {@literal "nearest neighbor"}
+     * unless both neighbors are equidistant, in which case round
+     * down.  Behaves as for {@code ROUND_UP} if the discarded
+     * fraction is {@literal >} 0.5; otherwise, behaves as for
+     * {@code ROUND_DOWN}.
+     * 向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,向下舍入, 例如1.55 保留一位小数结果为1.5 
+     */
+    public final static int ROUND_HALF_DOWN =    5;
+
+    /**
+     * Rounding mode to round towards the {@literal "nearest neighbor"}
+     * unless both neighbors are equidistant, in which case, round
+     * towards the even neighbor.  Behaves as for
+     * {@code ROUND_HALF_UP} if the digit to the left of the
+     * discarded fraction is odd; behaves as for
+     * {@code ROUND_HALF_DOWN} if it's even.  Note that this is the
+     * rounding mode that minimizes cumulative error when applied
+     * repeatedly over a sequence of calculations.
+     * 向(距离)最近的一边舍入,除非两边(的距离)是相等,如果是这样,如果保留位数是奇数,使用ROUND_HALF_UP ,如果是偶数,使用ROUND_HALF_DOWN 
+     */
+    public final static int ROUND_HALF_EVEN =    6;
+
+    /**
+     * Rounding mode to assert that the requested operation has an exact
+     * result, hence no rounding is necessary.  If this rounding mode is
+     * specified on an operation that yields an inexact result, an
+     * {@code ArithmeticException} is thrown.
+     * 计算结果是精确的,不需要舍入模式 
+     */
+    public final static int ROUND_UNNECESSARY =  7;
+

所以我们在使用BigDecimal除法的时候,最好要指定精确小数位和舍入模式.

+
初始化
+

BigDecimal一共有两种方法可以进行初始化赋值

+
    +
  • 构造器初始化赋值
  • +
  • valueOf方法初始化赋值
  • +
+
valueOf
+

valueOf(double val)底层也是调用String构造

+
    public static BigDecimal valueOf(double val) {
+        // Reminder: a zero double returns '0.0', so we cannot fastpath
+        // to use the constant ZERO.  This might be important enough to
+        // justify a factory approach, a cache, or a few private
+        // constants, later.
+        return new BigDecimal(Double.toString(val));
+    }
+

valueOf(long val)

+
    public static BigDecimal valueOf(long val) {
+        // 判断在不在 缓存常用的小BigDecimal值中
+        if (val >= 0 && val < zeroThroughTen.length)
+            return zeroThroughTen[(int)val];
+        else if (val != INFLATED)
+            return new BigDecimal(null, val, 0, 0);
+        // 会调用BigInteger中 valueOf(long val)涉及到BigDecimal原理
+        return new BigDecimal(INFLATED_BIGINT, val, 0, 0);
+    }
+
double类型构造器
+

重点记录一下double类型构造器初始化赋值

+

在使用BigDecimal中参数为double类型的构造器时,发现存储结果并不准确.

+
    public static void main(String[] args) {
+        // 打印结果: 0.200000000000000011102230246251565404236316680908203125
+        System.out.println(new BigDecimal(0.2));
+    }
+

发现Java API 中有相关的描述

+
+

The results of this constructor can be somewhat unpredictable. +One might assume that writing {@code new BigDecimal(0.1)} in +Java creates a {@code BigDecimal} which is exactly equal to +0.1 (an unscaled value of 1, with a scale of 1), but it is +actually equal to +0.1000000000000000055511151231257827021181583404541015625. +This is because 0.1 cannot be represented exactly as a +{@code double} (or, for that matter, as a binary fraction of +any finite length). Thus, the value that is being passed +in to the constructor is not exactly equal to 0.1, +appearances notwithstanding.

+The {@code String} constructor, on the other hand, is perfectly predictable: writing {@code new BigDecimal(“0.1”)} creates a {@code BigDecimal} which is exactly equal to +0.1, as one would expect. Therefore, it is generally +recommended that the {@linkplain #BigDecimal(String) +String constructor} be used in preference to this one.

+
+

大概意思是说 用double作为参数的构造函数,无法精确构造一个BigDecimal对象,需要自己指定一个上下文的环境,也就是指定精确位。

+

而利用String对象作为参数传入的构造函数能精确的构造出一个BigDecimal对象.

+
    public static void main(String[] args) {
+        // 0.2
+        System.out.println(new BigDecimal("0.2"));
+    }
+

所以要想获得精确的结果,要使用BigDecimal的字符串构造函数,不要使用double参数的构造函数.

+

PS: 另外说一下BigDecimal转换其他类型.BigDecimal类提供了intValue,byteValue,shortValue…将BigDecimal对象转换成对应的值.

+
    public static void main(String[] args) {
+        BigDecimal bigDecimal = new BigDecimal("1.2");
+        System.out.println(bigDecimal.byteValue());
+        System.out.println(bigDecimal.shortValue());
+        System.out.println(bigDecimal.intValue());
+        System.out.println(bigDecimal.floatValue());
+        System.out.println(bigDecimal.doubleValue());
+        System.out.println(bigDecimal.longValue());
+    }
+
比较
+

BigDecimal的equals来进行比较

+
    public static void main(String[] args) {
+        BigDecimal num1 = new BigDecimal("1.10");
+        BigDecimal num2 = new BigDecimal("1.1");
+        BigDecimal num3 = new BigDecimal("1.1");
+        // false
+        System.out.println(num1.equals(num2));
+        // true
+        System.out.println(num2.equals(num3));
+    }
+

我们可以看到,用BigDecimalequals方法进行比较,我们可以看到1.101.1数值是相等的,而equals方法返回的却是false.

+

为了查清原因,看一下BigDecimal中重写的equals的源码

+
    /**
+     * Compares this {@code BigDecimal} with the specified
+     * {@code Object} for equality.  Unlike {@link
+     * #compareTo(BigDecimal) compareTo}, this method considers two
+     * {@code BigDecimal} objects equal only if they are equal in
+     * value and scale (thus 2.0 is not equal to 2.00 when compared by
+     * this method).
+     *
+     * @param  x {@code Object} to which this {@code BigDecimal} is
+     *         to be compared.
+     * @return {@code true} if and only if the specified {@code Object} is a
+     *         {@code BigDecimal} whose value and scale are equal to this
+     *         {@code BigDecimal}'s.
+     * @see    #compareTo(java.math.BigDecimal)
+     * @see    #hashCode
+     */
+    @Override
+    public boolean equals(Object x) {
+        if (!(x instanceof BigDecimal))
+            return false;
+        BigDecimal xDec = (BigDecimal) x;
+        if (x == this)
+            return true;
+        if (scale != xDec.scale)
+            return false;
+        long s = this.intCompact;
+        long xs = xDec.intCompact;
+        if (s != INFLATED) {
+            if (xs == INFLATED)
+                xs = compactValFor(xDec.intVal);
+            return xs == s;
+        } else if (xs != INFLATED)
+            return xs == compactValFor(this.intVal);
+
+        return this.inflated().equals(xDec.inflated());
+    }
+

源码API注释中有写.

+
+

objects equal only if they are equal in value and scale. thus 2.0 is not equal to 2.00 when compared by this method

+
+

大概意思: 对象只有在valuescale相等时才相等.因此,用本方法比较时,2.0不等于2.00.

+

PS: BigDecimal对象中scale字段属性

+
    /**
+     * The scale of this BigDecimal, as returned by {@link #scale}.
+     *
+     * @serial
+     * @see #scale
+     */
+    private final int scale;  // Note: this may have any value, so
+                              // calculations must be done in longs
+

scale指的是小数点后面的位数.

+

BigDecimal可以通过setScale来提高精度,只要新设的值比原来的大.

+
    public static void main(String[] args) {
+        BigDecimal num1 = new BigDecimal("1.12345");
+        num1.setScale(8);
+        System.out.println(num1);
+    }
+    
+

setScale方法返回一个BigDecimal对象,它的scale是指定的值,并且它的值在数字上大于等于这个BigDecimal对象的值。

+
public BigDecimal setScale(int newScale);
+

如果不是,则抛出arithmex exception.

+

例如: 在上面的示例中,如果设置setScale(4)就会报错

+
    public static void main(String[] args) {
+        BigDecimal num1 = new BigDecimal("1.12345");
+        num1.setScale(4);
+        System.out.println(num1);
+    }
+
java.lang.ArithmeticException: Rounding necessary
+

BigDecimal也可以通过setScale来降低精度.因为新设的值比原来的小,所以必须保证原来数值的该位小数点后面都是0,只有这样才可以设比原来小的精度。

+

例如

+
    public static void main(String[] args) {
+        BigDecimal num1 = new BigDecimal("1.1234500000");
+        num1.setScale(5);
+        // 1.1234500000
+        System.out.println(num1);
+    }
+

所以不要用equals方法来比较BigDecimal对象,因为它的equals方法会比较scale,如果scale不一样,它会返回false.

+

比较大小建议使用BigDecimal类中重写的compareTo方法进行比较

+
public static void main(String[] args) {
+        BigDecimal a = new BigDecimal("1.10");
+        BigDecimal b = new BigDecimal("1.1");
+
+        if(a.compareTo(b) == -1){
+            System.out.println("a小于b");
+        }
+
+        if(a.compareTo(b) == 0){
+            System.out.println("a等于b");
+        }
+
+        if(a.compareTo(b) == 1){
+            System.out.println("a大于b");
+        }
+
+        if(a.compareTo(b) > -1){
+            System.out.println("a大于等于b");
+        }
+
+        if(a.compareTo(b) < 1){
+            System.out.println("a小于等于b");
+        }
+    }
+

保证精度原因及注意事项

+

为什么BigDecimal能够保证精度?

+

因为十进制整数在转化成二进制数时不会有精度问题,那么把十进制小数扩大N倍让它在整数的维度上进行计算,并保留相应的精度信息.所以就能保证精度了.

+

注意事项

+

1.在需要精确的小数计算时再使用BigDecimal,BigDecimal的性能比doublefloat差,在处理庞大,复杂的运算时尤为明显.故一般精度的计算没必要使用BigDecimal.

+
    +
  1. +

    尽量使用参数类型为String的构造函数.如果处理double类型数据,可使用BigDecimal.valueOf(double val)

    +
  2. +
  3. +

    BigDecimal都是不可变的的,在进行每一次四则运算时,都会产生一个新的对象,所以在做加减乘除运算时要记得要保存操作后的值.

    +
  4. +
+

Math

+

java.lang.Math,该类和Java中的运算息息相关.Math类被final修饰.构造方法是私有的.Math类中大部分方法都被public static 修饰.

+

常用方法

+
public static void main(String[] args) {
+        // 计算平方根
+        System.out.printf("4的平方根: %s\n",Math.sqrt(4));
+
+        //计算立方根
+        System.out.printf("8的立方根: %s\n",Math.cbrt(8));
+
+        // 计算n的m次方
+        System.out.printf("2的3次方: %s\n",Math.pow(2,3));
+
+        // 计算最大值
+        System.out.printf("1和2中最大值: %s\n",Math.max(1,2));
+
+        // 计算最小值
+        System.out.printf("1和2中最小值: %s\n",Math.min(1,2));
+
+        // 求绝对值
+        System.out.printf("-1的绝对值: %s\n",Math.abs(-1));
+
+        // 向上取整
+        System.out.printf("1.2向上取整: %s\n",Math.ceil(1.2));
+
+        // 向下取整
+        System.out.printf("1.2向下取整: %s\n",Math.floor(1.2));
+
+        // [0,1)区间的随机数
+        System.out.printf("[0,1)区间的随机数: %s\n",Math.random());
+
+        // 返回与参数值最接近的 double值
+        double rint = Math.rint(1.5);
+        System.out.printf("rint方法: %s\n",rint);
+
+        // 四舍五入 float时返回int值,double时返回long值
+        long round = Math.round(1.5);
+        int round1 = Math.round(1.5f);
+        System.out.printf("round方法: 1.5四舍五入: %s\n",round);
+    }
+
4的平方根: 2.0
+8的立方根: 2.0
+2的3次方: 8.0
+1和2中最大值: 2
+1和2中最小值: 1
+-1的绝对值: 1
+1.2向上取整: 2.0
+1.2向下取整: 1.0
+[0,1)区间的随机数: 8.293290468311953E-4
+rint方法: 1.5四舍五入: 2.0
+round方法: 1.5四舍五入: 2
+

其中sqrt方法,cbrt方法,pow方法用native关键字修饰,是用其他语言实现的.这里不做过多分析.

+
max
+
        // 计算最大值
+        System.out.printf("1和2中最大值: %s\n",Math.max(1,2));
+
+

返回两个{@code int}值中较大的一个。也就是说,结果是参数更接近{@link Integer#MAX_VALUE}的值。如果参数具有相同的值,则结果为相同的值。

+
+

源码

+
    public static int max(int a, int b) {
+        return (a >= b) ? a : b;
+    }
+
min
+
        // 计算最小值
+        System.out.printf("1和2中最小值: %s\n",Math.min(1,2));
+
+

返回两个{@code int}值中较小的一个。也就是说,参数的结果更接近{@link Integer#MIN_VALUE}的值。如果参数具有相同的值,则结果为相同的值。

+
+

源码

+
    public static int min(int a, int b) {
+        return (a <= b) ? a : b;
+    }
+
abs
+
        // 求绝对值
+        System.out.printf("-1的绝对值: %s\n",Math.abs(-1));
+
+

返回{@code int}值的绝对值。如果实参不是负数,则返回实参。如果参数为负数,则返回参数的否定值。

+
+

源码

+
    public static int abs(int a) {
+        return (a < 0) ? -a : a;
+    }
+
ceil和floor
+

ceil方法的功能是向上取整。ceil译为“天花板”,顾名思义就是对操作数取顶,Math.ceil(a)就是取大于a的最小整数。需要注意的是它的返回值不是int类型,而是double类型.

+
        // 向上取整
+        System.out.printf("1.2向上取整: %s\n",Math.ceil(1.2));
+

floor方法的功能是向下取整。floor译为“地板”,顾名思义是对操作数取底。Math.floor(a),就会取小于a的最大整数。它的返回值类型与ceil一致,也是double类型。

+
        // 向下取整
+        System.out.printf("1.2向下取整: %s\n",Math.floor(1.2));
+

由于ceil方法和floor方法逻辑是一样的,区别只是传入的参数不同,所以下面主要分析一个方法.

+

ceil方法为例

+
+

返回大于或等于参数的最小(最接近负无穷){@code double}值,并且等于一个数学整数。
+特殊情况:

+
+
    +
  • 如果参数值已经等于一个数学整数,则结果与参数相同。
  • +
  • 如果参数是NaN或无穷大或正零或负零,则结果与参数相同。
  • +
  • 如果实参值小于零但大于-1.0,则结果为负零。 +注意{@code Math.ceil(x)}的值正好是{@code -Math.floor(-x)}的值。
  • +
+

源码

+
    public static double ceil(double a) {
+        // default impl. delegates to StrictMath
+        return StrictMath.ceil(a);
+    }
+    /**
+     * |
+     * |
+     * |
+     * ↓
+     */
+    // StrictMath.ceil
+    public static double ceil(double a) {
+        return floorOrCeil(a, -0.0, 1.0, 1.0);
+    }
+    /**
+     * |
+     * |
+     * |
+     * ↓
+     */
+   /*
+    *内部方法共享floor和ceil之间的逻辑。
+    *
+    * @param a 被floored或ceiled的值
+    * @param negativeBoundary (- 1,0)中的值的结果
+    * @param positiveBoundary (0,1)中的值的结果
+    * @param increment 当参数是非整数时要添加的值
+    */
+    private static double floorOrCeil(double a,
+                                      double negativeBoundary,
+                                      double positiveBoundary,
+                                      double sign) {
+        /**
+         * 返回用于{@code double}表示的无偏指数。
+         *
+         * 特殊情况:
+         * 
+         * 如果参数为NaN或infinite,则结果为{@link Double#MAX_EXPONENT} + 1。
+         * 如果参数为零或低于标准,则结果为{@link Double#MIN_EXPONENT} -1。
+         */
+        // 将浮点数或双精度数转换为浮点表示形式.该方法从表示中返回指数部分
+        int exponent = Math.getExponent(a);
+
+        if (exponent < 0) {
+            /*
+             * 参数的绝对值小于1
+             * floorOrceil(-0.0) => -0.0
+             * floorOrceil(+0.0) => +0.0
+             */
+            return ((a == 0.0) ? a :
+                    ( (a < 0.0) ?  negativeBoundary : positiveBoundary) );
+        } else if (exponent >= 52) {
+            /*
+             * 无穷,NaN,或者一个很大的值.但一定是整数
+             */
+            return a;
+        }
+        // 否则,参数要么是一个已经异或运算的整数值
+        // 必须四舍五入为1
+        assert exponent >= 0 && exponent <= 51;
+
+        long doppel = Double.doubleToRawLongBits(a);
+        long mask   = DoubleConsts.SIGNIF_BIT_MASK >> exponent;
+
+        if ( (mask & doppel) == 0L )
+            return a; // integral value
+        else {
+            double result = Double.longBitsToDouble(doppel & (~mask));
+            if (sign*a > 0.0)
+                result = result + sign;
+            return result;
+        }
+    } 
+    
+
random
+

返回一个大于0的double类型数据,该值大于等于0.0且小于1.0,返回的是一个伪随机选择数,在该范围内(几乎)均匀分布.

+
        // [0,1)区间的随机数
+        System.out.printf("[0,1)区间的随机数: %s\n",Math.random());
+

API中random方法描述

+
+

返回一个大于或等于{@code 0.0}且小于{@code 1.0}{@code double}值,带有正号。返回值是在该范围内(近似)均匀分布的伪随机选择的。
+当这个方法第一次被调用时,它会创建一个新的伪随机数生成器,与表达式{@code new java.util.Random()}完全一样。 +此方法被正确同步以允许多个线程正确使用。但是,如果许多线程需要以非常快的速度生成伪随机数,那么每个线程拥有自己的伪随机数生成器就可以减少争用。

+
+

API中nextDouble方法中描述

+
+

返回该随机数生成器序列中在{@code 0.0}{@code 1.0}之间均匀分布的{@code double}值的下一个伪随机值。 +{@code nextDouble}的一般约定是,从{@code 0.0d}(包括)到{@code 1.0d}(排除)范围内(近似地)统一选择一个{@code double}值,伪随机生成并返回。

+
+

源码

+
    public static double random() {
+        return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
+    }
+    /**
+     * |
+     * |
+     * |
+     * ↓
+     */
+    // Random.nextDouble
+    public double nextDouble() {
+        return (((long)(next(26)) << 27) + next(27)) * DOUBLE_UNIT;
+    }
+

通过源码可以看到,Math.random底层是调用了Random类中nextDouble方法.

+

也就是说以下打印语句执行代码相同

+
    // [0,1)区间的随机数
+    System.out.printf("[0,1)区间的随机数: %s\n",Math.random());
+    System.out.printf("[0,1)区间的随机数: %s\n",new Random().nextDouble());
+

Random

+
+

该类的一个实例用于生成伪随机数流。该类使用48位种子,并使用线性同余公式进行修改。(参见Donald Knuth, 计算机编程的艺术,第二卷,第3.2.1节。)

+
+
+

如果用相同的种子创建了{@code Random}的两个实例,并且对每个实例进行了相同的方法调用序列,那么它们将生成并返回相同的数字序列。为了保证这个属性,为类{@code Random}指定了特定的算法。为了保证Java代码的绝对可移植性,Java实现必须对{@code Random}类使用这里显示的所有算法。然而,{@code Random}类的子类约定了所有的方法。

+
+

Random类位于java.util包下,此类的实例用于生成伪随机数流。之所以称之为伪随机,是因为真正意义上的随机数(或者称为随机事件)在某次产生过程中是按照实验过程表现的分布概率随机产生的,其结果是不可预测,不可见的。而计算机中的随机函数是按照一定的算法模拟产生的,其结果是确定的,可见的。我们认为这样产生的数据不是真正意义上的随机数,因而称之为伪随机。

+

该类提供了两种构造方法.

+

无参构造底层调用的也是有参构造.将System.nanoTime()作为参数传递。即如果使用无参构造,默认的seed值为System.nanoTime()

+
    public Random() {
+        this(seedUniquifier() ^ System.nanoTime());
+    }
+
    public Random(long seed) {
+        if (getClass() == Random.class)
+            this.seed = new AtomicLong(initialScramble(seed));
+        else {
+            // subclass might have overriden setSeed
+            this.seed = new AtomicLong();
+            setSeed(seed);
+        }
+    }
+

要注意的是用有参构造创建Random对象,如果随机种子相同,不管执行多少次,最后结果都是相同的.

+

例如

+
    public static void main(String[] args) {
+        Random random = new Random(1);
+        // 第一次执行程序打印结果: 随机数: -1155869325
+        // 第二次执行程序打印结果: 随机数: -1155869325
+        System.out.printf("随机数: %s\n",random.nextInt());
+    }
+

随机数应用举例

+

生成100个不重复的随机数,1~100的范围

+
    public static void main(String[] args) {
+        int[] nums=new int[100];
+        boolean[] flag=new boolean[101];
+        Random random=new Random();
+        for (int i = 0; i < nums.length; i++) {
+            int num=random.nextInt(100)+1;
+            while (flag[num]) {
+                num=random.nextInt(100)+1;
+            }
+            flag[num]=true;
+            nums[i]=num;
+        }
+        Arrays.sort(nums);
+        System.out.println(Arrays.toString(nums));
+        System.out.println(Arrays.toString(flag));
+    }
+
rint
+
+

返回与参数值最接近的{@code double}值,该值等于一个数学整数。如果两个数学整数{@code double}值相同,则结果为偶数整数值.


+特殊情况:
+如果参数值已经等于一个数学整数,则结果与参数相同。
+如果参数是NaN或无穷大或正零或负零,则结果与参数相同

+
+
        // 返回与参数值最接近的 double值
+        double rint = Math.rint(1.5);
+        System.out.printf("rint方法: %s\n",rint);
+

若存在两个这样的数,则返回其中的偶数值

+

例如

+
    public static void main(String[] args) {
+        double rint = Math.rint(100.5);
+        double rint2 = Math.rint(101.5);  
+        // 100.0
+        System.out.printf(" %s\n",rint);
+        // 102.0
+        System.out.printf(" %s\n",rint2); 
+    }
+
round
+
+

round 表示"四舍五入",算法为Math.floor(x+0.5) ,即将原来的数字加上 0.5 后再向下取整,所以 Math.round(11.5) 的结果为 12,Math.round(-11.5) 的结果为 -11。

+
+
        // 四舍五入 float时返回int值,double时返回long值
+        long round = Math.round(1.5);
+        int round1 = Math.round(1.5f);
+        System.out.printf("round方法: 1.5四舍五入: %s\n",round);
+
+

返回与实参最接近的{@code int},并四舍五入到正无穷。 +特殊情况:

+
+
    +
  • 如果参数是NaN,结果是0。 +如果参数是负无穷或任何值小于或等于{@code Integer.MIN_VALUE},结果等于{@code Integer.MIN_VALUE}的值。
  • +
  • 如果参数是正无穷或任何大于或等于{@code Integer.MAX_VALUE},结果等于{@code Integer.MAX_VALUE}的值.
  • +
+
+

PS: NAN +NaN表示非数值,例如:0.0/0结果为NAN,负数的平方根结果也为NAN.

+
+
   public static void main(String[] args) {
+        // 四舍五入 float时返回int值,double时返回long值
+        int round1 = Math.round(0);
+        long round2 = Math.round(Double.NaN);
+        int round3 = Math.round(2147483648123L);
+        int round4 = Math.round(-2147483647123L);
+        System.out.printf("0四舍五入: %s\n", round1);
+        System.out.printf("Double.NaN=[%s],四舍五入: %s\n", Double.NaN, round2);
+        System.out.printf("Integer.MAX_VALUE=[%s] + 1 四舍五入: %s\n", Integer.MAX_VALUE, round3);
+        System.out.printf("Integer.MIN_VALUE=[%s] - 1 四舍五入: %s\n", Integer.MIN_VALUE, round4);
+    }
+
0四舍五入: 0
+Double.NaN=[NaN],四舍五入: 0
+Integer.MAX_VALUE=[2147483647] + 1 四舍五入: 2147483647
+Integer.MIN_VALUE=[-2147483648] - 1 四舍五入: -2147483648
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/java/rookie-reflect/index.html b/blog-site/public/posts/java/rookie-reflect/index.html new file mode 100644 index 00000000..bca7b2e1 --- /dev/null +++ b/blog-site/public/posts/java/rookie-reflect/index.html @@ -0,0 +1,1137 @@ + + + + + + + + + + + Java反射 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Java反射

+ 2021.10.02 +
+

概述

+

什么是反射

+

在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。

+
+

反射是Java语言的一个特性,它允许程序在运行时来进行自我检查并且对内部的成员进行操作.

+
+

通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。程序中一般的对象的类型都是在编译期就确定下来的,而 Java 反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的.

+

反射的核心是JVM在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。

+

反射的作用

+
    +
  • 在运行时判断任意一个对象所属的类;
  • +
  • 在运行时判断任意一个类所具有的成员变量和方法;
  • +
  • 在运行时任意调用一个对象的方法;
  • +
  • 在运行时构造任意一个类的对象;
  • +
+

反射与注解

+

注解我们经常会遇到,如:@Override, @Deprecated ...

+

是否思考过注解是怎样工作的呢? 自定义一个注解体会一下注解是怎样工作的.

+

创建注解: MyAnnotation

+
import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface MyAnnotation {
+    String value() default "";
+}
+

使用注解: MyAnnotationTest

+
public class MyAnnotationTest {
+
+    @MyAnnotation("123")
+    public void test(String str){
+        System.out.println("invoke test ...param: "+ str);
+    }
+
+    public void t2(){
+        System.out.println("I am t2 ...");
+    }
+
+}
+

实现注解@MyAnnotation

+
import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+public class AnnotationInvoke {
+
+
+    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
+        // 获取使用@MyAnnotation注解的类,这里举例子 就直接写了,如果想要实现的话可以参照spring扫描包
+        Class<MyAnnotationTest> clazz = MyAnnotationTest.class;
+        Method[] methods = clazz.getDeclaredMethods();
+        for (Method method : methods) {
+            //判断该类是否使用了 @MyAnnotation注解
+            MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
+            if (annotation != null) {
+                // 可以进行一系列操作 ...
+                // 获取该方法上 @MyAnnotation 注解的值
+                System.out.println(annotation.value());
+                // 执行test方法
+                if (method.getName().equals("test")) {
+                    method.invoke(clazz.newInstance(), "hello ...");
+                }
+            }
+        }
+    }
+}
+

执行main方法,结果

+
123
+invoke test ...param: hello ...
+

反射与枚举

+

经典案例: 用枚举实现单例设计模式,防止反射破坏单例

+
public enum EnumSingleton {
+
+    INSTANCE;
+
+    public EnumSingleton getInstance(){
+        return INSTANCE;
+    }
+
+    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
+        EnumSingleton singleton1=EnumSingleton.INSTANCE;
+        EnumSingleton singleton2=EnumSingleton.INSTANCE;
+        System.out.println("正常情况下,实例化两个实例是否相同:"+(singleton1==singleton2));
+        Constructor<EnumSingleton> constructor= null;
+        constructor = EnumSingleton.class.getDeclaredConstructor();
+        constructor.setAccessible(true);
+        EnumSingleton singleton3= null;
+        singleton3 = constructor.newInstance();
+        System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);
+        System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(singleton1==singleton3));
+    }
+
+
+}
+

结果

+
正常情况下,实例化两个实例是否相同:true
+Exception in thread "main" java.lang.NoSuchMethodException
+

原因 +Constructor类中 newInstance方法 不能通过反射来创建对象

+
    @CallerSensitive
+    public T newInstance(Object ... initargs)
+        throws InstantiationException, IllegalAccessException,
+               IllegalArgumentException, InvocationTargetException
+    {
+        if (!override) {
+            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
+                Class<?> caller = Reflection.getCallerClass();
+                checkAccess(caller, clazz, null, modifiers);
+            }
+        }
+        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
+            throw new IllegalArgumentException("Cannot reflectively create enum objects");
+        ConstructorAccessor ca = constructorAccessor;   // read volatile
+        if (ca == null) {
+            ca = acquireConstructorAccessor();
+        }
+        @SuppressWarnings("unchecked")
+        T inst = (T) ca.newInstance(initargs);
+        return inst;
+    }
+

枚举类无法通过反射来创建对象,原因是newInstance方法加了判断如果是枚举类就抛出异常throw new IllegalArgumentException("Cannot reflectively create enum objects");

+

除了不能创建枚举类的对象外,反射还是能够调用枚举类的方法的

+
public enum EnumSingleton {
+
+    public EnumSingleton getInstance(){
+        return INSTANCE;
+    }
+
+    public void getTst(){
+        System.out.println("enum method ...");
+    }
+    
+    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException, ClassNotFoundException {
+        Class<?> aClass = Class.forName("com.test.EnumSingleton");
+        Method getInstance = aClass.getMethod("getTst");
+        // 枚举对应的class没有newInstance方法,会报NoSuchMethodException,应该使用getEnumConstants方法
+        Object[] oo = aClass.getEnumConstants();
+        getInstance.invoke(oo[0]);
+        Method getTst = aClass.getMethod("getTst");
+        getTst.invoke(oo[0]);
+    }
+}
+

结果

+
enum getInstance ... 
+enum method ...
+

反射与泛型

+

反射会擦除泛型,因为泛型只在编译期间生效.而反射是在Java程序运行期间生效。

+
public static void main(String[] args) throws Exception {
+
+        ArrayList<Integer> list = new ArrayList<Integer>();
+
+        list.add(1);  //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer
+
+        list.getClass().getMethod("add", Object.class).invoke(list, "string");
+
+        for (int i = 0; i < list.size(); i++) 
+            System.out.println(list.get(i));// 1 string
+        //list.forEach(System.out::print);  //语法详情 见Java8 Lambda表达式
+    }
+

反射与框架

+

反射最重要的用途就是开发各种通用框架。

+

以 spring 的 IOC 框架为例:

+ +

操作反射

+

在Java中,只要给定类的名字,那么就可以通过反射机制来获得类的所有信息.

+

使用反射会有异常出现.注意处理异常

+

Class类 和 java.lang.reflect一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:

+
    +
  • Field:可以使用get()set()方法读取和修改Field对象关联的字段;
  • +
  • Method:可以使用 invoke() 方法调用与 Method 对象关联的方法;
  • +
  • Constructor:可以用 Constructor newInstance() 创建新的对象;
  • +
+

获取

+

在程序运行时,反射可以获取Java类中所有的属性,下边举几个经常用的栗子

+

获取反射入口

+

在操作反射前我们要先了解一些Class类

+
+

Java的Class类是java反射机制的基础,通过Class类我们可以获得关于一个类的相关信息.
+虚拟机为每种类型管理一个独一无二的Class对象。也就是说,每个类(型)都有一个Class对象。运行程序时,Java虚拟机(JVM)首先检查是否所要加载的类对应的Class对象是否已经加载。如果没有加载,JVM就会根据类名查找.class文件,并将其Class对象载入

+
+
private  Class(ClassLoader loader) { 
+    classLoader = loader; 
+}
+

class类的构造器时私有的,只有JVM可以创建Class的对象,因此不可以像普通类一样new一个Class对象, 但是却可以通过已有的类得到一个Class对象,共有三种方式

+

在运行时获取 class 的对象.

+
    +
  1. Class.forName("包名+类名"); +例如 连接Oracle数据库加载JDBC驱动
  2. +
+
// 注意此种方式请写全类名(包名+类名)
+String driver = "oracle.jdbc.driver.OracleDriver";
+Class.forName(driver);
+
    +
  1. 类名.class
  2. +
+
Class<?> clazz = int.class;
+Class<?> classInt = Integer.TYPE;
+
    +
  1. 调用某个对象的getClass()方法. 实例.getClass()
  2. +
+
StringBuilder str = new StringBuilder("123");
+Class<?> clazz = str.getClass();
+

反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。

+

获取方法

+

创建MainTest类,A类,创建一个B类和C接口.让A类去继承B类.A类实现C接口,MainTest为主类,A类,B类为内部类

+
public class MainTest {
+
+   class B{
+        private void privateMethodB(){
+            System.out.println("private method B ...");
+        }
+
+        void defaultMethodB(){
+            System.out.println("default method B... ");
+        }
+
+        protected void protectedMethodB(){
+            System.out.println("protected method B...");
+        }
+
+        public void publicMethodB(){
+            System.out.println("public method B...");
+        }
+    }
+    
+    interface C{}
+
+    class A extends B implements C{
+
+        private void privateMethod(){
+            System.out.println("private method ...");
+        }
+
+        void defaultMethod(){
+            System.out.println("default method ... ");
+        }
+
+        protected void protectedMethod(){
+            System.out.println("protected method ...");
+        }
+
+        public void publicMethod(){
+            System.out.println("public method ...");
+        }
+
+    }
+}
+
    +
  • getDeclaredMethods 方法返回类或接口声明的所有方法,包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法。 +执行下边代码
  • +
+
    @Test
+    public void contextLoad() throws ClassNotFoundException {
+        Class<?> clazz = Class.forName("com.test.MainTest$A");
+        Method[] methods = clazz.getDeclaredMethods();
+        for (Method method : methods) {
+            System.out.println(method);
+        }
+    }
+

打印结果

+
public void com.test.MainTest$A.publicMethod()
+private void com.test.MainTest$A.privateMethod()
+void com.test.MainTest$A.defaultMethod()
+protected void com.test.MainTest$A.protectedMethod()
+
    +
  • getMethods方法返回某个类的所有公用public方法,包括其继承类的公用方法.
  • +
+
    @Test
+    public void contextLoad() throws ClassNotFoundException {
+        Class<?> clazz = Class.forName("com.test.MainTest$A");
+        Method[] methods = clazz.getMethods();
+        for (Method method : methods) {
+            System.out.println(method);
+        }
+    }
+

打印结果

+
public void com.test.MainTest$A.publicMethod()
+public void com.test.MainTest$B.publicMethodB()
+public final void java.lang.Object.wait() throws java.lang.InterruptedException
+public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
+public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
+public boolean java.lang.Object.equals(java.lang.Object)
+public java.lang.String java.lang.Object.toString()
+public native int java.lang.Object.hashCode()
+public final native java.lang.Class java.lang.Object.getClass()
+public final native void java.lang.Object.notify()
+public final native void java.lang.Object.notifyAll()
+// 接口也被打印了
+public default void com.test.MainTest$C.interfaceC()
+
    +
  • getMethod方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象.只能获取到public修饰的方法.能获取到接口和父类的public修饰的方法
  • +
+
    @Test
+    public void contextLoad() throws ClassNotFoundException, NoSuchMethodException {
+        Class<?> clazz = Class.forName("com.test.MainTest$A");
+        System.out.println(clazz.getMethod("publicMethod"));
+//        System.out.println(clazz.getMethod("defaultMethod"));
+//        System.out.println(clazz.getMethod("protectedMethod"));
+//        System.out.println(clazz.getMethod("privateMethod"));
+        System.out.println(clazz.getMethod("publicMethodB"));
+        System.out.println(clazz.getMethod("interfaceC"));
+    }
+

打印结果

+
public void com.test.MainTest$A.publicMethod()
+public void com.test.MainTest$B.publicMethodB()
+public default void com.test.MainTest$C.interfaceC()
+

获取类成员信息

+
    +
  • getFiled:访问公有的成员变量
  • +
  • getDeclaredField:所有已声明的成员变量,但不能得到其父类的成员变量
  • +
  • getFiledsgetDeclaredFields 方法用法同上(参照 获取方法).
  • +
+

创建

+
    +
  • 创建数组
  • +
+
public static void testArray() throws ClassNotFoundException {
+        Class<?> cls = Class.forName("java.lang.String");
+        Object array = Array.newInstance(cls,25);
+        //往数组里添加内容
+        Array.set(array,0,"hello");
+        Array.set(array,1,"Java");
+        Array.set(array,2,"fuck");
+        Array.set(array,3,"Scala");
+        Array.set(array,4,"Clojure");
+        //获取某一项的内容
+        System.out.println(Array.get(array,3));
+    }
+
    +
  • 创建对象 +通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例,创建之前要确保该类存在构造无参构造器
  • +
+
    @Test
+    public void contextLoad() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException {
+        Class<?> clazz = Class.forName("com.test.MainTest");
+        System.out.println(clazz.newInstance());
+    }
+

打印结果

+
com.test.MainTest@65e579dc
+

调用方法

+

MainTest 加入该方法

+
    public void T(){
+        System.out.println("t method invoke ... ");
+    }
+

执行

+
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
+        Class<?> outerClass = Class.forName("com.MainTest");
+        outerClass.getMethod("T").invoke(outerClass.newInstance());
+    }
+

结果

+
t method invoke ... 
+

内部类调用方法方式

+
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
+        Class<?> outerClass = Class.forName("com.MainTest");
+        Class<?> innerClass = Class.forName("com.MainTest$A");
+        Method method = innerClass.getDeclaredMethod("publicMethod");
+        Object o = innerClass.getDeclaredConstructors()[0].newInstance(outerClass.newInstance());
+        method.invoke(o);
+    }
+

执行结果

+
public method ...
+

当内部类私有化(private class InnerClass)时,也可以调用,这里就不列举了

+

反射的优点

+

通过反射机制我们可以获得类的各种内容,进行反编译。对于JAVA这种先编译再运行的语言来说,反射机制可以使代码更加灵活,更加容易实现面向对象.

+
    +
  • 可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。降低模块的耦合性;
  • +
  • 可视化开发环境:可视化开发环境(如IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。
  • +
  • 调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的API定义,以确保一组测试中有较高的代码覆盖率。
  • +
+

反射的缺点

+

反射功能虽然强大,但不应任意使用.如果一个功能可以不用反射完成,那么最好就不用。通过反射访问代码时,应牢记以下注意事项

+
    +
  • 性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。
  • +
  • 安全限制 :使用反射通常需要程序的运行没有安全方面的限制。如果一个程序对安全性提出要求,则最好不要使用反射。
  • +
  • 内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(例如访问private字段和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此可能会随着平台的升级而改变行为.
  • +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/jvm/java-garbage-collection/index.html b/blog-site/public/posts/jvm/java-garbage-collection/index.html new file mode 100644 index 00000000..1c24689d --- /dev/null +++ b/blog-site/public/posts/jvm/java-garbage-collection/index.html @@ -0,0 +1,1070 @@ + + + + + + + + + + + JVM-垃圾回收 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JVM-垃圾回收

+ 2021.04.21 +
+

垃圾回收

+

垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。

+

垃圾收集机制是Java的招牌能力,极大地提高了开发效率。 +如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展,Java的垃圾收集机制仍然在不断的演进中,不同大小的设备、不同特征的应用场景,对垃圾收集提出了新的挑战,这当然也是面试的热点。

+

什么是垃圾

+
+

垃圾是指在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。

+
+

如果不及时对内存中的垃圾进行清理,那么,这些垃圾对象所占的内存空间会一直保留到应用程序的结束,被保留的空间无法被其它对象使用,甚至可能导致内存溢出

+

为什么需要垃圾回收

+

对于高级语言来说,一个基本认知是如果不进行垃圾回收,内存迟早都会被消耗完,因为不断地分配内存空间而不进行回收,就好像不停地生产生活垃圾而从来不打扫一样。

+

除了释放没用的对象,垃圾回收也可以清除内存里的记录碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象。

+

随着应用程序所应付的业务越来越庞大、复杂,用户越来越多,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以才会不断地尝试对GC进行优化。

+

早期垃圾回收

+

在早期的C/C++时代,垃圾回收基本上是手工进行的。 +开发人员可以使用 new 关键字进行内存申请,并使用 delete 关键字进行内存释放。比如以下代码:

+
MibBridge *pBridge= new cmBaseGroupBridge();
+//如果注册失败,使用Delete释放该对象所占内存区域
+if(pBridge->Register (kDestroy) != NO ERROR)
+	delete pBridge;
+

这种方式可以灵活控制内存释放的时间,但是会给开发人员带来频繁申请和释放内存的管理负担。

+

倘若有一处内存区间由于程序员编码的问题忘记被回收,那么就会产生内存泄漏; +垃圾对象永远无法被清除,随着系统运行时间的不断增长,垃圾对象所耗内存可能持续上升,直到出现内存溢出并造成应用程序崩溃。

+

有了垃圾回收机制后,上述代码极有可能变成这样

+
MibBridge *pBridge=new cmBaseGroupBridge(); 
+pBridge->Register(kDestroy);
+

现在,除了Java以外,C#、Python、Ruby等语言都使用了自动垃圾回收的思想,也是未来发展趋势,可以说这种自动化的内存分配和来及回收方式已经成为了线代开发语言必备的标准。

+

Java垃圾回收机制

+

自动内存管理,无需开发人员手动参与内存的分配与回收,这样降低内存泄漏内存溢出的风险; +没有垃圾回收器,java也会和c++一样,各种悬垂指针,野指针,泄露问题让你头疼不已。

+

自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发.

+

对于Java开发人员而言,自动内存管理就像是一个黑匣子,如果过度依赖于“自动”,那么这将会是一场灾难,最严重的就会弱化Java开发人员在程序出现内存溢出时定位问题和解决问题的能力。

+

此时,了解JVM的自动内存分配和内存回收原理就显得非常重要,只有在真正了解JVM是如何管理内存后,我们才能够在遇见outOfMemoryError时,快速地根据错误异常日志定位问题和解决问题。

+

当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就必须对这些“自动化”的技术实施必要的监控和调节。

+

垃圾回收主要关注的区域

+

垃圾收集器主要对 方法区 、堆中的垃圾收集 +GC作用区域

+

垃圾收集器可以对年轻代回收,也可以对老年代回收,甚至是全栈和方法区的回收; +其中,Java堆是垃圾收集器的工作重点。

+

从次数上讲:频繁收集新生代 > 较少收集老年代 » 基本不收集方法区

+

垃圾回收相关算法思想

+

标记阶段

+

在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。 +只有被标记为己经死亡的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称为垃圾标记阶段

+

在运行程序中,当一个对象已经不再被任何存活的对象引用时,就可以就可以判定该对象已经死亡了; +判定对象是否存活在有两种算法,应用技术算法、可达性分析算法。

+

引用计数算法

+

对每个对象保存一个整型的引用计数器属性。用于记录对象被引用的情况。 +在堆中判定新生代中的幸存者区是否可以进老年代,会有一个年龄计数器,这里用的就是引用计数算法。

+
    +
  • 优点:实现简单,垃圾对象便于辨识;判定效率高,回收垃圾没有延迟性。
  • +
+

缺点:

+
    +
  • 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
  • +
  • 每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
  • +
  • 无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
  • +
+

循环引用

+

当p的指针断开的时候,内部的引用形成一个循环,从而造成内存泄漏。

+

循环引用

+

Java并没有选择引用计数算法,是因为其存在一个基本的难题,也就是很难处理循环引用关系。 +虽然引用计数算法存在循环引用的问题,但是很多语言的资源回收选择,例如:因人工智能而更加火热的Python,它更是同时支持引用计数和垃圾收集机制; +具体哪种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试。

+

Python如何解决循环引用?

+
    +
  • 手动解除:很好理解,就是在合适的时机,将引用计数器中的计数属性置为零,解除引用关系。
  • +
  • 使用弱引用weakrefweakref是Python提供的标准库,旨在解决循环引用。
  • +
+

可达性分析算法

+

可达性分析算法是以根对象集合(GCRoots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。 +所谓根集合"GCRoots”就是一组必须活跃的引用,即有在栈中有指针指向堆中的地址; +可达性分析算法:也可以称为 根搜索算法、追踪性垃圾收集。

+

相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。

+

在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象; +使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链; +如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。 +可达性分析算法

+

对象集合(GC Roots)可以是哪些?

+
    +
  1. 虚拟机栈中引用的对象;例如:各个线程被调用的方法中使用到的参数、局部变量等
  2. +
  3. 本地方法栈内,本地方法引用对象方法区中类静态属性引用的对象;例如:Java类的引用类型静态变量
  4. +
  5. 方法区中常量引用的对象;例如:字符串常量池里的引用
  6. +
  7. 所有被同步锁synchronized持有的对象;例如:
  8. +
  9. Java虚拟机内部的引用;例如:一些常驻的异常对象、系统类加载器等
  10. +
  11. 反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
  12. +
  13. 除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完整GC Roots集合;比如:分代收集和局部回收
  14. +
+

除了堆空间产生对象的一些结构外,比如:虚拟机栈、本地方法栈、方法区、字符串常量池等地方对堆空间的对象的引用,都可以作为GC Roots进行可达性分析。 +对象集合GCroots

+
+

如何判定是否为GC root? +由于Root采用栈方式存放变量和指针,所以如果一个指针,保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root

+
+

使用注意

+

如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。 +简单来说就是执行这个算法的时候,要停止程序标记对象,不能一边改变对象的引用一边判定对象是不是垃圾。

+

这点也是导致GC进行时必须stop The World的一个重要原因。即使是号称几乎不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。

+

清除阶段

+

当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。 +目前在JVM中比较常见的三种垃圾收集算法是:

+
    +
  • 标记清除算法(Mark-Sweep)
  • +
  • 复制算法(Copying)
  • +
  • 标记压缩算法(Mark-Compact)
  • +
+

标记清除算法

+

标记清除算法是一种非常基础和常见的垃圾收集算法,该算法被J.McCarthy等人在1960年提出并并应用于Lisp语言。

+

执行过程

+

当堆中的有效内存空间被耗尽的时候,就会停止整个程序(stop the world),然后进行两项工作,第一项则是标记,第二项则是清除;

+
    +
  • 标记:垃圾收集器从引用根节点(GCRoots)开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
  • +
  • 清除:垃圾收集器对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
  • +
+

标记的是可达对象,不是垃圾对象;清除回收的是垃圾对象。

+
+

什么是清除? +上面所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空闲的地址列表里。 +下次有新对象需要加载时,判断垃圾的位置空间是否够,如果够,就存放覆盖原有的地址。

+
+

缺点

+
    +
  • 标记清除算法的效率不算高;
  • +
  • 在进行GC的时候,需要停止整个应用程序,用户体验较差;
  • +
  • 这种方式清理出来的空闲内存是不连续的,会产生内碎片,需要维护一个空闲列表;存放大对象可能会存不下;
  • +
+

复制算法

+

为了解决标记-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年发表了著名的论文:“使用双存储区的Lisp语言垃圾收集器(CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)”。 +M.L.Minsky在该论文中描述的算法被人们称为复制算法,它也被M.L.Minsky本人成功地引入到了Lisp语言的一个实现版本中。

+

执行过程

+

将分配内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中存活对象复制到未被使用的内存块中去, +之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

+

缺点

+
    +
  • 因为用复制的形式,需要两倍的内存空间,或者说只能用分配内存的一半。
  • +
  • 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小; +简单来说,从From区被复制到To区的可达对象,需要改变之前对象的指针引用,需要内存和时间的开销。
  • +
+

优点 +解决了标记清除算法带来的问题;

+
    +
  • 没有标记和清除过程,实现简单,运行高效
  • +
  • 复制过去以后保证空间的连续性,不会出现“碎片”问题。
  • +
+

复制算法最坏情况,如果系统中的垃圾对象很少,复制算法需要复制的存活对象数量就会很多,那么大部分对象从一个区域移动到另一个区域,GCRoots需要改变了对象的地址,加大了维护成本; +所以复制算法适合,系统中的垃圾对象很多,可复制的存活的对象很少的情况。利用这个特点,在新生代中的幸存者区里面就用到了复制算法的思想。

+

标记整理算法

+

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。 +如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。标记整理算法应运而生。

+

标记一清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JvM的设计者需要在此基础之上进行改进。 +1970年前后,G.L.Steele、C.J.CheneD.s.Wise等研究者发布标记-压缩算法。在许多现代的垃圾收集器中,人们都使用了标记-压缩算法或其改进版本。

+

执行过程

+
    +
  • 标记:垃圾收集器从引用根节点(GCRoots)开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
  • +
  • 压缩:将所有的存活对象压缩到内存的一端,按顺序排放,之后执行清除的步骤。
  • +
  • 清除:垃圾收集器对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。
  • +
+

与标记清理算法相比,多了一个步骤"压缩(整理)",也就是移动对象的步骤; +是否移动回收后的存活对象是一项优缺点并存的风险决策。标记的存活对象将会被整理,按照内存地址依次排列,而未被标记的内存会被清理掉。 +如此一来,当我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可,这比维护一个空闲列表显然少了许多开销。

+

优点

+
    +
  • 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
  • +
  • 消除了复制算法当中,内存减半的高额代价。
  • +
+

缺点

+
    +
  • 从效率上来说,标记-整理算法要低于复制算法。
  • +
  • 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址
  • +
  • 移动过程中,需要全程暂停用户应用程序。即:STW
  • +
+

小结

+

标记-整理算法相对来说更平滑一些,但是效率上不尽如人意,它比复制算法多了一个标记的阶段,比标记-清除多了一个整理内存的阶段。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
标记清除算法标记整理算法复制算法
时间开销中等最慢最快
空间开销少(会堆积碎片)少(不堆积碎片)通常需要存活对象的两倍空间(不堆积碎片)
移动对象
+

这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。分代收集算法思想应运而生。

+

分代收集思想

+

分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。 +一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。

+

在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。 +但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:string对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

+

分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代。 +在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。

+
    +
  • +

    年轻代:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。 +适合复制算法;复制算法内存利用率不高的问题,通过Hotspot中的两个幸存者区的设计得到缓解。

    +
  • +
  • +

    老年代:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。 +不适合复制算法,一般是由标记-清除或者是标记-清除与标记-整理的混合实现。

    +
  • +
+

增量收集算法

+

在垃圾回收过程中,应用软件将处于一种stop the World的状态。 +在STW状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。 +如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。 +为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集算法的诞生。

+

如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。 +每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

+

总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。 +增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作

+

使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。 +但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

+

分区算法

+

一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生的停顿也越长。 +为了更好地控制GC产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。

+

分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。 +每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/jvm/java-garbage-collector/index.html b/blog-site/public/posts/jvm/java-garbage-collector/index.html new file mode 100644 index 00000000..0f32012a --- /dev/null +++ b/blog-site/public/posts/jvm/java-garbage-collector/index.html @@ -0,0 +1,1044 @@ + + + + + + + + + + + JVM-垃圾回收器 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JVM-垃圾回收器

+ 2021.05.06 +
+

垃圾回收器分类

+

垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。

+

由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。

+
+

Java不同版本新特性学习思路: +语法层面:Lambda表达式、switch、自动拆箱装箱、enum +API层面:Stream API、新的日期时间、Optional、String、集合框架 +底层优化:JVM优化、GC的变化、元空间、静态域、字符串常量池位置变化

+
+

从不同角度分析垃圾收集器,可以将GC分为不同的类型。

+

按线程数分类

+
    +
  • 串行垃圾回收器
  • +
  • 并行垃圾回收器
  • +
+

串行垃圾回收器

+

在单核cpu的硬件情况下,该收集器会在工作时冻结所有应用程序线程,这使它在所有目的和用途上都无法在服务器环境中使用。

+

串行垃圾回收器

+

并行垃圾回收器

+

在停止用户线程之后,多条GC线程并行进行垃圾回收。和串行回收相反,并行收集可以运用多个CPU同时执行垃圾回收,因此提升了应用的吞吐量。

+

并行垃圾回收器

+

按工作模式分类

+
    +
  • 并发式垃圾回收器
  • +
  • 独占式垃圾回收器
  • +
+

并发式垃圾回收器

+

不会出现STW现象,指多条垃圾收集线程同时进行工作,GC线程和用户线程同时运行,不会出现STW现象。 +并发垃圾回收器

+

独占式垃圾回收器

+

会出现STW现象,一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。

+

按处理方式分类

+
    +
  • 压缩式垃圾回收器:压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片。所以在为对象分配内存的时候用指针碰撞
  • +
  • 非压缩式垃圾回收器:非压缩式的垃圾回收器不进行整理这步操作。所以在为为对象分配内存的时候使用空闲列表
  • +
+

评估垃圾回收器性能指标

+
    +
  • 吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)。
  • +
  • 垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。 +- 暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
  • +
  • 收集频率:相对于应用程序的执行,收集操作发生的频率。 +- 内存占用:Java堆区所占的内存大小。
  • +
  • 快速:一个对象从诞生到被回收所经历的时间。
  • +
+

吞吐量、暂停时间、内存占用 这三者共同构成一个“不可能三角”。 +这三项里,暂停时间的重要性日益凸显。因为随着硬件发展,内存占用多些越来越能容忍,硬件性能的提升也有助于降低收集器运行时对应用程序的影响,即提高了吞吐量。 +而内存的扩大,对延迟反而带来负面效果。

+

一款优秀的收集器通常最多同时满足其中的两项。 简单来说,主要抓住两点:

+
    +
  • 吞吐量
  • +
  • 暂停时间
  • +
+

吞吐量

+

吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值。吞吐量=运行用户代码时间 /(运行用户代码时间+垃圾收集时间)

+

在注重吞吐量的这种情况下,应用程序能容忍较高的暂停时间,因此,高吞吐量的应用程序有更长的时间基准,快速响应是不必考虑的。 +吞吐量优先,意味着在单位时间内,STW的时间最短。

+

暂停时间

+

暂停时间是指一个时间段内应用程序线程暂停,让GC线程执行的状态。

+

例如:GC期间100毫秒的暂停时间意味着在这100毫秒期间内没有应用程序线程是活动的。暂停时间优先,意味着尽可能让单次STW的时间最短。

+

吞吐量对比暂停时间

+

高吞吐量较好因为这会让应用程序的最终用户感觉只有应用程序线程在做“生产性”工作。直觉上,吞吐量越高程序运行越快。

+

低暂停时间较好因为从最终用户的角度来看不管是GC还是其他原因导致一个应用被挂起始终是不好的。 +这取决于应用程序的类型,有时候甚至短暂的200毫秒暂停都可能打断终端用户体验。 +因此,具有低的较大暂停时间是非常重要的,特别是对于一个交互式应用程序。

+

不幸的是”高吞吐量”和”低暂停时间”是相互矛盾的。

+

因为如果选择以吞吐量优先,那么必然需要降低内存回收的执行频率,但是这样会导致GC需要更长的暂停时间来执行内存回收。 +相反的,如果选择以低延迟优先为原则,那么为了降低每次执行内存回收时的暂停时间,也只能频繁地执行内存回收,但这又引起了年轻代内存的缩减和导致程序吞吐量的下降。

+

所以,在设计或使用GC算法时,我们必须确定我们的目标:一个GC算法只可能针对两个目标之一即只专注于较大吞吐量或最小暂停时间,或尝试找到一个二者的折中。

+

垃圾回收器概述

+

垃圾收集机制是Java的招牌能力,极大地提高了开发效率。

+

垃圾回收器发展史

+

垃圾收集器是和JVM一脉相承的,它是和JVM进行搭配使用,在不同的使用场景对应的收集器也是有区别。 +有了虚拟机,就一定需要收集垃圾的机制,这就是Garbage Collection,对应的产品我们称为Garbage Collector

+

经典GC

+
    +
  • 1999年随JDK1.3.1一起来的是串行方式的serialGc,它是第一款GC。ParNew垃圾收集器是Serial收集器的多线程版本
  • +
  • 2002年2月26日,Parallel GCConcurrent Mark Sweep GC跟随JDK1.4.2一起发布·
  • +
  • Parallel GC 在JDK6之后成为HotSpot默认GC。
  • +
  • 2012年,在JDK1.7u4版本中,G1可用。
  • +
  • 2017年,JDK9中G1变成默认的垃圾收集器,以替代CMS。
  • +
  • 2018年3月,JDK10中G1垃圾回收器的并行完整垃圾回收,实现并行性来改善最坏情况下的延迟。
  • +
+
+

高性能GC

+
    +
  • 2018年9月,JDK11发布。引入 Epsilon 垃圾回收器,又被称为 “No-Op(无操作)“ 回收器。同时,引入ZGC:可伸缩的低延迟垃圾回收器(Experimental)
  • +
  • 2019年3月,JDK12发布。增强G1,自动返回未用堆内存给操作系统。同时,引入 Shenandoah GC:低停顿时间的GC(Experimental)。·2019年9月,JDK13发布。增强ZGC,自动返回未用堆内存给操作系统。
  • +
  • 2020年3月,JDK14发布。删除CMS垃圾回收器。扩展zGC在 MacOS 和 Windows 上的应用
  • +
+

7种经典的垃圾回收器

+
    +
  • 串行回收器:SerialSerial old
  • +
  • 并行回收器:ParNewParallel ScavengeParallel old
  • +
  • 并发回收器:CMSG1
  • +
+

垃圾回收器与垃圾分代之间的关系

+

收集器与垃圾分代之间的关系

+
    +
  • 新生代收集器:SerialParNewParallel Scavenge
  • +
  • 老年代收集器:Serial oldParallel oldCMS
  • +
  • 整堆收集器:G1
  • +
+

垃圾收集器的组合关系

+

垃圾回收器组合关系

+
    +
  • 两个收集器间有连线,表明它们可以搭配使用 +
      +
    • Serial/Serial old
    • +
    • Serial/CMS
    • +
    • ParNew/Serial old
    • +
    • ParNew/CMS、Parallel
    • +
    • Scavenge/Serial 0ld
    • +
    • Parallel Scavenge/Parallel old
    • +
    • G1
    • +
    +
  • +
  • Serial oldCMS之间的连线表示,Serial old作为CMS出现"Concurrent Mode Failure"失败的后备预案
  • +
  • 红色虚线表示,在JDK 8时将Serial + CMSParNew + Serial old这两个组合声明为废弃;并在JDK9中完全取消了这些组合的支持
  • +
  • 绿色虚线表示,JDK14中:弃用Parallel ScavengeSerial old 组合
  • +
  • 青色虚线表示,在JDK14中删除CMS垃圾回收器
  • +
+
+

PS 为什么要有很多垃圾回收器? +因为垃圾回收器没有最好的实现,想要STW时间段的就需要吞吐量少一点;所以我们选择的只是对具体应用最合适的收集器。 +针对不同的场景,提供不同的垃圾收集器,来提高垃圾收集的性能。

+
+

查看默认垃圾收集器

+

使用虚拟机参数-XX:+PrintCommandLineFlags:查看命令行相关参数

+

运行下列程序

+
public class MainTest {
+    public static void main(String[] args) {
+        try {
+            Thread.sleep(100000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+}
+

输出结果

+
-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC 
+

使用命令行指令:jinfo -flag 垃圾回收器参数 进程ID +jinfo命令查看GC

+

Serial GC

+

Serial GC由于弊端较大,只有放在单核CPU上才能充分发挥其作用,由于现在都是多核CPU已经不用串行收集器了,所以以下内容了解即可。

+

对于交互较强的应用而言,这种垃圾收集器是不能接受的。一般在Java web应用程序中是不会采用串行垃圾收集器的。

+

Serial GC(串行垃圾回收回器)是最基本、历史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的选择。

+

Serial GC作为HotSpot中client模式下的默认新生代垃圾收集器; +Serial GC采用复制算法、串行回收和"stop-the-world"机制的方式执行内存回收;

+

除了年轻代之外,Serial GC还提供用于执行老年代垃圾收集的Serial old GC;Serial old GC是运行在Client模式下默认的老年代的垃圾回收器; +同样也采用了串行回收和"stop the world"机制,只不过内存回收算法使用的是标记-压缩算法。

+

Serial GC是一个单线程的收集器,但它的“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束

+

Serial GC优点, 简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial GC由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。 +是运行在client模式下的虚拟机是个不错的选择。

+

使用Serial GC

+

运行任意程序,设置虚拟机参数如下;当设置使用Serial GC时,新生代和老年代都会使用串行收集器。

+
-XX:+PrintCommandLineFlags -XX:+UseSerialGC
+

输出

+
-XX:InitialHeapSize=268435456 -XX:MaxHeapSize=4294967296 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC 
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/jvm/java-object/index.html b/blog-site/public/posts/jvm/java-object/index.html new file mode 100644 index 00000000..53e867c1 --- /dev/null +++ b/blog-site/public/posts/jvm/java-object/index.html @@ -0,0 +1,1012 @@ + + + + + + + + + + + JVM-Java对象 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JVM-Java对象

+ 2021.04.12 +
+

对象实例化

+

对象创建步骤

+

对象的创建方式

+
    +
  • 使用new关键字创建:最常见的方式、单例类中调用getInstance的静态类方法,XXXFactory的静态方法;
  • +
  • 使用反射方式创建: +
      +
    • 使用ClassnewInstance方法:在JDK9里面被标记为过时的方法,因为只能调用空参构造器;
    • +
    • 使用ConstructornewInstance(XXX):反射的方式,可以调用空参的,或者带参的构造器;
    • +
    • 使用clone():不调用任何的构造器,要求当前的类需要实现Cloneable接口中的clone接口;
    • +
    • 使用序列化:序列化一般用于Socket的网络传输;
    • +
    +
  • +
  • 使用第三方库 Objenesis
  • +
+

对象的创建步骤

+

创建对象代码演示及字节码指令

+
public class MainTest {
+    public static void main(String[] args) {
+        Object obj = new Object();
+    }
+}
+

创建对象字节码指令

+

创建对象步骤:

+
    +
  1. 加载类元信息
  2. +
  3. 为对象分配内存
  4. +
  5. 处理并发问题
  6. +
  7. 属性的默认初始化(零值初始化)
  8. +
  9. 设置对象头信息
  10. +
  11. 属性的显示初始化、代码块中初始化、构造器中初始化
  12. +
+

对象的创建过程

+

判断对象对应的类是否加载、链接、初始化

+

虚拟机遇到一条new指令,首先去检查这个指令的参数能否在 Metaspace 的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化。(即判断类元信息是否存在)。

+

如果没有,那么在双亲委派模式下,使用当前类加载器以 ClassLoader + 包名 + 类名 为key进行查找对应的 .class 文件,如果没有找到文件,则抛出 ClassNotFoundException 异常,如果找到,则进行类加载,并生成对应的Class对象。

+

为对象分配内存

+

首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象。 +如果实例成员变量是引用类型,仅分配引用变量空间即可,即4个字节大小.

+

如果内存规整:指针碰撞;如果内存不规整:虚拟表需要维护一个列表,即空闲列表分配

+
+

指针碰撞: +所有用过的内存在一边,空闲的内存放另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针指向空闲那边挪动一段与对象大小相等的距离罢了。 +如果垃圾收集器选择的是Serial ,ParNew这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带Compact(整理)过程的收集器时,使用指针碰撞。

+
+
+

空闲列表分配: +虚拟机维护了一个列表,记录上那些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容。 +这种分配方式成为了 “空闲列表(Free List)”。

+
+

选择哪种分配方式由Java堆是否规整所决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

+

处理并发问题

+
    +
  • 采用CAS配上失败重试保证更新的原子性
  • +
  • 每个线程预先分配TLAB - 通过设置 -XX:+UseTLAB参数来设置,在Java8是默认开启的
  • +
+

初始化分配到的内存

+

就是给对象属性赋值的操作

+
    +
  • 属性的默认初始化
  • +
  • 显示初始化
  • +
  • 代码块中的初始化
  • +
  • 构造器初始化
  • +
+

给所有属性设置默认值,保证对象实例字段在不赋值时可以直接使用。

+

设置对象的对象头

+

将对象的所属类(即类的元数据信息)、对象的 HashCode 和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。

+

执行init方法进行初始化

+

初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。 +因此一般来说(由字节码中跟随 invokespecial 指令所决定),new指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完成创建出来。

+

对象组成

+

Java对象的布局

+

查看对象的组成

+

引入依赖

+
<!-- 引入查看对象布局的依赖 -->
+<dependency>
+    <groupId>org.openjdk.jol</groupId>
+    <artifactId>jol-core</artifactId>
+    <version>0.9</version>
+</dependency>
+

测试代码

+
public class MainTest {
+    public static void main(String[] args) {
+        //这个对象里面是空的什么都没有
+        T t = new T();
+        System.out.println(ClassLayout.parseInstance(t).toPrintable());
+    }
+}
+class T{}
+

测试结果

+
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
+      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
+      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
+      8     4        (object header)                           a1 2c 01 20 (10100001 00101100 00000001 00100000) (536947873)
+     12     4        (loss due to the next object alignment)
+Instance size: 16 bytes
+Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
+

在64位JVM下,通过测试的结果我们可以看到Java对象的布局对象头占12byte,因为Jvm规定内存分配的字节必须是8的倍数,否则无法分配内存,所以就出现了,4byte的对齐数据。

+

通过测试发现,Java的对象布局包括:

+
    +
  • 必定存在一个对象头
  • +
  • 如果分配的JVM分配的字节不是8的倍数的话,还要存在对齐数据
  • +
  • 当该类里边有变量时,还包含实例变量
  • +
+

对象头

+

官方文档对于对象头的解释

+
+

object header +Common structure at the beginning of every GC-managed heap object. +(Every oop points to an object header.) Includes fundamental information about the heap object’s layout, type, GC state, synchronization state, and identity hash code. +Consists of two words. In arrays it is immediately followed by a length field. +Note that both Java objects and VM-internal objects have a common object header format.

+
+

大意:每个GC管理堆对象开始处的公共结构。(每个oop都指向一个对象头。)包括关于堆对象布局、类型、GC状态、同步状态和标识散列代码的基本信息。 +由两个词组成。在数组中,它后面紧跟着一个长度字段。注意,Java对象和vm内部对象都有一个共同的对象头格式。

+

其中上面的两个词指的是

+
+

mark word +The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. +May also be a pointer (with characteristic low bit encoding) to synchronization related information. +During GC, may contain GC state bits.

+
+

大意:每个对象头文件的第一个字。通常是一组位域,包括同步状态和身份散列码。也可以是一个用于同步相关信息的指针(具有特征的低位编码)。在GC期间,可能包含GC状态位。

+
+

klass pointer +The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original object. +For Java objects, the “klass” contains a C++ style “vtable”.

+
+

大意:每个对象头文件的第二个字。指向另一个对象(元对象),它描述了原始对象的布局和行为。对于Java对象,“klass”包含一个c++风格的“vtable”。

+

简单来说,对象头包含了包括两部分,分别是标志词(mark word)和类型指针(klass pointer);如果是数组,还需要记录数组的长度。

+
+

类型指针,是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

+
+

引用上面在64位虚拟机下的测试结果,对象头中的这些二进制数字代表什么呢?

+
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
+      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
+      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
+      8     4        (object header)                           a1 2c 01 20 (10100001 00101100 00000001 00100000) (536947873)
+

openjdk8-mastermarkOop.hpp,有对mark word的描述

+
//  32 bits:
+//  --------
+//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
+//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
+//             size:32 ------------------------------------------>| (CMS free block)
+//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
+//
+//  64 bits:
+//  --------
+//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
+//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
+//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
+//  size:64 ----------------------------------------------------->| (CMS free block)
+
    +
  • 在32位虚拟机下,正常的对象对象头的mark word,从前到后的顺序,25个位表示hash值,4位表示GC分代的年龄,1位偏向锁的信息,2位锁的状态;
  • +
  • 在64位虚拟机下,正常的对象对象头的mark word,从前到后的顺序,25个未使用,31个位表示hash值,未使用1位,4位表示GC分代的年龄,1位偏向锁的信息,2位锁的状态;
  • +
+

64位虚拟机下对象头共12个字节(96bit),mark word占8字节(64位),所以klass pointer占4字节(32位);

+
+

有些资料上显示: mark word占8字节(64位),klass pointer也占8字节(64位); +其实这种说法也正确,因为虚拟机默认开启了指针压缩,所以默认情况下klass pointer占4字节(32位)。

+
+

那么mark word64位中存放什么呢?

+

从上面可以看出,标志词(mark word)主要存放:

+
    +
  • 哈希值
  • +
  • GC分代年龄
  • +
  • 锁标志位
  • +
  • 分代年龄
  • +
  • 线程ID
  • +
+

当然mark word会根据对象的不同状态存放的也不相同;

+
+

对象的状态

+
    +
  • 无状态:对象刚被new出来
  • +
  • 偏向锁状态: 一个线程持有对象
  • +
  • 轻量级锁状态
  • +
  • 重量级锁状态
  • +
  • 被垃圾回收器标记的状态
  • +
+
+

目前32位虚拟机已经几乎没人用了,所以只介绍64位JVM

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
对象状态对象头-markword
无锁状态状态unused:25|hash:31| unused:1 |age:4 | biased_lock:1|lock:2
偏向锁状态JavaThread:54| epoch:2| unused:1 | age:4| biased_lock:1|lock:2
轻量级锁状态ptr_to_lock_record:62(指向栈中锁记录的指针)|lock:2
重量级锁状态Ptr_to_heavyweight_monitor:62(指向重量级锁的指针)|lock:2
被GC标记状态lock:2
+

对象的状态是5种,但是在markword中表示对象状态的lock却是2bit,2bit最多能表示4种状态,那么对象的5种状态是怎么表示的?

+
+

2bit 排列组合:00、11、01、10,最多四种

+
+

对象锁的状态是联合用biased_lock: 1lock: 2 表示的:

+
    +
  • biased_lock :0 lock: 01: 表示无锁状态
  • +
  • biased_lock :1 lock: 01: 表示偏向锁状态
  • +
  • lock: 00: 表示轻量级锁状态
  • +
  • lock: 10: 表示重量级锁状态
  • +
  • lock: 11: 表示被垃圾回收器标记的状态
  • +
+

markword中存储的内容:

+
    +
  • lock:锁标志位;区分锁状态,11时表示对象待GC回收状态, 只有最后2位锁标识(11)有效。
  • +
  • biased_lock:是否偏向锁;由于正常锁和偏向锁的锁标识都是 01,没办法区分,这里引入一位的偏向锁标识位。
  • +
  • age:分代年龄;表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。
  • +
  • hash: 对象的 hashcode ;运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。当对象加锁后,计算的结果31位不够表示,在偏向锁,轻量锁,重量锁,hashcode会被转移到Monitor中。
  • +
  • JavaThread:偏向锁的线程ID,偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。 在后面的操作中,就无需再进行尝试获取锁的动作。
  • +
  • epoch:时间戳,代表偏向锁的有效性;偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。
  • +
  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。当锁获取是无竞争的时,JVM使用原子操作而不是OS互斥。这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的标题字中设置指向锁记录的指针。
  • +
  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程。在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针。
  • +
+

mark word在对象的不同状态下会有不同的表现形式,主要有三种状态,无锁状态、加锁状态、GC标记状态。

+

那么可以理解为Java当中的取锁其实可以理解是给对象上锁,也就是改变对象头中markword>lock的状态,如果上锁成功则进入同步代码块。 +但是Java当中的锁有分为很多种,从上面可以看出大体分为偏向锁、轻量锁、重量锁三种锁状态.

+

实例数据

+

独立于方法之外的变量,在类里定义的变量无 static 修饰;包括从父类继承下来的变量和本身拥有的字段。

+

一些规则:

+
    +
  • 相同宽度的字段总是被分配到一起(比如都是8个字节的,会放到一起);
  • +
  • 父类中定义的变量会出现在子类之前;
  • +
  • 如果JVM参数CompactFields设置为true,子类的窄变量可能插入到父类的间隙;
  • +
+

对齐填充数据

+

不是必须的,也没有特别的含义,仅仅起到占位符的作用。 +因为Jvm规定内存分配的字节必须是8的倍数,否则无法分配内存.如果对象头占得字节 + 实例变量占得字节刚好为8的倍数,对齐填充数据则不存在。

+

对象的访问定位

+

JVM是如何通过栈帧中的对象引用访问到其内部的对象实例呢? +对象指针访问

+

hotspot使用的是直接访问。因为句柄访问开辟了句柄池,所以直接访问相较于句柄访问效率稍高一点。

+

句柄访问

+

句柄访问是在栈的局部变量表中,记录的对象的引用,然后在堆空间中开辟了一块空间,也就是句柄池。指向的是方法区中的对象类型数据。

+

优点:reference 中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference 本身不需要被修改

+

句柄访问

+

直接访问

+

直接指针是局部变量表中的引用,直接指向堆中的实例,在对象实例中有类型指针,指向的是方法区中的对象类型数据。

+

直接访问

+

对象的终止机制

+

finalize()方法是Java提供的对象终止机制,允许开发人员提供对象被销毁之前的自定义处理逻辑。 +当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。

+

finalize() 方法允许在子类中被重写,用于在对象被回收时进行资源释放。 +通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。

+

文档注释大意:当GC确定不再有对对象的引用时,由垃圾收集器在对象上调用。子类重写finalize方法来释放系统资源或执行其他清理。

+
   /**
+     * Called by the garbage collector on an object when garbage collection
+     * determines that there are no more references to the object.
+     * A subclass overrides the {@code finalize} method to dispose of
+     * system resources or to perform other cleanup.
+     */
+    protected void finalize() throws Throwable { }
+

简而言之,finalize方法是与Java中的垃圾回收器有关系。即:当一个对象变成一个垃圾对象的时候,如果此对象的内存被回收,那么就会调用该类中定义的finalize方法。

+

当一个对象可被回收时,就需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会再调用该方法。

+

永远不要主动调用某个对象的finalize方法应该交给垃圾回收机制调用的原因:

+
    +
  • 在调用finalize方法时时可能会导致对象复活;
  • +
  • finalize方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下,若不发生GC,则finalize方法将没有执行机会;因为优先级比较低,即使主动调用该方法,也不会因此就直接进行回收;
  • +
  • 一个糟糕的finalize方法会严重影响GC的性能;
  • +
+

由于finalize方法的存在,虚拟机中的对象一般可能处于三种状态:

+

如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。 +但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,虚拟机中定义了的对象可能的三种状态:

+
    +
  • 可触及的:从根节点开始,可以到达这个对象;对象存活被使用;
  • +
  • 可复活的:对象的所有引用都被释放,但是对象有可能在finalize中复活;对象被复活,对象在finalize方法中被重新使用;
  • +
  • 不可触及的:对象的finalize方法被调用,并且没有复活,那么就会进入不可触及状态;对象死亡,对象没有被使用;
  • +
+

只有在对象不可触及时才可以被回收。不可触及的对象不可能被复活,因为finalize()只会被调用一次。

+

finalize对象终止机制判定一个对象能否被回收过程:

+

判定一个对象是否可回收,至少要经历两次标记过程:

+
    +
  • 如果对象没有没有引用链,则进行第一次标记
  • +
  • 进行筛选,判断此对象是否有必要执行finalize方法 +
      +
    1. 如果对象没有重写finalize方法,或者finalize方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,对象被判定为不可触及的。
    2. +
    3. 如果对象重写了finalize方法,且还未执行过,那么会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize方法执行。
    4. +
    5. finalize方法是对象逃脱死亡的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果对象在finalize方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,该对象会被移出“即将回收”集合。 +之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。
    6. +
    +
  • +
+

代码演示对象能否被回收:

+
public class MainTest {
+
+    public static MainTest var;
+
+    /**
+     * 此方法只能被调用一次
+     * 可对该方法进行注释,来测试finalize方法是否能复活对象
+     */
+    @Override
+    protected void finalize() throws Throwable {
+        System.out.println("调用当前类重写的finalize()方法");
+        // 复活对象 让当前带回收对象重新与引用链中的对象建立联系
+        var = this;
+    }
+
+    public static void main(String[] args) throws InterruptedException {
+        var = new MainTest();
+        var = null;
+        System.gc();
+        System.out.println("-----------------第一次gc操作------------");
+        // 因为Finalizer线程的优先级比较低,暂停2秒,以等待它
+        Thread.sleep(2000);
+        if (var == null) {
+            System.out.println("对象已经死了");
+            // 如果第一次对象就死亡了 就终止
+            return;
+        } else {
+            System.out.println("对象还活着");
+        }
+
+        System.out.println("-----------------第二次gc操作------------");
+        var = null;
+        System.gc();
+        // 下面代码和上面代码是一样的,但是 对象却自救失败了
+        Thread.sleep(2000);
+        if (var == null) {
+            System.out.println("对象已经死了");
+        } else {
+            System.out.println("对象还活着");
+        }
+    }
+
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/jvm/jvm-about/index.html b/blog-site/public/posts/jvm/jvm-about/index.html new file mode 100644 index 00000000..3322d7e1 --- /dev/null +++ b/blog-site/public/posts/jvm/jvm-about/index.html @@ -0,0 +1,1015 @@ + + + + + + + + + + + JVM-相关概念 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JVM-相关概念

+ 2021.04.27 +
+

内存溢出

+

内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。 +官方文档中对内存溢出的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。

+

由于GC一直在发展,所有一般情况下,除非应用程序占用的内存增长速度非常快,造成垃圾回收已经跟不上内存消耗的速度,否则不太容易出现OOM的情况。

+

引起内存溢出的原因:

+
    +
  • Java虚拟机的堆内存设置不够。
  • +
  • 代码中创建了大量大对象,并且长时间不能被垃圾收集器收集。
  • +
+

OOM异常信息变化:

+
    +
  • JDK7及之前:java.lang.OutOfMemoryError:PermGen space
  • +
  • JDK8:java.lang.OutofMemoryError:Metaspace
  • +
+

在抛出OutOfMemoryError之前,通常垃圾收集器会被触发,尽其所能去清理出空间。当然,不是在任何情况下垃圾收集器都会被触发的: +比如,我们去分配一个超大对象,类似一个超大数组超过堆的最大值,JVM可以判断出垃圾收集并不能解决这个问题,所以直接抛出OutOfMemoryError

+

内存泄漏

+

严格来说,只有对象不会再被程序用到了,但是GC又不能回收他们的情况,才叫内存泄漏。 +但实际情况很多时候一些不太好的实践(或疏忽)会导致对象的生命周期变得很长甚至导致00M,也可以叫做宽泛意义上的“内存泄漏”。

+

Java使用可达性分析算法来标记垃圾,最上面的数据不可达,就是需要被回收的。 +后期有一些对象不用了,按道理应该断开引用,但是存在一些链没有断开,从而导致没有办法被回收。从而造成内存泄漏。

+

内存泄漏

+

内存泄漏与内存溢出的关系

+

尽管内存泄漏并不会立刻引起程序崩溃,但是一旦发生内存泄漏,程序中的可用内存就会被逐步蚕食,直至耗尽所有内存,最终出现outOfMemory异常,导致程序崩溃。

+

举例

+
+

买房子:80平的房子,但是有10平是公摊的面积,我们是无法使用这10平的空间,这就是所谓的内存泄漏

+
+
    +
  • +

    单例模式;单例的生命周期和应用程序是一样长的,所以单例程序中,如果单例对象持有对外部对象的引用的话,而外部对象引用却不再使用,那么这个外部对象是不能被回收的,则会导致内存泄漏的产生。

    +
  • +
  • +

    提供close的资源未关闭导致内存泄漏;数据库连接,网络连接和IO连接必须手动,否则是不能被回收的。

    +
  • +
+

STW

+

Stop-the-world直译为:停止一切,简称STW,指的是垃圾回收发生过程中,会产生应用程序的停顿。 +停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉。

+

STW事件和采用哪款GC无关,因为所有的GC都有这个事件。任何垃圾回收器都不能完全避免Stop-the-world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。

+

为什么垃圾回收时要STW?

+

在垃圾回收标记阶段,JVM使用可达性分析算法进行标记那些对象是垃圾,如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证。 +所以在垃圾回收的时候要STW,分析工作必须在一个能确保一致性的快照中进行。

+

STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。 +被STW中断的应用程序线程会在完成GC之后恢复,频繁中断会让用户感觉像是网速不快造成电影卡带一样,所以我们需要减少STW的发生。

+

System.gc

+

在默认情况下,通过System.gc()Runtime.getRuntime().gc()的调用,会显式触发FullGC,同时对老年代和新生代进行回收,尝试释放被丢弃对象占用的内存。

+

源码调用了Runtime.getRuntime().gc();

+

大意为:

+
+

运行垃圾收集器。 +调用GC方法意味着Java虚拟机要努力回收未使用的对象,以便使它们当前占用的内存能够快速重用。当控制从方法调用中返回时,Java虚拟机已经尽了最大努力从所有丢弃的对象中回收空间。 +调用System.gc()有效地等同于调用:Runtime.getRuntime().gc()

+
+
    /**
+     * Runs the garbage collector.
+     * <p>
+     * Calling the <code>gc</code> method suggests that the Java Virtual
+     * Machine expend effort toward recycling unused objects in order to
+     * make the memory they currently occupy available for quick reuse.
+     * When control returns from the method call, the Java Virtual
+     * Machine has made a best effort to reclaim space from all discarded
+     * objects.
+     * <p>
+     * The call <code>System.gc()</code> is effectively equivalent to the
+     * call:
+     * <blockquote><pre>
+     * Runtime.getRuntime().gc()
+     * </pre></blockquote>
+     *
+     * @see     java.lang.Runtime#gc()
+     */
+    public static void gc() {
+        Runtime.getRuntime().gc();
+    }
+

调用System.gc();无法保证对垃圾收集器的调用;一般情况下,垃圾回收应该是自动进行的,无须手动触发。

+

代码演示是否触发GC

+
// 在线程不忙的情况下,GC几乎都会执行都会调用finalize()方法 多试几次(15~30)
+public class MainTest {
+    public static void main(String[] args) {
+        new MainTest();
+
+        // 建议垃圾回收器执行垃圾收集行为 不会立即执行
+        System.gc();
+
+        // 调用System.gc();后再调用System.runFinalization(); 会强制调用失去引用对象的 finalize() 方法
+//        System.runFinalization();
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        super.finalize();
+        System.out.println("finalize 被调用 ...");
+    }
+}
+

垃圾回收的的串行、并行、并发

+
+

PS: 并行、并发、串行

+
    +
  • 并行:前提是在多核CPU或多个CPU条件下,多个线程同时被多个CPU执行,同时执行的线程并不会抢占CPU资源。
  • +
  • 串行:前提是在单核CPU条件下,单线程程序执行,不能同时执行,也不能去切换执行。也就是在同一时间段只能做一件事。
  • +
  • 并发:前提是多线程条件下,多个线程抢占一个CPU资源,多个线程被交替执行。因为CPU运算速度很快,所以用户感觉不到线程切换的卡顿。
  • +
+
+

无论并行、并发,都可以有多个线程执行,如果是多个线程抢占一个CPU就成了并发,多个CPU同时执行多个线程就是并行。

+

对于单CPU的计算机来说,同一时间是只能干一件事儿的,如果是单线程线程就是串行;如果是多个线程就是并发。 +而对于多CPU的计算机说,同一时间能干多个事,如果多个CPU同时执行多个线程就是并行;如果一个CPU同时执行多个线程就是并行。

+
    +
  • +

    并行垃圾回收:在停止用户线程之后,多条GC线程并行进行垃圾回收,此时用户线程仍处于等待状态,出现STW现象。

    +
  • +
  • +

    并发垃圾回收:指多条垃圾收集线程同时进行工作,GC线程和用户线程同时运行,不会出现STW现象。

    +
  • +
  • +

    串行垃圾回收:在同一时间段内只允许有一个CPU用于执行垃圾回收操作,该收集器会在工作时冻结所有应用程序线程,这使它在所有目的和用途上都无法在服务器环境中使用。

    +
  • +
+

安全点与安全区域

+

安全点

+

程序执行时并非在所有地方都能停顿下来开始GC,只有在特定的位置才能停顿下来开始GC,这些位置称为安全点。

+

安全点的选择很重要,如果太少可能导致GC等待的时间太长,如果太频繁可能导致运行时的性能问题。 +大部分指令的执行时间都非常短暂,通常会根据“是否具有让程序长时间执行的特征”为标准。比如:选择一些执行时间较长的指令作为安全点。

+

如何在GC发生时,检查所有线程都跑到最近的安全点停顿下来呢?

+
    +
  • 抢先式中断:首先中断所有线程。如果还有线程不在安全点,就恢复线程,让线程跑到安全点。目前没有虚拟机采用了;
  • +
  • 主动式中断:设置一个中断标志,各个线程运行到安全点的时候主动轮询这个标志,如果中断标志为真,则将自己进行中断挂起。
  • +
+

安全区域

+

安全点机制保证了程序执行时,在不太长的时间内就会遇到可进入GC的安全点。 +但是,程序“不执行”的时候呢?例如线程处于sleep状态或Blocked状态,这时候线程无法响应JVM的中断请求,“走”到安全点去中断挂起,JVM也不太可能等待线程被唤醒。 +对于这种情况,就需要安全区域来解决。

+

安全区域是指在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始Gc都是安全的。 +可以把安全点看做是被扩展了的安全区域。

+
    +
  • 当线程运行到安全区域的代码时,首先标识已经进入了安全区域,如果这段时间内发生GC,JVM会忽略标识为安全区域状态的线程
  • +
  • 当线程即将离开安全区域时,会检查JVM是否已经完成GC,如果完成了,则继续运行,否则线程必须等待直到收到可以安全离开安全区域的信号为止;
  • +
+

引用

+

在JDK1.2版之后,Java对引用的概念进行了扩充,将引用分为:

+
    +
  • 强引用(StrongReference):最传统的“引用”的定义;无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
  • +
  • 软引用(SoftReference):在系统将要发生内存溢出之前,将会把这些对象列入回收范围之中进行第二次回收。如果这次回收后还没有足够的内存,才会抛出内存溢出异常。
  • +
  • 弱引用(WeakReference):被弱引用关联的对象只能生存到下一次垃圾收集之前。当垃圾收集器工作时,无论内存空间是否足够,都会回收掉被弱引用关联的对象。
  • +
  • 虚引用(PhantomReference):一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。
  • +
+

这4种引用强度依次逐渐减弱。除强引用外,其他3种引用均可以在java.lang.ref包中找到它们的身影。 +强引用为JVM内部实现。其他三类引用类型全部继承自Reference父类。

+

强软弱虚

+

上述引用垃圾回收的前提条件是:对象都是可触及的(可达性分析结果为可达),如果对象不可触及就直接被垃圾回收器回收了。

+

强引用

+

在Java程序中,最常见的引用类型是强引用,普通系统99%以上都是强引用,也就是我们最常见的普通对象引用,也是默认的引用类型。 +当在Java语言中使用new操作符创建一个新的对象,并将其赋值给一个变量的时候,这个变量就成为指向该对象的一个强引用。

+

强引用测试

+
// 强引用测试
+public class MainTest {
+    public static void main(String[] args) {
+        StringBuffer var0 = new StringBuffer("hello world");
+        StringBuffer var1 = var0;
+
+        var0 = null;
+        System.gc();
+        try {
+            Thread.sleep(3000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+        System.out.println(var1.toString());
+    }
+}
+

强引用所指向的对象在任何时候都不会被系统回收,虚拟机会抛出OOM异常,也不会回收强引用所指向对象; +所以强引用是导致内存泄漏的主要原因。

+

软引用

+

软引用是一种比强引用生命周期稍弱的一种引用类型。在JVM内存充足的情况下,软引用并不会被垃圾回收器回收,只有在JVM内存不足的情况下,才会被垃圾回收器回收。

+

软引用是用来描述一些还有用,但非必需的对象。 +只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

+
+

这里的第一次回收是指不可达的对象

+
+

所以软引用一般用来实现一些内存敏感的缓存,只要内存空间足够,对象就会保持不被回收掉。 +比如:高速缓存就有用到软引用。如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。

+

软引用测试

+
/**
+ * 软引用测试
+ * 
+ * 虚拟机参数:
+ * -Xms10m
+ * -Xmx10m
+ * -XX:+PrintGCDetails
+ */
+public class MainTest {
+    public static void main(String[] args) {
+        //SoftReference<User> softReference = new SoftReference<>(new User("hello"));
+        // 上面的一行代码等价于下面的三行代码
+        User user = new User("hello");
+        SoftReference<User> softReference = new SoftReference<User>(user);
+        // 一定要销毁强引用对象 否则创建软引用对象将毫无意义
+        user = null;
+        System.out.println("创建大对象之前:" + softReference.get());
+        try{
+            // 模拟堆内存资源紧张 看软引用对象是否会被回收
+            byte[] bytes = new byte[1024 * 1024 *7];
+        }catch (Throwable e) {
+            e.printStackTrace();
+        }finally {
+            System.out.println("创建大对象之后:" + softReference.get());
+        }
+    }
+}
+
+class User {
+    private String name;
+
+    public User(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return "User{" +
+                "name='" + name + '\'' +
+                '}';
+    }
+}
+

弱引用

+

弱引用也是用来描述那些非必需对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。

+

在系统GC时,只要发现弱引用,不管系统堆空间使用是否充足,都会回收掉只被弱引用关联的对象。 +由于垃圾回收器的线程通常优先级很低,因此,并不一定能很快地发现持有弱引用的对象。在这种情况下,弱引用对象可以存在较长的时间。

+

弱引用和软引用一样,在构造弱引用时,也可以指定一个引用队列,当弱引用对象被回收时,就会加入指定的引用队列,通过这个队列可以跟踪对象的回收情况。

+

软引用、弱引用都非常适合来保存那些可有可无的缓存数据。如果这么做,当系统内存不足时,这些缓存数据会被回收,不会导致内存溢出。 +而当内存资源充足时,这些缓存数据又可以存在相当长的时间,从而起到加速系统的作用。

+

弱引用测试

+
/**
+ * 弱引用测试
+ */
+public class MainTest {
+    public static void main(String[] args) {
+        WeakReference<User> weakReference = new WeakReference<>(new User("hello"));
+        System.out.println("建议GC之前:" + weakReference.get());
+        System.gc();
+        System.out.println("建议GC之后:" + weakReference.get());
+    }
+}
+
+class User {
+    private String name;
+
+    public User(String name) {
+        this.name = name;
+    }
+
+    @Override
+    public String toString() {
+        return "User{" +
+                "name='" + name + '\'' +
+                '}';
+    }
+}
+

虚引用

+

虚引用也称为“幽灵引用”或者“幻影引用”,是所有引用类型中最弱的一个。

+

一个对象是否有虚引用的存在,完全不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它和没有引用几乎是一样的,随时都可能被垃圾回收器回收。 +它不能单独使用,也无法通过虚引用来获取被引用的对象。当试图通过虚引用的get()方法取得对象时,总是null;

+

为一个对象设置虚引用关联的唯一目的在于跟踪垃圾回收过程。比如:能在这个对象被收集器回收时收到一个系统通知。 +虚引用必须和引用队列一起使用。 虚引用在创建时必须提供一个引用队列作为参数。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象后,将这个虚引用加入引用队列,以通知应用程序对象的回收情况。 +由于虚引用可以跟踪对象的回收时间,因此,也可以将一些资源释放操作放置在虚引用中执行和记录。

+

虚引用测试

+
/**
+ * 虚引用测试
+ */
+public class MainTest {
+    // 当前类对象的声明
+    public static MainTest obj;
+    // 引用队列
+    static ReferenceQueue<MainTest> phantomQueue = null;
+
+    @Override
+    protected void finalize() throws Throwable {
+        super.finalize();
+        System.out.println("调用当前类的finalize方法");
+        obj = this;
+    }
+
+    public static void main(String[] args) {
+        Thread thread = new Thread(() -> {
+            while(true) {
+                if (phantomQueue != null) {
+                    PhantomReference<MainTest> objt = null;
+                    try {
+                        objt = (PhantomReference<MainTest>) phantomQueue.remove();
+                    } catch (Exception e) {
+                        e.getStackTrace();
+                    }
+                    if (objt != null) {
+                        System.out.println("追踪垃圾回收过程:PhantomReferenceTest实例被GC了");
+                    }
+                }
+            }
+        }, "t1");
+        thread.setDaemon(true);
+        thread.start();
+
+        phantomQueue = new ReferenceQueue<>();
+        obj = new MainTest();
+        // 构造了PhantomReferenceTest对象的虚引用,并指定了引用队列
+        PhantomReference<MainTest> phantomReference = new PhantomReference<>(obj, phantomQueue);
+        try {
+            System.out.println(phantomReference.get());
+            // 去除强引用
+            obj = null;
+            // 第一次进行GC,由于对象可复活,GC无法回收该对象
+            System.out.println("第一次GC操作");
+            System.gc();
+            Thread.sleep(1000);
+            if (obj == null) {
+                System.out.println("obj 是 null");
+            } else {
+                System.out.println("obj 不是 null");
+            }
+            System.out.println("第二次GC操作");
+            obj = null;
+            System.gc();
+            Thread.sleep(1000);
+            if (obj == null) {
+                System.out.println("obj 是 null");
+            } else {
+                System.out.println("obj 不是 null");
+            }
+        } catch (Exception e) {
+            e.printStackTrace();
+        } finally {
+
+        }
+    }
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/jvm/jvm-classloader/index.html b/blog-site/public/posts/jvm/jvm-classloader/index.html new file mode 100644 index 00000000..e8c9b2e4 --- /dev/null +++ b/blog-site/public/posts/jvm/jvm-classloader/index.html @@ -0,0 +1,1266 @@ + + + + + + + + + + + JVM-Java类加载机制 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JVM-Java类加载机制

+ 2021.02.05 +
+

类加载过程

+

在Java中,类加载器把一个类装入JVM中,要经过以下步骤: 加载、验证、准备、解析和初始化。其中验证,准备,解析统称为连接。 这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。

+

Jvm内存图

+

类加载器只负责class文件的加载,至于它是否可以运行,则由执行引擎(Execution Engine)决定。

+

被加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息,可能还包括字符串字面量和数字常量。

+

类加载过程流程图:

+

类加载过程

+

类加载过程详细

+

加载

+
+

加载阶段是类加载过程的第一个阶段。在这个阶段,JVM 的主要目的是将字节码从各个位置(网络、磁盘等)转化为二进制字节流加载到内存中,接着会为这个类在 JVM 的方法区创建一个对应的 Class 对象,这个 Class 对象就是这个类各种数据的访问入口。

+
+

类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构.简单来说,加载指的是把class字节码文件从各个来源通过类加载器装载入内存中。

+

类加载器并不需要等到某个类被“首次主动使用”时再加载它,JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。

+

JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。

+

JVM在类加载阶段作用:

+
    +
  • 通过类的全限定名获取该类的二进制字节流
  • +
  • 将字节流所代表的存储结构转化为方法区的运行时的数据结构
  • +
  • 在内存中生成一个该类的java.lang.Class对象作为方法区的这个类的各种数据访问入口.
  • +
+

验证

+

JVM会在该阶段对二进制字节流进行校验,只有符合JVM字节码规范的才能被 JVM 正确执行。

+

大致都会完成以下四个阶段的验证

+
    +
  • 文件格式验证
  • +
  • 元数据验证,是否符合java语言的规范
  • +
  • 字节码验证,确保程序语义合法,符合逻辑
  • +
  • 符号引用验证,确保下一步解析能正常执行
  • +
+

准备

+

为静态变量在方法取分配内存,并设置默认初始值。将在方法区中进行分配.

+

举个例子:

+
public String var1 = "var1";
+public static String var2 = "var2";
+public static final String var3 = "var3";
+

变量var1不会被分配内存,但是var2会被分配.var2会被分配初始值为null而不是’var2'.

+

这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。

+

实例变量会在对象实例化时随着对象一块分配在Java堆中。

+

这里不包含final修饰的static,因为final在编译的时候就已经分配了.也就是说var3被分配的值为’var3'

+

解析

+

虚拟机将常量池中的符号引用替换为直接引用.

+

符号引用

+

符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括了以下三类常量: 类和接口的全限定名 字段的名称和描述符 方法的名称和描述符.

+
+

符号引用 :符号引用以一组符号来描述所引用的目标。符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。

+
+

在编译的时候每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替,而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。

+

直接引用

+

直接引用和虚拟机的布局是相关的,不同的虚拟机对于相同的符号引用所翻译出来的直接引用一般是不同的。 +如果有了直接引用,那么直接引用的目标一定被加载到了内存中。 +直接引用通过对符号引用进行解析,找到引用的实际内存地址。

+

解析操作往往会伴随者JVM在执行完初始化之后再执行。 +解析动作主要针对接口、字段、类方法、接口方法、方法类型等。对应常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等。

+

初始化

+

初始化阶段就是执行类构造器<clinit>()的过程。

+
+

<clinit>() 可理解为类中的方法。

+
+

此方法不需定义,是javac编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来。 +也就是说,当我们代码中包含static变量的时候,就会有<clinit>()方法

+

在准备阶段,静态变量已经被赋过默认初始值,而在初始化阶段,静态变量将被赋值为代码期望赋的值.

+

举个例子

+
public static String var2 = "var2";
+

在准备阶段变量var2的值为null,在初始化阶段赋值为’var2'.

+

在Java中对类变量进行初始值设定有两种方式:

+
    +
  • 声明类变量时指定初始值
  • +
  • 使用静态代码块为类变量指定初始值
  • +
+

初始化步骤

+
    +
  • 如果这个类还没有被加载和连接,则程序先加载并连接该类
  • +
  • 如果该类的直接父类还没有被初始化,则先初始化其直接父类
  • +
  • 如果类中有初始化语句,则系统依次执行这些初始化语句
  • +
+

何时初始化

+
    +
  • 创建类的实例,也就是new一个对象需要初始化
  • +
  • 读取或者设置静态字段的时候需要初始化(但被final修饰的字段,在编译时就被放入静态常量池的字段除外.)
  • +
  • 调用类的静态方法
  • +
  • 使用反射Class.forName("");对类反射调用的时候,该类需要初始化
  • +
  • 初始化一个类的时候,有父类,先初始化父类 +
      +
    • 接口除外,父接口在调用的时候才会被初始化;
    • +
    • 子类引用父类的静态字段,只会引发父类的初始化;
    • +
    +
  • +
  • 被标明为启动类的类(即包含main()方法),需要初始化
  • +
+

初始化规则

+

若该类具有父类,JVM会保证子类的()方法执行前,父类的()方法已经执行完毕。

+

初始化顺序

+
父类静态域 --> 子类静态域 --> 父类成员初始化 -->父类构造块 --->父类构造方法 -->子类成员初始化 -->子类构造块 -->子类构造方法
+

一些初始化规则:

+
+
    +
  • 类从顶至底的顺序初始化,所以声明在顶部的字段的早于底部的字段初始化
  • +
  • 超类早于子类和衍生类的初始化
  • +
  • 如果类的初始化是由于访问静态域而触发,那么只有声明静态域的类才被初始化,而不会触发超类的初始化或者子类的
  • +
  • 初始化即使静态域被子类或子接口或者它的实现类所引用。
  • +
  • 接口初始化不会导致父接口的初始化。
  • +
  • 静态域的初始化是在类的静态初始化期间,非静态域的初始化时在类的实例创建期间。这意味这静态域初始化在非静态域之前。
  • +
  • 非静态域通过构造器初始化,子类在做任何初始化之前构造器会隐含地调用父类的构造器,他保证了非静态或实例变量(父类)初始化早于子类。
  • +
  • 虚拟机必须保证一个类的<clinit>()方法在多线程下被同步加锁。也就是说类只能被初始化一次。
  • +
+
+

类加载器

+

JVM设计者把类加载阶段中的"通过’类全名’来获取定义此类的二进制字节流" 这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

+

从虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现(这里仅限于Hotspot,也就是JDK1.5之后默认的虚拟机,有很多其他的虚拟机是用Java语言实现的),属于虚拟机自身的一部分。 +另外一种就是自定义类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader

+
public class ClassloaderTest {
+    public static void main(String[] args) {
+        // 获取系统类加载器
+        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
+        // sun.misc.Launcher$AppClassLoader@18b4aac2
+        System.out.println(systemClassLoader);
+
+        // 获取其上层的:扩展类加载器
+        ClassLoader extClassLoader = systemClassLoader.getParent();
+        // sun.misc.Launcher$ExtClassLoader@5cad8086
+        System.out.println(extClassLoader);
+
+        // 试图获取 启动类加载器
+        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
+        // null 不能获取到启动类加载器
+        System.out.println(bootstrapClassLoader);
+
+        // 获取自定义加载器
+        ClassLoader classLoader = ClassloaderTest.class.getClassLoader();
+        // sun.misc.Launcher$AppClassLoader@18b4aac2
+        System.out.println(classLoader);
+
+        // 获取String类型的加载器
+        // Java 核心包都是用启动类加载器加载的
+        ClassLoader classLoader1 = String.class.getClassLoader();
+        // null
+        System.out.println(classLoader1);
+    }
+}
+

可以看出 启动类加载器无法直接通过代码获取,同时目前用户代码所使用的加载器为系统类加载器。同时我们通过获取String类型的加载器,发现是null, +那么说明String类型是通过根加载器进行加载的,也就是说Java的核心类库都是使用根加载器进行加载的。

+

获取启动类加载器加载的路径

+
public class ClassloaderTest {
+    public static void main(String[] args) {
+        System.out.println("*********启动类加载器加载的路径************");
+        // 获取BootstrapClassLoader 能够加载的API的路径
+        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
+        for (URL url : urls) {  
+            System.out.println(url.toExternalForm());
+        }
+
+        // 从上面路径中,随意选择一个类,来看看他的类加载器是什么:得到的是null,说明是  根加载器
+        ClassLoader classLoader = Provider.class.getClassLoader();
+        System.out.println(classLoader);
+
+    }
+}
+

启动类加载器(引导类加载器、Bootstrap ClassLoader)

+
    +
  • 该类加载器使用C/C++语言实现的,嵌套在JVM内部,可理解为就是JVM的一部分。
  • +
  • 它用来加载Java的核心库(JAVAHOME/jre/1ib/rt.jar、resources.jarsun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
  • +
  • 并不继承自java.lang.ClassLoader,没有父加载器。
  • +
  • 加载扩展类和应用程序类加载器,并指定为他们的父类加载器。
  • +
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
  • +
+

扩展类加载器

+
    +
  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现。
  • +
  • 派生于ClassLoader类。
  • +
  • 父类加载器为启动类加载器。
  • +
  • java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录(扩展目录)下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
  • +
+

应用程序类加载器(系统类加载器、AppClassLoader)

+
    +
  • javI语言编写,由sun.misc.LaunchersAppClassLoader实现。
  • +
  • 派生于ClassLoader类。
  • +
  • 父类加载器为扩展类加载器。
  • +
  • 它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库。
  • +
  • 该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载。
  • +
  • 通过classLoader#getSystemclassLoader()方法可以获取到该类加载器。
  • +
+

自定义类加载器

+
+

PS 为什么会有自定义类加载器?

+
    +
  • 一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。
  • +
  • 另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。
  • +
+

自定义加载器使用场景

+
    +
  1. 隔离加载类
  2. +
  3. 修改类加载的方式
  4. +
  5. 扩展加载源
  6. +
  7. 防止源码泄漏
  8. +
+
+

若要实现自定义类加载器,只需要继承java.lang.ClassLoader类.按需重写相关方法即可.

+
    +
  • 如果不想打破双亲委派模型,那么只需要重写findClass方法
  • +
  • 如果想打破双亲委派模型,那么就重写整个loadClass方法
  • +
+
+

在JDK1.2之前,类加载尚未引入双亲委派模式,因此实现自定义类加载器时常常重写loadClass方法,提供双亲委派逻辑,从JDK1.2之后,双亲委派模式已经被引入到类加载体系中,自定义类加载器时不需要在自己写双亲委派的逻辑,因此不鼓励重写loadClass方法,而推荐重写findClass方法。

+

在Java中,任意一个类都需要由加载它的类加载器和这个类本身一同确定其在java虚拟机中的唯一性,即比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这两个类来源于同一个Class类文件,只要加载它的类加载器不相同,那么这两个类必定不相等(这里的相等包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法和instanceof关键字的结果)。

+
+

重写findClass方法

+

准备一个class文件,编译后放到C盘根目录下

+
public class People {
+	private String name;
+	public String getName() {
+		return name;
+	}
+	public void setName(String name) {
+		this.name = name;
+	}
+}
+

自定义类加载器,继承ClassLoader重写findClass方法(其中defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class

+
public class MyClassLoader extends ClassLoader {
+
+    public MyClassLoader(){}
+    
+    public MyClassLoader(ClassLoader parent)
+    {
+        super(parent);
+    }
+    
+    protected Class<?> findClass(String name) throws ClassNotFoundException {
+    	File file = new File("C:/People.class");
+        try{
+            byte[] bytes = getClassBytes(file);
+            //defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class
+            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
+            return c;
+        } 
+        catch (Exception e)
+        {
+            e.printStackTrace();
+        }
+        
+        return super.findClass(name);
+    }
+    
+    private byte[] getClassBytes(File file) throws Exception {
+        // 这里要读入.class的字节,因此要使用字节流
+        FileInputStream fis = new FileInputStream(file);
+        FileChannel fc = fis.getChannel();
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        WritableByteChannel wbc = Channels.newChannel(baos);
+        ByteBuffer by = ByteBuffer.allocate(1024);
+        
+        while (true){
+            int i = fc.read(by);
+            if (i == 0 || i == -1)
+            break;
+            by.flip();
+            wbc.write(by);
+            by.clear();
+        }
+        fis.close();
+        return baos.toByteArray();
+    }
+}
+

双亲委派模型

+

双亲委派模型

+

这种层次关系称为类加载器的双亲委派模型。 我们把每一层上面的类加载器叫做当前层类加载器的父加载器,当然,它们之间的父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中的代码。 +该模型在JDK1.2期间被引入并广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者们推荐给开发者的一种类的加载器实现方式。

+

工作原理

+
    +
  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行;
  • +
  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器;
  • +
  • 如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载。
  • +
+

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上。 +因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

+

rt.jar包中的java.lang.ClassLoader类中,我们可以查看类加载实现过程loadClass方法的代码,具体源码如下:

+
    protected Class<?> loadClass(String name, boolean resolve)
+        throws ClassNotFoundException
+    {
+        synchronized (getClassLoadingLock(name)) {
+            // First, check if the class has already been loaded
+            // 首先检查该name指定的class是否有被加载
+            Class<?> c = findLoadedClass(name);
+            if (c == null) {
+                long t0 = System.nanoTime();
+                try {
+                    if (parent != null) {
+                   // 如果parent不为null,则调用parent的loadClass进行加载
+                        c = parent.loadClass(name, false);
+                    } else {
+                    // parent为null,则调用BootstrapClassLoader进行加载  
+                        c = findBootstrapClassOrNull(name);
+                    }
+                } catch (ClassNotFoundException e) {
+                   // 如果从非空父类加载器中找不到类,则抛出ClassNotFoundException
+                }
+
+                if (c == null) {
+                // 如果仍然无法加载成功,则调用自身的findClass进行加载 
+                    long t1 = System.nanoTime();
+                    c = findClass(name);
+
+                   // 这是定义类加载器;记录统计数据
+                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
+                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
+                    sun.misc.PerfCounter.getFindClasses().increment();
+                }
+            }
+            if (resolve) {
+                resolveClass(c);
+            }
+            return c;
+        }
+    }
+

根据代码以及代码中的注释可以很清楚地了解整个过程: +先检查是否已经被加载过,如果没有则调用父加载器的loadClass()方法,如果父加载器为空则默认使用启动类加载器作为父加载器。 +如果父类加载器加载失败,则先抛出ClassNotFoundException,然后再调用自己的findClass()方法进行加载。

+

优势

+

使用这种模型来组织类加载器之间的关系的好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。 +例如java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。 +否则的话,如果不使用该模型的话,如果用户自定义一个java.lang.Object类且存放在classpath中,那么系统中将会出现多个Object类,应用程序也会变得很混乱。 +如果我们自定义一个rt.jar中已有类的同名Java类,会发现JVM可以正常编译,但该类永远无法被加载运行。

+
    +
  • 避免类的重复加载
  • +
  • 保护程序安全,防止核心API被随意篡改
  • +
+

沙箱安全机制

+

自定义string类,但是在加载自定义String类的时候会率先使用引导类加载器加载,而引导类加载器在加载的过程中会先加载jdk自带的文件(rt.jar包中java\lang\String.class), +报错信息说没有main方法,就是因为加载的是rt.jar包中的string类。这样可以保证对java核心源代码的保护,这就是沙箱安全机制。

+

补充

+

判断两个class对象是否相同

+

在JVM中表示两个class对象是否为同一个类存在两个必要条件:

+
    +
  • 类的完整类名必须一致,包括包名。
  • +
  • 加载这个类的ClassLoader(指ClassLoader实例对象)必须相同。
  • +
+

换句话说,在JvM中,即使这两个类对象(class对象)来源同一个Class文件,被同一个虚拟机所加载, +但只要加载它们的ClassLoader实例对象不同,那么这两个类对象也是不相等的。

+

JVM必须知道一个类型是由启动加载器加载的还是由用户类加载器加载的。如果一个类型是由用户类加载器加载的,那么JVM会将这个类加载器的一个引用作为类型信息的一部分保存在方法区中。 +当解析一个类型到另一个类型的引用的时候,JVM需要保证这两个类型的类加载器是相同的。

+

类的主动使用和被动使用

+

Java程序对类的使用方式分为:王动使用和被动使用。 主动使用,又分为七种情况:

+
    +
  • 创建类的实例
  • +
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • +
  • 调用类的静态方法I
  • +
  • 反射(比如:Class.forName(“com.atguigu.Test”))
  • +
  • 初始化一个类的子类
  • +
  • Java虚拟机启动时被标明为启动类的类
  • +
  • JDK7开始提供的动态语言支持:
  • +
  • java.lang.invoke.MethodHandle实例的解析结果REF getStatic、REF putStatic、REF invokeStatic句柄对应的类没有初始化,则初始化
  • +
+

除了以上七种情况,其他使用Java类的方式都被看作是对类的被动使用,都不会导致类的初始化。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/jvm/jvm-direct-memory/index.html b/blog-site/public/posts/jvm/jvm-direct-memory/index.html new file mode 100644 index 00000000..f30e9132 --- /dev/null +++ b/blog-site/public/posts/jvm/jvm-direct-memory/index.html @@ -0,0 +1,353 @@ + + + + + + + + + + + JVM-直接内存 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JVM-直接内存

+ 2021.04.14 +
+

直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。 +直接内存是在Java堆外的、直接向系统申请的内存区间。

+

操作直接内存演示代码:

+
public class MainTest {
+    public static void main(String[] args) {
+        ByteBuffer allocate = ByteBuffer.allocate(1024 * 1024 * 1024);
+
+        System.out.println("直接内存分配完成 ...");
+        Scanner scanner = new Scanner(System.in);
+        scanner.next();
+
+        System.out.println("直接内存开始释放");
+        System.gc();
+
+        scanner.next();
+        System.out.println("退出!");
+    }
+}
+

使用NIO,通过存在堆中的直接内存操作本地内存

+

IO读写 +NIO读写

+

通常,访问直接内存的速度会优于Java堆。即读写性能高。因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。 +Java的NIO库允许Java程序使用直接内存,用于数据缓冲区。

+

直接内存也存在 OutOfMemoryError 异常:OutOfMemoryError: Direct buffer memory

+
public class MainTest {
+    private static final int BUFFER = 1024 * 1024 * 20;
+    public static void main(String[] args) {
+        ArrayList<ByteBuffer> list = new ArrayList<>();
+        int count = 0;
+        try {
+            while(true){
+                ByteBuffer byteBuffer = ByteBuffer.allocateDirect(BUFFER);
+                list.add(byteBuffer);
+                count++;
+                try {
+                    Thread.sleep(100);
+                } catch (InterruptedException e) {
+                    e.printStackTrace();
+                }
+            }
+        } finally {
+            System.out.println(count);
+        }
+    }
+}
+

由于直接内存在Java堆外,因此它的大小不会直接受限于-Xmx指定的最大堆大小,但是系统内存是有限的, +Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。

+

直接内存缺点:

+
    +
  • 分配回收成本较高
  • +
  • 不受JVM内存回收管理
  • +
+

直接内存大小可以通过 MaxDirectMemorySize 设置,如果不指定,默认与堆的最大值-Xmx参数值一致.

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/jvm/jvm-execute-engine/index.html b/blog-site/public/posts/jvm/jvm-execute-engine/index.html new file mode 100644 index 00000000..0ee9fbc5 --- /dev/null +++ b/blog-site/public/posts/jvm/jvm-execute-engine/index.html @@ -0,0 +1,846 @@ + + + + + + + + + + + JVM-执行引擎 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JVM-执行引擎

+ 2021.04.15 +
+

概述

+

执行引擎是Java虚拟机核心的组成部分之一,属于JVM的下层,里面包括 解释器、及时编译器、垃圾回收器。

+

执行引擎

+

“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力, +其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的, +因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。

+

JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令, +它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。 +那么,如果想要让一个Java程序运行起来,执行引擎的任务就是将字节码指令 解释/编译 为对应平台上的本地机器指令才可以。 +简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。

+

所有的Java虚拟机的执行引擎输入,输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行过程。

+

工作流程

+

执行引擎负责将字节码指令翻译为cpu执行的命令。在执行过程中究竟执行什么样的字节码指令完全依赖于PC寄存器; +每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址; +当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。

+

执行引擎工作流程

+

执行过程

+

大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过图中的各个步骤:

+
+

1.前面橙色部分是生成字节码文件的过程,和JVM无关 +2.后面蓝色和绿色才是JVM需要考虑的过程

+
+

执行引擎执行过程

+

解释器&即时编译器

+

现在JVM在执行Java代码的时候,会将解释执行与编译执行二者结合起来进行。如今Java采用的是解释和编译混合的模式。

+

执行引擎获取到,由 javac 将源码编译成字节码文件.class 之后,然后在运行的时候通过解释器 interpreter 转换成最终的机器码。 +另外JVM平台支持一种叫作即时编译的技术。即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,这种方式可以使执行效率大幅度提升。 +解释器&即时编译器

+
+

为什么说Java是半解释半编译型语言? +最初的Java语言只有解释器,所以定位为“解释执行”还是比较准确的:先编译成字节码,再对字节码逐行用解释器解释执行; +后来Java也发展出来可以直接生成本地代码的编译器:JVM执行引擎中解释器和即时编译器共存的。故叫做半解释半编译型。

+
+

解释器

+

解释器(Interpreter):Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。

+

解释器真正意义上所承担的角色就是一个运行时“翻译者”,将字节码文件中的内容“翻译”为对应平台的本地机器指令执行。 +当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。

+

即时编译器

+

即时编译器(Just In Time Compiler):就是虚拟机将源代码直接编译成和本地机器平台相关的机器语言。

+

由于解释器在设计和实现上非常简单,因此除了Java语言之外,还有许多高级语言同样也是基于解释器执行的, +比如Python、 Perl、Ruby等。但是在今天,基于解释器执行已经沦落为低效的代名词,并且时常被一些C/C+ +程序员所调侃。

+

为了解决这个问题,JVM平台支持一种叫作即时编译的技术。 +即时编译的目的是避免函数被解释执行,而是将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。 +不过无论如何,基于解释器的执行模式仍然为中间语言的发展做出了不可磨灭的贡献。

+

为什么还需要再使用解释器

+

既然即时编译器执行效率比解释器执行效率高,那为什么还需要再使用解释器?

+

当程序启动后,解释器可以马上发挥作用,省去编译的时间,立即执行。(解释器响应速度比即时编译器速度快)编译器要想发挥作用,把代码编译成本地代码,需要一定的执行时间。 +但编译为本地代码后,即时编译器执行效率高。

+

在此模式下,当Java虚拟器启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。 +随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。

+

同时,解释执行在编译器进行激进优化不成立的时候,作为编译器的“逃生门”。

+
+

JRockit 虚拟机是砍掉了解释器,也就是只采及时编译器。那是因为 JRockit 只部署在服务器上, +一般已经有时间让他进行指令编译的过程了,对于响应来说要求不高,等及时编译器的编译完成后,就会提供更好的性能. +尽管 JRockit VM中程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。 +对于服务端应用来说,启动时间并非是关注重点,但对于那些看中启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一个平衡点。

+
+

即使编译器分类

+

JIT的编译器还分为了两种,分别是C1和C2,在HotSpot VM中内嵌有两个JIT编译器,分别为Client Compiler和Server Compiler; +但大多数情况下我们简称为C1编译器 和 C2编译器。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器。

+
    +
  • +

    client:指定Java虚拟机运行在Client模式下,并使用C1编译器; +C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。

    +
  • +
  • +

    server:指定Java虚拟机运行在server模式下,并使用C2编译器; +C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。

    +
  • +
+
+

选择Java HotSpot Server 虚拟机。64位版本的JDK只支持 Server VM,因此在这种情况下,该选项是隐式的。

+
+

在不同的编译器上有不同的优化策略,C1编译器上主要有方法内联,去虚拟化、冗余消除:

+
    +
  • 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程;
  • +
  • 去虚拟化:对唯一的实现樊进行内联;
  • +
  • 冗余消除:在运行期间把一些不会执行的代码折叠掉;
  • +
+

C2的优化主要是在全局层面,逃逸分析是优化的基础。 +基于逃逸分析在C2上有如下几种优化:

+
    +
  • 标量替换:用标量值代替聚合对象的属性值;
  • +
  • 栈上分配:对于未逃逸的对象分配对象在栈而不是堆;
  • +
  • 同步消除:清除同步操作,通常指synchronized;
  • +
+

分层编译策略:程序解释执行(不开启性能监控)可以触发C1编译,将字节码编译成机器码,可以进行简单优化; +也可以加上性能监控,C2编译会根据性能监控信息进行激进优化。

+

在Java7版本之后,一旦开发人员在程序中显式指定命令“-server"时,默认将会开启分层编译策略,由C1编译器和C2编译器相互协作共同来执行编译任务。

+

总的来说,C2编译器启动时长比C1慢,系统稳定执行以后,C2编译器执行速度远快于C1编译器

+

热点探测技术

+
+

关于编译器: +前端编译器:把 .java 文件转变成 .class 文件的过程; +后端编译器:把 .class 文件转变为 机器指令的过程;

+
+

是否需要启动即时编译器将字节码转换为机器指令,则需要根据代码的调用频率而定。 +一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为“热点代码”,即时编译器在运行时会针对那些被频繁调用的热点代码做出深度优化,将其直接编译为本地的机器指令,以此来提升程序的性能。 +由于这种编译方式发生在方法的执行过程中,因此被称之为栈上替换,或简称为OSR(On Stack Replacement)编译。

+

一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个标准? +必然需要一个明确的阈值,JIT编译器才会将这些“热点代码”编译为本地机器指令执行。这里主要依靠热点探测功能。

+

目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。 +HotSpot 将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器和回边计数器。

+
    +
  • 方法调用计数器用于统计方法的调用次数
  • +
  • 回边计数器则用于统计循环体执行的循环次数
  • +
+
方法调用计数器
+

这个计数器就用于统计方法被调用的次数,它的默认阈值在Client模式下是1500次,在Server模式下是10000次。超过这个阈值,就会触发即时编译。

+

这个阀值可以通过虚拟机参数 -XX:CompileThreshold 来设定。

+

当一个方法被调用时,会先检查该方法是否存在被即时编译器编译过的版本,如果存在,则优先使用编译后的本地代码来执行。 +如果不存在已被编译过的版本,则将此方法的调用计数器值加1,然后判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阀值; +如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求;否则就通过解释器执行。

+
回边计数器
+

它的作用是统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”(Back Edge)。 +跟方法调用计数器搭配使用,如何两者相加总和超过计数器的阀值,那么就会除法即时编译器。 +显然,建立回边计数器统计的目的就是为了触发栈上替换编译。

+
热点衰减
+

如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,可理解为一段时间之内方法被调用的次数。 +当超过一定的时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被减少一半, +这个过程称为方法调用计数器热度的衰减,而这段时间就称为此方法统计的半衰周期。

+

可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。

+

进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数 -XX:-UseCounterDecay 来关闭热度衰减,让方法计数器统计方法调用的绝对次数; +这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。

+

解释器编译器切换

+

缺省情况下HotSpot VM是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。

+
    +
  • -Xint:完全采用解释器模式执行程序;
  • +
  • -Xcomp:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行;
  • +
  • -Xmixed:采用解释器+即时编译器的混合模式共同执行程序。
  • +
+

Hotspot编译器解释器切换

+

静态提前编译器

+

JDK9 引入了静态提前编译器(Ahead of Time Compiler)。

+

Java 9引入了实验性AOT编译工具AOTC。它借助了Graal编译器,将所输入的Java类文件转换为机器码,并存放至生成的动态共享库之中。

+
+

静态提前编译器: 直接把 .java 文件编译成机器指令的过程。 +.java -> .class -> (使用jaotc) -> .so

+
+

所谓AOT编译,是与即时编译相对立的一个概念。 +即时编译指的是在程序的运行过程中,将字节码转换为可在硬件上直接运行的机器码,并部署至托管环境中的过程。 +而AOT编译指的则是,在程序运行之前,便将字节码转换为机器码的过程。

+

优点:

+
    +
  • Java虚拟机加载已经预编译成二进制库,可以直接执行。不必等待及时编译器的预热,减少Java应用给人带来“第一次运行慢” 的不良体验;
  • +
+

缺点:

+
    +
  • 破坏了 java “ 一次编译,到处运行”,必须为每个不同的硬件,OS编译对应的发行包;
  • +
  • 降低了Java链接过程的动态性,加载的代码在编译器就必须全部已知;
  • +
  • 还需要继续优化中,最初只支持Linux X64 java base;
  • +
+

Graal编译器

+

自JDK10起,HotSpot又加入了一个全新的及时编译器:Graal编译器; +编译效果短短几年时间就追评了G2编译器,未来可期。

+

特点:

+
    +
  • 高效能运行 Java: 使用 GraalVM 执行 Java 程序可以变得更快;
  • +
  • 多语言并行:可以在 Java 里面同时使用多种语言,像是 JavaScript等;
  • +
  • 快速启动:直接把 Java 应用编译成机器码,执行起来体积更小、启动速度更快;
  • +
+

计算机指令演变过程

+
+

二进制编码 –> 指令、指令集 –> 汇编语言 –> 高级语言 –> ?

+
+

用二进制编码方式表示的指令,叫做机器指令码。最开始,人们就用它采编写程序,这就是机器语言。 +机器语言虽然能够被计算机理解和接受,但和人们的语言差别太大,不易被人们理解和记忆,并且用它编程容易出差错。

+

由于机器码是有0和1组成的二进制序列,可读性实在太差,于是人们发明了指令。 +指令就是把机器码中特定的0和1序列,简化成对应的指令(一般为英文简写,如mov,inc等),可读性稍好。

+

不同的硬件平台,各自支持的指令,是有差别的。因此每个平台所支持的指令,称之为对应平台的指令集。

+

由于指令的可读性还是太差,于是人们又发明了汇编语言。 +由于计算机只认识指令码,所以用汇编语言编写的程序还必须翻译成机器指令码,计算机才能识别和执行。

+

为了使计算机用户编程序更容易些,后来就出现了各种高级计算机语言。 +高级语言比机器语言、汇编语言更接近人的语言当计算机执行高级语言编写的程序时,仍然需要把程序解释和编译成机器的指令码。

+

高级语言也不是直接翻译成机器指令,而是翻译成汇编语言,在由汇编语言翻译成机器指令;当然也可以先翻译为字节码,在由字节码翻译为机器指令。

+
+

字节码: +字节码是一种中间状态(中间码)的二进制代码(文件),它比机器码更抽象,需要直译器转译后才能成为机器码。 +字节码主要为了实现特定软件运行和软件环境、与硬件环境无关。 +实现方式:编译器将源码编译成字节码,特定平台上的虚拟机器将字节码转译为可以直接执行的指令。

+
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/jvm/jvm-heap/index.html b/blog-site/public/posts/jvm/jvm-heap/index.html new file mode 100644 index 00000000..22e4b277 --- /dev/null +++ b/blog-site/public/posts/jvm/jvm-heap/index.html @@ -0,0 +1,1715 @@ + + + + + + + + + + + JVM-堆 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JVM-堆

+ 2021.04.03 +
+

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 +另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

+

运行时数据区

+

运行时数据区域包括

+
    +
  • 程序计数寄存器
  • +
  • 虚拟机栈
  • +
  • 本地方法栈
  • +
  • +
  • 方法区
  • +
+

其中:方法区、堆为线程共享;程序计数寄存器、虚拟机栈、本地方法栈 为线程私有。

+

+

堆的核心概念

+

堆针对一个JVM进程来说是唯一的,也就是一个进程只有一个JVM,但是进程包含多个线程,他们是共享同一堆空间的。 +一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。

+

Java堆区在JVM启动的时候即被创建,其空间大小也就确定了。是JVM管理的最大一块内存空间。堆内存的大小是可以调节的。

+
+

-Xms10m:最小堆内存

+

-Xmx10m:最大堆内存

+
+

《Java虚拟机规范》规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。 +所有的线程共享Java堆,在这里还可以划分线程私有的缓冲区(Thread Local Allocation Buffer,TLAB)。

+

《Java虚拟机规范》中对Java堆的描述是:所有的对象实例以及数组都应当在运行时分配在堆上。

+
+

The heap is the run-time data area from which memory for all class instances and arrays is allocated

+
+

“几乎”所有的对象实例都在这里分配内存。—从实际使用角度看的。因为还有一些对象是在栈上分配的。

+

数组和对象可能永远不会存储在栈上,因为栈帧中保存引用,这个引用指向对象或者数组在堆中的位置。

+

在方法结束后,堆中的对象不会马上被移除,仅仅在垃圾收集的时候才会被移除。

+
    +
  • 也就是触发了GC的时候,才会进行回收
  • +
  • 如果堆中对象马上被回收,那么用户线程就会收到影响,一个方法频繁的调用频繁的回收程序性能会收到影响
  • +
+

堆,是GC(Garbage Collection,垃圾收集器)执行垃圾回收的重点区域。 +堆内存分配

+

堆内存细分

+

堆内存划分

+

Java 7及之前堆内存逻辑上分为三部分:新生区+养老区+永久区

+
    +
  • Young Generation Space 新生区 Young/New 又被划分为Eden区和Survivor区
  • +
  • Tenure generation space 养老区
  • +
  • Permanent Space永久区
  • +
+

Java 8及之后堆内存逻辑上分为三部分:新生区+养老区+元空间

+
    +
  • Young Generation Space新生区 Young/New 又被划分为Eden区和Survivor区
  • +
  • Tenure generation space 养老区
  • +
  • Meta Space 元空间
  • +
+

堆空间内部结构,JDK1.8之前从 永久代 替换成 元空间。

+
+ jvm1.8之前 +
+
+ jvm1.8 +
+

设置堆内存大小

+

Java堆区用于存储Java对象实例,那么堆的大小在JVM启动时就已经设定好了,可以通过选项"-Xmx"和"-Xms"来进行设置。

+
+

“-Xms"用于表示堆区的起始内存,等价于 -XX:InitialHeapSize +“-Xmx"则用于表示堆区的最大内存,等价于 -XX:MaxHeapSize

+
+

设置堆大小

+

默认情况下,初始堆内存大小:物理电脑内存大小/64;最大堆内存大小:物理电脑内存大小/4

+
+

在生产环境和开发环境,通常会将-Xms和-Xmx两个参数配置相同的值, +其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。

+
+

使用代码查看

+
public class MainTest {
+    public static void main(String[] args) {
+        // 返回Java虚拟机中的堆内存总量
+        long initialMemory = Runtime.getRuntime().totalMemory() / 1024 / 1024;
+        // 返回Java虚拟机试图使用的最大堆内存
+        long maxMemory = Runtime.getRuntime().maxMemory() / 1024 / 1024;
+        System.out.println("-Xms:" + initialMemory + "M");
+        System.out.println("-Xmx:" + maxMemory + "M");
+    }
+}
+

查看堆内存大小

+

-XX:+PrintGCDetails

+

程序启动加入-XX:+PrintGCDetails参数

+

-XXPrintGCDetails

+

PrintGCDetails查看堆内存

+

jstat -gc

+
+

jps -> jstat -gc 进程ID

+
+

jstat

+

OOM

+

一旦堆区中的内存大小超过“-Xmx"所指定的最大内存时,将会抛出outOfMemoryError异常。

+

代码实现

+
public class MainTest {
+    public static void main(String[] args) {
+        ArrayList<Object> list = new ArrayList<>();
+        while (true){
+//            try {
+//                Thread.sleep(1000000);
+//            } catch (InterruptedException e) {
+//                e.printStackTrace();
+//            }
+            list.add(new Picture(new Random().nextInt(1024 * 1024)));
+        }
+    }
+}
+
+class Picture {
+    private int data;
+    public Picture(int data) {
+        this.data = data;
+    }
+}
+

OOM

+
+

jvisual 工具在 jdk /bin/jvisualvm.exe +亦可在编译器 下载jvisual插件

+
+

出现OOM错误后,可以通过 VisualVM 这个工具查看具体是什么参数造成的 +jvisual-OOM

+

年轻代与老年代

+

存储在JVM中的Java对象,按照生命周期可以被划分为两类:

+

一类是生命周期较短的瞬时对象,这类对象的创建和消亡都非常迅速,生命周期短的,及时回收; +另外一类对象的生命周期却非常长,在某些极端的情况下还能够与JVM的生命周期保持一致。

+

Java堆区进一步细分的话,可以划分为年轻代和老年代。 +其中年轻代又可以划分为Eden区、Survivor0区和 Survivor1 区(有时也叫做from区、to区)。 +年轻代与老年代

+
+

没有明确规定,to 区是 Survivor1;这两个区域是不断进行交换的;是从一个区到另外一个区

+
+

测试用的代码,用于测试以下JVM参数

+
public class MainTest {
+    public static void main(String[] args) {
+        System.out.println("start ...");
+        try {
+            Thread.sleep(1000000);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+    }
+}
+

-XX:NewRatio

+

该参数是配置新生代与老年代在堆结构的占比。

+

默认情况下,新生代:老年代 - > 1 : 2

+
    +
  • 默认-XX:NewRatio=2;表示新生代占1,老年代占2,新生代占整个堆的1/3
  • +
  • 可以修改-XX:NewRatio=4; 表示新生代占1,老年代占4,新生代占整个堆的1/5 +NewRatio
  • +
+

当发现在整个项目中,生命周期长的对象偏多,那么就可以通过调整年轻代与老年代的比例,来进行调优。

+

-XX:SurvivorRatio

+

该命令是调整eden区与survivor区比例。这个参数一般使用默认值就可以了。

+

在HotSpot中,Eden空间和另外两个survivor空间缺省所占的比例是8:1:1, +当然开发人员可以通过选项“-XX:SurvivorRatio”调整这个空间比例。比如:-XX:SurvivorRatio=8。

+
+

PS 在实际开发中使用hotspot虚拟机,默认情况下不是8:1:1; +是因为虚拟机有一个自适应内存分配策略,可以通过-XX:-UseAdaptiveSizePolicy关闭再来进行查看. +SurvivorRatio

+
+

几乎所有的Java对象都是在Eden区被new出来的。绝大部分的Java对象的销毁都在新生代进行了。(有些大的对象在Eden区无法存储时候,将直接进入老年代)

+

IBM公司的专门研究表明,新生代中80%的对象都是“朝生夕死”的。

+

可以使用选项"-Xmn"设置新生代最大内存大小。

+
+

PS 当 -Xmn 参数与 -XX:NewRatio 设置的值发生冲突时,会以 -Xmn 设置的具体值为准。

+
+

eden与survivor与tenured

+

为对象分配内存

+

为新对象分配内存是一件非常严谨和复杂的任务,JM的设计者们不仅需要考虑内存如何分配、在哪里分配等问题, +并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执行完内存回收后是否会在内存空间中产生内存碎片。

+

对象分配内存步骤: +对象内存分配策略

+
    +
  1. 新的对象先放伊甸园区。此区有大小限制。(如果对象过大可能直接分配在老年代)
  2. +
  3. 当伊甸园的空间填满时,程序又需要创建对象,JVM的垃圾回收器将对伊甸园区进行MinorGC,将伊甸园区中的不再被其他对象所引用的对象进行销毁。 +再将新的对象放到伊甸园区。
  4. +
  5. 然后将伊甸园中的剩余对象移动到幸存者0区。
  6. +
  7. 如果再次触发MinorGC,会首先将没有被回收的对象放到幸存者1区,然后判断幸存者0区中的对象是否能被回收,如果没有回收,就会放到幸存者1区。
  8. +
  9. 重复步骤3、4,默认情况下如果一个对象被扫描了15次(阈值),都不能被回收,则将该对象晋升到老年代。
  10. +
  11. 当老年代内存不足时,再次触发GC:Major GC,进行老年代的内存清理。
  12. +
  13. 若老年代执行了Major GC之后,发现依然无法进行对象的保存,就会产生OOM错误。
  14. +
+

可以用 -XX:MaxTenuringThreshold=N 进行设置幸存者区到老年代的GC扫描次数,默认15次。

+
+

PS 如果幸存者区满了? +如果Survivor区满了后,将会触发一些特殊的规则,也就是可能直接晋升老年代。 +需要特别注意,在Eden区满了的时候,才会触发MinorGC,而幸存者区满了后,不会触发MinorGC操作

+
+

代码演示对象分配过程

+
// -Xms600m -Xmx600m
+public class HeapInstanceTest {
+    byte [] buffer = new byte[new Random().nextInt(1024 * 200)];
+    public static void main(String[] args) throws InterruptedException {
+        ArrayList<HeapInstanceTest> list = new ArrayList<>();
+        while (true) {
+            list.add(new HeapInstanceTest());
+            Thread.sleep(10);
+        }
+    }
+}
+

打开VisualVM图形化界面,通过VisualGC进行动态化查看 +代码演示对象分配过程

+
+

总结:

+
    +
  • 针对幸存者s0,s1区的总结:复制之后有交换,谁空谁是to区
  • +
  • 关于垃圾回收:频繁在新生区收集,很少在老年代收集,几乎不再永久代和元空间进行收集
  • +
  • 新生代采用复制算法的目的:是为了减少内碎片
  • +
+
+

GC简单介绍

+
    +
  • Minor GC:新生代的GC
  • +
  • Major GC:老年代的GC
  • +
  • Full GC:整堆收集,收集整个Java堆和方法区的垃圾
  • +
+

JVM的调优的一个环节,也就是垃圾收集,我们需要尽量的避免垃圾回收,因为在垃圾回收的过程中,容易出现 STW 的问题; +而 Major GC 和 Full GC出现 STW 的时间,是Minor GC的10倍以上。

+
+

STW: Java中 Stop-The-World 机制简称 STW ,是在执行垃圾收集算法时,Java 应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。 +Java中一种全局暂停现象,全局停顿,所有 Java 代码停止,native 代码可以执行,但不能与 JVM 交互;这些现象多半是由于 GC 引起。

+
+

JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。

+

针对Hotspot VM的实现,它里面的GC按照回收区域又分为两大种类型:一种是部分收集(Partial GC),一种是整堆收集(FullGC)。

+
    +
  • 部分收集:不是完整收集整个Java堆的垃圾收集。其中又分为: +
      +
    • 新生代收集(MinorGC/YoungGC):只是新生代的垃圾收集
    • +
    • 老年代收集(MajorGC/OldGC):只是老年代的圾收集。目前,只有CMSGC会有单独收集老年代的行为。注意,很多时候Major GC会和FullGC混淆使用,需要具体分辨是老年代回收还是整堆回收。
    • +
    • 混合收集(MixedGC):收集整个新生代以及部分老年代的垃圾收集。目前,只有G1 GC会有这种行为
    • +
    +
  • +
  • 整堆收集(FullGC):收集整个Java堆和方法区的垃圾收集。
  • +
+

Minor GC

+

当年轻代空间不足时,就会触发MinorGC,这里的年轻代满指的是Eden代满,Survivor满了不会引发Minor GC。每次Minor GC会清理年轻代的垃圾。 +因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

+

Minor GC会引发STW,暂停其它用户的线程,等垃圾回收结束,用户线程才恢复运行。

+

Major GC

+

发生在老年代的GC,对象从老年代消失时,我们说 “Major GC” 或 “Full GC” 发生了。

+

出现了MajorGc,经常会伴随至少一次的Minor GC,但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行MajorGC的策略选择过程。

+

也就是在老年代空间不足时,会先尝试触发MinorGc。 +如果之后空间还不足,则触发Major GC,Major GC的速度一般会比MinorGc慢10倍以上,STW的时间更长,如果Major GC后,内存还不足,就报OOM了。

+

Full GC

+

触发Full GC执行的情况有如下五种:

+
    +
  1. 调用System.gc()时,系统建议执行Full GC,但是不必然执行.
  2. +
  3. 老年代空间不足. +
      +
    • 通过Minor GC后进入老年代的平均大小,大于老年代的可用内存.也就是老年代空间不足。
    • +
    • 由Eden区、survivor space(From Space)区向survivor space(To Space)区复制时,对象大小大于To Space可用内存, +则把该对象转存到老年代,且老年代的可用内存小于该对象大小. 也就是老年代空间不足。
    • +
    +
  4. +
  5. 方法区空间不足.
  6. +
+

GC举例

+

测试GC代码

+
public class MainTest {
+    public static void main(String[] args) {
+        int i = 0;
+        try {
+            List<String> list = new ArrayList<>();
+            String a = "awsl";
+            while(true) {
+                list.add(a);
+                a = a + a;
+                i++;
+            }
+        }catch (Exception e) {
+            e.getStackTrace();
+        }
+    }
+}
+

加入如下虚拟机参数

+
-Xms10m -Xmx10m -XX:+PrintGCDetails
+

GC 日志

+
[GC (Allocation Failure) [PSYoungGen: 1933K->496K(2560K)] 1933K->736K(9728K), 0.0009799 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
+[GC (Allocation Failure) [PSYoungGen: 2476K->480K(2560K)] 2716K->1464K(9728K), 0.0014628 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
+[Full GC (Ergonomics) [PSYoungGen: 2156K->0K(2560K)] [ParOldGen: 7128K->4559K(7168K)] 9284K->4559K(9728K), [Metaspace: 3029K->3029K(1056768K)], 0.0033635 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
+[GC (Allocation Failure) [PSYoungGen: 56K->128K(2560K)] 6663K->6735K(9728K), 0.0009897 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] 
+[Full GC (Ergonomics) [PSYoungGen: 128K->0K(2560K)] [ParOldGen: 6607K->6509K(7168K)] 6735K->6509K(9728K), [Metaspace: 3047K->3047K(1056768K)], 0.0040646 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
+[GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] 6509K->6509K(9728K), 0.0006842 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
+[Full GC (Allocation Failure) [PSYoungGen: 0K->0K(2560K)] [ParOldGen: 6509K->6491K(7168K)] 6509K->6491K(9728K), [Metaspace: 3047K->3047K(1056768K)], 0.0039890 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
+Heap
+ PSYoungGen      total 2560K, used 111K [0x00000007bfd00000, 0x00000007c0000000, 0x00000007c0000000)
+  eden space 2048K, 5% used [0x00000007bfd00000,0x00000007bfd1bf38,0x00000007bff00000)
+  from space 512K, 0% used [0x00000007bff80000,0x00000007bff80000,0x00000007c0000000)
+  to   space 512K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007bff80000)
+ ParOldGen       total 7168K, used 6491K [0x00000007bf600000, 0x00000007bfd00000, 0x00000007bfd00000)
+  object space 7168K, 90% used [0x00000007bf600000,0x00000007bfc56f18,0x00000007bfd00000)
+ Metaspace       used 3093K, capacity 4496K, committed 4864K, reserved 1056768K
+  class space    used 338K, capacity 388K, committed 512K, reserved 1048576K
+Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
+

堆空间分代思想

+

为什么要把Java堆分代?不分代就不能正常工作了吗?经研究,不同对象的生命周期不同。70%-99% 的对象是临时对象。

+

不分代完全可以,分代的唯一理由就是优化GC性能。 +如果没有分代,那所有的对象都在一块,GC的时候要找到哪些对象没用,这样就会对堆的所有区域进行扫描。比较耗费性能。 +而很多对象都是朝生夕死的,如果分代的话,把新创建的对象放到某一地方,当GC的时候先把这块存储“朝生夕死”对象的区域进行回收,这样就会腾出很大的空间出来。

+

内存分配策略

+

如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到survivor空间中,并将对象年龄设为1。 +对象在survivor区中每熬过一次MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,其实每个JVM、每个GC都有所不同)时,就会被晋升到老年代。

+
+

PS: 对象晋升老年代的年龄阀值,可以通过选项 -XX:MaxTenuringThreshold 来设置

+
+

针对不同年龄段的对象分配原则:

+
    +
  • +

    优先分配到Eden。 +但是开发中比较长的字符串或者数组,会直接存在老年代。因为新创建的对象都是朝生夕死的,所以这个大对象可能也很快被回收,由于老年代触发Major GC的次数比 Minor GC要更少,因此可能回收起来就会比较慢

    +
  • +
  • +

    大对象直接分配到老年代。 +尽量避免程序中出现过多的大对象

    +
  • +
  • +

    长期存活的对象分配到老年代。

    +
  • +
  • +

    动态对象年龄判断。 +如果Survivor区中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代,无须等到 MaxTenuringThreshold 中要求的年龄。

    +
  • +
  • +

    空间分配担保。 +就是经过Minor GC后,所有的对象都存活,因为Survivor比较小,所以就需要将Survivor无法容纳的对象,存放到老年代中。通过-XX:HandlePromotionFailure参数来调节。

    +
  • +
+

TLAB

+
+

堆空间都是共享的么? +不是,因为还有 TLAB 这个概念,在堆中划分出一块区域,为每个线程所独占,以此来保证线程安全。

+
+

TLAB全称:Thread Local Allocation Buffer 译为:线程本地分配缓冲区。

+

因为堆区是线程共享区域,任何线程都可以访问到堆区中的共享数据, +由于对象实例的创建在JVM中非常频繁,因此在并发环境下从堆区中划分内存空间是线程不安全的。 +为避免多个线程操作同一地址,需要使用加锁等机制,进而影响分配速度。 +使用锁又会影响性能,TLAB应运而生。 +多线程同时分配内存时,使用TLAB可以避免一系列的非线程安全问题,同时还能够提升内存分配的吞吐量,因此我们可以将这种内存分配方式称之为快速分配策略。

+

TLAB

+

从内存模型而不是垃圾收集的角度,对Eden区域继续进行划分,JVM为每个线程分配了一个私有缓存区域,它包含在Eden空间内。 +默认情况下,TLAB空间的内存非常小,仅占有整个Eden空间的1%,当然我们可以通过选项-XX:TLABWasteTargetPercent设置TLAB空间所占用Eden空间的百分比大小。

+

对象首先是通过TLAB开辟空间,如果不能放入,那么需要通过Eden来进行分配。 +尽管不是所有的对象实例都能够在TLAB中成功分配内存,但JVM确实是将TLAB作为内存分配的首选。 +可以通过选项-XX:UseTLAB设置是否开启TLAB空间,默认是开启的。 +一旦对象在TLAB空间分配内存失败时,JVM就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden空间中分配内存。

+

TLAB分配过程.png

+

JVM堆空间的参数设置

+
    +
  • +

    -XX:+PrintFlagsInitial:查看所有的参数的默认初始值

    +
  • +
  • +

    -XX:+PrintFlagsFinal:查看所有的参数的最终值(可能会存在修改,不再是初始值)

    +
  • +
  • +

    -Xms:初始堆空间内存(默认为物理内存的1/64)

    +
  • +
  • +

    -Xmx:最大堆空间内存(默认为物理内存的1/4)

    +
  • +
  • +

    -Xmn:设置新生代的大小。(初始值及最大值)

    +
  • +
  • +

    -XX:NewRatio:配置新生代与老年代在堆结构的占比(默认是2)

    +
  • +
  • +

    -XX:SurvivorRatio:设置新生代中Eden和S0/S1空间的比例(默认是8)

    +
  • +
  • +

    -XX:MaxTenuringThreshold:设置新生代垃圾的最大年龄((默认是15)

    +
  • +
  • +

    -XX:+PrintGCDetails:输出详细的GC处理日志

    +
  • +
  • +

    -XX:+PrintGC - verbose:gc 打印gc简要信息

    +
  • +
  • +

    -XX:HandlePromotionFailure:是否设置空间分配担保(默认true)

    +
  • +
+
+

在发生Minor GC之前,虚拟机会检查老年代最大可用的连续空间是否大于新生代所有对象的总空间。 +如果大于,则此次Minor GC是安全的。 +如果小于,则虚拟机会查看-xx:HandlePromotionFailure设置值是否允担保失败。 +如果HandlePromotionFailure=true,那么会继续检查老年代最大可用连续空间是否大于历次晋升到老年代的对象的平均大小。 +如果大于,则尝试进行一次Minor GC,但这次Minor GC依然是有风险的; +如果小于,则改为进行一次FullGC。 +如果HandlePromotionFailure=false,则改为进行一次Full Gc。 +在JDK7之后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略,观察openJDK中的源码变化,虽然源码中还定义了HandlePromotionFailure参数,但是在代码中已经不会再使用它。 +JDK7之后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行FullGC。

+

简而言之,在JDK7之后 -XX:HandlePromotionFailure=true 默认为true,且不会受到分配担保策略。

+
+

逃逸分析

+
+

随着JIT编译期的发展与逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术将会导致一些微妙的变化,所有的对象都分配到堆上也渐渐变得不那么“绝对”了。

+
+

在Java虚拟机中,对象是在Java堆中分配内存的,这是一个普遍的常识。 +但是,有一种特殊情况,那就是如果经过逃逸分析后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。 +这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。

+

还有基于openJDK深度定制的TaoBaoVM,其中创新的GCIH(GC invisible heap)技术实现off-heap: +将生命周期较长的Java对象从heap中移至heap外,并且GC不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升GC的回收效率的目的。

+

逃逸分析是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。 +通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。 +逃逸分析的基本行为就是分析对象动态作用域:

+
    +
  • 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
  • +
  • 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。例如作为调用参数传递到其他地方中。
  • +
+

逃逸分析举例

+

没有发生逃逸出方法的对象,则可以分配到栈上,随着方法执行的结束,栈空间就被移除,每个栈里面包含了很多栈帧,也就是发生逃逸分析

+
public static StringBuffer createStringBuffer(String s1, String s2) {
+    StringBuffer sb = new StringBuffer();
+    sb.append(s1);
+    sb.append(s2);
+    return sb;
+}
+

如果想要StringBuffer sb对象不发生逃逸方法,则发生逃逸分析,可以这样写

+
public static String createStringBuffer(String s1, String s2) {
+    StringBuffer sb = new StringBuffer();
+    sb.append(s1);
+    sb.append(s2);
+    return sb.toString();
+}
+
+

如何快速的判断是否发生了逃逸分析,看new的对象实体是否在方法外被调用。

+
+

参数设置

+

在JDK 1.7 版本之后,HotSpot中默认就已经开启了逃逸分析

+

如果使用的是较早的版本,则可以通过:

+
    +
  • 选项“-XX:+DoEscapeAnalysis"显式开启逃逸分析
  • +
  • 通过选项“-XX:+PrintEscapeAnalysis"查看逃逸分析的筛选结果
  • +
+

在开发中能使用局部变量的,就不要使用在方法外定义。

+

栈上分配

+

将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈上分配的候选,而不是堆上分配。

+

JIT即时编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。 +分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。

+

代码演示

+
/**
+ * 通过代码来演示,逃逸分析前,逃逸分析后的变化情况
+ * 逃逸分析前虚拟机参数: -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
+ * 逃逸分析后虚拟机参数: -Xmx1G -Xms1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
+ */
+public class MainTest {
+    public static void main(String[] args) throws InterruptedException {
+        long start = System.currentTimeMillis();
+        for (int i = 0; i < 100000000; i++) {
+            alloc();
+        }
+        long end = System.currentTimeMillis();
+        System.out.println("花费的时间为:" + (end - start) + " ms");
+
+        // 为了方便查看堆内存中对象个数,线程sleep
+        Thread.sleep(10000000);
+    }
+
+    private static void alloc() {
+        // 未发生逃逸
+        User user = new User();
+    }
+}
+class User {
+    private String name;
+    private String age;
+    private String gender;
+    private String phone;
+}
+

逃逸分析之前

+
花费的时间为:881 ms
+

栈上分配逃逸分析前

+

逃逸分析之后

+
花费的时间为:5 ms
+

栈上分配逃逸分析后

+

同步省略

+

如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

+

线程同步的代价是相当高的,同步的后果是降低并发性和性能。

+

在动态编译同步块的时候,JIT编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问而没有被发布到其他线程。 +如果没有,那么JIT编译器在编译这个同步块的时候就会取消对这部分代码的同步。这样就能大大提高并发性和性能。这个取消同步的过程就叫同步省略,也叫锁消除。

+

代码演示

+
public void func() {
+    Object obj = new Object();
+    synchronized(obj) {
+        System.out.println(obj);
+    }
+}
+

当多个线程同时进来,每个线程都会重新new Object() 不会发生线程安全问题;还有obj对象的生命周期只在func()方法中,并不会被其他线程所访问到. +所以在JIT编译阶段就会被优化掉,提高效率。

+
public void func() {
+    Object obj = new Object();
+	System.out.println(obj);
+}
+

分离对象或标量替换

+

标量是指一个无法再分解成更小的数据的数据。Java中的原始数据类型就是标量。 +相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。

+

在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化, +就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。这个过程就是标量替换。

+

有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

+

代码演示

+
public static void main(String args[]) {
+    alloc();
+}
+class Point {
+    private int x;
+    private int y;
+}
+private static void alloc() {
+    Point point = new Point(1,2);
+    System.out.println("point.x" + point.x + ";point.y" + point.y);
+}
+

经过标量替换后

+
private static void alloc() {
+    int x = 1;
+    int y = 2;
+    System.out.println("point.x = " + x + "; point.y=" + y);
+}
+

这样做的好处是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。 标量替换为栈上分配提供了很好的基础。

+

逃逸分析的不足

+

关于逃逸分析的论文在1999年就已经发表了,但直到JDK1.6才有实现,而且这项技术到如今也并不是十分成熟。

+

其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。 +虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。 +一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。

+

虽然这项技术并不十分成熟,但是它也是即时编译器优化技术中一个十分重要的手段。 +注意到有一些观点,认为通过逃逸分析,JVM会在栈上分配那些不会逃逸的对象,这在理论上是可行的,但是取决于JvM设计者的选择。 +oracle Hotspot JVM中并未这么做,这一点在逃逸分析相关的文档里已经说明,所以可以明确所有的对象实例都是创建在堆上。

+

目前很多书籍还是基于JDK7以前的版本,JDK已经发生了很大变化,intern字符串的缓存和静态变量曾经都被分配在永久代上,而永久代已经被元数据区取代。 +但是,intern字符串缓存和静态变量并不是被转移到元数据区,而是直接在堆上分配,所以这一点同样符合前面一点的结论:对象实例都是分配在堆上

+

总结

+

年轻代是对象的诞生、成长、消亡的区域,一个对象在这里产生、应用,最后被垃圾回收器收集、结束生命。

+

老年代放置长生命周期的对象,通常都是从survivor区域筛选拷贝过来的Java对象。 +当然,也有特殊情况,我们知道普通的对象会被分配在TLAB上;如果对象较大,JVM会试图直接分配在Eden其他位置上; +如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM就会直接分配到老年代。当GC只发生在年轻代中,回收年轻代对象的行为被称为MinorGC。

+

当GC发生在老年代时则被称为MajorGc或者FullGC。一般的,MinorGc的发生频率要比MajorGC高很多,即老年代中垃圾回收发生的频率将大大低于年轻代。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/jvm/jvm-method-area/index.html b/blog-site/public/posts/jvm/jvm-method-area/index.html new file mode 100644 index 00000000..24f82ab9 --- /dev/null +++ b/blog-site/public/posts/jvm/jvm-method-area/index.html @@ -0,0 +1,1289 @@ + + + + + + + + + + + JVM-方法区 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JVM-方法区

+ 2021.04.08 +
+

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 +另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

+

运行时数据区

+

运行时数据区域包括

+
    +
  • 程序计数寄存器
  • +
  • 虚拟机栈
  • +
  • 本地方法栈
  • +
  • +
  • 方法区
  • +
+

其中:方法区、堆为线程共享;程序计数寄存器、虚拟机栈、本地方法栈 为线程私有。

+

方法区

+

对于方法区的理解

+
+

尽管所有的方法区在逻辑上是属于堆的一部分,但一些简单的实现可能不会选择去进行垃圾收集或者进行压缩。 +”但对于HotSpotJVM而言,方法区还有一个别名叫做Non-Heap(非堆),目的就是要和堆分开。

+
+

方法区看作是一块独立于Java堆的内存空间。下图说明了栈、堆、方法区的交互关系 +方法区定位

+
    +
  • 方法区主要存放的是 class,而堆中主要存放的是实例化的对象.
  • +
  • 方法区与Java堆一样,是各个线程共享的内存区域。
  • +
  • 方法区在JVM启动的时候被创建,并且它的实际的物理内存空间中和Java堆区一样都可以是不连续的。
  • +
  • 方法区的大小,跟堆空间一样,可以选择固定大小或者可扩展。
  • +
  • 方法区的大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出, +虚拟机同样会抛出内存溢出错误:java.lang.OuOfMemoryError:PermGen space 或者java.lang.OutOfMemoryError:Metaspace
  • +
  • 关闭JVM就会释放这个区域的内存。
  • +
+

HotSpot中JDK7与JDK8

+

在JDK7及以前,习惯上把方法区,称为永久代。JDK8开始,使用元空间取代了永久代。 +JDK 1.8后,元空间存放在直接内存中。

+
+ jvm1.8之前 +
+
+ jvm1.8 +
+

元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代最大的区别在于:元空间不在虚拟机设置的内存中,而是使用本地内存。

+

永久代、元空间二者并不只是名字变了,内部结构也调整了。 +根据《Java虚拟机规范》的规定,如果方法区无法满足新的内存分配需求时,将抛出OOM异常。

+

设置方法区大小

+

方法区的大小不必是固定的,JVM可以根据应用的需要动态调整。

+

JDK7及之前

+

通过参数-XX:Permsize=size来设置永久代初始分配空间。默认值是20.75M +通过参数-XX:MaxPermsize=size来设定永久代最大可分配空间。32位机器默认是64M,64位机器模式是82M +当JVM加载的类信息容量超过了这个值,会报异常OuOfMemoryError:PermGen space

+

JDK8及之后

+

元空间大小可以使用参数 -XX:MetaspaceSize=size-XX:MaxMetaspaceSize=size 来指定。

+

默认值依赖于平台。windows下,-XX:MetaspaceSize是21M,-XX:MaxMetaspaceSize的值是-1,由于直接存放在直接内存中所以没有限制。

+

与永久代不同,如果不指定大小,默认情况下,虚拟机会耗尽所有的可用系统内存。 +如果元数据区发生溢出,虚拟机一样会抛出异常OutOfMemoryError:Metaspace

+

-XX:MetaspaceSize=size设置初始的元空间大小。对于一个64位的服务器端JVM来说,其默认的-XX:MetaspaceSize值为21MB。 +这就是初始的高水位线,一旦触及这个水位线,Ful1GC将会被触发并卸载没用的类即这些类对应的类加载器不再存活然后这个高水位线将会重置。 +新的高水位线的值取决于GC后释放了多少元空间。 +如果释放的空间不足,那么在不超过MaxMetaspaceSize时,适当提高该值。如果释放空间过多,则适当降低该值。

+

如果初始化的高水位线设置过低,上述高水位线调整情况会发生很多次。 +通过垃圾回收器的日志可以观察到Ful1GC多次调用。 +为了避免频繁地GC,建议将-XX:MetaspaceSize=size设置为一个相对较高的值。

+

OOM

+

JDK8 方法区/元空间 OOM代码演示

+

设置虚拟机参数

+
-XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
+
public class MainTest  extends ClassLoader{
+    public static void main(String[] args) {
+        MainTest mainTest = new MainTest();
+        int count = 0;
+        try {
+            for (int i = 0; i < 1000; i++) {
+
+                ClassWriter classWriter = new ClassWriter(0);
+                classWriter.visit(Opcodes.V1_8,Opcodes.ACC_PUBLIC ,"Class" + i,null,"java/lang/Object",null);
+
+                byte[] bytes = classWriter.toByteArray();
+                mainTest.defineClass("Class" + i, bytes, 0, bytes.length);
+                count ++;
+            }
+        }finally {
+            System.out.println(count);
+        }
+    }
+}
+

如何解决OOM?

+
    +
  1. +

    要解决ooM异常或heap space的异常,一般的手段是首先通过内存映像分析工具(如Ec1ipse Memory Analyzer)对dump出来的堆转储快照进行分析, +重点是确认内存中的对象是否是必要的,也就是要先分清楚到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow).

    +
  2. +
  3. +

    如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。 +于是就能找到泄漏对象是通过怎样的路径与GCRoots相关联并导致垃圾收集器无法自动回收它们的。 +掌握了泄漏对象的类型信息,以及GCRoots引用链的信息,就可以比较准确地定位出泄漏代码的位置。

    +
  4. +
+
+

内存泄漏: 有大量的引用指向某些对象,但是这些对象以后不会使用了,但是因为它们还和GC ROOT有关联,所以导致以后这些对象也不会被回收,这就是内存泄漏的问题

+
+
    +
  1. 如果不存在内存泄漏,换句话说就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms), +与机器物理内存对比看是否还可以调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的情况,尝试减少程序运行期的内存消耗。
  2. +
+

方法区的内部结构

+

方法区内部结构

+
+

方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

+
+

展示方法区内部结构,演示代码

+
public class MainTest {
+
+    private String string = "awsl";
+
+    public static void context() {
+        try {
+            int a = 0;
+            int b = 20/a;
+        }catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    private String context2() {
+        return string;
+    }
+
+    public static void main(String[] args) {
+        new MainTest().context2();
+        context();
+    }
+
+}
+

编译该类,找到该类的class文件,打开终端执行javap -v MainTest.class > test.txt命令,在当前目录会生成test.txt文件. +打开即可查看反编译后的字节码信息。

+

类型信息

+

对每个加载的类型(类class、接口interface、枚举enum、注解annotation),Jvm必须在方法区中存储以下类型信息:

+
    +
  • 这个类型的完整有效名称(包名.类名)
  • +
  • 这个类型直接父类的完整有效名(对于interface或是java.lang.object,都没有父类)
  • +
  • 这个类型的修饰符(public,abstract,final的某个子集)
  • +
  • 这个类型直接接口的一个有序列表
  • +
+
// ...
+public class content.posts.jvm.MainTest
+  minor version: 0
+  major version: 52
+  flags: ACC_PUBLIC, ACC_SUPER
+//...
+

域信息

+

JVM必须在方法区中保存类型的所有域的相关信息以及域的声明顺序。

+

域的相关信息包括:域名称、域类型、域修饰符(public,private,protected,static,final,volatile,transient的某个子集)

+
// ...
+Constant pool:
+   #1 = Methodref          #10.#35        // java/lang/Object."<init>":()V
+   #2 = String             #36            // awsl
+   #3 = Fieldref           #6.#37         // content/posts/jvm/MainTest.string:Ljava/lang/String;
+
+// ...
+

方法信息

+

JVM必须保存所有方法的以下信息,同域信息一样包括声明顺序:

+
    +
  • 方法名称
  • +
  • 方法的返回类型(或void)
  • +
  • 方法参数的数量和类型(按顺序)
  • +
  • 方法的修饰符(public,private,protected,static,final,synchronized,native,abstract的一个子集)
  • +
  • 方法的字节码(bytecodes)、操作数栈、局部变量表及大小(abstract和native方法除外)
  • +
  • 异常表(abstract和native方法除外)
  • +
+

每个异常处理的开始位置、结束位置、代码处理在程序计数器中的偏移地址、被捕获的异常类的常量池索引

+
 public static void context();
+    descriptor: ()V
+    flags: ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=2, locals=2, args_size=0
+         0: iconst_0
+         1: istore_0
+         2: bipush        20
+         4: iload_0
+         5: idiv
+         6: istore_1
+         7: goto          15
+        10: astore_0
+        11: aload_0
+        12: invokevirtual #5                  // Method java/lang/Exception.printStackTrace:()V
+        15: return
+       // 异常表
+      Exception table:
+         from    to  target type
+             0     7    10   Class java/lang/Exception
+       // 代码字节码指令行号对照表
+      LineNumberTable:
+        line 11: 0
+        line 12: 2
+        line 15: 7
+        line 13: 10
+        line 14: 11
+        line 16: 15
+        // 局部变量表
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            2       5     0     a   I
+           11       4     0     e   Ljava/lang/Exception;
+      StackMapTable: number_of_entries = 2
+        frame_type = 74 /* same_locals_1_stack_item */
+          stack = [ class java/lang/Exception ]
+        frame_type = 4 /* same */
+

non-final的类变量

+

静态变量和类关联在一起,随着类的加载而加载,他们成为类数据在逻辑上的一部分。

+

类变量被类的所有实例共享,即使没有类实例时,也可以访问它:

+
public class MainTest {
+
+    public static void main(String[] args) {
+        Test test = null;
+        // 相当于 Test.hello();
+        test.hello();
+        // Test.count;
+        System.out.println(test.count);
+    }
+}
+
+class Test {
+    public static int count = 1;
+    public static final int number = 2;
+
+    public static void hello() {
+        System.out.println("hello!");
+    }
+}
+

全局常量就是使用 static final 进行修饰

+

被声明为final的类变量的处理方法则不同,每个全局常量在编译的时候就会被分配了。

+

运行时常量池

+

字节码指令解析

+

代码如下

+
public class MethodAreaDemo {
+    public static void main(String args[]) {
+        int x = 500;
+        int y = 100;
+        int a = x / y;
+        int b = 50;
+        System.out.println(a+b);
+    }
+}
+

反编译后该方法字节码指令

+
// ....
+     // 栈的最大深度为3 局部变量表长度为5 参数长度为1
+      stack=3, locals=5, args_size=1
+         // 500压入操作数栈
+         0: sipush        500
+         // 在局部变量表里存放 500
+         3: istore_1
+         // 100压入栈
+         4: bipush        100
+         // 在局部变量表里存放 100 
+         6: istore_2
+         //  500 从局部变量表里边取出,并压入操作数栈
+         7: iload_1
+         //  100 从局部变量表里边取出,并压入操作数栈
+         8: iload_2
+         // 调用 CPU 执行除法 500/100=5
+         9: idiv
+        // 存储在局部变量表里
+        10: istore_3
+        // 50压入栈中
+        11: bipush        50
+        //  50 存储在局部变量表中
+        13: istore        4
+        // 获取 #2 地址上类或接口字段的值并将其推入操作数栈
+        15: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
+        //  5 从本地变量表取出放到栈中
+        18: iload_3
+        // 50从本地变量表,压入栈
+        19: iload         4
+        // 调用cpu执行加法 5 + 50=55
+        21: iadd
+        // 虚方法调用 #3 中的方法 
+        // JVM会根据这个方法的描述,创建新的栈桢,方法的参数从操作数栈中弹出来,压入虚拟机栈,然后虚拟机会开始执行虚拟机栈上最上面的栈桢
+        22: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
+        // void 类型返回 main方法执行结束
+        25: return
+// ...
+

方法区演进细节

+

只有Hotspot才有永久代(方法区具体实现)。BEA JRockit、IBMJ9等来说,是不存在永久代的概念的。

+

Hotspot中方法区的变化

+ + + + + + + + + + + + + + + + + + + + + +
JDK版本方法区变化
JDK1.6及以前有永久代,字符串常量池、静态变量存储在永久代上
JDK1.7有永久代,但已经逐步 “去永久代”,字符串常量池,静态变量移除,保存在堆中
JDK1.8无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中
+

Jdk1.6方法区变化

+

Jdk1.7方法区变化

+

Jdk1.8方法区变化

+

为什么永久代要被元空间替代

+

根据官方的解释:替代是JRockitHotSpot融合后的结果,因为JRockit没有永久代,所以hotspot用元空间替代了永久代。

+

由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间,这项改动是很有必要的,原因有:

+
    +
  • +

    因为永久代设置空间大小是很难确定的。 +在某些场景下,如果动态加载类过多,容易产生方法区的OOM。 +比如某个实际Web工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。 +而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。 因此,默认情况下,元空间的大小仅受本地内存限制。

    +
  • +
  • +

    对永久代进行调优是很困难的。 +因为FullGC的花费时间是MinorGC的10倍,所以我们可以降低GC的频率,尽量不让方法区执行GC来提高效率。 +方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不在使用的类型。

    +
  • +
+

字符串常量池为什么要调整位置

+

JDK7中将StringTable放到了堆空间中。因为对永久代的回收效率很低,只有在Full GC的时候才会触发。

+
+

Full GC 是老年代的空间不足、永久代不足时才会触发。

+
+

这就导致StringTable回收效率不高。而我们开发中会有大量的字符串被创建,回收效率低,导致永久代内存不足。 +所以JDK7之后将字符串常量池放到堆里,能及时回收内存,避免出现错误。

+

方法区的垃圾回收

+

有些人认为JVM的方法区(如 Hotspot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。 +《Java虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。 +事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如JDK11时期的ZGC收集器就不支持类卸载)。

+

一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。 +以前sun公司的Bug列表中,曾出现过的若干个严重的Bug就是由于低版本的HotSpot虚拟机对此区域未完全回收而导致内存泄漏。

+

方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不再使用的类型。

+

方法区内运行时常量池之中主要存放的两大类常量:字面量符号引用

+

HotSpot虚拟机对常量池的回收策略是很明确的,只要常量池中的常量没有被任何地方引用,就可以被回收。

+

判定一个常量是否“废弃”还是相对简单,而要判定一个类型是否属于“不再被使用的类”的条件就比较苛刻了。 +需要同时满足下面三个条件:

+
    +
  • +

    该类所有的实例都已经被回收,也就是Java堆中不存在该类及其任何派生子类的实例。 +加载该类的类加载器已经被回收,这个条件除非是经过精心设计的可替换类加载器的场景,如osGi、JSP的重加载等,否则通常是很难达成的。

    +
  • +
  • +

    该类对应的java.lang.C1ass对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

    +
  • +
  • +

    加载该类的类加载器已经被回收,这个条件除非是经过精心设计可替换类加载器的场景,如JSP、OSGi,否则通常很难达成。

    +
  • +
+

Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是和对象一样,没有引用了就必然会回收。 +关于是否要对类型进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可以使用-verbose:class 以及 -XX:+TraceClass-Loading、-XX:+TraceClassUnLoading查看类加载和卸载信息。

+

在大量使用反射、动态代理、CGLib等字节码框架,动态生成JSP以及OSGi这类频繁自定义类加载器的场景中, +通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的内存压力。

+

符号引用

+

符号引用是编译原理中的概念,是相对于直接引用来说的。主要包括了以下三类常量:

+
    +
  • 类和接口的全限定名
  • +
  • 字段的名称和描述符
  • +
  • 方法的名称和描述符
  • +
+

符号引用 :符号引用以一组符号来描述所引用的目标。 +符号引用可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可,符号引用和虚拟机的布局无关。

+

在编译的时候每个java类都会被编译成一个class文件,但在编译的时候虚拟机并不知道所引用类的地址,所以就用符号引用来代替, +而在这个解析阶段就是为了把这个符号引用转化成为真正的地址的阶段。

+

字面量

+

在计算机科学中,字面量(literal)是用于表达源代码中一个固定值的表示法(notation)。 +几乎所有计算机编程语言都具有对基本值的字面量表示,诸如:整数、浮点数以及字符串; +而有很多也对布尔类型和字符类型的值也支持字面量表示; +还有一些甚至对枚举类型的元素以及像数组、记录和对象等复合类型的值也支持字面量表示法。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/jvm/jvm-native-interface/index.html b/blog-site/public/posts/jvm/jvm-native-interface/index.html new file mode 100644 index 00000000..c105fa33 --- /dev/null +++ b/blog-site/public/posts/jvm/jvm-native-interface/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + JVM-本地方法接口 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JVM-本地方法接口

+ 2021.04.02 +
+

概念

+

简单地讲,一个Native Methodt是一个Java调用非Java代码的接囗。 +一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。 +这个特征并非Java所特有,很多其它的编程语言都有这一机制,比如在C++中,你可以用extern “c” 告知c++编译器去调用一个c的函数。

+

在定义一个native method时,并不提供实现体(有些像定义一个Java interface),因为其实现体是由非java语言在外面实现的。

+

本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。 +本地方法接口

+

需要注意的是:标识符native可以与其它java标识符连用,但是abstract除外。

+
public class IhaveNatives {
+    public native void Native1(int x);
+    native static public long Native2();
+    native synchronized private float Native3(Object o);
+    native void Natives(int[] ary) throws Exception;
+}
+

为什么使用本地方法

+

Java使用起来非常方便,然而有些层次的任务用Java实现起来不容易,或者我们对程序的效率很在意时,问题就来了。

+

与Java环境的交互

+

有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。你可以想想Java需要与一些底层系统,如操作系统或某些硬件交换信息时的情况。 +本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。

+

与操作系统的交互

+

JVM支持着Java语言本身和运行时库,它是Java程序赖以生存的平台,它由一个解释器(解释字节码)和一些连接到本地代码的库组成。 +然而不管怎样,它毕竟不是一个完整的系统,它经常依赖于一底层系统的支持。这些底层系统常常是强大的操作系统。 +通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用c写的。 +还有,如果我们要使用一些Java语言本身没有提供封装的操作系统的特性时,我们也需要使用本地方法。

+

Sun’s Java

+

Sun的解释器是用C实现的,这使得它能像一些普通的C一样与外部交互。jre大部分是用Java实现的,它也通过一些本地方法与外界交互。 +例如:类java.lang.Thread的setPriority()方法是用Java实现的,但是它实现调用的是该类里的本地方法setPriorityo()。 +这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 setPriority()ApI。 +这是一个本地方法的具体实现由JVM直接提供,更多的情况是本地方法由外部的动态链接库提供,然后被JVM调用

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/jvm/jvm-native-stack/index.html b/blog-site/public/posts/jvm/jvm-native-stack/index.html new file mode 100644 index 00000000..26e78e07 --- /dev/null +++ b/blog-site/public/posts/jvm/jvm-native-stack/index.html @@ -0,0 +1,398 @@ + + + + + + + + + + + JVM-本地方法栈 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JVM-本地方法栈

+ 2021.04.02 +
+

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 +另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

+

运行时数据区

+

运行时数据区域包括

+
    +
  • 程序计数寄存器
  • +
  • 虚拟机栈
  • +
  • 本地方法栈
  • +
  • +
  • 方法区
  • +
+

其中:方法区、堆为线程共享;程序计数寄存器、虚拟机栈、本地方法栈 为线程私有。

+

本地方法栈

+

概览

+

Java虚拟机栈于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。

+

本地方法栈,也是线程私有的。

+

允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)

+

如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java虚拟机将会抛出一个stackOverflowError异常。 +如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个OutOfMemoryError异常。 +本地方法是使用C语言实现的。

+

它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。 +本地方法栈

+

当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。

+

本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。

+
    +
  • 它甚至可以直接使用本地处理器中的寄存器
  • +
  • 直接从本地内存的堆中分配任意数量的内存。 +并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。
  • +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/jvm/jvm-pc-register/index.html b/blog-site/public/posts/jvm/jvm-pc-register/index.html new file mode 100644 index 00000000..6de75976 --- /dev/null +++ b/blog-site/public/posts/jvm/jvm-pc-register/index.html @@ -0,0 +1,478 @@ + + + + + + + + + + + JVM-程序计数寄存器 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JVM-程序计数寄存器

+ 2021.03.27 +
+

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 +另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

+

运行时数据区

+

运行时数据区域包括

+
    +
  • 程序计数寄存器
  • +
  • 虚拟机栈
  • +
  • 本地方法栈
  • +
  • +
  • 方法区
  • +
+

其中:方法区、堆为线程共享;程序计数寄存器、虚拟机栈、本地方法栈 为线程私有。

+

程序计数寄存器

+

概述

+

JVM中的程序计数寄存器(Program Counter Register)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。 +CPU只有把数据装载到寄存器才能够运行。 +这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。 +JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。

+

特点

+
    +
  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 +字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。
  • +
  • 它是一块很小的内存空间,几乎可以忽略不记。也是运行速度最快的存储区域。
  • +
  • 它是唯一一个在Java虚拟机规范中没有规定任何outOfMemoryError情况的区域。
  • +
+

程序计数器中既不存在GC又不存在OOM,所以不存在垃圾回收问题。

+

作用

+
    +
  • PC寄存器用来存储指向下一条指令的地址,也即将要执行的指令代码。由执行引擎读取下一条指令。
  • +
+

由于Java的多线程是通过线程轮流切换完成的,一个线程没有执行完时就需要一个东西记录它执行到哪了,下次抢占到了CPU资源时再从这开始, +这个东西就是程序计数器,正是因为这样,所以它也是“线程私有”的内存。

+

代码演示

+
public class MainTest {
+    public static void main(String[] args) {
+        int i = 10;
+        int j = 20;
+        int k = i + j;
+
+        String str = "abc";
+        System.out.println(str);
+        System.out.println(k);
+    }
+}
+

通过javap -verbose MainTest.class命令反编译.class文件,得到如下

+
// ...
+ public static void main(java.lang.String[]);
+    descriptor: ([Ljava/lang/String;)V
+    flags: ACC_PUBLIC, ACC_STATIC
+    Code:
+      stack=2, locals=5, args_size=1
+         0: bipush        10
+         2: istore_1
+         3: bipush        20
+         5: istore_2
+         6: iload_1
+         7: iload_2
+         8: iadd
+         9: istore_3
+        10: ldc           #2                  // String abc
+        12: astore        4
+        14: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
+        17: aload         4
+        19: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
+        22: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
+        25: iload_3
+        26: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
+        29: return
+// ...
+

通过PC寄存器,我们就可以知道当前程序执行到哪一步了。 +PC寄存器保存指令示意

+

常见问题

+

使用PC寄存器存储字节码指令地址有什么用呢?

+

因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行。 +JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令。

+

PC寄存器为什么被设定为私有的?

+

我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复, +如何保证分毫无差呢?为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器, +这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

+

由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。

+

这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/jvm/jvm-stack/index.html b/blog-site/public/posts/jvm/jvm-stack/index.html new file mode 100644 index 00000000..a0b6a0f7 --- /dev/null +++ b/blog-site/public/posts/jvm/jvm-stack/index.html @@ -0,0 +1,1645 @@ + + + + + + + + + + + JVM-虚拟机栈 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JVM-虚拟机栈

+ 2021.03.28 +
+

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 +另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。

+

运行时数据区

+

运行时数据区域包括

+
    +
  • 程序计数寄存器
  • +
  • 虚拟机栈
  • +
  • 本地方法栈
  • +
  • +
  • 方法区
  • +
+

其中:方法区、堆为线程共享;程序计数寄存器、虚拟机栈、本地方法栈 为线程私有。

+

虚拟机栈

+

介绍

+

Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈, +其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。

+

生命周期

+

生命周期和线程一致,也就是线程结束了,该虚拟机栈也销毁了

+

作用

+

主管Java程序的运行,它保存方法的局部变量(8中基本数据类型及对象的引用地址)、部分结果,并参与方法的调用和返回。

+

栈的特点

+

栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。JVM直接对Java栈的操作只有两个

+
    +
  • 每个方法执行,伴随着进栈(入栈、压栈)
  • +
  • 执行结束后的出栈工作
  • +
+

对于栈来说不存在垃圾回收问题(栈存在溢出的情况:OOM异常)

+
+

PS 栈与堆 +1.首先栈是运行时的单位,而堆是存储的单位 +2.栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放,放哪里

+
+

与程序计数器一样,Java的虚拟机栈也是线程私有的,虚拟机栈描述的是Java的方法执行的内存模型, +方法每个执行在同时的创建都会一个栈桢用于存储局部变量表,操作数栈,动态链接,方法出口等信息。

+

栈的异常

+

Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。

+

如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。 +如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。

+
public class MainTest {
+    private static int count = 1;
+    public static void main(String[] args) {
+        System.out.println(count++);
+        main(args);
+    }
+}
+

抛出异常栈内存不足

+
// ...
+9377*** 
+Exception in thread "main" java.lang.StackOverflowError
+// ...
+

在使用递归的情况下,如果线程请求的栈的深度超过虚拟机所允许栈的深度就会抛出StackOverflowError; +但是大部分虚拟机栈的深度都可以动态扩展,HotSpot中使用 Xss 可以设置栈的深度,如果扩展时无法请求到足够的内存就会抛出OutOfMemoryError

+

可以设置栈的内存大小,使用参数 -Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

+
-Xss256m
+-Xss256k
+

栈的存储结构

+

栈的存储单位

+
    +
  • 每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。
  • +
  • 在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。
  • +
  • 栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。
  • +
+

栈中存储内容

+

每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。 +在这个线程上正在执行的每个方法都各自对应一个栈颜(Stack Frame)。 +栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

+

JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”或“后进先出”原则。

+

在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的, +这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。

+

执行引擎运行的所有字节码指令只针对当前栈帧进行操作。

+

如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。 +栈中存储内容

+

代码演示

+
public class MainTest {
+
+    public static void main(String[] args) {
+        method01();
+    }
+
+    private static int method01() {
+        System.out.println("方法1的开始");
+        int i = method02();
+        System.out.println("方法1的结束");
+        return i;
+    }
+
+    private static int method02() {
+        System.out.println("方法2的开始");
+        int i = method03();;
+        System.out.println("方法2的结束");
+        return i;
+    }
+    private static int method03() {
+        System.out.println("方法3的开始");
+        int i = 30;
+        System.out.println("方法3的结束");
+        return i;
+    }
+
+}
+

栈的运行原理

+

不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。

+

如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。

+

Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。

+

栈帧的结构

+

每个栈帧中存储着:

+
    +
  • 局部变量表
  • +
  • 操作数栈(或表达式栈)
  • +
  • 动态链接(或指向运行时常量池的方法引用)
  • +
  • 方法返回地址(或方法正常退出或者异常退出的定义)
  • +
  • 一些附加信息
  • +
+

栈桢的结构

+

每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由 局部变量表 和 操作数栈 决定的。

+

栈桢

+

局部变量表

+

局部变量表:Local Variables,被称之为局部变量数组或本地变量表。

+

定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。

+

由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。

+

局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。 +在方法运行期间是不会改变局部变量表的大小的。

+

字节码介绍 +局部变量表

+

方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。 +对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。 +进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。

+

局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。 +当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

+
Slot
+

局部变量表,最基本的存储单元是Slot(变量槽)局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。

+

参数值的存放总是在局部变量数组的 index0 开始,到数组长度-1的索引结束。

+

在局部变量表里,32位以内的类型只占用一个slot,64位的类型(1ong和double)占用两个slot。

+

byte、short、char 在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true。 1ong和double则占据两个slot。 +JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值

+

当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上

+

如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问1ong或doub1e类型变量)

+

如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的s1ot处,其余的参数按照参数表顺序继续排列。

+

slot

+
Slot重复利用
+

栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变就很有可能会复用过期局部变量的槽位, +从而达到节省资源的目的。

+

代码演示

+
    public void test() {
+        int a = 0;
+        {
+            int b = 0;
+            b = a + 1;
+        }
+        int c = a + 1;
+    }
+

slot重复利用

+
静态变量与局部变量
+

变量的分类:

+
    +
  • 按数据类型分:基本数据类型、引用数据类型
  • +
  • 按类中声明的位置分:成员变量(类变量,实例变量)、局部变量 +
      +
    • 类变量:linking的paper阶段,给类变量默认赋值,init阶段给类变量显示赋值即静态代码块
    • +
    • 实例变量:随着对象创建,会在堆空间中分配实例变量空间,并进行默认赋值
    • +
    • 局部变量:在使用前必须进行显式赋值,不然编译不通过。
    • +
    +
  • +
+

参数表分配完毕之后,再根据方法体内定义的变量的顺序和作用域分配。

+

我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。 +和类变量初始化不同的是,局部变量表不存在系统初始化的过程,这意味着一旦定义了局部变量则必须人为的初始化,否则无法使用。

+

在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。 +局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。

+

操作数栈

+

每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出的 操作数栈,也可以称之为 表达式栈。

+
+

PS: 栈为抽象数据结构,不是真实存在的。一般可以用数组或者链表来实现。这里是假定是用数组实现栈结构。

+
+

操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)。

+
    +
  • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。
  • +
  • 比如:执行复制、交换、求和等操作。 +操作数栈add
  • +
+

举例

+

操作数栈代码举例

+

操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

+
+

PS:这个时候操作数栈是有长度的,数组一旦创建,那么就是不可变的

+
+

操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。

+

每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为maxstack的值。

+

栈中的任何一个元素都可以是任意的Java数据类型

+
    +
  • 32bit的类型占用一个栈单位深度
  • +
  • 64bit的类型占用两个栈单位深度
  • +
+

操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问; +如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

+

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。 +另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

+

动态链接

+

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。 +比如:invokedynamic指令。

+

动态链接 +常量池

+

在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里。 +比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的, +那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

+
+

为什么需要运行时常量池?

+

因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,节省了空间。常量池的作用:就是为了提供一些符号和常量,便于指令的识别

+
+

方法返回值

+

存放调用该方法的pc寄存器的值。 当一个方法开始执行后,只有两种方式可以退出:

+
    +
  • 正常执行完成
  • +
  • 出现未处理的异常,非正常退出
  • +
+

无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时, +调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。 +而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

+

执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;

+

在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口。

+

一个方法在正常调用完成之后,究竟需要使用哪一个返回指令,还需要根据方法返回值的实际数据类型而定。

+
+

在字节码指令中,返回指令包含ireturn(当返回值是boolean,byte,char,short和int类型时使用),lreturn(Long类型),freturn(Float类型),dreturn(Double类型),areturn。 +另外还有一个return指令声明为void的方法,实例初始化方法,类和接口的初始化方法使用。

+
+

方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码 +异常表 +本质上,方法的退出就是当前栈帧出栈的过程。 +此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。

+

正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

+

一些附加信息

+

栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。

+

栈顶缓存技术

+

基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令, +这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。

+

由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。 +为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术, +将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。

+
+

寄存器:指令更少,执行速度快

+
+

方法的调用

+

在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。

+

链接

+

静态链接

+

当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期克制,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接

+

动态链接

+

如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。

+

绑定机制

+

对应的方法的绑定机制为:早期绑定(Early Binding)和晚期绑定(Late Binding)。 +绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。

+

早期绑定

+

早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。

+

晚期绑定

+

如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。

+

虚方法和非虚方法

+

Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。 +如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。

+
    +
  • 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法。
  • +
  • 静态方法、私有方法、final方法、实例构造器、父类方法都是非虚方法。
  • +
  • 其他方法称为虚方法。
  • +
+

虚拟机中提供了以下几条方法调用指令

+

普通调用指令

+
    +
  • invokestatic:调用静态方法,解析阶段确定唯一方法版本
  • +
  • invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本
  • +
  • invokevirtual:调用所有虚方法
  • +
  • invokeinterface:调用接口方法
  • +
+

虚拟机指令虚非虚方法

+

动态调用指令

+
    +
  • invokedynamic:动态解析出需要调用的方法,然后执行
  • +
+

JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynamic指令,这是Java为了实现动态类型语言】支持而做的一种改进。

+

但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。 +直到Java8的Lambda表达式的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式。

+

Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂, +增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。 +虚拟机虚方法指令

+

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。 +其中invokestatic指令和invokespecial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。

+

重写的本质

+

Java 语言中方法重写的本质:

+
    +
  • 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C。
  • +
  • 如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.1ang.I1legalAccessError异常。 +否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程。
  • +
  • 如果始终没有找到合适的方法,则抛出java.1ang.AbstractMethodError异常。
  • +
+
+

PS: IllegalAccessError 程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。

+
+

虚方法表

+

在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表 (virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。

+

每个类中都有一个虚方法表,表中存放着各个方法的实际入口。

+

虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的方法表也初始化完毕。

+

虚方法表

+

如果类中重写了方法,那么调用的时候,就会直接在虚方法表中查找,否则将会直接连接到Object的方法中。

+

栈的相关面试题

+

举例栈溢出的情况?(StackOverflowError)

+

通过 -Xss设置栈的大小;使用递归调用同一个方法;

+

调整栈大小,就能保证不出现溢出么?

+

不能保证。一定时间内降低了OOM概率,但是不能避免OOM;

+

分配的栈内存越大越好么?

+

不是,因为整个空间是有限的,会挤占其它的线程空间。

+

垃圾回收是否涉及到虚拟机栈?

+

不会;因为栈结构,出栈就相当于垃圾回收了。

+

运行时数据区,是否存在Error和GC

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
运行时数据区是否存在Error是否存在GC
程序计数器
虚拟机栈
本地方法栈
方法区是(OOM)
+

方法中定义的局部变量是否线程安全?

+
+

PS 何为线程安全? +如果只有一个线程才可以操作此数据,则必是线程安全的 +如果有多个线程操作,则此数据是共享数据,如果不考虑共享机制,则为线程不安全

+
+

如果对象是在内部产生,并在内部消亡,没有返回到外部,那么它就是线程安全的,反之则是线程不安全的。

+
public class StringBuilderTest {
+
+    // s1的声明方式是线程安全的
+    public static void method01() {
+        // 线程内部创建的,属于局部变量
+        StringBuilder s1 = new StringBuilder();
+        s1.append("a");
+        s1.append("b");
+    }
+
+    // 这个也是线程不安全的,因为有返回值,有可能被其它的程序所调用
+    public static StringBuilder method04() {
+        StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append("a");
+        stringBuilder.append("b");
+        return stringBuilder;
+    }
+
+    // stringBuilder 是线程不安全的,操作的是共享数据
+    public static void method02(StringBuilder stringBuilder) {
+        stringBuilder.append("a");
+        stringBuilder.append("b");
+    }
+
+
+    /**
+     * 同时并发的执行,会出现线程不安全的问题
+     */
+    public static void method03() {
+        StringBuilder stringBuilder = new StringBuilder();
+        new Thread(() -> {
+            stringBuilder.append("a");
+            stringBuilder.append("b");
+        }, "t1").start();
+
+        method02(stringBuilder);
+    }
+
+    // StringBuilder是线程安全的,但是String也可能线程不安全的
+    public static String method05() {
+        StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append("a");
+        stringBuilder.append("b");
+        return stringBuilder.toString();
+    }
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/jvm/jvm-start/index.html b/blog-site/public/posts/jvm/jvm-start/index.html new file mode 100644 index 00000000..8c59d396 --- /dev/null +++ b/blog-site/public/posts/jvm/jvm-start/index.html @@ -0,0 +1,1031 @@ + + + + + + + + + + + JVM-JVM介绍 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

JVM-JVM介绍

+ 2021.03.05 +
+

为什么要学习JVM

+

大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。 +一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要, +这其实是一种本末倒置的“病态”。 +如果我们把核心类库的API比做数学公式的话,那么Java虚拟机的知识就好比公式的推导过程。

+
+

不要以钱为你的最终目标,当你把技术做到位的时候,你会发现你的薪资待遇、社会地位,都会自然而然的提升上来。不要本末倒置、急功近利。

+
+
    +
  • 面试的需要(BATJ、TMD,PKQ等面试都爱问)
  • +
  • 中高级程序员必备技能:项目管理、调优的需求
  • +
  • 追求极客的精神,比如:垃圾回收算法、JIT(即时编译器)、底层原理
  • +
+

垃圾收集机制为我们打理了很多繁琐的工作,大大提高了开发的效率, +但是,垃圾收集也不是万能的,懂得JVM内部的内存结构、工作机制,是设计高扩展性应用和诊断运行时问题的基础,也是Java工程师进阶的必备能力。

+

虚拟机概念

+

所谓虚拟机(Virtual Machine),就是一台虚拟的计算机。它是一款软件,用来执行一系列虚拟计算机指令。 +大体上,虚拟机可以分为系统虚拟机和程序虚拟机。

+
    +
  • 大名鼎鼎的Virtual Box,VMware就属于系统虚拟机,它们完全是对物理计算机的仿真,提供了一个可运行完整操作系统的软件平台。
  • +
  • 程序虚拟机的典型代表就是Java虚拟机,它专门为执行单个计算机程序而设计,在Java虚拟机中执行的指令我们称为Java字节码指令。
  • +
  • 无论是系统虚拟机还是程序虚拟机,在上面运行的软件都被限制于虚拟机提供的资源中.
  • +
+

Java虚拟机

+
    +
  • Java虚拟机是一台执行Java字节码的虚拟计算机,它拥有独立的运行机制,其运行的Java字节码也未必由Java语言编译而成。
  • +
  • JVM平台的各种语言可以共享Java虚拟机带来的跨平台性、优秀的垃圾回器,以及可靠的即时编译器。
  • +
  • Java技术的核心就是Java虚拟机(JVM,Java Virtual Machine),因为所有的Java程序都运行在Java虚拟机内部。
  • +
  • Java虚拟机就是二进制字节码的运行环境,负责装载字节码到其内部,解释/编译为对应平台上的机器指令执行。每一条Java指令,Java虚拟机规范中都有详细定义,如怎么取操作数,怎么处理操作数,处理结果放在哪里。
  • +
+

JVM整体结构

+

HotSpot VM是目前市面上高性能虚拟机的代表作之一。下图就是 HotSport 虚拟机结构图 +Jvm内存模型

+
    +
  • 类加载子系统:将字节码文件加载到内存当中生成一个class文件。具体分为三部分:装载,链接,初始化。
  • +
  • 运行时数据区:方法区、堆、Java栈(虚拟机栈)、本地方法栈、程序计数器。其中线程共享,堆和方法区;Java栈、本地方法栈和程序计数器为每个线程独有一份的。
  • +
  • 执行引擎:包括:解释器、JIT及时编译器、GC垃圾回收器。
  • +
+

Java代码执行流程

+

Java代码通过编译器生成字节码文件,字节码通过Java虚拟机跟操作系统交互。 +Java代码执行流程

+

JVM架构模型

+

Java编译器输入的指令流基本上是一种 基于栈的指令集架构 ,另外一种指令集架构则是 基于寄存器的指令集架构 。 +Hotsport 是基于栈的指令集架构。

+

基于栈式架构的特点

+
    +
  • 设计和实现更简单,适用于资源受限的系统
  • +
  • 避开了寄存器的分配难题:使用零地址指令方式分配
  • +
  • 指令流中的指令大部分是零地址指令,其执行过程依赖于操作栈。指令集更小,编译器容易实现
  • +
  • 不需要硬件支持,可移植性更好,更好实现跨平台
  • +
+

基于寄存器架构的特点

+
    +
  • 典型的应用是x86的二进制指令集:比如传统的PC以及Android的Davlik虚拟机。
  • +
  • 指令集架构则完全依赖硬件,与硬件的耦合度高,可移植性差
  • +
  • 性能优秀和执行更高效
  • +
  • 花费更少的指令去完成一项操作
  • +
  • 在大部分情况下,基于寄存器架构的指令集往往都以一地址指令、二地址指令和三地址指令为主,而基于栈式架构的指令集却是以零地址指令为主
  • +
+

JVM生命周期

+

启动 –> 执行 –> 退出

+

虚拟机的启动

+
    +
  • Java虚拟机的启动是通过引导类加载器(bootstrap class loader)创建一个初始类(initial class)来完成的,这个类是由虚拟机的具体实现指定的。
  • +
+

虚拟机的执行

+
    +
  • 一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序
  • +
  • 程序开始执行时他才运行,程序结束时他就停止
  • +
  • 执行一个所谓的Java程序的时候,真真正正在执行的是一个叫做Java虚拟机的进程
  • +
+

虚拟机的退出

+
    +
  • 程序正常执行结束
  • +
  • 程序在执行过程中遇到了异常或错误而异常终止
  • +
  • 由于操作系统用现错误而导致Java虚拟机进程终止
  • +
  • 某线程调用Runtime类或System类的exit()方法,或Runtime类的halt()方法,并且Java安全管理器也允许这次exit()或halt()操作。
  • +
  • 除此之外,JNI(Java Native Interface)规范描述了用JNI Invocation API来加载或卸载 Java虚拟机时,Java虚拟机的退出情况。
  • +
+

JVM发展历程

+

Hotspot VM、JRockit、J9 是目前主要流行的Java虚拟机。所有虚拟机的原则:一次编译,到处运行。

+

具体JVM的内存结构,其实取决于其实现,不同厂商的JVM,或者同一厂商发布的不同版本,都有可能存在一定差异。 +主要以oracle HotSpot VM为默认虚拟机。

+

Sun Classic VM

+

早在1996 Java1.0 的时候,Sun 公司发布了了第一款名为 Sun Classic VM 的Java虚拟机,他同时也是世界上第一款商用的Java虚拟机, +JDK1.4 的时候完全被淘汰。这款虚拟机只提供解释器。

+
+

Java虚拟机分为两类执行引擎

+
    +
  • 解释型:一行一行执行代码,执行效率慢,类似于javascript、python这类解释型的编程语言
  • +
  • 及时编译型:将字节码中的热点代码编译成机器码,并且将机器码缓存到方法区的代码缓存区。
  • +
+
+

这款虚拟机只能使用纯解释器方式来执行Java代码,如果要使用即时编译器那 就必须进行外挂, +但是假如外挂了即时编译器的话,即时编译器就会完全接管虚拟机的执行系统,解释器便不能再工作了。 +因此这个阶段的虚拟机 虽然用了即时编译器输出本地代码, +其执行效率也和传统的 C/C++ 程序有很大差距,“Java语言很慢”的 印象就是在这阶段开始在用户心中树立起来的。

+

Hotspot 虚拟机内置了 Sun Classic 虚拟机。

+

Exact VM

+

Sun的虚拟机团队努力去解决Classic虚拟机所面临的各种问题,提升运行效率, +在JDK 1.2时,曾在 Solaris 平台上发布过一款名为 Exact VM 的虚拟机, +它的编译执行系统已经具备现代高性能虚拟机雏形,如热点探测、两级即时编译器、编译器与解释器混合工作模式等。

+

Hotspot VM

+

Hotspot VM 是目前使用范围最广的Java虚拟机。Hotspot 并非由Sun公司开发,而是由一家名为“Longview Technologies”的小公司设计的;

+

HotSpot VM既继承了Sun之前两款商用虚拟机的优点(如前面提到的准确式内存管理),也有许多自己新的技术优势, +如它名称中的HotSpot指的就是它的热点代码探测技术,HotSpot VM的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后通知JIT编译器以方法为单位进行编译。 +如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发标准编译和OSR(栈上替换)编译动作。 +通过编译器与解释器恰当地协同工作,可以在最优化的程序响应时间与最佳执行性能中取得平衡,而且无须等待本地代码输出才能执行程序, 即时编译的时间压力也相对减小, +这样有助于引入更多的代码优化技术,输出质量更高的本地代码。

+

JRockit

+

JRockit 专注服务端的应用,内部不包含解析器的实现,全部代码都靠即时编译器编译后执行。

+
+

大量的行业基准测试显示,JRockit JVM是世界上最快的JVM。 JRockit面向延迟敏感型应用的解决方案JRockit Real Time提供以毫秒或微秒计的JVM响应时间, +适合财务前端办公、军事指挥与控制和电信网络的需要。使用JRockit产品,客户已经体验到了显著的性能提高(一些超过了70% )和硬件成本的减少(达50%)。

+
+

全面的Java运行时解决方案

+
    +
  • JRockit 面向延迟敏感性应用的解决方案,JRockit Real Time 提供以毫秒或微秒级的JVM响应时间,适合财务、军事指挥、电信网络的需要。
  • +
  • MissionControl 服务套件,它是一组以模板低的开销来监控、管理和分析生产环境中的应用程序的工具。
  • +
+

J9 VM

+

IBM的J9全称:IBM Technology for Java Virtual Machine,简称IT4J,内部代号J9。

+

J9的市场定位与HotSpot接近,服务器端、桌面应用、嵌入式等多用途VM。

+

J9是目前由影响力的三大商业虚拟机之一,2017年IBM发布了开源J9 VM,命名为OpenJ9,交给Eclipse基金会管理,也称Eclipse OpenJ9。

+
+

IBM J9直至今天仍旧非常活跃,IBM J9虚拟机的职责分离与模块化做得比HotSpot更优秀, +由J9 虚拟机中抽象封装出来的核心组件库(包括垃圾收集器、即时编译器、诊断监控子系统等)就单独构 成了IBM OMR项目, +可以在其他语言平台如Ruby、Python中快速组装成相应的功能。从2016年起, IBM逐步将OMR项目和J9虚拟机进行开源, +完全开源后便将它们捐献给了Eclipse基金会管理,并重新 命名为Eclipse OMR和OpenJ9。 +如果为了学习虚拟机技术而去阅读源码,更加模块化的OpenJ9代码 其实是比HotSpot更好的选择。 +如果为了使用Java虚拟机时多一种选择,那可以通过AdoptOpenJDK来 获得采用OpenJ9搭配上OpenJDK其他类库组成的完整JDK。

+
+

KVM和CDC/CLDC Hotspot VM

+

Oracle 在Java Me 产品线上的两款虚拟机: CDC/CLDC Hotspot VM。目前移动领域地位的尴尬,智能手机被IOS和android二分天下。

+

KVM 简单、轻量,高度可移植性,在面向更低端的设备上还维持着自己的一片市场。

+
    +
  • 只能控制器、传感器。
  • +
  • 老年手机、经济欠发达地区简单的功能手机。
  • +
+

Azul VM

+

前面三大“高性能Java虚拟机”使用在通用硬件平台上这里Azu1VW和BEALiquid VM是与特定硬件平台绑定、软硬件配合的专有虚拟机I

+

高性能Java虚拟机中的战斗机。 +Azul VM是Azu1Systems公司在HotSpot基础上进行大量改进,运行于Azul Systems公司的专有硬件Vega系统上的ava虚拟机。

+

每个Azu1VM实例都可以管理至少数十个CPU和数百GB内存的硬件资源,并提供在巨大内存范围内实现可控的GC时间的垃圾收集器、专有硬件优化的线程调度等优秀特性。

+

2010年,AzulSystems公司开始从硬件转向软件,发布了自己的zing JVM,可以在通用x86平台上提供接近于Vega系统的特性。

+

Apache Marmony

+

Apache也曾经推出过与JDK1.5和JDK1.6兼容的Java运行平台Apache Harmony。

+

它是IElf和Inte1联合开发的开源JVM,受到同样开源的openJDK的压制,Sun坚决不让Harmony获得JCP认证,最终于2011年退役,IBM转而参与OpenJDK

+

虽然目前并没有Apache Harmony被大规模商用的案例,但是它的Java类库代码吸纳进了Android SDK。

+

Micorsoft JVM

+

微软为了在IE3浏览器中支持Java Applets,开发了Microsoft JVM。

+

只能在window平台下运行。但确是当时Windows下性能最好的Java VM。

+

1997年,sun以侵犯商标、不正当竞争罪名指控微软成功,赔了sun很多钱。微软windowsXPSP3中抹掉了其VM。 +现在windows上安装的jdk都是HotSpot.

+

Taobao JVM

+

由AliJVM团队发布。阿里,国内使用Java最强大的公司,覆盖云计算、金融、物流、电商等众多领域,需要解决高并发、高可用、分布式的复合问题。有大量的开源产品。

+

基于openJDK开发了自己的定制版本AlibabaJDK,简称AJDK。是整个阿里Java体系的基石。

+

基于openJDK Hotspot VM发布的国内第一个优化、深度定制且开源的高性能服务器版Java虚拟机。

+

创新的GCIH(GCinvisible heap)技术实现了off-heap,即将生命周期较长的Java对象从heap中移到heap之外,并且Gc不能管理GCIH内部的Java对象,以此达到降低GC的回收频率和提升Gc的回收效率的目的。 +GCIH中的对象还能够在多个Java虚拟机进程中实现共享 +使用crc32指令实现JvM intrinsic降低JNI的调用开销 +PMU hardware的Java profiling tool和诊断协助功能 +针对大数据场景的ZenGc +taobao vm应用在阿里产品上性能高,硬件严重依赖inte1的cpu,损失了兼容性,但提高了性能

+

目前已经在淘宝、天猫上线,把oracle官方JvM版本全部替换了。

+

Dalvik VM

+

谷歌开发的,应用于Android系统,并在Android2.2中提供了JIT,发展迅猛。

+

Dalvik y只能称作虚拟机,而不能称作“Java虚拟机”,它没有遵循 Java虚拟机规范

+

不能直接执行Java的Class文件

+

基于寄存器架构,不是jvm的栈架构。

+

执行的是编译以后的dex(Dalvik Executable)文件。执行效率比较高。

+

它执行的dex(Dalvik Executable)文件可以通过class文件转化而来,使用Java语法编写应用程序,可以直接使用大部分的Java API等。 +Android 5.0使用支持提前编译(Ahead of Time Compilation,AoT)的ART VM替换Dalvik VM。

+

Graal VM

+

2018年4月,oracle Labs公开了GraalvM,号称 “Run Programs Faster Anywhere”,勃勃野心。 +与1995年Java的”write once,run anywhere"遥相呼应。

+

GraalVM在HotSpot VM基础上增强而成的跨语言全栈虚拟机,可以作为“任何语言” 的运行平台使用。语言包括:Java、Scala、Groovy、Kotlin;C、C++、Javascript、Ruby、Python、R等

+

支持不同语言中混用对方的接口和对象,支持这些语言使用已经编写好的本地库文件

+

工作原理是将这些语言的源代码或源代码编译后的中间格式,通过解释器转换为能被Graal VM接受的中间表示。Graal VM提供Truffle工具集快速构建面向一种新语言的解释器。在运行时还能进行即时编译优化,获得比原生编译器更优秀的执行效率。

+

如果说HotSpot有一天真的被取代,Graalvm希望最大。 但是Java的软件生态没有丝毫变化。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/resume/interview-junior-javaer/index.html b/blog-site/public/posts/resume/interview-junior-javaer/index.html new file mode 100644 index 00000000..c2e627a2 --- /dev/null +++ b/blog-site/public/posts/resume/interview-junior-javaer/index.html @@ -0,0 +1,1980 @@ + + + + + + + + + + + 面试Java可能会被问到的问题 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

面试Java可能会被问到的问题

+ 2021.05.11 +
+

面试必问

+

自我介绍一下

+

你有什么职业规划

+

你为什么要离职

+

说一下你的优缺点

+

你的期望薪资是多少

+

你为什么要选择我们公司

+

你能否接受加班

+

你有对象了吗

+

你还有什么问题要问的吗

+

基础

+

说一下UDP、TCP及http与https

+

如何保证线程安全

+

线程池工作原理

+

如何避免死锁

+

说一下NIO、BIO、AIO

+

说一下ArrayList与LinkedList

+

介绍一下HashMap

+

Object类常用方法:equals、hash等

+

常用的设计模式:工厂、单例、代理、模板、适配器、策略等

+

说一下你对JVM的认识

+

介绍一下JVM中的堆

+

JVM垃圾收集算法

+

手写一个排序算法和查找算法

+

数据库

+

数据库基础存储、存储、事务、锁

+

MySQL如何优化索引

+

分库分表

+

如何实现MySQL的读写分离

+

分布式

+

为什么要进行系统拆分

+

SpringBoot与SSM对比优势

+

分布式注册中心、熔断降级、网关知道吗

+

SpringBoot自动配置原理

+

Spring IOC容器启动流程

+

Spring中的事务你了解吗

+

知道分布式事务吗

+

Kafka如何保证消息不丢失

+

Kafka如何避免重复消费

+

简单说说RocketMQ

+

Redis分布式锁有了解吗

+

Redis持久化

+

Redis内存淘汰策略

+

Redis部署策略

+

Elasticsearch的分布式架构原理

+

Elasticsearch读取写入数据的工作原理

+

手写一个负载均衡算法

+

Netty用过吗

+

解决问题及设计能力

+

你平时是怎么进行故障排查的

+

如何重构系统

+

如何设计一个高可用的系统

+

如何优化接口

+

设计示例代码

+ +

其他

+

说一个实际项目中遇到的问题及如何解决的

+

指定工作场景来提问

+

结合简历上的具体项目提问

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/resume/interview-questions-and-answers/index.html b/blog-site/public/posts/resume/interview-questions-and-answers/index.html new file mode 100644 index 00000000..45ddef62 --- /dev/null +++ b/blog-site/public/posts/resume/interview-questions-and-answers/index.html @@ -0,0 +1,1079 @@ + + + + + + + + + + + 面试中常见的问题 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

面试中常见的问题

+ 2021.04.23 +
+

面试常见问题

+

自我介绍

+
    +
  • 个人经历可以进行适当包装,但是不能造假,一方面如果一旦被人拆穿,后果就不用我说了吧,另一方面如果你说谎,说了一些你自己不感兴趣的项目,在入职之后可能会被分配到该项目上;
  • +
  • 在介绍的时候要说明你对面试的公司有什么用,根据不同类型的公司,定制不同的自我介绍;写下来并多多练习最好能脱稿背诵;
  • +
  • 在讲的时候不要太僵硬,放轻松,要使自我介绍自然一点儿;
  • +
  • 在说的时候要灵活一点儿,如果面试官对你的经历某个点很感兴趣,就详细的展开多说一点,使他更加相信你的能力;
  • +
+
面试官您好,我叫XXX,来自XXX,毕业于XXXX,在大学期间参与老师组织的"XXX"项目建设,获得一些Java框架的使用经验。有良好的编程习惯和扎实的Java基础,可以从容应对用Java框架编写分布式项目、使用Vue编写页面的场景。
+
+毕业后,就职于北京某金融领域公司,参与XXXX从零到一的搭建,通过一段时间,自学了Vue框架、Java设计模式。
+在和同事共同开发的过程中,得到了领导的赏识,将部分模块完全交付给我。后端主要通过Springboot、SSM 框架构建、前端通过Vue构建;
+后来因为项目的需求又通过一段时间学习了用`gitlab-runner`流水线发版的技能和一些新的技术;
+在项目中的这一年的时间,每天996的工作,尤其是在项目上线前一个月左右,每天顶着巨大的压力工作到半夜,这对于刚步入职场的新人来说,无疑是一种挑战,也使我明白了这个行业的艰辛.
+
+因此,我在使用Vue、Java搭建项目上有相关的经验;在工作方面有不错的抗压能力和学习能力;这个经验和工作能力应该能为公司的日后持续发展,做出贡献。
+
+项目上线后又自学了JVM等相关知识,通过不断对Java的学习,也使我对Java这门语言有了新的认识;
+到今年,我参与了XXXX的建设,通过引入Java设计模式对代码设计进行优化,使其更符合面向对象编程;通过用netty框架替代了直接使用socket,提高了系统的并发和响应速度,使系统更加健壮;
+
+相信通过我的工作能力,能帮助公司项目中的程序更上一层楼。
+

你从上一家公司离职的原因

+

在说离职的原因的时候不要过多的表露自己的负面情绪,即使你有理由也不要把大把的时间花到这个上面,因为面试的时间是非常宝贵的,你不要让面试官抓到它不找你的理由,而是要把握机会,展示自己的闪光点。

+

在说离职原因时要注意:

+
    +
  1. 不要去说原公司或原老板的坏话,原因是,你现在可以说这家公司的坏话,那一天这个公司把你招进去,你离开的时候也会说这个公司的坏话,这样给人的印象就显的非常的差,把自己的时间都解释在自己前公司为什么不好上;
  2. +
  3. 说离职原因是没有晋升空间,说这个理由时要十分的小心,千万不要给面试官留下别人都升了,就你没升的印象,这样子就是你有问题了;
  4. +
  5. 在说离职原因是工作累,加班频繁时,虽然加班是很多人离职的原因,但是你说出来并不加分,建议说的时候一笔带过;
  6. +
+

离职是分两种情况,一种是主动离职,另一种是被辞退,说的时候不要撒谎,万一公司有被调的,入职会受影响。下面按照这两种情况分别来说明:

+
    +
  • 主动辞职:
  • +
+
在过往的两三年当中,我在这个职位上成长非常多,也非常感激这个机遇,我学到了XXX方向的知识和XXX方向的技能,但是在晋升空间上确实有一定的瓶颈。
+放眼望去,我的前辈们,他们在自己的职位上呆了有三四年的时间了,在这段时间里都没有动过,我理解大部分的职位都是有很多重复性的,但是对于我这个职位来说确实学习空间和成长空间太小了。
+所以我希望能看看外面的机会,能够给自己带来更多的成长空间。
+
    +
  • 被动辞退:
  • +
+
公司的整个利润有所下滑,组织架构有改变,不是我一个人被辞退,是我和其他员工都被辞退,有大规模的裁员。
+但是我也是很感激这两年的学习机遇的,我在XXX方面和XXX方面都有很多成长,那么我也相信我的一些成长能够给公司带来很多价值。
+

你的优点是什么

+

大致思路:自己工作能力+事例+对日后的工作有什么好处。 +结合具体的事件来说,显得优点更加真实、更加容易让被人相信你,当然前提是你可以适当包装,但是不能撒谎。

+
我有很强的自驱力和自学能力,在之前的工作中自学了Vue、设计模式、JVM等Java相关技术;
+如果在未来工作中遇到不会的,也会去努力的去学习;包括向前辈学习、书本学习、自己去搜集资料,而不断的去提升自己;我的这个能力相信会对公司未来发展做出贡献。
+

你有什么缺点吗

+

缺点不能说的太真诚、直白;例如:我的缺点是我很懒,不细心、爱嫉妒别人、不爱团队合作 … +缺点也不能说的太过委婉,不能让面试官觉得你在自作聪明、不诚实;例如:我最大的缺点就是过于追求完美;不要做出非常明显的将自己的缺点转化为优点的答案。

+

要避免雷区蹦迪:如果你面试的工作非常需要这个能力,而你却说我的这个能力不行;如果这样说,这是对你应聘这个岗位是非常不利的。 +所以在说的时候要避免说这样的缺点,可以谈一些这个岗位不需要的能力,或者说比较不在意的。 +在说缺点的时候可以穿插着优点,先说优点再说缺点。

+
1.我是一个很理性的人,在混乱的时候,我能迅速的理清头绪,梳理出多种方案思路;然后再去探讨问题;
+但同时因为这一点,如果没有任何方案的情况下,共同头脑风暴,在这个时候我很难去提出自己的一些观点和看法。
+我也在慢慢的努力去改进,因为有时候只有打破这个规则,才能有一些创新性的想法,同时我相信我这个分析能力也能够帮助团队,去更好的达成目标。
+
+2. 我的工作经验相较于其他前辈比较少,我会在工作中多多学习来弥补我的经验不足。
+

你有对象吗

+

应该反问一句你问的是男对象还是女对象?(/doge)

+

hr问你问题的目的无非就三个:

+
    +
  • 你能不能胜任这份工作
  • +
  • 你能胜任多久这份工作
  • +
  • 招你是不是比招聘别人更划算
  • +
+

其实有对象也不一定是坏事,没有对象也不一定是好事;如果你面试的是需要沟通能力比较强的岗位那么你有对象可能就是优势,如果没有对象可能给对方留下你这个人沟通能力不行的印象。

+

如果你回答有对象,可能hr会顺着感情稳不稳定,什么时候结婚,什么时候生孩子的思路问下去,毕竟这些问题和企业付出的成本有很大的关系。

+

如果你的感情不稳定,比如和对象分居,吵架的时候总想去对方的城市去哄她(他),这就肯定会影响到工作。一般女生被问道这个问题的可能行会比较大,毕竟女生可以生育,生育需要放产假,需要企业付出比较大的成本。可以这么回答

+
我和对象的感情比较稳定,我们两个达成了共识,就是先打拼好事业,有好的物质基础在考虑.
+

如果你回答没有对象,hr可能会顺着什么时候准备找对象,结婚的思路问下去。可以这么回答

+
最近一些时间没有找对象的心思,我会先全身心投入工作,提升自己的专业水平,争取在2-3年间更上一层楼
+

你的职业规划是什么

+

HR其实并不关心你的职业规划,甚至她自己的职业规划都不清楚,那么为什么要问这个问题?

+

因为HR关心你的其他几件事:

+
    +
  • 你的工作稳定性
  • +
  • 与公司的匹配度
  • +
  • 工作能力的考察
  • +
+

所谓的稳定性,是指你在没有想清楚或者没有详细了解一个岗位的综合情况下,就来应聘这个岗位,这就意味这你的稳定性是不佳的; 你可能会干3个月、5个月,感觉跟你想象中的不太一样你就会离职。 所以你在选择一份工作之前你要详细了解它的发展前景,尽量将我想公司长期发展的观点表达出来。

+

岗位发展前景和职业规划的匹配度,比如:我希望两年就当上经理,但是公司的晋升比较缓慢,可能公司3年一次晋升或者我们现在的经理5年10年才熬到这个位置, 那你想两年就晋升,可能想是不大的。

+

如果HR了解到你的职业规划,那意味着你可能未来的2、3年都会在这条路上沉淀一直下去,这种沉淀一方面代表了稳定性,另一方面也代表了在工作的这个方向有足够的能力。

+

在说职业规划的时候有三个注意的点:

+
    +
  • 说1~3年的规划,如果时间太长,需要填的坑太多了;
  • +
  • 与岗位的匹配程度;比如:我面试的是Java开发,但是我还是挺想尝试Golang开发的,你想尝试Golang开发你为什么要应聘Java开发?你会不会做了两个月Java开发后就离职,去找Golang开发了。
  • +
  • 说话要留余地,不要说的太死;
  • +
+
我去年才毕业,说实话太长远的职业规划还没有去做,但是短期三年之内,我希望能在Java开发这个岗位深耕。
+为什么呢?
+1. 我很喜欢Java开发这个行业,用写的程序可以应用到实际生活中去,可以帮助到人们;
+2. 我在平时的时候也会看一些开源项目的源代码,并且在Github有自己的小项目;
+3. 我之前工作做的还是比较不错的,也间接证明了我比较适合这个岗位;
+
+因此,我还是希望未来在这个方向继续发展下去。如果公司认可我的能力的话,比如在工作一两年后,如果公司确实需要我会其他语言的开发或者需要我学习其他新的技术,我也是很乐意的。
+

面试如何谈薪资

+

如果前面面试谈的不错的话,面试是有戏的,这个时候就可以谈薪资了。

+

一般来说HR问你的期望薪资会根据你上一家公司的薪资来进行判断,也就是判断你的能力价值;这个时候不要最好说谎,因为一般的公司都会去做背景调查,被发现说谎的话,就不用我说了。

+

为什么薪资是重要的

+

虽然我们都说钱很重要,但经常又在谈薪的时候,因为不知道怎么开口就麻痹地说,钱也不是唯一的,然后就放弃了好好谈薪。

+
    +
  • 薪酬的确定性,在所有工作待遇里确定一定能够兑现的,就是在offer里承认给你的薪资,工资号称有更多的项目奖励,升职调薪机会等等都说存在不确定性的,我们就不能作为一定会有的收入;
  • +
  • offer之后的薪资,是之后每次涨薪甚至是跳槽的重要基础,这时候薪资低了,要涨起来是很困难的;
  • +
  • 薪酬福利,是我们考虑接受一个offer的重要因素,问清楚了谈明白了才能够更好的左决定。有的人总觉得谈钱、谈薪资有点别扭,觉得自己如果一致追着薪资福利问,甚至是展开谈判,有些不好意思,其实大可不必,一方面薪酬本身不仅仅指月薪或者年薪的一个简单数字,还包含了五险一金的缴纳比例、绩效占比、年终奖、涨薪制度、加班工资计算等等;
  • +
+

谈薪容易踩的坑

+

把谈谈薪的人分为两类人,一类是谈薪资过于激进派,一类是怂怂派,即不知道怎么合理提出薪资诉求。

+

激进派存在的问题:

+
    +
  • 太早提起薪资这件事儿。比如,在面试的流程中都还不确定能不能拿到offer,就已经开始向面试官问薪资的情况。这个时候问薪资只会让面试官觉得你不关心业务,心里面只有钱,所以当还在面试流程里的时候建议不要主动问钱的事。等对方觉得咱们还不错,要咱们了在谈也不迟,因为这个话题是不可避免的,你不谈公司也会主动找你谈。
  • +
  • 狮子大开口,提出的要求缺乏支撑。比如,面试官问薪资预期,明明同学上一份的薪资是6k,现在面的这份工作,挑战新也好,职责范畴也好,差异都不大,公司挂出的薪资范畴也是在6k~9k之间,同时自己也没有拿到其他薪资更高的offer,结果开口就要15k,这时候就会觉得你太贪心了,以及自我认知有问题。
  • +
  • 薪资造假。不要造假,包括撰写简历,还是在面试中回答问题,都不要撒谎不要编故事,无论在什么岗位、什么阶段,诚信二字,应该是我们在职场要坚守的基本准则。
  • +
+

怂怂派存在的问题:

+
    +
  • 不敢提薪资预期。生怕一提就被对方认为是没诚意,或者被对方认为是钻到钱眼里了。其实谈钱本身并不可耻,公司不也是大大方方地说,根据你现有的能力和匹配度,暂时只能给这么多。如果我们对工资开出的offer基本满意,那就直接可以愉快的合作了,但是确实薪资距离和预期的有差距,我们又不是狮子大开口、漫天要价,那就应该心平气和的提出来,看看双方有没有聊的空间;
  • +
  • 自曝软肋。谈薪的时候虽然hr是想要促成合作,让我们加入公司的,但同时也是希望我们能接受公司给到的薪资,所以hr肯定会寻找理由,说明薪资就是这么多,不能在高了,这个时候很多人就自曝软肋,给hr提供不涨薪资的理由,比如说:“薪资还好主要是更看重在公司发展的机会和成长空间”、“我更看中积累经验工资少点没关系”、“工作前几年我不是那么看中钱”;正确做法: +
    自己是很认可公司的,同时也很开心能有机会加入,同时薪酬也是一个重要的考虑维度,自己在这方面也有一些诉求
    +
  • +
  • 过早的亮出底牌。过早的亮出自己的底线,导致就比这你的底线谈,多的一点都不给。比如,hr直接问题最低能接受多少呢,如果这时候我希望不少于xxx,后面公司给的薪资多半也不会比你说的这些数字多多少了。建议不要对方一问就把底牌亮出来。我们应该先了解一些其他的信息,给定一个方向性的回答,比如: +
    我的预期是相较于现有的工资水平,有一定合理比例的上浮,也想了解一下公司对我的想法。
    +
    +能否先了解一下工资的薪酬结构,比如基础薪资和绩效奖金的构成、绩效奖金的发放标准、公司有没有年终奖、项目奖金,公司有没有期权、五险一金缴纳方式具体是怎么样的呢、公司有没有大小周,周末上班的计薪方式是什么样的。
    +
  • +
+

当对方非要问工资预期的时候,先给到方向性的回复,而不是具体的数字,争取让对方先报数,比如hr在介绍完薪资结构之后会再次问我们,了解了公司的薪酬结构你现在是什么想法呢?这个是否可以回复说

+
我希望总体上相较于上一份工作,有一定合理比例的涨幅,也想听听公司和我目前聊下来对我的提议
+

此时会出现两种情况,第一种直接反馈一个数字,第二种对方仍然不给你数字,并追问你你希望的涨幅是多少呢,你预计的薪资数字是多少呢

+
    +
  1. 如果工资满意,这个时候就可以继续愉快的合作了,如果觉得低了,希望在高一点,那我们可以说 +
    这个距离我的预期还是有一定的差距,看能否给到xxx呢
    +
  2. +
  3. 针对第二种情况可以将一个数字,要比自己的底线要高10%~20%的数字,此处的底线是低于该数字的就不考虑了,正好是这个数字是可以考虑的,建议说一个范围。
  4. +
+

如果对方能够给到那就愉快的接受,如果不能给到,那个可以基于这个底线,以及offer的其他情况,做一个判断,然后和对方有进一步的沟通。在谈的过程中找好参照物,帮助在谈薪资的过程中找到更有力的条款,我们需要论据来说明这个论据是合理的。

+

论据可以从这几个方面来找:

+
    +
  • 现有工作的完整度,完整薪资情况:公司福利、项目奖金、五险一金等。可以将在上一个公司的的晋升发展空间也算进去;如:我在上一家公司工作的很棒,今年下半年有可能加薪;
  • +
  • 如果已经拿到了其他的offer机会,这个时候也可以提出来,不是拿这A公司的offer向B公司要价,而是让他作为你一个市场竞争力的一个体现,让其更加相信你的能力,更能体现出你的期望工资的合理性;
  • +
  • 如果在岗位匹配度上还有什么特殊的优势,那也可以在谈薪资的时候,例如,这个岗位你之前已经有非常成功的经验,能够快速上手,有比如说,这个岗位要求基本掌握某些软件,而你已经对这些软件已经能熟练运用了。
  • +
+

在整个沟通过程中,如果期望薪资,对方的条件没有达到我们预期的时候,我们都要记得礼貌而坚定的表达自己的想法。并且有理有具的给予说明。面试是一个双向选择的过程,不要过于卑微亦不要过于高傲自大。正确的认清自己的价值,合理的谈论薪酬。

+

你能否接受加班

+
我能够接受加班,但是不赞成加班。
+有两种情况加班,我个人认为是没有问题的:
+1. 我个人能力不够,不能够按时完成上级交给我的任务,需要通过加班来完成;
+2. 公司遇到重要且紧急的事情或者突发事件需要马上来处理的;
+
+这两种情况下的加班我认为是必要的,对公司很重要且需要长期加班的项目,我希望公司能够平衡我个人的付出与收入之间的关系;
+对于不是特别重要或不是很紧急的项目加班,我个人不是很赞成。
+
+那我们公司的加班情况,您能介绍一下吗?
+

通过HR的回答,可以了解入职之后的工作强度如何。那些强制加班或强制996的公司你能否接受就看你自己了,如果你有能力有选择,那么你可以避开这些公司。

+

你为什么要选择我们公司

+

这个问题看上去是面试官在问我们选择行业、选择公司、选择岗位,事实上面试官在意的是为什么要选择我们。

+

先来弄清这个问题的本质,理解面试官的目的,面试官问你的求职动机,通常是想从三个维度来判断你是否适合这家公司:

+
    +
  • 评估你的职业规划是否合理
  • +
  • 对岗位的预期,即你想要的是不是公司想给的
  • +
  • 寻求通过理由,即找理由说服面试官
  • +
+

回答思路:

+
    +
  1. 看中这个行业、公司、岗位的什么
  2. +
  3. 这个行业、公司,要求哪些核心能力
  4. +
  5. 分析自己具备哪些能力或经验,使得更适合这个岗位
  6. +
+
Java工程师这个岗位吸引我的地方是自己可以去动手设计程序,并且通过自己做产品,来满足用户的需求,影响很多人。而咱公司的产品,我认为它解决了XX问题,创造了价值,这一点是我非常认可的。
+
+我理解作为一个程序员需要有较强的逻辑思维能力、良好的编码能力、团队协作能力以及学习能力,我在之前的岗位上也都具备这些能力,以及有一些项目经验;同时,我逻辑分析能力不错,过往在解决问题时,都知道怎么样用合理的去拆解和定位问题,最终找到机会点。
+

你还有什么想问的吗

+

这个问题,一般面试官都会放到最后来问,一般是面试官出于对面试者的尊重,客气一下,又或者真的想给面试者一个了解公司的机会; +无论哪种情况下,这个问题我们都应该让它有价值,不要问了公司情况之后自己也没有面试上,那样的话也没啥意义。 +当然,如果你自己有底线的话,对公司有一定要求,某一个点不能接受,就像我吃菜时不吃辣椒一样,接受不了,这样的话要提早说。

+

如果你面试的是中、大公司,你是第一次面试,还会有第二次、第三次面试等,先不要问你关心的问题,因为大概率上问了也没啥用。 +这时候你可以说,我对贵公司有一定的了解,贵公司是从事什么什么行业的,主要是做什么产品、技术的等。 +如果有机会的话,我很期待能加入贵公司;希望您能给机会进行下一次面试,到时候我想再详细的了解贵公司的情况; +说一些,表达我很想去这个公司,我是对公司有一点了解的,我是在这个方面下过功夫,把你的态度表达出来。

+

如果你面试的是小公司,这个公司只面试一次,公司决定要你的话,大概率会在这个问题之前给出结果; +如果在这之前面试官没有表达出来这层意思,那么面试也就八成凉凉了,那自然问题问不问也就没啥用了; +如果公司决定要你了,你就要问一些,你自己比较关心的问题。

+
1. 咱公司平常的项目中会用到的技术栈?
+2. 公司的项目开发流程?敏捷开发?开会是否频繁?
+3. 公司出差是否频繁、出差地点?
+4. 您对我有什么好的建议吗?
+5. 上班的环境、上班的位置、晋升机制、交通补贴、吃饭补贴、是否提供住宿、员工福利、加班补助 ...
+6. 格局大一些,把面试官问你的问题,反问一遍
+

如何正确提出辞职

+

辞职的时候不要给公司埋雷,当然也不要被公司薅最后一把羊毛;正所谓害人之心不可有,防人之心不可无。

+

如果你在一个公司实在是工作不下去了,请不要忍气吞声,也不要不管三七二十一的直接撂挑子走人;前者是对自己的不负责任,后者是对他人的不负责任。 +作为一个"人",我们既要对自己负责,又要对他人负责,当然这需要掌握一些辞职小技巧。下面将从这几个方面展开来说:

+
    +
  • 你为什么要辞职
  • +
  • 你应该什么时候辞职
  • +
  • 辞职后如何保护自己的权益
  • +
  • 辞职后应该做什么
  • +
+

为什么要辞职

+

这个问题其实对于不同的人来说,有不同的答案,总的来说就是对公司某些方面不满意,甚至怨恨。

+

其实,大部分人辞职的主要原因就是为了涨工资;俗话说:

+
月薪100w:公司就是我活着的意义,公司方向就是我信仰的方向。
+月薪80w:公司是我家,老板是爸妈。
+月薪50w:我与公司共存亡,务必对我耍流氓。
+月薪30w:千万不要因为我是娇花而怜惜我。
+月薪10w:修福报算什么,看我给你修一个新世纪福音战士。
+月薪8w:老板你今天想喝苹果汁、葡萄汁、西瓜汁,还是我这个小比灾汁。
+月薪5w:加班那是家常便饭,我直接不需要下班。
+月薪3w:老板说的都是对的,错的是这个世界。
+月薪2w:老板说啥就是啥,只要给钱就是好老板。
+月薪1w:老板时不时脑子有坑,我在背后说他点坏话。
+月薪8k:人在屋檐下,不得不低头,生活就是这么苦涩。
+月薪5k:大家都是碳基生物,老板你凭什么这么跳。
+月薪2k老板脸上那不是嘴,而是括约肌,说话就像放屁一样。
+月薪1k:老板祖坟冒烟,一行白鹭上青天。
+月薪500:老板何不乘风起,扶摇直上九万里。
+工资扣到0:老板我是你爹。吃我大XX,哦不对这样不礼貌对孩子怎么能说脏话呢,应该说老板我是您爹。
+

所以就有些人想辞职,但又不是真的想辞职,只是借机想提高一下自己的工资。奉劝有这些想法的兄弟姐妹们,这么做之前先想一下自己的能力,想一下自己在公司中的地位; +如果老板真的把你开了呢?请三思而后行,最好的涨工资方法就是攒足了劲儿然后跳槽。

+

还有一部分人,因为一些事或人就是在公司中呆不下去了,看啥啥都烦,浑身不刺挠,恨不得上午提辞职下午就走。这种情况下建议用一些个人的原因,如:身体不舒服、家里有事,同时把这些事情所需要的时间说的长一点,事情说的严重一些,让老板无法回绝你,让它感觉你是很焦急的样子。例如:

+
老板,我家里有点着急的事,需要我立刻就走回去处理,短的来说可能需要一两个月,长的来说可能需要三五个月,半年这样。
+

这种情况下,如果你的业务能力相当出色,老板一般是会拒绝你离职的,会说一些话来挽留你:

+
身体不舒服,那休息一段时间怎么样啊?家里有事先去处理吧,等回来的时候在看。
+

相反如果你的业务能力一般或较差,可替代性非常强,老板应该很快就能同意。

+

除了前两种比较极端的情况,大部分人都是不着急离职,但是又决定了我要离职,典型的骑驴找马。这种情况下可以用职业发展的这个原因:

+
我在公司三年了,我非常喜欢公司的氛围,工作内容跟我自己想的也很一致,但是现在我想对我下一阶段的规划,有一个重新的定位。
+
+现在我是做金融的,那么接下来我可能会更多想尝试互联网这个行业。我知道咱公司现在还没有做这方面的打算,所以经过我慎重的考虑,我今天正式跟您提出离职。
+
+同时我也会交接好下面的工作,您放心,希望您能够理解。
+

上述第三种情况,注意事项:

+
    +
  • 说清楚自己的职业规划和目前工作的冲突
  • +
  • 掌握新的职业规划,发展前景如何、适不适合你、要做足功课,避免老板把你给劝回来
  • +
  • 现在的职业新的规划和公司发展到底有没有冲突,防止老板把你调岗
  • +
  • 语气要坚定,态度要坚决
  • +
+

其实除了这些离职原因外,还有一些其他的原因,如:工作的地点离我太远了、工作内容不喜欢、对企业氛围不满意。 不建议说这些理由原因是,以上这些理由多多少少都带有一些不满和借口,都离职了,给对方留下个好的印象,没准之后还能碰见,路走宽点。

+

应该什么时候辞职

+

应该什么时候辞职,取决于你想什么时候辞职,如果你想要离职需要提前打招呼,针对于转正的员工,公司让你交接工作一个月是合情合法合理的,如果你想一个月之内离职,需要你的态度好点。

+

针对于这种情况下,来谈谈辞职时机的重要性:

+
    +
  • 不要在特别忙的时候提出辞职;你提出辞职会有人跟你交接,你马上就要走,如果没有招聘到这个人那就意味着工作会落在其他同事身上,其他同事已经很忙了,还要再去分担你的工作,同事可能会埋怨你。
  • +
  • 不要再一个项目做的过程中提出离职;要不就在项目开始的时候走,要不就在项目结束的时候走;因为在项目做的过程中可能只有你一个人跟进项目中的每个结点,这个时候提出辞职会给别人带来很大的不便。
  • +
  • 不要扎堆儿提辞职;很多人喜欢在春节之后提出辞职,领完年终奖了,蠢蠢欲动,听别人说春节之后工作好找,然后就一起提出辞职。这个时候提出辞职,如果你不是提前提出辞职,那么会由于很多同事都走了,可能会不太容易放你走,肯定是希望你交接满一个月的。
  • +
  • 不要在你将要升职加薪的时候提出辞职;如果你这时候提出辞职,你的加薪没了,升职也没了,你的离职证明上写的是之前的职位和薪资,这对你找新工作是不利的。建议升职加薪后在离职。
  • +
+

辞职后如何保护自己的权益

+

这时候辞职已经提好了,可能会有一段时间需要你交接工作,那么这段时间我们要注意几个问题:

+
    +
  • 要书面形式提出离职,问HR离职需要办理哪些手续。
  • +
  • 提前将自己在公司中不涉密的个人资料拷贝。
  • +
  • 辞职的时候想一想自己的未休假期、调休等,这些假期看看到最后怎么样去跟HR清算。
  • +
  • 确认社保的交接日期,避免中间出现空档期,之后再补是非常麻烦的。
  • +
  • 帮同事牵线搭桥;如果新同事有新的交接事项找到你,你可以跟人家说找谁谁谁,走之前提前跟同事说一下。
  • +
  • 注意保存好离职证明,因为离职证明仅此一份不接受复印件,原公司也不会在给你再开,但是入职新公司的时候需要,所以一定要保护好。
  • +
+

辞职后应该做什么

+

当你成功做完上面几个事情之后,请不要删掉某些同事的微信,甚至跟某些同事直接翻脸,发生不愉快的事情,这样做是非常不好的;这并不是非的让你维护某些人际关系,因为你会在职场后期你会发现自己的圈子并不大。 +不是说非要你去维护人际关系,那至少请不要闹僵了。

+

包括可以逢年过节的时候,可以去发一个微信、或者去关心一下。人脉关系这件事情,如果你现在没有时间去维持,那么你现在可以先保持。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/resume/interview-resume-20201124/index.html b/blog-site/public/posts/resume/interview-resume-20201124/index.html new file mode 100644 index 00000000..18518d58 --- /dev/null +++ b/blog-site/public/posts/resume/interview-resume-20201124/index.html @@ -0,0 +1,607 @@ + + + + + + + + + + + 20201124简历 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

20201124简历

+ 2020.11.24 +
+

自我介绍

+ + +
+
+
+
+ +
+ 1998 · 李济芝 +
+
+
+ +
+ 河北唐山 +
+
+
+
+
+ +
+ 15176733539 +
+
+
+ +
+  m15176733539@163.com +
+
+
+

专业技能

+
    +
  • 熟练使用 SSM,SpringBoot等框架技术;
  • +
  • 熟练使用HTML,CSS等相关技术;
  • +
  • 有Redis,VUE相关使用经验;
  • +
  • 有对接第三方系统,调用外系统相关经验;
  • +
  • 熟悉 MySQL,ORACLE.基本操作,熟练使用SQL;
  • +
  • 熟悉Maven ,Gradle依赖管理依赖工具操作, git版本控制工具;
  • +
  • 掌握Linux OS.Mac OS.Window10基本操作;
  • +
  • 了解SQL优化.JVM相关配置;
  • +
  • 了解Docker.MQ.等技术;
  • +
  • 良好的编码习惯对代码有”洁癖”,有一定的文档编写能力;
  • +
  • 良好的沟通表达能力,学习及领悟能力,较强的责任心与团队精神;
  • +
+

项目经历

+

shopping-mall校园商城系统

+

系统使用springboot,spring,springmvc,mybatisplus,mysql,redis,Tomcat进行开发,采用进行docker部署 +主要模块功能:

+
    +
  • 商品展示功能: springboot整合redis实现
  • +
  • 商品搜索功能: springboot整合elasticsearch实现
  • +
  • 支付订单功能: springboot整合支付宝接口实现
  • +
  • 短信注册功能: 短信验证接口
  • +
+

该系统全部都由本人完成,包括项目文档编写,接口设计,数据库,设计页面设计

+

highspeed直流远程供电监控系统

+

项目周期:2019.2~2019.10

+

在校期间 参与远程直流供电系统,后管系统建设。系统基于springboot进行开发、前后端分离。主要分为参数模块,设备模块等。 主要负责 设备模块及主要模块开发。

+

贵州银行”紫薇工程”– 电子验印系统

+

项目周期:2019.11~至今

+

新系统基于电子验印系统原型进行升级、改造.采用前后端分离。后端基于springboot 开发,前端基于Vue.js进行开发。新电子验印系统集成到客户端。 主要负责电子验印系统前端开发、后管系统开发、接口功能开发(柜面接口服务等) 、外系统对接(集中作业系统,影像平台系统,银企对账系统等)等及相关维护,通过easyOps发布流水线持续发版更新等。

+

教育经历

+
    +
  • 2017.09 ~ 2020.06 就读于河北软件学院 专业为软件技术 学历为专科
  • +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/resume/interview-resume-20220422/index.html b/blog-site/public/posts/resume/interview-resume-20220422/index.html new file mode 100644 index 00000000..9df3aae7 --- /dev/null +++ b/blog-site/public/posts/resume/interview-resume-20220422/index.html @@ -0,0 +1,796 @@ + + + + + + + + + + + 20220422简历 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

20220422简历

+ 2022.04.22 +
+

自我介绍

+ + +
+
+
+
+ +
+ 1998 · 李济芝 +
+
+
+ +
+ 河北唐山 +
+
+
+
+
+ +
+ 15176733539 +
+
+
+ +
+  m15176733539@163.com +
+
+
+

+ 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和三年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对SQL能够进行分析调优、对Java服务端程序故障能独立排查。工作责任心强,具有一定的承压能力。 +

+
+
+

求职意向

+
    +
  • 期望城市:北京
  • +
  • 工作薪资: 面议
  • +
+

专业技能

+
    +
  • 具备扎实的Java基础,熟练使用Java集合、Java IO、多线程、反射等技术;
  • +
  • 熟悉多线程及使用,掌握线程池底层实现原理,有多线程方面开发经验;
  • +
  • 熟练使用SpringSpring BootNettyKafkaRedisElasticsearch等开源框架;
  • +
  • 熟悉微服务架构,Spring CloudSpring CloudAlibaba体系;
  • +
  • 较深入理解SpringNetty框架,研究过核心源码,较为熟练借鉴框架中的设计,具备一定框架定制开发能力;
  • +
  • 熟练编写SQL、视图及存储过程,熟练使用索引和执行计划进行数据库调优;
  • +
  • 熟读阿里巴巴开发手册,有良好的编程习惯,掌握面向对象编程,熟悉常见数据结构,开发过程中大量使用设计模式进行代码编写;
  • +
+

项目经历

+

成联电商 2022.4~今

+

公司主要做“产业电商官网”模式,旗下运营:“中国耐材之窗网”、“冶金炉料网” 、“中国陶瓷官网”、“中国物流官网”四大行业电商平台,在职期间主要负责维护“中国耐材之窗网”和建设耐材部门相关项目。

+

项目一:中国耐材之窗网

+
    +
  • 简介:网站主要展示一些耐火材料行业相关的资讯信息,至今共为其迭代80多个版本。 +由于项目较为混乱复杂,所以利用设计模式梳理业务代码(部分重构等),,将定时任务从项目中拆出,使得项目耦合度降低,复杂度降低,将定时任务可视化,极大方便了管理; +将 Nacos 作为配置中心整合到项目中,使其配置支持热部署,方便管理,同时为项目拆分转微服务奠定基础。
  • +
  • 工作职责: +
      +
    • 维护系统正常运行,完成阶段性业务需求,使用Java设计模式设计了一些较为复杂的业务功能,使其扩展性、可重用性大大增强,提高了开发效率;
    • +
    • 引入ElasticSearch重构了全局搜索功能,响应速度提高近三倍,提升了用户体验;
    • +
    • 利用Redis自增方法,实现访问量统计功能;使用Redis作为浏览轨迹功能写入缓存,并解决缓存击穿问题,极大提升了系统响应速度;
    • +
    • 优化程序慢SQL,例价格信息栏目慢SQL,响应时间由6055ms提升至332ms,提升了用户交互体验;
    • +
    • 使用复合设计模式接入了支付宝支付和微信支付功能,使其扩展性,可维护性大大增强;
    • +
    • 使用SpringBoot监听机制实现了微信公众号消息推送等功能,大大降低了业务耦合,提升了代码扩展性,提高了开发效率;
    • +
    • 利用Webflux异步非堵塞的特性,实现服务端消息推送功能;接入第三方上传文件功能: 阿里云,FastDFS;
    • +
    • 处理过线上由cpu飙升引起的系统卡顿问题;
    • +
    • 拆分项目,将项目中的定时任务拆出,形成单独项目,使得项目耦合度降低;
    • +
    • 将Nacos作为配置中心整合到项目中,使项目配置支持热部署,方便管理,同时为项目拆分重构转微服务做准备;
    • +
    +
  • +
+

项目二:河北国亮耐火材料电子交易平台

+
    +
  • 简介:国亮公司的耐火材料交易平台,在职期间负责用户模块、首页模块、关于我们模块开发和一些交易处理。
  • +
  • 工作职责: +
      +
    • 开发用户登录、注册、短信邮件发送功能及维护用户的一些接口,使用JWT进行认证操作;
    • +
    • 引入Redisson分布式锁解决用户重复提交,处理数据串行化;
    • +
    • 为保证程序高性能,引入Redis将项目首页数据加入缓存,并解决双写一致性问题;
    • +
    • 大表取消数据库join,使用内存join并引入多线程并发查询,查询速度提升近5倍;
    • +
    • 利用Spring AOP特点整合了一些公共方法,如记录日志,公共校验,敏感词过滤等,提高了代码的复用性,增强了代码的可维护性;
    • +
    +
  • +
+

北京利联 2019.11 ~ 2022.1

+

公司主要为金融行业提供计算机软件服务,在职期间主要负责建设验印系统和票据影像交换系统。

+

项目一:盛京银行-票据影像交换系统

+
    +
  • 简介:票据影像交换系统,即交换银行与银行之间的凭证对其进行验印。盛京银行-票据影像交换系统,基于旧系统改造。新票据影像交换系统采用TCP/IP与其他系统通讯,后端使用Spring Boot、Netty +框架与其他系统进行交互。
  • +
  • 工作职责: +
      +
    • 修复旧系统历史遗留问题,完成现阶段业务需求;
    • +
    • 采用Spring Boot重构旧系统项目结构,使新系统能够用可执行war包的形式发布,运行、开发更便捷,提升近一倍开发测试效率;
    • +
    • 使用模版、策略设计模式对接第三方系统(支付系统、柜面系统、影像平台系统等),提高代码的可读性和扩展性;
    • +
    • 使用Netty作为服务方,与其他系统进行交互,利用NIO提高了系统交互速度;
    • +
    • 重构旧系统不规范代码及查询报表统计导出,优化SQL,提升了近一倍程序运行速度;
    • +
    • 将导出excel主要步骤由后端迁移到前端,解决了导出报表数据量较大问题,导出速度提升5倍以上;
    • +
    • 负责对接盛京银行单点登录系统,使用Redis解决了Session不共享的问题;
    • +
    +
  • +
+

项目二:贵州银行-电子验印系统

+
    +
  • 简介:电子验印系统,即通过该程序可以核对凭证上印章与银行预留印章是否匹配。贵州银行-新电子验印系统,基于验印系统原型进行改造,推翻原型所有代码进行重建。新电子验印系统采用前后端分离设计,将前端部分集成到客户端中;后端基于Spring Boot +等主流框架进行开发,前端基于Vue.js进行开发,采用分布式部署。
  • +
  • 工作职责: +
      +
    • 独立开发电子验印管理系统,采用前后端分离架构;
    • +
    • 使用Vue、Element UI构建前端;
    • +
    • 为适应产品特性,加快迭代速度,后端采用Spring Cloud分布式架构,使用devops集中式配置管理,提高了接近1倍的开发效率;
    • +
    • 采用Redis实现了高频信息缓存,加快了请求响应速度,降低了50%以上的数据库压力;
    • +
    • 使用Kafka异步解耦机制对接第三方接口功能开发(柜面接口服务等)、外系统对接(集中作业系统,影像平台系统,银企对账系统等)提高了程序的响应速度,保证了数据最终一致性;
    • +
    • 使用Java多线程,开发定时同步文件传输平台程序,提高了50%同步文件速度;
    • +
    • 采用Nginx和devops进行前端部署和反向代理增强了软件系统的安全性;
    • +
    • 为提升开发效率,接入Gitlab CI/CD进行持续集成和持续部署,实现了构建部署自动化;
    • +
    • 独立使用linux命令进行测试环境、生产环境故障排查、程序部署、及数据迁移;
    • +
    • 负责沟通协调项目中遇到的程序问题与业务问题,及撰写单元测试报告、接口说明文档、系统使用手册等相关文档;
    • +
    +
  • +
+

教育经历

+
    +
  • 2017.09 ~ 2020.06 就读于河北软件学院 专业为软件技术 学历为专科
  • +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/resume/interview-resume-20230915/index.html b/blog-site/public/posts/resume/interview-resume-20230915/index.html new file mode 100644 index 00000000..44fcd58f --- /dev/null +++ b/blog-site/public/posts/resume/interview-resume-20230915/index.html @@ -0,0 +1,802 @@ + + + + + + + + + + + 20230915简历 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

20230915简历

+ 2023.09.15 +
+

自我介绍

+ + +
+
+
+
+ +
+ 1998 · 李济芝 +
+
+
+ +
+ 河北唐山 +
+
+
+
+
+ +
+ 15176733539 +
+
+
+ +
+  m15176733539@163.com +
+
+
+

+ 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和四年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对Java服务端程序故障能独立排查。工作责任心强,具有一定的承压能力。 +

+
+
+

求职意向

+
    +
  • 期望城市:北京
  • +
  • 工作薪资: 面议
  • +
+

专业技能

+
    +
  • 具备扎实的Java基础,熟练使用Java集合、Java IO、多线程、反射等技术;熟悉java核心的集合框架;
  • +
  • 熟悉多线程及使用,掌握线程池底层实现原理,有多线程方面开发经验;
  • +
  • 熟悉微服务架构,Spring CloudSpringcloud Alibaba体系,对分布式微服务,旧服务改造,服务划分、服务治理、服务分层都有深入理解,有线上项目经验;
  • +
  • 熟练掌握 Redis,深入了解底层网络模型、底层数据结构、持久化机制,有分布式锁、分布式缓存的设计经验;
  • +
  • 较深入理解SpringNetty框架,研究过核心源码,较为熟练借鉴框架中的设计,具备一定框架定制开发能力;
  • +
  • 熟练编写SQL、视图及存储过程,熟练使用索引和执行计划进行数据库调优;
  • +
  • 熟读阿里巴巴开发手册,有良好的编程习惯,掌握面向对象编程,开发过程中大量使用设计模式进行代码编写;具有较强的分析设计能力,熟悉系统的性能调优和故障排查;
  • +
+

项目经历

+

成联电商 2022.4~今

+

公司主要做“产业电商官网”模式,旗下运营:“中国耐材之窗网”、“冶金炉料网” 、“中国陶瓷官网”、“中国物流官网”四大行业电商平台,在职期间主要负责维护“中国耐材之窗网”正常运行和建设耐材部门相关项目。

+

项目一:中国耐材之窗网

+

简介:网站主要展示一些耐火材料行业相关的资讯信息,至今共为其迭代80多个版本。

+

技术栈:Springboot、Springcloud alibaba、 Nacos、Mybatisplus、Redis、Elasticsearch、xxljob

+

工作职责:

+
    +
  1. 负责底层组件开发与设计
  2. +
+
    +
  • 独立负责ElasticSearch组件的设计与开发。基于es 7.*版本封装,采用High Level REST Client,提供创建Index、删除Index、数据的增、删、改、查等能力,业务侧查询覆盖模糊查询、批量查询、批量新增、批量修改、排序等功能,对使用方友好屏蔽es操作细节。组件上线后,在客户前端全局搜索场景得到深度使用,经过充分压测,节点性能QPS轻松支持 2000+;
  • +
  • 独立负责系统分布式锁组件的设计与开发。核心实现方案通过集成Redisson框架,采用注解+AOP实现组件与业务解耦,提供锁互斥、阻塞与非阻塞锁、锁安全释放线程、锁重入、锁续期、公平锁等常见的功能,另外利用分布式锁实现后端应用管理、积分规则维护等功能防重提交。组件上线后,在积分兑换等场景得到大量应用,经过充分压测,分布式锁单节点性能QPS支持1000+;
  • +
  • 独立负责业务管道流组件设计与开发。参照Netty中的pipeline管道实现,在编写业务代码时,将数据从一个阶段传递到下一个阶段的方法。每个阶段执行特定的数据处理操作,从而实现了业务代码上的解耦,避免类膨胀,又将管道与配置相结合,实现了跳链功能,增加其灵活性;
  • +
+
    +
  1. 负责系统重构,解决历史技术债务
  2. +
+
    +
  • 使用Java设计模式设计了一些较为复杂的业务功能,使其扩展性、可重用性大大增强,提高了开发效率;
  • +
  • 通过多种设计模式的组合,重构支付模块(目前已接入微信、支付宝)、重构文件上传模块,(目前已接入阿里云、FastDFS)接入新渠道的开发周期,从之前的8人天降低到4人天;
  • +
  • 通过引入Elasticsearch,重构了全局搜索功能,响应速度提高近10倍,极大提升了用户体验;
  • +
+
    +
  1. 负责系统技术架构升级与改造
  2. +
+
    +
  • 系统模块目录非常混乱,缺乏统一的规范,重复建设严重,通过基于业务来进行功能目录划分,公共的技术组件下沉到底层framework,开发效率提升一倍;
  • +
  • 业务定时任务从传统的spring task,迁移到xxljob分布式调度任务,方便任务可视化管理,任务执行的稳定性也有很大提升;
  • +
+
    +
  1. 负责核心功能模块设计与开发
  2. +
+
    +
  • 负责用户浏览量功能设计与开发。利用Redis自增方法,实现访问量统计功能;使用Redis作为浏览轨迹功能写入缓存,并解决缓存击穿问题,极大提升了系统响应速度;
  • +
  • 负责用户登录、注册、短信邮件发送功能及维护用户的一些接口。技术侧,基于JWT Token实现用户鉴权,实现用户后台拉黑、Token续期,跨端鉴权等功能;
  • +
  • 完成首页相关接口的优化与开发。将项目首页数据加入本地缓存redis缓存, 为了保证DB和Redis数据一致性,配置更新后,更新DB,通过MQ广播消息,各个应用节后收到MQ后,从DB中查询信息回写到本地缓存和redis缓存,有效时间设置有效截止时间+1天,同时设计一个兜底的定时任务定时刷新,将DB数据刷新到缓存保证整个全局一致性;
  • +
+
    +
  1. 负责生产疑难问题排查与优化处理
  2. +
+
    +
  • 采用随机睡眠机制防止redis CPU飙高。分页每次处理500条数据写入redis后,随机睡眠0-200ms,防止redis CPU短时间内飙高;
  • +
  • 实时查询改为定时查询。之前每次查库3个维度计算都在一个SQL中处理,长事务,平均耗时3s左右。优化方案为每隔30分钟,根据交易明细数据进行统计,将相关数据处理完成后写入到redis,耗时降为300ms,提升10倍;
  • +
  • 项目产品列表信息报文非常大,用户查询,请求量非常大,高并发下redis IO容易被打爆,为了解决这个难题,将之前的redis存储改为list存储,采用拆分大key的方案,同时redis key按项目分开存储,对redis value进行压缩,剔除非必要字段(比如创建时间、创建人、备注等),优化完成后,redis cpu从高峰的60%稳定在15%左右;
  • +
  • 控制单次写表数量。通过重写mybatis拦截器,当业务单次提交记录大于动态配置开关配置的N条,分批次提交,防止数据库CPU飙高;
  • +
  • 大表取消数据库join,使用内存join并引入多线程并发查询,查询速度提升近5倍;
  • +
+

工作业绩:

+
    +
  1. 成中国耐材之窗网核心功能的设计与开发、系统重构。系统上线后比较稳定,用户投诉很少,整体体验很流畅;
  2. +
  3. 完成部分核心接口的性能优化。响应时间从3s降低到0.3s,性能提升10倍;
  4. +
  5. 多次解决线上各种疑难问题。例如:redis大key、线上CPU飙高等;
  6. +
+

北京利联 2019.10 ~ 2022.1

+

公司主要为金融行业提供计算机软件服务,在职期间主要负责建设验印系统和票据影像交换系统。

+

项目一:盛京银行-票据影像交换系统

+

简介:票据影像交换系统,即交换银行与银行之间的凭证对其进行验印。盛京银行-票据影像交换系统,基于旧系统改造。

+

技术栈:Springboot、Netty、Springcloud alibaba、 Nacos、Mybatisplus、Redis

+

工作职责:

+
    +
  1. 负责系统技术架构改造
  2. +
+
    +
  • 技术架构从原始的spring升级到springboot架构,通过自身内嵌的tomcat容器运行,基于CI/CD,可快速完成应用编译、打包、部署,提升研发交付效率;
  • +
  • 使用Netty作为服务方,与其他系统进行交互,利用NIO提高了系统交互速度;
  • +
  • 将 Nacos 作为配置中心整合到项目中,使其配置支持热部署,方便管理,同时为项目拆分转微服务奠定基础;
  • +
+
    +
  1. 负责核心功能模块设计与开发
  2. +
+
    +
  • 使用模版、策略设计模式对接第三方系统(支付系统、柜面系统、影像平台系统等),提高代码的可读性和扩展性;
  • +
  • 负责对接盛京银行单点登录系统,通过Redis解决了Session不共享的问题;
  • +
  • 利用Spring AOP整合了一些公共方法,如记录日志,公共校验,敏感词过滤等,提高了代码的复用性,增强了代码的可维护性;
  • +
+
    +
  1. 负责生产疑难问题优化
  2. +
+
    +
  • 重构旧系统不规范代码及查询报表统计导出,通过执行计划,判断索引是否命中来优化SQL,提升了近5倍程序运行速度;
  • +
  • 将导出excel主要步骤由后端迁移到前端,解决了导出报表数据量较大问题,导出速度提升10倍以上;
  • +
+

项目二:贵州银行-电子验印系统

+

简介:电子验印系统,即通过该程序可以核对凭证上印章与银行预留印章是否匹配。贵州银行-新电子验印系统,基于验印系统原型进行改造,推翻原型所有代码进行重建。新电子验印系统采用前后端分离设计,将前端部分集成到客户端中;后端基于Spring Boot等主流框架进行开发,前端基于Vue.js进行开发,采用分布式部署。

+

技术栈:Springboot、Springcloud、Redis、Kafka、Mybatisplus、Vue、Hystrix

+

工作职责:

+
    +
  1. 负责核心功能模块设计与开发
  2. +
+
    +
  • 独立开发电子验印后台管理系统,采用前后端分离架构,使用Vue、Element UI构建前端,为适应产品特性,加快迭代速度,后端采用Spring Cloud分布式架构,使用devops集中式配置管理,提高了接近1倍的开发效率;
  • +
  • 使用Kafka异步解耦机制对接第三方接口功能开发(柜面接口服务等)、外系统对接(集中作业系统,影像平台系统,银企对账系统等)提高了程序的响应速度,保证了数据最终一致性;
  • +
+
    +
  1. 负责生产疑难问题排查与优化处理
  2. +
+
    +
  • 优化数据批量入库。通过开启allowMultiQueries=true&rewriteBatchedStatements=true参数配置,通过mybatis的foreach标签批量插入或更新数据,提升批量写库性能;
  • +
  • kafka消息积压优化。通过扩kafka分区,从之前的4分区扩到8分区;日志保留时间从1个月到2天;调整文件刷盘策略:写入10000条消息或间隔2s,刷数据到磁盘;同步提交改为异步提交;
  • +
  • 优化同步文件程序。利用Java多线程技术,将大文件拆分成小块,并通过多线程同时处理这些小块,充分利用多核处理器的并行性,同时传输多个文件,避免了传统单线程传输时的等待阻塞,从而最大程度地减少文件传输的总耗时;
  • +
  • 优化查询高频信息速度。在系统启动时,通过预热将一些热门数据加载到内存中,减少冷启动时的性能影响;
  • +
+
    +
  1. 负责项目上线及部署
  2. +
+
    +
  • 采用Nginx和devops进行前端部署和反向代理增强了软件系统的安全性;
  • +
  • 为提升开发效率,接入Gitlab CI/CD进行持续集成和持续部署,实现了构建部署自动化;
  • +
  • 独立使用linux命令进行测试环境、生产环境故障排查、程序部署、及数据迁移;
  • +
+

教育经历

+
    +
  • 2017.09 ~ 2020.06 就读于河北软件学院 专业为软件技术 学历为专科
  • +
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/spring/java-spring-mvc-webflux/index.html b/blog-site/public/posts/spring/java-spring-mvc-webflux/index.html new file mode 100644 index 00000000..39dc9a55 --- /dev/null +++ b/blog-site/public/posts/spring/java-spring-mvc-webflux/index.html @@ -0,0 +1,638 @@ + + + + + + + + + + + SpringMVC与SpringWebFlux | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

SpringMVC与SpringWebFlux

+ 2023.04.14 +
+

Spring MVC

+

Spring Web MVC是建立在Servlet API上的原始Web框架,从一开始就包含在Spring框架中。正式名称 “Spring Web MVC “来自其源模块的名称(spring-webmvc),但它更常被称为 “Spring MVC”。

+

SpringMVC是基于Spring的,是Spring中的一个模块,专门用来做web开发使用的。

+
    +
  • Model 封装了数据与进行数据进行处理的代码,是实际经行数据处理的地方,也是与数据库交互的地方
  • +
  • View 负责将应用显示给用户和显示模型的状态,一般来说,它生成客户端浏览器可以解释的 HTML 输出;
  • +
  • Controller 负责视图和模型之间的交互,并将其传递给视图进行渲染;
  • +
+

SpringMVC也是一个容器,使用IoC核心技术,管理界面层中的控制器对象。SpringMVC的底层就是servlet,以servlet为核心,接收请求、处理请求,显示处理结果给用户。在此之前这个功能是由Servlet来实现的,现在使用SpringMVC来代替Servlet行驶控制器的角色和功能。 其核心Servlet是:DispatcherServlet。

+

更多详情查看官方资料这里不在赘述。

+

MVC架构

+

MVC架构图

+

MVC模式是一种将应用程序分为三个主要部分的架构模式,分别是模型(Model)、视图(View)和控制器(Controller)。模型负责处理数据,视图负责展示数据,控制器负责协调模型和视图之间的交互。

+

在没有使用SpringMVC之前都是使用Servlet在做Web开发。但是使用Servlet开发在接收请求参数,数据共享,页面跳转等操作相对比较复杂。servlet是java进行web开发的标准,既然springMVC是对servlet的封装,那么很显然SpringMVC底层就是Servlet,SpringMVC就是对Servlet进行深层次的封装。

+

SpringMVC 是一种基于 Spring 框架的 MVC 设计模型,它是 Spring 框架的一个子项目。SpringMVC 的核心思想是将 MVC 设计模式应用于 Spring 框架,实现了请求-响应模式,将业务逻辑、数据、显示分离,提高了部分代码的复用性,降低了各个模块间的耦合性。

+

请求处理流程

+

SpringMVC原理图

+

流程描述:

+
    +
  • 用户发送请求至前端控制器 DispatcherServlet;
  • +
  • DispatcherServlet 收到请求调用 HandlerMapping 处理器映射器;
  • +
  • 处理器映射器找到具体的处理器(可以根据xml配置、注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给 DispatcherServlet;
  • +
  • DispatcherServlet 调用 HandlerAdapter 处理器适配器;
  • +
  • HandlerAdapter 经过适配调用具体的处理器(Controller,也叫后端控制器);
  • +
  • Controller 执行完成返回 ModelAndView;
  • +
  • HandlerAdaptercontroller执行结果 ModelAndView 返回给 DispatcherServlet;
  • +
  • DispatcherServletModelAndView 传给 ViewReslover 视图解析器;
  • +
  • ViewReslover 解析后返回具体 View;
  • +
  • DispatcherServlet 根据 View 进行渲染视图(即将模型数据填充至视图中);
  • +
  • DispatcherServlet 响应用户;
  • +
+

语法代码示例

+
    <dependency>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-web</artifactId>
+    </dependency>
+
@Controller
+@RequestMapping("/hello")
+public class HelloController{
+ 
+   @RequestMapping(method = RequestMethod.GET)
+   public String printHello(ModelMap model) {
+      model.addAttribute("message", "Hello Spring MVC Framework!");
+      return "hello";
+   }
+
+}
+

Spring WebFlux

+

与Spring Web MVC并行,Spring Framework 5.0引入了一个响应式Web框架,其名称为 “Spring WebFlux”。与Spring MVC不同,它不需要Servlet API,完全异步和非阻塞,又被叫做响应式WebClient。

+

Spring WebFlux 模块将默认的 web 服务器改为 Netty,所以具有Netty的特点,是完全非阻塞式的。通过少量的容器线程就可以支撑大量的并发访问,有一种池化思想在里面,所以 Spring WebFlux 可以有效提升系统的吞吐量和伸缩性但是并不能使接口的响应时间缩短。

+

Spring WebFlux 是一个异步非阻塞式的 Web 框架,所以,它特别适合应用在 IO 密集型的服务中,比如微服务网关这样的应用中。

+
    +
  • 比如一个日志监控系统,我们的前端页面将不再需要通过“命令式”的轮询的方式不断向服务器请求数据然后进行更新,而是在建立好通道之后,数据流从系统源源不断流向页面,从而展现实时的指标变化曲线;
  • +
  • 再比如一个社交平台,朋友的动态、点赞和留言不是手动刷出来的,而是当后台数据变化的时候自动体现到界面上的。
  • +
+

更多信息请查看官方资料这里不在赘述。

+

请求处理流程

+

Spring WebFlux 底层实现依赖 reactor 和 netty。Spring 做的就是通过抽象和封装,把 reactor 的能力通过 Controller 来使用。

+

SpringMVC原理图

+

请求执行的流程大致和SpringMVC差不多,SpringMVC核心控制器是DispatcherServlet,SpringWebFlux核心处理器是DispatcherHandler:

+
    +
  • 先是通过RequestMapping,拿到HandlerMethod处理器;
  • +
  • 在通过HandlerMethod寻找合适的HandlerAdapter;
  • +
  • 拿到合适的适配器对象之后,根据不同规则的Handler执行不同的Handler,这里就包含Controller
  • +
  • Handler的执行结果返回HandlerResult对象,触发handlerResult方法,针对不同的返回类找到不同的HandlerResultHandler如视图渲染ViewResolutionResultHandler、ServerResponseResultHandler、ResponseBodyResultHandler、ResponseEntityResultHandler不同容器有不同的实现,如Reactor,Jetty,Tomcat等;
  • +
+

语法代码示例

+
    <dependency>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-webflux</artifactId>
+    </dependency>
+
@RestController
+@RequestMapping("/webflux")
+public class HelloController {
+
+    @GetMapping("/hello")
+    public Mono<String> hello() {
+        return Mono.just("Hello Spring Webflux");
+    }
+
+    @GetMapping("/posts")
+    public Flux<Post> posts() {
+        WebClient webClient = WebClient.create();
+        Flux<Post> postFlux = webClient.get().uri("http://jsonplaceholder.typicode.com/posts").retrieve().bodyToFlux(Post.class);
+        return postFlux;
+    }
+    
+}
+

在 WebFlux 中 Mono 和 Flux 均能充当响应式编程中发布者的角色:

+
    +
  • Mono:返回 0 或 1 个元素,即单个对象;
  • +
  • Flux:返回 N 个元素,即 List 列表对象;
  • +
+

两者使用对比及建议

+

SpringMVC原理图

+

Spring WebFlux 并不是 Spring MVC 的替代方案,两者可以混合使用。都可以使用 Spring MVC 注解,如 @Controller、@RequestMapping等。均可以使用 Tomcat, Jetty, Undertow Servlet 容器。

+

Spring WebFlux相比较Spring MVC最大的优势在于它是异步非堵塞的框架,可以让我们在不扩充硬件资源的前提下,提升系统的吞吐量和伸缩性;但是由于是异步非堵塞的框架,对于开发人员来说调试起来不太友好。 +而 Spring MVC 是同步阻塞的,如果你目前在 Spring MVC 框架中大量使用异步方案,那么,WebFlux 才是你想要的,否则,使用 Spring MVC 才是你的首选。

+

官方建议:

+
    +
  • 如果已经有了一个运行良好的 SpringMVC 应用程序,则无需更改。命令式编程是编写、理解和调试代码的最简单方法,我们可以选择最多的库,因为从历史上看,大多数都是阻塞的。
  • +
  • 如果是个新应用且决定使用 非阻塞 Web 技术栈,那么 WebFlux 是个不错的选择。
  • +
  • 对于使用 Java8 Lambda 或者 Kotlin 且 要求不那么复杂的小型应用程序或微服务来说,WebFlux 也是一个不错的选择
  • +
  • 在微服务架构中,可以混合使用 SpringMVC 和 Spring WebFlux,两个都支持基于注解的编程模型
  • +
  • 评估应用程序的一种简单方法是检查其依赖关系。如果要使用阻塞持久性 API(JPA、JDBC)或网络 API,那么 Spring MVC 至少是常见架构的最佳选择
  • +
  • 如果有一个调用远程服务的 Spring MVC 应用程序,请尝试响应式WebClient
  • +
  • 对于一个大型团队,向非阻塞、函数式和声明式编程转变的学习曲线是陡峭的。在没有全局开关的情况下,想启动 WebFlux,可以先使用 reactive WebClient。此外,从小处着手并衡量收益。我们预计,对于广泛的应用,这种转变是不必要的。
  • +
+

当我们引入一门新技术到原有的项目中,我们需要评估究竟为我们带来多少益处,同时还要评估为了这些益处所要付出的学习和改造成本,然后衡量收益,如果收益大值得则值得尝试。不能为了装逼而装逼,为了技术而技术。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/spring/java-spring/index.html b/blog-site/public/posts/spring/java-spring/index.html new file mode 100644 index 00000000..57bbfa85 --- /dev/null +++ b/blog-site/public/posts/spring/java-spring/index.html @@ -0,0 +1,2893 @@ + + + + + + + + + + + Spring详解 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Spring详解

+ 2021.05.13 +
+

概览

+

Spring是一个轻量级的Java开源框架,为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IOC)和面向切面(AOP)。

+

简单来说,Spring是一个分层的JavaSE/EE 一站式轻量级开源框架。在每一层都提供支持。

+
    +
  • 表示层:spring mvc
  • +
  • 业务层:spring
  • +
  • 持久层:jdbctemplate、spring data
  • +
+

Spring详解-001

+

对Spring的理解

+

Spring是一个轻量级的框架,简化我们的开发,里面重点包含两个模块分别是IOC和AOP。

+
    +
  • IOC叫控制反转,在没用IOC之前都要手动new创建对象,使用IOC之后由容器进行对象的创建,并且由容器来管理对象,减去了开发上的成本,提高了工作效率。
  • +
  • AOP叫面向切面编程,在实际项目开发中需要嵌入一些与业务不想关的代码的时候就可以使用AOP。比如,权限日志的增加。
  • +
+

Spring虽然把它当成框架来使用,但其本质是一个容器,即IOC容器,里面最核心是如何创建对象和管理对象,里面包含了Bean的生命周期和Spring的一些扩展点,包含对AOP的应用。 +除此之外,Spring真正的强大之处在于其生态,它包含了Spring Framework、Spring Boot、Spring Cloud等一些列框架,极大提高了开发效率。

+

Spring 启动流程

+

参考:https://blog.csdn.net/scjava/article/details/109587619

+

Spring详解-004

+

核心方法AbstractApplicationContext#refresh()

+
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) {
+         // ... 
+      }
+
+      finally {
+         // ...
+      }
+  }
+}
+
    +
  1. prepareRefresh 准备刷新容器,此方法做一些刷新容器的准备工作: +
      +
    • 设置开启时间和对应标志位
    • +
    • 获取环境对象
    • +
    • 设置监听器和一些时间的集合对象
    • +
    +
  2. +
  3. obtainFreshBeanFactory 创建容器对象:DefaultListableBeanFactory;加载xml配置文件属性值到工厂中,最重要的是BeanDefinition
  4. +
  5. prepareBeanFactory 完成bean工厂的某些初始化操作 +
      +
    • 设置BeanDefinition的类加载器
    • +
    • 设置spring容器默认的类型转换器
    • +
    • 设置spring解析el表达式的解析器
    • +
    • 添加一个Bean的后置处理器ApplicationContextAwareProcessor
    • +
    • 将bean工厂的一些类,比如ApplicationContext直接注册到单例池中
    • +
    • 去除一些在byType或者byName的时候需要过滤掉的一些bean(spring在依赖注入的时候会先在这些默认注册的bean中进行byType找,如果找到了,就加入到列表中,简单来说就是比如你在bean中依赖注入了ApplicationContext context,那么spring会把默认注册的这些bean中找到然后进行注册)
    • +
    • 将系统的环境信息、spring容器的启动环境信息、操作系统的环境信息直接注册成一个单例的bean
    • +
    +
  6. +
  7. postProcessBeanFactory 这里是一个空壳方法,spring目前还没有对他进行实现;这个方法是留给子类进行实现的,后续可以添加一些用户自定义的或者默认的一些特殊的后置处理器工程到beanFactory中去
  8. +
  9. invokeBeanFactoryPostProcessors 调用后置处理器;将系统中所有符合条件的普通类都扫描成了一个BeanDefinition 并且放入到了beanDefinitionMap中,包括业务的bean,ban的后置处理器、bean工厂的后置处理器等等 +
      +
    • 将标记为容器单例类扫描成BeanDefinition放入BeanDefinition Map
    • +
    • 处理@Import注解
    • +
    • 如果我们的配置类是@Configuration的,那么会生成这个配置类的CGLIB代理类,如果没有加@Configuration,则就是一个普通Bean
    • +
    +
  10. +
  11. registerBeanPostProcessors 从beanDefinitionMap中取出bean的后置处理器然后放入到后置处理器的缓存列表中
  12. +
  13. initMessageSource 初始化国际化资源信息
  14. +
  15. initApplicationEventMulticaster 事件注册器初始化
  16. +
  17. onRefresh 空壳方法,留给子类实现
  18. +
  19. registerListeners 将容器中和BeanDefinitionMap中的监听器添加到事件监听器中
  20. +
  21. finishBeanFactoryInitialization 创建单例池,将容器中非懒加载的Bean,单例bean创建对象放入单例池中,包括容器的依赖注入
  22. +
  23. finishRefresh 容器启动过后,发布事件
  24. +
+

Spring循环依赖与三级缓存

+

Spring详解-003

+

Spring循环依赖调用流程:

+

在BeanA中注入BeanB,BeanB中注入BeanA,在BeanA创建的过程中,会先判断容器中A是否存在,如果不存在会先初始化BeanA,然后给BeanA赋值,此时会给BeanA里的BeanB属性赋值,在赋值之前会将创建BeanA的流程放到三级缓存中(三级缓存为Map结构,key为String,value为函数式接口); 由于BeanA里面包含BeanB,所以接下来给BeanB执行创建流程,判断容器中是否存在BeanB,给属性B赋值,此时会给BeanB里的BeanA属性赋值。

+

在判断容器中是否存在该Bean时,查找顺序为:一级缓存->二级缓存->三级缓存,经历过上面的步骤后,此时三级缓存中A和B都有值(为BeanA、B的创建流程),不需要再进行初始化操作,然后将会执行BeanA的创建流程并将其放入二级缓存中并删除三级缓存中的值,但是此时BeanA中的BeanB还未赋值进行完全的初始化, +BeanA已经创建,此时会将BeanA赋值给BeanB中的A属性,至此BeanB已经完全赋值,然后将完全赋值的BeanB放入一级缓存中并删除三级缓存中的值,由于BeanB已经完全赋值,此时将其赋值给BeanA,将BeanA放入一级缓存并删除二级缓存,至此循环依赖问题解决。

+

Spring循环依赖大致调用思路:

+
    +
  • 第一次:A,容器是否存在?(一级缓存->二级缓存->三级缓存)初始化A,-> 将A的创建流程加入三级缓存 -> 给A赋值
  • +
  • 第二次:B,容器中是否存在?(一级缓存->二级缓存->三级缓存)初始化B -> 将B的创建流程加入三级缓存 -> 给B赋值
  • +
  • 第三次:A的三级缓存中有值,不需要进行初始化操作,执行创建A的流程,将其放入二级缓存,返回值给到创建B,此时B已经创建完全,将其加入一级缓存,然后将该返回值给到A,将A加入一级缓存,至此循环依赖问题解决。
  • +
+

SpringBoot

+

官网地址:https://spring.io/projects/spring-boot

+
+

SpringBoot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。 +该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。SpringBoot 提供了一种新的编程范式,可以更加快速便捷地开发 Spring 项目,在开发过程当中可以专注于应用程序本身的功能开发,而无需在 Spring 配置上花太大的工夫。

+
+

SpringBoot 基于 Sring4 进行设计,继承了原有 Spring 框架的优秀基因。SpringBoot 准确的说并不是一个框架,而是一些类库的集合。maven 或者 gradle 项目导入相应依赖即可使用 SpringBoot,而无需自行管理这些类库的版本。

+

特点:

+
    +
  • 独立运行的 Spring 项目: +SpringBoot 可以以 jar 包的形式独立运行,运行一个 SpringBoot 项目只需通过 java–jar xx.jar 来运行。
  • +
  • 内嵌 Servlet 容器: +SpringBoot 可选择内嵌 TomcatJetty 或者 Undertow,这样我们无须以 war 包形式部署项目。
  • +
  • 提供 starter 简化 Maven 配置: +Spring 提供了一系列的 starter pom 来简化 Maven 的依赖加载,例如,当你使用了spring-boot-starter-web 时,会自动加入依赖包。
  • +
  • 自动配置 Spring: +SpringBoot 会根据在类路径中的 jar 包、类,为 jar 包里的类自动配置 Bean,这样会极大地减少我们要使用的配置。当然,SpringBoot 只是考虑了大多数的开发场景,并不是所有的场景,若在实际开发中我们需要自动配置 Bean,而 SpringBoot 没有提供支持,则可以自定义自动配置。
  • +
  • 准生产的应用监控: +SpringBoot 提供基于 http、ssh、telnet 对运行时的项目进行监控。
  • +
  • 无代码生成和 xml 配置: +SpringBoot 的神奇的不是借助于代码生成来实现的,而是通过条件注解来实现的,这是 Spring 4.x 提供的新特性。Spring 4.x 提倡使用 Java 配置和注解配置组合,而 SpringBoot 不需要任何 xml 配置即可实现 Spring 的所有配置。
  • +
+

@SpringBootApplication

+

@SpringBootApplication这个注解通常标注在启动类上:

+
@SpringBootApplication
+public class SpringBootExampleApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(SpringBootExampleApplication.class, args);
+    }
+}
+

@SpringBootApplication是一个复合注解,即由其他注解构成。核心注解是@SpringBootConfiguration@EnableAutoConfiguration

+
@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{
+}
+

@SpringBootConfiguration

+

@SpringBootConfiguration核心注解是@Configuration代表自己是一个Spring的配置类

+
@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Configuration
+public @interface SpringBootConfiguration {
+}
+

@Configuration底层实现就是一个Component

+
+

指示带注释的类是一个“组件”。 +在使用基于注释的配置和类路径扫描时,这些类被视为自动检测的候选类。

+
+
/**
+ * Indicates that an annotated class is a "component".
+ * Such classes are considered as candidates for auto-detection
+ * when using annotation-based configuration and classpath scanning.
+ *
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Indexed
+public @interface Component 
+

@EnableAutoConfiguration

+

核心注解是@AutoConfigurationPackage@Import({AutoConfigurationImportSelector.class})

+
@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+@AutoConfigurationPackage
+@Import({AutoConfigurationImportSelector.class})
+public @interface EnableAutoConfiguration {
+}
+

@AutoConfigurationPackage注解核心是引入了一个@Import(AutoConfigurationPackages.Registrar.class)配置类,该类实现了ImportBeanDefinitionRegistrar接口

+
	/**
+	 * {@link ImportBeanDefinitionRegistrar} to store the base package from the importing
+	 * configuration.
+	 */
+	static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
+		@Override
+		public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
+			register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
+		}
+		@Override
+		public Set<Object> determineImports(AnnotationMetadata metadata) {
+			return Collections.singleton(new PackageImports(metadata));
+		}
+
+	}
+
+

这里可以打断点自己看一下

+
+

@AutoConfigurationPackage 这个注解本身的含义就是将主配置类(@SpringBootApplication标注的类)所在的包下面所有的组件都扫描到 spring 容器中。

+

AutoConfigurationImportSelector核心代码如下

+
	/**
+	 * Return the auto-configuration class names that should be considered. By default
+	 * this method will load candidates using {@link SpringFactoriesLoader} with
+	 * {@link #getSpringFactoriesLoaderFactoryClass()}.
+	 * @param metadata the source metadata
+	 * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
+	 * attributes}
+	 * @return a list of candidate configurations
+	 */
+	protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
+		List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
+				getBeanClassLoader());
+		Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+				+ "are using a custom packaging, make sure that file is correct.");
+		return configurations;
+	}
+
+	/**
+	 * Return the class used by {@link SpringFactoriesLoader} to load configuration
+	 * candidates.
+	 * @return the factory class
+	 */
+	protected Class<?> getSpringFactoriesLoaderFactoryClass() {
+		return EnableAutoConfiguration.class;
+	}
+	protected ClassLoader getBeanClassLoader() {
+		return this.beanClassLoader;
+	}
+

getSpringFactoriesLoaderFactoryClass方法返回EnableAutoConfiguration.class目的就是为了将启动类所需的所有资源导入。

+

getCandidateConfigurations中有如下代码

+
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
+

大意:在META-INF/spring.factories中没有发现自动配置类。如果您使用的是自定义打包,请确保该文件是正确的。 +找到spring.factories

+

spring.factories包含了很多类,但不是全部都加载的,在某些类里面,是有一个条件@ConditionalOnXXX注解,只有当这个注解上的条件满足才会加载。

+

例如:SpringApplicationAdminJmxAutoConfiguration

+
@Configuration(proxyBeanMethods = false)
+@AutoConfigureAfter(JmxAutoConfiguration.class)
+@ConditionalOnProperty(prefix = "spring.application.admin", value = "enabled", havingValue = "true",
+		matchIfMissing = false)
+public class SpringApplicationAdminJmxAutoConfiguration 
+

总结

+

@SpringbootApplication原理

+

Springboot 启动的时候,会执行AutoConfigurationImportSelector这个类中的getCandidateConfigurations方法,这个方法会帮我们加载META-INF/spring.factories文件里面的当@ConditionXXX注解条件满足的类。

+

Bean的自动装配

+

提到Bean的自动装配就不得不说一下Spring的核心IOC,IOC全称为Inversion of Control,中文译为控制反转,是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。

+

IOC的一个重点是在系统运行中,动态的向某个对象提供它所需要的其他对象。这一点是通过DI(Dependency Injection,依赖注入)来实现的。

+

所谓IOC,对于spring框架来说,就是由spring来负责控制对象的生命周期和对象间的关系。IOC负责创建对象,把它们连接在一起,配置它们,并管理他们的整个生命周期从创建到销毁,所以可将IOC理解为一个大容器。IOC使用依赖注入(DI)来管理组成一个应用程序的组件。这些对象被称为 Spring Beans。

+

一般在代码里面由这些注解体现:

+
@Component
+@Service
+@Repository
+@Controller
+@Autowired
+@Resource
+@Inject
+

Spring利用依赖注入(DI),完成对IOC容器中各个组件的依赖关系赋值。

+

Spring提供三种装配方式:

+
    +
  • 基于注解的自动装配
  • +
  • 基于 XML 配置的显式装配
  • +
  • 基于 Java 配置的显式装配
  • +
+

此处详细介绍基于注解的自动装配

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
自动装配来源支持@Primaryspringboot支持属性
@AutowiredSpringboot原生支持boolean required
@ResourceJSR-250,JDK自带不支持String name
@InjectJSR-330,需要导入javax.inject支持无其他属性
+

@Autowired

+

可以放在构造器、参数、方法、属性上

+

源码如下:

+
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Autowired {
+
+	/**
+	 * Declares whether the annotated dependency is required.
+	 * <p>Defaults to {@code true}.
+	 */
+	boolean required() default true;
+
+}
+

加在属性上

+

使用@Autowired注解通常将其加载属性上或者构造器上,让其自动注入;默认是按照类型去容器中寻找对应的组件,例如:

+

+public class SpringBootExampleApplication {
+
+
+    public static void main(String[] args) {
+        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TestConfig.class);
+
+        TestService testService = annotationConfigApplicationContext.getBean(TestService.class);
+        // TestService 实例=====>TestService(testDao=TestDao(name=default))
+        System.out.println("TestService 实例=====>" +testService);
+
+    }
+
+}
+
+
+// 扫描的包名称
+@ComponentScan({"com.example.springboot.example.task"})
+@Configuration
+class TestConfig{
+
+}
+
+
+@ToString
+@Service
+class TestService {
+
+    @Autowired
+    TestDao testDao;
+
+}
+
+@ToString
+@Repository
+class TestDao{
+
+    @Getter
+    @Setter
+    private String name = "default";
+
+}
+

如果容器中有多个组件的名称相同,可以通过@Qualifier来进行选择注入;

+
public class SpringBootExampleApplication {
+
+
+    public static void main(String[] args) {
+        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TestConfig.class);
+
+        TestService testService = annotationConfigApplicationContext.getBean(TestService.class);
+        // TestService 实例=====>TestService(testDao=TestDao(name=我是默认的TestDao))
+        System.out.println("TestService 实例=====>" +testService);
+
+    }
+
+}
+
+
+@ComponentScan({"com.example.springboot.example.task"})
+@Configuration
+class TestConfig{
+
+    @Bean(name = "testDao2")
+     public TestDao testDao(){
+        TestDao testDao = new TestDao();
+        testDao.setName("我是testDao2");
+        return testDao;
+    }
+}
+
+@ToString
+@Service
+class TestService {
+
+    @Autowired
+    @Qualifier("testDao")
+    TestDao testDao;
+
+}
+
+@ToString
+@Repository
+class TestDao{
+
+    @Getter
+    @Setter
+    private String name = "我是默认的TestDao";
+
+}
+

除了使用@Qualifier来进行选择注入外,也可以使用@Primary来设置 bean 的优先级,默认情况下指定让哪个 bean 优先注入;

+

@Primary注解是在没有明确指定的情况下,默认使用的 bean,如果你明确用@Qualifier指定,则会使用@Qualifier指定的bean; +确保测试结果准确,在使用@Primary时,将@Qualifier去掉。

+
public class SpringBootExampleTaskApplication {
+
+
+    public static void main(String[] args) {
+        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TestConfig.class);
+
+        TestService testService = annotationConfigApplicationContext.getBean(TestService.class);
+//        TestService 实例=====>TestService(testDao=TestDao(name=我是testDao2))
+        System.out.println("TestService 实例=====>" +testService);
+
+    }
+
+}
+
+
+@ComponentScan({"com.example.springboot.example.task"})
+@Configuration
+class TestConfig{
+
+    @Primary
+    @Bean(name = "testDao2")
+     public TestDao testDao(){
+        TestDao testDao = new TestDao();
+        testDao.setName("我是testDao2");
+        return testDao;
+    }
+}
+
+@ToString
+@Service
+class TestService {
+
+    @Autowired
+    TestDao testDao;
+
+}
+
+@ToString
+@Repository
+class TestDao{
+
+    @Getter
+    @Setter
+    private String name = "我是默认的TestDao";
+
+}
+

如果使用@Autowired在容器中没有对应的组件名称,默认情况下会报错。

+
nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException
+

如果没有找到对应的 bean 不报错,可以通过@Autowired(required = false)来进行设置

+
@ToString
+@Service
+class TestService {
+    
+    // TestService 实例=====>TestService(testDao=null)
+
+    @Autowired(required = false)
+    TestDao testDao;
+
+}
+

加在方法、参数上

+

@Autowired注解不仅可以标注在属性上,也可以标注在方法上,当标注在方法上时,Spring容器创建当前对象,就会调用该方法完成赋值,方法使用的参数从IOC容器中获取。

+

通过测试打印对象的地址可以看到,方法中的参数确实是从IOC容器中获取的。

+
public class SpringBootExampleTaskApplication {
+
+
+    public static void main(String[] args) {
+        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TestConfig.class);
+
+        TestService testService = annotationConfigApplicationContext.getBean(TestService.class);
+        System.out.println("TestService 中的实例=====>" +testService);
+
+        TestDao testDao = annotationConfigApplicationContext.getBean(TestDao.class);
+        System.out.println("TestDao 中的实例=====>" +testDao);
+        
+        // TestService 中的实例=====>TestService(testDao=com.example.springboot.example.task.TestDao@5fe94a96)
+        //TestDao 中的实例=====>com.example.springboot.example.task.TestDao@5fe94a96
+
+
+    }
+
+}
+
+
+@ComponentScan({"com.example.springboot.example.task"})
+@Configuration
+class TestConfig{
+}
+
+@ToString
+@Service
+class TestService {
+
+
+    TestDao testDao;
+
+    @Autowired
+    public void setTestDao(TestDao testDao) {
+        this.testDao = testDao;
+    }
+}
+
+
+@Repository
+class TestDao{
+
+    private String name = "我是默认的TestDao";
+
+}
+

也可以加在参数上,与加在方法上类似也是从IOC容器中获取该对象。

+
@ToString
+@Service
+class TestService {
+
+
+    TestDao testDao;
+
+    public void setTestDao(@Autowired TestDao testDao) {
+        this.testDao = testDao;
+    }
+}
+

Spring创建对象的时候会默认调用组件的无参构造方法,如果只有一个有参构造,如果想要创建对象,则必须调用该有参构造; +所以当一个组件只有一个有参构造时,则可以不用写@Autowired注解。

+
@ToString
+@Service
+class TestService {
+
+
+    TestDao testDao;
+    
+     // @Autowired
+    public TestService(TestDao testDao) {
+        this.testDao = testDao;
+    }
+
+}
+

除了通过构造方法的方式实例化组件,也可以通过用bean标注的形式,来实例化容器中的组件。

+
public class SpringBootExampleTaskApplication {
+
+
+    public static void main(String[] args) {
+        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TestConfig.class);
+
+        TestService testService = annotationConfigApplicationContext.getBean(TestService.class);
+        System.out.println("TestService 中的实例=====>" +testService);
+
+        TestDao testDao = annotationConfigApplicationContext.getBean(TestDao.class);
+        System.out.println("TestDao 中的实例=====>" +testDao);
+
+        TestDao1 testDao1 = annotationConfigApplicationContext.getBean(TestDao1.class);
+        System.out.println("TestDao1 中的实例=====>" +testDao);
+        
+        // TestService 中的实例=====>TestService(testDao=com.example.springboot.example.task.TestDao@639c2c1d)
+        //TestDao 中的实例=====>com.example.springboot.example.task.TestDao@639c2c1d
+        //TestDao1 中的实例=====>com.example.springboot.example.task.TestDao@639c2c1d
+
+    }
+
+}
+
+
+@ComponentScan({"com.example.springboot.example.task"})
+@Configuration
+class TestConfig{
+
+    @Bean
+    public TestDao1 testDao1(TestDao testDao){
+        TestDao1 testDao1 = new TestDao1();
+        testDao1.setTestDao(testDao);
+        return testDao1;
+    }
+}
+
+@ToString
+@Service
+class TestService {
+
+
+    TestDao testDao;
+
+    @Autowired
+    public TestService(TestDao testDao) {
+        this.testDao = testDao;
+    }
+
+}
+
+
+@Component
+class TestDao{
+}
+
+class TestDao1{
+    @Setter
+    TestDao testDao;
+}
+

原理

+
/
+ * @see AutowiredAnnotationBeanPostProcessor
+ */
+@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Autowired{}
+

@Autowired注解文档注释上面,可以看到与之息息相关的一个类AutowiredAnnotationBeanPostProcessor,即@Autowired后置处理器; +可以看到该类实现了MergedBeanDefinitionPostProcessor接口,在postProcessMergedBeanDefinition方法上打一个断点,就可以看到@Autowired的调用栈。

+

@Autowired注解调用栈:

+
AbstractApplicationContext.refresh(容器初始化)
+---> registerBeanPostProcessors (注册AutowiredAnnotationBeanPostProcessor) 
+---> finishBeanFactoryInitialization
+---> AbstractAutowireCapableBeanFactory.doCreateBean
+---> AbstractAutowireCapableBeanFactory.applyMergedBeanDefinitionPostProcessors
+---> MergedBeanDefinitionPostProcessor.postProcessMergedBeanDefinition
+---> AutowiredAnnotationBeanPostProcessor.findAutowiringMetadata
+

核心调用:

+
postProcessMergedBeanDefinition`->`findAutowiringMetadata`->`buildAutowiringMetadata
+

相关源码:

+
@Override
+public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
+    // 调用 findAutowiringMetadata
+    InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
+    metadata.checkConfigMembers(beanDefinition);
+}
+
+private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
+		// Fall back to class name as cache key, for backwards compatibility with custom callers.
+		String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
+		// Quick check on the concurrent map first, with minimal locking.
+		InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
+		if (InjectionMetadata.needsRefresh(metadata, clazz)) {
+			synchronized (this.injectionMetadataCache) {
+				metadata = this.injectionMetadataCache.get(cacheKey);
+				if (InjectionMetadata.needsRefresh(metadata, clazz)) {
+					if (metadata != null) {
+						metadata.clear(pvs);
+					}
+                    // 调用buildAutowiringMetadata
+					metadata = buildAutowiringMetadata(clazz);
+					this.injectionMetadataCache.put(cacheKey, metadata);
+				}
+			}
+		}
+		return metadata;
+	}
+
+
+
+private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
+		LinkedList<InjectionMetadata.InjectedElement> elements = new LinkedList<>();
+		Class<?> targetClass = clazz;//需要处理的目标类
+       
+		do {
+			final LinkedList<InjectionMetadata.InjectedElement> currElements = new LinkedList<>();
+ 
+            /*通过反射获取该类所有的字段,并遍历每一个字段,并通过方法findAutowiredAnnotation遍历每一个字段的所用注解,并如果用autowired修饰了,则返回auotowired相关属性*/  
+ 
+			ReflectionUtils.doWithLocalFields(targetClass, field -> {
+				AnnotationAttributes ann = findAutowiredAnnotation(field);
+				if (ann != null) {//校验autowired注解是否用在了static方法上
+					if (Modifier.isStatic(field.getModifiers())) {
+						if (logger.isWarnEnabled()) {
+							logger.warn("Autowired annotation is not supported on static fields: " + field);
+						}
+						return;
+					}//判断是否指定了required
+					boolean required = determineRequiredStatus(ann);
+					currElements.add(new AutowiredFieldElement(field, required));
+				}
+			});
+            //和上面一样的逻辑,但是是通过反射处理类的method
+			ReflectionUtils.doWithLocalMethods(targetClass, method -> {
+				Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
+				if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
+					return;
+				}
+				AnnotationAttributes ann = findAutowiredAnnotation(bridgedMethod);
+				if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
+					if (Modifier.isStatic(method.getModifiers())) {
+						if (logger.isWarnEnabled()) {
+							logger.warn("Autowired annotation is not supported on static methods: " + method);
+						}
+						return;
+					}
+					if (method.getParameterCount() == 0) {
+						if (logger.isWarnEnabled()) {
+							logger.warn("Autowired annotation should only be used on methods with parameters: " +
+									method);
+						}
+					}
+					boolean required = determineRequiredStatus(ann);
+					PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
+              	    currElements.add(new AutowiredMethodElement(method, required, pd));
+				}
+			});
+    //用@Autowired修饰的注解可能不止一个,因此都加在currElements这个容器里面,一起处理		
+			elements.addAll(0, currElements);
+			targetClass = targetClass.getSuperclass();
+		}
+		while (targetClass != null && targetClass != Object.class);
+ 
+		return new InjectionMetadata(clazz, elements);
+	}
+

Spring 容器启动时,AutowiredAnnotationBeanPostProcessor 组件会被注册到容器中,然后扫描代码,如果带有 @Autowired 注解,则将依赖注入信息封装到 InjectionMetadata 中。

+

最后创建 bean,即实例化对象和调用初始化方法,会调用各种 XXXBeanPostProcessorbean 初始化,其中包括AutowiredAnnotationBeanPostProcessor,它负责将相关的依赖注入到容器中。

+

@Resource、@Inject

+

Spring 自动装配除了@Autowired注解外,也支持JSR-250中的@Resource和JSR-330中的@Inject注解,来进行自动装配;

+
public class SpringBootExampleTaskApplication {
+
+
+    public static void main(String[] args) {
+        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TestConfig.class);
+
+        TestService testService = annotationConfigApplicationContext.getBean(TestService.class);
+//        TestService 实例=====>TestService(testDao=TestDao(name=我是默认的TestDao))
+        System.out.println("TestService 实例=====>" +testService);
+
+    }
+
+}
+
+
+@ComponentScan({"com.example.springboot.example.task"})
+@Configuration
+class TestConfig{
+
+    @Bean(name = "testDao2")
+     public TestDao testDao(){
+        TestDao testDao = new TestDao();
+        testDao.setName("我是testDao2");
+        return testDao;
+    }
+}
+
+@ToString
+@Service
+class TestService {
+
+
+    @Resource
+    TestDao testDao;
+
+}
+
+
+@ToString
+@Repository
+class TestDao{
+
+    @Getter
+    @Setter
+    private String name = "我是默认的TestDao";
+
+}
+

使用@Inject注解需要导入:

+
<dependency>
+    <groupId>javax.inject</groupId>
+    <artifactId>javax.inject</artifactId>
+    <version>1</version>
+</dependency>
+
@ToString
+@Service
+class TestService {
+
+    // TestService 实例=====>TestService(testDao=TestDao(name=我是默认的TestDao))
+    @Inject
+    TestDao testDao;
+
+}
+

使用Spring底层组件

+

通过实现Aware接口的子接口,来使用Spring的底层的组件。Aware接口类似于回调方法的形式在 Spring 加载的时候将我们自定以的组件加载。

+
/**
+ * A marker superinterface indicating that a bean is eligible to be notified by the
+ * Spring container of a particular framework object through a callback-style method.
+ * The actual method signature is determined by individual subinterfaces but should
+ * typically consist of just one void-returning method that accepts a single argument.
+ */
+public interface Aware {
+
+}
+

Aware子接口

+

使用测试

+
@Component
+class TestService implements ApplicationContextAware, EmbeddedValueResolverAware, BeanFactoryAware {
+
+    public TestService() {
+    }
+
+    ApplicationContext applicationContext;
+
+
+    @Override
+    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+        System.out.println("获取实例名称===>" + beanFactory.getBean("TestService"));
+    }
+
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.applicationContext = applicationContext;
+        System.out.println("获取容器对象===> "+ applicationContext);
+    }
+
+    @Override
+    public void setEmbeddedValueResolver(StringValueResolver resolver) {
+        System.out.println(resolver.resolveStringValue("我是${os.name},今年${10*2.1}岁"));
+    }
+}
+

关于这些Aware都是使用AwareProcessor进行处理的,比如:ApplicationContextAwareProcessor就是处理ApplicationContextAware接口的。

+

Bean的创建流程

+

Spring详解-002

+
    +
  1. 在xml或注解上标注定义Bean;
  2. +
  3. 使用IO流读取文件并使用dom4j或其他技术来解析xml,将其转换为document对象,并设置到BeanDefinition对象(注解则需要读取哪些类标注了该注解最终转换为BeanDefinition对象);
  4. +
  5. 判断是否需要扩展,执行多个BeanFactoryProcessor方法,其目的在于获取一个完整的BeanDefinition对象。例如在xml中定义{jdbc.username}此处就可以进行替换操作;
  6. +
  7. bean的实例化,执行createBeanInstance方法,通过反射来创建对象;
  8. +
  9. bean的初始化: +
      +
    1. 设置对象属性包括设置自定义属性和通过调用Aware接口给容器中的属性赋值;
    2. +
    3. 判断是否需要扩展,如需要可执行前置处理方法;
    4. +
    5. 调用初始化方法执行invokeInitMethods方法,判断当前是否实现了InitialzingBean接口,如果实现该接口则调用afterPropertiesSet方法;
    6. +
    7. 判断是否需要扩展,如需要可执行后置处理方法;
    8. +
    9. 最终将对象交给容器来管理;
    10. +
    +
  10. +
  11. 使用对象
  12. +
  13. 销毁对象
  14. +
+

Bean的生命周期

+

Bean的生命周期,即Bean实例化->初始化->使用->销毁 的过程。

+

注入Bean

+

我们可以使用 xml 配置的方式来指定,bean 在初始化、销毁的时候调用对应的方法:

+
<bean id="getDemoEntity" class="com.my.demo" init-method="init" destroy-method="destroy" />
+

也可以使用注解的方式,来调用bean在初始化、销毁的时候调用对应的方法:

+
public class MainTest {
+    public static void main(String[] args) {
+        // 获取Spring IOC容器
+        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(DemoConfiguration.class);
+        System.out.println("容器初始化完成...");
+
+        annotationConfigApplicationContext.close();
+        System.out.println("容器销毁了...");
+    }
+}
+
+@Configuration
+class DemoConfiguration {
+    @Bean(initMethod = "init", destroyMethod = "destroy")
+    public DemoEntity getDemoEntity() {
+        return new DemoEntity();
+    }
+}
+
+class DemoEntity {
+    public DemoEntity(){
+        System.out.println("调用了构造器...");
+    }
+
+    public void init(){
+        System.out.println("调用了初始化方法...");
+    }
+
+    public void destroy(){
+        System.out.println("调用了销毁方法...");
+    }
+}
+

需要注意的是,上面演示的是单实例 bean,如果是多实例 bean,初始化和销毁会不一样。

+

单实例 bean

+
    +
  • 在容器启动的时候创建对象;
  • +
  • 在容器关闭的时候销毁;
  • +
+

多实例 bean

+
    +
  • 在每次获取bean的时候创建对象;
  • +
  • 容器不会自动帮你处理,需要手动销毁 bean
  • +
+

多实例注解代码:

+
@Scope("prototype")
+@Bean(initMethod="init",destroyMethod="destroy")
+public Test test(){}
+

InitializingBean、DisposableBean

+

通过让Bean实现 InitializingBean(定义初始化逻辑)和实现DisposableBean(销毁逻辑)实现初始化bean和销毁bean:

+
public class MainTest {
+    public static void main(String[] args) {
+        // 获取Spring IOC容器
+        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(DemoEntity.class);
+        System.out.println("容器初始化完成...");
+
+        annotationConfigApplicationContext.close();
+        System.out.println("容器销毁了...");
+    }
+}
+
+@Component
+class DemoEntity implements InitializingBean, DisposableBean {
+    public DemoEntity(){
+        System.out.println("调用了构造器...");
+    }
+
+    @Override
+    public void destroy(){
+        System.out.println("调用了销毁方法...");
+    }
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        System.out.println("调用了初始化方法...");
+    }
+}
+

@PostConstruct、@PreDestroy

+

Java提供了对应的注解,也可以调用Bean的初始化方法和销毁方法:

+
    +
  • @PostConstruct 标注该注解的方法,在bean创建完成并且属性赋值完成 来执行初始化方法;
  • +
  • @PreDestroy, 在容器销毁bean之前通知我们进行bean的清理工作;
  • +
+

这两个注解不是spring的注解是JSR250JDK带的注解。

+
public class MainTest {
+    public static void main(String[] args) {
+        // 获取Spring IOC容器
+        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(DemoEntity.class);
+        System.out.println("容器初始化完成...");
+
+        annotationConfigApplicationContext.close();
+        System.out.println("容器销毁了...");
+    }
+}
+
+@Component
+class DemoEntity  {
+    public DemoEntity(){
+        System.out.println("调用了构造器...");
+    }
+
+    // 销毁之前调用
+    @PreDestroy
+    public void destroy(){
+        System.out.println("调用了销毁方法...");
+    }
+
+    // 对象创建并赋值之后调用
+    @PostConstruct
+    public void init() {
+        System.out.println("调用了初始化方法...");
+    }
+}
+

BeanPostProcessor

+

除了上面的几种方法,也可以使用BeanPostProcessor,Bean的后置处理器,在初始化前后进行处理工作。

+

postProcessBeforeInitialization:会在初始化完成之前调用 +postProcessAfterInitialization:会在初始化完成之后调用

+
public class MainTest {
+    public static void main(String[] args) {
+        // 获取Spring IOC容器
+        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(DemoConfiguration.class);
+        System.out.println("容器初始化完成...");
+
+        annotationConfigApplicationContext.close();
+        System.out.println("容器销毁了...");
+    }
+}
+
+@Configuration
+class DemoConfiguration implements BeanPostProcessor {
+
+    @Bean(initMethod = "init", destroyMethod = "destroy")
+    public DemoEntity getDemoEntity(){
+       return new DemoEntity();
+    }
+
+    @Override
+    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
+        System.out.println("调用了 postProcessBeforeInitialization");
+        return bean;
+    }
+
+    @Override
+    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+        System.out.println("调用了 postProcessAfterInitialization");
+        return bean;
+    }
+}
+
+@Component
+class DemoEntity  {
+    public DemoEntity(){
+        System.out.println("调用了构造器...");
+    }
+
+    public void destroy(){
+        System.out.println("调用了销毁方法...");
+    }
+
+    public void init() {
+        System.out.println("调用了初始化方法...");
+    }
+}
+

调用顺序:

+
+

创建对象 –> postProcessBeforeInitialization –> 初始化 –> postProcessAfterInitialization –> 销毁

+
+

原理

+

通过打断点,可以看到,在创建bean的时候会,会调用AbstractAutowireCapableBeanFactory类的doCreateBean方法,这也是创建bean的核心方法。

+
    try {
+        populateBean(beanName, mbd, instanceWrapper);
+        exposedObject = initializeBean(beanName, exposedObject, mbd);
+    }
+
+    // ======= initializeBean  =======
+    if (mbd == null || !mbd.isSynthetic()) {
+        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
+    }
+
+    try {
+        invokeInitMethods(beanName, wrappedBean, mbd);
+    }
+    catch (Throwable ex) {
+        throw new BeanCreationException(
+                (mbd != null ? mbd.getResourceDescription() : null),
+                beanName, "Invocation of init method failed", ex);
+    }
+    if (mbd == null || !mbd.isSynthetic()) {
+        wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
+    }
+

调用栈大致如下:

+
populateBean()
+{
+    applyBeanPostProcessorsBeforeInitialization() -> invokeInitMethods() -> applyBeanPostProcessorsAfterInitialization()
+}
+

在初始化之前调用populateBean()方法,给bean进行属性赋值,之后在调用applyBeanPostProcessorsBeforeInitialization方法;

+

applyBeanPostProcessorsBeforeInitialization源码:

+
	public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName)
+			throws BeansException {
+
+		Object result = existingBean;
+		for (BeanPostProcessor processor : getBeanPostProcessors()) {
+			Object current = processor.postProcessBeforeInitialization(result, beanName);
+			if (current == null) {
+				return result;
+			}
+			result = current;
+		}
+		return result;
+	}
+

该方法作用,遍历容器中所有的BeanPostProcessor挨个执行postProcessBeforeInitialization方法,一旦返回null,将不会执行后面beanpostProcessBeforeInitialization方法。

+

之后在调用invokeInitMethods方法,进行bean的初始化,最后在执行applyBeanPostProcessorsAfterInitialization方法,执行一些初始化之后的工作。

+

AOP

+

AOP全称:Aspect-Oriented Programming,译为面向切面编程 。AOP可以说是对OOP的补充和完善。在程序原有的纵向执行流程中,针对某一个或某些方法添加通知(方法),形成横切面的过程就叫做面向切面编程。

+

实现AOP的技术,主要分为两大类: 一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“切面”,从而使得编译器可以在编译期间织入有关“切面”的代码,属于静态代理。

+

作用:

+
    +
  • 将复杂的需求分解出不同的方面,将公共功能集中解决。例如:处理日志。
  • +
  • 采用代理机制组装起来运行,在不改变原程序的基础上对代码段进行增强处理,增加新的功能。
  • +
+

动态代理

+

动态代理,可以说是AOP的核心了。在Spring中主要使用了两种动态代理

+
    +
  • JDK 动态代理技术
  • +
  • CGLib 动态代理技术
  • +
+

JDK的动态代理时基于Java 的反射机制来实现的,是Java 原生的一种代理方式。他的实现原理就是让代理类和被代理类实现同一接口,代理类持有目标对象来达到方法拦截的作用。 +通过接口的方式有两个弊端一个就是必须保证被代理类有接口,另一个就是如果相对被代理类的方法进行代理拦截,那么就要保证这些方法都要在接口中声明。接口继承的是java.lang.reflect.InvocationHandler

+

CGLib 动态代理使用的 ASM 这个非常强大的 Java 字节码生成框架来生成class ,基于继承的实现动态代理,可以直接通过 super 关键字来调用被代理类的方法.子类可以调用父类的方法,不要求有接口。

+

使用

+

使用AOP大致可以分为三步:

+
    +
  1. 将业务逻辑组件和切面类都加入到容器中,并用@Aspect注解标注切面类。
  2. +
  3. 在切面类的通知方法上,要注意切面表达式的写法,标注通知注解,告诉Spring何时何地的运行: +
      +
    • @Before:前置通知,在目标方法运行之前执行;
    • +
    • @After: 后置通知,在目标方法运行之后执行,无论方法是否出现异常都会执行;
    • +
    • @Around: 环绕通知,通过joinPoint.proceed()方法手动控制目标方法的执行;
    • +
    • @AfterThrowing: 异常通知,在目标方法出现异常之后执行;
    • +
    • @AfterReturning: 返回通知,在目标方法返回之后执行;
    • +
    +
  4. +
  5. 使用@EnableAspectJAutoProxy开启基于注解的AOP模式。
  6. +
+
public class MainTest {
+    public static void main(String[] args) {
+        // 获取Spring IOC容器
+        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(DemoConfiguration.class);
+
+        DemoEntity demoEntity = annotationConfigApplicationContext.getBean(DemoEntity.class);
+        demoEntity.myAspectTest("123");
+
+        annotationConfigApplicationContext.close();
+    }
+}
+
+@EnableAspectJAutoProxy
+@Configuration
+class DemoConfiguration{
+
+    @Bean
+    public DemoEntity getDemoEntity(){
+        return new DemoEntity();
+    }
+
+    @Bean
+    public DemoAspect gerDemoAspect(){
+        return new DemoAspect();
+    }
+
+}
+
+@Aspect
+class DemoAspect {
+
+    @Pointcut("execution(* com.lilian.ticket.image.exchange.DemoEntity.myAspectTest(..))")
+    public void pointer() {}
+
+    @Before("pointer()")
+    public void beforeTest(JoinPoint joinPoint) {
+        System.out.println("调用了AOP,前置通知");
+        Object[] args = joinPoint.getArgs();
+        System.out.println("前置通知:目标方法参数:[" + args[0] + "]");
+    }
+
+    @After("pointer()")
+    public void afterTest(JoinPoint joinPoint){
+        System.out.println("调用了AOP,后置通知");
+        Object[] args = joinPoint.getArgs();
+        System.out.println("后置通知:目标方法参数:[" + args[0] + "]");
+    }
+
+    @Around("pointer()")
+    public Object aroundTest(ProceedingJoinPoint joinPoint) {
+        System.out.println("===调用了AOP,环绕通知===");
+        System.out.println("环绕通知目标方法执行前");
+        Object[] args = joinPoint.getArgs();
+        System.out.println("环绕通知:目标方法参数:[" + args[0] + "]");
+        Object proceed = null;
+        try {
+             proceed = joinPoint.proceed();
+        } catch (Throwable throwable) {
+            throwable.printStackTrace();
+        }
+        System.out.println("环绕通知目标方法执行后\n");
+        return proceed;
+    }
+
+    @AfterThrowing(pointcut="pointer()", throwing="ex")
+    public void afterThrowingTest(JoinPoint joinPoint, Exception ex) {
+        System.out.println("异常通知==>["+ex.getMessage()+"]\n");
+    }
+
+    @AfterReturning("pointer()")
+    public void afterReturnTest(JoinPoint joinPoint){
+        Object[] args = joinPoint.getArgs();
+        System.out.println("有返回值的后置通知:目标方法参数:[" + args[0] + "]");
+    }
+
+}
+
+class DemoEntity {
+
+    public String myAspectTest(String name) {
+        System.out.println("调用了 myAspectTest 方法;\t name=[" + name + "]");
+        // 当name传入null时,模拟异常
+        name.split("123");
+        return name;
+    }
+}
+

@EnableAspectJAutoProxy

+

要想AOP起作用,就要加@EnableAspectJAutoProxy注解,所以AOP的原理可以从@EnableAspectJAutoProxy入手研究。

+

它是一个复合注解,启动的时候,给容器中导入了一个AspectJAutoProxyRegistrar组件:

+
@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Import(AspectJAutoProxyRegistrar.class)
+public @interface EnableAspectJAutoProxy {}
+

发现该类实现了ImportBeanDefinitionRegistrar接口,而该接口的作用是给容器中注册bean的;所以AspectJAutoProxyRegistrar作用是,添加自定义组件给容器中注册bean

+
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
+
+	/**
+	 * Register, escalate, and configure the AspectJ auto proxy creator based on the value
+	 * of the @{@link EnableAspectJAutoProxy#proxyTargetClass()} attribute on the importing
+	 * {@code @Configuration} class.
+	 */
+	@Override
+	public void registerBeanDefinitions(
+			AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
+
+        // 注册了 AnnotationAwareAspectJAutoProxyCreator 组件
+		AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
+
+		AnnotationAttributes enableAspectJAutoProxy =
+				AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
+        // 获取 @EnableAspectJAutoProxy 中的属性,做一些工作
+		if (enableAspectJAutoProxy != null) {
+			if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
+				AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
+			}
+			if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
+				AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
+			}
+		}
+	}
+}
+

AspectJAutoProxyRegistrar组件何时注册?

+

通过对下面代码打断点

+
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
+

可以看到该方法是给容器中注册了一个AnnotationAwareAspectJAutoProxyCreator组件,实际上是注册AnnotationAwareAspectJAutoProxyCreator组件。

+

AOP核心组件1

+

可以看出@EnableAspectJAutoProxy注解最主要的作用实际上就是通过@Import注解把AnnotationAwareAspectJAutoProxyCreator这个对象注入到spring容器中。

+

现在只要把AnnotationAwareAspectJAutoProxyCreator组件何时注册搞懂,AspectJAutoProxyRegistrar组件何时注册也就明白了。

+

AnnotationAwareAspectJAutoProxyCreator继承关系:

+
AnnotationAwareAspectJAutoProxyCreator
+    extends AspectJAwareAdvisorAutoProxyCreator
+        extends AbstractAdvisorAutoProxyCreator
+            extends AbstractAutoProxyCreator
+                extends ProxyProcessorSupport implements SmartInstantiationAwareBeanPostProcessor,BeanFactoryAware
+                    extends ProxyConfig implements Ordered, BeanClassLoaderAware, AopInfrastructureBean 
+

可以看到其中的一个父类AbstractAutoProxyCreator这个父类实现了SmartInstantiationAwareBeanPostProcessor接口,该接口是一个后置处理器接口;同样实现了BeanFactoryAware接口,这意味着,该类可以通过接口中的方法进行自动装配BeanFactory

+

这两个接口的在AOP体系中具体的实现方法:

+
1.AbstractAutoProxyCreator
+BeanFactoryAware重写:
+- AbstractAutoProxyCreator.setBeanFactory
+
+SmartInstantiationAwareBeanPostProcessor重写:
+- AbstractAutoProxyCreator.postProcessBeforeInstantiation
+- AbstractAutoProxyCreator.postProcessAfterInitialization
+
+2.AbstractAdvisorAutoProxyCreator
+BeanFactoryAware重写:
+- AbstractAdvisorAutoProxyCreator.setBeanFactory -> initBeanFactory
+
+3. AnnotationAwareAspectJAutoProxyCreator
+BeanFactoryAware重写:
+- AnnotationAwareAspectJAutoProxyCreator.initBeanFactory
+

在上面的任何方法搭上断点即可看到类似下面的方法调用栈:

+
AnnotationConfigApplicationContext.AnnotationConfigApplicationContext()
+    ->AbstractApplicationContext.refresh() //刷新容器,给容器初始化bean
+        ->AbstractApplicationContext.finishBeanFactoryInitialization()
+            ->DefaultListableBeanFactory.preInstantiateSingletons()
+                ->AbstractBeanFactory.getBean()
+                    ->AbstractBeanFactory.doGetBean()
+                        ->DefaultSingletonBeanRegistry.getSingleton()
+                            ->AbstractBeanFactory.createBean()
+                                ->AbstractAutowireCapableBeanFactory.resolveBeforeInstantiation()
+                                    ->AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInstantiation()
+                                        ->AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInstantiation()
+                                            ->调用AOP相关的后置处理器
+

其中 AbstractApplicationContext.refresh() 方法,调用了 registerBeanPostProcessors()方法 ,它是用来注册后置处理器,以拦截 bean 的创建。也是在这个方法中完成了对 AnnotationAwareAspectJAutoProxyCreator 的注册。 +在下面详细的展开。

+

注册完 BeanPostProcessor 后,还调用了方法 finishBeanFactoryInitialization() ,完成 BeanFactory 初始化工作,并创建剩下的单实例 bean

+
@Override
+public void refresh() throws BeansException, IllegalStateException {
+    
+    // .....
+
+    // Register bean processors that intercept bean creation.
+    registerBeanPostProcessors(beanFactory);
+
+    // .....
+
+    // Instantiate all remaining (non-lazy-init) singletons.
+    finishBeanFactoryInitialization(beanFactory);
+
+    // .....
+
+}
+

registerBeanPostProcessors

+

registerBeanPostProcessors方法中注册了所有的BeanPostProcessor;注册顺序是:

+
    +
  1. 注册实现了PriorityOrdered接口的BeanPostProcessor;
  2. +
  3. 注册实现了 Ordered 接口的 BeanPostProcessor;
  4. +
  5. 注册常规的 BeanPostProcessor ,也就是没有实现优先级接口的 BeanPostProcessor;
  6. +
  7. 注册 Spring 内部 BeanPostProcessor;
  8. +
+

由于AnnotationAwareAspectJAutoProxyCreator类间接实现了Ordered接口。所以它是在注册实现Ordered接口的BeanPostProcessor中完成注册。

+

注册时会调用AbstractBeanFactory.getBean() -> AbstractBeanFactory.doGetBean()创建bean

+

doGetBean()方法作用:

+
    +
  • 创建beancreateBeanInstance();
  • +
  • bean中的属性赋值:populateBean();
  • +
  • 初始化beaninitializeBean();
  • +
+

初始化bean时,initializeBean方法会调用BeanPostProcessorBeanFactory以及Aware接口的相关方法。这也是BeanPostProcessor发挥初始化bean的原理。

+
protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
+    
+    // ...
+
+    invokeAwareMethods(beanName, bean);   //处理Aware接口的方法回调
+
+    Object wrappedBean = bean;
+    if (mbd == null || !mbd.isSynthetic()) {
+        // 执行后置处理器的postProcessBeforeInitialization方法
+        wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
+    }
+    try {
+        // 执行自定义的初始化方法,也就是在这执行 setBeanFactory方法
+        invokeInitMethods(beanName, wrappedBean, mbd);  
+    }
+
+    // ...
+
+    if (mbd == null || !mbd.isSynthetic()) {
+        // 执行后置处理器的postProcessAfterInitialization方法
+        wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
+    }
+    return wrappedBean;
+}
+
+// ...invokeAwareMethods方法简要 ...
+private void invokeAwareMethods(String beanName, Object bean) {
+    if (bean instanceof Aware) {
+        if (bean instanceof BeanFactoryAware) {
+            ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);
+        }
+    }
+}
+

initializeBean作用:

+
    +
  • 处理 Aware 接口的方法回调:invokeAwareMethods();
  • +
  • 执行后置处理器的postProcessBeforeInitialization()方法;
  • +
  • 执行自定义的初始化方法:invokeInitMethods();
  • +
  • 执行后置处理器的postProcessAfterInitialization()方法;
  • +
+

initializeBean方法执行成功,AnnotationAwareAspectJAutoProxyCreator组件才会注册和初始化成功。

+

finishBeanFactoryInitialization

+

除了弄懂AnnotationAwareAspectJAutoProxyCreator组件何时注册,也需要知道它什么时候被调用,这就涉及到finishBeanFactoryInitialization方法。

+

继续看方法的调用:

+
AnnotationConfigApplicationContext.AnnotationConfigApplicationContext()
+    ->AbstractApplicationContext.refresh() // 刷新容器,给容器初始化bean
+        ->AbstractApplicationContext.finishBeanFactoryInitialization() // 从这继续
+            ->DefaultListableBeanFactory.preInstantiateSingletons()
+                ->AbstractBeanFactory.getBean()
+                    ->AbstractBeanFactory.doGetBean()
+                        ->DefaultSingletonBeanRegistry.getSingleton()
+                            ->AbstractBeanFactory.createBean()
+                                ->AbstractAutowireCapableBeanFactory.resolveBeforeInstantiation()
+                                    ->AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInstantiation()
+                                        ->AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInstantiation()
+                                            ->调用AOP相关的后置处理器
+

finishBeanFactoryInitialization源码简要:

+
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
+
+    // ...
+    
+    // 注释大意: 实例化所有剩余的(非lazy-init)单例。
+    // Instantiate all remaining (non-lazy-init) singletons.
+    beanFactory.preInstantiateSingletons(); // 断点停在这里
+}
+

finishBeanFactoryInitialization 方法也需要注册Bean。它会调用 preInstantiateSingletons() 方法遍历获取容器中所有的 Bean,实例化所有剩余的非懒加载初始化单例 Bean

+

preInstantiateSingletons()方法源码简要:

+
	@Override
+	public void preInstantiateSingletons() throws BeansException {
+
+		// Iterate over a copy to allow for init methods which in turn register new bean definitions.
+		// While this may not be part of the regular factory bootstrap, it does otherwise work fine.
+		List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);
+
+		// Trigger initialization of all non-lazy singleton beans...
+		for (String beanName : beanNames) {
+			RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
+            // 获取,非抽象、单例、非懒加载Bean
+			if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
+                // 是否 是FactoryBean类型
+				if (isFactoryBean(beanName)) {
+                    // ...
+				}
+				else {
+					getBean(beanName); // 断点停在这
+				}
+			}
+		}
+
+        // ...
+	}
+

preInstantiateSingletons() 调用 getBean() 方法,获取Bean实例,执行过程getBean()->doGetBean()->getSingleton()->createBean(),又回到了上面注册Bean的步骤。

+

这里要注意createBean()方法中的resolveBeforeInstantiation()方法,这里可以理解为缓存Bean,如果被创建了就拿来直接用,如果没有则创建Bean

+
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
+        throws BeanCreationException {
+
+    // ...
+
+    try {
+        // 注释大意:给 BeanPostProcessors 一个返回代理而不是目标bean实例的机会。
+        // Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
+        Object bean = resolveBeforeInstantiation(beanName, mbdToUse); // 断点停在这里
+        if (bean != null) {
+            return bean;
+        }
+    }
+
+    // ...
+
+    try {
+        Object beanInstance = doCreateBean(beanName, mbdToUse, args);
+        if (logger.isTraceEnabled()) {
+            logger.trace("Finished creating instance of bean '" + beanName + "'");
+        }
+        return beanInstance;
+    }
+
+    // ...
+}
+

resolveBeforeInstantiation()applyBeanPostProcessorsBeforeInstantiation()方法源码:

+
protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
+    Object bean = null;
+    if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
+        // Make sure bean class is actually resolved at this point.
+        if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
+            Class<?> targetType = determineTargetType(beanName, mbd);
+            if (targetType != null) {
+                // 调用 applyBeanPostProcessorsBeforeInstantiation 方法
+                bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName); // 断点停在这
+                if (bean != null) {
+                    bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
+                }
+            }
+        }
+        mbd.beforeInstantiationResolved = (bean != null);
+    }
+    return bean;
+}
+
+// ... 上面代码调用的方法 ...
+
+protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) {
+    // 遍历所有的 BeanPostProcessor
+    for (BeanPostProcessor bp : getBeanPostProcessors()) {
+
+        // //如果是 InstantiationAwareBeanPostProcessor 类型
+        if (bp instanceof InstantiationAwareBeanPostProcessor) {
+            InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
+
+            // 调用 postProcessBeforeInstantiation 方法
+            Object result = ibp.postProcessBeforeInstantiation(beanClass, beanName); // 断点停在这
+            if (result != null) {
+                return result;
+            }
+        }
+    }
+    return null;
+}
+

到了这里在回过头来看一下AnnotationAwareAspectJAutoProxyCreator组件实现的SmartInstantiationAwareBeanPostProcessor接口,继承关系:

+
SmartInstantiationAwareBeanPostProcessor 
+    ->extends InstantiationAwareBeanPostProcessor
+        ->extends BeanPostProcessor
+

到这就跟前边对上了,AOP相关的后置处理器也就是在这被调用的。

+

回头在看上面的createBean()方法,刚才看到的是resolveBeforeInstantiation()方法的调用栈,所以从层次结构上看AnnotationAwareAspectJAutoProxyCreator组件的调用是在创建 Bean实例之前先尝试用后置处理器返回对象的。

+

AOP@EnableAspectJAutoProxy原理

+

Spring事务

+

Spring 为事务管理提供了丰富的功能支持。Spring 事务管理分为编码式和声明式的两种方式。编程式事务指的是通过编码方式实现事务;声明式事务基于 AOP,将具体业务逻辑与事务处理解耦。声明式事务管理使业务代码逻辑不受污染, 因此在实际使用中声明式事务用的比较多。

+

Spring事务的隔离级别

+

事务隔离级别指的是一个事务对数据的修改与另一个并行的事务的隔离程度,当多个事务同时访问相同数据时,如果没有采取必要的隔离机制,就可能发生以下问题:

+ + + + + + + + + + + + + + + + + + + + + +
问题描述
脏读一个事务读到另一个事务未提交的更新数据。比如银行取钱,事务A开启事务,此时切换到事务B,事务B开启事务–>取走100元,此时切换回事务A,事务A读取的肯定是数据库里面的原始数据,因为事务B取走了100块钱,并没有提交,数据库里面的账务余额肯定还是原始余额,这就是脏读。
幻读是指当事务不是独立执行时发生的一种现象。如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。 同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象 发生了幻觉一样。
不可重复读在一个事务里面的操作中发现了未被操作的数据。 比方说在同一个事务中先后执行两条一模一样的select语句,期间在此次事务中没有执行过任何DDL语句,但先后得到的结果不一致,这就是不可重复读。
+

Spring支持的隔离级别:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
隔离级别描述
DEFAULT使用数据库本身使用的隔离级别。ORACLE(读已提交) MySQL(可重复读)
READ_UNCOMITTED读未提交(脏读)最低的隔离级别,一切皆有可能。
READ_COMMITED读已提交,ORACLE默认隔离级别,有幻读以及不可重复读风险。
REPEATABLE_READ可重复读,解决不可重复读的隔离级别,但还是有幻读风险。
SERLALIZABLE串行化,所有事务请求串行执行,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了。
+

不是事务隔离级别设置得越高越好,事务隔离级别设置得越高,意味着势必要花手段去加锁用以保证事务的正确性,那么效率就要降低,因此实际开发中往往要在效率和并发正确性之间做一个取舍,一般情况下会设置为READ_COMMITED,此时避免了脏读,并发性也还不错,之后再通过一些别的手段去解决不可重复读和幻读的问题就好了。

+

Spring建议的是使用DEFAULT,即数据库本身的隔离级别,配置好数据库本身的隔离级别,无论在哪个框架中读写数据都不用操心了。

+

Spring事务的传播

+

事务传播行为指当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。

+

Spring定义了七种传播行为:

+
    +
  • @Transactional(propagation=Propagation.REQUIRED):如果有事务, 那么加入事务, 没有的话新建一个(默认情况下)
  • +
  • @Transactional(propagation=Propagation.NOT_SUPPORTED):容器不为这个方法开启事务
  • +
  • @Transactional(propagation=Propagation.REQUIRES_NEW):不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务
  • +
  • @Transactional(propagation=Propagation.MANDATORY):必须在一个已有的事务中执行,否则抛出异常
  • +
  • @Transactional(propagation=Propagation.NEVER):必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY相反)
  • +
  • @Transactional(propagation=Propagation.SUPPORTS):如果其他bean调用这个方法,在其他bean中声明事务,那就用事务.如果其他bean没有声明事务,那就不用事务.
  • +
+

@Transactional注解

+
@Target({ElementType.TYPE, ElementType.METHOD})
+@Retention(RetentionPolicy.RUNTIME)
+@Inherited
+@Documented
+public @interface Transactional {
+    @AliasFor("transactionManager")
+    String value() default "";
+
+    @AliasFor("value")
+    String transactionManager() default "";
+
+    // 事务的传播行为
+    Propagation propagation() default Propagation.REQUIRED;
+
+    // 事务的隔离级别
+    Isolation isolation() default Isolation.DEFAULT;
+
+    // 超时时间
+    // 事务需要在一定时间内提交,如不提交则进行回滚
+    // 默认-1,设置时间以秒单位进行计算 
+    int timeout() default -1;
+
+    // 是否只读
+    // 读:查询操作;写:添加、修改、删除操作
+    // 默认值false,表示可以进行读、写操作
+    // 设置true后 只能查询
+    boolean readOnly() default false;
+
+    // 回滚
+    // 设置出现哪些异常进行回滚 
+    Class<? extends Throwable>[] rollbackFor() default {};
+
+    String[] rollbackForClassName() default {};
+
+    // 不回滚
+    // 设置出现哪些异常不进行回滚 
+    Class<? extends Throwable>[] noRollbackFor() default {};
+
+    String[] noRollbackForClassName() default {};
+}
+

失效情况

+
    +
  1. 如果某个方法是非public的,那么@Transactional就会失效,因为底层cglib是基于父子类来实现的,子类是不能重载父类的private方法,所以无法很好利用代理,这种情况下会导致@Transactional失效
  2. +
  3. 使用的数据库引擎不支持事务,例如在使用mysql的时候使用MyISAM引擎不支持事务,InnoDB支持,并且从mysql5.5之后开始默认的存储引擎就为InnoDB 。
  4. +
  5. 调用的问题,因为Spring事务是基于代理来实现的,所以某个加了@Transactional的方法只有是被代理对象调用时,那么这个注解才会生效,所以当被代理对象来调用这个方法那么事务就不会生效,简单的可以理解为添加了@Transactional注解的方法不能在同一个类中调用,否则会使事务失效。
  6. +
  7. @Transactional 注解属性 propagation 设置错误,若是错误的配置以下三种 propagation ,事务将不会发生回滚: +
      +
    • TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
    • +
    • TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
    • +
    • TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
    • +
    +
  8. +
  9. @Transactional 注解属性 rollbackFor 设置错误:rollbackFor 可以指定能够触发事务回滚的异常类型。Spring默认抛出了未检查unchecked异常(继承自RuntimeException 的异常)或者 Error才回滚事务;其他异常不会触发回滚事务。如果在事务中抛出其他类型的异常,但却期望 Spring 能够回滚事务,就需要指定 rollbackFor属性。
  10. +
  11. 异常被 catch 了 导致@Transactional失效:当事务方法中抛出一个异常后,应该是需要表示当前事务需要 rollback ,如果在事务方法中手动捕获了该异常,那么事务方法则会任务当前事务应该正常 commit,此时就会出现事务方法中明明有报错信息表示当前事务需要 rollback 但是事务方法任务是正常,出现了前后不一致,也是因为这样就会抛出 UnexpectedRollbackException异常。
  12. +
+

原理

+

利用Spring Aop实现的。 当一个方法使用了@Transactional注解,在运行时,JVM为该Bean创建一个代理对象,并且在调用该方法的时候进行使用TransactionInterceptor拦截,在方法执行之前会开启一个事务,然后执行方法的逻辑。 方法执行成功,则提交事务。如果执行方法中出现异常,则回滚事务。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/spring/springboot-docker/index.html b/blog-site/public/posts/spring/springboot-docker/index.html new file mode 100644 index 00000000..6bb90838 --- /dev/null +++ b/blog-site/public/posts/spring/springboot-docker/index.html @@ -0,0 +1,680 @@ + + + + + + + + + + + SpringBoot整合docker | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

SpringBoot整合docker

+ 2020.08.30 +
+

MacOS上安装docker

+

下载

+

国内下载网站: http://get.daocloud.io 不推荐下载docker版本太旧了

+

官网下载: https://docs.docker.com/get-started/#download-and-install-docker

+

或用homebrew进行下载安装

+
 brew install --cask --appdir=/Applications docker
+

配置镜像

+

由于网速原因,可以配置一下国内的镜像加速器

+ +

阿里云获取镜像地址: https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors +登陆后,左侧菜单选中镜像加速器就可以看到你的专属地址了; +获取阿里云docker地址

+

配置docker镜像地址 +docker配置

+
{
+  "debug": true,
+  "experimental": false,
+  "registry-mirrors": [
+    "https://docker.mirrors.ustc.edu.cn"
+  ]
+}
+

在终端执行docker info命令 +docker信息

+

出现上图所示,docker镜像加速器配置成功

+

参考菜鸟教程

+

Springboot整合docker

+

创建测试

+

HelloController.class

+
/**
+ * <p>
+ * Hello Controller
+ * </p>
+ */
+@RestController
+@RequestMapping
+public class HelloController {
+    @GetMapping
+    public String hello() {
+        return "Hello,From Docker";
+    }
+}
+

application.yml

+
server:
+  port: 8080
+  servlet:
+    context-path: /demo
+

Dockerfile

+

首先创建一个名字叫Dockerfile的文件,路径任意 +DockerFile

+
# 基础镜像
+FROM openjdk:8-jdk-alpine
+
+# 作者信息
+MAINTAINER "whitepure"
+
+# 添加一个存储空间 其效果是在主机 /var/lib/docker 目录下创建了一个临时文件,并链接到容器的/tmp
+VOLUME /tmp
+
+# 暴露8080端口
+EXPOSE 8080
+
+# 添加变量,获取target下的jar包; 如果使用dockerfile-maven-plugin,则会自动替换这里的变量内容
+ARG JAR_FILE=target/demo-docker.jar
+
+# 往容器中添加jar包
+ADD ${JAR_FILE} app.jar
+
+# 启动镜像自动运行程序
+ENTRYPOINT ["java","-Djava.security.egd=file:/dev/urandom","-jar","/app.jar"]
+

pom

+

在 pom 文件加入 docker 插件

+
    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+        <java.version>1.8</java.version>
+        <dockerfile-version>1.4.9</dockerfile-version>
+    </properties>
+
+ <build>
+        <finalName>demo-docker</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>com.spotify</groupId>
+                <artifactId>dockerfile-maven-plugin</artifactId>
+                <version>${dockerfile-version}</version>
+                <configuration>
+                    <repository>${project.build.finalName}</repository>
+                    <tag>${project.version}</tag>
+                    <buildArgs>
+                        <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
+                    </buildArgs>
+                </configuration>
+<!--                <executions>-->
+<!--                    &lt;!&ndash;设置运行 mvn package 的时候自动执行docker build&ndash;&gt;-->
+<!--                    <execution>-->
+<!--                        <id>default</id>-->
+<!--                        <phase>package</phase>-->
+<!--                        <goals>-->
+<!--                            <goal>build</goal>-->
+<!--                        </goals>-->
+<!--                    </execution>-->
+<!--                </executions>-->
+            </plugin>
+        </plugins>
+    </build>
+

使用 maven 打包 +docker打包

+

docker镜像测试

+

前往 Dockerfile 目录,打开命令行执行;

+
+

执行 docker build 命令,docker就会根据 Dockerfile 里你定义好的命令进行构建新的镜像; +-t代表要构建的镜像的 tag ,.代表当前目录,也就是 Dockerfile 所在的目录

+
+
docker build -t demo-docker .
+

查看docker镜像列表

+
docker images
+

运行该镜像

+
+

使用镜像 demo-docker ,将容器的 8080 端口映射到主机的 9090 端口

+
+
docker run -d -p 9090:8080 demo-docker
+

访问http://localhost:9090/demo +hellodocker

+

如果要停止docker镜像,首先获取镜像id,然后在用stop命令停止运行镜像

+
+

docker ps -a: 显示所有的容器,包括未运行的

+
+
whitepure@MacBook-Pro demo-docker % docker ps -a
+CONTAINER ID   IMAGE      ...    NAMES
+421fcff1be87   demo-docker   ...  affectionate_nightingale
+whitepure@MacBook-Pro demo-docker % docker stop 421fcff1be87
+421fcff1be87
+

如果要删除镜像,也需要先获取镜像id,在用rm命令进行删除

+
whitepure@MacBook-Pro demo-docker % docker ps -a
+CONTAINER ID   IMAGE         ...    NAMES
+421fcff1be87   demo-docker    ...     affectionate_nightingale
+whitepure@MacBook-Pro demo-docker % docker rm 421fcff1be87
+421fcff1be87
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/spring/springboot-elasticsearch/index.html b/blog-site/public/posts/spring/springboot-elasticsearch/index.html new file mode 100644 index 00000000..28ed1bef --- /dev/null +++ b/blog-site/public/posts/spring/springboot-elasticsearch/index.html @@ -0,0 +1,790 @@ + + + + + + + + + + + SpringBoot整合elasticsearch | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

SpringBoot整合elasticsearch

+ 2020.02.09 +
+

安装elasticsearch

+

要注意导入依赖的版本和安装elasticsearch的版本与springboot的兼容问题

+

springbootelasticsearch.jpg

+

用 docker 安装 elasticsearch

+

本例用elasticsearch-6.5.3springboot-2.1.0.RELEASE版本

+
    +
  1. 下载镜像:
  2. +
+
docker pull elasticsearch:6.5.3
+
    +
  1. 运行容器:
  2. +
+
docker run -d -p 9200:9200 -p 9300:9300 --name elasticsearch-6.5.3 elasticsearch:6.5.3
+
    +
  1. 进入容器:
  2. +
+
docker exec -it elasticsearch-6.5.3 /bin/bash
+
    +
  1. 安装 IK 分词器
  2. +
+
/bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.5.3/elasticsearch-analysis-ik-6.5.3.zip
+
    +
  1. 修改 es 配置文件:vi ./config/elasticsearch.yml
  2. +
+
cluster.name: "docker-cluster"
+network.host: 0.0.0.0
+
+# minimum_master_nodes need to be explicitly set when bound on a public IP
+# set to 1 to allow single node clusters
+# Details: https://github.com/elastic/elasticsearch/pull/17288
+discovery.zen.minimum_master_nodes: 1
+
+# just for elasticsearch-head plugin
+http.cors.enabled: true
+http.cors.allow-origin: "*"
+
    +
  1. 退出容器:
  2. +
+
exit
+
    +
  1. 停止容器:
  2. +
+
docker stop elasticsearch-6.5.3
+
    +
  1. 启动容器:
  2. +
+
docker start elasticsearch-6.5.3
+

Springboot集成elasticsearch

+

导入pom依赖

+
<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
+</dependency>
+

创建测试接口

+

EsConsts 常量池

+
public interface EsConsts {
+    /**
+     * 索引名称
+     */
+    String INDEX_NAME = "person";
+
+    /**
+     * 类型名称
+     */
+    String TYPE_NAME = "person";
+}
+

创建Javabean

+
+

@Document 注解主要声明索引名、类型名、分片数量和备份数量

+

@Field 注解主要声明字段对应ES的类型

+
+
/** 注意(坑点): ES 6以后不允许一个索引下有多个类型,只允许一个索引下有一个类型
+ *  @Document:
+ * 
+ * String indexName(); //索引库的名称,个人建议以项目的名称命名
+ *
+ * String type() default ""; //类型,建议以实体的名称命名
+ *
+ * short shards() default 5; //默认分区数
+ *
+ * short replicas() default 1; //每个分区默认的备份数
+ *
+ * String refreshInterval() default "1s"; //刷新间隔
+ *
+ * String indexStoreType() default "fs"; //索引文件存储类型
+ */
+@Document(indexName = EsConsts.INDEX_NAME, type = EsConsts.TYPE_NAME, shards = 1, replicas = 0)
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class Person {
+    /**
+     * 主键
+     */
+    @Id
+    private Long id;
+
+    /**
+     * 名字
+     */
+    @Field(type = FieldType.Keyword)
+    private String name;
+
+    /**
+     * 国家
+     */
+    @Field(type = FieldType.Keyword)
+    private String country;
+
+    /**
+     * 年龄
+     */
+    @Field(type = FieldType.Integer)
+    private Integer age;
+
+    /**
+     * 生日
+     */
+    @Field(type = FieldType.Date)
+    private Date birthday;
+
+    /**
+     * 介绍
+     */
+    @Field(type = FieldType.Text, analyzer = "ik_smart")
+    private String remark;
+}
+

PersonRepository

+
public interface PersonRepository extends ElasticsearchRepository<Person, Long> {
+
+    /**
+     * 根据年龄区间查询
+     *
+     * @param min 最小值
+     * @param max 最大值
+     * @return 满足条件的用户列表
+     */
+    List<Person> findByAgeBetween(Integer min, Integer max);
+}
+

application.yml配置

+
spring:
+  data:
+    elasticsearch:
+      cluster-name: docker-cluster
+      cluster-nodes: localhost:9300
+

创建测试类

+
@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoElasticsearchApplicationTests {
+
+    @Test
+    public void contextLoads() {
+    }
+
+}
+
@Slf4j
+public class PersonRepositoryTest extends SpringBootDemoElasticsearchApplicationTests {
+    @Autowired
+    private PersonRepository repo;
+
+    /**
+     * 测试新增
+     */
+    @Test
+    public void save() {
+        Person person = new Person(1L, "刘备", "蜀国", 18, DateUtil.parse("1990-01-02 03:04:05"), "刘备(161年-223年6月10日),即汉昭烈帝(221年-223年在位),又称先主,字玄德,东汉末年幽州涿郡涿县(今河北省涿州市)人,西汉中山靖王刘胜之后,三国时期蜀汉开国皇帝、政治家。\n刘备少年时拜卢植为师;早年颠沛流离,备尝艰辛,投靠过多个诸侯,曾参与镇压黄巾起义。先后率军救援北海相孔融、徐州牧陶谦等。陶谦病亡后,将徐州让与刘备。赤壁之战时,刘备与孙权联盟击败曹操,趁势夺取荆州。而后进取益州。于章武元年(221年)在成都称帝,国号汉,史称蜀或蜀汉。《三国志》评刘备的机权干略不及曹操,但其弘毅宽厚,知人待士,百折不挠,终成帝业。刘备也称自己做事“每与操反,事乃成尔”。\n章武三年(223年),刘备病逝于白帝城,终年六十三岁,谥号昭烈皇帝,庙号烈祖,葬惠陵。后世有众多文艺作品以其为主角,在成都武侯祠有昭烈庙为纪念。");
+        Person save = repo.save(person);
+        log.info("【save】= {}", save);
+    }
+
+    /**
+     * 测试批量新增
+     */
+    @Test
+    public void saveList() {
+        List<Person> personList = Lists.newArrayList();
+        personList.add(new Person(2L, "曹操", "魏国", 20, DateUtil.parse("1988-01-02 03:04:05"), "曹操(155年-220年3月15日),字孟德,一名吉利,小字阿瞒,沛国谯县(今安徽亳州)人。东汉末年杰出的政治家、军事家、文学家、书法家,三国中曹魏政权的奠基人。\n曹操曾担任东汉丞相,后加封魏王,奠定了曹魏立国的基础。去世后谥号为武王。其子曹丕称帝后,追尊为武皇帝,庙号太祖。\n东汉末年,天下大乱,曹操以汉天子的名义征讨四方,对内消灭二袁、吕布、刘表、马超、韩遂等割据势力,对外降服南匈奴、乌桓、鲜卑等,统一了中国北方,并实行一系列政策恢复经济生产和社会秩序,扩大屯田、兴修水利、奖励农桑、重视手工业、安置流亡人口、实行“租调制”,从而使中原社会渐趋稳定、经济出现转机。黄河流域在曹操统治下,政治渐见清明,经济逐步恢复,阶级压迫稍有减轻,社会风气有所好转。曹操在汉朝的名义下所采取的一些措施具有积极作用。\n曹操军事上精通兵法,重贤爱才,为此不惜一切代价将看中的潜能分子收于麾下;生活上善诗歌,抒发自己的政治抱负,并反映汉末人民的苦难生活,气魄雄伟,慷慨悲凉;散文亦清峻整洁,开启并繁荣了建安文学,给后人留下了宝贵的精神财富,鲁迅评价其为“改造文章的祖师”。同时曹操也擅长书法,唐朝张怀瓘在《书断》将曹操的章草评为“妙品”。"));
+        personList.add(new Person(3L, "孙权", "吴国", 19, DateUtil.parse("1989-01-02 03:04:05"), "孙权(182年-252年5月21日),字仲谋,吴郡富春(今浙江杭州富阳区)人。三国时代孙吴的建立者(229年-252年在位)。\n孙权的父亲孙坚和兄长孙策,在东汉末年群雄割据中打下了江东基业。建安五年(200年),孙策遇刺身亡,孙权继之掌事,成为一方诸侯。建安十三年(208年),与刘备建立孙刘联盟,并于赤壁之战中击败曹操,奠定三国鼎立的基础。建安二十四年(219年),孙权派吕蒙成功袭取刘备的荆州,使领土面积大大增加。\n黄武元年(222年),孙权被魏文帝曹丕册封为吴王,建立吴国。同年,在夷陵之战中大败刘备。黄龙元年(229年),在武昌正式称帝,国号吴,不久后迁都建业。孙权称帝后,设置农官,实行屯田,设置郡县,并继续剿抚山越,促进了江南经济的发展。在此基础上,他又多次派人出海。黄龙二年(230年),孙权派卫温、诸葛直抵达夷州。\n孙权晚年在继承人问题上反复无常,引致群下党争,朝局不稳。太元元年(252年)病逝,享年七十一岁,在位二十四年,谥号大皇帝,庙号太祖,葬于蒋陵。\n孙权亦善书,唐代张怀瓘在《书估》中将其书法列为第三等。"));
+        personList.add(new Person(4L, "诸葛亮", "蜀国", 16, DateUtil.parse("1992-01-02 03:04:05"), "诸葛亮(181年-234年10月8日),字孔明,号卧龙,徐州琅琊阳都(今山东临沂市沂南县)人,三国时期蜀国丞相,杰出的政治家、军事家、外交家、文学家、书法家、发明家。\n早年随叔父诸葛玄到荆州,诸葛玄死后,诸葛亮就在襄阳隆中隐居。后刘备三顾茅庐请出诸葛亮,联孙抗曹,于赤壁之战大败曹军。形成三国鼎足之势,又夺占荆州。建安十六年(211年),攻取益州。继又击败曹军,夺得汉中。蜀章武元年(221年),刘备在成都建立蜀汉政权,诸葛亮被任命为丞相,主持朝政。蜀后主刘禅继位,诸葛亮被封为武乡侯,领益州牧。勤勉谨慎,大小政事必亲自处理,赏罚严明;与东吴联盟,改善和西南各族的关系;实行屯田政策,加强战备。前后六次北伐中原,多以粮尽无功。终因积劳成疾,于蜀建兴十二年(234年)病逝于五丈原(今陕西宝鸡岐山境内),享年54岁。刘禅追封其为忠武侯,后世常以武侯尊称诸葛亮。东晋政权因其军事才能特追封他为武兴王。\n诸葛亮散文代表作有《出师表》《诫子书》等。曾发明木牛流马、孔明灯等,并改造连弩,叫做诸葛连弩,可一弩十矢俱发。诸葛亮一生“鞠躬尽瘁、死而后已”,是中国传统文化中忠臣与智者的代表人物。"));
+        Iterable<Person> people = repo.saveAll(personList);
+        log.info("【people】= {}", people);
+    }
+
+    /**
+     * 测试更新
+     */
+    @Test
+    public void update() {
+        repo.findById(1L).ifPresent(person -> {
+            person.setRemark(person.getRemark() + "\n更新更新更新更新更新");
+            Person save = repo.save(person);
+            log.info("【save】= {}", save);
+        });
+    }
+
+    /**
+     * 测试删除
+     */
+    @Test
+    public void delete() {
+        // 主键删除
+        repo.deleteById(1L);
+
+        // 对象删除
+        repo.findById(2L).ifPresent(person -> repo.delete(person));
+
+        // 批量删除
+        repo.deleteAll(repo.findAll());
+    }
+
+    /**
+     * 测试普通查询,按生日倒序
+     */
+    @Test
+    public void select() {
+        repo.findAll(Sort.by(Sort.Direction.DESC, "birthday")).
+            forEach(person -> log.info("{} 生日: {}", person.getName(), DateUtil.formatDateTime(person.getBirthday())));
+    }
+
+    /**
+     * 自定义查询,根据年龄范围查询
+     */
+    @Test
+    public void customSelectRangeOfAge() {
+        repo.findByAgeBetween(18, 19).forEach(person -> log.info("{} 年龄: {}", person.getName(), person.getAge()));
+    }
+
+    /**
+     * 高级查询
+     */
+    @Test
+    public void advanceSelect() {
+        // QueryBuilders 提供了很多静态方法,可以实现大部分查询条件的封装
+        MatchQueryBuilder queryBuilder = QueryBuilders.matchQuery("name", "孙权");
+        log.info("【queryBuilder】= {}", queryBuilder.toString());
+
+        repo.search(queryBuilder).forEach(person -> log.info("【person】= {}", person));
+    }
+
+    /**
+     * 自定义高级查询
+     */
+    @Test
+    public void customAdvanceSelect() {
+        // 构造查询条件
+        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
+        // 添加基本的分词条件
+        queryBuilder.withQuery(QueryBuilders.matchQuery("remark", "东汉"));
+        // 排序条件
+        queryBuilder.withSort(SortBuilders.fieldSort("age").order(SortOrder.DESC));
+        // 分页条件
+        queryBuilder.withPageable(PageRequest.of(0, 2));
+        Page<Person> people = repo.search(queryBuilder.build());
+        log.info("【people】总条数 = {}", people.getTotalElements());
+        log.info("【people】总页数 = {}", people.getTotalPages());
+        people.forEach(person -> log.info("【person】= {},年龄 = {}", person.getName(), person.getAge()));
+    }
+
+    /**
+     * 测试聚合,测试平均年龄
+     */
+    @Test
+    public void agg() {
+        // 构造查询条件
+        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
+        // 不查询任何结果
+        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
+
+        // 平均年龄
+        queryBuilder.addAggregation(AggregationBuilders.avg("avg").field("age"));
+
+        log.info("【queryBuilder】= {}", JSONUtil.toJsonStr(queryBuilder.build()));
+
+        AggregatedPage<Person> people = (AggregatedPage<Person>) repo.search(queryBuilder.build());
+        double avgAge = ((InternalAvg) people.getAggregation("avg")).getValue();
+        log.info("【avgAge】= {}", avgAge);
+    }
+
+    /**
+     * 测试高级聚合查询,每个国家的人有几个,每个国家的平均年龄是多少
+     */
+    @Test
+    public void advanceAgg() {
+        // 构造查询条件
+        NativeSearchQueryBuilder queryBuilder = new NativeSearchQueryBuilder();
+        // 不查询任何结果
+        queryBuilder.withSourceFilter(new FetchSourceFilter(new String[]{""}, null));
+
+        // 1. 添加一个新的聚合,聚合类型为terms,聚合名称为country,聚合字段为age
+        queryBuilder.addAggregation(AggregationBuilders.terms("country").field("country")
+            // 2. 在国家聚合桶内进行嵌套聚合,求平均年龄
+            .subAggregation(AggregationBuilders.avg("avg").field("age")));
+
+        log.info("【queryBuilder】= {}", JSONUtil.toJsonStr(queryBuilder.build()));
+
+        // 3. 查询
+        AggregatedPage<Person> people = (AggregatedPage<Person>) repo.search(queryBuilder.build());
+
+        // 4. 解析
+        // 4.1. 从结果中取出名为 country 的那个聚合,因为是利用String类型字段来进行的term聚合,所以结果要强转为StringTerm类型
+        StringTerms country = (StringTerms) people.getAggregation("country");
+        // 4.2. 获取桶
+        List<StringTerms.Bucket> buckets = country.getBuckets();
+        for (StringTerms.Bucket bucket : buckets) {
+            // 4.3. 获取桶中的key,即国家名称  4.4. 获取桶中的文档数量
+            log.info("{} 总共有 {} 人", bucket.getKeyAsString(), bucket.getDocCount());
+            // 4.5. 获取子聚合结果:
+            InternalAvg avg = (InternalAvg) bucket.getAggregations().asMap().get("avg");
+            log.info("平均年龄:{}", avg);
+        }
+    }
+
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/spring/springboot-kafka/index.html b/blog-site/public/posts/spring/springboot-kafka/index.html new file mode 100644 index 00000000..824f8a26 --- /dev/null +++ b/blog-site/public/posts/spring/springboot-kafka/index.html @@ -0,0 +1,816 @@ + + + + + + + + + + + SpringBoot整合kafka | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

SpringBoot整合kafka

+ 2020.08.20 +
+

kafka介绍

+ +

Kafka是一种分布式的,基于发布/订阅的消息系统。主要特点如下:

+
    +
  • 以时间复杂度为O(1)的方式提供消息持久化能力,并保证即使对TB级以上数据也能保证常数时间的访问性能
  • +
  • 高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条消息的传输
  • +
  • 支持Kafka Server间的消息分区,及分布式消息消费,同时保证每个partition内的消息顺序传输
  • +
  • 同时支持离线数据处理和实时数据处理
  • +
+

Windows平台kafka环境搭建

+ +

MacOS平台kafka环境搭建

+

安装kafka

+

kafka依赖Java和zookeeper,安装前请先安装Java和zookeeper。

+

安装zookeeper

+
brew install zookeeper
+

安装kafka

+
brew install kafka
+

启动kafka

+

假定你是按照上边的方法安装的kafka,接下来启动kafka

+

因为kafka依赖zk,所以首先要启动zookeeper

+
    +
  • cd到该路径:cd /usr/local/Cellar/zookeeper/xxx/bin xxx是zookeeper的版本
  • +
  • 执行启动命令 zkServer start,停止命令为 zkServer stop
  • +
+

启动之前必须修改kafka的配置文件,否则后面的启动会失败

+
    +
  • 执行该命令 vim /usr/local/etc/kafka/server.properties
  • +
  • 增加listeners=PLAINTEXT://localhost:9092 其中有一行,默认被注释掉了,打开修改即可
  • +
+

最后启动kafka

+
    +
  • cd到该路径:cd /usr/local/Cellar/kafka/xxx/bin xxx是kafka的版本
  • +
  • 执行该命令启动kafka kafka-server-start /usr/local/etc/kafka/server.properties &
  • +
+

到此为止kafka启动完成

+

Kafka消息测试

+
    +
  • 创建test主题 kafka-topics --create --zookeeper localhost:2181
    --replication-factor 1 --partitions 1 --topic test
  • +
  • 查看主题是否创建成功 kafka-topics --list --zookeeper localhost:2181 该命令会列出所有的主题
  • +
  • 在kafka/bin目录,topic为test的主题 kafka-console-producer --topic test --broker-list localhost:9092
  • +
  • 打开新的终端,在kafka/bin目录,topic为test的主题 kafka-console-consumer --bootstrap-server localhost:9092 -topic test
  • +
+

在生产者终端输入信息,在消费者终端就能看的见

+
+ 生产者 +
+
+ 消费者 +
+

Springboot整合kafka

+

引入pom依赖

+
    <dependency>
+        <groupId>org.springframework.kafka</groupId>
+        <artifactId>spring-kafka</artifactId>
+    </dependency>
+

配置application.yml

+
server:
+  port: 8080
+  servlet:
+    context-path: /demo
+spring:
+  kafka:
+    bootstrap-servers: localhost:9092
+    producer:
+      retries: 0
+      batch-size: 16384
+      buffer-memory: 33554432
+      key-serializer: org.apache.kafka.common.serialization.StringSerializer
+      value-serializer: org.apache.kafka.common.serialization.StringSerializer
+    consumer:
+      group-id: spring-boot-demo
+      # 手动提交
+      enable-auto-commit: false
+      auto-offset-reset: latest
+      key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+      value-deserializer: org.apache.kafka.common.serialization.StringDeserializer
+      properties:
+        session.timeout.ms: 60000
+    listener:
+      log-container-config: false
+      concurrency: 5
+      # 手动提交
+      ack-mode: manual_immediate
+

创建kafka配置类

+
public interface KafkaConsts {
+    /**
+     * 默认分区大小
+     */
+    Integer DEFAULT_PARTITION_NUM = 3;
+
+    /**
+     * Topic 名称
+     */
+    String TOPIC_TEST = "test";
+}
+
@Configuration
+@EnableConfigurationProperties({KafkaProperties.class})
+@EnableKafka
+@AllArgsConstructor
+public class KafkaConfig {
+    private final KafkaProperties kafkaProperties;
+
+    @Bean
+    public KafkaTemplate<String, String> kafkaTemplate() {
+        return new KafkaTemplate<>(producerFactory());
+    }
+
+    @Bean
+    public ProducerFactory<String, String> producerFactory() {
+        return new DefaultKafkaProducerFactory<>(kafkaProperties.buildProducerProperties());
+    }
+
+    @Bean
+    public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
+        ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
+        factory.setConsumerFactory(consumerFactory());
+        factory.setConcurrency(KafkaConsts.DEFAULT_PARTITION_NUM);
+        factory.setBatchListener(true);
+        factory.getContainerProperties().setPollTimeout(3000);
+        return factory;
+    }
+
+    @Bean
+    public ConsumerFactory<String, String> consumerFactory() {
+        return new DefaultKafkaConsumerFactory<>(kafkaProperties.buildConsumerProperties());
+    }
+
+    @Bean("ackContainerFactory")
+    public ConcurrentKafkaListenerContainerFactory<String, String> ackContainerFactory() {
+        ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
+        factory.setConsumerFactory(consumerFactory());
+        factory.getContainerProperties().setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
+        factory.setConcurrency(KafkaConsts.DEFAULT_PARTITION_NUM);
+        return factory;
+    }
+
+}
+

创建消息处理器

+
@Component
+@Slf4j
+public class MessageHandler {
+
+    @KafkaListener(topics = KafkaConsts.TOPIC_TEST, containerFactory = "ackContainerFactory")
+    public void handleMessage(ConsumerRecord record, Acknowledgment acknowledgment) {
+        try {
+            String message = (String) record.value();
+            log.info("收到消息: {}", message);
+        } catch (Exception e) {
+            log.error(e.getMessage(), e);
+        } finally {
+            // 手动提交 offset
+            acknowledgment.acknowledge();
+        }
+    }
+}
+

创建测试类

+

测试之前请确保kafka已启动

+
@RunWith(SpringRunner.class)
+@SpringBootTest
+public class SpringBootDemoMqKafkaApplicationTests {
+    @Autowired
+    private KafkaTemplate<String, String> kafkaTemplate;
+
+    /**
+     * 测试发送消息
+     */
+    @Test
+    public void testSend() {
+        kafkaTemplate.send(KafkaConsts.TOPIC_TEST, "hello,kafka...");
+    }
+
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/spring/springboot-nacos/index.html b/blog-site/public/posts/spring/springboot-nacos/index.html new file mode 100644 index 00000000..2e7a6f5c --- /dev/null +++ b/blog-site/public/posts/spring/springboot-nacos/index.html @@ -0,0 +1,1016 @@ + + + + + + + + + + + SpringBoot整合nacos | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

SpringBoot整合nacos

+ 2023.09.04 +
+

nacos

+

nacos下载

+

下载地址

+

一键傻瓜试安装即可,官网写的很清楚这里不在赘述 http://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html

+

nacos启动

+

将模式改为单机模式

+

SpringBoot整合nacos

+

启动成功

+

SpringBoot整合nacos

+

nacos相关配置

+

SpringBoot整合nacos

+

demo-dev.yaml

+
server:
+  port: 8001
+
+config:
+  info: "config info for dev from nacos config center"
+

demo-test.yaml

+
server:
+  port: 3333
+
+config:
+  info: "config info for test from nacos config center"
+

user.yaml

+
user:
+  name: zs1112222
+  age: 10
+  address: 测试地址
+

SpringBoot整合nacos

+

代码

+

整合nacos配置中心,注册中心,完整项目地址 gitee地址

+

pom.xml

+
<parent>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-parent</artifactId>
+    <version>2.2.2.RELEASE</version>
+</parent>
+
+<dependencies>
+    <dependency>
+        <groupId>org.projectlombok</groupId>
+        <artifactId>lombok</artifactId>
+    </dependency>
+    <dependency>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-web</artifactId>
+    </dependency>
+    <dependency>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-test</artifactId>
+        <scope>test</scope>
+    </dependency>
+    <dependency>
+        <groupId>com.alibaba.cloud</groupId>
+        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
+        <version>2.2.2.RELEASE</version>
+    </dependency>
+    <dependency>
+        <groupId>com.alibaba.cloud</groupId>
+        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
+        <version>2.2.2.RELEASE</version>
+    </dependency>
+</dependencies>
+

UserConfig

+
@Data
+@Configuration
+@ConfigurationProperties(prefix = "user")
+public class UserConfig {
+
+    private String name;
+
+    private Integer age;
+
+    private String address;
+
+}
+

BeanAutoRefreshConfigExample

+
@RestController
+public class BeanAutoRefreshConfigExample {
+
+    @Autowired
+    private UserConfig userConfig;
+
+    @GetMapping("/user/hello")
+    public String hello(){
+        return userConfig.getName() + userConfig.getAge() + userConfig.getAddress();
+    }
+
+}
+

ValueAnnotationExample

+
@RestController
+@RefreshScope
+public class ValueAnnotationExample {
+
+    @Value("${config.info}")
+    private String configInfo;
+
+    @GetMapping("/config/info")
+    public String getConfigInfo() {
+        return configInfo;
+    }
+
+}
+

DemoApplication

+
@SpringBootApplication
+public class DemoApplication {
+
+    public static void main(String[] args) {
+        SpringApplication.run(DemoApplication.class, args);
+    }
+
+}
+

bootstrap.yml

+
spring:
+  profiles:
+    # 指定环境 切换环境
+    active: dev
+  application:
+    name: demo
+  cloud:
+    # nacos server dataId
+    # ${spring.application.name)}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
+    nacos:
+      # Nacos服务注册中心
+      discovery:
+        serverAddr: @serverAddr@
+        group: DEMO_GROUP
+        namespace: 25af15f3-ae79-41c3-847d-960adb953185
+        username: @username@
+        password: @password@
+      # Nacos作为配置中心
+      config:
+        server-addr: @serverAddr@
+        file-extension: yaml
+        group: DEMO_GROUP
+        namespace: 25af15f3-ae79-41c3-847d-960adb953185
+        username: @username@
+        password: @password@
+        # 加载多配置
+        extension-configs:
+          - data-id: user.yaml
+            group: DEMO_GROUP
+            refresh: true
+

测试结果

+

SpringBoot整合nacos

+

SpringBoot整合nacos

+

补充.刷新静态配置

+

有时候一些老项目或者一些写法会遇到静态的配置,这时候可以利用Java的反射特性来刷新静态变量.

+

大致原理为: 监听nacos配置改动,通过nacos改动确定改动的配置,进而缩小更新范围,通过反射更新变量.

+
<!-- https://mvnrepository.com/artifact/com.purgeteam/dynamic-config-spring-boot-starter -->
+<dependency>
+    <groupId>com.purgeteam</groupId>
+    <artifactId>dynamic-config-spring-boot-starter</artifactId>
+    <version>0.1.1.RELEASE</version>
+</dependency>
+<dependency>
+    <groupId>org.projectlombok</groupId>
+    <artifactId>lombok</artifactId>
+</dependency>
+

@NacosRefreshStaticField

+
@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface NacosRefreshStaticField {
+
+    String configPrefix() default "";
+
+}
+

NacosListener

+
@Slf4j
+@Component
+@EnableDynamicConfigEvent
+public class NacosListener implements ApplicationListener<ActionConfigEvent> {
+
+    @Autowired
+    private ApplicationContext applicationContext;
+
+    @SneakyThrows
+    @Override
+    public void onApplicationEvent(ActionConfigEvent environment) {
+        Map<String, HashMap> map = environment.getPropertyMap();
+        for (Map.Entry<String, HashMap> entry : map.entrySet()) {
+            String key = entry.getKey();
+            Map changeMap = entry.getValue();
+            String before = String.valueOf(changeMap.get("before"));
+            String after = String.valueOf(changeMap.get("after"));
+            log.info("配置[key:{}]被改变,改变前before:{},改变后after:{}",key,before,after);
+
+            String[] configNameArr = key.split("\\.");
+            String configPrefix = configNameArr[0];
+            String configRealVal = configNameArr[configNameArr.length-1];
+
+            AtomicReference<Class<?>> curClazz = new AtomicReference<>();
+            Map<String, Object> refreshStaticFieldBeanMap = applicationContext.getBeansWithAnnotation(NacosRefreshStaticField.class);
+            for (Map.Entry<String, Object> mapEntry : refreshStaticFieldBeanMap.entrySet()) {
+                String beanName = mapEntry.getKey();
+                if (ObjectUtil.isEmpty(beanName)) {
+                    continue;
+                }
+
+                String fullClassName = refreshStaticFieldBeanMap.get(beanName).toString().split("@")[0];
+                Class<?> refreshStaticFieldClass;
+                try {
+                    refreshStaticFieldClass = Class.forName(fullClassName);
+                } catch (ClassNotFoundException e) {
+                    throw new ClassNotFoundException("监听nacos刷新当前静态类属性,未找到当前类",e);
+                }
+                NacosRefreshStaticField refreshStaticConfig = refreshStaticFieldClass.getAnnotation(NacosRefreshStaticField.class);
+                if (Objects.nonNull(refreshStaticConfig) && refreshStaticConfig.configPrefix().equalsIgnoreCase(configPrefix)) {
+                    curClazz.set(refreshStaticFieldClass);
+                }
+            }
+            Class<?> aClass = curClazz.get();
+            if (Objects.isNull(aClass)) {
+                return;
+            }
+
+            // 利用反射动态更新 静态变量
+            Field[] declaredFields = aClass.getDeclaredFields();
+            for (Field declaredField : declaredFields) {
+                if (declaredField.getName().equalsIgnoreCase(configRealVal)) {
+                    log.info("刷新当前配置 更新当前类[{}] 静态属性 [{}]",aClass.getSimpleName(),declaredField.getName());
+                    declaredField.setAccessible(true);
+                    declaredField.set(null,after);
+                }
+            }
+
+        }
+
+    }
+}
+

CommonWebConfig

+
@Data
+@Component
+@ConfigurationProperties(prefix = "common")
+@RefreshScope
+public class CommonWebConfig {
+
+    private String apiUrl;
+
+}
+

使用

+
@Component
+@NacosRefreshStaticField(configPrefix="common")
+public class ExampleComponent {
+    public static String apiUrl = SpringUtil.getBean(CommonWebConfig.class).getApiUrl();
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/spring/springboot-redis/index.html b/blog-site/public/posts/spring/springboot-redis/index.html new file mode 100644 index 00000000..ff5b79f2 --- /dev/null +++ b/blog-site/public/posts/spring/springboot-redis/index.html @@ -0,0 +1,893 @@ + + + + + + + + + + + SpringBoot整合redis | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

SpringBoot整合redis

+ 2020.03.01 +
+

Redis介绍

+

redis是开源的一个高性能的 key-value 数据库。

+

主要特点

+
    +
  • Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用
  • +
  • Redis支持数据的备份,即master-slave模式的数据备份
  • +
  • Redis 可以存储键与5种不同数据结构类型之间的映射 +
      +
    • String 可以是字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作;对象和浮点数执行自增(increment)或者自减(decrement)
    • +
    • List 一个链表,链表上的每个节点都包含了一个字符串 从链表的两端推入或者弹出元素;根据偏移量对链表进行修剪(trim);读取单个或者多个元素;根据值来查找或者移除元素
    • +
    • Set 包含字符串的无序收集器(unorderedcollection),并且被包含的每个字符串都是独一无二的、各不相同 添加、获取、移除单个元素;检查一个元素是否存在于某个集合中;计算交集、并集、差集;从集合里卖弄随机获取元素
    • +
    • Hash 包含键值对的无序散列表 添加、获取、移除单个键值对;获取所有键值对
    • +
    • Zset 字符串成员(member)与浮点数分值(score)之间的有序映射,元素的排列顺序由分值的大小决定 添加、获取、删除单个元素;根据分值范围(range)或者成员来获取元素
    • +
    +
  • +
+

注意: Java bean 要序列化

+

Redis优点

+
    +
  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
  • +
  • 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。
  • +
  • 原子 – Redis的所有操作都是原子性的,意思就是要么成功执行要么失败完全不执行。单个操作是原子性的。多个操作也支持事务,即原子性,通过MULTI和EXEC指令包起来。
  • +
  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。
  • +
+

参考菜鸟教程

+

导入依赖

+
<dependency>
+    <groupId>org.springframework.boot</groupId>
+    <artifactId>spring-boot-starter-data-redis</artifactId>
+    <version>2.1.3.RELEASE</version>
+</dependency>
+

配置redis

+

RedisConfig.class

+
@Configuration
+public class RedisConfig {
+
+    //用于解决注解操作redis 序列话的问题
+    @Bean(name = "myCacheManager")
+    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
+
+        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
+        RedisSerializer<Object> jsonSerializer = new GenericJackson2JsonRedisSerializer();
+        RedisSerializationContext.SerializationPair<Object> pair = RedisSerializationContext.SerializationPair
+                .fromSerializer(jsonSerializer);
+        RedisCacheConfiguration defaultCacheConfig= RedisCacheConfiguration.defaultCacheConfig()
+                .serializeValuesWith(pair);
+        defaultCacheConfig.entryTtl(Duration.ofMinutes(30));
+        return new RedisCacheManager(redisCacheWriter, defaultCacheConfig);
+    }
+
    /**
+ * 解决用redisTemplate操作的序列化的问题
+ *
+ * @param factory RedisConnectionFactory
+ * @return redisTemplate
+ */
+    @Bean
+    @ConditionalOnMissingBean(name = "redisTemplate")
+    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
+        // 配置redisTemplate
+        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
+        redisTemplate.setConnectionFactory(factory);
+        // key序列化
+        redisTemplate.setKeySerializer(STRING_SERIALIZER);
+        // value序列化
+        redisTemplate.setValueSerializer(JACKSON__SERIALIZER);
+        // Hash key序列化
+        redisTemplate.setHashKeySerializer(STRING_SERIALIZER);
+        // Hash value序列化
+        redisTemplate.setHashValueSerializer(JACKSON__SERIALIZER);
+        redisTemplate.afterPropertiesSet();
+        return redisTemplate;
+    }
+
+}
+

启动类配置

+
@EnableCaching  //允许注解操作缓存
+@SpringBootApplication
+public class RedisExampleApplication {
+   public static void main(String[] args) {
+      SpringApplication.run(RedisExampleApplication.class, args);
+   }
+}
+

application.yml 配置

+
#redis 缓存配置
+redis:
+  database: 0
+  host: @ip
+  port: 6379
+  timeout: 8000
+  #如果没有可不写
+  password:
+  jedis:
+    pool:
+      #连接池最大连接数量
+      max-active: 10
+      #连接池最大堵塞时间
+      max-wait: -1
+      #连接池最小空闲连接
+      min-idle: 0
+      #连接池最大空闲连接
+      max-idle: 8
+

缓存注解介绍

+

@Cacheable

+

@Cacheable作用将方法的运行结果进行缓存,以后要相同的数据,直接从缓存中获取. +cacheNames 和 key 都必须填,如果不填 key ,默认的 key 是当前的方法名,更新缓存时会因为方法名不同而更新失败.

+

属性

+
    +
  • value: 指定缓存名称 可指定多个(数组)
  • +
  • key: 缓存数据使用的key默认为方法参数支持 spEL
  • +
  • keyGenerator: key的生成器
  • +
  • CacheManager: 指定缓存管理器,或获取解析器,两者二选一
  • +
  • condition: 指定条件缓存
  • +
  • unless: 否定缓存. 当unless条件为true时,返回值就不会被缓存,可获取到结果判断
  • +
  • sync: 是否异步, 不支持unless
  • +
+

key 也可以动态设置为方法的参数(支持EL)

+
@Cacheable(cacheNames = "xxx", key = "#openid")
+public Response detail(@RequestParam("openid") String openid){
+      //to do sthing
+}
+
@Cacheable(cacheNames = "xxx", key = "xxx",keyGenerator = "xxx", CacheManager = "xxx", condition = "xxx", unless = "xxx", sync = "xxx")
+

@CachePut

+

既调用方法又更新缓存修改了数据库某个数据. +更新缓存,先调用目标方法将目标方法缓存起来,更新时要注意,要和查询时的key相同,否则缓存不会更新,属性和 @Cacheable 相同

+
@CachePut(cacheNames = "xxx", key = "xxx",keyGenerator = "xxx", CacheManager = "xxx", condition = "xxx", unless = "xxx", sync = "xxx")
+

@CacheEvict

+

该注解作用是删除缓存,需要指定 key

+
    +
  • allEntries:是否删除所有字段的缓存
  • +
  • beforeInvocation:是否在方法之前执行
  • +
+
@CacheEvict(cacheNames = "xxx", key = "xxx", allEntries = "xxx", beforeInvocation="xxx")
+

@CacheConfig

+

@CacheConfig 可指定公共key的生成策略

+
// 公共的cacheNames (value) 可以统一写在类上面 这样就不用每缓存一个就起名字了
+// 公共的CacheManager
+@CacheConfig(cacheNames = "xxx")
+public class RedisExampleController {
+  
+    @Caching(cacheable = {@Cacheable}, put = {@CachePut})
+    public Response<Map<String, Object>> test(){
+          //to do
+    }
+}
+

复合注解

+
// 查询数据时 先去更新数据 在放到缓存中
+@Caching(cacheable = {@Cacheable}, put = {@CachePut})
+@Caching(cacheable = {@Cacheable}, put = {@CachePut}, evict = {@CacheEvict})
+

测试

+
@Autowired
+private RedisTemplate redisTemplate;
+
+ @Test 
+public void demo(){
+
+    //常见redis类型数据操作 set zset 未列出
+        
+    //string 类型数据
+    redisTemplate.opsForValue().set("test","123");
+    redisTemplate.opsForValue().get("test") // 输出结果为123
+    
+   //list 数据类型
+   redisTemplate.opsForList().rightPushAll("list",new String[]{"1","2","3"});//从右边插入元素
+   redisTemplate.opsForList().range("list",0,-1);//获取所有元素
+   redisTemplate.opsForList().index("listRight",1);//获取下标为2的元素
+   redisTemplate.opsForList().rightPush("listRiht","1");//从右边插入 也可从左边插
+   redisTemplate.opsForList().leftPop("list");//从左边弹出元素 元素弹出将不存在
+
+   //hash
+   redisTemplate.opsForHash().hasKey("redisHash","111");//判断该hash key 是否存在
+   redisTemplate.opsForHash().put("redisHash","name","111");//存放 hash 数据
+   redisTemplate.opsForHash().keys("redisHash");//获取该key对应的hash值
+   redisTemplate.opsForHash().get("redisHash","age");//给定key 获取 hash 值
+ 
+ 
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/toy/java-multi-gadget/index.html b/blog-site/public/posts/toy/java-multi-gadget/index.html new file mode 100644 index 00000000..b2f5cd95 --- /dev/null +++ b/blog-site/public/posts/toy/java-multi-gadget/index.html @@ -0,0 +1,2796 @@ + + + + + + + + + + + Java小程序集合 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Java小程序集合

+ 2022.04.09 +
+

写在前面

+

本文中所涉及的程序均为Java开发,如果您想要直接使用这些工具需要提前配置Java环境。所涉及到的程序均提供完整代码,如果您有兴趣可以尝试运行。

+

使用java -jar命令启动

+

Java玩具-009

+

某些程序功能并不是很完善,但是也可以凑合着用,写这些程序的主要目的是为了方便理解一些常用软件的实现逻辑。

+

强密码生成器

+

生成6到20位的随机强密码,可指定密码长度、密码内容。点击下载

+

Java玩具-001

+

代码

+
/**
+ * 随机生成字符抽象类
+ */
+public abstract class AbstractRandomChar {
+
+    protected static final String LOW_STR = "abcdefghijklmnopqrstuvwxyz";
+    protected static final String UPPER_STR = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    protected static final String SPECIAL_STR = "~!@#$%^&*()_+/|-=[]{};:'<>?.";
+    protected static final String NUM_STR = "0123456789";
+
+
+    /**
+     * 根据字符串获取随机获取字符
+     *
+     * @param str 指定字符串
+     * @return 返回一个随机字符
+     */
+    protected char getRandomChar(String str) {
+        SecureRandom random = new SecureRandom();
+        return str.charAt(random.nextInt(str.length()));
+    }
+
+    /**
+     * 获取随机字符,根据不同的子类不同的实现。现有类型,随机数字、随机大些字母、随机小写字母、随机特殊字符
+     *
+     * @return 随机字符
+     */
+    protected abstract char getRandomChar();
+
+}
+
public class RandomLowChar extends AbstractRandomChar {
+
+    /**
+     * 随机获取小写字符
+     * @return 随机小写字符
+     */
+    @Override
+    public char getRandomChar() {
+        return getRandomChar(LOW_STR);
+    }
+}
+
public class RandomNumChar extends AbstractRandomChar {
+
+    /**
+     * 获取随机获取数字字符
+     * @return 随机获取数字字符
+     */
+    @Override
+    public char getRandomChar() {
+        return getRandomChar(NUM_STR);
+    }
+}
+
public class RandomSpecialChar extends AbstractRandomChar {
+
+    /**
+     * 获取随机获取特殊字符
+     * @return 随机大写字符
+     */
+    @Override
+    public char getRandomChar() {
+        return getRandomChar(SPECIAL_STR);
+    }
+}
+
public class RandomUpperChar extends AbstractRandomChar {
+
+    /**
+     * 随机获取大写字符
+     * @return 随机大写字符
+     */
+    @Override
+    public char getRandomChar() {
+        return getRandomChar(UPPER_STR);
+    }
+}
+
/**
+ * 生成随机密码
+ */
+public class GenRandomPwd {
+
+    /**
+     * 保存 AbstractRandomChar 对象,用于随机数生成
+     */
+    private final List<AbstractRandomChar> randomCharList = new ArrayList<>();
+
+    public GenRandomPwd(boolean genNumCharPwd, boolean genLowCharPwd, boolean genUpperCharPwd, boolean genSpecialCharPwd) {
+
+        if (genNumCharPwd) {
+            randomCharList.add(new RandomNumChar());
+        }
+
+        if (genLowCharPwd) {
+            randomCharList.add(new RandomLowChar());
+        }
+
+        if (genUpperCharPwd) {
+            randomCharList.add(new RandomUpperChar());
+        }
+
+        if (genSpecialCharPwd) {
+            randomCharList.add(new RandomSpecialChar());
+        }
+
+        // 默认都是false的情况下 指定只用数字生成随机数字
+        if (randomCharList.isEmpty()) {
+            randomCharList.add(new RandomNumChar());
+        }
+    }
+
+
+    public String getRandomPwd(int length) {
+        // 密码最大长度
+        int maxPwdLength = 25;
+        // 密码最小长度
+        int minPwdLength = 6;
+        if (length > maxPwdLength || length < minPwdLength) {
+            System.out.printf("密码长度在%d~%d位之间", minPwdLength, maxPwdLength);
+            return "";
+        }
+        // 创建集合将随机生成的字符放入到集合中
+        List<Character> list = new ArrayList<>(length);
+
+        // 产生随机数用于随机调用生成字符的函数
+        int randomListSize = randomCharList.size();
+        for (int i = 0; i < length; i++) {
+            int randomCharNum = new SecureRandom().nextInt(randomListSize);
+            // 随机从randomCharList中获取一个字符,并加入到list中
+            list.add(randomCharList.get(randomCharNum).getRandomChar());
+        }
+
+        // 将选好的list打乱顺序重新排序
+        Collections.shuffle(list);
+
+        // 将char类型转string字符串
+        StringBuilder result = new StringBuilder(list.size());
+        for (Character c : list) {
+            result.append(c);
+        }
+        return result.toString();
+    }
+    
+}
+
public class PwdFrame {
+
+    public void init() {
+        Frame f = new Frame("强密码生成器");
+        String pwdNumContent = "123";
+        String pwdLowContent = "abc";
+        String pwdUpperContent = "ABC";
+        String pwdSpecialContent = "!@#";
+
+        // 一些组件
+        TextField tf = new TextField(22);
+        f.setBounds(600, 250, 280, 140);
+        JButton genPwdBtn = new JButton("生成");
+        JLabel lenLabel = new JLabel("长度", JLabel.LEFT);
+        JLabel contentLabel = new JLabel("内容", JLabel.LEFT);
+        JPanel panel1 = new JPanel(new FlowLayout());
+        JPanel panel2 = new JPanel(new FlowLayout());
+
+        // 密码长度单选框
+        ButtonGroup lenGroup = new ButtonGroup();
+        JRadioButton len1 = new JRadioButton("6", true);
+        JRadioButton len2 = new JRadioButton("8");
+        JRadioButton len3 = new JRadioButton("14");
+        JRadioButton len4 = new JRadioButton("16");
+        JRadioButton len5 = new JRadioButton("20");
+        lenGroup.add(len1);
+        lenGroup.add(len2);
+        lenGroup.add(len3);
+        lenGroup.add(len4);
+        lenGroup.add(len5);
+
+        // 密码内容复选框
+        JRadioButton content1 = new JRadioButton(pwdNumContent, true);
+        JRadioButton content2 = new JRadioButton(pwdLowContent);
+        JRadioButton content3 = new JRadioButton(pwdUpperContent);
+        JRadioButton content4 = new JRadioButton(pwdSpecialContent);
+
+        // 将组件添加到 panel
+        panel1.add(lenLabel);
+        panel1.add(len1);
+        panel1.add(len2);
+        panel1.add(len3);
+        panel1.add(len4);
+        panel1.add(len5);
+
+        panel2.add(contentLabel);
+        panel2.add(content1);
+        panel2.add(content2);
+        panel2.add(content3);
+        panel2.add(content4);
+
+
+        // 设置按钮功能
+        genPwdBtn.addActionListener(e -> {
+            // 获取密码长度单选框的值
+            String checkBoxValLength = getCheckBoxVal(panel1);
+
+            // 获取密码内容单选框的值
+            String checkBoxValContent = getCheckBoxVal(panel2);
+            boolean numPwd = checkBoxValContent.contains(pwdNumContent);
+            boolean lowPwd = checkBoxValContent.contains(pwdLowContent);
+            boolean upperPwd = checkBoxValContent.contains(pwdUpperContent);
+            boolean specialPwd = checkBoxValContent.contains(pwdSpecialContent);
+
+            // 生成强密码
+            GenRandomPwd genRandomPwd = new GenRandomPwd(numPwd, lowPwd, upperPwd, specialPwd);
+            tf.setText(genRandomPwd.getRandomPwd(Integer.parseInt(checkBoxValLength)));
+
+            // 获取光标,使光标一直在文本框内
+            tf.requestFocus();
+        });
+
+        f.add(tf);
+        f.add(genPwdBtn);
+        f.add(panel1);
+        f.add(panel2);
+        f.setResizable(false);
+
+        //设置窗体布局模式为流式布局
+        f.setLayout(new FlowLayout());
+
+        //关闭窗口
+        f.addWindowListener(new WindowAdapter() {
+            @Override
+            public void windowClosing(WindowEvent e) {
+                System.exit(0);
+            }
+        });
+        f.setVisible(true);
+    }
+
+
+    /**
+     * 获取单选框、复选框的值
+     *
+     * @param panel panel对象
+     * @return 单选框的值
+     */
+    public String getCheckBoxVal(JPanel panel) {
+        StringBuilder info = new StringBuilder();
+        for (Component c : panel.getComponents()) {
+            if (c instanceof JRadioButton && ((JRadioButton) c).isSelected()) {
+                // 按空格拆分获取复选框的值
+                info.append(((JRadioButton) c).getText());
+            }
+        }
+        return info.toString();
+    }
+
+}
+
public class AutoPwdMainStarter {
+    public static void main(String[] args) {
+        new PwdFrame().init();
+    }
+}
+

截图工具

+

截图小工具,支持复制到粘贴板、一次性截取多张图片。点击下载

+

Java玩具-002

+

Java玩具-003

+

代码

+
/**
+ * 截屏小工具 参考:https://blog.csdn.net/Code__rookie/article/details/103509851 有改动
+ * @author whitepure
+ */
+public class CaptureScreen extends JFrame implements ActionListener {
+    private JButton start, cancel;
+    private JPanel c;
+    private BufferedImage get;
+    private final JTabbedPane jtp;
+    private int index;
+
+    public CaptureScreen() {
+        super("屏幕截取");
+        try {
+            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+        } catch (Exception exe) {
+            System.out.println("截图异常");
+            exe.printStackTrace();
+        }
+        initWindow();
+        jtp = new JTabbedPane(JTabbedPane.TOP, JTabbedPane.SCROLL_TAB_LAYOUT);
+    }
+
+
+    /**
+     * 初始化窗口
+     */
+    private void initWindow() {
+        start = new JButton("开始截取");
+        cancel = new JButton("退出");
+        start.addActionListener(this);
+        cancel.addActionListener(this);
+
+        JPanel panel = new JPanel();
+        JPanel all = new JPanel();
+
+        c = new JPanel(new BorderLayout());
+        panel.add(start);
+        panel.add(cancel);
+
+        all.add(panel);
+        this.getContentPane().add(c, BorderLayout.CENTER);
+        this.getContentPane().add(all, BorderLayout.SOUTH);
+        setFrameSizeDefault();
+        this.setVisible(true);
+        this.setAlwaysOnTop(true);
+        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        this.setResizable(false);
+    }
+
+    /**
+     * 设置窗口size 位置
+     */
+    private void setFrameSizeWhenExitsImg() {
+        this.setSize(720, 620);
+        this.setLocation(400,150);
+    }
+
+    /**
+     * 设置窗口size 位置
+     */
+    private void setFrameSizeDefault() {
+        this.setSize(180, 80);
+        this.setLocation(600,300);
+    }
+
+    private void updates() {
+        this.setVisible(true);
+        if (get == null) {
+            return;
+        }
+        // 如果索引是0,则表示一张图片都没有被加入过,则要清除当前的东西,重新把tabpane放进来
+        if (index == 0) {
+            c.removeAll();
+            c.add(jtp, BorderLayout.CENTER);
+        }
+        PicPanel pic = new PicPanel(get);
+        jtp.addTab("图片" + (++index), pic);
+        jtp.setSelectedComponent(pic);
+        SwingUtilities.updateComponentTreeUI(c);
+    }
+
+
+    /**
+     * 点击开始截屏执行
+     */
+    private void doStart() {
+        // 点击截屏后隐藏主界面并 sleep 500ms 彻底隐藏主界面
+        this.setVisible(false);
+        try {
+            Thread.sleep(500);
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        Dimension dimension = Toolkit.getDefaultToolkit().getScreenSize();
+        Rectangle rec = new Rectangle(0, 0, dimension.width, dimension.height);
+        BufferedImage bi = null;
+        try {
+            bi = new Robot().createScreenCapture(rec);
+        } catch (AWTException e) {
+            e.printStackTrace();
+        }
+        JFrame jf = new JFrame();
+        // 自定义的截屏时窗口对象,调整大小然后开始截图
+        CutScreen temp = new CutScreen(jf, bi, dimension.width, dimension.height);
+        jf.getContentPane().add(temp, BorderLayout.CENTER);
+
+        jf.setUndecorated(true);
+        jf.setSize(dimension);
+        jf.setVisible(true);
+        jf.setAlwaysOnTop(true);
+
+        // 设置当前窗口大小
+        setFrameSizeWhenExitsImg();
+    }
+
+    /**
+     * 公用的处理保存图片的方法
+     */
+    public void doSave(BufferedImage get) {
+        try {
+            if (get == null) {
+                JOptionPane.showMessageDialog(this
+                        , "图片不能为空!!", "错误", JOptionPane.ERROR_MESSAGE);
+                return;
+            }
+            JFileChooser jfc = new JFileChooser(".");
+            jfc.addChoosableFileFilter(new gifFilter());
+            jfc.addChoosableFileFilter(new bmpFilter());
+            jfc.addChoosableFileFilter(new jpgFilter());
+            jfc.addChoosableFileFilter(new pngFilter());
+            int i = jfc.showSaveDialog(this);
+            if (i == JFileChooser.APPROVE_OPTION) {
+                File file = jfc.getSelectedFile();
+                String about = "PNG";
+                String ext = file.toString().toLowerCase();
+                javax.swing.filechooser.FileFilter ff = jfc.getFileFilter();
+                if (ff instanceof jpgFilter) {
+                    if (!ext.endsWith(".jpg")) {
+                        String ns = ext + ".jpg";
+                        file = new File(ns);
+                        about = "JPG";
+                    }
+                } else if (ff instanceof pngFilter) {
+                    if (!ext.endsWith(".png")) {
+                        String ns = ext + ".png";
+                        file = new File(ns);
+                        about = "PNG";
+                    }
+                } else if (ff instanceof bmpFilter) {
+                    if (!ext.endsWith(".bmp")) {
+                        String ns = ext + ".bmp";
+                        file = new File(ns);
+                        about = "BMP";
+                    }
+                } else if (ff instanceof gifFilter) {
+                    if (!ext.endsWith(".gif")) {
+                        String ns = ext + ".gif";
+                        file = new File(ns);
+                        about = "GIF";
+                    }
+                }
+                if (ImageIO.write(get, about, file)) {
+                    JOptionPane.showMessageDialog(this, "保存成功!");
+                } else {
+                    JOptionPane.showMessageDialog(this, "保存失败!");
+                }
+            }
+        } catch (Exception exe) {
+            exe.printStackTrace();
+        }
+    }
+
+    /**
+     * 公共的处理把当前的图片加入剪帖板的方法
+     */
+    public void doCopy(final BufferedImage image) {
+        try {
+            if (get == null) {
+                JOptionPane.showMessageDialog(this
+                        , "图片不能为空!!", "错误", JOptionPane.ERROR_MESSAGE);
+                return;
+            }
+
+            Transferable trans = new Transferable() {
+                @Override
+                public DataFlavor[] getTransferDataFlavors() {
+                    return new DataFlavor[]{DataFlavor.imageFlavor};
+                }
+
+                @Override
+                public boolean isDataFlavorSupported(DataFlavor flavor) {
+                    return DataFlavor.imageFlavor.equals(flavor);
+                }
+
+                @Override
+                public Object getTransferData(DataFlavor flavor)
+                        throws UnsupportedFlavorException, IOException {
+                    if (isDataFlavorSupported(flavor)) {
+                        return image;
+                    }
+                    throw new UnsupportedFlavorException(flavor);
+                }
+            };
+
+            Toolkit.getDefaultToolkit().getSystemClipboard().setContents(trans, null);
+            JOptionPane.showMessageDialog(this, "已复制到系统粘帖板!!");
+        } catch (Exception exe) {
+            exe.printStackTrace();
+            JOptionPane.showMessageDialog(this
+                    , "复制到系统粘帖板出错!!", "错误", JOptionPane.ERROR_MESSAGE);
+        }
+    }
+
+    /**
+     * 处理关闭事件
+     * @param c 窗口对象
+     */
+    private void doClose(Component c) {
+        jtp.remove(c);
+        if (jtp.getTabCount() == 0){
+            // 将当前截屏的窗口重置为初始化状态
+            setFrameSizeDefault();
+        }
+        c = null;
+        System.gc();
+    }
+
+    /**
+     * 判断当前按钮发生的事件
+     *
+     * @param ae 事件对象
+     */
+    @Override
+    public void actionPerformed(ActionEvent ae) {
+        Object source = ae.getSource();
+        if (source == start) {
+            doStart();
+            return;
+        }
+        if (source == cancel) {
+            System.exit(0);
+            return;
+        }
+        try {
+            UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
+            SwingUtilities.updateComponentTreeUI(this);
+        } catch (Exception exe) {
+            exe.printStackTrace();
+        }
+    }
+
+
+    /**
+     * 截图后预览截图的panel
+     */
+    private class PicPanel extends JPanel implements ActionListener {
+        JButton save;
+        JButton copy;
+        JButton close;
+        BufferedImage get;
+
+        public PicPanel(BufferedImage get) {
+            super(new BorderLayout());
+            this.get = get;
+            initPanel();
+        }
+
+        /**
+         * 初始化
+         */
+        private void initPanel() {
+            save = new JButton("保存");
+            copy = new JButton("复制到剪帖板");
+            close = new JButton("删除");
+
+            JPanel buttonPanel = new JPanel();
+            buttonPanel.add(copy);
+            buttonPanel.add(save);
+            buttonPanel.add(close);
+            JLabel icon = new JLabel(new ImageIcon(get));
+            this.add(new JScrollPane(icon), BorderLayout.CENTER);
+            this.add(buttonPanel, BorderLayout.SOUTH);
+
+            save.addActionListener(this);
+            copy.addActionListener(this);
+            close.addActionListener(this);
+        }
+
+        /**
+         * 判断当前按钮点击发生的事件
+         *
+         * @param e 事件对象
+         */
+        @Override
+        public void actionPerformed(ActionEvent e) {
+            Object source = e.getSource();
+            if (source == save) {
+                doSave(get);
+            } else if (source == copy) {
+                doCopy(get);
+            } else if (source == close) {
+                get = null;
+                doClose(this);
+            }
+        }
+    }
+
+    // 保存BMP格式的过滤器
+    private class bmpFilter extends javax.swing.filechooser.FileFilter {
+        public bmpFilter() {
+        }
+
+        @Override
+        public boolean accept(File file) {
+            if (file.toString().toLowerCase().endsWith(".bmp") ||
+                    file.isDirectory()) {
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public String getDescription() {
+            return "*.BMP(BMP图像)";
+        }
+    }
+
+    // 保存JPG格式的过滤器
+    private class jpgFilter extends javax.swing.filechooser.FileFilter {
+        public jpgFilter() {
+        }
+
+        @Override
+        public boolean accept(File file) {
+            if (file.toString().toLowerCase().endsWith(".jpg") ||
+                    file.isDirectory()) {
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public String getDescription() {
+            return "*.JPG(JPG图像)";
+        }
+    }
+
+    // 保存GIF格式的过滤器
+    private class gifFilter extends javax.swing.filechooser.FileFilter {
+        public gifFilter() {
+        }
+
+        @Override
+        public boolean accept(File file) {
+            return file.toString().toLowerCase().endsWith(".gif") ||
+                    file.isDirectory();
+        }
+
+        @Override
+        public String getDescription() {
+            return "*.GIF(GIF图像)";
+        }
+    }
+
+    // 保存PNG格式的过滤器
+    private class pngFilter extends javax.swing.filechooser.FileFilter {
+        @Override
+        public boolean accept(File file) {
+            return file.toString().toLowerCase().endsWith(".png") ||
+                    file.isDirectory();
+        }
+
+        @Override
+        public String getDescription() {
+            return "*.PNG(PNG图像)";
+        }
+    }
+
+    /**
+     * 显示当前的屏幕图像
+     */
+    private class CutScreen extends JPanel implements MouseListener, MouseMotionListener {
+        public static final int START_X = 1;
+        public static final int START_Y = 2;
+        public static final int END_X = 3;
+        public static final int END_Y = 4;
+        private final BufferedImage bi;
+        private final int width;
+        private final int height;
+        private final JFrame jf;
+        //表示一般情况下的鼠标状态(十字线)
+        private final Cursor cs = new Cursor(Cursor.CROSSHAIR_CURSOR);
+        private int startX, startY, endX, endY, tempX, tempY;
+        //表示选中的区域
+        private Rectangle select = new Rectangle(0, 0, 0, 0);
+        // 表示当前的编辑状态
+        private States current = States.DEFAULT;
+        //表示八个编辑点的区域
+        private Rectangle[] rec;
+        //当前被选中的X和Y,只有这两个需要改变
+        private int currentX, currentY;
+        //当前鼠标移的地点
+        private Point p = new Point();
+        //是否显示提示.如果鼠标左键一按,则提示就不再显示了
+        private boolean showTip = true;
+
+        public CutScreen(JFrame jf, BufferedImage bi, int width, int height) {
+            this.jf = jf;
+            this.bi = bi;
+            this.width = width;
+            this.height = height;
+            this.addMouseListener(this);
+            this.addMouseMotionListener(this);
+            initRecs();
+        }
+
+        private void initRecs() {
+            rec = new Rectangle[8];
+            for (int i = 0; i < rec.length; i++) {
+                rec[i] = new Rectangle();
+            }
+        }
+
+        @Override
+        public void paintComponent(Graphics g) {
+            g.drawImage(bi, 0, 0, width, height, this);
+            g.setColor(Color.RED);
+            g.drawLine(startX, startY, endX, startY);
+            g.drawLine(startX, endY, endX, endY);
+            g.drawLine(startX, startY, startX, endY);
+            g.drawLine(endX, startY, endX, endY);
+
+            int x = Math.min(startX, endX);
+            int y = Math.min(startY, endY);
+            select = new Rectangle(x, y, Math.abs(endX - startX), Math.abs(endY - startY));
+            int x1 = (startX + endX) / 2;
+            int y1 = (startY + endY) / 2;
+
+            g.fillRect(x1 - 2, startY - 2, 5, 5);
+            g.fillRect(x1 - 2, endY - 2, 5, 5);
+            g.fillRect(startX - 2, y1 - 2, 5, 5);
+            g.fillRect(endX - 2, y1 - 2, 5, 5);
+            g.fillRect(startX - 2, startY - 2, 5, 5);
+            g.fillRect(startX - 2, endY - 2, 5, 5);
+            g.fillRect(endX - 2, startY - 2, 5, 5);
+            g.fillRect(endX - 2, endY - 2, 5, 5);
+
+            rec[0] = new Rectangle(x - 5, y - 5, 10, 10);
+            rec[1] = new Rectangle(x1 - 5, y - 5, 10, 10);
+            rec[2] = new Rectangle((Math.max(startX, endX)) - 5, y - 5, 10, 10);
+            rec[3] = new Rectangle((Math.max(startX, endX)) - 5, y1 - 5, 10, 10);
+            rec[4] = new Rectangle((Math.max(startX, endX)) - 5, (Math.max(startY, endY)) - 5, 10, 10);
+            rec[5] = new Rectangle(x1 - 5, (Math.max(startY, endY)) - 5, 10, 10);
+            rec[6] = new Rectangle(x - 5, (Math.max(startY, endY)) - 5, 10, 10);
+            rec[7] = new Rectangle(x - 5, y1 - 5, 10, 10);
+
+            if (showTip) {
+                g.setColor(Color.CYAN);
+                g.fillRect(p.x, p.y, 235, 20);
+                g.setColor(Color.RED);
+                g.drawRect(p.x, p.y, 235, 20);
+                g.setColor(Color.BLACK);
+                g.drawString("按住鼠标不放选择截图,双击完成截图", p.x + 10, p.y + 15);
+            }
+        }
+
+        /**
+         * 根据东南西北等八个方向决定选中的要修改的X和Y的座标
+         *
+         * @param state 状态
+         */
+        private void initSelect(States state) {
+            switch (state) {
+                case EAST:
+                    currentX = (endX > startX ? END_X : START_X);
+                    currentY = 0;
+                    break;
+                case WEST:
+                    currentX = (endX > startX ? START_X : END_X);
+                    currentY = 0;
+                    break;
+                case NORTH:
+                    currentX = 0;
+                    currentY = (startY > endY ? END_Y : START_Y);
+                    break;
+                case SOUTH:
+                    currentX = 0;
+                    currentY = (startY > endY ? START_Y : END_Y);
+                    break;
+                case NORTH_EAST:
+                    currentY = (startY > endY ? END_Y : START_Y);
+                    currentX = (endX > startX ? END_X : START_X);
+                    break;
+                case NORTH_WEST:
+                    currentY = (startY > endY ? END_Y : START_Y);
+                    currentX = (endX > startX ? START_X : END_X);
+                    break;
+                case SOUTH_EAST:
+                    currentY = (startY > endY ? START_Y : END_Y);
+                    currentX = (endX > startX ? END_X : START_X);
+                    break;
+                case SOUTH_WEST:
+                    currentY = (startY > endY ? START_Y : END_Y);
+                    currentX = (endX > startX ? START_X : END_X);
+                    break;
+                case DEFAULT:
+                default:
+                    currentX = 0;
+                    currentY = 0;
+                    break;
+            }
+        }
+
+        /**
+         * 鼠标移动对象
+         *
+         * @param me 事件对象
+         */
+        @Override
+        public void mouseMoved(MouseEvent me) {
+            doMouseMoved(me);
+            initSelect(current);
+            if (showTip) {
+                p = me.getPoint();
+                repaint();
+            }
+        }
+
+        /**
+         * 处理鼠标移动,是为了每次都能初始化一下所要选择的区域
+         *
+         * @param me 鼠标事件对象
+         */
+        private void doMouseMoved(MouseEvent me) {
+            if (select.contains(me.getPoint())) {
+                this.setCursor(new Cursor(Cursor.MOVE_CURSOR));
+                current = States.MOVE;
+            } else {
+                States[] st = States.values();
+                for (int i = 0; i < rec.length; i++) {
+                    if (rec[i].contains(me.getPoint())) {
+                        current = st[i];
+                        this.setCursor(st[i].getCursor());
+                        return;
+                    }
+                }
+                this.setCursor(cs);
+                current = States.DEFAULT;
+            }
+
+        }
+
+        @Override
+        public void mouseExited(MouseEvent me) {
+        }
+
+        @Override
+        public void mouseEntered(MouseEvent me) {
+        }
+
+        /**
+         * 鼠标拖拽对象
+         *
+         * @param me 事件对象
+         */
+        @Override
+        public void mouseDragged(MouseEvent me) {
+            int x = me.getX();
+            int y = me.getY();
+            // 分别处理一系列的(光标)状态(枚举值)
+            if (current == States.MOVE) {
+                startX += (x - tempX);
+                startY += (y - tempY);
+                endX += (x - tempX);
+                endY += (y - tempY);
+                tempX = x;
+                tempY = y;
+            } else if (current == States.EAST || current == States.WEST) {
+                if (currentX == START_X) {
+                    startX += (x - tempX);
+                    tempX = x;
+                } else {
+                    endX += (x - tempX);
+                    tempX = x;
+                }
+            } else if (current == States.NORTH || current == States.SOUTH) {
+                if (currentY == START_Y) {
+                    startY += (y - tempY);
+                    tempY = y;
+                } else {
+                    endY += (y - tempY);
+                    tempY = y;
+                }
+            } else if (current == States.NORTH_EAST || current == States.SOUTH_EAST || current == States.SOUTH_WEST) {
+                if (currentY == START_Y) {
+                    startY += (y - tempY);
+                    tempY = y;
+                } else {
+                    endY += (y - tempY);
+                    tempY = y;
+                }
+                if (currentX == START_X) {
+                    startX += (x - tempX);
+                    tempX = x;
+                } else {
+                    endX += (x - tempX);
+                    tempX = x;
+                }
+            } else {
+                startX = tempX;
+                startY = tempY;
+                endX = me.getX();
+                endY = me.getY();
+            }
+            this.repaint();
+        }
+
+
+        /**
+         * 鼠标按压事件
+         *
+         * @param me 事件对象
+         */
+        @Override
+        public void mousePressed(MouseEvent me) {
+            showTip = false;
+            tempX = me.getX();
+            tempY = me.getY();
+        }
+
+
+        /**
+         * 鼠标释放事件
+         *
+         * @param me 事件对象
+         */
+        @Override
+        public void mouseReleased(MouseEvent me) {
+            // 鼠标右键
+            if (me.isPopupTrigger()) {
+                if (current == States.MOVE) {
+                    showTip = true;
+                    p = me.getPoint();
+                    startX = 0;
+                    startY = 0;
+                    endX = 0;
+                    endY = 0;
+                    repaint();
+                } else {
+                    jf.dispose();
+                    updates();
+                }
+            }
+        }
+
+
+        /**
+         * 鼠标点击事件
+         *
+         * @param me 事件
+         */
+        @Override
+        public void mouseClicked(MouseEvent me) {
+            // 双击左键触发事件
+            if (me.getClickCount() == 2 && select.contains(me.getPoint())) {
+                if (select.x + select.width < this.getWidth() && select.y + select.height < this.getHeight()) {
+                    get = bi.getSubimage(select.x, select.y, select.width, select.height);
+                    jf.dispose();
+                    updates();
+                    return;
+                }
+
+                int wid = select.width, het = select.height;
+                if (select.x + select.width >= this.getWidth()) {
+                    wid = this.getWidth() - select.x;
+                }
+                if (select.y + select.height >= this.getHeight()) {
+                    het = this.getHeight() - select.y;
+                }
+                get = bi.getSubimage(select.x, select.y, wid, het);
+                jf.dispose();
+                updates();
+            }
+        }
+
+    }
+    
+}
+
/**
+ * 截屏方向状态 东西南北
+ * @author whitepure
+ */
+public enum States {
+
+    NORTH_WEST(new Cursor(Cursor.NW_RESIZE_CURSOR)),
+
+    NORTH(new Cursor(Cursor.N_RESIZE_CURSOR)),
+
+    NORTH_EAST(new Cursor(Cursor.NE_RESIZE_CURSOR)),
+
+    EAST(new Cursor(Cursor.E_RESIZE_CURSOR)),
+
+    SOUTH_EAST(new Cursor(Cursor.SE_RESIZE_CURSOR)),
+
+    SOUTH(new Cursor(Cursor.S_RESIZE_CURSOR)),
+
+    SOUTH_WEST(new Cursor(Cursor.SW_RESIZE_CURSOR)),
+
+    WEST(new Cursor(Cursor.W_RESIZE_CURSOR)),
+
+    MOVE(new Cursor(Cursor.MOVE_CURSOR)),
+
+    DEFAULT(new Cursor(Cursor.DEFAULT_CURSOR));
+
+    private Cursor cs;
+
+    States(Cursor cs) {
+        this.cs = cs;
+    }
+
+    public Cursor getCursor() {
+        return cs;
+    }
+}
+
public class CutScreenMainStarter {
+
+    public static void main(String[] args) {
+        SwingUtilities.invokeLater(CaptureScreen::new);
+    }
+}
+

文字转二维码

+

输入文字,生成二维码。点击下载

+

Java玩具-004

+

代码

+

需要依赖第三方类库:点击下载

+
/**
+ * 创建二维码
+ *
+ */
+public class QRCode {
+
+
+    /**
+     * 生成二维码图像
+     *
+     * @param context 二维码内容
+     * @param size   二维码尺寸
+     * @return 二维码图像
+     */
+    public  BufferedImage createPassword(String context, int size) {
+        BufferedImage buffer;
+        Qrcode qrCodeHandler = new Qrcode();
+        qrCodeHandler.setQrcodeErrorCorrect('M');
+        qrCodeHandler.setQrcodeEncodeMode('B');
+        qrCodeHandler.setQrcodeVersion(size);
+        byte[] contextBytes = context.getBytes(StandardCharsets.UTF_8);
+        boolean[][] codeOut = qrCodeHandler.calQrcode(contextBytes);
+
+        // 图像的尺寸 都使用一个值生成一个正方形图案
+        int imgSize = 67 + 12 * (size - 1);
+        buffer = new BufferedImage(imgSize, imgSize, 1);
+        Graphics2D gs = buffer.createGraphics();
+        gs.setColor(Color.BLACK);
+        gs.setBackground(Color.white);
+        gs.clearRect(0, 0, imgSize, imgSize);
+        int pixOff = 2;
+
+        for (int i = 0; i < codeOut.length; ++i) {
+            for (int j = 0; j < codeOut.length; ++j) {
+                if (codeOut[i][j]) {
+                    gs.fillRect(j * 3 + pixOff, i * 3 + pixOff, 3, 3);
+                }
+            }
+        }
+        return buffer;
+    }
+
+}
+
public class QRCodeFrame {
+
+    public QRCodeFrame(){
+        init();
+    }
+
+
+    private void init(){
+        // 一些组件
+        JFrame frame = new JFrame("生成二维码");
+        JTextField input = new JTextField(13);
+        JPanel panel = new JPanel();
+        JPanel imgPanel = new JPanel();
+        JButton btn = new JButton("生成");
+        Border lineBorder = BorderFactory.createLineBorder(Color.gray, 1);
+        // 获取光标,使光标一直在文本框内
+        input.requestFocus();
+        input.setBorder(lineBorder);
+
+        panel.add(input);
+        panel.add(btn);
+
+        // 点击按钮执行
+        btn.addActionListener(e -> {
+            String text = input.getText();
+            if (text == null || "".equals(text)){
+                return;
+            }
+            // 刷新imgPanel
+            imgPanel.removeAll();
+            imgPanel.repaint();
+
+            QRCode qrCode = new QRCode();
+            BufferedImage qrCodeImg = qrCode.createPassword(text, 11);
+
+            ImageIcon imageIcon = new ImageIcon(qrCodeImg);
+            JLabel label = new JLabel(imageIcon);
+            imgPanel.add(label, BorderLayout.CENTER);
+            // 刷新imgPanel
+            imgPanel.updateUI();
+        });
+
+        frame.getContentPane().add(panel, BorderLayout.NORTH);
+        frame.getContentPane().add(imgPanel, BorderLayout.SOUTH);
+        frame.setLayout(new FlowLayout(FlowLayout.CENTER));
+        frame.setBounds(600,250,300,280);
+        frame.setVisible(true);
+        frame.setAlwaysOnTop(true);
+        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        frame.setResizable(false);
+    }
+
+}
+
public class QRCodeMainStarter {
+    public static void main(String[] args) {
+        new QRCodeFrame();
+    }
+}
+

英文翻译器

+

将英文翻译为中文,仅支持单词、短语翻译,程序默认加载自带的英语字典,如果您想要修改字典可在源代码中进行配置或在程序运行时指定字典路径。点击下载

+

Java玩具-005

+

代码

+
/**
+ * 参考:https://www.jb51.net/article/163917.htm 有改动
+ * <p>
+ * 暂时可翻译单词,需要手动录入词典;暂时不能翻译英文句子需要写一些语法判断,比如动词后面跟名词,倒装句结构等等。
+ *
+ * @author whitepure
+ */
+public class EnglishTranslation {
+
+    private Properties pps;
+
+    public EnglishTranslation(String dictPath) {
+        loadDict(dictPath);
+    }
+
+
+    /**
+     * 加载词典
+     *
+     * @param dictPath 词典路径
+     */
+    public void loadDict(String dictPath) {
+        if (dictPath == null || "".equals(dictPath) || !(new File(dictPath).exists())) {
+            System.out.println("加载词典文件不存在");
+            System.exit(0);
+            return;
+        }
+        pps = new Properties();
+        // 以字符载入时没有乱码,以字节载入时出现了乱码
+        try (FileReader fis = new FileReader(dictPath)) {
+            pps.load(fis);
+        } catch (Exception ex) {
+            ex.printStackTrace(System.out);
+            System.out.println("载入词库时出错");
+        }
+    }
+
+    /**
+     * 翻译将待翻译的去词典中去找然后在返回
+     *
+     * @param data 待翻译的数据
+     * @return 翻译后的数据
+     */
+    public String translation(byte[] data) {
+        String srcTxt = new String(data);
+        String dstTxt = srcTxt;
+        String delim = " ,.!?%$*()\n\t";
+        StringTokenizer st = new StringTokenizer(srcTxt, delim, false);
+        String sub, lowerSub, newSub;
+        while (st.hasMoreTokens()) {
+            // 获取待翻译的单词
+            sub = st.nextToken();
+            // 将单词转化为小写
+            lowerSub = sub.toLowerCase();
+            // 从词典中寻找中文对应的单词
+            newSub = pps.getProperty(lowerSub);
+            if (newSub != null) {
+                // 只替换第一个,即只替换了当前的字符串,否则容易造成翻译错误,如 china 翻译为 ch我na
+                dstTxt = dstTxt.replaceFirst(sub, newSub);
+            }
+        }
+        return dstTxt.replaceAll(" ", "");
+    }
+
+}
+
/**
+ * GUI翻译组件
+ *
+ * @author whitepure
+ */
+public class TranslationFrame {
+
+    private final EnglishTranslation englishTranslation;
+
+    public TranslationFrame(String dictPath){
+        englishTranslation = new EnglishTranslation(dictPath);
+        init();
+    }
+
+
+    /**
+     * 初始化gui组件
+     */
+    private void init(){
+        // 一些组件
+        JFrame frame = new JFrame("英语翻译");
+        JTextField input = new JTextField(20);
+        JPanel panel = new JPanel();
+        JButton btn = new JButton("翻译");
+        JTextArea textArea = new JTextArea(4,27);
+        Border lineBorder = BorderFactory.createLineBorder(Color.gray, 1);
+        textArea.setBorder(lineBorder);
+        // 获取光标,使光标一直在文本框内
+        input.requestFocus();
+        input.setBorder(lineBorder);
+
+        panel.add(input);
+        panel.add(btn);
+        frame.getContentPane().add(panel, BorderLayout.NORTH);
+        frame.add(textArea);
+        // 点击翻译按钮执行
+        btn.addActionListener(e -> {
+            String text = input.getText();
+            if (text == null || "".equals(text)){
+                return;
+            }
+            String translationText = englishTranslation.translation(text.getBytes());
+            System.out.printf("%s 译为 %s \n",text,translationText);
+            textArea.setText(translationText);
+        });
+
+        frame.setLayout(new FlowLayout(FlowLayout.CENTER));
+        frame.setBounds(600,250,355,160);
+        frame.setVisible(true);
+        frame.setAlwaysOnTop(true);
+        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        frame.setResizable(false);
+    }
+
+}
+
public class TranslationMainStarter {
+    public static void main(String[] args) {
+        Scanner scanner = new Scanner(System.in);
+        String path = null;
+        System.out.println("请输入字典的全路径,例如:/Users/whitepure/IdeaProjects/gadget/out/production/english-translation/io/github/whitepure/dict.txt");
+        if(scanner.hasNext()){
+            String dictPath = scanner.next();
+            System.out.println("键盘输入的内容是:"+ dictPath);
+            path = dictPath;
+        }
+        if (path == null || "".equals(path)){
+            path = new TranslationMainStarter().getDefaultLoadPath();
+        }
+        new TranslationFrame(path);
+    }
+
+    /**
+     * 加载词典文件加载到内存
+     *
+     * @return 词典路径
+     */
+    private String getDefaultLoadPath() {
+        final String dictPath = new File(Objects.requireNonNull(this.getClass().getResource("")).getPath())
+                + System.getProperty("file.separator")
+                + "dict.txt";
+        System.out.println("当前词典加载路径:" + dictPath);
+        return dictPath;
+    }
+}
+

字典文件dict.txt

+
i=我
+me=我
+myself=我
+he=他
+him=他
+she=她
+it=它
+they=它们
+us=我们
+our=我们
+we=我们
+her=她的
+his=他的
+them=他们
+you=你
+thee=你
+thou=你
+your=你的
+my=我的
+and=并且
+hello=你好
+world=世界
+love=爱
+china=中国
+chinese=中国人
+

提色器

+

按住alt(macOS用户按住option)鼠标滑动即可获取当前位置颜色。点击下载

+

Java玩具-006

+

代码

+
public class ExtracterFrame {
+
+
+    public ExtracterFrame() {
+        init();
+    }
+
+
+    /**
+     * 初始化
+     */
+    private void init() {
+        // 窗口组件
+        JFrame frame = new JFrame("提色器");
+        JPanel rgbPanel = new JPanel();
+        JPanel colorPanel = new JPanel();
+
+        // rgb 组件
+        JLabel labelR = new JLabel("R:");
+        JLabel labelG = new JLabel("G:");
+        JLabel labelB = new JLabel("B:");
+        JTextField txtR = new JTextField(3);
+        JTextField txtG = new JTextField(3);
+        JTextField txtB = new JTextField(3);
+
+        rgbPanel.add(labelR);
+        rgbPanel.add(txtR);
+        rgbPanel.add(labelG);
+        rgbPanel.add(txtG);
+        rgbPanel.add(labelB);
+        rgbPanel.add(txtB);
+
+        // 展示颜色组件
+        JLabel labelHex = new JLabel("16进制:");
+        JTextField txtHex = new JTextField(8);
+        JTextField colorTxt = new JTextField(5);
+        colorTxt.setBackground(Color.BLACK);
+
+        colorPanel.add(labelHex);
+        colorPanel.add(txtHex);
+        colorPanel.add(colorTxt);
+
+
+        class ExtractKeyListener implements KeyListener {
+            @Override
+            public void keyTyped(KeyEvent e) {
+            }
+
+            @Override
+            public void keyPressed(KeyEvent e) {
+            }
+
+            @Override
+            public void keyReleased(KeyEvent e) {
+                if (e.getKeyCode() == 18) {
+                    Robot robot;
+                    try {
+                        robot = new Robot();
+                    } catch (AWTException exception) {
+                        System.out.println("提取颜色失败");
+                        exception.printStackTrace();
+                        return;
+                    }
+                    Point point = MouseInfo.getPointerInfo().getLocation();
+                    Color pixelColor = robot.getPixelColor(point.x, point.y);
+
+                    int red = pixelColor.getRed();
+                    int green = pixelColor.getGreen();
+                    int blue = pixelColor.getBlue();
+
+                    txtR.setText(String.valueOf(red));
+                    txtG.setText(String.valueOf(green));
+                    txtB.setText(String.valueOf(blue));
+
+                    colorTxt.setBackground(pixelColor);
+                    txtHex.setText("#" + Integer.toHexString(red) + Integer.toHexString(green) + Integer.toHexString(blue));
+                }
+            }
+        }
+
+
+        // 监听键盘事件
+        ExtractKeyListener extractKeyListener = new ExtractKeyListener();
+        txtR.addKeyListener(extractKeyListener);
+        txtG.addKeyListener(extractKeyListener);
+        txtB.addKeyListener(extractKeyListener);
+        colorTxt.addKeyListener(extractKeyListener);
+
+
+        // 添加panel
+        frame.getContentPane().add(rgbPanel, BorderLayout.NORTH);
+        frame.getContentPane().add(colorPanel, BorderLayout.SOUTH);
+
+        frame.setLayout(new FlowLayout(FlowLayout.CENTER));
+        frame.setBounds(600, 250, 250, 120);
+        frame.setVisible(true);
+        frame.setAlwaysOnTop(true);
+        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        frame.setResizable(false);
+    }
+
+}
+
public class ExtractColorMainStarter {
+    public static void main(String[] args) {
+        new ExtracterFrame();
+    }
+}
+

图片水印生成器

+

加载图片,输入水印内容,在加载图片的右下会生成红色水印。点击下载

+

Java玩具-007

+

代码

+
public class ImgWatermarking {
+
+
+    /**
+     * 给图片添加水印文字、可设置水印文字的旋转角度
+     *
+     * @param logoText   水印文本
+     * @param srcImgPath 原图片路径
+     * @param degree     旋转角度,如果不旋转设置为 null
+     */
+    public BufferedImage createImgWatermarking(
+            String logoText,
+            String srcImgPath,
+            Integer degree,
+            Color color,
+            Font font,
+            float alpha
+    ) {
+        alpha = alpha == 0 ? 0.5f : alpha;
+        font = font == null ? new Font("微软雅黑", Font.PLAIN, 35) : font;
+        color = color == null ? Color.red : color;
+
+        System.out.println("生成图片=》图片路径:" + srcImgPath + " 水印内容=》" + logoText);
+
+        Image srcImg;
+        try {
+            srcImg = ImageIO.read(new File(srcImgPath));
+        } catch (IOException e) {
+            System.out.println("读取文件失败");
+            e.printStackTrace();
+            return null;
+        }
+        int width = srcImg.getWidth(null);
+        int height = srcImg.getHeight(null);
+        BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
+
+        // 拿到画笔对象
+        Graphics2D g = buffImg.createGraphics();
+
+        // 设置水印
+        g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
+        g.drawImage(srcImg.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null);
+
+        // 设置水印旋转
+        if (null != degree) {
+            g.rotate(Math.toRadians(degree), (buffImg.getWidth() >> 1), (buffImg.getHeight() >> 1));
+        }
+
+        // 设置水印
+        g.setColor(color);
+        g.setFont(font);
+        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
+        int x = width - logoText.length() * font.getSize();
+        int y = height - font.getSize() * 2;
+        g.drawString(logoText, Math.max(x, 0), Math.max(y, 0));
+        g.dispose();
+        return buffImg;
+    }
+
+}
+
/**
+ * @author whitepure
+ */
+public class WatermarkingFrame {
+
+    public WatermarkingFrame() {
+        init();
+    }
+
+    /**
+     * 初始化
+     */
+    private void init() {
+        // 一些组件
+        JFrame frame = new JFrame("生成水印");
+        JTextField input = new JTextField(13);
+        JPanel panel = new JPanel();
+        JPanel imgPanel = new JPanel();
+        JButton createBtn = new JButton("生成");
+        JButton loadImgBtn = new JButton("加载图片");
+        JButton saveAs = new JButton("另存为");
+        Border lineBorder = BorderFactory.createLineBorder(Color.gray, 1);
+        // 获取光标,使光标一直在文本框内
+        input.requestFocus();
+        input.setBorder(lineBorder);
+
+        panel.add(input);
+        panel.add(saveAs);
+        panel.add(createBtn);
+        panel.add(loadImgBtn);
+
+        // 图片路径
+        String[] imgPath = new String[1];
+        // 用于另存为的图片
+        final BufferedImage[] waterMarkingImg = new BufferedImage[1];
+        // 屏幕的尺寸
+        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+
+        // 点击按钮加载图片
+        loadImgBtn.addActionListener(e -> {
+            JFileChooser jfc = new JFileChooser(FileSystemView.getFileSystemView().getHomeDirectory());
+            jfc.setDialogTitle("选择图片");
+            jfc.setAcceptAllFileFilterUsed(false);
+            FileNameExtensionFilter filter = new FileNameExtensionFilter("PNG and JPG images", "png", "jpg");
+            jfc.addChoosableFileFilter(filter);
+
+            int returnValue = jfc.showOpenDialog(null);
+            // 获取图片路径
+            if (returnValue == JFileChooser.APPROVE_OPTION) {
+                String path = jfc.getSelectedFile().getPath();
+                imgPath[0] = path;
+            }
+
+            String path = jfc.getSelectedFile().getPath();
+            File sourceImage = new File(path);
+            BufferedImage image;
+            try {
+                image = ImageIO.read(sourceImage);
+            } catch (IOException ex) {
+                System.out.println("图片加载失败");
+                ex.printStackTrace();
+                JOptionPane.showMessageDialog(panel, "图片加载失败," + ex.getMessage(), "提示", JOptionPane.PLAIN_MESSAGE);
+                return;
+            }
+            // 刷新imgPanel
+            imgPanel.removeAll();
+            imgPanel.repaint();
+
+            // 将图片放入到数组中,用于另存为保存该图片
+            waterMarkingImg[0] = image;
+            int width = image.getWidth(null);
+            int height = image.getHeight(null);
+
+            ImageIcon imageIcon = new ImageIcon(image);
+            imageIcon.setImage(imageIcon.getImage().getScaledInstance(width >> 1, height >> 1, Image.SCALE_DEFAULT));
+            JLabel label = new JLabel(imageIcon);
+
+            imgPanel.add(label, BorderLayout.CENTER);
+            // 刷新imgPanel
+            imgPanel.updateUI();
+            frame.setBounds(((screenSize.width - (width >> 1)) >> 1) , ((screenSize.height - (height >> 1)) >> 2), Math.max((width >> 1), 450), (height >> 1) + 90);
+        });
+
+        // 点击按钮生成水印
+        createBtn.addActionListener(e -> {
+            String text = input.getText();
+            if (text == null || "".equals(text)) {
+                System.out.println("水印内容为空");
+                JOptionPane.showMessageDialog(panel, "水印内容为空", "提示", JOptionPane.WARNING_MESSAGE);
+                return;
+            }
+            if (imgPath[0] == null) {
+                JOptionPane.showMessageDialog(panel, "未加载图片", "提示", JOptionPane.WARNING_MESSAGE);
+                System.out.println("未加载图片");
+                return;
+            }
+            BufferedImage imgWatermarking = new ImgWatermarking().createImgWatermarking(text, imgPath[0], null, null, null, 0);
+
+            // 刷新imgPanel
+            imgPanel.removeAll();
+            imgPanel.repaint();
+
+            // 保存图片用于另存为
+            waterMarkingImg[0] = imgWatermarking;
+            int width = imgWatermarking.getWidth(null);
+            int height = imgWatermarking.getHeight(null);
+
+            ImageIcon imageIcon = new ImageIcon(imgWatermarking);
+            imageIcon.setImage(imageIcon.getImage().getScaledInstance(width >> 1, height >> 1, Image.SCALE_DEFAULT));
+            JLabel label = new JLabel(imageIcon);
+
+            imgPanel.add(label, BorderLayout.CENTER);
+            // 刷新imgPanel
+            imgPanel.updateUI();
+        });
+
+        // 点击另存为
+        saveAs.addActionListener(e -> {
+            BufferedImage image = waterMarkingImg[0];
+            if (image == null) {
+                JOptionPane.showMessageDialog(panel, "未加载图片", "提示", JOptionPane.WARNING_MESSAGE);
+                return;
+            }
+
+            FileDialog fd = new FileDialog(frame, "另存为", FileDialog.SAVE);
+            fd.setVisible(true);
+            // 获取路径
+            String directory = fd.getDirectory();
+            // 获取文件名
+            String fileName = fd.getFile();
+            try (OutputStream out = new FileOutputStream(directory + fileName);) {
+                // 将图片输出到文件
+                ImageIO.write(image, "jpg", out);
+            } catch (IOException ex) {
+                System.out.println("另存为图片失败");
+                ex.printStackTrace();
+                JOptionPane.showMessageDialog(panel, "另存为图片失败," + ex.getMessage(), "提示", JOptionPane.PLAIN_MESSAGE);
+            }
+        });
+
+        frame.getContentPane().add(panel, BorderLayout.NORTH);
+        frame.getContentPane().add(imgPanel, BorderLayout.SOUTH);
+        frame.setLayout(new FlowLayout(FlowLayout.CENTER));
+        frame.setBounds(screenSize.width >> 1, screenSize.height >> 2, 450, 80);
+        frame.setVisible(true);
+        frame.setAlwaysOnTop(true);
+        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+        frame.setResizable(false);
+    }
+
+}
+
public class ImgWatermarkingMainStarter {
+    public static void main(String[] args) {
+        new WatermarkingFrame();
+    }
+}
+

huffman压缩

+

采用huffman算法对文件进行无损压缩,目前仅支持压缩单个文件,压缩好的文件以.huf后缀结尾,会与被压缩文件放在相同目录下。点击下载

+

Java玩具-008

+

代码

+
public abstract class HuffmanCode {
+
+
+    /**
+     * 存放 huffman 编码表
+     */
+    private final Map<Byte, String> huffmanCodes = new HashMap<>();
+
+
+    public Map<Byte, String> getHuffmanCodesTab() {
+        return huffmanCodes;
+    }
+
+    /**
+     * 将文件解码或将文件压缩
+     *
+     * @param zipFile 原文件
+     * @param dstFile 目标文件
+     */
+    public abstract void zipOrUnZip(String zipFile, String dstFile);
+
+    /**
+     * 生成 huffman 编码 压缩
+     *
+     * @param bytes 将传入的文件转成字节传入
+     * @return 将传入字节转换为huffman压缩后的字节数组
+     */
+    public byte[] encode(byte[] bytes) {
+        List<Node> nodes = buildHuffmanNodes(bytes);
+        Node huffmanTreeRoot = buildHuffmanTree(nodes);
+        Map<Byte, String> huffmanCodes = buildHuffmanCodeTab(huffmanTreeRoot);
+        return zip(bytes, huffmanCodes);
+    }
+
+    /**
+     * 将 huffman编码解码,解压缩
+     *
+     * @param huffmanCodes huffman编码表
+     * @param huffmanBytes 待解码的huffman编码(byte数组类型)
+     * @return 解码后的
+     */
+    public byte[] decode(Map<Byte, String> huffmanCodes, byte[] huffmanBytes) {
+        StringBuilder stringBuilder = new StringBuilder();
+
+        // 将byte数组转成二进制的字符串
+        for (int i = 0; i < huffmanBytes.length - 1; i++) {
+            byte b = huffmanBytes[i];
+            String strToAppend = byteToBitString(b);
+            // 判断是不是最后一个字节
+            boolean isLastByte = (i == huffmanBytes.length - 2);
+            if (isLastByte) {
+                // 得到最后一个字节的有效位数
+                byte validBits = huffmanBytes[huffmanBytes.length - 1];
+                strToAppend = strToAppend.substring(0, validBits);
+            }
+            stringBuilder.append(strToAppend);
+        }
+
+        // 将huffman编码key,val反转
+        Map<String, Byte> map = new HashMap<>();
+        huffmanCodes.forEach((key, value) -> map.put(value, key));
+
+        // 创建要给集合,存放byte
+        List<Byte> list = new ArrayList<>();
+        for (int i = 0; i < stringBuilder.length(); ) {
+            int count = 1;
+            boolean flag = true;
+            Byte b = null;
+
+            // 根据huffman编码表来匹配Huffman编码
+            while (flag) {
+                String key = stringBuilder.substring(i, i + count);
+                b = map.get(key);
+                if (b == null) {
+                    // 没有匹配到
+                    count++;
+                } else {
+                    // 匹配到
+                    flag = false;
+                }
+            }
+            list.add(b);
+            i += count;
+        }
+        byte[] b = new byte[list.size()];
+        IntStream.range(0, b.length).forEach(i -> b[i] = list.get(i));
+        return b;
+    }
+
+    /**
+     * 计算传入字节数组中每个字符出现的次数
+     * 在Node对象中添加对应的值
+     *
+     * @param bytes 传入字节数组
+     * @return list
+     */
+    private List<Node> buildHuffmanNodes(byte[] bytes) {
+        ArrayList<Node> nodes = new ArrayList<>();
+        // 利用map记录集合中元素出现的次数
+        Map<Byte, Integer> counts = new HashMap<>();
+        for (byte b : bytes) {
+            counts.merge(b, 1, Integer::sum);
+        }
+        // 把每一个键值对转成一个Node 对象,并加入到nodes集合
+        counts.forEach((key, value) -> nodes.add(new Node(key, value)));
+        return nodes;
+    }
+
+    /**
+     * 构建Huffman树
+     *
+     * @param nodes 传入统计好的 每个字符出现的次数,即{@method buildHuffmanNodes}方法执行完毕
+     * @return 构建好Huffman的根结点
+     */
+    private Node buildHuffmanTree(List<Node> nodes) {
+        while (nodes.size() > 1) {
+            // 排序, 从小到大
+            Collections.sort(nodes);
+            // 取出第一颗最小的二叉树
+            Node leftNode = nodes.get(0);
+            // 取出第二颗最小的二叉树
+            Node rightNode = nodes.get(1);
+            // 创建一颗新的二叉树,它的根节点 没有data, 只有权值
+            Node parent = new Node(null, leftNode.weight + rightNode.weight);
+            parent.left = leftNode;
+            parent.right = rightNode;
+
+            // 将已经处理的两颗二叉树从nodes删除
+            nodes.remove(leftNode);
+            nodes.remove(rightNode);
+            // 将新的二叉树,加入到nodes
+            nodes.add(parent);
+        }
+        // nodes 最后的结点,就是赫夫曼树的根结点
+        return nodes.get(0);
+    }
+
+    /**
+     * 构建huffman编码表
+     *
+     * @param root huffman root结点
+     * @return 返回一个Huffman编码表
+     */
+    private Map<Byte, String> buildHuffmanCodeTab(Node root) {
+        if (root == null) {
+            return null;
+        }
+        // 处理root的左子树
+        buildHuffmanCodeTab(root.left, "0", new StringBuilder());
+        // 处理root的右子树
+        buildHuffmanCodeTab(root.right, "1", new StringBuilder());
+        return huffmanCodes;
+    }
+
+    private void buildHuffmanCodeTab(Node node, String code, StringBuilder stringBuilder) {
+        StringBuilder curNodeCode = new StringBuilder(stringBuilder);
+        curNodeCode.append(code);
+        if (node == null) {
+            return;
+        }
+        // 判断当前node 是叶子结点还是非叶子结点,如果 node.data == null 为非叶子结点
+        if (node.data == null) {
+            // 向左递归
+            buildHuffmanCodeTab(node.left, "0", curNodeCode);
+            // 向右递归
+            buildHuffmanCodeTab(node.right, "1", curNodeCode);
+        } else {
+            // 表示找到某个叶子结点的最后
+            huffmanCodes.put(node.data, curNodeCode.toString());
+        }
+    }
+
+    // 压缩传入字节(将传入字符串转成字节类型)将待压缩字节转换为字节数组
+    private byte[] zip(byte[] bytes, Map<Byte, String> huffmanCodes) {
+        // 利用 huffmanCodes 将 bytes 转成 赫夫曼编码对应的字符串
+        StringBuilder stringBuilder = new StringBuilder();
+        // 遍历bytes 数组
+        for (byte b : bytes) {
+            stringBuilder.append(huffmanCodes.get(b));
+        }
+
+        // 统计返回 byte[] huffmanCodeBytes 长度
+        int len;
+        // 等同于 int len = (stringBuilder.length() + 7) / 8;
+        byte countToEight = (byte) (stringBuilder.length() & 7);
+        if (countToEight == 0) {
+            len = stringBuilder.length() >> 3;
+        } else {
+            len = (stringBuilder.length() >> 3) + 1;
+            // 后面补零
+            for (int i = countToEight; i < 8; i++) {
+                stringBuilder.append("0");
+            }
+        }
+
+        // 创建 存储压缩后的 byte数组,huffmanCodeBytes[len]记录赫夫曼编码最后一个字节的有效位数
+        byte[] huffmanCodeBytes = new byte[len + 1];
+        huffmanCodeBytes[len] = countToEight;
+        int index = 0;
+        // 因为是每8位对应一个byte,所以步长 +8
+        for (int i = 0; i < stringBuilder.length(); i += 8) {
+            String strByte;
+            strByte = stringBuilder.substring(i, i + 8);
+            // 将strByte 转成一个byte,放入到 huffmanCodeBytes
+            huffmanCodeBytes[index] = (byte) Integer.parseInt(strByte, 2);
+            index++;
+        }
+        return huffmanCodeBytes;
+    }
+
+    /**
+     * 将 byte 转换为对应的字符串 解码时要用
+     *
+     * @param b 待处理的字节
+     * @return 将字节转换成二进制字符串
+     */
+    private String byteToBitString(byte b) {
+        int temp = b;
+        // 如果是正数我们需要将高位补零
+        temp |= 0x100;
+        // 转换为二进制字符串,正数:高位补 0 即可,然后截取低八位即可;负数直接截取低八位即可
+        // 负数在计算机内存储的是补码,补码转原码:先 -1 ,再取反
+        String binaryStr = Integer.toBinaryString(temp);
+        return binaryStr.substring(binaryStr.length() - 8);
+    }
+
+
+    /**
+     * huffman 树的结点
+     */
+    public static class Node implements Comparable<Node> {
+        Byte data;
+        int weight;
+        Node left;
+        Node right;
+
+        public Node(Byte data, int weight) {
+            this.data = data;
+            this.weight = weight;
+        }
+
+        @Override
+        public int compareTo(Node o) {
+            // 从小到大排序
+            return this.weight - o.weight;
+        }
+
+        @Override
+        public String toString() {
+            return "Node [data = " + data + " weight=" + weight + "]";
+        }
+    }
+}
+
public class HuffmanUnZip extends HuffmanCode {
+
+
+    @Override
+    public void zipOrUnZip(String zipFile, String dstFile) {
+        // 定义文件输入流
+        InputStream is = null;
+        // 定义一个对象输入流
+        ObjectInputStream ois = null;
+        // 定义文件的输出流
+        OutputStream os = null;
+        try {
+            // 创建文件输入流
+            is = new FileInputStream(zipFile);
+            // 创建一个和 is关联的对象输入流
+            ois = new ObjectInputStream(is);
+            // 读取byte数组 huffmanBytes
+            byte[] huffmanBytes = (byte[]) ois.readObject();
+            // 读取赫夫曼编码表
+            Map<Byte, String> huffmanCodes = (Map<Byte, String>) ois.readObject();
+            // 解码
+            byte[] bytes = decode(huffmanCodes, huffmanBytes);
+            // 将bytes 数组写入到目标文件
+            os = new FileOutputStream(dstFile);
+            // 写数据到 dstFile 文件
+            os.write(bytes);
+        } catch (Exception e) {
+            // TODO: handle exception
+            System.out.println(e.getMessage());
+        } finally {
+            try {
+                if (os != null) {
+                    os.close();
+                }
+                if (ois != null) {
+                    ois.close();
+                }
+                if (is != null) {
+                    is.close();
+                }
+            } catch (Exception e2) {
+                // TODO: handle exception
+                System.out.println(e2.getMessage());
+            }
+
+        }
+    }
+}
+
public class HuffmanZip extends HuffmanCode{
+
+    @Override
+    public void zipOrUnZip(String srcFile, String dstFile){
+        // 创建输出流
+        OutputStream os = null;
+        ObjectOutputStream oos = null;
+        // 创建文件的输入流
+        FileInputStream is = null;
+        try {
+            // 创建文件的输入流
+            is = new FileInputStream(srcFile);
+            // 创建一个和源文件大小一样的byte[]
+            byte[] b = new byte[is.available()];
+            // 读取文件
+            is.read(b);
+            // 直接对源文件压缩
+            byte[] huffmanBytes = encode(b);
+            // 创建文件的输出流, 存放压缩文件
+            os = new FileOutputStream(dstFile);
+            // 创建一个和文件输出流关联的ObjectOutputStream
+            oos = new ObjectOutputStream(os);
+            // 把 赫夫曼编码后的字节数组写入压缩文件
+            oos.writeObject(huffmanBytes);
+            // 这里我们以对象流的方式写入 赫夫曼编码,是为了以后我们恢复源文件时使用
+            // 注意一定要把赫夫曼编码 写入压缩文件
+            oos.writeObject(getHuffmanCodesTab());
+
+        } catch (Exception e) {
+            throw new RuntimeException("使用huffman编码压缩异常!", e);
+        } finally {
+            try {
+                if (is != null) {
+                    is.close();
+                }
+                if (oos != null) {
+                    oos.close();
+                }
+                if (os != null) {
+                    os.close();
+                }
+            } catch (Exception e) {
+                // TODO: handle exception
+                System.out.println(e.getMessage());
+            }
+        }
+    }
+
+}
+
public class HuffmanFrame {
+
+    /**
+     * 当前屏幕的宽
+     */
+    private final int WINDOW_WIDTH;
+
+    /**
+     * 当前屏幕的高
+     */
+    private final int WINDOW_HEIGHT;
+
+    public HuffmanFrame() {
+        Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
+        WINDOW_WIDTH = screenSize.width;
+        WINDOW_HEIGHT = screenSize.height;
+    }
+
+
+    /**
+     * 初始化huffman窗口
+     *
+     * @param width  宽
+     * @param height 高
+     */
+    public void init(int width, int height) {
+        Frame frame = new Frame();
+        frame.setTitle("huffman压缩");
+        // 设置窗口可见
+        frame.setVisible(true);
+        // 禁止调整窗口大小
+        frame.setResizable(false);
+        // 设置窗口大小
+        frame.setSize(width, height);
+        // 设置窗口出现在屏幕的位置
+        frame.setLocation((WINDOW_WIDTH - width) >> 1, (WINDOW_HEIGHT - height) >> 2);
+        FlowLayout flowLayout = new FlowLayout();
+        //设置对齐方式
+        flowLayout.setAlignment(FlowLayout.CENTER);
+        // 设置流式布局
+        frame.setLayout(flowLayout);
+        // 设置按钮
+        Button zipBtn = new Button("压缩");
+        Button unZipBtn = new Button("解压缩");
+        frame.add(zipBtn, BorderLayout.CENTER);
+        frame.add(unZipBtn, BorderLayout.CENTER);
+        listenerBtnOfFile(zipBtn, new HuffmanZip(),frame);
+        listenerBtnOfFile(unZipBtn, new HuffmanUnZip(),frame);
+        //关闭窗口
+        frame.addWindowListener(new WindowAdapter() {
+            @Override
+            public void windowClosing(WindowEvent e) {
+                System.exit(0);
+            }
+        });
+    }
+
+
+    /**
+     * 监听点击按钮触发事件,解压缩或压缩
+     *
+     * @param btn         按钮对象
+     * @param huffmanCode 压缩或解压缩对象
+     */
+    private void listenerBtnOfFile(Button btn, HuffmanCode huffmanCode,Frame frame) {
+        btn.addActionListener(e -> {
+            FileDialog openDialog = new FileDialog(frame, "打开文件", FileDialog.LOAD);
+            openDialog.setVisible(true);
+
+            String dirName = openDialog.getDirectory();
+            String fileName = openDialog.getFile();
+            String src = dirName + fileName;
+            if (dirName == null || fileName == null) {
+                return;
+            }
+
+            // 压缩或解压缩后 弹框提示
+            JDialog jDialog = new JDialog();
+            jDialog.setSize(200, 200);
+            jDialog.setLocation((WINDOW_WIDTH - 200) / 2, (WINDOW_HEIGHT - 200) / 3);
+            jDialog.setVisible(true);
+            jDialog.setTitle("提示");
+            jDialog.setLayout(new FlowLayout());
+            JLabel jLabel = new JLabel();
+            String suffix = ".huf";
+            String dist;
+
+            // 如果文件包含了后缀则需要解压缩文件 不给原文件添加后缀
+            if (src.contains(suffix)) {
+                dist = src.substring(0, src.length() - 4);
+            } else {
+                // 如果不包含压缩后缀 && 传递解压缩类型则无法解压
+                if (huffmanCode instanceof HuffmanUnZip) {
+                    jLabel.setText("不包含" + suffix + "无法解压!");
+                    jDialog.add(jLabel);
+                    return;
+                }
+                dist = src + suffix;
+            }
+            System.out.printf("原文件:%s \t 目标文件:%s \n", src, dist);
+
+            try {
+                huffmanCode.zipOrUnZip(src, dist);
+                jLabel.setText("操作成功!");
+            } catch (Exception exception) {
+                jLabel.setText("操作异常!" + exception.getMessage());
+            }
+            jDialog.add(jLabel);
+        });
+    }
+
+}
+
public class HuffmanMainStarter {
+    public static void main(String[] args) {
+        new HuffmanFrame().init(200,70);
+    }
+}
+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/toy/js-birthday-gift/index.html b/blog-site/public/posts/toy/js-birthday-gift/index.html new file mode 100644 index 00000000..122ead5a --- /dev/null +++ b/blog-site/public/posts/toy/js-birthday-gift/index.html @@ -0,0 +1,477 @@ + + + + + + + + + + + Js生日礼物 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Js生日礼物

+ 2018.08.24 +
+

index.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>card</title>
+    <style>
+        body,html{
+            width: 100%;
+            height: 100%;
+        }
+        body{
+            display: flex;/*弹性盒模型*/
+            justify-content: center;/*水平对齐  盒子位于中心*/
+            align-items: center;/*竖直对齐  居中对齐*/
+            background-color:  yellow;
+            perspective: 1000px;/*景深:眼到屏幕的距离*/
+        }
+        body,h1,p{
+            margin: 0;
+        }
+        .card{
+            width: 520px;
+            height: 350px;
+            border-radius: 15px;
+            background: linear-gradient(#020333 70%,#fff 75%);/*渐变色*/
+            transform:rotateX(0deg);
+            transform-style: preserve-3d;
+            animation: move 1.5s;
+        }
+        .box{
+            width: 520px;
+            height: 350px;
+            font-family: Rockwell;
+            transform-style: preserve-3d;
+            transform: translateZ(88px);
+            box-shadow: 0 0 30px #000 inset;
+        }
+        @keyframes move {
+            0%{
+                transform:  scale(0);
+            }
+            30%{
+                transform:  scale(1.2);
+            }
+            40%{
+                transform:  scale(0.85);
+            }
+            50%{
+                transform:  scale(1.15);
+            }
+            60%{
+                transform:  scale(0.9);
+            }
+            70%{
+                transform: scale(1.1);
+            }
+            80%{
+                transform:  scale(0.95);
+            }
+            90%{
+                transform:  scale(1.05);
+            }
+            100%{
+                transform:  scale(1);
+            }
+        }
+        h1{
+
+            color: #fff;
+            height: 60%;
+            font-size: 46px;
+            text-align: center;
+            line-height: 210px;
+        }
+        p{
+
+            color: #a9467d;
+            height: 40%;
+            font-size: 24px;
+            text-align: center;
+            line-height: 140px;
+
+        }
+
+    </style>
+    <script src="jquery-3.3.1.js"></script>
+
+</head>
+<body>
+<div class="card">
+    <div class="box">
+        <h1>Happy Birthday
+            <span style="color: #FF0000;font-size: 50px">XXX</span>
+        </h1>
+        <p>To my friend,
+            爱你的XXX爸爸!
+        </p>
+    </div>
+</div>
+<script>
+    var card = $(".card");
+    $(document).on("mousemove",function (e) {
+        var ax = ($(window).innerWidth() / 2 - e.pageX)/20;
+        var ay = ($(window).innerHeight() / 2 - e.pageY)/10;
+        card.attr("style","transform: rotateY("+ ax +"deg)rotateX("+ay +"deg) translateZ(7em)");
+    });
+</script>
+</body>
+</html>
+

运行效果

+

Js-生日礼物-01.png

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/toy/js-box-drag/index.html b/blog-site/public/posts/toy/js-box-drag/index.html new file mode 100644 index 00000000..41bd7c16 --- /dev/null +++ b/blog-site/public/posts/toy/js-box-drag/index.html @@ -0,0 +1,474 @@ + + + + + + + + + + + Js滑块拖拽 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Js滑块拖拽

+ 2018.09.08 +
+

index.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>滑块拖拽</title>
+</head>
+<style>
+    body {
+        margin: 0;
+        padding: 0;
+        user-select: none;
+    }
+
+    .content {
+        position: relative;
+        width: 300px;
+        height: 40px;
+        margin: 50px auto;
+        background-color: #E8E8EB;
+        text-align: center;
+        line-height: 40px;
+    }
+
+    .rect {
+        position: absolute;
+        width: 100%;
+        height: 100%;
+
+    }
+
+    .rect .bg {
+        position: absolute;
+        left: 0;
+        top: 0;
+        z-index: 1;
+        width: 0;
+        height: 100%;
+        background: rgba(122,194,60,.4);
+    }
+
+    .rect .move {
+        -webkit-box-sizing: border-box;
+        -moz-box-sizing: border-box;
+        box-sizing: border-box;
+        width: 45px;
+        height: 40px;
+        position: absolute;
+        top: 0;
+        left: 0;
+        background-color: #fff;
+        border: 1px solid #cccccc;
+    }
+    .rect span{
+
+    }
+</style>
+<body>
+<div class="content">
+    <div class="rect">
+        <div class="bg"></div>
+        <span>滑块拖动验证</span>
+        <div class="move">
+            <img src="images/right.png" alt="图片拖动">
+        </div>
+    </div>
+</div>
+<script>
+    var oMove = document.querySelector(".move"),
+        oBg = document.querySelector(".bg"),
+        oRect = document.querySelector(".rect"),
+        oImage = document.querySelector("img"),
+        oSpan = document.querySelector("span"),
+        _X = 0;
+    oMove.onmousedown = function (e) {
+
+        var startX = e.clientX;
+        document.onmousemove = function (e) {
+            var endX = e.clientX;
+            _X = endX - startX;
+            if (_X < 0) {
+                _X = 0;
+            }
+            if (_X > 255) {
+                _X = 255;
+            }
+            oBg.style.width = oMove.style.left = _X + "px";
+
+            if (_X >= 255) {
+                oRect.style.color = "black";
+                oImage.src = "images/left.png";
+                oSpan.innerHTML = "验证成功";
+                oBg.style.width = oMove.style.left = 255 + "px";
+            }
+        }
+    };
+    document.onmouseup = function () {
+        document.onmousemove = null;
+        if (_X < 255) {
+            oBg.style.width = oMove.style.left = 0;
+        }
+
+    }
+</script>
+
+</body>
+</html>
+

运行效果

+

Js滑块拖拽-01.png

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/toy/js-gomoku/index.html b/blog-site/public/posts/toy/js-gomoku/index.html new file mode 100644 index 00000000..fa38a406 --- /dev/null +++ b/blog-site/public/posts/toy/js-gomoku/index.html @@ -0,0 +1,654 @@ + + + + + + + + + + + Js五子棋 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Js五子棋

+ 2018.09.10 +
+

index.html

+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+<title>五子棋</title>
+<meta name="viewport" content="device-width; initial-scale=1.0;" />
+<style>
+#c1 {
+	display: block;
+	margin: 60px auto;
+	box-shadow: 1px 1px 5px #000000;
+}
+</style>
+<script src="js/index.js"></script>
+
+</head>
+
+<body>
+<canvas id="c1" width="450px" height="450px"></canvas>
+</body>
+</html>
+

index.js

+
window.onload = function(){
+    var oC = document.getElementById('c1');
+    var oGc = oC.getContext('2d');
+
+    var over = false;
+
+    oGc.strokeStyle = "#bfbfbf";
+
+    //绘制棋盘
+    for(var i=0;i<15;i++){
+        oGc.moveTo(15+i*30,15);
+        oGc.lineTo(15+i*30,435);
+        oGc.stroke();
+        oGc.moveTo(15,15+i*30);
+        oGc.lineTo(435,15+i*30);
+        oGc.stroke();
+    }
+
+
+    /*	AI难点解析
+        赢法数组:记录了五子棋说有的赢法,三维数组
+        每一种赢法的统计数组,一维数组
+        如何判断胜负
+        计算机落子规则*/
+
+    //赢法数组
+    var wins = [];
+
+    for(var i=0;i<15;i++){
+        wins[i] = [];
+        for(var j=0;j<15;j++){
+            wins[i][j] = [];
+        }
+    }
+
+    var count = 0;
+    for(var i=0;i<15;i++){
+        for(var j=0;j<11;j++){
+            //i=0 j=0
+            //wins[0][0][0] = true;
+            //wins[0][1][0] = true;
+            //wins[0][2][0] = true;
+            //wins[0][3][0] = true;
+            //wins[0][4][0] = true;
+
+            //wins[0][1][1] = true;
+            //wins[0][2][1] = true;
+            //wins[0][3][1] = true;
+            //wins[0][4][1] = true;
+            //wins[0][5][1] = true;
+            for(var k=0;k<5;k++){
+                wins[i][j+k][count] = true;
+            }
+            count++;
+        }
+    }
+    for(var i=0;i<15;i++){
+        for(var j=0;j<11;j++){
+            for(var k=0;k<5;k++){
+                wins[j+k][i][count] = true;
+            }
+            count++;
+        }
+    }
+    for(var i=0;i<11;i++){
+        for(var j=0;j<11;j++){
+            for(var k=0;k<5;k++){
+                wins[i+k][j+k][count] = true;
+            }
+            count++;
+        }
+    }
+    for(var i=0;i<11;i++){
+        for(var j=14;j>3;j--){
+            for(var k=0;k<5;k++){
+                wins[i+k][j-k][count] = true;
+            }
+            count++;
+        }
+    }
+
+    var myWin = [];
+    var computerWin = [];
+
+    for(var i=0;i<count;i++){
+        myWin[i] = 0;
+        computerWin[i] = 0;
+    }
+
+    function oneStep(i,j,me){
+        oGc.beginPath();
+        oGc.arc(15+i*30,15+j*30,13,0,2*Math.PI);
+        oGc.closePath();
+        var gradient = oGc.createRadialGradient(15+i*30+2,15+j*30+2,13,15+i*30+2,15+j*30+2,0);
+        if(me){
+            gradient.addColorStop(0,"#0A0A0A");
+            gradient.addColorStop(1,"#636766");
+        }else{
+            gradient.addColorStop(0,"#D1D1D1");
+            gradient.addColorStop(1,"#F9F9F9");
+        }
+
+        oGc.fillStyle = gradient;
+        oGc.fill();
+
+    };
+
+    var me = true;
+    var chessBoard = [];
+    for(var i=0;i<15;i++){
+        chessBoard[i] = [];
+        for(var j=0;j<15;j++){
+            chessBoard[i][j] = 0;
+        }
+    };
+
+    oC.onclick = function(ev){
+        if(!me){return;}
+        if(over){return;}
+
+        var x = ev.offsetX;
+        var y = ev.offsetY;
+        var i = Math.floor(x/30);
+        var j = Math.floor(y/30);
+
+        if(chessBoard[i][j] == 0){
+            oneStep(i,j,me);
+            chessBoard[i][j] = 1;
+
+        }
+
+        for(var k=0;k<count;k++){
+            if(wins[i][j][k]){
+                myWin[k]++;
+                computerWin[k] = 6;
+                if(myWin[k] == 5){
+                    window.alert("恭喜你,获得了胜利!");
+                    over = true;
+                }
+            }
+        }
+
+        if(!over){
+            computerAI();
+            me = !me;
+        }
+
+    }
+
+    function computerAI(){
+        var myScore = [];
+        var computerScore = [];
+        var iMax = 0;
+        var u =0;
+        var v= 0;
+
+        for(var i=0;i<15;i++){
+            myScore[i] = [];
+            computerScore[i] = [];
+            for(var j=0;j<15;j++){
+                myScore[i][j] = 0;
+                computerScore[i][j] = 0;
+            }
+        }
+
+        for(var i=0;i<15;i++){
+            for(var j=0;j<15;j++){
+                if(chessBoard[i][j] == 0){
+                    for(var k=0;k<count;k++){
+                        if(wins[i][j][k]){
+                            if(myWin[k] == 1){
+                                myScore[i][j]+=200;
+                            }else if(myWin[k] == 2){
+                                myScore[i][j]+=400;
+                            }else if(myWin[k] == 3){
+                                myScore[i][j]+=2000;
+                            }else if(myWin[k] == 4){
+                                myScore[i][j]+=10000;
+                            }
+
+                            if(computerWin[k] == 1){
+                                computerScore[i][j]+=400;
+                            }else if(computerWin[k] == 2){
+                                computerScore[i][j]+=800;
+                            }else if(computerWin[k] == 3){
+                                computerScore[i][j]+=2200;
+                            }else if(computerWin[k] == 4){
+                                computerScore[i][j]+=20000;
+                            }
+                        }
+                    }
+
+                    if(myScore[i][j]>iMax){
+                        iMax = myScore[i][j];
+                        u = i;
+                        v = j;
+                    }else if(myScore[i][j]==iMax){
+                        if(computerScore[i][j]>computerScore[u][v]){
+                            u = i;
+                            v = j;
+                        }
+                    }
+
+                    if(computerScore[i][j]>iMax){
+                        iMax = computerScore[i][j];
+                        u = i;
+                        v = j;
+                    }else if(computerScore[i][j]==iMax){
+                        if(myScore[i][j]>myScore[u][v]){
+                            u = i;
+                            v = j;
+                        }
+                    }
+                }
+            }
+        }
+
+        oneStep(u,v,false);
+        chessBoard[u][v] = 2;
+
+        for(var k=0;k<count;k++){
+            if(wins[u][v][k]){
+                computerWin[k]++;
+                myWin[k] = 6;
+                if(computerWin[k] == 5){
+                    window.alert('YOU LOST!');
+                    over = true;
+                }
+            }
+        }
+
+        console.log(iMax);
+        if(!over){
+            me = !me;
+        }
+
+    };
+
+
+};
+

运行效果

+

Js五子棋-01

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/toy/js-loadding-lazy/index.html b/blog-site/public/posts/toy/js-loadding-lazy/index.html new file mode 100644 index 00000000..fb03e9e9 --- /dev/null +++ b/blog-site/public/posts/toy/js-loadding-lazy/index.html @@ -0,0 +1,623 @@ + + + + + + + + + + + Js懒加载 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Js懒加载

+ 2018.09.21 +
+

index.html

+
<!doctype html>
+<html lang="en">
+ <head>
+  <meta charset="UTF-8">
+  <meta name="Generator" content="EditPlus®">
+  <meta name="Author" content="">
+  <meta name="Keywords" content="">
+  <meta name="Description" content="">
+  <title>懒加载技术</title>
+  <style>
+  	*{
+		margin: 0;
+		padding:0;
+	}
+	body{
+		background: rgb(0,0,0);
+	}
+	.box{
+		overflow: hidden;
+		width: 948px;
+		background-color: #7c7c7c;
+		margin: 50px auto;
+		-webkit-border-radius: 10px;
+		-moz-border-radius: 10px;
+		border-radius: 10px;
+
+	}
+	.box img{
+		float: left;
+		display: block;
+		width: 300px;
+		height: 150px;
+		margin: 6px;
+		border: 2px solid grey;
+		-webkit-border-radius: 10px;
+		-moz-border-radius: 10px;
+		border-radius: 10px;
+	}
+  </style>
+	 <script src="js/lazyload.js"></script>
+
+ </head>
+ <body>
+ 	<div class="box">
+		<img src="images/1.jpg"/>
+		<img src="images/2.jpg"/>
+		<img src="images/3.jpg"/>
+		<img src="images/4.jpg"/>
+		<img src="images/5.jpg"/>
+		<img src="images/6.jpg"/>
+		<img src="images/7.jpg"/>
+		<img src="images/8.jpg"/>
+		<img src="images/9.jpg"/>
+		<img src="images/10.jpg"/>
+		<img src="images/11.jpg"/>
+		<img src="images/12.jpg"/>
+		<img src="images/13.jpg"/>
+		<img src="images/14.jpg"/>
+		<img src="images/15.jpg"/>
+		<img src="images/16.jpg"/>
+		<img src="images/17.jpg"/>
+		<img src="images/18.jpg"/>
+		<img src="images/19.jpg"/>
+		<img src="images/20.jpg"/>
+		<img src="images/1.jpg"/>
+		<img src="images/2.jpg"/>
+		<img src="images/3.jpg"/>
+		<img src="images/4.jpg"/>
+		<img src="images/5.jpg"/>
+		<img src="images/6.jpg"/>
+		<img src="images/7.jpg"/>
+		<img src="images/8.jpg"/>
+		<img src="images/9.jpg"/>
+		<img src="images/10.jpg"/>
+		<img src="images/11.jpg"/>
+		<img src="images/12.jpg"/>
+		<img src="images/13.jpg"/>
+		<img src="images/14.jpg"/>
+		<img src="images/15.jpg"/>
+		<img src="images/16.jpg"/>
+		<img src="images/17.jpg"/>
+		<img src="images/18.jpg"/>
+		<img src="images/19.jpg"/>
+		<img src="images/20.jpg"/>
+		<img src="images/1.jpg"/>
+		<img src="images/2.jpg"/>
+		<img src="images/3.jpg"/>
+		<img src="images/4.jpg"/>
+		<img src="images/5.jpg"/>
+		<img src="images/6.jpg"/>
+		<img src="images/7.jpg"/>
+		<img src="images/8.jpg"/>
+		<img src="images/9.jpg"/>
+		<img src="images/10.jpg"/>
+		<img src="images/11.jpg"/>
+		<img src="images/12.jpg"/>
+		<img src="images/13.jpg"/>
+		<img src="images/14.jpg"/>
+		<img src="images/15.jpg"/>
+		<img src="images/16.jpg"/>
+		<img src="images/17.jpg"/>
+		<img src="images/18.jpg"/>
+		<img src="images/19.jpg"/>
+		<img src="images/20.jpg"/>
+	</div>
+  <script src="js/jquery-1.11.1.min.js"></script>
+  <script src="js/lazyload.js"></script>
+  <script>
+	$("img").lazyload({
+  placeholder : "images/loading.gif", //用图片提前占位
+    // placeholder,值为某一图片路径.此图片用来占据将要加载的图片的位置,待图片加载时,占位图则会隐藏
+  effect: "fadeIn", // 载入使用何种效果
+    // effect(特效),值有show(直接显示),fadeIn(淡入),slideDown(下拉)等,常用fadeIn
+  threshold: -150, // 提前开始加载
+    // threshold,值为数字,代表页面高度.如设置为200,表示滚动条在离目标位置还有200的高度时就开始加载图片,可以做到不让用户察觉
+  //event: 'click',  // 事件触发时才加载
+    // event,值有click(点击),mouseover(鼠标划过),sporty(运动的),foobar(…).可以实现鼠标莫过或点击图片才开始加载,后两个值未测试…
+  //container: $("#container"),  // 对某容器中的图片实现效果
+    // container,值为某容器.lazyload默认在拉动浏览器滚动条时生效,这个参数可以让你在拉动某DIV的滚动条时依次加载其中的图片
+  //failurelimit : 10 // 图片排序混乱时
+     // failurelimit,值为数字.lazyload默认在找到第一张不在可见区域里的图片时则不再继续加载,但当HTML容器混乱的时候可能出现可见区域内图片并没加载出来的情况,failurelimit意在加载N张可见区域外的图片,以避免出现这个问题.
+});
+  </script>
+ </body>
+</html>
+

lazyload.js

+
(function($){
+    $.fn.lazyload = function(options){
+        var settings = {
+            threshold: 0,
+            failurelimit: 0,
+            event: "scroll",
+            effect: "show",
+            container: window
+        };
+        if(options){
+            $.extend(settings, options);
+        }
+        var elements = this;
+        if("scroll" == settings.event){
+            $(settings.container).bind("scroll", function(event){
+                var counter = 0;
+                elements.each(function(){
+                    if($.abovethetop(this, settings) || $.leftofbegin(this, settings)){
+                    } else if(!$.belowthefold(this, settings) && !$.rightoffold(this, settings)){
+                        $(this).trigger("appear");
+                    } else {
+                        if(counter++ > settings.failurelimit){
+                            return false;
+                        }
+                    }
+                });
+                var temp = $.grep(elements, function(element){
+                    return !element.loaded;
+                });
+                elements = $(temp);
+            });
+        }
+        this.each(function(){
+            var self = this;
+            if(undefined == $(self).attr("original")){
+                $(self).attr("original", $(self).attr("src"));
+            }
+            if("scroll" != settings.event || undefined == $(self).attr("src") || settings.placeholder == $(self).attr("src") || ($.abovethetop(self, settings) || $.leftofbegin(self, settings) || $.belowthefold(self, settings) || $.rightoffold(self, settings))){
+                if(settings.placeholder){
+                    $(self).attr("src", settings.placeholder);
+                } else {
+                    $(self).removeAttr("src");
+                }
+                self.loaded = false;
+            } else {
+                self.loaded = true;
+            }
+            $(self).one("appear", function(){
+                if(!this.loaded){
+                    $("<img />").bind("load", function(){
+                        $(self).hide().attr("src", $(self).attr("original"))[settings.effect](settings.effectspeed);
+                        self.loaded = true;
+                    }).attr("src", $(self).attr("original"));
+                }
+            });
+            if("scroll" != settings.event){
+                $(self).bind(settings.event, function(event){
+                    if(!self.loaded){
+                        $(self).trigger("appear");
+                    }
+                });
+            }
+        });
+        $(settings.container).trigger(settings.event);
+        return this;
+    };
+    $.belowthefold = function(element, settings){
+        if(settings.container === undefined || settings.container === window){
+            var fold = $(window).height() + $(window).scrollTop();
+        } else {
+            var fold = $(settings.container).offset().top + $(settings.container).height();
+        }
+        return fold <= $(element).offset().top - settings.threshold;
+    };
+    $.rightoffold = function(element, settings){
+        if(settings.container === undefined || settings.container === window){
+            var fold = $(window).width() + $(window).scrollLeft();
+        } else {
+            var fold = $(settings.container).offset().left + $(settings.container).width();
+        }
+        return fold <= $(element).offset().left - settings.threshold;
+    };
+    $.abovethetop = function(element, settings){
+        if(settings.container === undefined || settings.container === window){
+            var fold = $(window).scrollTop();
+        } else {
+            var fold = $(settings.container).offset().top;
+        }
+        return fold >= $(element).offset().top + settings.threshold + $(element).height();
+    };
+    $.leftofbegin = function(element, settings){
+        if(settings.container === undefined || settings.container === window){
+            var fold = $(window).scrollLeft();
+        } else {
+            var fold = $(settings.container).offset().left;
+        }
+        return fold >= $(element).offset().left + settings.threshold + $(element).width();
+    };
+    $.extend($.expr[':'], {
+        "below-the-fold": "$.belowthefold(a, {threshold : 0, container: window})",
+        "above-the-fold": "!$.belowthefold(a, {threshold : 0, container: window})",
+        "right-of-fold": "$.rightoffold(a, {threshold : 0, container: window})",
+        "left-of-fold": "!$.rightoffold(a, {threshold : 0, container: window})"
+    });
+})(jQuery);
+

运行效果

+

Js-懒加载-01

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/toy/js-love-heart/index.html b/blog-site/public/posts/toy/js-love-heart/index.html new file mode 100644 index 00000000..8f43e53f --- /dev/null +++ b/blog-site/public/posts/toy/js-love-heart/index.html @@ -0,0 +1,706 @@ + + + + + + + + + + + Js表白神器 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Js表白神器

+ 2018.10.14 +
+

index.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>love</title>
+    <style>
+        *{
+            margin: 0;
+            padding: 0;
+        }
+        body{
+            background-color: #000;
+            background-size: cover;
+            overflow-y: hidden;
+        }
+        .love{
+            width: 400px;
+            height: 400px;
+            /*background-color: #7c7c7c;*/
+            margin: 130px auto;
+            animation: move 1s infinite alternate;
+        }
+        @keyframes move {
+            100%{
+                transform: scale(1.5);
+            }
+        }
+        .left{
+            float: left;
+            width: 150px;
+            height: 250px;
+            background-color: #FF0000;
+            border-radius: 75px 75px 0 5px;
+            -webkit-transform: rotate(-45deg);
+            -moz-transform: rotate(-45deg);
+            -ms-transform: rotate(-45deg);
+            -o-transform: rotate(-45deg);
+            transform: rotate(-45deg);
+            margin-left: 85px;
+            box-shadow: 0 0 20px #FF0000;
+            animation: shadow 1s infinite alternate;
+        }
+        @keyframes shadow {
+            100%{
+                box-shadow: 0 0 100px #FF0000;
+            }
+        }
+        .right{
+            float: left;
+            width: 150px;
+            height: 250px;
+            background-color: #FF0000;
+            border-radius: 75px 75px 5px 0;
+            -webkit-transform: rotate(45deg);
+            -moz-transform: rotate(45deg);
+            -ms-transform: rotate(45deg);
+            -o-transform: rotate(45deg);
+            transform: rotate(45deg);
+            margin-left: -78px;
+            box-shadow: 0 0 10px #FF0000;
+            animation: shadow 1s infinite alternate;
+        }
+        p{
+            color: #FF0000;
+            box-shadow: 0 0 100px #FF0000;
+            text-align: center;
+            font-size: 80px;
+            margin-top: -110px;
+        }
+        .snowfall-flakes:before,.snowfall-flakes:after{
+            content:'';
+            position:absolute;/*绝对定位  参考物   一般是父元素*/
+            width:10px;
+            height:16px;
+            background-color:red;
+            border-radius:50px 50px 0 0;/*圆角   左上 右上 右下  左下*/
+            transform:rotate(-45deg);/*css3新增 transform变换 rotate旋转 */
+        }
+
+
+        /*在元素之后添加内容*/
+        .snowfall-flakes:after{
+            left:5px;
+            transform:rotate(45deg);/*css3新增 transform变换 rotate旋转 */
+        }
+    </style>
+</head>
+<body>
+
+<div class="love">
+    <div class="left"></div>
+    <div class="right"></div>
+</div>
+<script src="http://libs.baidu.com/jquery/2.1.4/jquery.min.js"></script>
+<script>
+    (function () {
+        var lastTime = 0;
+        var vendors = ['webkit', 'moz'];
+        for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+            window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
+            window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
+        }
+        if (!window.requestAnimationFrame)
+            window.requestAnimationFrame = function (callback, element) {
+                var currTime = new Date().getTime();
+                var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+                var id = window.setTimeout(function () {
+                    callback(currTime + timeToCall);
+                }, timeToCall);
+                lastTime = currTime + timeToCall;
+                return id;
+            };
+        if (!window.cancelAnimationFrame)
+            window.cancelAnimationFrame = function (id) {
+                clearTimeout(id);
+            };
+    }());
+    (function ($) {
+        $.snowfall = function (element, options) {
+            var defaults = {
+                flakeCount: 35,
+                flakeColor: 'transparent',
+                flakePosition: 'absolute',
+                flakeIndex: 999999,
+                minSize: 1,
+                maxSize: 2,
+                minSpeed: 1,
+                maxSpeed: 5,
+                round: false,
+                shadow: false,
+                collection: false,
+                collectionHeight: 40,
+                deviceorientation: false
+            }, options = $.extend(defaults, options), random = function random(min, max) {
+                return Math.round(min + Math.random() * (max - min));
+            };
+            $(element).data("snowfall", this);
+
+            function Flake(_x, _y, _size, _speed, _id) {
+                this.id = _id;
+                this.x = _x;
+                this.y = _y;
+                this.size = _size;
+                this.speed = _speed;
+                this.step = 0;
+                this.stepSize = random(1, 10) / 100;
+                if (options.collection) {
+                    this.target = canvasCollection[random(0, canvasCollection.length - 1)];
+                }
+                var flakeMarkup = null;
+                if (options.image) {
+                    flakeMarkup = $(document.createElement("div"));
+                    /*flakeMarkup[0].src=options.图片;*/
+                } else {
+                    flakeMarkup = $(document.createElement("div"));
+                    flakeMarkup.css({'background': options.flakeColor});
+                }
+                flakeMarkup.attr({'class': 'snowfall-flakes', 'id': 'flake-' + this.id}).css({
+                    'width': this.size,
+                    'height': this.size,
+                    'position': options.flakePosition,
+                    'top': this.y,
+                    'left': this.x,
+                    'fontSize': 0,
+                    'zIndex': options.flakeIndex
+                });
+                if ($(element).get(0).tagName === $(document).get(0).tagName) {
+                    $('body').append(flakeMarkup);
+                    element = $('body');
+                } else {
+                    $(element).append(flakeMarkup);
+                }
+                this.element = document.getElementById('flake-' + this.id);
+                this.update = function () {
+                    this.y += this.speed;
+                    if (this.y > (elHeight) - (this.size + 6)) {
+                        this.reset();
+                    }
+                    this.element.style.top = this.y + 'px';
+                    this.element.style.left = this.x + 'px';
+                    this.step += this.stepSize;
+                    if (doRatio === false) {
+                        this.x += Math.cos(this.step);
+                    } else {
+                        this.x += (doRatio + Math.cos(this.step));
+                    }
+                    if (options.collection) {
+                        if (this.x > this.target.x && this.x < this.target.width + this.target.x && this.y > this.target.y && this.y < this.target.height + this.target.y) {
+                            var ctx = this.target.element.getContext("2d"), curX = this.x - this.target.x,
+                                    curY = this.y - this.target.y, colData = this.target.colData;
+                            if (colData[parseInt(curX)][parseInt(curY + this.speed + this.size)] !== undefined || curY + this.speed + this.size > this.target.height) {
+                                if (curY + this.speed + this.size > this.target.height) {
+                                    while (curY + this.speed + this.size > this.target.height && this.speed > 0) {
+                                        this.speed *= .5;
+                                    }
+                                    ctx.fillStyle = "#fff";
+                                    if (colData[parseInt(curX)][parseInt(curY + this.speed + this.size)] == undefined) {
+                                        colData[parseInt(curX)][parseInt(curY + this.speed + this.size)] = 1;
+                                        ctx.fillRect(curX, (curY) + this.speed + this.size, this.size, this.size);
+                                    } else {
+                                        colData[parseInt(curX)][parseInt(curY + this.speed)] = 1;
+                                        ctx.fillRect(curX, curY + this.speed, this.size, this.size);
+                                    }
+                                    this.reset();
+                                } else {
+                                    this.speed = 1;
+                                    this.stepSize = 0;
+                                    if (parseInt(curX) + 1 < this.target.width && colData[parseInt(curX) + 1][parseInt(curY) + 1] == undefined) {
+                                        this.x++;
+                                    } else if (parseInt(curX) - 1 > 0 && colData[parseInt(curX) - 1][parseInt(curY) + 1] == undefined) {
+                                        this.x--;
+                                    } else {
+                                        ctx.fillStyle = "#fff";
+                                        ctx.fillRect(curX, curY, this.size, this.size);
+                                        colData[parseInt(curX)][parseInt(curY)] = 1;
+                                        this.reset();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                    if (this.x > (elWidth) - widthOffset || this.x < widthOffset) {
+                        this.reset();
+                    }
+                }
+                this.reset = function () {
+                    this.y = 0;
+                    this.x = random(widthOffset, elWidth - widthOffset);
+                    this.stepSize = random(1, 10) / 100;
+                    this.size = random((options.minSize * 100), (options.maxSize * 100)) / 100;
+                    this.speed = random(options.minSpeed, options.maxSpeed);
+                }
+            }//素材家园 - www.sucaijiayuan.com
+            var flakes = [], flakeId = 0, i = 0, elHeight = $(element).height(), elWidth = $(element).width(),
+                    widthOffset = 0, snowTimeout = 0;
+            if (options.collection !== false) {
+                var testElem = document.createElement('canvas');
+                if (!!(testElem.getContext && testElem.getContext('2d'))) {
+                    var canvasCollection = [], elements = $(options.collection),
+                            collectionHeight = options.collectionHeight;
+                    for (var i = 0; i < elements.length; i++) {
+                        var bounds = elements[i].getBoundingClientRect(), canvas = document.createElement('canvas'),
+                                collisionData = [];
+                        if (bounds.top - collectionHeight > 0) {
+                            document.body.appendChild(canvas);
+                            canvas.style.position = options.flakePosition;
+                            canvas.height = collectionHeight;
+                            canvas.width = bounds.width;
+                            canvas.style.left = bounds.left + 'px';
+                            canvas.style.top = bounds.top - collectionHeight + 'px';
+                            for (var w = 0; w < bounds.width; w++) {
+                                collisionData[w] = [];
+                            }
+                            canvasCollection.push({
+                                element: canvas,
+                                x: bounds.left,
+                                y: bounds.top - collectionHeight,
+                                width: bounds.width,
+                                height: collectionHeight,
+                                colData: collisionData
+                            });
+                        }
+                    }
+                } else {
+                    options.collection = false;
+                }
+            }
+            if ($(element).get(0).tagName === $(document).get(0).tagName) {
+                widthOffset = 25;
+            }
+            $(window).bind("resize", function () {
+                elHeight = $(element)[0].clientHeight;
+                elWidth = $(element)[0].offsetWidth;
+            });
+            for (i = 0; i < options.flakeCount; i += 1) {
+                flakeId = flakes.length;
+                flakes.push(new Flake(random(widthOffset, elWidth - widthOffset), random(0, elHeight), random((options.minSize * 100), (options.maxSize * 100)) / 100, random(options.minSpeed, options.maxSpeed), flakeId));
+            }
+            if (options.round) {
+                $('.snowfall-flakes').css({
+                    '-moz-border-radius': options.maxSize,
+                    '-webkit-border-radius': options.maxSize,
+                    'border-radius': options.maxSize
+                });
+            }
+            if (options.shadow) {
+                $('.snowfall-flakes').css({
+                    '-moz-box-shadow': '1px 1px 1px #555',
+                    '-webkit-box-shadow': '1px 1px 1px #555',
+                    'box-shadow': '1px 1px 1px #555'
+                });
+            }
+            var doRatio = false;
+            if (options.deviceorientation) {
+                $(window).bind('deviceorientation', function (event) {
+                    doRatio = event.originalEvent.gamma * 0.1;
+                });
+            }
+
+            function snow() {
+                for (i = 0; i < flakes.length; i += 1) {
+                    flakes[i].update();
+                }
+                snowTimeout = requestAnimationFrame(function () {
+                    snow()
+                });
+            }
+
+            snow();
+            this.clear = function () {
+                $(element).children('.snowfall-flakes').remove();
+                flakes = [];
+                cancelAnimationFrame(snowTimeout);
+            }
+        };
+        $.fn.snowfall = function (options) {
+            if (typeof(options) == "object" || options == undefined) {
+                return this.each(function (i) {
+                    (new $.snowfall(this, options));
+                });
+            } else if (typeof(options) == "string") {
+                return this.each(function (i) {
+                    var snow = $(this).data('snowfall');
+                    if (snow) {
+                        snow.clear();
+                    }
+                });
+            }
+        };
+    })(jQuery);
+</script>
+<script>
+    $(document).snowfall({
+        flakeCount:200//规定爱心的数量
+    })
+</script>
+</body>
+</html>
+

运行效果

+

Js表白神器-01

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/toy/js-paper-folding/index.html b/blog-site/public/posts/toy/js-paper-folding/index.html new file mode 100644 index 00000000..ffda6023 --- /dev/null +++ b/blog-site/public/posts/toy/js-paper-folding/index.html @@ -0,0 +1,521 @@ + + + + + + + + + + + Js折纸导航栏 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Js折纸导航栏

+ 2018.10.25 +
+

index.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>折纸导航栏</title>
+</head>
+<style>
+    *{
+        margin: 0;
+        padding: 0;
+    }
+    .content{
+        position: relative;
+        width: 400px;
+        height: 30px;
+        margin: 50px auto;
+        /*-webkit-perspective: 1000px;
+        -moz-perspective: 1000px;
+        -ms-perspective: 1000px;*/
+        perspective: 1000px;/*景深相当于眼睛距离元素的位置距离*/
+    }
+    .content .open{
+        transform: rotateX(0);
+        animation: open 1s linear;
+    }
+    @keyframes open {
+        0%{
+            transform: rotateX(-90deg);
+        }
+        20%{
+            transform:rotateX(30deg);
+        }
+        40%{
+            transform:rotateX(-60deg);
+        }
+        60%{
+            transform:rotateX(60deg);
+        }
+        80%{
+            transform:rotateX(-30deg);
+        }
+        100%{
+            transform:rotateX(0);
+        }
+    }
+    .content .close{
+        transform: rotateX(-120deg);
+        animation: close 1s ease;
+    }
+    @keyframes close {
+        0%{
+            transform: rotateX(0);
+        }
+        100%{
+            transform: rotateX(-90deg);
+        }
+    }
+    .content div{
+        position: absolute;
+        left: 0;
+        top: 30px;
+        width: 100%;
+        transform-style: preserve-3d;
+        transform-origin: top;
+        transform: rotateX(-90deg);
+    }
+    .content div span{
+        display: block;
+        height: 28px;
+        line-height: 30px;
+        text-align: center;
+        background: rgb(153,102,102);
+    }
+    input{
+        position: absolute;
+        left: 0;
+        top: 0;
+        z-index: 999;
+        width: 400px;
+        height: 30px;
+        border: 0;
+        background-color: #c74;
+    }
+</style>
+<body>
+<div class="content">
+    <input type="button" value="打开" id="btn"/>
+    <div>
+        <span>导航1</span>
+        <div>
+            <span>导航2</span>
+            <div>
+                <span>导航3</span>
+                <div>
+                    <span>导航4</span>
+                    <div>
+                        <span>导航5</span>
+                        <div>
+                            <span>导航6</span>
+                            <div>
+                                <span>导航7</span>
+                                <div>
+                                    <span>导航8</span>
+                                    <div>
+                                        <span>导航9</span>
+                                    </div>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<script>
+    var oBtn = document.getElementById('btn');
+    var oCon = document.getElementsByClassName('content')[0];
+    var oDiv = oCon.getElementsByTagName('div');
+    var time = null;
+    var i = 0;
+    var mark = true;
+    oBtn.onclick = function () {
+        if (mark){
+            i = 0;
+            timer=setInterval( function () {
+                if (i == oDiv.length -1)
+                {
+                    clearInterval(timer);
+                }
+                oDiv[i].className = 'open';
+                i++;
+            },200)
+            oBtn.value = '关闭';
+        }
+        else
+        {
+            i = oDiv.length -1;
+            timer=setInterval( function () {
+                if (i == 0)
+                {
+                    clearInterval(timer);
+                }
+                oDiv[i].className = 'close';
+                i--;
+            },100)
+            oBtn.value = '打开';
+        }
+        mark = !mark;
+    }
+</script>
+</body>
+</html>
+

运行效果

+

Js折纸导航栏

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/toy/js-rain/index.html b/blog-site/public/posts/toy/js-rain/index.html new file mode 100644 index 00000000..19d9c9f9 --- /dev/null +++ b/blog-site/public/posts/toy/js-rain/index.html @@ -0,0 +1,518 @@ + + + + + + + + + + + Js下雨特效 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Js下雨特效

+ 2018.12.10 +
+

index.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>
+        rain
+    </title>
+
+    <style>
+        html {
+            width: 100%;
+        }
+
+        body {
+            width: 100%;
+            margin: 0;
+            padding: 0;
+            background-color: #000;
+        }
+
+        .rain {
+            display: block;
+        }
+
+        embed {
+            display: block;
+        }
+    </style>
+</head>
+<body>
+<!--
+  2、使用hidden="true"表示隐藏音乐播放按钮,相反使用hidden="false"表示开启音乐播放按钮。
+  3、使用autostart="true" 表示是打开网页加载完后自动播放。
+  4、使用loop="true"表示 循环播放 如仅想播放一次则为:loop="false"
+  -->
+<embed src="music/恋如雨止.mp3" hidden="true" autostart="true" loop="false">
+
+<canvas class="rain"></canvas>
+<!--
+canvas雨滴逻辑思路:
+1  画什么 -- 雨滴
+   雨滴 是由许多个小矩形拼接成的 加上 遮盖层 让每个矩形透明度依次递减 就能看到此效果
+   首先画一个 然后再去复制这个雨滴
+2  怎么画让雨滴动起来
+    定义随机数让雨滴的 起始位置 长 宽 下落速度 绽放速度 不同
+    然后开启定时器让雨滴动起来
+3 需要什么东西
+    需要画笔: 实心画笔 空心画笔
+    创建雨滴的模板
+    盛放雨滴的容器
+-->
+<script>
+    var oCanvas = document.querySelector(".rain");//获取元素rain
+    var w, h;//定义变量w ,h
+    var aRain = [];//用来存放新生成的雨滴
+
+    ~~function () {//自执行函数:不用调用自己执行 把canvas和整个屏幕无缝贴合
+        window.onresize = arguments.callee;//随着浏览器的变化宽和高都变化
+        w = window.innerWidth;//获取浏览器的宽
+        h = window.innerHeight;//获取浏览器的高
+        oCanvas.width = w;//把值赋给canvas
+        oCanvas.height = h;//把值赋给canvas
+    }();
+
+    function random(min, max) {//定义随机函数
+        return Math.random() * (max - min) + min;//返回想要的任意的值
+    }
+
+    var canCon = oCanvas.getContext("2d");//创建一个2d画笔
+
+    Rain.prototype = {//this 指代当前函数
+        init: function () {//雨滴的基本参数
+            this.x = random(0, w);//雨滴横坐标
+            this.y = 0;//纵坐标
+            this.w = random(1.6, 3);//雨滴的 宽度 1.6px 到 2.5px
+            this.h = random(8, 15);//雨滴的 长度8px 到 15px
+            this.color = "#3ff";//颜色
+            this.vy = random(1.2, 2.3);//下降速度
+            this.vr = random(0.5, 1.5);//绽放速度
+            this.ground = random(0.8 * h, 0.9 * h);//雨滴绽放的位置
+            this.r = this.w / 2;//圆的半径
+        },
+        draw: function () {//把基本参数变化成雨滴效果
+            if (this.y < this.ground)//如果雨滴y坐标小于绽放位置,就一直下落
+            {
+                canCon.beginPath();//抬笔
+                canCon.fillStyle = this.color;//用画笔蘸颜色画雨滴
+                canCon.fillRect(this.x, this.y, this.w, this.h);//画矩形区域左上角的坐标 矩形的宽 高
+            } else//绽放的圆形区域
+            {
+                canCon.beginPath();//抬笔 雨滴停止下落
+                canCon.strokeStyle = "rgba(50,250,250,0.96)";//空心画笔 rgba() rgb是颜色  a是透明度
+                canCon.arc(this.x, this.y, this.r, 0, Math.PI * 2);//画圆形 圆心坐标 半径 画多少(弧度制)
+                canCon.stroke();//下笔作画
+            }
+
+        },
+        move: function () {//移动函数:设置怎么移动
+
+            if (this.y < this.ground)//如果雨滴的y坐标小于定义的地面高度  则雨滴往下移动
+            {
+                this.y += this.vy;//让每个雨滴下降速度不同
+            } else//如果大于  就绽放
+            {
+                if (this.r < 100)//绽放半径
+                {
+                    this.r += this.vr;//让每个雨滴绽放速度不同
+                } else {
+                    this.init();//循环雨滴:重新设置雨滴,重头再来 循环
+                }
+
+            }
+
+            this.draw();//调用draw函数
+        }
+    };
+
+
+    function Rain() {
+    }//雨滴的模板
+    //创建num个雨滴
+    function createRain(num) {//参数为创建多少雨滴
+        for (let i = 0; i < num; i++)//let 为立执行
+        {
+            setTimeout(function () {//创建num个雨滴的定时器
+                var rain = new Rain();//创建雨滴对象 创建什么样的雨滴 : 调用init函数 draw函数
+                rain.init();
+                rain.draw();
+                aRain.push(rain);//把雨滴放在aRain里边, 目的是 循环他
+            }, 200 * i);//时间200*i目的是为了让每个雨滴下落时间不同  单位为毫秒
+
+        }
+    }
+
+
+    setInterval(function () {//下落运动的定时器
+        canCon.fillStyle = "rgba(0,0,0,0.04)";//遮盖层,透明度递减,让画的矩形看起来更像雨滴
+        canCon.fillRect(0, 0, w, h);//画矩形
+        for (var item of aRain)//遍历num个雨滴
+        {
+            item.move();//把遍历后的雨滴都执行move函数 运动起来
+        }
+    }, 1000 / 90);//单位为毫秒
+
+
+    createRain(70);//方法入口  下多少雨滴
+
+</script>
+</body>
+</html>
+

运行效果

+

Js下雨特效-01.png

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/toy/js-snow/index.html b/blog-site/public/posts/toy/js-snow/index.html new file mode 100644 index 00000000..4e11fd24 --- /dev/null +++ b/blog-site/public/posts/toy/js-snow/index.html @@ -0,0 +1,611 @@ + + + + + + + + + + + Js雪花飘落 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Js雪花飘落

+ 2018.12.25 +
+

index.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>snow</title>
+</head>
+<style>
+    html {
+        width: 100%;
+    }
+
+    body {
+        margin: 0;
+        padding: 0;
+        overflow-y: hidden;
+        width: 100%;
+    }
+
+    .header {
+        width: 100%;
+        height: 315px;
+        background: url("images/header-bg.png") repeat;
+
+    }
+
+    .snow {
+        position: relative;
+        height: inherit;
+        width: 960px;
+        background: url("images/con-bg.png") no-repeat 0 204px,
+        url("images/snow-bg.png") no-repeat 0 0;;
+        margin: 0 auto;
+        animation: auto 10s linear infinite;
+    }
+
+    /* 下雪动画
+
+    插入两个背景图片*/
+    @keyframes auto {
+        from {
+            background: url("images/con-bg.png") no-repeat 0 204px,
+            url("images/snow-bg.png") repeat 0 0;
+        }
+        to {
+            background: url("images/con-bg.png") no-repeat 0 204px,
+            url("images/snow-bg.png") repeat 0 1000px;
+        }
+    }
+
+    tree, snow {
+        position: absolute;
+    }
+
+    tree {
+        width: 112px;
+        height: 137px;
+        background: url("images/tree.png");
+    }
+
+    snow {
+        left: 410px;
+        top: 210px;
+        width: 115px;
+        height: 103px;
+        background: url("images/ice.png");
+        animation: play 3s;
+    }
+
+    @keyframes play {
+        from {
+            transform: translate(0, -500px);
+        }
+        to {
+            transform: translate(0, 0);
+        }
+    }
+
+    tree:nth-child(1) {
+        left: 35px;
+        top: 169px;
+        animation: play 1s;
+    }
+
+    tree:nth-child(2) {
+        left: 200px;
+        top: 180px;
+        animation: play 1.9s;
+    }
+
+    tree:nth-child(3) {
+        left: 350px;
+        top: 125px;
+        animation: play 2.2s;
+    }
+
+    tree:nth-child(4) {
+        left: 515px;
+        top: 150px;
+        animation: play 1s;
+    }
+
+    tree:nth-child(5) {
+        left: 680px;
+        top: 170px;
+        animation: play 2s;
+    }
+
+    tree:nth-child(6) {
+        left: 805px;
+        top: 125px;
+        animation: play 1.7s;
+    }
+
+    /* 文字部分  start*/
+    .content {
+        position: relative;
+        width: inherit;
+        opacity: 0.7;
+        background-attachment:fixed;
+        background: url("images/snow.jpg") no-repeat center/cover;
+        height: 650px;
+        background-color: #ffffff;
+        padding-top: 60px;
+    }
+
+    .content .text {
+        padding-left: 250px;
+    }
+
+    .content .text p {
+        margin: 0;
+        font-size: 20px;
+        font-family: "微软雅黑 Light"; /* 这里可以改文字的字体*/
+        font-weight: bold;
+        color: #000000;
+    }
+
+    .content .box {
+        position: absolute;
+        top: 0;
+        width: inherit;
+        height: 500px;
+        float: left;
+        background-color: #ffffff;
+    }
+
+
+</style>
+<body>
+<div class="header">
+    <div class="snow">
+        <!-- 自定义标签 <tree> -->
+        <tree></tree>
+        <tree></tree>
+        <tree></tree>
+        <tree></tree>
+        <tree></tree>
+        <tree></tree>
+        <snow></snow>
+    </div>
+</div>
+<div class="content">
+    <div class="box"></div>
+    <div class="text">
+        <h1 style="color: red ">这里写主题哦!</h1>
+        <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+        <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+        <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+        <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+        <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+        <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+        <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+        <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+        <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+        <p>文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!文字实例!</p>
+
+    </div>
+
+</div>
+
+<script>
+    /**
+     * 如何使文字实现渐变出现效果
+     *
+     */
+    var oContent = document.getElementsByClassName("content")[0],
+            oText = document.getElementsByClassName("text")[0],
+            oMove = document.getElementsByClassName("box")[0],
+            oP = document.getElementsByTagName("p")
+    ;
+
+    (function startMove() {
+        var timer = null;
+        clearInterval(timer);
+        timer = setInterval(function () {
+            var speed = 5;
+            if (oMove.offsetTop >= oContent.offsetHeight) {
+                clearInterval(timer);
+            }
+            else {
+                oMove.style.top = oMove.offsetTop + speed + 'px';
+
+            }
+        }, 30);
+
+    })()
+
+
+</script>
+
+</body>
+</html>
+

图片

+

Js雪花飘落

+

Js雪花飘落

+

Js雪花飘落

+

Js雪花飘落

+

Js雪花飘落

+

Js雪花飘落

+

运行效果

+

Js雪花飘落

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/toy/js-trans-skin/index.html b/blog-site/public/posts/toy/js-trans-skin/index.html new file mode 100644 index 00000000..298ae369 --- /dev/null +++ b/blog-site/public/posts/toy/js-trans-skin/index.html @@ -0,0 +1,455 @@ + + + + + + + + + + + Js换肤特效 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

Js换肤特效

+ 2018.11.14 +
+

index.html

+
<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <title>换肤特效</title>
+    <style type="text/css">
+        body {
+            margin: 0;
+            background-image: url("images/1.jpg");
+            background-size: cover;
+        }
+
+        ul {
+            margin: 0;
+            padding: 0;
+            list-style-type: none;
+        }
+
+        .bg-list {
+            display: none;
+            margin: 0;
+            width: 100%;
+            height: 200px;
+            background: rgba(0, 0, 0, 0.5);
+        }
+
+        .img-wrap {
+            height: 200px;
+            display: flex;
+            justify-content: space-around;
+            align-items: center;
+        }
+
+        .tab-btn {
+            background-image: url("images/upseek.png");
+            height: 50px;
+            width: 50px;
+            position: fixed;
+            top: 0;
+            right: 0;
+        }
+
+        .tab-btn:hover {
+            background-position-y: -63.6px;
+        }
+    </style>
+
+
+</head>
+<body>
+<div class="bg-list">
+    <ul class="img-wrap">
+        <li class="img-item" data-src="images/1.jpg">
+            <img src="images/1-1.jpg" width="160px"/>
+        </li>
+        <li class="img-item" data-src="images/2.jpg">
+            <img src="images/2-2.jpg" width="160px"/>
+        </li>
+        <li class="img-item" data-src="images/3.jpg">
+            <img src="images/3-3.jpg" width="160px"/>
+        </li>
+        <li class="img-item" data-src="images/4.jpg">
+            <img src="images/4-4.jpg" width="160px"/>
+        </li>
+        <li class="img-item" data-src="images/5.jpg">
+            <img src="images/5-5.jpg" width="160px"/>
+        </li>
+        <li class="img-item" data-src="images/6.jpg">
+            <img src="images/6-6.jpg" width="160px"/>
+        </li>
+    </ul>
+</div>
+<div class="tab-btn"></div>
+<script src="js\jquery.js" type="text/javascript"></script>
+<script type="text/javascript">
+    $(".tab-btn").click(function () {
+        $(".bg-list").slideToggle();
+    });
+    $(".img-item").click(function () {
+        var src = $(this).attr("data-src");
+        $("body").css({
+            "background-image": "url(" + src + ")"
+        })
+    });
+</script>
+</body>
+</html>
+

运行效果

+

Js-换肤特效-01

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/worksummary/work-summary-2019/index.html b/blog-site/public/posts/worksummary/work-summary-2019/index.html new file mode 100644 index 00000000..882b7f25 --- /dev/null +++ b/blog-site/public/posts/worksummary/work-summary-2019/index.html @@ -0,0 +1,318 @@ + + + + + + + + + + + 2019工作总结 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

2019工作总结

+ 2019.12.01 +
+

本人在进入公司起,期间一直对自己要求严谨,遵守公司的相应制度. 在过去的一个月时间里,我参与了贵州银行的电子验印系统的开发,一直努力完成和完善分配给我的任务,在这一个月发现了自身还有很多的不足,所以抱着虚心学习的态度,学习公司的开发流程,了解公司的产品架构,主要技术,主动和同事沟通,学习经验,希望能快速融入公司,能够全心的投入工作.

+

试用期完成的工作有限,主要负责验印系统的统一门开发,学习了一些新技术,因为自己在经验上不足,对于技术的学习和掌握还不够深入,发现问题的能力还不够,所以拖慢了自己的开发进度,简单列一些. 通过开发的过程中,学习并掌握了vue框架的使用,学习到了Oracle数据库的使用….. 使我认识到了一个称职的开发人员应当具备良好的语言表达能力,较强的逻辑能力,灵活的处理应变能力,有效的对外联系能力. 在参与项目的开发过程中,发现很多看似简单的工作,其实里面有很多技巧

+

今后,我会多注意在这些方面的学习和积累,努力做好开发人员的本职工作,注重工作态度,把自己的工作做好做扎实,为项目的开发及公司的发展贡献自己的一份力量.在工作的这段时间里.我得到了同事的帮助,经常与我交流,指出技术上的问题,传授了很多开发经验,在生活上也给与快了我很大的帮助,使得我很快就适应了这里的生活.

+

整个工作学习过程中,我认为自己工作比较认真负责.具有较强的责任心和进取心,能完成领导交付的工作,但也存在着许多缺点与不足,对工作的专业性还不够,业务经验不够丰富,对于发现问题的处理还不是很全面,我会在以后的工作中不断实践和总结,并积极学习新知识,弥补自身不足,来提高自己的综合素质. 总之,认真的回顾了这段时间的工作,发现了一些不足之处,这都是我在接下的工作中需要完善的.同时,也会尽最大努力的学习和积累经验,逐步发展成一个全面的技术开发人员,更好的完成工作.

+

以上是我对2019年的工作总结及2020年工作计划,可能还不是很成熟,希望领导指正.展望2020年,我会更加努力,认真负责的去对待工作,相信自己会完成新的任务,能迎接新的挑战。为公司的发展做出自己的贡献.

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/posts/worksummary/work-summary-2023/index.html b/blog-site/public/posts/worksummary/work-summary-2023/index.html new file mode 100644 index 00000000..cbdaa630 --- /dev/null +++ b/blog-site/public/posts/worksummary/work-summary-2023/index.html @@ -0,0 +1,323 @@ + + + + + + + + + + + 2023工作总结 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+
+
+

2023工作总结

+ 2023.12.01 +
+

在职期间,我主要负责耐材项目的开发与维护,共迭代171个版本。通过与团队成员的紧密合作,我们按时完成了项目中的需求。在这段时间里,我不断提升自己的专业技能和知识,增强了自己的专业能力。我始终认为团队合作是成功的关键。在工作中,我积极与同事沟通交流,共同解决问题,促进了团队的凝聚力。

+

在过去,我发现自己在与部分同事沟通时存在障碍。为了解决这一问题,我主动与他们进行深入交流,明确工作目标和期望,并加强了团队间的协作和沟通。

+

由于工作任务的繁重,我曾面临较大的工作压力。为了释放压力,我学会了合理安排时间,优化工作流程,并积极参与团队活动,放松身心。

+

我认为我始终保持积极的工作态度,认真对待每一个任务,尽最大努力去完成。认真履行工作职责,完成了各项工作任务,取得了一定的成绩。具体如下:

+
    +
  1. 积极参与团队建设,与同事们共同协作,提高了整体的工作效率。
  2. +
  3. 在开发过程中使用一些设计设计思想,使其扩展性、可重用性大大增强,提高了开发效率,降低了维护成本。
  4. +
  5. 及时的处理客户反馈的问题,提高了客户满意度。
  6. +
+

回顾过去一年,我认为自己在工作中展现出了较强的责任心和团队精神。但在时间管理和应对突发事件方面,仍有待提高。

+
+ +
+ + + +
+
发表评论
+
+
+ + + + + + + + +
+
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/sitemap.xml b/blog-site/public/sitemap.xml new file mode 100644 index 00000000..dd573ab8 --- /dev/null +++ b/blog-site/public/sitemap.xml @@ -0,0 +1,16 @@ + + + + + http://localhost:1313/iblog/zh/sitemap.xml + + 2023-12-01T00:00:00+00:00 + + + + + http://localhost:1313/iblog/en/sitemap.xml + + + + diff --git a/blog-site/public/tags/docker/index.html b/blog-site/public/tags/docker/index.html new file mode 100644 index 00000000..2d425b8a --- /dev/null +++ b/blog-site/public/tags/docker/index.html @@ -0,0 +1,203 @@ + + + + + + + + + + + Docker | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2020
+
+ +
+
+ SpringBoot整合docker +
08-30
+
+
+ +
+
+ Docker介绍 +
04-07
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/tags/docker/index.xml b/blog-site/public/tags/docker/index.xml new file mode 100644 index 00000000..11f15157 --- /dev/null +++ b/blog-site/public/tags/docker/index.xml @@ -0,0 +1,26 @@ + + + + Docker on 唯手熟尔 + http://localhost:1313/iblog/tags/docker/ + Recent content in Docker on 唯手熟尔 + Hugo -- gohugo.io + zh + Sun, 30 Aug 2020 00:00:00 +0000 + + + SpringBoot整合docker + http://localhost:1313/iblog/posts/spring/springboot-docker/ + Sun, 30 Aug 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-docker/ + MacOS上安装docker 下载 国内下载网站: http://get.daocloud.io 不推荐下载docker版本太旧了 官网下载: https://docs.docker.com/get-started/#download-and-install-docker 或用homebrew进行下载安装 brew install --cask --appdir=/Applications docker 配置镜像 由于网速原因,可以配置一下国内的镜像加速器 中科大镜像: https://docker.mirrors.ustc.edu.cn 网易: https://hub-mirror.c.163.com 阿里云: https://&lt;你 + + + Docker介绍 + http://localhost:1313/iblog/posts/essays/docker-start/ + Tue, 07 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/docker-start/ + docker是什么 Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前流行的 Linux 容器解决方案。 Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样 + + + diff --git a/blog-site/public/tags/elasticsearch/index.html b/blog-site/public/tags/elasticsearch/index.html new file mode 100644 index 00000000..46cc53d4 --- /dev/null +++ b/blog-site/public/tags/elasticsearch/index.html @@ -0,0 +1,208 @@ + + + + + + + + + + + Elasticsearch | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2023
+
+ +
+
+ Elasticsearch详解 +
02-14
+
+
+ +
+ +
2020
+
+ + + +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/tags/elasticsearch/index.xml b/blog-site/public/tags/elasticsearch/index.xml new file mode 100644 index 00000000..ab32199b --- /dev/null +++ b/blog-site/public/tags/elasticsearch/index.xml @@ -0,0 +1,26 @@ + + + + Elasticsearch on 唯手熟尔 + http://localhost:1313/iblog/tags/elasticsearch/ + Recent content in Elasticsearch on 唯手熟尔 + Hugo -- gohugo.io + zh + Tue, 14 Feb 2023 00:00:00 +0000 + + + Elasticsearch详解 + http://localhost:1313/iblog/posts/essays/elasticsearch/ + Tue, 14 Feb 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/elasticsearch/ + 概览 Elasticsearch,简称为 ES, ES是一个开源的高扩展的分布式全文搜索引擎, 是整个 ElasticStack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。 Elastic Stack, 包括 Elasticsearch、 Ki + + + SpringBoot整合elasticsearch + http://localhost:1313/iblog/posts/spring/springboot-elasticsearch/ + Sun, 09 Feb 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-elasticsearch/ + 安装elasticsearch 要注意导入依赖的版本和安装elasticsearch的版本与springboot的兼容问题 用 docker 安装 elasticsearch 本例用elasticsearch-6.5.3和springboot-2.1.0.RELEASE版本 下载镜像: docker + + + diff --git a/blog-site/public/tags/index.html b/blog-site/public/tags/index.html new file mode 100644 index 00000000..8c824b01 --- /dev/null +++ b/blog-site/public/tags/index.html @@ -0,0 +1,427 @@ + + + + + + + + + + + Tags | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/tags/index.xml b/blog-site/public/tags/index.xml new file mode 100644 index 00000000..8bd22aeb --- /dev/null +++ b/blog-site/public/tags/index.xml @@ -0,0 +1,222 @@ + + + + Tags on 唯手熟尔 + http://localhost:1313/iblog/tags/ + Recent content in Tags on 唯手熟尔 + Hugo -- gohugo.io + zh + Fri, 01 Dec 2023 00:00:00 +0000 + + + 工作总结 + http://localhost:1313/iblog/tags/%E5%B7%A5%E4%BD%9C%E6%80%BB%E7%BB%93/ + Fri, 01 Dec 2023 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E5%B7%A5%E4%BD%9C%E6%80%BB%E7%BB%93/ + + + + 简历 + http://localhost:1313/iblog/tags/%E7%AE%80%E5%8E%86/ + Fri, 15 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E7%AE%80%E5%8E%86/ + + + + 求职 + http://localhost:1313/iblog/tags/%E6%B1%82%E8%81%8C/ + Fri, 15 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E6%B1%82%E8%81%8C/ + + + + 设计 + http://localhost:1313/iblog/tags/%E8%AE%BE%E8%AE%A1/ + Sat, 09 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E8%AE%BE%E8%AE%A1/ + + + + 应用 + http://localhost:1313/iblog/tags/%E5%BA%94%E7%94%A8/ + Sat, 09 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E5%BA%94%E7%94%A8/ + + + + Nacos + http://localhost:1313/iblog/tags/nacos/ + Mon, 04 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/tags/nacos/ + + + + Springboot + http://localhost:1313/iblog/tags/springboot/ + Mon, 04 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/tags/springboot/ + + + + Java + http://localhost:1313/iblog/tags/java/ + Fri, 14 Apr 2023 00:00:00 +0000 + http://localhost:1313/iblog/tags/java/ + + + + Spring + http://localhost:1313/iblog/tags/spring/ + Fri, 14 Apr 2023 00:00:00 +0000 + http://localhost:1313/iblog/tags/spring/ + + + + MySQL + http://localhost:1313/iblog/tags/mysql/ + Mon, 13 Mar 2023 00:00:00 +0000 + http://localhost:1313/iblog/tags/mysql/ + + + + 技巧 + http://localhost:1313/iblog/tags/%E6%8A%80%E5%B7%A7/ + Fri, 10 Mar 2023 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E6%8A%80%E5%B7%A7/ + + + + Elasticsearch + http://localhost:1313/iblog/tags/elasticsearch/ + Tue, 14 Feb 2023 00:00:00 +0000 + http://localhost:1313/iblog/tags/elasticsearch/ + + + + 书籍 + http://localhost:1313/iblog/tags/%E4%B9%A6%E7%B1%8D/ + Mon, 19 Dec 2022 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E4%B9%A6%E7%B1%8D/ + + + + 玩具 + http://localhost:1313/iblog/tags/%E7%8E%A9%E5%85%B7/ + Sat, 09 Apr 2022 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E7%8E%A9%E5%85%B7/ + + + + 算法 + http://localhost:1313/iblog/tags/%E7%AE%97%E6%B3%95/ + Fri, 10 Dec 2021 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E7%AE%97%E6%B3%95/ + + + + 网络 + http://localhost:1313/iblog/tags/%E7%BD%91%E7%BB%9C/ + Fri, 19 Nov 2021 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E7%BD%91%E7%BB%9C/ + + + + MQ + http://localhost:1313/iblog/tags/mq/ + Tue, 19 Oct 2021 00:00:00 +0000 + http://localhost:1313/iblog/tags/mq/ + + + + Java基础 + http://localhost:1313/iblog/tags/java%E5%9F%BA%E7%A1%80/ + Mon, 04 Oct 2021 00:00:00 +0000 + http://localhost:1313/iblog/tags/java%E5%9F%BA%E7%A1%80/ + + + + 分布式 + http://localhost:1313/iblog/tags/%E5%88%86%E5%B8%83%E5%BC%8F/ + Mon, 02 Aug 2021 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E5%88%86%E5%B8%83%E5%BC%8F/ + + + + 事务 + http://localhost:1313/iblog/tags/%E4%BA%8B%E5%8A%A1/ + Mon, 02 Aug 2021 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E4%BA%8B%E5%8A%A1/ + + + + Redis + http://localhost:1313/iblog/tags/redis/ + Thu, 17 Jun 2021 00:00:00 +0000 + http://localhost:1313/iblog/tags/redis/ + + + + JVM + http://localhost:1313/iblog/tags/jvm/ + Thu, 06 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/tags/jvm/ + + + + 集合 + http://localhost:1313/iblog/tags/%E9%9B%86%E5%90%88/ + Mon, 03 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E9%9B%86%E5%90%88/ + + + + 转载 + http://localhost:1313/iblog/tags/%E8%BD%AC%E8%BD%BD/ + Sat, 10 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E8%BD%AC%E8%BD%BD/ + + + + 使用介绍 + http://localhost:1313/iblog/tags/%E4%BD%BF%E7%94%A8%E4%BB%8B%E7%BB%8D/ + Fri, 05 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E4%BD%BF%E7%94%A8%E4%BB%8B%E7%BB%8D/ + + + + Nginx + http://localhost:1313/iblog/tags/nginx/ + Thu, 04 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/tags/nginx/ + + + + Docker + http://localhost:1313/iblog/tags/docker/ + Sun, 30 Aug 2020 00:00:00 +0000 + http://localhost:1313/iblog/tags/docker/ + + + + 线程 + http://localhost:1313/iblog/tags/%E7%BA%BF%E7%A8%8B/ + Mon, 20 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/tags/%E7%BA%BF%E7%A8%8B/ + + + + Vue + http://localhost:1313/iblog/tags/vue/ + Thu, 23 May 2019 00:00:00 +0000 + http://localhost:1313/iblog/tags/vue/ + + + + Js + http://localhost:1313/iblog/tags/js/ + Tue, 25 Dec 2018 00:00:00 +0000 + http://localhost:1313/iblog/tags/js/ + + + + diff --git a/blog-site/public/tags/java/index.html b/blog-site/public/tags/java/index.html new file mode 100644 index 00000000..6dc81444 --- /dev/null +++ b/blog-site/public/tags/java/index.html @@ -0,0 +1,449 @@ + + + + + + + + + + + Java | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2023
+
+ +
+
+ SpringMVC与SpringWebFlux +
04-14
+
+
+ +
+
+ 编程常用词汇汇总 +
02-13
+
+
+ +
+ +
2022
+
+ + + +
+
+ Java小程序集合 +
04-09
+
+
+ +
+ +
2021
+
+ +
+
+ 数据结构与算法 +
12-10
+
+
+ +
+
+ 网络编程 +
11-19
+
+
+ +
+
+ Java集合 +
10-04
+
+
+ +
+
+ Java反射 +
10-02
+
+
+ +
+
+ Object类方法 +
07-10
+
+
+ +
+
+ Spring详解 +
05-13
+
+
+ +
+
+ JVM-垃圾回收器 +
05-06
+
+
+ +
+
+ Java多线程 +
05-05
+
+
+ +
+
+ HashMap详解 +
05-03
+
+
+ +
+
+ JVM-相关概念 +
04-27
+
+
+ +
+
+ JVM-垃圾回收 +
04-21
+
+
+ +
+
+ JVM-执行引擎 +
04-15
+
+
+ +
+
+ JVM-直接内存 +
04-14
+
+
+ +
+
+ JVM-Java对象 +
04-12
+
+
+ +
+
+ Java语法糖 +
04-10
+
+
+ +
+
+ JavaIO +
04-09
+
+
+ +
+
+ JVM-方法区 +
04-08
+
+
+ +
+
+ JVM-堆 +
04-03
+
+
+ +
+
+ JVM-本地方法栈 +
04-02
+
+
+ +
+
+ JVM-本地方法接口 +
04-02
+
+
+ +
+
+ JVM-虚拟机栈 +
03-28
+
+
+ +
+
+ JVM-程序计数寄存器 +
03-27
+
+
+ +
+
+ JVM-JVM介绍 +
03-05
+
+
+ +
+
+ 面向对象 +
02-15
+
+
+ +
+
+ JVM-Java类加载机制 +
02-05
+
+
+ +
+
+ Java运算 +
01-30
+
+
+ +
+
+ Java数据类型 +
01-20
+
+
+ +
+
+ Java异常 +
01-13
+
+
+ +
+ +
2020
+
+ +
+
+ 线程状态及创建方式 +
04-20
+
+
+ +
+
+ Java中常用到的锁 +
04-07
+
+
+ + + +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/tags/java/index.xml b/blog-site/public/tags/java/index.xml new file mode 100644 index 00000000..da380250 --- /dev/null +++ b/blog-site/public/tags/java/index.xml @@ -0,0 +1,257 @@ + + + + Java on 唯手熟尔 + http://localhost:1313/iblog/tags/java/ + Recent content in Java on 唯手熟尔 + Hugo -- gohugo.io + zh + Fri, 14 Apr 2023 00:00:00 +0000 + + + SpringMVC与SpringWebFlux + http://localhost:1313/iblog/posts/spring/java-spring-mvc-webflux/ + Fri, 14 Apr 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/java-spring-mvc-webflux/ + Spring MVC Spring Web MVC是建立在Servlet API上的原始Web框架,从一开始就包含在Spring框架中。正式名称 &ldquo;Spring Web MVC &ldquo;来自其源模块的名称(spring-webmvc),但它更常被称为 &ldquo;Spring MVC&rdquo;。 SpringMVC是基于S + + + 编程常用词汇汇总 + http://localhost:1313/iblog/posts/essays/java-dict/ + Mon, 13 Feb 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-dict/ + QPS 即 Queries Per Second的缩写,每秒能处理查询数目。是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。 TPS 即 Transactions Per Second的缩写,每秒处理的事务数目。一个事务是指一个客户机向服务器发送请求然后服务 + + + 如何做好程序设计功能 + http://localhost:1313/iblog/posts/essays/java-design/ + Tue, 02 Aug 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-design/ + 产品需求澄清、PN排期及任务分解 开发设计评审 功能设计流程图 与外部系统交互、本系统模块之间流程,比较好用的画圈软件draw .io或在线的process on 数据库设计 从DDD角度界限上下文、ER图、评审表结构设计是否合理,表的关联关系是否合理、是 + + + Java小程序集合 + http://localhost:1313/iblog/posts/toy/java-multi-gadget/ + Sat, 09 Apr 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/java-multi-gadget/ + 写在前面 本文中所涉及的程序均为Java开发,如果您想要直接使用这些工具需要提前配置Java环境。所涉及到的程序均提供完整代码,如果您有兴趣可以尝试运行。 使用java -jar命令启动 某些程序功能并不是很完善,但是也可以凑合着用,写这些程序的主 + + + 数据结构与算法 + http://localhost:1313/iblog/posts/essays/data-structures-algorithms/ + Fri, 10 Dec 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/data-structures-algorithms/ + 数据结构 数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好数据结构可以编写出更有效率的代码。数据结构是算法的基础,想要学好算法,就必须把数据结构学到位。 数据结构包括:线性结构、非线性结构。 线性结构作为最常用的数据结构, + + + 网络编程 + http://localhost:1313/iblog/posts/essays/net-program-java/ + Fri, 19 Nov 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/net-program-java/ + 网络协议 以下内容摘自百度百科: https://baike.baidu.com/item/网络协议/328636 https://baike.baidu.com/item/网络七层协议/6056879 网络协议指的是计算机网络中互相通信的对等实体 + + + Java集合 + http://localhost:1313/iblog/posts/java/rookie-java-container/ + Mon, 04 Oct 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-java-container/ + 概述 Java中的集合主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 如果你看过ArrayList类源码,就知道ArrayList底层是通过数组来存储元素的,所以如果严格来说,数组也算集合的一种 + + + Java反射 + http://localhost:1313/iblog/posts/java/rookie-reflect/ + Sat, 02 Oct 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-reflect/ + 概述 什么是反射 在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。 反射是Java语言的一个特性,它允许程序 + + + Object类方法 + http://localhost:1313/iblog/posts/java/rookie-objectclass-methods/ + Sat, 10 Jul 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-objectclass-methods/ + 概览 Object 类位于 java.lang 包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承Object,成为Object的子类。 Object类可以显示继承,也可以隐式继承,效果都是一样的。 class A extends Object{ // to do } class A { // to do } Java Objec + + + Spring详解 + http://localhost:1313/iblog/posts/spring/java-spring/ + Thu, 13 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/java-spring/ + 概览 Spring是一个轻量级的Java开源框架,为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IOC)和面向切面(AOP)。 简单来说,Spring是一个分层的JavaSE/EE 一站式轻量级开源框架。在每一层都提供支持。 + + + JVM-垃圾回收器 + http://localhost:1313/iblog/posts/jvm/java-garbage-collector/ + Thu, 06 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/java-garbage-collector/ + 垃圾回收器分类 垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。 由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。 Java不同版本新特性学习思路: 语法层面:Lambda表达式、s + + + Java多线程 + http://localhost:1313/iblog/posts/java/rookie-multi-thread/ + Wed, 05 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-multi-thread/ + 相关概念 线程与进程 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。例如,一个正在运行的程序的实例就是一个进程。 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 一条线程指的是进程中一个单一 + + + HashMap详解 + http://localhost:1313/iblog/posts/essays/java-hashmap/ + Mon, 03 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-hashmap/ + 相关概念 capacity: 容量,默认16; loadFactor: 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容; threshold: 阈值;阈值 + + + JVM-相关概念 + http://localhost:1313/iblog/posts/jvm/jvm-about/ + Tue, 27 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-about/ + 内存溢出 内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。 官方文档中对内存溢出的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。 由于GC一 + + + JVM-垃圾回收 + http://localhost:1313/iblog/posts/jvm/java-garbage-collection/ + Wed, 21 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/java-garbage-collection/ + 垃圾回收 垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。 垃圾收集机制是Java的招牌能力,极大地提高了开发效率。 如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展 + + + JVM-执行引擎 + http://localhost:1313/iblog/posts/jvm/jvm-execute-engine/ + Thu, 15 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-execute-engine/ + 概述 执行引擎是Java虚拟机核心的组成部分之一,属于JVM的下层,里面包括 解释器、及时编译器、垃圾回收器。 “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力, 其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统 + + + JVM-直接内存 + http://localhost:1313/iblog/posts/jvm/jvm-direct-memory/ + Wed, 14 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-direct-memory/ + 直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。 直接内存是在Java堆外的、直接向系统申请的内存区间。 操作直接内存演示代码: public class MainTest { public static void main(String[] args) { ByteBuffer allocate = ByteBuffer.allocate(1024 * 1024 * 1024); System.out.println(&# + + + JVM-Java对象 + http://localhost:1313/iblog/posts/jvm/java-object/ + Mon, 12 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/java-object/ + 对象实例化 对象的创建方式 使用new关键字创建:最常见的方式、单例类中调用getInstance的静态类方法,XXXFactory的静态方法; 使用反射方式创建: 使用Class的newInstance方法:在JDK9里面被标记为过时的方法,因为 + + + Java语法糖 + http://localhost:1313/iblog/posts/essays/java-syntax-sugar/ + Sat, 10 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-syntax-sugar/ + 原文地址:https://www.jianshu.com/p/0f967298a5d7 语法糖 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法, 这种语法对语言的功能并没有 + + + JavaIO + http://localhost:1313/iblog/posts/java/rookie-io/ + Fri, 09 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-io/ + 概念 Java IO通过数据流、序列化和文件系统提供系统输入和输出。 IO,即 in 和 out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。 传统的 IO 是通过流技术来处理的。 流(Stream),是一个抽象的概念, + + + JVM-方法区 + http://localhost:1313/iblog/posts/jvm/jvm-method-area/ + Thu, 08 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-method-area/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-堆 + http://localhost:1313/iblog/posts/jvm/jvm-heap/ + Sat, 03 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-heap/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-本地方法接口 + http://localhost:1313/iblog/posts/jvm/jvm-native-interface/ + Fri, 02 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-native-interface/ + 概念 简单地讲,一个Native Methodt是一个Java调用非Java代码的接囗。 一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。 这个特征并非Java所特有,很多其它的编程语言都有这一机制,比 + + + JVM-本地方法栈 + http://localhost:1313/iblog/posts/jvm/jvm-native-stack/ + Fri, 02 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-native-stack/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-虚拟机栈 + http://localhost:1313/iblog/posts/jvm/jvm-stack/ + Sun, 28 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-stack/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-程序计数寄存器 + http://localhost:1313/iblog/posts/jvm/jvm-pc-register/ + Sat, 27 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-pc-register/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-JVM介绍 + http://localhost:1313/iblog/posts/jvm/jvm-start/ + Fri, 05 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-start/ + 为什么要学习JVM 大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。 一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要, + + + 面向对象 + http://localhost:1313/iblog/posts/java/rookie-object-oriented/ + Mon, 15 Feb 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-object-oriented/ + 面向对象是一种编程思想,包括三大特性和六大原则,其中,三大特性指的是封装、继承和多态;六大原则指的是单一职责原则、开放封闭原则、迪米特原则、里氏替换原则、依赖倒置原则以及接口隔离原则,其中,单一职责原则是指一个类应该是一组相关性很高的函数和 + + + JVM-Java类加载机制 + http://localhost:1313/iblog/posts/jvm/jvm-classloader/ + Fri, 05 Feb 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-classloader/ + 类加载过程 在Java中,类加载器把一个类装入JVM中,要经过以下步骤: 加载、验证、准备、解析和初始化。其中验证,准备,解析统称为连接。 这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。 类加载器只负责class文 + + + Java运算 + http://localhost:1313/iblog/posts/java/rookie-operation/ + Sat, 30 Jan 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-operation/ + 运算符与表达式 运算符 运算符指明对操作数的运算方式。组成表达式的Java操作符有很多种。运算符按照其要求的操作数数目来分,可以有单目运算符、双目运算符和三目运算符,它们分别对应于1个、2个、3个操作数。 种类 运算符按其功能来分:有算术运算符、赋 + + + Java数据类型 + http://localhost:1313/iblog/posts/java/rookie-datatype/ + Wed, 20 Jan 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-datatype/ + 基本类型 Java语言提供了八种基本类型。六种数值类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. 俗称4类8种 这里只介绍称4类8种.实际上,JAVA中还存在另外一种基本类型 void,它也有对应的包装类java.lang.Void + + + Java异常 + http://localhost:1313/iblog/posts/java/rookie-exception/ + Wed, 13 Jan 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-exception/ + 异常类型 Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:Error 和 Exception。 其中 Error 用来表示Java程序无法处理的错误;这类错误一般与硬件有关,与程序本身无关,通常由系统进行处理,程序本身无法捕获和处理。是不可控制的。 Exception 分为两种 + + + 线程状态及创建方式 + http://localhost:1313/iblog/posts/essays/thread-state-and-created/ + Mon, 20 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/thread-state-and-created/ + 线程状态及转换 线程状态共包含6种,6中状态又可以互相的转换。 新建状态(New): 创建了线程后尚未启动; 可运行状态(Runnable): 可能正在运行,也可能正在等待 CPU 时间片。包含了运行中(Running)和 就绪(Ready)状态; 就绪(Rea + + + Java中常用到的锁 + http://localhost:1313/iblog/posts/essays/java-lock/ + Tue, 07 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-lock/ + 公平锁 指多个线程按照申请锁的顺序来获取锁类似排队打饭 先来后到 优点: 所有的线程都能得到资源,不会饿死在队列中。 缺点: 吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。 非公平锁 指在多线程获取锁的顺序并 + + + Java中集合的线程不安全问题 + http://localhost:1313/iblog/posts/essays/java-thread-collection/ + Sun, 05 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-thread-collection/ + ArrayList ArrayList线程不安全示例: public static void main(String[] args) { ArrayList&lt;String&gt; arrayList = new ArrayList&lt;&gt;(); for(int i=0; i&lt; 3; i++) { new Thread(() -&gt; { arrayList.add(UUID.randomUUID().toString()); System.out.println(arrayList); },String.valueOf(i)).start(); } } // ConcurrentModificationException 同步修改异常 Exception in thread &#34;8&#34; java.util.ConcurrentModificationException [null, 2041b613-8068-4ddd-9d01-305f5680d377] [null, 2041b613-8068-4ddd-9d01-305f5680d377, b3e0296d-e263-4632-a023-4267cdec5e25] [null, 2041b613-8068-4ddd-9d01-305f5680d377] 原因分析: 当某个线程正在执行 add()方法时,被某个线程打断,添加到一半被打断,没有被添加完 解决方案: 使用Vec + + + diff --git "a/blog-site/public/tags/java\345\237\272\347\241\200/index.html" "b/blog-site/public/tags/java\345\237\272\347\241\200/index.html" new file mode 100644 index 00000000..618db8cb --- /dev/null +++ "b/blog-site/public/tags/java\345\237\272\347\241\200/index.html" @@ -0,0 +1,252 @@ + + + + + + + + + + + Java基础 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2021
+
+ +
+
+ Java集合 +
10-04
+
+
+ +
+
+ Java反射 +
10-02
+
+
+ +
+
+ Object类方法 +
07-10
+
+
+ +
+
+ Java多线程 +
05-05
+
+
+ +
+
+ JavaIO +
04-09
+
+
+ +
+
+ 面向对象 +
02-15
+
+
+ +
+
+ Java运算 +
01-30
+
+
+ +
+
+ Java数据类型 +
01-20
+
+
+ +
+
+ Java异常 +
01-13
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/java\345\237\272\347\241\200/index.xml" "b/blog-site/public/tags/java\345\237\272\347\241\200/index.xml" new file mode 100644 index 00000000..2c300bd6 --- /dev/null +++ "b/blog-site/public/tags/java\345\237\272\347\241\200/index.xml" @@ -0,0 +1,75 @@ + + + + Java基础 on 唯手熟尔 + http://localhost:1313/iblog/tags/java%E5%9F%BA%E7%A1%80/ + Recent content in Java基础 on 唯手熟尔 + Hugo -- gohugo.io + zh + Mon, 04 Oct 2021 00:00:00 +0000 + + + Java集合 + http://localhost:1313/iblog/posts/java/rookie-java-container/ + Mon, 04 Oct 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-java-container/ + 概述 Java中的集合主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 如果你看过ArrayList类源码,就知道ArrayList底层是通过数组来存储元素的,所以如果严格来说,数组也算集合的一种 + + + Java反射 + http://localhost:1313/iblog/posts/java/rookie-reflect/ + Sat, 02 Oct 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-reflect/ + 概述 什么是反射 在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。 反射是Java语言的一个特性,它允许程序 + + + Object类方法 + http://localhost:1313/iblog/posts/java/rookie-objectclass-methods/ + Sat, 10 Jul 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-objectclass-methods/ + 概览 Object 类位于 java.lang 包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承Object,成为Object的子类。 Object类可以显示继承,也可以隐式继承,效果都是一样的。 class A extends Object{ // to do } class A { // to do } Java Objec + + + Java多线程 + http://localhost:1313/iblog/posts/java/rookie-multi-thread/ + Wed, 05 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-multi-thread/ + 相关概念 线程与进程 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。例如,一个正在运行的程序的实例就是一个进程。 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 一条线程指的是进程中一个单一 + + + JavaIO + http://localhost:1313/iblog/posts/java/rookie-io/ + Fri, 09 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-io/ + 概念 Java IO通过数据流、序列化和文件系统提供系统输入和输出。 IO,即 in 和 out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。 传统的 IO 是通过流技术来处理的。 流(Stream),是一个抽象的概念, + + + 面向对象 + http://localhost:1313/iblog/posts/java/rookie-object-oriented/ + Mon, 15 Feb 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-object-oriented/ + 面向对象是一种编程思想,包括三大特性和六大原则,其中,三大特性指的是封装、继承和多态;六大原则指的是单一职责原则、开放封闭原则、迪米特原则、里氏替换原则、依赖倒置原则以及接口隔离原则,其中,单一职责原则是指一个类应该是一组相关性很高的函数和 + + + Java运算 + http://localhost:1313/iblog/posts/java/rookie-operation/ + Sat, 30 Jan 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-operation/ + 运算符与表达式 运算符 运算符指明对操作数的运算方式。组成表达式的Java操作符有很多种。运算符按照其要求的操作数数目来分,可以有单目运算符、双目运算符和三目运算符,它们分别对应于1个、2个、3个操作数。 种类 运算符按其功能来分:有算术运算符、赋 + + + Java数据类型 + http://localhost:1313/iblog/posts/java/rookie-datatype/ + Wed, 20 Jan 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-datatype/ + 基本类型 Java语言提供了八种基本类型。六种数值类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. 俗称4类8种 这里只介绍称4类8种.实际上,JAVA中还存在另外一种基本类型 void,它也有对应的包装类java.lang.Void + + + Java异常 + http://localhost:1313/iblog/posts/java/rookie-exception/ + Wed, 13 Jan 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/java/rookie-exception/ + 异常类型 Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:Error 和 Exception。 其中 Error 用来表示Java程序无法处理的错误;这类错误一般与硬件有关,与程序本身无关,通常由系统进行处理,程序本身无法捕获和处理。是不可控制的。 Exception 分为两种 + + + diff --git a/blog-site/public/tags/js/index.html b/blog-site/public/tags/js/index.html new file mode 100644 index 00000000..d71b0a3b --- /dev/null +++ b/blog-site/public/tags/js/index.html @@ -0,0 +1,252 @@ + + + + + + + + + + + Js | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2018
+
+ +
+
+ Js雪花飘落 +
12-25
+
+
+ +
+
+ Js下雨特效 +
12-10
+
+
+ +
+
+ Js换肤特效 +
11-14
+
+
+ +
+
+ Js折纸导航栏 +
10-25
+
+
+ +
+
+ Js表白神器 +
10-14
+
+
+ +
+
+ Js懒加载 +
09-21
+
+
+ +
+
+ Js五子棋 +
09-10
+
+
+ +
+
+ Js滑块拖拽 +
09-08
+
+
+ +
+
+ Js生日礼物 +
08-24
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/tags/js/index.xml b/blog-site/public/tags/js/index.xml new file mode 100644 index 00000000..3cfc7067 --- /dev/null +++ b/blog-site/public/tags/js/index.xml @@ -0,0 +1,75 @@ + + + + Js on 唯手熟尔 + http://localhost:1313/iblog/tags/js/ + Recent content in Js on 唯手熟尔 + Hugo -- gohugo.io + zh + Tue, 25 Dec 2018 00:00:00 +0000 + + + Js雪花飘落 + http://localhost:1313/iblog/posts/toy/js-snow/ + Tue, 25 Dec 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-snow/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;snow&lt;/title&gt; &lt;/head&gt; &lt;style&gt; html { width: 100%; } body { margin: 0; padding: 0; overflow-y: hidden; width: 100%; } .header { width: 100%; height: 315px; background: url(&#34;images/header-bg.png&#34;) repeat; } .snow { position: relative; height: inherit; width: 960px; background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) no-repeat 0 0;; margin: 0 auto; animation: auto 10s linear infinite; } /* 下雪动画 插入两个背景图片*/ @keyframes auto { from { background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) repeat 0 0; } to { background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) repeat 0 1000px; } } tree, snow { position: absolute; } tree { width: 112px; height: 137px; background: url(&#34;images/tree.png&#34;); + + + Js下雨特效 + http://localhost:1313/iblog/posts/toy/js-rain/ + Mon, 10 Dec 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-rain/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt; rain &lt;/title&gt; &lt;style&gt; html { width: 100%; } body { width: 100%; margin: 0; padding: 0; background-color: #000; } .rain { display: block; } embed { display: block; } &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;!-- 2、使用hidden=&#34;true&#34;表示隐藏音乐播放按钮,相反使用hidden=&#34;false&#34;表示开启音乐播放按钮。 3、使用a + + + Js换肤特效 + http://localhost:1313/iblog/posts/toy/js-trans-skin/ + Wed, 14 Nov 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-trans-skin/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;换肤特效&lt;/title&gt; &lt;style type=&#34;text/css&#34;&gt; body { margin: 0; background-image: url(&#34;images/1.jpg&#34;); background-size: cover; } ul { margin: 0; padding: 0; list-style-type: none; } .bg-list { display: none; margin: 0; width: 100%; height: 200px; background: rgba(0, 0, 0, 0.5); } .img-wrap { height: 200px; display: flex; justify-content: space-around; align-items: center; } .tab-btn { background-image: url(&#34;images/upseek.png&#34;); height: 50px; width: 50px; position: fixed; top: 0; right: 0; } .tab-btn:hover { background-position-y: -63.6px; } &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;div class=&#34;bg-list&#34;&gt; &lt;ul class=&#34;img-wrap&#34;&gt; &lt;li class=&#34;img-item&#34; data-src=&#34;images/1.jpg&#34;&gt; &lt;img src=&#34;images/1-1.jpg&#34; width=&#34;160px&#34;/&gt; &lt;/li&gt; + + + Js折纸导航栏 + http://localhost:1313/iblog/posts/toy/js-paper-folding/ + Thu, 25 Oct 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-paper-folding/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;折纸导航栏&lt;/title&gt; &lt;/head&gt; &lt;style&gt; *{ margin: 0; padding: 0; } .content{ position: relative; width: 400px; height: 30px; margin: 50px auto; /*-webkit-perspective: 1000px; -moz-perspective: 1000px; -ms-perspective: 1000px;*/ perspective: 1000px;/*景深相当于眼睛距离元素的位置距离*/ } .content .open{ transform: rotateX(0); animation: open 1s linear; } @keyframes open { 0%{ transform: rotateX(-90deg); } 20%{ transform:rotateX(30deg); } 40%{ transform:rotateX(-60deg); } 60%{ transform:rotateX(60deg); } 80%{ transform:rotateX(-30deg); + + + Js表白神器 + http://localhost:1313/iblog/posts/toy/js-love-heart/ + Sun, 14 Oct 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-love-heart/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;love&lt;/title&gt; &lt;style&gt; *{ margin: 0; padding: 0; } body{ background-color: #000; background-size: cover; overflow-y: hidden; } .love{ width: 400px; height: 400px; /*background-color: #7c7c7c;*/ margin: 130px auto; animation: move 1s infinite alternate; } @keyframes move { 100%{ transform: scale(1.5); } } .left{ float: left; width: 150px; height: 250px; background-color: #FF0000; border-radius: 75px 75px 0 5px; -webkit-transform: rotate(-45deg); -moz-transform: rotate(-45deg); -ms-transform: rotate(-45deg); -o-transform: rotate(-45deg); transform: rotate(-45deg); margin-left: 85px; box-shadow: 0 0 20px #FF0000; animation: shadow 1s infinite alternate; } @keyframes shadow { 100%{ box-shadow: 0 0 100px #FF0000; } } .right{ float: left; width: 150px; height: 250px; background-color: #FF0000; border-radius: 75px 75px 5px 0; -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -ms-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg); + + + Js懒加载 + http://localhost:1313/iblog/posts/toy/js-loadding-lazy/ + Fri, 21 Sep 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-loadding-lazy/ + index.html &lt;!doctype html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;meta name=&#34;Generator&#34; content=&#34;EditPlus®&#34;&gt; &lt;meta name=&#34;Author&#34; content=&#34;&#34;&gt; &lt;meta name=&#34;Keywords&#34; content=&#34;&#34;&gt; &lt;meta name=&#34;Description&#34; content=&#34;&#34;&gt; &lt;title&gt;懒加载技术&lt;/title&gt; &lt;style&gt; *{ margin: 0; padding:0; } body{ background: rgb(0,0,0); } .box{ overflow: hidden; width: 948px; background-color: #7c7c7c; margin: 50px auto; -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; } .box img{ float: left; display: block; width: 300px; height: 150px; margin: + + + Js五子棋 + http://localhost:1313/iblog/posts/toy/js-gomoku/ + Mon, 10 Sep 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-gomoku/ + index.html &lt;!DOCTYPE html PUBLIC &#34;-//W3C//DTD XHTML 1.0 Transitional//EN&#34; &#34;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&#34;&gt; &lt;html xmlns=&#34;http://www.w3.org/1999/xhtml&#34;&gt; &lt;head&gt; &lt;meta http-equiv=&#34;Content-Type&#34; content=&#34;text/html; charset=UTF-8&#34; /&gt; &lt;title&gt;五子棋&lt;/title&gt; &lt;meta name=&#34;viewport&#34; content=&#34;device-width; initial-scale=1.0;&#34; /&gt; &lt;style&gt; #c1 { display: block; margin: 60px auto; box-shadow: 1px 1px 5px #000000; } &lt;/style&gt; &lt;script src=&#34;js/index.js&#34;&gt;&lt;/script&gt; &lt;/head&gt; &lt;body&gt; &lt;canvas id=&#34;c1&#34; width=&#34;450px&#34; height=&#34;450px&#34;&gt;&lt;/canvas&gt; &lt;/body&gt; &lt;/html&gt; index.js window.onload = function(){ var oC = document.getElementById(&#39;c1&#39;); var oGc = oC.getContext(&#39;2d&#39;); var over = false; oGc.strokeStyle = &#34;#bfbfbf&#34;; //绘制棋盘 for(var i=0;i&lt;15;i++){ oGc.moveTo(15+i*30,15); oGc.lineTo(15+i*30,435); oGc.stroke(); oGc.moveTo(15,15+i*30); oGc.lineTo(435,15+i*30); oGc.stroke(); } /* AI难点解析 赢法 + + + Js滑块拖拽 + http://localhost:1313/iblog/posts/toy/js-box-drag/ + Sat, 08 Sep 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-box-drag/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;滑块拖拽&lt;/title&gt; &lt;/head&gt; &lt;style&gt; body { margin: 0; padding: 0; user-select: none; } .content { position: relative; width: 300px; height: 40px; margin: 50px auto; background-color: #E8E8EB; text-align: center; line-height: 40px; } .rect { position: absolute; width: 100%; height: 100%; } .rect .bg { position: absolute; left: 0; top: 0; z-index: 1; width: 0; height: 100%; background: rgba(122,194,60,.4); } .rect .move { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; width: 45px; height: 40px; position: absolute; top: 0; left: 0; background-color: #fff; border: 1px solid #cccccc; + + + Js生日礼物 + http://localhost:1313/iblog/posts/toy/js-birthday-gift/ + Fri, 24 Aug 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-birthday-gift/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;card&lt;/title&gt; &lt;style&gt; body,html{ width: 100%; height: 100%; } body{ display: flex;/*弹性盒模型*/ justify-content: center;/*水平对齐 盒子位于中心*/ align-items: center;/*竖直对齐 居中对齐*/ background-color: yellow; perspective: 1000px;/*景深:眼到屏幕的距离*/ } body,h1,p{ margin: 0; } .card{ width: 520px; height: 350px; border-radius: 15px; background: linear-gradient(#020333 70%,#fff 75%);/* + + + diff --git a/blog-site/public/tags/jvm/index.html b/blog-site/public/tags/jvm/index.html new file mode 100644 index 00000000..edfb60b3 --- /dev/null +++ b/blog-site/public/tags/jvm/index.html @@ -0,0 +1,287 @@ + + + + + + + + + + + JVM | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2021
+
+ +
+
+ JVM-垃圾回收器 +
05-06
+
+
+ +
+
+ JVM-相关概念 +
04-27
+
+
+ +
+
+ JVM-垃圾回收 +
04-21
+
+
+ +
+
+ JVM-执行引擎 +
04-15
+
+
+ +
+
+ JVM-直接内存 +
04-14
+
+
+ +
+
+ JVM-Java对象 +
04-12
+
+
+ +
+
+ JVM-方法区 +
04-08
+
+
+ +
+
+ JVM-堆 +
04-03
+
+
+ +
+
+ JVM-本地方法栈 +
04-02
+
+
+ +
+
+ JVM-本地方法接口 +
04-02
+
+
+ +
+
+ JVM-虚拟机栈 +
03-28
+
+
+ +
+
+ JVM-程序计数寄存器 +
03-27
+
+
+ +
+
+ JVM-JVM介绍 +
03-05
+
+
+ +
+
+ JVM-Java类加载机制 +
02-05
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/tags/jvm/index.xml b/blog-site/public/tags/jvm/index.xml new file mode 100644 index 00000000..54c66e86 --- /dev/null +++ b/blog-site/public/tags/jvm/index.xml @@ -0,0 +1,110 @@ + + + + JVM on 唯手熟尔 + http://localhost:1313/iblog/tags/jvm/ + Recent content in JVM on 唯手熟尔 + Hugo -- gohugo.io + zh + Thu, 06 May 2021 00:00:00 +0000 + + + JVM-垃圾回收器 + http://localhost:1313/iblog/posts/jvm/java-garbage-collector/ + Thu, 06 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/java-garbage-collector/ + 垃圾回收器分类 垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。 由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。 Java不同版本新特性学习思路: 语法层面:Lambda表达式、s + + + JVM-相关概念 + http://localhost:1313/iblog/posts/jvm/jvm-about/ + Tue, 27 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-about/ + 内存溢出 内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。 官方文档中对内存溢出的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。 由于GC一 + + + JVM-垃圾回收 + http://localhost:1313/iblog/posts/jvm/java-garbage-collection/ + Wed, 21 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/java-garbage-collection/ + 垃圾回收 垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。 垃圾收集机制是Java的招牌能力,极大地提高了开发效率。 如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展 + + + JVM-执行引擎 + http://localhost:1313/iblog/posts/jvm/jvm-execute-engine/ + Thu, 15 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-execute-engine/ + 概述 执行引擎是Java虚拟机核心的组成部分之一,属于JVM的下层,里面包括 解释器、及时编译器、垃圾回收器。 “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力, 其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统 + + + JVM-直接内存 + http://localhost:1313/iblog/posts/jvm/jvm-direct-memory/ + Wed, 14 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-direct-memory/ + 直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。 直接内存是在Java堆外的、直接向系统申请的内存区间。 操作直接内存演示代码: public class MainTest { public static void main(String[] args) { ByteBuffer allocate = ByteBuffer.allocate(1024 * 1024 * 1024); System.out.println(&# + + + JVM-Java对象 + http://localhost:1313/iblog/posts/jvm/java-object/ + Mon, 12 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/java-object/ + 对象实例化 对象的创建方式 使用new关键字创建:最常见的方式、单例类中调用getInstance的静态类方法,XXXFactory的静态方法; 使用反射方式创建: 使用Class的newInstance方法:在JDK9里面被标记为过时的方法,因为 + + + JVM-方法区 + http://localhost:1313/iblog/posts/jvm/jvm-method-area/ + Thu, 08 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-method-area/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-堆 + http://localhost:1313/iblog/posts/jvm/jvm-heap/ + Sat, 03 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-heap/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-本地方法接口 + http://localhost:1313/iblog/posts/jvm/jvm-native-interface/ + Fri, 02 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-native-interface/ + 概念 简单地讲,一个Native Methodt是一个Java调用非Java代码的接囗。 一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。 这个特征并非Java所特有,很多其它的编程语言都有这一机制,比 + + + JVM-本地方法栈 + http://localhost:1313/iblog/posts/jvm/jvm-native-stack/ + Fri, 02 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-native-stack/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-虚拟机栈 + http://localhost:1313/iblog/posts/jvm/jvm-stack/ + Sun, 28 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-stack/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-程序计数寄存器 + http://localhost:1313/iblog/posts/jvm/jvm-pc-register/ + Sat, 27 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-pc-register/ + Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 + + + JVM-JVM介绍 + http://localhost:1313/iblog/posts/jvm/jvm-start/ + Fri, 05 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-start/ + 为什么要学习JVM 大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。 一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要, + + + JVM-Java类加载机制 + http://localhost:1313/iblog/posts/jvm/jvm-classloader/ + Fri, 05 Feb 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-classloader/ + 类加载过程 在Java中,类加载器把一个类装入JVM中,要经过以下步骤: 加载、验证、准备、解析和初始化。其中验证,准备,解析统称为连接。 这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。 类加载器只负责class文 + + + diff --git a/blog-site/public/tags/mq/index.html b/blog-site/public/tags/mq/index.html new file mode 100644 index 00000000..f72baf97 --- /dev/null +++ b/blog-site/public/tags/mq/index.html @@ -0,0 +1,208 @@ + + + + + + + + + + + MQ | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2021
+
+ +
+
+ MQ详解 +
10-19
+
+
+ +
+ +
2020
+
+ +
+
+ SpringBoot整合kafka +
08-20
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/tags/mq/index.xml b/blog-site/public/tags/mq/index.xml new file mode 100644 index 00000000..6f761c31 --- /dev/null +++ b/blog-site/public/tags/mq/index.xml @@ -0,0 +1,26 @@ + + + + MQ on 唯手熟尔 + http://localhost:1313/iblog/tags/mq/ + Recent content in MQ on 唯手熟尔 + Hugo -- gohugo.io + zh + Tue, 19 Oct 2021 00:00:00 +0000 + + + MQ详解 + http://localhost:1313/iblog/posts/essays/java-mq/ + Tue, 19 Oct 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-mq/ + 概念 MQ 即 messagequeue 消息队列,是分布式系统的重要组件,主要解决异步消息,应用解耦,消峰等问题。从而实现高可用,高性能,可伸缩和最终一致性的架构。使用较多的MQ有:activeMQ,rabbitMQ,kafka,metaMQ。 MQ优点 异步消息处理:可以 + + + SpringBoot整合kafka + http://localhost:1313/iblog/posts/spring/springboot-kafka/ + Thu, 20 Aug 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-kafka/ + kafka介绍 kafka官网: http://kafka.apache.org kafka中文官网: https://kafka.apachecn.org Kafka是一种分布式的,基于发布/订阅的消息系统。主要特点如下: 以时间复杂度为O(1)的方式提供消息持久化能力,并保证即使对TB级以上数据也能保证常数时间的访问性能 高吞吐率。即使在非常 + + + diff --git a/blog-site/public/tags/mysql/index.html b/blog-site/public/tags/mysql/index.html new file mode 100644 index 00000000..69ba6ac4 --- /dev/null +++ b/blog-site/public/tags/mysql/index.html @@ -0,0 +1,196 @@ + + + + + + + + + + + MySQL | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2023
+
+ +
+
+ MySQL详解 +
03-13
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/tags/mysql/index.xml b/blog-site/public/tags/mysql/index.xml new file mode 100644 index 00000000..08e7cef2 --- /dev/null +++ b/blog-site/public/tags/mysql/index.xml @@ -0,0 +1,19 @@ + + + + MySQL on 唯手熟尔 + http://localhost:1313/iblog/tags/mysql/ + Recent content in MySQL on 唯手熟尔 + Hugo -- gohugo.io + zh + Mon, 13 Mar 2023 00:00:00 +0000 + + + MySQL详解 + http://localhost:1313/iblog/posts/essays/sql-select-fast/ + Mon, 13 Mar 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/sql-select-fast/ + 逻辑架构 主要分为:连接层,服务层,引擎层,存储层。 客户端执行一条select命令的流程如下: 连接层:最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcplip的通信。主要完成一些类似于连接处理、 + + + diff --git a/blog-site/public/tags/nacos/index.html b/blog-site/public/tags/nacos/index.html new file mode 100644 index 00000000..722fecb9 --- /dev/null +++ b/blog-site/public/tags/nacos/index.html @@ -0,0 +1,196 @@ + + + + + + + + + + + Nacos | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2023
+
+ +
+
+ SpringBoot整合nacos +
09-04
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/tags/nacos/index.xml b/blog-site/public/tags/nacos/index.xml new file mode 100644 index 00000000..fa7c33ed --- /dev/null +++ b/blog-site/public/tags/nacos/index.xml @@ -0,0 +1,19 @@ + + + + Nacos on 唯手熟尔 + http://localhost:1313/iblog/tags/nacos/ + Recent content in Nacos on 唯手熟尔 + Hugo -- gohugo.io + zh + Mon, 04 Sep 2023 00:00:00 +0000 + + + SpringBoot整合nacos + http://localhost:1313/iblog/posts/spring/springboot-nacos/ + Mon, 04 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-nacos/ + nacos nacos下载 下载地址 一键傻瓜试安装即可,官网写的很清楚这里不在赘述 http://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html nacos启动 将模式改为单机模式 启动成功 nacos相关配置 demo-dev.yaml server: port: 8001 config: info: &#34;config info for dev from nacos config center&#34; demo-test.yaml server: port: 3333 config: info: &#34;config info for test from nacos config center&#34; user.yaml user: name: zs1112222 age: 10 address: 测试地址 代码 整合nacos配置中心,注册 + + + diff --git a/blog-site/public/tags/nginx/index.html b/blog-site/public/tags/nginx/index.html new file mode 100644 index 00000000..bc5e79f4 --- /dev/null +++ b/blog-site/public/tags/nginx/index.html @@ -0,0 +1,196 @@ + + + + + + + + + + + Nginx | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2021
+
+ +
+
+ Nginx介绍 +
03-04
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/tags/nginx/index.xml b/blog-site/public/tags/nginx/index.xml new file mode 100644 index 00000000..ff562f02 --- /dev/null +++ b/blog-site/public/tags/nginx/index.xml @@ -0,0 +1,19 @@ + + + + Nginx on 唯手熟尔 + http://localhost:1313/iblog/tags/nginx/ + Recent content in Nginx on 唯手熟尔 + Hugo -- gohugo.io + zh + Thu, 04 Mar 2021 00:00:00 +0000 + + + Nginx介绍 + http://localhost:1313/iblog/posts/essays/nginx-start/ + Thu, 04 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/nginx-start/ + Nginx介绍 Nginx (&ldquo;engine x&rdquo;)是一个高性能的HTTP和反向代理服务器,特点是占有内存少,并发能力强,事实上Nginx的并发能力确实在同类型的网页服务器中表现较好. Nginx专为性能优化而开发,性能是其最重要的考量,实现上非常注重效率, + + + diff --git a/blog-site/public/tags/redis/index.html b/blog-site/public/tags/redis/index.html new file mode 100644 index 00000000..d0cc7bb8 --- /dev/null +++ b/blog-site/public/tags/redis/index.html @@ -0,0 +1,208 @@ + + + + + + + + + + + Redis | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2021
+
+ +
+
+ Redis详解 +
06-17
+
+
+ +
+ +
2020
+
+ +
+
+ SpringBoot整合redis +
03-01
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/tags/redis/index.xml b/blog-site/public/tags/redis/index.xml new file mode 100644 index 00000000..6543e1fd --- /dev/null +++ b/blog-site/public/tags/redis/index.xml @@ -0,0 +1,26 @@ + + + + Redis on 唯手熟尔 + http://localhost:1313/iblog/tags/redis/ + Recent content in Redis on 唯手熟尔 + Hugo -- gohugo.io + zh + Thu, 17 Jun 2021 00:00:00 +0000 + + + Redis详解 + http://localhost:1313/iblog/posts/essays/java-redis/ + Thu, 17 Jun 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-redis/ + Redis概述 参考文章: https://www.runoob.com/redis/redis-intro.html https://www.redis.com.cn/redis-interview-questions.html 什么是Redis Redis(Remote Dictionary Server) Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API 的非关系型数据库。 简而言之,Redis是一个可基于内存亦可持久 + + + SpringBoot整合redis + http://localhost:1313/iblog/posts/spring/springboot-redis/ + Sun, 01 Mar 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-redis/ + Redis介绍 redis是开源的一个高性能的 key-value 数据库。 主要特点 Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用 Redis支持数据的备份,即master-slave模式的数据备份 Redis 可以存储键与5种不同 + + + diff --git a/blog-site/public/tags/spring/index.html b/blog-site/public/tags/spring/index.html new file mode 100644 index 00000000..3d8a792a --- /dev/null +++ b/blog-site/public/tags/spring/index.html @@ -0,0 +1,208 @@ + + + + + + + + + + + Spring | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2023
+
+ +
+
+ SpringMVC与SpringWebFlux +
04-14
+
+
+ +
+ +
2021
+
+ +
+
+ Spring详解 +
05-13
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/tags/spring/index.xml b/blog-site/public/tags/spring/index.xml new file mode 100644 index 00000000..7e4e477a --- /dev/null +++ b/blog-site/public/tags/spring/index.xml @@ -0,0 +1,26 @@ + + + + Spring on 唯手熟尔 + http://localhost:1313/iblog/tags/spring/ + Recent content in Spring on 唯手熟尔 + Hugo -- gohugo.io + zh + Fri, 14 Apr 2023 00:00:00 +0000 + + + SpringMVC与SpringWebFlux + http://localhost:1313/iblog/posts/spring/java-spring-mvc-webflux/ + Fri, 14 Apr 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/java-spring-mvc-webflux/ + Spring MVC Spring Web MVC是建立在Servlet API上的原始Web框架,从一开始就包含在Spring框架中。正式名称 &ldquo;Spring Web MVC &ldquo;来自其源模块的名称(spring-webmvc),但它更常被称为 &ldquo;Spring MVC&rdquo;。 SpringMVC是基于S + + + Spring详解 + http://localhost:1313/iblog/posts/spring/java-spring/ + Thu, 13 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/java-spring/ + 概览 Spring是一个轻量级的Java开源框架,为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IOC)和面向切面(AOP)。 简单来说,Spring是一个分层的JavaSE/EE 一站式轻量级开源框架。在每一层都提供支持。 + + + diff --git a/blog-site/public/tags/springboot/index.html b/blog-site/public/tags/springboot/index.html new file mode 100644 index 00000000..b2afb7a5 --- /dev/null +++ b/blog-site/public/tags/springboot/index.html @@ -0,0 +1,236 @@ + + + + + + + + + + + Springboot | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2023
+
+ +
+
+ SpringBoot整合nacos +
09-04
+
+
+ +
+
+ Validator参数校验 +
07-01
+
+
+ +
+ +
2020
+
+ +
+
+ SpringBoot整合docker +
08-30
+
+
+ +
+
+ SpringBoot整合kafka +
08-20
+
+
+ +
+
+ SpringBoot整合redis +
03-01
+
+
+ + + +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/tags/springboot/index.xml b/blog-site/public/tags/springboot/index.xml new file mode 100644 index 00000000..0b6f5ff3 --- /dev/null +++ b/blog-site/public/tags/springboot/index.xml @@ -0,0 +1,54 @@ + + + + Springboot on 唯手熟尔 + http://localhost:1313/iblog/tags/springboot/ + Recent content in Springboot on 唯手熟尔 + Hugo -- gohugo.io + zh + Mon, 04 Sep 2023 00:00:00 +0000 + + + SpringBoot整合nacos + http://localhost:1313/iblog/posts/spring/springboot-nacos/ + Mon, 04 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-nacos/ + nacos nacos下载 下载地址 一键傻瓜试安装即可,官网写的很清楚这里不在赘述 http://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html nacos启动 将模式改为单机模式 启动成功 nacos相关配置 demo-dev.yaml server: port: 8001 config: info: &#34;config info for dev from nacos config center&#34; demo-test.yaml server: port: 3333 config: info: &#34;config info for test from nacos config center&#34; user.yaml user: name: zs1112222 age: 10 address: 测试地址 代码 整合nacos配置中心,注册 + + + Validator参数校验 + http://localhost:1313/iblog/posts/essays/springboot-validator/ + Sat, 01 Jul 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/springboot-validator/ + 常见参数校验 在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,最简单就是用if条件语句来判断,但是随着参数越来越多,业务越来越复杂,判断参数代码语句显得尤为冗长. 或者有些程序会将if封装起来,例如spring中 + + + SpringBoot整合docker + http://localhost:1313/iblog/posts/spring/springboot-docker/ + Sun, 30 Aug 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-docker/ + MacOS上安装docker 下载 国内下载网站: http://get.daocloud.io 不推荐下载docker版本太旧了 官网下载: https://docs.docker.com/get-started/#download-and-install-docker 或用homebrew进行下载安装 brew install --cask --appdir=/Applications docker 配置镜像 由于网速原因,可以配置一下国内的镜像加速器 中科大镜像: https://docker.mirrors.ustc.edu.cn 网易: https://hub-mirror.c.163.com 阿里云: https://&lt;你 + + + SpringBoot整合kafka + http://localhost:1313/iblog/posts/spring/springboot-kafka/ + Thu, 20 Aug 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-kafka/ + kafka介绍 kafka官网: http://kafka.apache.org kafka中文官网: https://kafka.apachecn.org Kafka是一种分布式的,基于发布/订阅的消息系统。主要特点如下: 以时间复杂度为O(1)的方式提供消息持久化能力,并保证即使对TB级以上数据也能保证常数时间的访问性能 高吞吐率。即使在非常 + + + SpringBoot整合redis + http://localhost:1313/iblog/posts/spring/springboot-redis/ + Sun, 01 Mar 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-redis/ + Redis介绍 redis是开源的一个高性能的 key-value 数据库。 主要特点 Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用 Redis支持数据的备份,即master-slave模式的数据备份 Redis 可以存储键与5种不同 + + + SpringBoot整合elasticsearch + http://localhost:1313/iblog/posts/spring/springboot-elasticsearch/ + Sun, 09 Feb 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/spring/springboot-elasticsearch/ + 安装elasticsearch 要注意导入依赖的版本和安装elasticsearch的版本与springboot的兼容问题 用 docker 安装 elasticsearch 本例用elasticsearch-6.5.3和springboot-2.1.0.RELEASE版本 下载镜像: docker + + + diff --git a/blog-site/public/tags/vue/index.html b/blog-site/public/tags/vue/index.html new file mode 100644 index 00000000..009f0134 --- /dev/null +++ b/blog-site/public/tags/vue/index.html @@ -0,0 +1,196 @@ + + + + + + + + + + + Vue | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2019
+
+ +
+
+ Vue2.0学习笔记 +
05-23
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/blog-site/public/tags/vue/index.xml b/blog-site/public/tags/vue/index.xml new file mode 100644 index 00000000..ecd52e73 --- /dev/null +++ b/blog-site/public/tags/vue/index.xml @@ -0,0 +1,19 @@ + + + + Vue on 唯手熟尔 + http://localhost:1313/iblog/tags/vue/ + Recent content in Vue on 唯手熟尔 + Hugo -- gohugo.io + zh + Thu, 23 May 2019 00:00:00 +0000 + + + Vue2.0学习笔记 + http://localhost:1313/iblog/posts/essays/vue2-note/ + Thu, 23 May 2019 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/vue2-note/ + 参考资料 vue官方文档: https://cn.vuejs.org/v2/guide vue参考视频资料: https://www.bilibili.com/video/av50680998 vue菜鸟教程文档: https://www.runoob.com/vue2/vue-tutorial.html vue-组件 参考资料: https://cn.vuejs.org/v2/guide/components.html#ad 组件是可复用的 Vue 实例,且带有一个名字. 组件的出现是为了拆分vue实例的代码量,能够让我们以不同的组件,来划分不同的功能模块,将来我们需要什么样的功 + + + diff --git "a/blog-site/public/tags/\344\271\246\347\261\215/index.html" "b/blog-site/public/tags/\344\271\246\347\261\215/index.html" new file mode 100644 index 00000000..b209f176 --- /dev/null +++ "b/blog-site/public/tags/\344\271\246\347\261\215/index.html" @@ -0,0 +1,215 @@ + + + + + + + + + + + 书籍 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2022
+
+ +
+
+ 孙子兵法 +
12-19
+
+
+ +
+
+ 关于编程的书籍 +
08-08
+
+
+ +
+ +
2021
+
+ +
+
+ 道德经 +
03-03
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\344\271\246\347\261\215/index.xml" "b/blog-site/public/tags/\344\271\246\347\261\215/index.xml" new file mode 100644 index 00000000..b4fe7372 --- /dev/null +++ "b/blog-site/public/tags/\344\271\246\347\261\215/index.xml" @@ -0,0 +1,33 @@ + + + + 书籍 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E4%B9%A6%E7%B1%8D/ + Recent content in 书籍 on 唯手熟尔 + Hugo -- gohugo.io + zh + Mon, 19 Dec 2022 00:00:00 +0000 + + + 孙子兵法 + http://localhost:1313/iblog/posts/books/books-sunzibingfa/ + Mon, 19 Dec 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/books/books-sunzibingfa/ + 始计 兵者,国之大事,死生之地,存亡之道,不可不察也。 故经之以五事,校之以计,而索其情:一曰道,二曰天,三曰地,四曰将,五曰法。道者,令民于上同意,可与之死,可与之生,而不危也;天者,阴阳、寒暑、时制也;地者,远近、险易、广狭、死生也;将者, + + + 关于编程的书籍 + http://localhost:1313/iblog/posts/books/java-books/ + Mon, 08 Aug 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/books/java-books/ + HeadFirst设计模式 Java数据结构和算法 Java核心技术卷I基础知识 Java编程思想 代码整洁之道 大型网站技术架构 大话数据结构 深入分析JavaWeb技术内幕 疯狂Java讲义 重构:改善既有代码的设计 领域驱动设计 + + + 道德经 + http://localhost:1313/iblog/posts/books/books-daodejing/ + Wed, 03 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/books/books-daodejing/ + 第一章 道可道,非常道。名可名,非常名。 无名天地之始﹔有名万物之母。 故常无,欲以观其妙﹔常有,欲以观其徼。 此两者,同出而异名,同谓之玄。 玄之又玄,众妙之门。 第二章 天下皆知美之为美,斯恶已。 皆知善之为善,斯不善已。 有无相生,难易相成,长短相形, + + + diff --git "a/blog-site/public/tags/\344\272\213\345\212\241/index.html" "b/blog-site/public/tags/\344\272\213\345\212\241/index.html" new file mode 100644 index 00000000..5cf7647b --- /dev/null +++ "b/blog-site/public/tags/\344\272\213\345\212\241/index.html" @@ -0,0 +1,196 @@ + + + + + + + + + + + 事务 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2021
+
+ +
+
+ 分布式事务详解 +
08-02
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\344\272\213\345\212\241/index.xml" "b/blog-site/public/tags/\344\272\213\345\212\241/index.xml" new file mode 100644 index 00000000..9e51d9a7 --- /dev/null +++ "b/blog-site/public/tags/\344\272\213\345\212\241/index.xml" @@ -0,0 +1,19 @@ + + + + 事务 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E4%BA%8B%E5%8A%A1/ + Recent content in 事务 on 唯手熟尔 + Hugo -- gohugo.io + zh + Mon, 02 Aug 2021 00:00:00 +0000 + + + 分布式事务详解 + http://localhost:1313/iblog/posts/essays/java-transaction/ + Mon, 02 Aug 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-transaction/ + 基础概念 什么是事务 什么是事务?举个例子:你去超市买东西,&ldquo;一手交钱,一手交货&quot;就是一个事务的例子,交钱和交货必须同时成功,事务才算成功,其中有一个环节失败,事务将会撤销所有已成功的活动。 所以事务可以看作是一次重大的活动 + + + diff --git "a/blog-site/public/tags/\344\275\277\347\224\250\344\273\213\347\273\215/index.html" "b/blog-site/public/tags/\344\275\277\347\224\250\344\273\213\347\273\215/index.html" new file mode 100644 index 00000000..7433100f --- /dev/null +++ "b/blog-site/public/tags/\344\275\277\347\224\250\344\273\213\347\273\215/index.html" @@ -0,0 +1,215 @@ + + + + + + + + + + + 使用介绍 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2021
+
+ +
+
+ JVM-JVM介绍 +
03-05
+
+
+ +
+
+ Nginx介绍 +
03-04
+
+
+ +
+ +
2020
+
+ +
+
+ Docker介绍 +
04-07
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\344\275\277\347\224\250\344\273\213\347\273\215/index.xml" "b/blog-site/public/tags/\344\275\277\347\224\250\344\273\213\347\273\215/index.xml" new file mode 100644 index 00000000..94d67965 --- /dev/null +++ "b/blog-site/public/tags/\344\275\277\347\224\250\344\273\213\347\273\215/index.xml" @@ -0,0 +1,33 @@ + + + + 使用介绍 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E4%BD%BF%E7%94%A8%E4%BB%8B%E7%BB%8D/ + Recent content in 使用介绍 on 唯手熟尔 + Hugo -- gohugo.io + zh + Fri, 05 Mar 2021 00:00:00 +0000 + + + JVM-JVM介绍 + http://localhost:1313/iblog/posts/jvm/jvm-start/ + Fri, 05 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/jvm/jvm-start/ + 为什么要学习JVM 大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。 一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要, + + + Nginx介绍 + http://localhost:1313/iblog/posts/essays/nginx-start/ + Thu, 04 Mar 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/nginx-start/ + Nginx介绍 Nginx (&ldquo;engine x&rdquo;)是一个高性能的HTTP和反向代理服务器,特点是占有内存少,并发能力强,事实上Nginx的并发能力确实在同类型的网页服务器中表现较好. Nginx专为性能优化而开发,性能是其最重要的考量,实现上非常注重效率, + + + Docker介绍 + http://localhost:1313/iblog/posts/essays/docker-start/ + Tue, 07 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/docker-start/ + docker是什么 Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前流行的 Linux 容器解决方案。 Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样 + + + diff --git "a/blog-site/public/tags/\345\210\206\345\270\203\345\274\217/index.html" "b/blog-site/public/tags/\345\210\206\345\270\203\345\274\217/index.html" new file mode 100644 index 00000000..2535fa9f --- /dev/null +++ "b/blog-site/public/tags/\345\210\206\345\270\203\345\274\217/index.html" @@ -0,0 +1,203 @@ + + + + + + + + + + + 分布式 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2021
+
+ +
+
+ 分布式事务详解 +
08-02
+
+
+ +
+
+ 微服务治理 +
06-21
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\345\210\206\345\270\203\345\274\217/index.xml" "b/blog-site/public/tags/\345\210\206\345\270\203\345\274\217/index.xml" new file mode 100644 index 00000000..dae2d402 --- /dev/null +++ "b/blog-site/public/tags/\345\210\206\345\270\203\345\274\217/index.xml" @@ -0,0 +1,26 @@ + + + + 分布式 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E5%88%86%E5%B8%83%E5%BC%8F/ + Recent content in 分布式 on 唯手熟尔 + Hugo -- gohugo.io + zh + Mon, 02 Aug 2021 00:00:00 +0000 + + + 分布式事务详解 + http://localhost:1313/iblog/posts/essays/java-transaction/ + Mon, 02 Aug 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-transaction/ + 基础概念 什么是事务 什么是事务?举个例子:你去超市买东西,&ldquo;一手交钱,一手交货&quot;就是一个事务的例子,交钱和交货必须同时成功,事务才算成功,其中有一个环节失败,事务将会撤销所有已成功的活动。 所以事务可以看作是一次重大的活动 + + + 微服务治理 + http://localhost:1313/iblog/posts/essays/java-small-service/ + Mon, 21 Jun 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-small-service/ + 什么是微服务架构 In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API。 These services are built around business capabilities and independently deployable by fully automated deployment machinery。 There is a bare minimum of centralized management of these services, which may be written in different programming + + + diff --git "a/blog-site/public/tags/\345\255\246\344\271\240\350\267\257\347\272\277/index.html" "b/blog-site/public/tags/\345\255\246\344\271\240\350\267\257\347\272\277/index.html" new file mode 100644 index 00000000..d7a14c10 --- /dev/null +++ "b/blog-site/public/tags/\345\255\246\344\271\240\350\267\257\347\272\277/index.html" @@ -0,0 +1,196 @@ + + + + + + + + + + + 学习路线 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2020
+
+ +
+
+ 前端学习路线 +
04-04
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\345\255\246\344\271\240\350\267\257\347\272\277/index.xml" "b/blog-site/public/tags/\345\255\246\344\271\240\350\267\257\347\272\277/index.xml" new file mode 100644 index 00000000..cb9525b2 --- /dev/null +++ "b/blog-site/public/tags/\345\255\246\344\271\240\350\267\257\347\272\277/index.xml" @@ -0,0 +1,19 @@ + + + + 学习路线 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF/ + Recent content in 学习路线 on 唯手熟尔 + Hugo -- gohugo.io + zh + Sat, 04 Apr 2020 00:00:00 +0000 + + + 前端学习路线 + http://localhost:1313/iblog/posts/essays/front-learning-route/ + Sat, 04 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/front-learning-route/ + + + + diff --git "a/blog-site/public/tags/\345\267\245\344\275\234\346\200\273\347\273\223/index.html" "b/blog-site/public/tags/\345\267\245\344\275\234\346\200\273\347\273\223/index.html" new file mode 100644 index 00000000..e2ca8c68 --- /dev/null +++ "b/blog-site/public/tags/\345\267\245\344\275\234\346\200\273\347\273\223/index.html" @@ -0,0 +1,208 @@ + + + + + + + + + + + 工作总结 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2023
+
+ +
+
+ 2023工作总结 +
12-01
+
+
+ +
+ +
2019
+
+ +
+
+ 2019工作总结 +
12-01
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\345\267\245\344\275\234\346\200\273\347\273\223/index.xml" "b/blog-site/public/tags/\345\267\245\344\275\234\346\200\273\347\273\223/index.xml" new file mode 100644 index 00000000..67b01685 --- /dev/null +++ "b/blog-site/public/tags/\345\267\245\344\275\234\346\200\273\347\273\223/index.xml" @@ -0,0 +1,26 @@ + + + + 工作总结 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E5%B7%A5%E4%BD%9C%E6%80%BB%E7%BB%93/ + Recent content in 工作总结 on 唯手熟尔 + Hugo -- gohugo.io + zh + Fri, 01 Dec 2023 00:00:00 +0000 + + + 2023工作总结 + http://localhost:1313/iblog/posts/worksummary/work-summary-2023/ + Fri, 01 Dec 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/worksummary/work-summary-2023/ + 在职期间,我主要负责耐材项目的开发与维护,共迭代171个版本。通过与团队成员的紧密合作,我们按时完成了项目中的需求。在这段时间里,我不断提升自己的专业技能和知识,增强了自己的专业能力。我始终认为团队合作是成功的关键。在工作中,我积极与同事沟 + + + 2019工作总结 + http://localhost:1313/iblog/posts/worksummary/work-summary-2019/ + Sun, 01 Dec 2019 00:00:00 +0000 + http://localhost:1313/iblog/posts/worksummary/work-summary-2019/ + 本人在进入公司起,期间一直对自己要求严谨,遵守公司的相应制度. 在过去的一个月时间里,我参与了贵州银行的电子验印系统的开发,一直努力完成和完善分配给我的任务,在这一个月发现了自身还有很多的不足,所以抱着虚心学习的态度,学习公司的开发流程,了解 + + + diff --git "a/blog-site/public/tags/\345\272\224\347\224\250/index.html" "b/blog-site/public/tags/\345\272\224\347\224\250/index.html" new file mode 100644 index 00000000..ea79842e --- /dev/null +++ "b/blog-site/public/tags/\345\272\224\347\224\250/index.html" @@ -0,0 +1,224 @@ + + + + + + + + + + + 应用 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2023
+
+ +
+
+ 定时任务可视化管理 +
09-09
+
+
+ +
+
+ 整合文件上传功能 +
08-11
+
+
+ +
+
+ 整合支付功能 +
08-10
+
+
+ +
+
+ Validator参数校验 +
07-01
+
+
+ + + +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\345\272\224\347\224\250/index.xml" "b/blog-site/public/tags/\345\272\224\347\224\250/index.xml" new file mode 100644 index 00000000..d09423f7 --- /dev/null +++ "b/blog-site/public/tags/\345\272\224\347\224\250/index.xml" @@ -0,0 +1,47 @@ + + + + 应用 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E5%BA%94%E7%94%A8/ + Recent content in 应用 on 唯手熟尔 + Hugo -- gohugo.io + zh + Sat, 09 Sep 2023 00:00:00 +0000 + + + 定时任务可视化管理 + http://localhost:1313/iblog/posts/essays/scheduled-job/ + Sat, 09 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/scheduled-job/ + 代码实现 代码结构 pom &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-quartz&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;/dependency&gt; 库表结构 -- ---------------------------- -- 定时任务调度表 -- ---------------------------- drop table if exists sys_job; create table sys_job ( job_id bigint(20) not null auto_increment comment &#39;任务ID&#39;, job_name varchar(64) default &#39;&#39; comment &#39;任务名称&#39;, job_group varchar(64) default &#39;DEFAULT&#39; comment &#39;任务组名&#39;, invoke_target varchar(500) not null comment + + + 整合文件上传功能 + http://localhost:1313/iblog/posts/essays/uploadfile-code/ + Fri, 11 Aug 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/uploadfile-code/ + 结构 pom.xml fastdfs-client-java-1.27.jar:点击下载 &lt;dependencies&gt; &lt;!-- fastdfs --&gt; &lt;dependency&gt; &lt;groupId&gt;org.csource&lt;/groupId&gt; &lt;artifactId&gt;fastdfs-client-java&lt;/artifactId&gt; &lt;version&gt;1.27&lt;/version&gt; &lt;systemPath&gt;${project.basedir}/lib/fastdfs-client-java-1.27.jar&lt;/systemPath&gt; &lt;scope&gt;system&lt;/scope&gt; &lt;/dependency&gt; &lt;!--aliyun oss 依赖--&gt; &lt;dependency&gt; &lt;groupId&gt;com.aliyun.oss&lt;/groupId&gt; &lt;artifactId&gt;aliyun-sdk-oss&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;5.8.11&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;commons-io&lt;/groupId&gt; &lt;artifactId&gt;commons-io&lt;/artifactId&gt; &lt;version&gt;2.11.0&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; application.yml server: port: 80 公共部分 FileManagement public interface FileManagement { /** * 设置下一个bean的对象 * * @param nextFileManagement 下一个 + + + 整合支付功能 + http://localhost:1313/iblog/posts/essays/pay-code/ + Thu, 10 Aug 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/pay-code/ + 结构 pom.xml &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;5.8.11&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.alipay.sdk&lt;/groupId&gt; &lt;artifactId&gt;alipay-sdk-java&lt;/artifactId&gt; &lt;version&gt;4.9.9&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.github.binarywang&lt;/groupId&gt; &lt;artifactId&gt;weixin-java-pay&lt;/artifactId&gt; &lt;version&gt;4.5.0&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; application.yml server: port: 8080 pay: wechat: #微信公众号或者小程序等的appid appId: &#34;&#34; #微信支付商户号 mchId: &#34;&#34; #微信支付商户密钥 mchKey: &#34;&#34; #服务商模式下的子商户公众账号ID subAppId: #服务商模式下的子商户号 subMchId: # p12证书的位 + + + Validator参数校验 + http://localhost:1313/iblog/posts/essays/springboot-validator/ + Sat, 01 Jul 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/springboot-validator/ + 常见参数校验 在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,最简单就是用if条件语句来判断,但是随着参数越来越多,业务越来越复杂,判断参数代码语句显得尤为冗长. 或者有些程序会将if封装起来,例如spring中 + + + 管道流设计模式结合业务 + http://localhost:1313/iblog/posts/essays/pipeline-business/ + Thu, 15 Jun 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/pipeline-business/ + 流程图 代码实现 pom &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.plugin&lt;/groupId&gt; &lt;artifactId&gt;spring-plugin-core&lt;/artifactId&gt; &lt;version&gt;${spring.plugin.core.version}&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;${hutool.version}&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; context EventContext public interface EventContext { /** * 是否继续调用链 */ boolean continueChain(); /** * 获取当前过滤器选择器 */ FilterSelector getFilterSelector(); } BizType public interface BizType { /** * 获取业务类型码值 */ Integer getCode(); /** * 业务类型名称 * */ String getName(); } AbstractEventContext public abstract class AbstractEventContext implements EventContext{ private final BizType businessType; private final FilterSelector filterSelector; protected AbstractEventContext(BizType businessType, FilterSelector filterSelector) { + + + diff --git "a/blog-site/public/tags/\346\212\200\345\267\247/index.html" "b/blog-site/public/tags/\346\212\200\345\267\247/index.html" new file mode 100644 index 00000000..ede00267 --- /dev/null +++ "b/blog-site/public/tags/\346\212\200\345\267\247/index.html" @@ -0,0 +1,220 @@ + + + + + + + + + + + 技巧 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2023
+
+ +
+
+ 如何减少及解决bug +
03-10
+
+
+ +
+ +
2022
+
+ + + +
+ +
2021
+
+ + + +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\346\212\200\345\267\247/index.xml" "b/blog-site/public/tags/\346\212\200\345\267\247/index.xml" new file mode 100644 index 00000000..71ecf75c --- /dev/null +++ "b/blog-site/public/tags/\346\212\200\345\267\247/index.xml" @@ -0,0 +1,33 @@ + + + + 技巧 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E6%8A%80%E5%B7%A7/ + Recent content in 技巧 on 唯手熟尔 + Hugo -- gohugo.io + zh + Fri, 10 Mar 2023 00:00:00 +0000 + + + 如何减少及解决bug + http://localhost:1313/iblog/posts/essays/java-bugs/ + Fri, 10 Mar 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-bugs/ + bug的起源: 1945年,一只小飞蛾钻进了计算机电路里,导致系统无法工作,一位名叫格蕾丝·赫柏的人把飞蛾拍死在工作日志上,写道:就是这个 bug(虫子),害我们今天的工作无法完成——于是,bug一词成了电脑系统程序的专业术语,形容那些系统中的 + + + IDEA常用配置及使用技巧 + http://localhost:1313/iblog/posts/essays/dev-idea/ + Fri, 16 Dec 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/dev-idea/ + 下载 工欲善其事必先利其器,一个好的开发工具,能极大提高开发效率. 新UI很漂亮。IDEA 2022.2.3 官方下载地址: https://www.jetbrains.com/zh-cn/idea/download/other.html 激活工具 百度云下载. 链接:https://pan.baidu.com/s/19sCUTCBXvwXgEQc8vX-SYQ?pwd=gwu + + + 常见故障排查及程序配置 + http://localhost:1313/iblog/posts/essays/eye-beam/ + Wed, 08 Sep 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/eye-beam/ + 故障排查基础 收录Linux常用命令,以下命令来自https://www.bilibili.com/video/BV14A411378a 关机/重启/注销 常用命令 作用 shutdown -h now 即刻关机 shutdown -h 10 10分钟后关机 shutdown -h 11:00 11:00关机 shutdown -h +10 预定时间关机(10 + + + diff --git "a/blog-site/public/tags/\346\261\202\350\201\214/index.html" "b/blog-site/public/tags/\346\261\202\350\201\214/index.html" new file mode 100644 index 00000000..1f47b7ae --- /dev/null +++ "b/blog-site/public/tags/\346\261\202\350\201\214/index.html" @@ -0,0 +1,239 @@ + + + + + + + + + + + 求职 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2023
+
+ +
+
+ 20230915简历 +
09-15
+
+
+ +
+ +
2022
+
+ +
+
+ 20220422简历 +
04-22
+
+
+ +
+ +
2021
+
+ + + +
+
+ 面试中常见的问题 +
04-23
+
+
+ +
+ +
2020
+
+ +
+
+ 20201124简历 +
11-24
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\346\261\202\350\201\214/index.xml" "b/blog-site/public/tags/\346\261\202\350\201\214/index.xml" new file mode 100644 index 00000000..c53c8fcc --- /dev/null +++ "b/blog-site/public/tags/\346\261\202\350\201\214/index.xml" @@ -0,0 +1,47 @@ + + + + 求职 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E6%B1%82%E8%81%8C/ + Recent content in 求职 on 唯手熟尔 + Hugo -- gohugo.io + zh + Fri, 15 Sep 2023 00:00:00 +0000 + + + 20230915简历 + http://localhost:1313/iblog/posts/resume/interview-resume-20230915/ + Fri, 15 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-resume-20230915/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和四年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对Jav + + + 20220422简历 + http://localhost:1313/iblog/posts/resume/interview-resume-20220422/ + Fri, 22 Apr 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-resume-20220422/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和三年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对SQL + + + 面试Java可能会被问到的问题 + http://localhost:1313/iblog/posts/resume/interview-junior-javaer/ + Tue, 11 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-junior-javaer/ + 面试必问 自我介绍一下 你有什么职业规划 你为什么要离职 说一下你的优缺点 你的期望薪资是多少 你为什么要选择我们公司 你能否接受加班 你有对象了吗 你还有什么问题要问的吗 基础 说一下UDP、TCP及http与https 如何保证线程安全 线程池工作原理 如何避免死 + + + 面试中常见的问题 + http://localhost:1313/iblog/posts/resume/interview-questions-and-answers/ + Fri, 23 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-questions-and-answers/ + 面试常见问题 自我介绍 个人经历可以进行适当包装,但是不能造假,一方面如果一旦被人拆穿,后果就不用我说了吧,另一方面如果你说谎,说了一些你自己不感兴趣的项目,在入职之后可能会被分配到该项目上; 在介绍的时候要说明你对面试的公司有什么用,根据不同类 + + + 20201124简历 + http://localhost:1313/iblog/posts/resume/interview-resume-20201124/ + Tue, 24 Nov 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-resume-20201124/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 专业技能 熟练使用 SSM,SpringBoot等框架技术; 熟练使用HTML,CSS等相关技术; 有Redis,VUE相关使用经验; 有对接第三方系统,调用外系统相关经验; 熟悉 MySQL,ORACLE.基本操作,熟练使 + + + diff --git "a/blog-site/public/tags/\347\216\251\345\205\267/index.html" "b/blog-site/public/tags/\347\216\251\345\205\267/index.html" new file mode 100644 index 00000000..d4fdbbdd --- /dev/null +++ "b/blog-site/public/tags/\347\216\251\345\205\267/index.html" @@ -0,0 +1,264 @@ + + + + + + + + + + + 玩具 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2022
+
+ +
+
+ Java小程序集合 +
04-09
+
+
+ +
+ +
2018
+
+ +
+
+ Js雪花飘落 +
12-25
+
+
+ +
+
+ Js下雨特效 +
12-10
+
+
+ +
+
+ Js换肤特效 +
11-14
+
+
+ +
+
+ Js折纸导航栏 +
10-25
+
+
+ +
+
+ Js表白神器 +
10-14
+
+
+ +
+
+ Js懒加载 +
09-21
+
+
+ +
+
+ Js五子棋 +
09-10
+
+
+ +
+
+ Js滑块拖拽 +
09-08
+
+
+ +
+
+ Js生日礼物 +
08-24
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\347\216\251\345\205\267/index.xml" "b/blog-site/public/tags/\347\216\251\345\205\267/index.xml" new file mode 100644 index 00000000..2c067921 --- /dev/null +++ "b/blog-site/public/tags/\347\216\251\345\205\267/index.xml" @@ -0,0 +1,82 @@ + + + + 玩具 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E7%8E%A9%E5%85%B7/ + Recent content in 玩具 on 唯手熟尔 + Hugo -- gohugo.io + zh + Sat, 09 Apr 2022 00:00:00 +0000 + + + Java小程序集合 + http://localhost:1313/iblog/posts/toy/java-multi-gadget/ + Sat, 09 Apr 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/java-multi-gadget/ + 写在前面 本文中所涉及的程序均为Java开发,如果您想要直接使用这些工具需要提前配置Java环境。所涉及到的程序均提供完整代码,如果您有兴趣可以尝试运行。 使用java -jar命令启动 某些程序功能并不是很完善,但是也可以凑合着用,写这些程序的主 + + + Js雪花飘落 + http://localhost:1313/iblog/posts/toy/js-snow/ + Tue, 25 Dec 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-snow/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;snow&lt;/title&gt; &lt;/head&gt; &lt;style&gt; html { width: 100%; } body { margin: 0; padding: 0; overflow-y: hidden; width: 100%; } .header { width: 100%; height: 315px; background: url(&#34;images/header-bg.png&#34;) repeat; } .snow { position: relative; height: inherit; width: 960px; background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) no-repeat 0 0;; margin: 0 auto; animation: auto 10s linear infinite; } /* 下雪动画 插入两个背景图片*/ @keyframes auto { from { background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) repeat 0 0; } to { background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) repeat 0 1000px; } } tree, snow { position: absolute; } tree { width: 112px; height: 137px; background: url(&#34;images/tree.png&#34;); + + + Js下雨特效 + http://localhost:1313/iblog/posts/toy/js-rain/ + Mon, 10 Dec 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-rain/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt; rain &lt;/title&gt; &lt;style&gt; html { width: 100%; } body { width: 100%; margin: 0; padding: 0; background-color: #000; } .rain { display: block; } embed { display: block; } &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;!-- 2、使用hidden=&#34;true&#34;表示隐藏音乐播放按钮,相反使用hidden=&#34;false&#34;表示开启音乐播放按钮。 3、使用a + + + Js换肤特效 + http://localhost:1313/iblog/posts/toy/js-trans-skin/ + Wed, 14 Nov 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-trans-skin/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;换肤特效&lt;/title&gt; &lt;style type=&#34;text/css&#34;&gt; body { margin: 0; background-image: url(&#34;images/1.jpg&#34;); background-size: cover; } ul { margin: 0; padding: 0; list-style-type: none; } .bg-list { display: none; margin: 0; width: 100%; height: 200px; background: rgba(0, 0, 0, 0.5); } .img-wrap { height: 200px; display: flex; justify-content: space-around; align-items: center; } .tab-btn { background-image: url(&#34;images/upseek.png&#34;); height: 50px; width: 50px; position: fixed; top: 0; right: 0; } .tab-btn:hover { background-position-y: -63.6px; } &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;div class=&#34;bg-list&#34;&gt; &lt;ul class=&#34;img-wrap&#34;&gt; &lt;li class=&#34;img-item&#34; data-src=&#34;images/1.jpg&#34;&gt; &lt;img src=&#34;images/1-1.jpg&#34; width=&#34;160px&#34;/&gt; &lt;/li&gt; + + + Js折纸导航栏 + http://localhost:1313/iblog/posts/toy/js-paper-folding/ + Thu, 25 Oct 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-paper-folding/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;折纸导航栏&lt;/title&gt; &lt;/head&gt; &lt;style&gt; *{ margin: 0; padding: 0; } .content{ position: relative; width: 400px; height: 30px; margin: 50px auto; /*-webkit-perspective: 1000px; -moz-perspective: 1000px; -ms-perspective: 1000px;*/ perspective: 1000px;/*景深相当于眼睛距离元素的位置距离*/ } .content .open{ transform: rotateX(0); animation: open 1s linear; } @keyframes open { 0%{ transform: rotateX(-90deg); } 20%{ transform:rotateX(30deg); } 40%{ transform:rotateX(-60deg); } 60%{ transform:rotateX(60deg); } 80%{ transform:rotateX(-30deg); + + + Js表白神器 + http://localhost:1313/iblog/posts/toy/js-love-heart/ + Sun, 14 Oct 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-love-heart/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;love&lt;/title&gt; &lt;style&gt; *{ margin: 0; padding: 0; } body{ background-color: #000; background-size: cover; overflow-y: hidden; } .love{ width: 400px; height: 400px; /*background-color: #7c7c7c;*/ margin: 130px auto; animation: move 1s infinite alternate; } @keyframes move { 100%{ transform: scale(1.5); } } .left{ float: left; width: 150px; height: 250px; background-color: #FF0000; border-radius: 75px 75px 0 5px; -webkit-transform: rotate(-45deg); -moz-transform: rotate(-45deg); -ms-transform: rotate(-45deg); -o-transform: rotate(-45deg); transform: rotate(-45deg); margin-left: 85px; box-shadow: 0 0 20px #FF0000; animation: shadow 1s infinite alternate; } @keyframes shadow { 100%{ box-shadow: 0 0 100px #FF0000; } } .right{ float: left; width: 150px; height: 250px; background-color: #FF0000; border-radius: 75px 75px 5px 0; -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -ms-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg); + + + Js懒加载 + http://localhost:1313/iblog/posts/toy/js-loadding-lazy/ + Fri, 21 Sep 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-loadding-lazy/ + index.html &lt;!doctype html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;meta name=&#34;Generator&#34; content=&#34;EditPlus®&#34;&gt; &lt;meta name=&#34;Author&#34; content=&#34;&#34;&gt; &lt;meta name=&#34;Keywords&#34; content=&#34;&#34;&gt; &lt;meta name=&#34;Description&#34; content=&#34;&#34;&gt; &lt;title&gt;懒加载技术&lt;/title&gt; &lt;style&gt; *{ margin: 0; padding:0; } body{ background: rgb(0,0,0); } .box{ overflow: hidden; width: 948px; background-color: #7c7c7c; margin: 50px auto; -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; } .box img{ float: left; display: block; width: 300px; height: 150px; margin: + + + Js五子棋 + http://localhost:1313/iblog/posts/toy/js-gomoku/ + Mon, 10 Sep 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-gomoku/ + index.html &lt;!DOCTYPE html PUBLIC &#34;-//W3C//DTD XHTML 1.0 Transitional//EN&#34; &#34;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&#34;&gt; &lt;html xmlns=&#34;http://www.w3.org/1999/xhtml&#34;&gt; &lt;head&gt; &lt;meta http-equiv=&#34;Content-Type&#34; content=&#34;text/html; charset=UTF-8&#34; /&gt; &lt;title&gt;五子棋&lt;/title&gt; &lt;meta name=&#34;viewport&#34; content=&#34;device-width; initial-scale=1.0;&#34; /&gt; &lt;style&gt; #c1 { display: block; margin: 60px auto; box-shadow: 1px 1px 5px #000000; } &lt;/style&gt; &lt;script src=&#34;js/index.js&#34;&gt;&lt;/script&gt; &lt;/head&gt; &lt;body&gt; &lt;canvas id=&#34;c1&#34; width=&#34;450px&#34; height=&#34;450px&#34;&gt;&lt;/canvas&gt; &lt;/body&gt; &lt;/html&gt; index.js window.onload = function(){ var oC = document.getElementById(&#39;c1&#39;); var oGc = oC.getContext(&#39;2d&#39;); var over = false; oGc.strokeStyle = &#34;#bfbfbf&#34;; //绘制棋盘 for(var i=0;i&lt;15;i++){ oGc.moveTo(15+i*30,15); oGc.lineTo(15+i*30,435); oGc.stroke(); oGc.moveTo(15,15+i*30); oGc.lineTo(435,15+i*30); oGc.stroke(); } /* AI难点解析 赢法 + + + Js滑块拖拽 + http://localhost:1313/iblog/posts/toy/js-box-drag/ + Sat, 08 Sep 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-box-drag/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;滑块拖拽&lt;/title&gt; &lt;/head&gt; &lt;style&gt; body { margin: 0; padding: 0; user-select: none; } .content { position: relative; width: 300px; height: 40px; margin: 50px auto; background-color: #E8E8EB; text-align: center; line-height: 40px; } .rect { position: absolute; width: 100%; height: 100%; } .rect .bg { position: absolute; left: 0; top: 0; z-index: 1; width: 0; height: 100%; background: rgba(122,194,60,.4); } .rect .move { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; width: 45px; height: 40px; position: absolute; top: 0; left: 0; background-color: #fff; border: 1px solid #cccccc; + + + Js生日礼物 + http://localhost:1313/iblog/posts/toy/js-birthday-gift/ + Fri, 24 Aug 2018 00:00:00 +0000 + http://localhost:1313/iblog/posts/toy/js-birthday-gift/ + index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;card&lt;/title&gt; &lt;style&gt; body,html{ width: 100%; height: 100%; } body{ display: flex;/*弹性盒模型*/ justify-content: center;/*水平对齐 盒子位于中心*/ align-items: center;/*竖直对齐 居中对齐*/ background-color: yellow; perspective: 1000px;/*景深:眼到屏幕的距离*/ } body,h1,p{ margin: 0; } .card{ width: 520px; height: 350px; border-radius: 15px; background: linear-gradient(#020333 70%,#fff 75%);/* + + + diff --git "a/blog-site/public/tags/\347\256\200\345\216\206/index.html" "b/blog-site/public/tags/\347\256\200\345\216\206/index.html" new file mode 100644 index 00000000..1b12fc03 --- /dev/null +++ "b/blog-site/public/tags/\347\256\200\345\216\206/index.html" @@ -0,0 +1,220 @@ + + + + + + + + + + + 简历 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2023
+
+ +
+
+ 20230915简历 +
09-15
+
+
+ +
+ +
2022
+
+ +
+
+ 20220422简历 +
04-22
+
+
+ +
+ +
2020
+
+ +
+
+ 20201124简历 +
11-24
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\347\256\200\345\216\206/index.xml" "b/blog-site/public/tags/\347\256\200\345\216\206/index.xml" new file mode 100644 index 00000000..1cfe1567 --- /dev/null +++ "b/blog-site/public/tags/\347\256\200\345\216\206/index.xml" @@ -0,0 +1,33 @@ + + + + 简历 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E7%AE%80%E5%8E%86/ + Recent content in 简历 on 唯手熟尔 + Hugo -- gohugo.io + zh + Fri, 15 Sep 2023 00:00:00 +0000 + + + 20230915简历 + http://localhost:1313/iblog/posts/resume/interview-resume-20230915/ + Fri, 15 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-resume-20230915/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和四年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对Jav + + + 20220422简历 + http://localhost:1313/iblog/posts/resume/interview-resume-20220422/ + Fri, 22 Apr 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-resume-20220422/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和三年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对SQL + + + 20201124简历 + http://localhost:1313/iblog/posts/resume/interview-resume-20201124/ + Tue, 24 Nov 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/resume/interview-resume-20201124/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 专业技能 熟练使用 SSM,SpringBoot等框架技术; 熟练使用HTML,CSS等相关技术; 有Redis,VUE相关使用经验; 有对接第三方系统,调用外系统相关经验; 熟悉 MySQL,ORACLE.基本操作,熟练使 + + + diff --git "a/blog-site/public/tags/\347\256\227\346\263\225/index.html" "b/blog-site/public/tags/\347\256\227\346\263\225/index.html" new file mode 100644 index 00000000..48e2a012 --- /dev/null +++ "b/blog-site/public/tags/\347\256\227\346\263\225/index.html" @@ -0,0 +1,196 @@ + + + + + + + + + + + 算法 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2021
+
+ +
+
+ 数据结构与算法 +
12-10
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\347\256\227\346\263\225/index.xml" "b/blog-site/public/tags/\347\256\227\346\263\225/index.xml" new file mode 100644 index 00000000..41dba418 --- /dev/null +++ "b/blog-site/public/tags/\347\256\227\346\263\225/index.xml" @@ -0,0 +1,19 @@ + + + + 算法 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E7%AE%97%E6%B3%95/ + Recent content in 算法 on 唯手熟尔 + Hugo -- gohugo.io + zh + Fri, 10 Dec 2021 00:00:00 +0000 + + + 数据结构与算法 + http://localhost:1313/iblog/posts/essays/data-structures-algorithms/ + Fri, 10 Dec 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/data-structures-algorithms/ + 数据结构 数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好数据结构可以编写出更有效率的代码。数据结构是算法的基础,想要学好算法,就必须把数据结构学到位。 数据结构包括:线性结构、非线性结构。 线性结构作为最常用的数据结构, + + + diff --git "a/blog-site/public/tags/\347\272\277\347\250\213/index.html" "b/blog-site/public/tags/\347\272\277\347\250\213/index.html" new file mode 100644 index 00000000..b974947a --- /dev/null +++ "b/blog-site/public/tags/\347\272\277\347\250\213/index.html" @@ -0,0 +1,217 @@ + + + + + + + + + + + 线程 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2020
+
+ +
+
+ 线程状态及创建方式 +
04-20
+
+
+ +
+
+ Java中常用到的锁 +
04-07
+
+
+ + + +
+
+ CAS原理 +
04-04
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\347\272\277\347\250\213/index.xml" "b/blog-site/public/tags/\347\272\277\347\250\213/index.xml" new file mode 100644 index 00000000..09659445 --- /dev/null +++ "b/blog-site/public/tags/\347\272\277\347\250\213/index.xml" @@ -0,0 +1,40 @@ + + + + 线程 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E7%BA%BF%E7%A8%8B/ + Recent content in 线程 on 唯手熟尔 + Hugo -- gohugo.io + zh + Mon, 20 Apr 2020 00:00:00 +0000 + + + 线程状态及创建方式 + http://localhost:1313/iblog/posts/essays/thread-state-and-created/ + Mon, 20 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/thread-state-and-created/ + 线程状态及转换 线程状态共包含6种,6中状态又可以互相的转换。 新建状态(New): 创建了线程后尚未启动; 可运行状态(Runnable): 可能正在运行,也可能正在等待 CPU 时间片。包含了运行中(Running)和 就绪(Ready)状态; 就绪(Rea + + + Java中常用到的锁 + http://localhost:1313/iblog/posts/essays/java-lock/ + Tue, 07 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-lock/ + 公平锁 指多个线程按照申请锁的顺序来获取锁类似排队打饭 先来后到 优点: 所有的线程都能得到资源,不会饿死在队列中。 缺点: 吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。 非公平锁 指在多线程获取锁的顺序并 + + + Java中集合的线程不安全问题 + http://localhost:1313/iblog/posts/essays/java-thread-collection/ + Sun, 05 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-thread-collection/ + ArrayList ArrayList线程不安全示例: public static void main(String[] args) { ArrayList&lt;String&gt; arrayList = new ArrayList&lt;&gt;(); for(int i=0; i&lt; 3; i++) { new Thread(() -&gt; { arrayList.add(UUID.randomUUID().toString()); System.out.println(arrayList); },String.valueOf(i)).start(); } } // ConcurrentModificationException 同步修改异常 Exception in thread &#34;8&#34; java.util.ConcurrentModificationException [null, 2041b613-8068-4ddd-9d01-305f5680d377] [null, 2041b613-8068-4ddd-9d01-305f5680d377, b3e0296d-e263-4632-a023-4267cdec5e25] [null, 2041b613-8068-4ddd-9d01-305f5680d377] 原因分析: 当某个线程正在执行 add()方法时,被某个线程打断,添加到一半被打断,没有被添加完 解决方案: 使用Vec + + + CAS原理 + http://localhost:1313/iblog/posts/essays/cas-principle/ + Sat, 04 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/cas-principle/ + CAS CAS全称为Compare and Swap被译为比较并交换。是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。 java.util.concurrent.atomic 并发包下的所有原子类都是基于 CAS 来实现的。 以 AtomicInteger 原子整型类为例。 public class MainTest { public static void main(String[] args) { new AtomicInteger().compareAndSet(1,2); } } 以上面的代码为例 + + + diff --git "a/blog-site/public/tags/\347\275\221\347\273\234/index.html" "b/blog-site/public/tags/\347\275\221\347\273\234/index.html" new file mode 100644 index 00000000..3ca43597 --- /dev/null +++ "b/blog-site/public/tags/\347\275\221\347\273\234/index.html" @@ -0,0 +1,196 @@ + + + + + + + + + + + 网络 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2021
+
+ +
+
+ 网络编程 +
11-19
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\347\275\221\347\273\234/index.xml" "b/blog-site/public/tags/\347\275\221\347\273\234/index.xml" new file mode 100644 index 00000000..64576f9e --- /dev/null +++ "b/blog-site/public/tags/\347\275\221\347\273\234/index.xml" @@ -0,0 +1,19 @@ + + + + 网络 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E7%BD%91%E7%BB%9C/ + Recent content in 网络 on 唯手熟尔 + Hugo -- gohugo.io + zh + Fri, 19 Nov 2021 00:00:00 +0000 + + + 网络编程 + http://localhost:1313/iblog/posts/essays/net-program-java/ + Fri, 19 Nov 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/net-program-java/ + 网络协议 以下内容摘自百度百科: https://baike.baidu.com/item/网络协议/328636 https://baike.baidu.com/item/网络七层协议/6056879 网络协议指的是计算机网络中互相通信的对等实体 + + + diff --git "a/blog-site/public/tags/\350\256\276\350\256\241/index.html" "b/blog-site/public/tags/\350\256\276\350\256\241/index.html" new file mode 100644 index 00000000..57330bc2 --- /dev/null +++ "b/blog-site/public/tags/\350\256\276\350\256\241/index.html" @@ -0,0 +1,262 @@ + + + + + + + + + + + 设计 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2023
+
+ +
+
+ 定时任务可视化管理 +
09-09
+
+
+ +
+
+ 整合文件上传功能 +
08-11
+
+
+ +
+
+ 整合支付功能 +
08-10
+
+
+ + + +
+
+ 重构一个程序 +
04-20
+
+
+ +
+ +
2022
+
+ +
+
+ 接口优化 +
12-20
+
+
+ +
+
+ 整洁的代码 +
09-01
+
+
+ + + +
+ +
2021
+
+ + + +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\350\256\276\350\256\241/index.xml" "b/blog-site/public/tags/\350\256\276\350\256\241/index.xml" new file mode 100644 index 00000000..efaf2c01 --- /dev/null +++ "b/blog-site/public/tags/\350\256\276\350\256\241/index.xml" @@ -0,0 +1,75 @@ + + + + 设计 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E8%AE%BE%E8%AE%A1/ + Recent content in 设计 on 唯手熟尔 + Hugo -- gohugo.io + zh + Sat, 09 Sep 2023 00:00:00 +0000 + + + 定时任务可视化管理 + http://localhost:1313/iblog/posts/essays/scheduled-job/ + Sat, 09 Sep 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/scheduled-job/ + 代码实现 代码结构 pom &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-quartz&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;/dependency&gt; 库表结构 -- ---------------------------- -- 定时任务调度表 -- ---------------------------- drop table if exists sys_job; create table sys_job ( job_id bigint(20) not null auto_increment comment &#39;任务ID&#39;, job_name varchar(64) default &#39;&#39; comment &#39;任务名称&#39;, job_group varchar(64) default &#39;DEFAULT&#39; comment &#39;任务组名&#39;, invoke_target varchar(500) not null comment + + + 整合文件上传功能 + http://localhost:1313/iblog/posts/essays/uploadfile-code/ + Fri, 11 Aug 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/uploadfile-code/ + 结构 pom.xml fastdfs-client-java-1.27.jar:点击下载 &lt;dependencies&gt; &lt;!-- fastdfs --&gt; &lt;dependency&gt; &lt;groupId&gt;org.csource&lt;/groupId&gt; &lt;artifactId&gt;fastdfs-client-java&lt;/artifactId&gt; &lt;version&gt;1.27&lt;/version&gt; &lt;systemPath&gt;${project.basedir}/lib/fastdfs-client-java-1.27.jar&lt;/systemPath&gt; &lt;scope&gt;system&lt;/scope&gt; &lt;/dependency&gt; &lt;!--aliyun oss 依赖--&gt; &lt;dependency&gt; &lt;groupId&gt;com.aliyun.oss&lt;/groupId&gt; &lt;artifactId&gt;aliyun-sdk-oss&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;5.8.11&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;commons-io&lt;/groupId&gt; &lt;artifactId&gt;commons-io&lt;/artifactId&gt; &lt;version&gt;2.11.0&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; application.yml server: port: 80 公共部分 FileManagement public interface FileManagement { /** * 设置下一个bean的对象 * * @param nextFileManagement 下一个 + + + 整合支付功能 + http://localhost:1313/iblog/posts/essays/pay-code/ + Thu, 10 Aug 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/pay-code/ + 结构 pom.xml &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;5.8.11&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.alipay.sdk&lt;/groupId&gt; &lt;artifactId&gt;alipay-sdk-java&lt;/artifactId&gt; &lt;version&gt;4.9.9&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.github.binarywang&lt;/groupId&gt; &lt;artifactId&gt;weixin-java-pay&lt;/artifactId&gt; &lt;version&gt;4.5.0&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; application.yml server: port: 8080 pay: wechat: #微信公众号或者小程序等的appid appId: &#34;&#34; #微信支付商户号 mchId: &#34;&#34; #微信支付商户密钥 mchKey: &#34;&#34; #服务商模式下的子商户公众账号ID subAppId: #服务商模式下的子商户号 subMchId: # p12证书的位 + + + 管道流设计模式结合业务 + http://localhost:1313/iblog/posts/essays/pipeline-business/ + Thu, 15 Jun 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/pipeline-business/ + 流程图 代码实现 pom &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.plugin&lt;/groupId&gt; &lt;artifactId&gt;spring-plugin-core&lt;/artifactId&gt; &lt;version&gt;${spring.plugin.core.version}&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;${hutool.version}&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; context EventContext public interface EventContext { /** * 是否继续调用链 */ boolean continueChain(); /** * 获取当前过滤器选择器 */ FilterSelector getFilterSelector(); } BizType public interface BizType { /** * 获取业务类型码值 */ Integer getCode(); /** * 业务类型名称 * */ String getName(); } AbstractEventContext public abstract class AbstractEventContext implements EventContext{ private final BizType businessType; private final FilterSelector filterSelector; protected AbstractEventContext(BizType businessType, FilterSelector filterSelector) { + + + 重构一个程序 + http://localhost:1313/iblog/posts/essays/java-project-reconstitution/ + Thu, 20 Apr 2023 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-project-reconstitution/ + 什么是重构 摘自《重构:改善既有代码的设计》 重构(名词形式): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。 重构(动词形式): 使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构。 重 + + + 接口优化 + http://localhost:1313/iblog/posts/essays/java-improve/ + Tue, 20 Dec 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-improve/ + 接口优化 线上接口很慢,线上生产问题,我们绝对不能马虎放过抱着侥幸心理,必须要找到根本原因及时处理,防止下次留下更大的坑.大致思路要定位接口问题,然后具体问题具体分析,讨论不同解决方案. 定位问题 要快速定位接口哪一个环节比较慢,性能瓶颈在哪里, + + + 整洁的代码 + http://localhost:1313/iblog/posts/essays/clean-code/ + Thu, 01 Sep 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/clean-code/ + 为什么要写整洁的代码 为什么要写整洁的代码,回答这个问题之前,也许应该想想写糟糕的代码的原因 是想快点完成吗?还是要赶时间吗?有可能.或许你觉得自己要干好所需要的时间不够;假使花时间清理代码,老板就会大发雷霆.或许你只是不耐烦再搞这套程序,期望 + + + 如何做好程序设计功能 + http://localhost:1313/iblog/posts/essays/java-design/ + Tue, 02 Aug 2022 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-design/ + 产品需求澄清、PN排期及任务分解 开发设计评审 功能设计流程图 与外部系统交互、本系统模块之间流程,比较好用的画圈软件draw .io或在线的process on 数据库设计 从DDD角度界限上下文、ER图、评审表结构设计是否合理,表的关联关系是否合理、是 + + + 规范编写Java代码总结 + http://localhost:1313/iblog/posts/essays/java-code-rule/ + Thu, 25 Nov 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-code-rule/ + 编码规范 我们为什么要遵守规范来编码? 是因为通常在编码过程中我们不只自己进行开发,通常需要一个团队来进行,开发好之后还需要维护,所以编码规范就显的尤为重要。 代码维护时间比较长,那么保证代码可读性就显得很重要。作为一个程序员,咱们得有点追求和信 + + + diff --git "a/blog-site/public/tags/\350\275\254\350\275\275/index.html" "b/blog-site/public/tags/\350\275\254\350\275\275/index.html" new file mode 100644 index 00000000..beeb8b24 --- /dev/null +++ "b/blog-site/public/tags/\350\275\254\350\275\275/index.html" @@ -0,0 +1,196 @@ + + + + + + + + + + + 转载 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2021
+
+ +
+
+ Java语法糖 +
04-10
+
+
+ +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\350\275\254\350\275\275/index.xml" "b/blog-site/public/tags/\350\275\254\350\275\275/index.xml" new file mode 100644 index 00000000..2790b050 --- /dev/null +++ "b/blog-site/public/tags/\350\275\254\350\275\275/index.xml" @@ -0,0 +1,19 @@ + + + + 转载 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E8%BD%AC%E8%BD%BD/ + Recent content in 转载 on 唯手熟尔 + Hugo -- gohugo.io + zh + Sat, 10 Apr 2021 00:00:00 +0000 + + + Java语法糖 + http://localhost:1313/iblog/posts/essays/java-syntax-sugar/ + Sat, 10 Apr 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-syntax-sugar/ + 原文地址:https://www.jianshu.com/p/0f967298a5d7 语法糖 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法, 这种语法对语言的功能并没有 + + + diff --git "a/blog-site/public/tags/\351\233\206\345\220\210/index.html" "b/blog-site/public/tags/\351\233\206\345\220\210/index.html" new file mode 100644 index 00000000..e1f86fb8 --- /dev/null +++ "b/blog-site/public/tags/\351\233\206\345\220\210/index.html" @@ -0,0 +1,208 @@ + + + + + + + + + + + 集合 | 唯手熟尔 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+

+ +

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ 访问量 +
+
+ 访客数 +
+
+ +
+
+
+
+
+ +
2021
+
+ +
+
+ HashMap详解 +
05-03
+
+
+ +
+ +
2020
+
+ + + +
+ +
+ +
+
+
+ + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git "a/blog-site/public/tags/\351\233\206\345\220\210/index.xml" "b/blog-site/public/tags/\351\233\206\345\220\210/index.xml" new file mode 100644 index 00000000..e7fddd4d --- /dev/null +++ "b/blog-site/public/tags/\351\233\206\345\220\210/index.xml" @@ -0,0 +1,26 @@ + + + + 集合 on 唯手熟尔 + http://localhost:1313/iblog/tags/%E9%9B%86%E5%90%88/ + Recent content in 集合 on 唯手熟尔 + Hugo -- gohugo.io + zh + Mon, 03 May 2021 00:00:00 +0000 + + + HashMap详解 + http://localhost:1313/iblog/posts/essays/java-hashmap/ + Mon, 03 May 2021 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-hashmap/ + 相关概念 capacity: 容量,默认16; loadFactor: 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容; threshold: 阈值;阈值 + + + Java中集合的线程不安全问题 + http://localhost:1313/iblog/posts/essays/java-thread-collection/ + Sun, 05 Apr 2020 00:00:00 +0000 + http://localhost:1313/iblog/posts/essays/java-thread-collection/ + ArrayList ArrayList线程不安全示例: public static void main(String[] args) { ArrayList&lt;String&gt; arrayList = new ArrayList&lt;&gt;(); for(int i=0; i&lt; 3; i++) { new Thread(() -&gt; { arrayList.add(UUID.randomUUID().toString()); System.out.println(arrayList); },String.valueOf(i)).start(); } } // ConcurrentModificationException 同步修改异常 Exception in thread &#34;8&#34; java.util.ConcurrentModificationException [null, 2041b613-8068-4ddd-9d01-305f5680d377] [null, 2041b613-8068-4ddd-9d01-305f5680d377, b3e0296d-e263-4632-a023-4267cdec5e25] [null, 2041b613-8068-4ddd-9d01-305f5680d377] 原因分析: 当某个线程正在执行 add()方法时,被某个线程打断,添加到一半被打断,没有被添加完 解决方案: 使用Vec + + + diff --git a/blog-site/public/zh/index.html b/blog-site/public/zh/index.html new file mode 100644 index 00000000..98223f79 --- /dev/null +++ b/blog-site/public/zh/index.html @@ -0,0 +1,10 @@ + + + + http://localhost:1313/iblog/ + + + + + + diff --git a/blog-site/public/zh/sitemap.xml b/blog-site/public/zh/sitemap.xml new file mode 100644 index 00000000..cd02e45b --- /dev/null +++ b/blog-site/public/zh/sitemap.xml @@ -0,0 +1,382 @@ + + + + http://localhost:1313/iblog/posts/worksummary/work-summary-2023/ + 2023-12-01T00:00:00+00:00 + + http://localhost:1313/iblog/posts/ + 2023-12-01T00:00:00+00:00 + + http://localhost:1313/iblog/tags/ + 2023-12-01T00:00:00+00:00 + + + + http://localhost:1313/iblog/tags/%E5%B7%A5%E4%BD%9C%E6%80%BB%E7%BB%93/ + 2023-12-01T00:00:00+00:00 + + http://localhost:1313/iblog/ + 2023-12-01T00:00:00+00:00 + + + + http://localhost:1313/iblog/posts/resume/interview-resume-20230915/ + 2023-09-15T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E7%AE%80%E5%8E%86/ + 2023-09-15T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E6%B1%82%E8%81%8C/ + 2023-09-15T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/scheduled-job/ + 2023-09-09T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E8%AE%BE%E8%AE%A1/ + 2023-09-09T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E5%BA%94%E7%94%A8/ + 2023-09-09T00:00:00+00:00 + + http://localhost:1313/iblog/tags/nacos/ + 2023-09-04T00:00:00+00:00 + + http://localhost:1313/iblog/tags/springboot/ + 2023-09-04T00:00:00+00:00 + + http://localhost:1313/iblog/posts/spring/springboot-nacos/ + 2023-09-04T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/uploadfile-code/ + 2023-08-11T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/pay-code/ + 2023-08-10T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/springboot-validator/ + 2023-07-01T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/pipeline-business/ + 2023-06-15T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/java-project-reconstitution/ + 2023-04-20T00:00:00+00:00 + + http://localhost:1313/iblog/tags/java/ + 2023-04-14T00:00:00+00:00 + + http://localhost:1313/iblog/tags/spring/ + 2023-04-14T00:00:00+00:00 + + http://localhost:1313/iblog/posts/spring/java-spring-mvc-webflux/ + 2023-04-14T00:00:00+00:00 + + http://localhost:1313/iblog/tags/mysql/ + 2023-03-13T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/sql-select-fast/ + 2023-03-13T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E6%8A%80%E5%B7%A7/ + 2023-03-10T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/java-bugs/ + 2023-03-10T00:00:00+00:00 + + http://localhost:1313/iblog/tags/elasticsearch/ + 2023-02-14T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/elasticsearch/ + 2023-02-14T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/java-dict/ + 2023-02-13T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/java-improve/ + 2022-12-20T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E4%B9%A6%E7%B1%8D/ + 2022-12-19T00:00:00+00:00 + + http://localhost:1313/iblog/posts/books/books-sunzibingfa/ + 2022-12-19T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/dev-idea/ + 2022-12-16T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/clean-code/ + 2022-09-01T00:00:00+00:00 + + http://localhost:1313/iblog/posts/books/java-books/ + 2022-08-08T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/java-design/ + 2022-08-02T00:00:00+00:00 + + http://localhost:1313/iblog/posts/resume/interview-resume-20220422/ + 2022-04-22T00:00:00+00:00 + + http://localhost:1313/iblog/posts/toy/java-multi-gadget/ + 2022-04-09T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E7%8E%A9%E5%85%B7/ + 2022-04-09T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/data-structures-algorithms/ + 2021-12-10T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E7%AE%97%E6%B3%95/ + 2021-12-10T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/java-code-rule/ + 2021-11-25T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E7%BD%91%E7%BB%9C/ + 2021-11-19T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/net-program-java/ + 2021-11-19T00:00:00+00:00 + + http://localhost:1313/iblog/tags/mq/ + 2021-10-19T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/java-mq/ + 2021-10-19T00:00:00+00:00 + + http://localhost:1313/iblog/tags/java%E5%9F%BA%E7%A1%80/ + 2021-10-04T00:00:00+00:00 + + http://localhost:1313/iblog/posts/java/rookie-java-container/ + 2021-10-04T00:00:00+00:00 + + http://localhost:1313/iblog/posts/java/rookie-reflect/ + 2021-10-02T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/eye-beam/ + 2021-09-08T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E5%88%86%E5%B8%83%E5%BC%8F/ + 2021-08-02T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/java-transaction/ + 2021-08-02T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E4%BA%8B%E5%8A%A1/ + 2021-08-02T00:00:00+00:00 + + http://localhost:1313/iblog/posts/java/rookie-objectclass-methods/ + 2021-07-10T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/java-small-service/ + 2021-06-21T00:00:00+00:00 + + http://localhost:1313/iblog/tags/redis/ + 2021-06-17T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/java-redis/ + 2021-06-17T00:00:00+00:00 + + http://localhost:1313/iblog/posts/spring/java-spring/ + 2021-05-13T00:00:00+00:00 + + http://localhost:1313/iblog/posts/resume/interview-junior-javaer/ + 2021-05-11T00:00:00+00:00 + + http://localhost:1313/iblog/tags/jvm/ + 2021-05-06T00:00:00+00:00 + + http://localhost:1313/iblog/posts/jvm/java-garbage-collector/ + 2021-05-06T00:00:00+00:00 + + http://localhost:1313/iblog/posts/java/rookie-multi-thread/ + 2021-05-05T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/java-hashmap/ + 2021-05-03T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E9%9B%86%E5%90%88/ + 2021-05-03T00:00:00+00:00 + + http://localhost:1313/iblog/posts/jvm/jvm-about/ + 2021-04-27T00:00:00+00:00 + + http://localhost:1313/iblog/posts/resume/interview-questions-and-answers/ + 2021-04-23T00:00:00+00:00 + + http://localhost:1313/iblog/posts/jvm/java-garbage-collection/ + 2021-04-21T00:00:00+00:00 + + http://localhost:1313/iblog/posts/jvm/jvm-execute-engine/ + 2021-04-15T00:00:00+00:00 + + http://localhost:1313/iblog/posts/jvm/jvm-direct-memory/ + 2021-04-14T00:00:00+00:00 + + http://localhost:1313/iblog/posts/jvm/java-object/ + 2021-04-12T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/java-syntax-sugar/ + 2021-04-10T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E8%BD%AC%E8%BD%BD/ + 2021-04-10T00:00:00+00:00 + + http://localhost:1313/iblog/posts/java/rookie-io/ + 2021-04-09T00:00:00+00:00 + + http://localhost:1313/iblog/posts/jvm/jvm-method-area/ + 2021-04-08T00:00:00+00:00 + + http://localhost:1313/iblog/posts/jvm/jvm-heap/ + 2021-04-03T00:00:00+00:00 + + http://localhost:1313/iblog/posts/jvm/jvm-native-interface/ + 2021-04-02T00:00:00+00:00 + + http://localhost:1313/iblog/posts/jvm/jvm-native-stack/ + 2021-04-02T00:00:00+00:00 + + http://localhost:1313/iblog/posts/jvm/jvm-stack/ + 2021-03-28T00:00:00+00:00 + + http://localhost:1313/iblog/posts/jvm/jvm-pc-register/ + 2021-03-27T00:00:00+00:00 + + http://localhost:1313/iblog/posts/jvm/jvm-start/ + 2021-03-05T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E4%BD%BF%E7%94%A8%E4%BB%8B%E7%BB%8D/ + 2021-03-05T00:00:00+00:00 + + http://localhost:1313/iblog/tags/nginx/ + 2021-03-04T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/nginx-start/ + 2021-03-04T00:00:00+00:00 + + http://localhost:1313/iblog/posts/books/books-daodejing/ + 2021-03-03T00:00:00+00:00 + + http://localhost:1313/iblog/about/ + 2021-02-20T00:00:00+00:00 + + http://localhost:1313/iblog/posts/java/rookie-object-oriented/ + 2021-02-15T00:00:00+00:00 + + http://localhost:1313/iblog/posts/jvm/jvm-classloader/ + 2021-02-05T00:00:00+00:00 + + http://localhost:1313/iblog/posts/java/rookie-operation/ + 2021-01-30T00:00:00+00:00 + + http://localhost:1313/iblog/posts/java/rookie-datatype/ + 2021-01-20T00:00:00+00:00 + + http://localhost:1313/iblog/posts/java/rookie-exception/ + 2021-01-13T00:00:00+00:00 + + http://localhost:1313/iblog/posts/resume/interview-resume-20201124/ + 2020-11-24T00:00:00+00:00 + + http://localhost:1313/iblog/tags/docker/ + 2020-08-30T00:00:00+00:00 + + http://localhost:1313/iblog/posts/spring/springboot-docker/ + 2020-08-30T00:00:00+00:00 + + http://localhost:1313/iblog/posts/spring/springboot-kafka/ + 2020-08-20T00:00:00+00:00 + + http://localhost:1313/iblog/tags/%E7%BA%BF%E7%A8%8B/ + 2020-04-20T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/thread-state-and-created/ + 2020-04-20T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/docker-start/ + 2020-04-07T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/java-lock/ + 2020-04-07T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/java-thread-collection/ + 2020-04-05T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/cas-principle/ + 2020-04-04T00:00:00+00:00 + + http://localhost:1313/iblog/posts/spring/springboot-redis/ + 2020-03-01T00:00:00+00:00 + + http://localhost:1313/iblog/posts/spring/springboot-elasticsearch/ + 2020-02-09T00:00:00+00:00 + + http://localhost:1313/iblog/posts/worksummary/work-summary-2019/ + 2019-12-01T00:00:00+00:00 + + http://localhost:1313/iblog/tags/vue/ + 2019-05-23T00:00:00+00:00 + + http://localhost:1313/iblog/posts/essays/vue2-note/ + 2019-05-23T00:00:00+00:00 + + http://localhost:1313/iblog/tags/js/ + 2018-12-25T00:00:00+00:00 + + http://localhost:1313/iblog/posts/toy/js-snow/ + 2018-12-25T00:00:00+00:00 + + http://localhost:1313/iblog/posts/toy/js-rain/ + 2018-12-10T00:00:00+00:00 + + http://localhost:1313/iblog/posts/toy/js-trans-skin/ + 2018-11-14T00:00:00+00:00 + + http://localhost:1313/iblog/posts/toy/js-paper-folding/ + 2018-10-25T00:00:00+00:00 + + http://localhost:1313/iblog/posts/toy/js-love-heart/ + 2018-10-14T00:00:00+00:00 + + http://localhost:1313/iblog/posts/toy/js-loadding-lazy/ + 2018-09-21T00:00:00+00:00 + + http://localhost:1313/iblog/posts/toy/js-gomoku/ + 2018-09-10T00:00:00+00:00 + + http://localhost:1313/iblog/posts/toy/js-box-drag/ + 2018-09-08T00:00:00+00:00 + + http://localhost:1313/iblog/posts/toy/js-birthday-gift/ + 2018-08-24T00:00:00+00:00 + + http://localhost:1313/iblog/categories/ + + + + diff --git a/blog-site/themes/zozo/layouts/_default/single.html b/blog-site/themes/zozo/layouts/_default/single.html index 1233c89c..2e5a5a5f 100644 --- a/blog-site/themes/zozo/layouts/_default/single.html +++ b/blog-site/themes/zozo/layouts/_default/single.html @@ -1,6 +1,25 @@ {{ partial "head.html" . }} +
+ {{ if ( .Params.password | default "" ) }} + + {{ end }} +
+ {{ if .Site.Params.toc | default false }} {{ partial "toc" . }} diff --git a/docs/about/index.html b/docs/about/index.html index 547c8bc8..01ac3dba 100644 --- a/docs/about/index.html +++ b/docs/about/index.html @@ -38,6 +38,10 @@ +
+ +
+ diff --git a/docs/categories/index.xml b/docs/categories/index.xml index 2659396a..2331f0f7 100644 --- a/docs/categories/index.xml +++ b/docs/categories/index.xml @@ -5,6 +5,7 @@ https://whiteppure.github.io/iblog/categories/ Recent content in Categories on 唯手熟尔 Hugo -- gohugo.io - zh + zh + diff --git a/docs/en/404.html b/docs/en/404.html index fd299433..88336676 100644 --- a/docs/en/404.html +++ b/docs/en/404.html @@ -1,5 +1,5 @@ - + @@ -79,7 +79,7 @@

- + diff --git a/docs/en/categories/index.html b/docs/en/categories/index.html index 1fa02e64..0f94d926 100644 --- a/docs/en/categories/index.html +++ b/docs/en/categories/index.html @@ -1,5 +1,5 @@ - + @@ -81,7 +81,7 @@

- + diff --git a/docs/en/categories/index.xml b/docs/en/categories/index.xml index 1d8d3b6f..e6cf0de9 100644 --- a/docs/en/categories/index.xml +++ b/docs/en/categories/index.xml @@ -5,6 +5,7 @@ https://whiteppure.github.io/iblog/en/categories/ Recent content in Categories on 唯手熟尔 Hugo -- gohugo.io - zh + en + diff --git a/docs/en/index.html b/docs/en/index.html index fa3860d6..f1d4e4ec 100644 --- a/docs/en/index.html +++ b/docs/en/index.html @@ -1,7 +1,7 @@ - + - + @@ -82,7 +82,7 @@

- + diff --git a/docs/en/index.xml b/docs/en/index.xml index b1d45587..c0200809 100644 --- a/docs/en/index.xml +++ b/docs/en/index.xml @@ -5,6 +5,7 @@ https://whiteppure.github.io/iblog/en/ Recent content on 唯手熟尔 Hugo -- gohugo.io - zh + en + diff --git a/docs/en/page/1/index.html b/docs/en/page/1/index.html index e9f88830..d7b20561 100644 --- a/docs/en/page/1/index.html +++ b/docs/en/page/1/index.html @@ -1,5 +1,5 @@ - + https://whiteppure.github.io/iblog/en/ diff --git a/docs/en/tags/index.html b/docs/en/tags/index.html index 6ad11cbd..86eb4a6f 100644 --- a/docs/en/tags/index.html +++ b/docs/en/tags/index.html @@ -1,5 +1,5 @@ - + @@ -81,7 +81,7 @@

- + diff --git a/docs/en/tags/index.xml b/docs/en/tags/index.xml index 4aa00bcf..5b77cf47 100644 --- a/docs/en/tags/index.xml +++ b/docs/en/tags/index.xml @@ -5,6 +5,7 @@ https://whiteppure.github.io/iblog/en/tags/ Recent content in Tags on 唯手熟尔 Hugo -- gohugo.io - zh + en + diff --git a/docs/index.html b/docs/index.html index 76f37f04..02330de3 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,7 +1,7 @@ - + @@ -128,12 +128,12 @@

-

定时任务可视化管理

+

前端学习路线

-

代码实现 代码结构 pom <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> 库表结构 -- ---------------------------- -- 定时任务调度表 -- ---------------------------- drop table if exists sys_job; create table sys_job ( job_id bigint(20) not null auto_increment comment '任务ID', job_name varchar(64) default '' comment '任务名称', job_group varchar(64) default 'DEFAULT' comment '任务组名', invoke_target varchar(500) not null comment......

+

基础知识 网络知识 HTTP DNS 域名 云服务 网络安全 HTTPS CORS 网络渗透 OWASP HTML CSS JavaScript JQuery Ajax ES6-ES11 综合应用 工程化体系 代码规范 CSS预处理器 Less Sass PostCSS Node Promise Axios 工具 包管理工具 Npm Yarn 打包工具 Webpack Parcel 代码格式化工具 ESLint Prettier 调试工具 Chrome IETest Postman 版本管理工具 Git GitLab GitHub 部署发布工具 Jenkins CICD 主流技术 TypeScript Vue React Angular 综合应用 静态......

@@ -142,15 +142,13 @@

定时任务可视化管理
- 2023.09.09 + 2024.02.29 - 应用 - - 设计 + 学习路线 @@ -161,12 +159,12 @@

定时任务可视化管理
-

SpringBoot整合nacos

+

2023工作总结

-

nacos nacos下载 下载地址 一键傻瓜试安装即可,官网写的很清楚这里不在赘述 http://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html nacos启动 将模式改为单机模式 启动成功 nacos相关配置 demo-dev.yaml server: port: 8001 config: info: "config info for dev from nacos config center" demo-test.yaml server: port: 3333 config: info: "config info for test from nacos config center" user.yaml user: name: zs1112222 age: 10 address: 测试地址 代码 整合nacos配置中心,注册......

+

在职期间,我主要负责耐材项目的开发与维护,共迭代171个版本。通过与团队成员的紧密合作,我们按时完成了项目中的需求。在这段时间里,我不断提升自己的专业技能和知识,增强了自己的专业能力。我始终认为团队合作是成功的关键。在工作中,我积极与同事沟......

@@ -175,15 +173,13 @@

SpringBoot整合nacos - 2023.09.04 + 2023.12.01 - springboot - - nacos + 工作总结 @@ -194,12 +190,12 @@

SpringBoot整合nacos
-

整合文件上传功能

+

20230915简历

-

结构 pom.xml fastdfs-client-java-1.27.jar:点击下载 <dependencies> <!-- fastdfs --> <dependency> <groupId>org.csource</groupId> <artifactId>fastdfs-client-java</artifactId> <version>1.27</version> <systemPath>${project.basedir}/lib/fastdfs-client-java-1.27.jar</systemPath> <scope>system</scope> </dependency> <!--aliyun oss 依赖--> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.11</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency> </dependencies> application.yml server: port: 80 公共部分 FileManagement public interface FileManagement { /** * 设置下一个bean的对象 * * @param nextFileManagement 下一个......

+

自我介绍 1998 · 李济芝 河北唐山 15176733539  m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和四年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对Jav......

@@ -208,15 +204,15 @@

整合文件上传功能<
- 2023.08.11 + 2023.09.15 - 应用 + 简历 - 设计 + 求职 @@ -227,12 +223,12 @@

整合文件上传功能<
-

整合支付功能

+

定时任务可视化管理

-

结构 pom.xml <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.11</version> </dependency> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.9.9</version> </dependency> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-pay</artifactId> <version>4.5.0</version> </dependency> </dependencies> application.yml server: port: 8080 pay: wechat: #微信公众号或者小程序等的appid appId: "" #微信支付商户号 mchId: "" #微信支付商户密钥 mchKey: "" #服务商模式下的子商户公众账号ID subAppId: #服务商模式下的子商户号 subMchId: # p12证书的位......

+

代码实现 代码结构 pom <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> 库表结构 -- ---------------------------- -- 定时任务调度表 -- ---------------------------- drop table if exists sys_job; create table sys_job ( job_id bigint(20) not null auto_increment comment '任务ID', job_name varchar(64) default '' comment '任务名称', job_group varchar(64) default 'DEFAULT' comment '任务组名', invoke_target varchar(500) not null comment......

@@ -241,7 +237,7 @@

整合支付功能

- 2023.08.10 + 2023.09.09 @@ -260,12 +256,12 @@

整合支付功能

-

Validator参数校验

+

SpringBoot整合nacos

-

常见参数校验 在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,最简单就是用if条件语句来判断,但是随着参数越来越多,业务越来越复杂,判断参数代码语句显得尤为冗长. 或者有些程序会将if封装起来,例如spring中......

+

nacos nacos下载 下载地址 一键傻瓜试安装即可,官网写的很清楚这里不在赘述 http://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html nacos启动 将模式改为单机模式 启动成功 nacos相关配置 demo-dev.yaml server: port: 8001 config: info: "config info for dev from nacos config center" demo-test.yaml server: port: 3333 config: info: "config info for test from nacos config center" user.yaml user: name: zs1112222 age: 10 address: 测试地址 代码 整合nacos配置中心,注册......

@@ -274,16 +270,16 @@

Validator参数校验 - 2023.07.01 + 2023.09.04 - 应用 - springboot + nacos +

@@ -293,12 +289,12 @@

Validator参数校验
-

管道流设计模式结合业务

+

整合文件上传功能

-

流程图 代码实现 pom <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.plugin</groupId> <artifactId>spring-plugin-core</artifactId> <version>${spring.plugin.core.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> </dependencies> context EventContext public interface EventContext { /** * 是否继续调用链 */ boolean continueChain(); /** * 获取当前过滤器选择器 */ FilterSelector getFilterSelector(); } BizType public interface BizType { /** * 获取业务类型码值 */ Integer getCode(); /** * 业务类型名称 * */ String getName(); } AbstractEventContext public abstract class AbstractEventContext implements EventContext{ private final BizType businessType; private final FilterSelector filterSelector; protected AbstractEventContext(BizType businessType, FilterSelector filterSelector) {......

+

结构 pom.xml fastdfs-client-java-1.27.jar:点击下载 <dependencies> <!-- fastdfs --> <dependency> <groupId>org.csource</groupId> <artifactId>fastdfs-client-java</artifactId> <version>1.27</version> <systemPath>${project.basedir}/lib/fastdfs-client-java-1.27.jar</systemPath> <scope>system</scope> </dependency> <!--aliyun oss 依赖--> <dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.11</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency> </dependencies> application.yml server: port: 80 公共部分 FileManagement public interface FileManagement { /** * 设置下一个bean的对象 * * @param nextFileManagement 下一个......

@@ -307,7 +303,7 @@

管道流设计模式结合
- 2023.06.15 + 2023.08.11 @@ -326,12 +322,12 @@

管道流设计模式结合
-

重构一个程序

+

整合支付功能

-

什么是重构 摘自《重构:改善既有代码的设计》 重构(名词形式): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。 重构(动词形式): 使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构。 重......

+

结构 pom.xml <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.11</version> </dependency> <dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.9.9</version> </dependency> <dependency> <groupId>com.github.binarywang</groupId> <artifactId>weixin-java-pay</artifactId> <version>4.5.0</version> </dependency> </dependencies> application.yml server: port: 8080 pay: wechat: #微信公众号或者小程序等的appid appId: "" #微信支付商户号 mchId: "" #微信支付商户密钥 mchKey: "" #服务商模式下的子商户公众账号ID subAppId: #服务商模式下的子商户号 subMchId: # p12证书的位......

@@ -340,12 +336,14 @@

重构一个程
- 2023.04.20 + 2023.08.10 + 应用 + 设计 @@ -357,12 +355,12 @@

重构一个程
-

SpringMVC与SpringWebFlux

+

Validator参数校验

-

Spring MVC Spring Web MVC是建立在Servlet API上的原始Web框架,从一开始就包含在Spring框架中。正式名称 “Spring Web MVC “来自其源模块的名称(spring-webmvc),但它更常被称为 “Spring MVC”。 SpringMVC是基于S......

+

常见参数校验 在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,最简单就是用if条件语句来判断,但是随着参数越来越多,业务越来越复杂,判断参数代码语句显得尤为冗长. 或者有些程序会将if封装起来,例如spring中......

@@ -371,15 +369,15 @@

SpringMVC与SpringWeb
- 2023.04.14 + 2023.07.01 - Java + 应用 - Spring + springboot @@ -390,12 +388,12 @@

SpringMVC与SpringWeb
-

MySQL详解

+

管道流设计模式结合业务

-

逻辑架构 主要分为:连接层,服务层,引擎层,存储层。 客户端执行一条select命令的流程如下: 连接层:最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcplip的通信。主要完成一些类似于连接处理、......

+

流程图 代码实现 pom <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.plugin</groupId> <artifactId>spring-plugin-core</artifactId> <version>${spring.plugin.core.version}</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>${hutool.version}</version> </dependency> </dependencies> context EventContext public interface EventContext { /** * 是否继续调用链 */ boolean continueChain(); /** * 获取当前过滤器选择器 */ FilterSelector getFilterSelector(); } BizType public interface BizType { /** * 获取业务类型码值 */ Integer getCode(); /** * 业务类型名称 * */ String getName(); } AbstractEventContext public abstract class AbstractEventContext implements EventContext{ private final BizType businessType; private final FilterSelector filterSelector; protected AbstractEventContext(BizType businessType, FilterSelector filterSelector) {......

@@ -404,13 +402,15 @@

MySQL详解

- 2023.03.13 + 2023.06.15 - MySQL + 应用 + + 设计 @@ -421,12 +421,12 @@

MySQL详解

-

如何减少及解决bug

+

重构一个程序

-

bug的起源: 1945年,一只小飞蛾钻进了计算机电路里,导致系统无法工作,一位名叫格蕾丝·赫柏的人把飞蛾拍死在工作日志上,写道:就是这个 bug(虫子),害我们今天的工作无法完成——于是,bug一词成了电脑系统程序的专业术语,形容那些系统中的......

+

什么是重构 摘自《重构:改善既有代码的设计》 重构(名词形式): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。 重构(动词形式): 使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构。 重......

@@ -435,13 +435,13 @@

如何减少及解决bug

- 2023.03.10 + 2023.04.20 - 技巧 + 设计 @@ -452,12 +452,12 @@

如何减少及解决bug

-

Elasticsearch详解

+

SpringMVC与SpringWebFlux

-

概览 Elasticsearch,简称为 ES, ES是一个开源的高扩展的分布式全文搜索引擎, 是整个 ElasticStack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。 Elastic Stack, 包括 Elasticsearch、 Ki......

+

Spring MVC Spring Web MVC是建立在Servlet API上的原始Web框架,从一开始就包含在Spring框架中。正式名称 “Spring Web MVC “来自其源模块的名称(spring-webmvc),但它更常被称为 “Spring MVC”。 SpringMVC是基于S......

@@ -466,13 +466,15 @@

Elasticsearch详解

- 2023.02.14 + 2023.04.14 - Elasticsearch + Java + + Spring @@ -483,12 +485,12 @@

Elasticsearch详解

-

编程常用词汇汇总

+

MySQL详解

-

QPS 即 Queries Per Second的缩写,每秒能处理查询数目。是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。 TPS 即 Transactions Per Second的缩写,每秒处理的事务数目。一个事务是指一个客户机向服务器发送请求然后服务......

+

逻辑架构 主要分为:连接层,服务层,引擎层,存储层。 客户端执行一条select命令的流程如下: 连接层:最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcplip的通信。主要完成一些类似于连接处理、......

@@ -497,13 +499,13 @@

编程常用词汇汇总

- 2023.02.13 + 2023.03.13 - Java + MySQL @@ -514,12 +516,12 @@

编程常用词汇汇总

-

接口优化

+

如何减少及解决bug

-

接口优化 线上接口很慢,线上生产问题,我们绝对不能马虎放过抱着侥幸心理,必须要找到根本原因及时处理,防止下次留下更大的坑.大致思路要定位接口问题,然后具体问题具体分析,讨论不同解决方案. 定位问题 要快速定位接口哪一个环节比较慢,性能瓶颈在哪里,......

+

bug的起源: 1945年,一只小飞蛾钻进了计算机电路里,导致系统无法工作,一位名叫格蕾丝·赫柏的人把飞蛾拍死在工作日志上,写道:就是这个 bug(虫子),害我们今天的工作无法完成——于是,bug一词成了电脑系统程序的专业术语,形容那些系统中的......

@@ -528,13 +530,13 @@

接口优化

- 2022.12.20 + 2023.03.10 - 设计 + 技巧 @@ -545,12 +547,12 @@

接口优化

-

孙子兵法

+

Elasticsearch详解

-

始计 兵者,国之大事,死生之地,存亡之道,不可不察也。 故经之以五事,校之以计,而索其情:一曰道,二曰天,三曰地,四曰将,五曰法。道者,令民于上同意,可与之死,可与之生,而不危也;天者,阴阳、寒暑、时制也;地者,远近、险易、广狭、死生也;将者,......

+

概览 Elasticsearch,简称为 ES, ES是一个开源的高扩展的分布式全文搜索引擎, 是整个 ElasticStack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。 Elastic Stack, 包括 Elasticsearch、 Ki......

@@ -559,13 +561,13 @@

孙子兵法

- 2022.12.19 + 2023.02.14 - 书籍 + Elasticsearch @@ -576,12 +578,12 @@

孙子兵法

-

IDEA常用配置及使用技巧

+

编程常用词汇汇总

-

下载 工欲善其事必先利其器,一个好的开发工具,能极大提高开发效率. 新UI很漂亮。IDEA 2022.2.3 官方下载地址: https://www.jetbrains.com/zh-cn/idea/download/other.html 激活工具 百度云下载. 链接:https://pan.baidu.com/s/19sCUTCBXvwXgEQc8vX-SYQ?pwd=gwu......

+

QPS 即 Queries Per Second的缩写,每秒能处理查询数目。是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。 TPS 即 Transactions Per Second的缩写,每秒处理的事务数目。一个事务是指一个客户机向服务器发送请求然后服务......

@@ -590,13 +592,13 @@

IDEA常用配置及使用技巧<
- 2022.12.16 + 2023.02.13 - 技巧 + Java @@ -607,12 +609,12 @@

IDEA常用配置及使用技巧<
-

整洁的代码

+

接口优化

-

为什么要写整洁的代码 为什么要写整洁的代码,回答这个问题之前,也许应该想想写糟糕的代码的原因 是想快点完成吗?还是要赶时间吗?有可能.或许你觉得自己要干好所需要的时间不够;假使花时间清理代码,老板就会大发雷霆.或许你只是不耐烦再搞这套程序,期望......

+

接口优化 线上接口很慢,线上生产问题,我们绝对不能马虎放过抱着侥幸心理,必须要找到根本原因及时处理,防止下次留下更大的坑.大致思路要定位接口问题,然后具体问题具体分析,讨论不同解决方案. 定位问题 要快速定位接口哪一个环节比较慢,性能瓶颈在哪里,......

@@ -621,7 +623,7 @@

整洁的代码

- 2022.09.01 + 2022.12.20 @@ -638,12 +640,12 @@

整洁的代码

-

关于编程的书籍

+

孙子兵法

-

HeadFirst设计模式 Java数据结构和算法 Java核心技术卷I基础知识 Java编程思想 代码整洁之道 大型网站技术架构 大话数据结构 深入分析JavaWeb技术内幕 疯狂Java讲义 重构:改善既有代码的设计 领域驱动设计......

+

始计 兵者,国之大事,死生之地,存亡之道,不可不察也。 故经之以五事,校之以计,而索其情:一曰道,二曰天,三曰地,四曰将,五曰法。道者,令民于上同意,可与之死,可与之生,而不危也;天者,阴阳、寒暑、时制也;地者,远近、险易、广狭、死生也;将者,......

@@ -652,7 +654,7 @@

关于编程的书籍

- 2022.08.08 + 2022.12.19 @@ -669,12 +671,12 @@

关于编程的书籍

-

如何做好程序设计功能

+

IDEA常用配置及使用技巧

-

产品需求澄清、PN排期及任务分解 开发设计评审 功能设计流程图 与外部系统交互、本系统模块之间流程,比较好用的画圈软件draw .io或在线的process on 数据库设计 从DDD角度界限上下文、ER图、评审表结构设计是否合理,表的关联关系是否合理、是......

+

下载 工欲善其事必先利其器,一个好的开发工具,能极大提高开发效率. 新UI很漂亮。IDEA 2022.2.3 官方下载地址: https://www.jetbrains.com/zh-cn/idea/download/other.html 激活工具 百度云下载. 链接:https://pan.baidu.com/s/19sCUTCBXvwXgEQc8vX-SYQ?pwd=gwu......

@@ -683,15 +685,13 @@

如何做好程序设计功能 - 2022.08.02 + 2022.12.16 - Java - - 设计 + 技巧 @@ -702,12 +702,12 @@

如何做好程序设计功能
-

Java小程序集合

+

整洁的代码

-

写在前面 本文中所涉及的程序均为Java开发,如果您想要直接使用这些工具需要提前配置Java环境。所涉及到的程序均提供完整代码,如果您有兴趣可以尝试运行。 使用java -jar命令启动 某些程序功能并不是很完善,但是也可以凑合着用,写这些程序的主......

+

为什么要写整洁的代码 为什么要写整洁的代码,回答这个问题之前,也许应该想想写糟糕的代码的原因 是想快点完成吗?还是要赶时间吗?有可能.或许你觉得自己要干好所需要的时间不够;假使花时间清理代码,老板就会大发雷霆.或许你只是不耐烦再搞这套程序,期望......

@@ -716,15 +716,13 @@

Java小程序集合

- 2022.04.09 + 2022.09.01 - Java - - 玩具 + 设计 @@ -735,12 +733,12 @@

Java小程序集合

-

数据结构与算法

+

关于编程的书籍

-

数据结构 数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好数据结构可以编写出更有效率的代码。数据结构是算法的基础,想要学好算法,就必须把数据结构学到位。 数据结构包括:线性结构、非线性结构。 线性结构作为最常用的数据结构,......

+

HeadFirst设计模式 Java数据结构和算法 Java核心技术卷I基础知识 Java编程思想 代码整洁之道 大型网站技术架构 大话数据结构 深入分析JavaWeb技术内幕 疯狂Java讲义 重构:改善既有代码的设计 领域驱动设计 深入理解计算机系统 Java......

@@ -749,15 +747,13 @@

数据结构与算
- 2021.12.10 + 2022.08.08 - Java - - 算法 + 书籍 diff --git a/docs/index.xml b/docs/index.xml index 777e6c4b..8302bd5a 100644 --- a/docs/index.xml +++ b/docs/index.xml @@ -6,699 +6,588 @@ Recent content on 唯手熟尔 Hugo -- gohugo.io zh - Sat, 09 Sep 2023 00:00:00 +0000 + Thu, 29 Feb 2024 00:00:00 +0000 + + + 前端学习路线 + https://whiteppure.github.io/iblog/posts/essays/front-learning-route/ + Thu, 29 Feb 2024 00:00:00 +0000 + https://whiteppure.github.io/iblog/posts/essays/front-learning-route/ + 基础知识 网络知识 HTTP DNS 域名 云服务 网络安全 HTTPS CORS 网络渗透 OWASP HTML CSS JavaScript JQuery Ajax ES6-ES11 综合应用 工程化体系 代码规范 CSS预处理器 Less Sass PostCSS Node Promise Axios 工具 包管理工具 Npm Yarn 打包工具 Webpack Parcel 代码格式化工具 ESLint Prettier 调试工具 Chrome IETest Postman 版本管理工具 Git GitLab GitHub 部署发布工具 Jenkins CICD 主流技术 TypeScript Vue React Angular 综合应用 静态 + + + 2023工作总结 + https://whiteppure.github.io/iblog/posts/worksummary/work-summary-2023/ + Fri, 01 Dec 2023 00:00:00 +0000 + https://whiteppure.github.io/iblog/posts/worksummary/work-summary-2023/ + 在职期间,我主要负责耐材项目的开发与维护,共迭代171个版本。通过与团队成员的紧密合作,我们按时完成了项目中的需求。在这段时间里,我不断提升自己的专业技能和知识,增强了自己的专业能力。我始终认为团队合作是成功的关键。在工作中,我积极与同事沟 + + + 20230915简历 + https://whiteppure.github.io/iblog/posts/resume/interview-resume-20230915/ + Fri, 15 Sep 2023 00:00:00 +0000 + https://whiteppure.github.io/iblog/posts/resume/interview-resume-20230915/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和四年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对Jav + 定时任务可视化管理 https://whiteppure.github.io/iblog/posts/essays/scheduled-job/ Sat, 09 Sep 2023 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/scheduled-job/ 代码实现 代码结构 pom &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-quartz&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;/dependency&gt; 库表结构 -- ---------------------------- -- 定时任务调度表 -- ---------------------------- drop table if exists sys_job; create table sys_job ( job_id bigint(20) not null auto_increment comment &#39;任务ID&#39;, job_name varchar(64) default &#39;&#39; comment &#39;任务名称&#39;, job_group varchar(64) default &#39;DEFAULT&#39; comment &#39;任务组名&#39;, invoke_target varchar(500) not null comment - SpringBoot整合nacos https://whiteppure.github.io/iblog/posts/spring/springboot-nacos/ Mon, 04 Sep 2023 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/spring/springboot-nacos/ nacos nacos下载 下载地址 一键傻瓜试安装即可,官网写的很清楚这里不在赘述 http://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html nacos启动 将模式改为单机模式 启动成功 nacos相关配置 demo-dev.yaml server: port: 8001 config: info: &#34;config info for dev from nacos config center&#34; demo-test.yaml server: port: 3333 config: info: &#34;config info for test from nacos config center&#34; user.yaml user: name: zs1112222 age: 10 address: 测试地址 代码 整合nacos配置中心,注册 - 整合文件上传功能 https://whiteppure.github.io/iblog/posts/essays/uploadfile-code/ Fri, 11 Aug 2023 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/uploadfile-code/ 结构 pom.xml fastdfs-client-java-1.27.jar:点击下载 &lt;dependencies&gt; &lt;!-- fastdfs --&gt; &lt;dependency&gt; &lt;groupId&gt;org.csource&lt;/groupId&gt; &lt;artifactId&gt;fastdfs-client-java&lt;/artifactId&gt; &lt;version&gt;1.27&lt;/version&gt; &lt;systemPath&gt;${project.basedir}/lib/fastdfs-client-java-1.27.jar&lt;/systemPath&gt; &lt;scope&gt;system&lt;/scope&gt; &lt;/dependency&gt; &lt;!--aliyun oss 依赖--&gt; &lt;dependency&gt; &lt;groupId&gt;com.aliyun.oss&lt;/groupId&gt; &lt;artifactId&gt;aliyun-sdk-oss&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;5.8.11&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;commons-io&lt;/groupId&gt; &lt;artifactId&gt;commons-io&lt;/artifactId&gt; &lt;version&gt;2.11.0&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; application.yml server: port: 80 公共部分 FileManagement public interface FileManagement { /** * 设置下一个bean的对象 * * @param nextFileManagement 下一个 - 整合支付功能 https://whiteppure.github.io/iblog/posts/essays/pay-code/ Thu, 10 Aug 2023 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/pay-code/ 结构 pom.xml &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;5.8.11&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.alipay.sdk&lt;/groupId&gt; &lt;artifactId&gt;alipay-sdk-java&lt;/artifactId&gt; &lt;version&gt;4.9.9&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;com.github.binarywang&lt;/groupId&gt; &lt;artifactId&gt;weixin-java-pay&lt;/artifactId&gt; &lt;version&gt;4.5.0&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; application.yml server: port: 8080 pay: wechat: #微信公众号或者小程序等的appid appId: &#34;&#34; #微信支付商户号 mchId: &#34;&#34; #微信支付商户密钥 mchKey: &#34;&#34; #服务商模式下的子商户公众账号ID subAppId: #服务商模式下的子商户号 subMchId: # p12证书的位 - Validator参数校验 https://whiteppure.github.io/iblog/posts/essays/springboot-validator/ Sat, 01 Jul 2023 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/springboot-validator/ 常见参数校验 在日常的接口开发中,为了防止非法参数对业务造成影响,经常需要对接口的参数做校验,最简单就是用if条件语句来判断,但是随着参数越来越多,业务越来越复杂,判断参数代码语句显得尤为冗长. 或者有些程序会将if封装起来,例如spring中 - 管道流设计模式结合业务 https://whiteppure.github.io/iblog/posts/essays/pipeline-business/ Thu, 15 Jun 2023 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/pipeline-business/ 流程图 代码实现 pom &lt;dependencies&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.plugin&lt;/groupId&gt; &lt;artifactId&gt;spring-plugin-core&lt;/artifactId&gt; &lt;version&gt;${spring.plugin.core.version}&lt;/version&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt; &lt;artifactId&gt;spring-boot-starter-test&lt;/artifactId&gt; &lt;scope&gt;test&lt;/scope&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;org.projectlombok&lt;/groupId&gt; &lt;artifactId&gt;lombok&lt;/artifactId&gt; &lt;optional&gt;true&lt;/optional&gt; &lt;/dependency&gt; &lt;dependency&gt; &lt;groupId&gt;cn.hutool&lt;/groupId&gt; &lt;artifactId&gt;hutool-all&lt;/artifactId&gt; &lt;version&gt;${hutool.version}&lt;/version&gt; &lt;/dependency&gt; &lt;/dependencies&gt; context EventContext public interface EventContext { /** * 是否继续调用链 */ boolean continueChain(); /** * 获取当前过滤器选择器 */ FilterSelector getFilterSelector(); } BizType public interface BizType { /** * 获取业务类型码值 */ Integer getCode(); /** * 业务类型名称 * */ String getName(); } AbstractEventContext public abstract class AbstractEventContext implements EventContext{ private final BizType businessType; private final FilterSelector filterSelector; protected AbstractEventContext(BizType businessType, FilterSelector filterSelector) { - 重构一个程序 https://whiteppure.github.io/iblog/posts/essays/java-project-reconstitution/ Thu, 20 Apr 2023 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/java-project-reconstitution/ 什么是重构 摘自《重构:改善既有代码的设计》 重构(名词形式): 对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。 重构(动词形式): 使用一些列重构手法,在不改变软件可观察行为的前提下,调整其结构。 重 - SpringMVC与SpringWebFlux https://whiteppure.github.io/iblog/posts/spring/java-spring-mvc-webflux/ Fri, 14 Apr 2023 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/spring/java-spring-mvc-webflux/ Spring MVC Spring Web MVC是建立在Servlet API上的原始Web框架,从一开始就包含在Spring框架中。正式名称 &ldquo;Spring Web MVC &ldquo;来自其源模块的名称(spring-webmvc),但它更常被称为 &ldquo;Spring MVC&rdquo;。 SpringMVC是基于S - MySQL详解 https://whiteppure.github.io/iblog/posts/essays/sql-select-fast/ Mon, 13 Mar 2023 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/sql-select-fast/ 逻辑架构 主要分为:连接层,服务层,引擎层,存储层。 客户端执行一条select命令的流程如下: 连接层:最上层是一些客户端和连接服务,包含本地sock通信和大多数基于客户端/服务端工具实现的类似于tcplip的通信。主要完成一些类似于连接处理、 - 如何减少及解决bug https://whiteppure.github.io/iblog/posts/essays/java-bugs/ Fri, 10 Mar 2023 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/java-bugs/ bug的起源: 1945年,一只小飞蛾钻进了计算机电路里,导致系统无法工作,一位名叫格蕾丝·赫柏的人把飞蛾拍死在工作日志上,写道:就是这个 bug(虫子),害我们今天的工作无法完成——于是,bug一词成了电脑系统程序的专业术语,形容那些系统中的 - Elasticsearch详解 https://whiteppure.github.io/iblog/posts/essays/elasticsearch/ Tue, 14 Feb 2023 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/elasticsearch/ 概览 Elasticsearch,简称为 ES, ES是一个开源的高扩展的分布式全文搜索引擎, 是整个 ElasticStack 技术栈的核心。它可以近乎实时的存储、检索数据;本身扩展性很好,可以扩展到上百台服务器,处理 PB 级别的数据。 Elastic Stack, 包括 Elasticsearch、 Ki - 编程常用词汇汇总 https://whiteppure.github.io/iblog/posts/essays/java-dict/ Mon, 13 Feb 2023 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/java-dict/ QPS 即 Queries Per Second的缩写,每秒能处理查询数目。是一台服务器每秒能够相应的查询次数,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。 TPS 即 Transactions Per Second的缩写,每秒处理的事务数目。一个事务是指一个客户机向服务器发送请求然后服务 - 接口优化 https://whiteppure.github.io/iblog/posts/essays/java-improve/ Tue, 20 Dec 2022 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/java-improve/ 接口优化 线上接口很慢,线上生产问题,我们绝对不能马虎放过抱着侥幸心理,必须要找到根本原因及时处理,防止下次留下更大的坑.大致思路要定位接口问题,然后具体问题具体分析,讨论不同解决方案. 定位问题 要快速定位接口哪一个环节比较慢,性能瓶颈在哪里, - 孙子兵法 https://whiteppure.github.io/iblog/posts/books/books-sunzibingfa/ Mon, 19 Dec 2022 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/books/books-sunzibingfa/ 始计 兵者,国之大事,死生之地,存亡之道,不可不察也。 故经之以五事,校之以计,而索其情:一曰道,二曰天,三曰地,四曰将,五曰法。道者,令民于上同意,可与之死,可与之生,而不危也;天者,阴阳、寒暑、时制也;地者,远近、险易、广狭、死生也;将者, - IDEA常用配置及使用技巧 https://whiteppure.github.io/iblog/posts/essays/dev-idea/ Fri, 16 Dec 2022 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/dev-idea/ 下载 工欲善其事必先利其器,一个好的开发工具,能极大提高开发效率. 新UI很漂亮。IDEA 2022.2.3 官方下载地址: https://www.jetbrains.com/zh-cn/idea/download/other.html 激活工具 百度云下载. 链接:https://pan.baidu.com/s/19sCUTCBXvwXgEQc8vX-SYQ?pwd=gwu - 整洁的代码 https://whiteppure.github.io/iblog/posts/essays/clean-code/ Thu, 01 Sep 2022 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/clean-code/ 为什么要写整洁的代码 为什么要写整洁的代码,回答这个问题之前,也许应该想想写糟糕的代码的原因 是想快点完成吗?还是要赶时间吗?有可能.或许你觉得自己要干好所需要的时间不够;假使花时间清理代码,老板就会大发雷霆.或许你只是不耐烦再搞这套程序,期望 - 关于编程的书籍 https://whiteppure.github.io/iblog/posts/books/java-books/ Mon, 08 Aug 2022 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/books/java-books/ - HeadFirst设计模式 Java数据结构和算法 Java核心技术卷I基础知识 Java编程思想 代码整洁之道 大型网站技术架构 大话数据结构 深入分析JavaWeb技术内幕 疯狂Java讲义 重构:改善既有代码的设计 领域驱动设计 + HeadFirst设计模式 Java数据结构和算法 Java核心技术卷I基础知识 Java编程思想 代码整洁之道 大型网站技术架构 大话数据结构 深入分析JavaWeb技术内幕 疯狂Java讲义 重构:改善既有代码的设计 领域驱动设计 深入理解计算机系统 Java - 如何做好程序设计功能 https://whiteppure.github.io/iblog/posts/essays/java-design/ Tue, 02 Aug 2022 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/java-design/ 产品需求澄清、PN排期及任务分解 开发设计评审 功能设计流程图 与外部系统交互、本系统模块之间流程,比较好用的画圈软件draw .io或在线的process on 数据库设计 从DDD角度界限上下文、ER图、评审表结构设计是否合理,表的关联关系是否合理、是 - + + 20220422简历 + https://whiteppure.github.io/iblog/posts/resume/interview-resume-20220422/ + Fri, 22 Apr 2022 00:00:00 +0000 + https://whiteppure.github.io/iblog/posts/resume/interview-resume-20220422/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和三年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对SQL + Java小程序集合 https://whiteppure.github.io/iblog/posts/toy/java-multi-gadget/ Sat, 09 Apr 2022 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/toy/java-multi-gadget/ 写在前面 本文中所涉及的程序均为Java开发,如果您想要直接使用这些工具需要提前配置Java环境。所涉及到的程序均提供完整代码,如果您有兴趣可以尝试运行。 使用java -jar命令启动 某些程序功能并不是很完善,但是也可以凑合着用,写这些程序的主 - 数据结构与算法 https://whiteppure.github.io/iblog/posts/essays/data-structures-algorithms/ Fri, 10 Dec 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/data-structures-algorithms/ 数据结构 数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好数据结构可以编写出更有效率的代码。数据结构是算法的基础,想要学好算法,就必须把数据结构学到位。 数据结构包括:线性结构、非线性结构。 线性结构作为最常用的数据结构, - 规范编写Java代码总结 https://whiteppure.github.io/iblog/posts/essays/java-code-rule/ Thu, 25 Nov 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/java-code-rule/ 编码规范 我们为什么要遵守规范来编码? 是因为通常在编码过程中我们不只自己进行开发,通常需要一个团队来进行,开发好之后还需要维护,所以编码规范就显的尤为重要。 代码维护时间比较长,那么保证代码可读性就显得很重要。作为一个程序员,咱们得有点追求和信 - 网络编程 https://whiteppure.github.io/iblog/posts/essays/net-program-java/ Fri, 19 Nov 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/net-program-java/ 网络协议 以下内容摘自百度百科: https://baike.baidu.com/item/网络协议/328636 https://baike.baidu.com/item/网络七层协议/6056879 网络协议指的是计算机网络中互相通信的对等实体 - MQ详解 https://whiteppure.github.io/iblog/posts/essays/java-mq/ Tue, 19 Oct 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/java-mq/ 概念 MQ 即 messagequeue 消息队列,是分布式系统的重要组件,主要解决异步消息,应用解耦,消峰等问题。从而实现高可用,高性能,可伸缩和最终一致性的架构。使用较多的MQ有:activeMQ,rabbitMQ,kafka,metaMQ。 MQ优点 异步消息处理:可以 - Java集合 https://whiteppure.github.io/iblog/posts/java/rookie-java-container/ Mon, 04 Oct 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/java/rookie-java-container/ 概述 Java中的集合主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 如果你看过ArrayList类源码,就知道ArrayList底层是通过数组来存储元素的,所以如果严格来说,数组也算集合的一种 - Java反射 https://whiteppure.github.io/iblog/posts/java/rookie-reflect/ Sat, 02 Oct 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/java/rookie-reflect/ 概述 什么是反射 在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。 反射是Java语言的一个特性,它允许程序 - 常见故障排查及程序配置 https://whiteppure.github.io/iblog/posts/essays/eye-beam/ Wed, 08 Sep 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/eye-beam/ 故障排查基础 收录Linux常用命令,以下命令来自https://www.bilibili.com/video/BV14A411378a 关机/重启/注销 常用命令 作用 shutdown -h now 即刻关机 shutdown -h 10 10分钟后关机 shutdown -h 11:00 11:00关机 shutdown -h +10 预定时间关机(10 - 分布式事务详解 https://whiteppure.github.io/iblog/posts/essays/java-transaction/ Mon, 02 Aug 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/java-transaction/ 基础概念 什么是事务 什么是事务?举个例子:你去超市买东西,&ldquo;一手交钱,一手交货&quot;就是一个事务的例子,交钱和交货必须同时成功,事务才算成功,其中有一个环节失败,事务将会撤销所有已成功的活动。 所以事务可以看作是一次重大的活动 - Object类方法 https://whiteppure.github.io/iblog/posts/java/rookie-objectclass-methods/ Sat, 10 Jul 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/java/rookie-objectclass-methods/ 概览 Object 类位于 java.lang 包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承Object,成为Object的子类。 Object类可以显示继承,也可以隐式继承,效果都是一样的。 class A extends Object{ // to do } class A { // to do } Java Objec - 微服务治理 https://whiteppure.github.io/iblog/posts/essays/java-small-service/ Mon, 21 Jun 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/java-small-service/ 什么是微服务架构 In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API。 These services are built around business capabilities and independently deployable by fully automated deployment machinery。 There is a bare minimum of centralized management of these services, which may be written in different programming - Redis详解 https://whiteppure.github.io/iblog/posts/essays/java-redis/ Thu, 17 Jun 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/java-redis/ Redis概述 参考文章: https://www.runoob.com/redis/redis-intro.html https://www.redis.com.cn/redis-interview-questions.html 什么是Redis Redis(Remote Dictionary Server) Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API 的非关系型数据库。 简而言之,Redis是一个可基于内存亦可持久 - Spring详解 https://whiteppure.github.io/iblog/posts/spring/java-spring/ Thu, 13 May 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/spring/java-spring/ 概览 Spring是一个轻量级的Java开源框架,为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IOC)和面向切面(AOP)。 简单来说,Spring是一个分层的JavaSE/EE 一站式轻量级开源框架。在每一层都提供支持。 - 面试Java可能会被问到的问题 https://whiteppure.github.io/iblog/posts/resume/interview-junior-javaer/ Tue, 11 May 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/resume/interview-junior-javaer/ 面试必问 自我介绍一下 你有什么职业规划 你为什么要离职 说一下你的优缺点 你的期望薪资是多少 你为什么要选择我们公司 你能否接受加班 你有对象了吗 你还有什么问题要问的吗 基础 说一下UDP、TCP及http与https 如何保证线程安全 线程池工作原理 如何避免死 - JVM-垃圾回收器 https://whiteppure.github.io/iblog/posts/jvm/java-garbage-collector/ Thu, 06 May 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/jvm/java-garbage-collector/ 垃圾回收器分类 垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。 由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。 Java不同版本新特性学习思路: 语法层面:Lambda表达式、s - Java多线程 https://whiteppure.github.io/iblog/posts/java/rookie-multi-thread/ Wed, 05 May 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/java/rookie-multi-thread/ 相关概念 线程与进程 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。例如,一个正在运行的程序的实例就是一个进程。 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 一条线程指的是进程中一个单一 - HashMap详解 https://whiteppure.github.io/iblog/posts/essays/java-hashmap/ Mon, 03 May 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/java-hashmap/ 相关概念 capacity: 容量,默认16; loadFactor: 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容; threshold: 阈值;阈值 - JVM-相关概念 https://whiteppure.github.io/iblog/posts/jvm/jvm-about/ Tue, 27 Apr 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/jvm/jvm-about/ 内存溢出 内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。 官方文档中对内存溢出的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。 由于GC一 - 面试中常见的问题 https://whiteppure.github.io/iblog/posts/resume/interview-questions-and-answers/ Fri, 23 Apr 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/resume/interview-questions-and-answers/ 面试常见问题 自我介绍 个人经历可以进行适当包装,但是不能造假,一方面如果一旦被人拆穿,后果就不用我说了吧,另一方面如果你说谎,说了一些你自己不感兴趣的项目,在入职之后可能会被分配到该项目上; 在介绍的时候要说明你对面试的公司有什么用,根据不同类 - JVM-垃圾回收 https://whiteppure.github.io/iblog/posts/jvm/java-garbage-collection/ Wed, 21 Apr 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/jvm/java-garbage-collection/ 垃圾回收 垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。 垃圾收集机制是Java的招牌能力,极大地提高了开发效率。 如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展 - JVM-执行引擎 https://whiteppure.github.io/iblog/posts/jvm/jvm-execute-engine/ Thu, 15 Apr 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/jvm/jvm-execute-engine/ 概述 执行引擎是Java虚拟机核心的组成部分之一,属于JVM的下层,里面包括 解释器、及时编译器、垃圾回收器。 “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力, 其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统 - JVM-直接内存 https://whiteppure.github.io/iblog/posts/jvm/jvm-direct-memory/ Wed, 14 Apr 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/jvm/jvm-direct-memory/ 直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。 直接内存是在Java堆外的、直接向系统申请的内存区间。 操作直接内存演示代码: public class MainTest { public static void main(String[] args) { ByteBuffer allocate = ByteBuffer.allocate(1024 * 1024 * 1024); System.out.println(&# - JVM-Java对象 https://whiteppure.github.io/iblog/posts/jvm/java-object/ Mon, 12 Apr 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/jvm/java-object/ 对象实例化 对象的创建方式 使用new关键字创建:最常见的方式、单例类中调用getInstance的静态类方法,XXXFactory的静态方法; 使用反射方式创建: 使用Class的newInstance方法:在JDK9里面被标记为过时的方法,因为 - Java语法糖 https://whiteppure.github.io/iblog/posts/essays/java-syntax-sugar/ Sat, 10 Apr 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/java-syntax-sugar/ 原文地址:https://www.jianshu.com/p/0f967298a5d7 语法糖 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法, 这种语法对语言的功能并没有 - JavaIO https://whiteppure.github.io/iblog/posts/java/rookie-io/ Fri, 09 Apr 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/java/rookie-io/ 概念 Java IO通过数据流、序列化和文件系统提供系统输入和输出。 IO,即 in 和 out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。 传统的 IO 是通过流技术来处理的。 流(Stream),是一个抽象的概念, - JVM-方法区 https://whiteppure.github.io/iblog/posts/jvm/jvm-method-area/ Thu, 08 Apr 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/jvm/jvm-method-area/ Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 - JVM-堆 https://whiteppure.github.io/iblog/posts/jvm/jvm-heap/ Sat, 03 Apr 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/jvm/jvm-heap/ Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 - JVM-本地方法接口 https://whiteppure.github.io/iblog/posts/jvm/jvm-native-interface/ Fri, 02 Apr 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/jvm/jvm-native-interface/ 概念 简单地讲,一个Native Methodt是一个Java调用非Java代码的接囗。 一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。 这个特征并非Java所特有,很多其它的编程语言都有这一机制,比 - JVM-本地方法栈 https://whiteppure.github.io/iblog/posts/jvm/jvm-native-stack/ Fri, 02 Apr 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/jvm/jvm-native-stack/ Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 - JVM-虚拟机栈 https://whiteppure.github.io/iblog/posts/jvm/jvm-stack/ Sun, 28 Mar 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/jvm/jvm-stack/ Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 - JVM-程序计数寄存器 https://whiteppure.github.io/iblog/posts/jvm/jvm-pc-register/ Sat, 27 Mar 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/jvm/jvm-pc-register/ Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚 - JVM-JVM介绍 https://whiteppure.github.io/iblog/posts/jvm/jvm-start/ Fri, 05 Mar 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/jvm/jvm-start/ 为什么要学习JVM 大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。 一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要, - Nginx介绍 https://whiteppure.github.io/iblog/posts/essays/nginx-start/ Thu, 04 Mar 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/nginx-start/ Nginx介绍 Nginx (&ldquo;engine x&rdquo;)是一个高性能的HTTP和反向代理服务器,特点是占有内存少,并发能力强,事实上Nginx的并发能力确实在同类型的网页服务器中表现较好. Nginx专为性能优化而开发,性能是其最重要的考量,实现上非常注重效率, - 道德经 https://whiteppure.github.io/iblog/posts/books/books-daodejing/ Wed, 03 Mar 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/books/books-daodejing/ 第一章 道可道,非常道。名可名,非常名。 无名天地之始﹔有名万物之母。 故常无,欲以观其妙﹔常有,欲以观其徼。 此两者,同出而异名,同谓之玄。 玄之又玄,众妙之门。 第二章 天下皆知美之为美,斯恶已。 皆知善之为善,斯不善已。 有无相生,难易相成,长短相形, - 关于 https://whiteppure.github.io/iblog/about/ Sat, 20 Feb 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/about/ 搭建博客 本博客使用 hugo + GitHubPage 进行搭建,使用的主题为 zozo Designed by VarKai。 如果你想要使用 hugo 搭建博客,可以参考以下相关资料: hugo中文帮助文档: https://hugo.aiaide.com hugo中文文档: https://www.gohugo.org hugo给文章添加目录: https://www.ariesme.com/posts/2019/add_toc_for_hugo 使用hugo搭建个人博客站点: https://blog.coderzh.com/2015/08/29/hugo 不蒜子计数统计: https://busuanzi.ibruce.info 暗黑主 - 面向对象 https://whiteppure.github.io/iblog/posts/java/rookie-object-oriented/ Mon, 15 Feb 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/java/rookie-object-oriented/ 面向对象是一种编程思想,包括三大特性和六大原则,其中,三大特性指的是封装、继承和多态;六大原则指的是单一职责原则、开放封闭原则、迪米特原则、里氏替换原则、依赖倒置原则以及接口隔离原则,其中,单一职责原则是指一个类应该是一组相关性很高的函数和 - JVM-Java类加载机制 https://whiteppure.github.io/iblog/posts/jvm/jvm-classloader/ Fri, 05 Feb 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/jvm/jvm-classloader/ 类加载过程 在Java中,类加载器把一个类装入JVM中,要经过以下步骤: 加载、验证、准备、解析和初始化。其中验证,准备,解析统称为连接。 这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。 类加载器只负责class文 - Java运算 https://whiteppure.github.io/iblog/posts/java/rookie-operation/ Sat, 30 Jan 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/java/rookie-operation/ 运算符与表达式 运算符 运算符指明对操作数的运算方式。组成表达式的Java操作符有很多种。运算符按照其要求的操作数数目来分,可以有单目运算符、双目运算符和三目运算符,它们分别对应于1个、2个、3个操作数。 种类 运算符按其功能来分:有算术运算符、赋 - Java数据类型 https://whiteppure.github.io/iblog/posts/java/rookie-datatype/ Wed, 20 Jan 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/java/rookie-datatype/ 基本类型 Java语言提供了八种基本类型。六种数值类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. 俗称4类8种 这里只介绍称4类8种.实际上,JAVA中还存在另外一种基本类型 void,它也有对应的包装类java.lang.Void - Java异常 https://whiteppure.github.io/iblog/posts/java/rookie-exception/ Wed, 13 Jan 2021 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/java/rookie-exception/ 异常类型 Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:Error 和 Exception。 其中 Error 用来表示Java程序无法处理的错误;这类错误一般与硬件有关,与程序本身无关,通常由系统进行处理,程序本身无法捕获和处理。是不可控制的。 Exception 分为两种 - + + 20201124简历 + https://whiteppure.github.io/iblog/posts/resume/interview-resume-20201124/ + Tue, 24 Nov 2020 00:00:00 +0000 + https://whiteppure.github.io/iblog/posts/resume/interview-resume-20201124/ + 自我介绍 1998 · 李济芝 河北唐山 15176733539 &nbsp;m15176733539@163.com 专业技能 熟练使用 SSM,SpringBoot等框架技术; 熟练使用HTML,CSS等相关技术; 有Redis,VUE相关使用经验; 有对接第三方系统,调用外系统相关经验; 熟悉 MySQL,ORACLE.基本操作,熟练使 + SpringBoot整合docker https://whiteppure.github.io/iblog/posts/spring/springboot-docker/ Sun, 30 Aug 2020 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/spring/springboot-docker/ MacOS上安装docker 下载 国内下载网站: http://get.daocloud.io 不推荐下载docker版本太旧了 官网下载: https://docs.docker.com/get-started/#download-and-install-docker 或用homebrew进行下载安装 brew install --cask --appdir=/Applications docker 配置镜像 由于网速原因,可以配置一下国内的镜像加速器 中科大镜像: https://docker.mirrors.ustc.edu.cn 网易: https://hub-mirror.c.163.com 阿里云: https://&lt;你 - SpringBoot整合kafka https://whiteppure.github.io/iblog/posts/spring/springboot-kafka/ Thu, 20 Aug 2020 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/spring/springboot-kafka/ kafka介绍 kafka官网: http://kafka.apache.org kafka中文官网: https://kafka.apachecn.org Kafka是一种分布式的,基于发布/订阅的消息系统。主要特点如下: 以时间复杂度为O(1)的方式提供消息持久化能力,并保证即使对TB级以上数据也能保证常数时间的访问性能 高吞吐率。即使在非常 - 线程状态及创建方式 https://whiteppure.github.io/iblog/posts/essays/thread-state-and-created/ Mon, 20 Apr 2020 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/thread-state-and-created/ 线程状态及转换 线程状态共包含6种,6中状态又可以互相的转换。 新建状态(New): 创建了线程后尚未启动; 可运行状态(Runnable): 可能正在运行,也可能正在等待 CPU 时间片。包含了运行中(Running)和 就绪(Ready)状态; 就绪(Rea - Docker介绍 https://whiteppure.github.io/iblog/posts/essays/docker-start/ Tue, 07 Apr 2020 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/docker-start/ docker是什么 Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前流行的 Linux 容器解决方案。 Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样 - Java中常用到的锁 https://whiteppure.github.io/iblog/posts/essays/java-lock/ Tue, 07 Apr 2020 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/java-lock/ 公平锁 指多个线程按照申请锁的顺序来获取锁类似排队打饭 先来后到 优点: 所有的线程都能得到资源,不会饿死在队列中。 缺点: 吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。 非公平锁 指在多线程获取锁的顺序并 - Java中集合的线程不安全问题 https://whiteppure.github.io/iblog/posts/essays/java-thread-collection/ Sun, 05 Apr 2020 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/java-thread-collection/ ArrayList ArrayList线程不安全示例: public static void main(String[] args) { ArrayList&lt;String&gt; arrayList = new ArrayList&lt;&gt;(); for(int i=0; i&lt; 3; i++) { new Thread(() -&gt; { arrayList.add(UUID.randomUUID().toString()); System.out.println(arrayList); },String.valueOf(i)).start(); } } // ConcurrentModificationException 同步修改异常 Exception in thread &#34;8&#34; java.util.ConcurrentModificationException [null, 2041b613-8068-4ddd-9d01-305f5680d377] [null, 2041b613-8068-4ddd-9d01-305f5680d377, b3e0296d-e263-4632-a023-4267cdec5e25] [null, 2041b613-8068-4ddd-9d01-305f5680d377] 原因分析: 当某个线程正在执行 add()方法时,被某个线程打断,添加到一半被打断,没有被添加完 解决方案: 使用Vec - CAS原理 https://whiteppure.github.io/iblog/posts/essays/cas-principle/ Sat, 04 Apr 2020 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/cas-principle/ CAS CAS全称为Compare and Swap被译为比较并交换。是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。 java.util.concurrent.atomic 并发包下的所有原子类都是基于 CAS 来实现的。 以 AtomicInteger 原子整型类为例。 public class MainTest { public static void main(String[] args) { new AtomicInteger().compareAndSet(1,2); } } 以上面的代码为例 - SpringBoot整合redis https://whiteppure.github.io/iblog/posts/spring/springboot-redis/ Sun, 01 Mar 2020 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/spring/springboot-redis/ Redis介绍 redis是开源的一个高性能的 key-value 数据库。 主要特点 Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用 Redis支持数据的备份,即master-slave模式的数据备份 Redis 可以存储键与5种不同 - SpringBoot整合elasticsearch https://whiteppure.github.io/iblog/posts/spring/springboot-elasticsearch/ Sun, 09 Feb 2020 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/spring/springboot-elasticsearch/ 安装elasticsearch 要注意导入依赖的版本和安装elasticsearch的版本与springboot的兼容问题 用 docker 安装 elasticsearch 本例用elasticsearch-6.5.3和springboot-2.1.0.RELEASE版本 下载镜像: docker - + + 2019工作总结 + https://whiteppure.github.io/iblog/posts/worksummary/work-summary-2019/ + Sun, 01 Dec 2019 00:00:00 +0000 + https://whiteppure.github.io/iblog/posts/worksummary/work-summary-2019/ + 本人在进入公司起,期间一直对自己要求严谨,遵守公司的相应制度. 在过去的一个月时间里,我参与了贵州银行的电子验印系统的开发,一直努力完成和完善分配给我的任务,在这一个月发现了自身还有很多的不足,所以抱着虚心学习的态度,学习公司的开发流程,了解 + Vue2.0学习笔记 https://whiteppure.github.io/iblog/posts/essays/vue2-note/ Thu, 23 May 2019 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/essays/vue2-note/ 参考资料 vue官方文档: https://cn.vuejs.org/v2/guide vue参考视频资料: https://www.bilibili.com/video/av50680998 vue菜鸟教程文档: https://www.runoob.com/vue2/vue-tutorial.html vue-组件 参考资料: https://cn.vuejs.org/v2/guide/components.html#ad 组件是可复用的 Vue 实例,且带有一个名字. 组件的出现是为了拆分vue实例的代码量,能够让我们以不同的组件,来划分不同的功能模块,将来我们需要什么样的功 - Js雪花飘落 https://whiteppure.github.io/iblog/posts/toy/js-snow/ Tue, 25 Dec 2018 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/toy/js-snow/ index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;snow&lt;/title&gt; &lt;/head&gt; &lt;style&gt; html { width: 100%; } body { margin: 0; padding: 0; overflow-y: hidden; width: 100%; } .header { width: 100%; height: 315px; background: url(&#34;images/header-bg.png&#34;) repeat; } .snow { position: relative; height: inherit; width: 960px; background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) no-repeat 0 0;; margin: 0 auto; animation: auto 10s linear infinite; } /* 下雪动画 插入两个背景图片*/ @keyframes auto { from { background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) repeat 0 0; } to { background: url(&#34;images/con-bg.png&#34;) no-repeat 0 204px, url(&#34;images/snow-bg.png&#34;) repeat 0 1000px; } } tree, snow { position: absolute; } tree { width: 112px; height: 137px; background: url(&#34;images/tree.png&#34;); - Js下雨特效 https://whiteppure.github.io/iblog/posts/toy/js-rain/ Mon, 10 Dec 2018 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/toy/js-rain/ index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt; rain &lt;/title&gt; &lt;style&gt; html { width: 100%; } body { width: 100%; margin: 0; padding: 0; background-color: #000; } .rain { display: block; } embed { display: block; } &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;!-- 2、使用hidden=&#34;true&#34;表示隐藏音乐播放按钮,相反使用hidden=&#34;false&#34;表示开启音乐播放按钮。 3、使用a - Js换肤特效 https://whiteppure.github.io/iblog/posts/toy/js-trans-skin/ Wed, 14 Nov 2018 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/toy/js-trans-skin/ index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;换肤特效&lt;/title&gt; &lt;style type=&#34;text/css&#34;&gt; body { margin: 0; background-image: url(&#34;images/1.jpg&#34;); background-size: cover; } ul { margin: 0; padding: 0; list-style-type: none; } .bg-list { display: none; margin: 0; width: 100%; height: 200px; background: rgba(0, 0, 0, 0.5); } .img-wrap { height: 200px; display: flex; justify-content: space-around; align-items: center; } .tab-btn { background-image: url(&#34;images/upseek.png&#34;); height: 50px; width: 50px; position: fixed; top: 0; right: 0; } .tab-btn:hover { background-position-y: -63.6px; } &lt;/style&gt; &lt;/head&gt; &lt;body&gt; &lt;div class=&#34;bg-list&#34;&gt; &lt;ul class=&#34;img-wrap&#34;&gt; &lt;li class=&#34;img-item&#34; data-src=&#34;images/1.jpg&#34;&gt; &lt;img src=&#34;images/1-1.jpg&#34; width=&#34;160px&#34;/&gt; &lt;/li&gt; - Js折纸导航栏 https://whiteppure.github.io/iblog/posts/toy/js-paper-folding/ Thu, 25 Oct 2018 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/toy/js-paper-folding/ index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;折纸导航栏&lt;/title&gt; &lt;/head&gt; &lt;style&gt; *{ margin: 0; padding: 0; } .content{ position: relative; width: 400px; height: 30px; margin: 50px auto; /*-webkit-perspective: 1000px; -moz-perspective: 1000px; -ms-perspective: 1000px;*/ perspective: 1000px;/*景深相当于眼睛距离元素的位置距离*/ } .content .open{ transform: rotateX(0); animation: open 1s linear; } @keyframes open { 0%{ transform: rotateX(-90deg); } 20%{ transform:rotateX(30deg); } 40%{ transform:rotateX(-60deg); } 60%{ transform:rotateX(60deg); } 80%{ transform:rotateX(-30deg); - Js表白神器 https://whiteppure.github.io/iblog/posts/toy/js-love-heart/ Sun, 14 Oct 2018 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/toy/js-love-heart/ index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;love&lt;/title&gt; &lt;style&gt; *{ margin: 0; padding: 0; } body{ background-color: #000; background-size: cover; overflow-y: hidden; } .love{ width: 400px; height: 400px; /*background-color: #7c7c7c;*/ margin: 130px auto; animation: move 1s infinite alternate; } @keyframes move { 100%{ transform: scale(1.5); } } .left{ float: left; width: 150px; height: 250px; background-color: #FF0000; border-radius: 75px 75px 0 5px; -webkit-transform: rotate(-45deg); -moz-transform: rotate(-45deg); -ms-transform: rotate(-45deg); -o-transform: rotate(-45deg); transform: rotate(-45deg); margin-left: 85px; box-shadow: 0 0 20px #FF0000; animation: shadow 1s infinite alternate; } @keyframes shadow { 100%{ box-shadow: 0 0 100px #FF0000; } } .right{ float: left; width: 150px; height: 250px; background-color: #FF0000; border-radius: 75px 75px 5px 0; -webkit-transform: rotate(45deg); -moz-transform: rotate(45deg); -ms-transform: rotate(45deg); -o-transform: rotate(45deg); transform: rotate(45deg); - Js懒加载 https://whiteppure.github.io/iblog/posts/toy/js-loadding-lazy/ Fri, 21 Sep 2018 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/toy/js-loadding-lazy/ index.html &lt;!doctype html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;meta name=&#34;Generator&#34; content=&#34;EditPlus®&#34;&gt; &lt;meta name=&#34;Author&#34; content=&#34;&#34;&gt; &lt;meta name=&#34;Keywords&#34; content=&#34;&#34;&gt; &lt;meta name=&#34;Description&#34; content=&#34;&#34;&gt; &lt;title&gt;懒加载技术&lt;/title&gt; &lt;style&gt; *{ margin: 0; padding:0; } body{ background: rgb(0,0,0); } .box{ overflow: hidden; width: 948px; background-color: #7c7c7c; margin: 50px auto; -webkit-border-radius: 10px; -moz-border-radius: 10px; border-radius: 10px; } .box img{ float: left; display: block; width: 300px; height: 150px; margin: - Js五子棋 https://whiteppure.github.io/iblog/posts/toy/js-gomoku/ Mon, 10 Sep 2018 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/toy/js-gomoku/ index.html &lt;!DOCTYPE html PUBLIC &#34;-//W3C//DTD XHTML 1.0 Transitional//EN&#34; &#34;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&#34;&gt; &lt;html xmlns=&#34;http://www.w3.org/1999/xhtml&#34;&gt; &lt;head&gt; &lt;meta http-equiv=&#34;Content-Type&#34; content=&#34;text/html; charset=UTF-8&#34; /&gt; &lt;title&gt;五子棋&lt;/title&gt; &lt;meta name=&#34;viewport&#34; content=&#34;device-width; initial-scale=1.0;&#34; /&gt; &lt;style&gt; #c1 { display: block; margin: 60px auto; box-shadow: 1px 1px 5px #000000; } &lt;/style&gt; &lt;script src=&#34;js/index.js&#34;&gt;&lt;/script&gt; &lt;/head&gt; &lt;body&gt; &lt;canvas id=&#34;c1&#34; width=&#34;450px&#34; height=&#34;450px&#34;&gt;&lt;/canvas&gt; &lt;/body&gt; &lt;/html&gt; index.js window.onload = function(){ var oC = document.getElementById(&#39;c1&#39;); var oGc = oC.getContext(&#39;2d&#39;); var over = false; oGc.strokeStyle = &#34;#bfbfbf&#34;; //绘制棋盘 for(var i=0;i&lt;15;i++){ oGc.moveTo(15+i*30,15); oGc.lineTo(15+i*30,435); oGc.stroke(); oGc.moveTo(15,15+i*30); oGc.lineTo(435,15+i*30); oGc.stroke(); } /* AI难点解析 赢法 - Js滑块拖拽 https://whiteppure.github.io/iblog/posts/toy/js-box-drag/ Sat, 08 Sep 2018 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/toy/js-box-drag/ index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;滑块拖拽&lt;/title&gt; &lt;/head&gt; &lt;style&gt; body { margin: 0; padding: 0; user-select: none; } .content { position: relative; width: 300px; height: 40px; margin: 50px auto; background-color: #E8E8EB; text-align: center; line-height: 40px; } .rect { position: absolute; width: 100%; height: 100%; } .rect .bg { position: absolute; left: 0; top: 0; z-index: 1; width: 0; height: 100%; background: rgba(122,194,60,.4); } .rect .move { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; width: 45px; height: 40px; position: absolute; top: 0; left: 0; background-color: #fff; border: 1px solid #cccccc; - Js生日礼物 https://whiteppure.github.io/iblog/posts/toy/js-birthday-gift/ Fri, 24 Aug 2018 00:00:00 +0000 - https://whiteppure.github.io/iblog/posts/toy/js-birthday-gift/ index.html &lt;!DOCTYPE html&gt; &lt;html lang=&#34;en&#34;&gt; &lt;head&gt; &lt;meta charset=&#34;UTF-8&#34;&gt; &lt;title&gt;card&lt;/title&gt; &lt;style&gt; body,html{ width: 100%; height: 100%; } body{ display: flex;/*弹性盒模型*/ justify-content: center;/*水平对齐 盒子位于中心*/ align-items: center;/*竖直对齐 居中对齐*/ background-color: yellow; perspective: 1000px;/*景深:眼到屏幕的距离*/ } body,h1,p{ margin: 0; } .card{ width: 520px; height: 350px; border-radius: 15px; background: linear-gradient(#020333 70%,#fff 75%);/* - diff --git a/docs/page/2/index.html b/docs/page/2/index.html index 785042fa..7cba6be5 100644 --- a/docs/page/2/index.html +++ b/docs/page/2/index.html @@ -1,7 +1,7 @@ - + @@ -128,12 +128,12 @@

-

规范编写Java代码总结

+

如何做好程序设计功能

-

编码规范 我们为什么要遵守规范来编码? 是因为通常在编码过程中我们不只自己进行开发,通常需要一个团队来进行,开发好之后还需要维护,所以编码规范就显的尤为重要。 代码维护时间比较长,那么保证代码可读性就显得很重要。作为一个程序员,咱们得有点追求和信......

+

产品需求澄清、PN排期及任务分解 开发设计评审 功能设计流程图 与外部系统交互、本系统模块之间流程,比较好用的画圈软件draw .io或在线的process on 数据库设计 从DDD角度界限上下文、ER图、评审表结构设计是否合理,表的关联关系是否合理、是......

@@ -142,12 +142,14 @@

规范编写Java代码总结 - 2021.11.25 + 2022.08.02 + Java + 设计 @@ -159,12 +161,12 @@

规范编写Java代码总结
-

网络编程

+

20220422简历

-

网络协议 以下内容摘自百度百科: https://baike.baidu.com/item/网络协议/328636 https://baike.baidu.com/item/网络七层协议/6056879 网络协议指的是计算机网络中互相通信的对等实体......

+

自我介绍 1998 · 李济芝 河北唐山 15176733539  m15176733539@163.com 本人有严谨的工作态度与高质量意识;能查阅各种开发技术手册,具有独立解决问题的能力。具备扎实的Java基础和三年开发经验,有良好的编程风格,独立熟练使用Spring全家桶等常用类库开发Java服务端程序、对SQL......

@@ -173,15 +175,15 @@

网络编程

- 2021.11.19 + 2022.04.22 - Java + 简历 - 网络 + 求职 @@ -192,12 +194,12 @@

网络编程

-

MQ详解

+

Java小程序集合

-

概念 MQ 即 messagequeue 消息队列,是分布式系统的重要组件,主要解决异步消息,应用解耦,消峰等问题。从而实现高可用,高性能,可伸缩和最终一致性的架构。使用较多的MQ有:activeMQ,rabbitMQ,kafka,metaMQ。 MQ优点 异步消息处理:可以......

+

写在前面 本文中所涉及的程序均为Java开发,如果您想要直接使用这些工具需要提前配置Java环境。所涉及到的程序均提供完整代码,如果您有兴趣可以尝试运行。 使用java -jar命令启动 某些程序功能并不是很完善,但是也可以凑合着用,写这些程序的主......

@@ -206,13 +208,15 @@

MQ详解

- 2021.10.19 + 2022.04.09 - MQ + Java + + 玩具 @@ -223,12 +227,12 @@

MQ详解

-

Java集合

+

数据结构与算法

-

概述 Java中的集合主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 如果你看过ArrayList类源码,就知道ArrayList底层是通过数组来存储元素的,所以如果严格来说,数组也算集合的一种......

+

数据结构 数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构,学好数据结构可以编写出更有效率的代码。数据结构是算法的基础,想要学好算法,就必须把数据结构学到位。 数据结构包括:线性结构、非线性结构。 线性结构作为最常用的数据结构,......

@@ -237,7 +241,7 @@

Java集合

- 2021.10.04 + 2021.12.10 @@ -245,7 +249,7 @@

Java集合

Java - Java基础 + 算法
@@ -256,12 +260,12 @@

Java集合

-

Java反射

+

规范编写Java代码总结

-

概述 什么是反射 在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。 反射是Java语言的一个特性,它允许程序......

+

编码规范 我们为什么要遵守规范来编码? 是因为通常在编码过程中我们不只自己进行开发,通常需要一个团队来进行,开发好之后还需要维护,所以编码规范就显的尤为重要。 代码维护时间比较长,那么保证代码可读性就显得很重要。作为一个程序员,咱们得有点追求和信......

@@ -270,15 +274,13 @@

Java反射

- 2021.10.02 + 2021.11.25 - Java - - Java基础 + 设计 @@ -289,12 +291,12 @@

Java反射

-

常见故障排查及程序配置

+

网络编程

-

故障排查基础 收录Linux常用命令,以下命令来自https://www.bilibili.com/video/BV14A411378a 关机/重启/注销 常用命令 作用 shutdown -h now 即刻关机 shutdown -h 10 10分钟后关机 shutdown -h 11:00 11:00关机 shutdown -h +10 预定时间关机(10......

+

网络协议 以下内容摘自百度百科: https://baike.baidu.com/item/网络协议/328636 https://baike.baidu.com/item/网络七层协议/6056879 网络协议指的是计算机网络中互相通信的对等实体......

@@ -303,13 +305,15 @@

常见故障排查及程序配置 - 2021.09.08 + 2021.11.19 - 技巧 + Java + + 网络 @@ -320,12 +324,12 @@

常见故障排查及程序配置
-

分布式事务详解

+

MQ详解

-

基础概念 什么是事务 什么是事务?举个例子:你去超市买东西,“一手交钱,一手交货"就是一个事务的例子,交钱和交货必须同时成功,事务才算成功,其中有一个环节失败,事务将会撤销所有已成功的活动。 所以事务可以看作是一次重大的活动......

+

概念 MQ 即 messagequeue 消息队列,是分布式系统的重要组件,主要解决异步消息,应用解耦,消峰等问题。从而实现高可用,高性能,可伸缩和最终一致性的架构。使用较多的MQ有:activeMQ,rabbitMQ,kafka,metaMQ。 MQ优点 异步消息处理:可以......

@@ -334,15 +338,13 @@

分布式事务详解 - 2021.08.02 + 2021.10.19 - 事务 - - 分布式 + MQ @@ -353,12 +355,12 @@

分布式事务详解
-

Object类方法

+

Java集合

-

概览 Object 类位于 java.lang 包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承Object,成为Object的子类。 Object类可以显示继承,也可以隐式继承,效果都是一样的。 class A extends Object{ // to do } class A { // to do } Java Objec......

+

概述 Java中的集合主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表。 如果你看过ArrayList类源码,就知道ArrayList底层是通过数组来存储元素的,所以如果严格来说,数组也算集合的一种......

@@ -367,7 +369,7 @@

Object类方法<
- 2021.07.10 + 2021.10.04 @@ -386,12 +388,12 @@

Object类方法<
-

微服务治理

+

Java反射

-

什么是微服务架构 In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API。 These services are built around business capabilities and independently deployable by fully automated deployment machinery。 There is a bare minimum of centralized management of these services, which may be written in different programming......

+

概述 什么是反射 在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制。 反射是Java语言的一个特性,它允许程序......

@@ -400,13 +402,15 @@

微服务治理

- 2021.06.21 + 2021.10.02 - 分布式 + Java + + Java基础 @@ -417,12 +421,12 @@

微服务治理

-

Redis详解

+

常见故障排查及程序配置

-

Redis概述 参考文章: https://www.runoob.com/redis/redis-intro.html https://www.redis.com.cn/redis-interview-questions.html 什么是Redis Redis(Remote Dictionary Server) Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API 的非关系型数据库。 简而言之,Redis是一个可基于内存亦可持久......

+

故障排查基础 收录Linux常用命令,以下命令来自https://www.bilibili.com/video/BV14A411378a 关机/重启/注销 常用命令 作用 shutdown -h now 即刻关机 shutdown -h 10 10分钟后关机 shutdown -h 11:00 11:00关机 shutdown -h +10 预定时间关机(10......

@@ -431,13 +435,13 @@

Redis详解

- 2021.06.17 + 2021.09.08 - Redis + 技巧 @@ -448,12 +452,12 @@

Redis详解

-

Spring详解

+

分布式事务详解

-

概览 Spring是一个轻量级的Java开源框架,为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IOC)和面向切面(AOP)。 简单来说,Spring是一个分层的JavaSE/EE 一站式轻量级开源框架。在每一层都提供支持。......

+

基础概念 什么是事务 什么是事务?举个例子:你去超市买东西,“一手交钱,一手交货"就是一个事务的例子,交钱和交货必须同时成功,事务才算成功,其中有一个环节失败,事务将会撤销所有已成功的活动。 所以事务可以看作是一次重大的活动......

@@ -462,15 +466,15 @@

Spring详解

- 2021.05.13 + 2021.08.02 - Java + 事务 - spring + 分布式 @@ -481,12 +485,12 @@

Spring详解

-

面试Java可能会被问到的问题

+

Object类方法

-

面试必问 自我介绍一下 你有什么职业规划 你为什么要离职 说一下你的优缺点 你的期望薪资是多少 你为什么要选择我们公司 你能否接受加班 你有对象了吗 你还有什么问题要问的吗 基础 说一下UDP、TCP及http与https 如何保证线程安全 线程池工作原理 如何避免死......

+

概览 Object 类位于 java.lang 包中,编译时会自动导入,我们创建一个类时,如果没有明确继承一个父类,那么它就会自动继承Object,成为Object的子类。 Object类可以显示继承,也可以隐式继承,效果都是一样的。 class A extends Object{ // to do } class A { // to do } Java Objec......

@@ -495,7 +499,7 @@

面试Java可能会
- 2021.05.11 + 2021.07.10 @@ -503,7 +507,7 @@

面试Java可能会 Java - 求职 + Java基础 @@ -514,12 +518,12 @@

面试Java可能会
-

JVM-垃圾回收器

+

微服务治理

-

垃圾回收器分类 垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。 由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。 Java不同版本新特性学习思路: 语法层面:Lambda表达式、s......

+

什么是微服务架构 In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API。 These services are built around business capabilities and independently deployable by fully automated deployment machinery。 There is a bare minimum of centralized management of these services, which may be written in different programming......

@@ -528,15 +532,13 @@

JVM-垃圾回收器 - 2021.05.06 + 2021.06.21 - Java - - JVM + 分布式 @@ -547,12 +549,12 @@

JVM-垃圾回收器
-

Java多线程

+

Redis详解

-

相关概念 线程与进程 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。例如,一个正在运行的程序的实例就是一个进程。 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 一条线程指的是进程中一个单一......

+

Redis概述 参考文章: https://www.runoob.com/redis/redis-intro.html https://www.redis.com.cn/redis-interview-questions.html 什么是Redis Redis(Remote Dictionary Server) Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API 的非关系型数据库。 简而言之,Redis是一个可基于内存亦可持久......

@@ -561,15 +563,13 @@

Java多线程

- 2021.05.05 + 2021.06.17 - Java - - Java基础 + Redis @@ -580,12 +580,12 @@

Java多线程

-

HashMap详解

+

Spring详解

-

相关概念 capacity: 容量,默认16; loadFactor: 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容; threshold: 阈值;阈值......

+

概览 Spring是一个轻量级的Java开源框架,为了解决企业应用开发的复杂性而创建的。Spring的核心是控制反转(IOC)和面向切面(AOP)。 简单来说,Spring是一个分层的JavaSE/EE 一站式轻量级开源框架。在每一层都提供支持。......

@@ -594,7 +594,7 @@

HashMap详解

- 2021.05.03 + 2021.05.13 @@ -602,7 +602,7 @@

HashMap详解

Java - 集合 + spring
@@ -613,12 +613,12 @@

HashMap详解

-

JVM-相关概念

+

面试Java可能会被问到的问题

-

内存溢出 内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。 官方文档中对内存溢出的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。 由于GC一......

+

面试必问 自我介绍一下 你有什么职业规划 你为什么要离职 说一下你的优缺点 你的期望薪资是多少 你为什么要选择我们公司 你能否接受加班 你有对象了吗 你还有什么问题要问的吗 基础 说一下UDP、TCP及http与https 如何保证线程安全 线程池工作原理 如何避免死......

@@ -627,15 +627,13 @@

JVM-相关概念

- 2021.04.27 + 2021.05.11 - Java - - JVM + 求职 @@ -646,12 +644,12 @@

JVM-相关概念

-

面试中常见的问题

+

JVM-垃圾回收器

-

面试常见问题 自我介绍 个人经历可以进行适当包装,但是不能造假,一方面如果一旦被人拆穿,后果就不用我说了吧,另一方面如果你说谎,说了一些你自己不感兴趣的项目,在入职之后可能会被分配到该项目上; 在介绍的时候要说明你对面试的公司有什么用,根据不同类......

+

垃圾回收器分类 垃圾收集器没有在规范中进行过多的规定,可以由不同的厂商、不同版本的JVM来实现。 由于JDK的版本处于高速迭代过程中,因此Java发展至今已经衍生了众多的GC版本。 Java不同版本新特性学习思路: 语法层面:Lambda表达式、s......

@@ -660,7 +658,7 @@

面试中常
- 2021.04.23 + 2021.05.06 @@ -668,7 +666,7 @@

面试中常 Java - 求职 + JVM @@ -679,12 +677,12 @@

面试中常
-

JVM-垃圾回收

+

Java多线程

-

垃圾回收 垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。 垃圾收集机制是Java的招牌能力,极大地提高了开发效率。 如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展......

+

相关概念 线程与进程 进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。例如,一个正在运行的程序的实例就是一个进程。 线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。 一条线程指的是进程中一个单一......

@@ -693,7 +691,7 @@

JVM-垃圾回收

- 2021.04.21 + 2021.05.05 @@ -701,7 +699,7 @@

JVM-垃圾回收

Java - JVM + Java基础
@@ -712,12 +710,12 @@

JVM-垃圾回收

-

JVM-执行引擎

+

HashMap详解

-

概述 执行引擎是Java虚拟机核心的组成部分之一,属于JVM的下层,里面包括 解释器、及时编译器、垃圾回收器。 “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力, 其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统......

+

相关概念 capacity: 容量,默认16; loadFactor: 负载因子,表示HashMap满的程度,默认值为0.75f,也就是说默认情况下,当HashMap中元素个数达到了容量的3/4的时候就会进行自动扩容; threshold: 阈值;阈值......

@@ -726,7 +724,7 @@

JVM-执行引擎

- 2021.04.15 + 2021.05.03 @@ -734,7 +732,7 @@

JVM-执行引擎

Java - JVM + 集合
@@ -745,12 +743,12 @@

JVM-执行引擎

-

JVM-直接内存

+

JVM-相关概念

-

直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。 直接内存是在Java堆外的、直接向系统申请的内存区间。 操作直接内存演示代码: public class MainTest { public static void main(String[] args) { ByteBuffer allocate = ByteBuffer.allocate(1024 * 1024 * 1024); System.out.println(&#......

+

内存溢出 内存溢出(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。 官方文档中对内存溢出的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。 由于GC一......

@@ -759,7 +757,7 @@

JVM-直接内存

- 2021.04.14 + 2021.04.27 diff --git a/docs/page/3/index.html b/docs/page/3/index.html index 6445f32a..b132afbd 100644 --- a/docs/page/3/index.html +++ b/docs/page/3/index.html @@ -1,7 +1,7 @@ - + @@ -128,12 +128,12 @@

-

JVM-Java对象

+

面试中常见的问题

-

对象实例化 对象的创建方式 使用new关键字创建:最常见的方式、单例类中调用getInstance的静态类方法,XXXFactory的静态方法; 使用反射方式创建: 使用Class的newInstance方法:在JDK9里面被标记为过时的方法,因为......

+

面试常见问题 自我介绍 个人经历可以进行适当包装,但是不能造假,一方面如果一旦被人拆穿,后果就不用我说了吧,另一方面如果你说谎,说了一些你自己不感兴趣的项目,在入职之后可能会被分配到该项目上; 在介绍的时候要说明你对面试的公司有什么用,根据不同类......

@@ -142,15 +142,13 @@

JVM-Java对象

- 2021.04.12 + 2021.04.23 - Java - - JVM + 求职 @@ -161,12 +159,12 @@

JVM-Java对象

-

Java语法糖

+

JVM-垃圾回收

-

原文地址:https://www.jianshu.com/p/0f967298a5d7 语法糖 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法, 这种语法对语言的功能并没有......

+

垃圾回收 垃圾收集,不是Java语言的伴生产物。早在1960年,第一门开始使用内存动态分配和垃圾收集技术的Lisp语言诞生。 垃圾收集机制是Java的招牌能力,极大地提高了开发效率。 如今,垃圾收集几乎成为现代语言的标配,即使经过如此长时间的发展......

@@ -175,7 +173,7 @@

Java语法糖

- 2021.04.10 + 2021.04.21 @@ -183,7 +181,7 @@

Java语法糖

Java - 转载 + JVM
@@ -194,12 +192,12 @@

Java语法糖

-

JavaIO

+

JVM-执行引擎

-

概念 Java IO通过数据流、序列化和文件系统提供系统输入和输出。 IO,即 in 和 out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。 传统的 IO 是通过流技术来处理的。 流(Stream),是一个抽象的概念,......

+

概述 执行引擎是Java虚拟机核心的组成部分之一,属于JVM的下层,里面包括 解释器、及时编译器、垃圾回收器。 “虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力, 其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统......

@@ -208,7 +206,7 @@

JavaIO

- 2021.04.09 + 2021.04.15 @@ -216,7 +214,7 @@

JavaIO

Java - Java基础 + JVM
@@ -227,12 +225,12 @@

JavaIO

-

JVM-方法区

+

JVM-直接内存

-

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......

+

直接内存不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。 直接内存是在Java堆外的、直接向系统申请的内存区间。 操作直接内存演示代码: public class MainTest { public static void main(String[] args) { ByteBuffer allocate = ByteBuffer.allocate(1024 * 1024 * 1024); System.out.println(&#......

@@ -241,7 +239,7 @@

JVM-方法区

- 2021.04.08 + 2021.04.14 @@ -260,12 +258,12 @@

JVM-方法区

-

JVM-堆

+

JVM-Java对象

-

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......

+

对象实例化 对象的创建方式 使用new关键字创建:最常见的方式、单例类中调用getInstance的静态类方法,XXXFactory的静态方法; 使用反射方式创建: 使用Class的newInstance方法:在JDK9里面被标记为过时的方法,因为......

@@ -274,7 +272,7 @@

JVM-堆

- 2021.04.03 + 2021.04.12 @@ -293,12 +291,12 @@

JVM-堆

-

JVM-本地方法接口

+

Java语法糖

-

概念 简单地讲,一个Native Methodt是一个Java调用非Java代码的接囗。 一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。 这个特征并非Java所特有,很多其它的编程语言都有这一机制,比......

+

原文地址:https://www.jianshu.com/p/0f967298a5d7 语法糖 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法, 这种语法对语言的功能并没有......

@@ -307,7 +305,7 @@

JVM-本地方法接口<
- 2021.04.02 + 2021.04.10 @@ -315,7 +313,7 @@

JVM-本地方法接口< Java - JVM + 转载 @@ -326,12 +324,12 @@

JVM-本地方法接口<
-

JVM-本地方法栈

+

JavaIO

-

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......

+

概念 Java IO通过数据流、序列化和文件系统提供系统输入和输出。 IO,即 in 和 out,也就是输入和输出,指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接。 传统的 IO 是通过流技术来处理的。 流(Stream),是一个抽象的概念,......

@@ -340,7 +338,7 @@

JVM-本地方法栈

- 2021.04.02 + 2021.04.09 @@ -348,7 +346,7 @@

JVM-本地方法栈

Java - JVM + Java基础
@@ -359,7 +357,7 @@

JVM-本地方法栈

-

JVM-虚拟机栈

+

JVM-方法区

@@ -373,7 +371,7 @@

JVM-虚拟机栈

- 2021.03.28 + 2021.04.08 @@ -392,7 +390,7 @@

JVM-虚拟机栈

-

JVM-程序计数寄存器

+

JVM-堆

@@ -406,7 +404,7 @@

JVM-程序计数寄存器 - 2021.03.27 + 2021.04.03 @@ -425,12 +423,12 @@

JVM-程序计数寄存器
-

JVM-JVM介绍

+

JVM-本地方法接口

-

为什么要学习JVM 大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。 一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要,......

+

概念 简单地讲,一个Native Methodt是一个Java调用非Java代码的接囗。 一个Native Method是这样一个Java方法:该方法的实现由非Java语言实现,比如C。 这个特征并非Java所特有,很多其它的编程语言都有这一机制,比......

@@ -439,7 +437,7 @@

JVM-JVM介绍

- 2021.03.05 + 2021.04.02 @@ -449,8 +447,6 @@

JVM-JVM介绍

JVM - 使用介绍 -
@@ -460,12 +456,12 @@

JVM-JVM介绍

-

Nginx介绍

+

JVM-本地方法栈

-

Nginx介绍 Nginx (“engine x”)是一个高性能的HTTP和反向代理服务器,特点是占有内存少,并发能力强,事实上Nginx的并发能力确实在同类型的网页服务器中表现较好. Nginx专为性能优化而开发,性能是其最重要的考量,实现上非常注重效率,......

+

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......

@@ -474,15 +470,15 @@

Nginx介绍

- 2021.03.04 + 2021.04.02 - 使用介绍 + Java - nginx + JVM @@ -493,12 +489,12 @@

Nginx介绍

-

道德经

+

JVM-虚拟机栈

-

第一章 道可道,非常道。名可名,非常名。 无名天地之始﹔有名万物之母。 故常无,欲以观其妙﹔常有,欲以观其徼。 此两者,同出而异名,同谓之玄。 玄之又玄,众妙之门。 第二章 天下皆知美之为美,斯恶已。 皆知善之为善,斯不善已。 有无相生,难易相成,长短相形,......

+

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......

@@ -507,13 +503,15 @@

道德经

- 2021.03.03 + 2021.03.28 - 书籍 + Java + + JVM @@ -524,12 +522,12 @@

道德经

-

面向对象

+

JVM-程序计数寄存器

-

面向对象是一种编程思想,包括三大特性和六大原则,其中,三大特性指的是封装、继承和多态;六大原则指的是单一职责原则、开放封闭原则、迪米特原则、里氏替换原则、依赖倒置原则以及接口隔离原则,其中,单一职责原则是指一个类应该是一组相关性很高的函数和......

+

Java虚拟机定义了若干种程序运行期间会使用到的运行时数据区,其中有一些会随着虚拟机启动而创建,随着虚拟机退出而销毁。 另外一些则是与线程一一对应的,这些与线程对应的数据区域会随着线程开始和结束而创建和销毁。 运行时数据区域包括 程序计数寄存器 虚......

@@ -538,7 +536,7 @@

面向对象

- 2021.02.15 + 2021.03.27 @@ -546,7 +544,7 @@

面向对象

Java - Java基础 + JVM
@@ -557,12 +555,12 @@

面向对象

-

JVM-Java类加载机制

+

JVM-JVM介绍

-

类加载过程 在Java中,类加载器把一个类装入JVM中,要经过以下步骤: 加载、验证、准备、解析和初始化。其中验证,准备,解析统称为连接。 这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。 类加载器只负责class文......

+

为什么要学习JVM 大部分Java开发人员,除了会在项目中使用到与Java平台相关的各种高精尖技术,对于Java技术的核心Java虚拟机了解甚少。 一些有一定工作经验的开发人员,打心眼儿里觉得SSM、微服务等上层技术才是重点,基础技术并不重要,......

@@ -571,7 +569,7 @@

JVM-Java类加载机制

- 2021.02.05 + 2021.03.05 @@ -581,6 +579,8 @@

JVM-Java类加载机制

JVM + 使用介绍 +
@@ -590,12 +590,12 @@

JVM-Java类加载机制

-

Java运算

+

Nginx介绍

-

运算符与表达式 运算符 运算符指明对操作数的运算方式。组成表达式的Java操作符有很多种。运算符按照其要求的操作数数目来分,可以有单目运算符、双目运算符和三目运算符,它们分别对应于1个、2个、3个操作数。 种类 运算符按其功能来分:有算术运算符、赋......

+

Nginx介绍 Nginx (“engine x”)是一个高性能的HTTP和反向代理服务器,特点是占有内存少,并发能力强,事实上Nginx的并发能力确实在同类型的网页服务器中表现较好. Nginx专为性能优化而开发,性能是其最重要的考量,实现上非常注重效率,......

@@ -604,15 +604,15 @@

Java运算

- 2021.01.30 + 2021.03.04 - Java + 使用介绍 - Java基础 + nginx @@ -623,12 +623,12 @@

Java运算

-

Java数据类型

+

道德经

-

基本类型 Java语言提供了八种基本类型。六种数值类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. 俗称4类8种 这里只介绍称4类8种.实际上,JAVA中还存在另外一种基本类型 void,它也有对应的包装类java.lang.Void......

+

第一章 道可道,非常道。名可名,非常名。 无名天地之始﹔有名万物之母。 故常无,欲以观其妙﹔常有,欲以观其徼。 此两者,同出而异名,同谓之玄。 玄之又玄,众妙之门。 第二章 天下皆知美之为美,斯恶已。 皆知善之为善,斯不善已。 有无相生,难易相成,长短相形,......

@@ -637,15 +637,13 @@

Java数据类型

- 2021.01.20 + 2021.03.03 - Java - - Java基础 + 书籍 @@ -656,12 +654,12 @@

Java数据类型

-

Java异常

+

面向对象

-

异常类型 Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:Error 和 Exception。 其中 Error 用来表示Java程序无法处理的错误;这类错误一般与硬件有关,与程序本身无关,通常由系统进行处理,程序本身无法捕获和处理。是不可控制的。 Exception 分为两种......

+

面向对象是一种编程思想,包括三大特性和六大原则,其中,三大特性指的是封装、继承和多态;六大原则指的是单一职责原则、开放封闭原则、迪米特原则、里氏替换原则、依赖倒置原则以及接口隔离原则,其中,单一职责原则是指一个类应该是一组相关性很高的函数和......

@@ -670,7 +668,7 @@

Java异常

- 2021.01.13 + 2021.02.15 @@ -689,12 +687,12 @@

Java异常

-

SpringBoot整合docker

+

JVM-Java类加载机制

-

MacOS上安装docker 下载 国内下载网站: http://get.daocloud.io 不推荐下载docker版本太旧了 官网下载: https://docs.docker.com/get-started/#download-and-install-docker 或用homebrew进行下载安装 brew install --cask --appdir=/Applications docker 配置镜像 由于网速原因,可以配置一下国内的镜像加速器 中科大镜像: https://docker.mirrors.ustc.edu.cn 网易: https://hub-mirror.c.163.com 阿里云: https://<你......

+

类加载过程 在Java中,类加载器把一个类装入JVM中,要经过以下步骤: 加载、验证、准备、解析和初始化。其中验证,准备,解析统称为连接。 这5个阶段一般是顺序发生的,但在动态绑定的情况下,解析阶段发生在初始化阶段之后。 类加载器只负责class文......

@@ -703,15 +701,15 @@

SpringBoot整合docker<
- 2020.08.30 + 2021.02.05 - springboot + Java - docker + JVM @@ -722,12 +720,12 @@

SpringBoot整合docker<
-

SpringBoot整合kafka

+

Java运算

-

kafka介绍 kafka官网: http://kafka.apache.org kafka中文官网: https://kafka.apachecn.org Kafka是一种分布式的,基于发布/订阅的消息系统。主要特点如下: 以时间复杂度为O(1)的方式提供消息持久化能力,并保证即使对TB级以上数据也能保证常数时间的访问性能 高吞吐率。即使在非常......

+

运算符与表达式 运算符 运算符指明对操作数的运算方式。组成表达式的Java操作符有很多种。运算符按照其要求的操作数数目来分,可以有单目运算符、双目运算符和三目运算符,它们分别对应于1个、2个、3个操作数。 种类 运算符按其功能来分:有算术运算符、赋......

@@ -736,15 +734,15 @@

SpringBoot整合kafka - 2020.08.20 + 2021.01.30 - springboot + Java - MQ + Java基础 @@ -755,12 +753,12 @@

SpringBoot整合kafka
-

线程状态及创建方式

+

Java数据类型

-

线程状态及转换 线程状态共包含6种,6中状态又可以互相的转换。 新建状态(New): 创建了线程后尚未启动; 可运行状态(Runnable): 可能正在运行,也可能正在等待 CPU 时间片。包含了运行中(Running)和 就绪(Ready)状态; 就绪(Rea......

+

基本类型 Java语言提供了八种基本类型。六种数值类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. 俗称4类8种 这里只介绍称4类8种.实际上,JAVA中还存在另外一种基本类型 void,它也有对应的包装类java.lang.Void......

@@ -769,16 +767,16 @@

线程状态及创 diff --git a/docs/page/4/index.html b/docs/page/4/index.html index 6a781a3e..f74f85b6 100644 --- a/docs/page/4/index.html +++ b/docs/page/4/index.html @@ -1,7 +1,7 @@ - + @@ -128,12 +128,12 @@

-

Docker介绍

+

Java异常

-

docker是什么 Docker 属于 Linux 容器的一种封装,提供简单易用的容器使用接口。它是目前流行的 Linux 容器解决方案。 Docker 将应用程序与该程序的依赖,打包在一个文件里面。运行这个文件,就会生成一个虚拟容器。程序在这个虚拟容器里运行,就好像在真实的物理机上运行一样......

+

异常类型 Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:Error 和 Exception。 其中 Error 用来表示Java程序无法处理的错误;这类错误一般与硬件有关,与程序本身无关,通常由系统进行处理,程序本身无法捕获和处理。是不可控制的。 Exception 分为两种......

@@ -142,13 +142,79 @@

Docker介绍

- 2020.04.07 + 2021.01.13 - 使用介绍 + Java + + Java基础 + + + +
+
+

+

+ +
+
+

20201124简历

+
+ +
+
+

自我介绍 1998 · 李济芝 河北唐山 15176733539  m15176733539@163.com 专业技能 熟练使用 SSM,SpringBoot等框架技术; 熟练使用HTML,CSS等相关技术; 有Redis,VUE相关使用经验; 有对接第三方系统,调用外系统相关经验; 熟悉 MySQL,ORACLE.基本操作,熟练使......

+
+
+ + +
+ +
+
+

SpringBoot整合docker

+
+ +
+
+

MacOS上安装docker 下载 国内下载网站: http://get.daocloud.io 不推荐下载docker版本太旧了 官网下载: https://docs.docker.com/get-started/#download-and-install-docker 或用homebrew进行下载安装 brew install --cask --appdir=/Applications docker 配置镜像 由于网速原因,可以配置一下国内的镜像加速器 中科大镜像: https://docker.mirrors.ustc.edu.cn 网易: https://hub-mirror.c.163.com 阿里云: https://<你......

+
+
+ +