diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..55754d07 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "compact": false +} diff --git a/.gitattributes b/.gitattributes index 07962a1f..eaae227f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -22,6 +22,7 @@ *.less text *.sql text *.properties text +*.md text # unix style *.sh text eol=lf diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..04010943 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,36 @@ +name: CI + +# 在master分支发生push事件时触发。 +on: + push: + branches: + - master + +env: # 设置环境变量 + TZ: Asia/Shanghai # 时区(设置时区可使页面中的`最近更新时间`使用时区时间) + +jobs: + build: # 自定义名称 + runs-on: ubuntu-latest # 运行在虚拟机环境ubuntu-latest + + strategy: + matrix: + node-version: [16.x] + + steps: + # 使用的动作。格式:userName/repoName。作用:检出仓库,获取源码。 官方actions库:https://github.com/actions + - name: Checkout + uses: actions/checkout@master + + # 指定 nodejs 版本 + - name: Use Nodejs ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + # 部署 + - name: Deploy + env: # 设置环境变量 + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} + GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} + run: npm install && npm run deploy diff --git a/.gitignore b/.gitignore index 83948575..7d98dac9 100644 --- a/.gitignore +++ b/.gitignore @@ -29,7 +29,6 @@ hs_err_pid* # maven plugin temp files .flattened-pom.xml -package-lock.json # ------------------------------- javascript ------------------------------- @@ -37,10 +36,12 @@ package-lock.json node_modules # temp folders -.temp +build dist _book _jsdoc +.temp +.deploy*/ # temp files *.log @@ -48,7 +49,11 @@ npm-debug.log* yarn-debug.log* yarn-error.log* bundle*.js +.DS_Store +Thumbs.db +db.json book.pdf +package-lock.json # ------------------------------- intellij ------------------------------- diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7f7498fb..00000000 --- a/.travis.yml +++ /dev/null @@ -1,24 +0,0 @@ -# 持续集成 CI -# @see https://docs.travis-ci.com/user/tutorial/ - -language: node_js - -sudo: required - -node_js: stable - -branches: - only: - - master - -before_install: - - export TZ=Asia/Shanghai - -script: bash ./scripts/deploy.sh - -notifications: - email: - recipients: - - forbreak@163.com - on_success: change - on_failure: always diff --git a/README.md b/README.md index 0841d736..7d3dbdd3 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,27 @@

- logo + logo

- license + + + star + + + + fork + + + + build + + + + code style + +

JavaTutorial

@@ -14,86 +30,199 @@ > > - 🔁 项目同步维护:[Github](https://github.com/dunwu/java-tutorial/) | [Gitee](https://gitee.com/turnon/java-tutorial/) > - 📖 电子书阅读:[Github Pages](https://dunwu.github.io/java-tutorial/) | [Gitee Pages](https://turnon.gitee.io/java-tutorial/) +> +> 说明: +> +> - 下面的内容清单中,凡是有 📚 标记的技术,都已整理成详细的教程。 +> - 部分技术因为可以应用于不同领域,所以可能会同时出现在不同的类别下。 -## javacore - -> 📚 [javacore](https://dunwu.github.io/javacore/) 是一个 Java 核心技术教程。内容包含:Java 基础特性、Java 高级特性、Java 并发、JVM、Java IO 等。 - -## javaee +## 📖 内容 -> [☕ JavaEE](docs/javaee/README.md) 技术是 Java Web 的基石 +### JavaSE -- [JavaEE 面经](docs/javaee/javaee-interview.md) -- [JavaEE 之 Servlet 指南](docs/javaee/javaee-servlet.md) -- [JavaEE 之 Jsp 指南](docs/javaee/javaee-jsp.md) -- [JavaEE 之 Filter 和 Listener](docs/javaee/javaee-filter-listener.md) -- [JavaEE 之 Cookie 和 Session](docs/javaee/javaee-cookie-sesion.md) +> 📚 [javacore](https://dunwu.github.io/javacore/) 是一个 Java 核心技术教程。内容包含:Java 基础特性、Java 高级特性、Java 并发、JVM、Java IO 等。 -## javatech +### JavaEE -> 📚 [javatech](https://dunwu.github.io/javatech/) 是一个 Java 应用技术教程。内容包含 Java 开发中常见应用技术,如:框架、缓存、消息队列、存储、安全、微服务、测试、服务器等。 +#### JavaWeb -## spring-tutorial +- [JavaWeb 面经](docs/02.JavaEE/01.JavaWeb/99.JavaWeb面经.md) +- [JavaWeb 之 Servlet 指南](docs/02.JavaEE/01.JavaWeb/01.JavaWeb之Servlet指南.md) +- [JavaWeb 之 Jsp 指南](docs/02.JavaEE/01.JavaWeb/02.JavaWeb之Jsp指南.md) +- [JavaWeb 之 Filter 和 Listener](docs/02.JavaEE/01.JavaWeb/03.JavaWeb之Filter和Listener.md) +- [JavaWeb 之 Cookie 和 Session](docs/02.JavaEE/01.JavaWeb/04.JavaWeb之Cookie和Session.md) -> 📚 [spring-tutorial](https://dunwu.github.io/spring-tutorial/) 是一个 Spring 实战教程。 +#### Java 服务器 -## spring-boot-tutorial +> Tomcat 和 Jetty 都是 Java 比较流行的轻量级服务器。 +> +> Nginx 是目前最流行的反向代理服务器,也常用于负载均衡。 -> 📚 [Spring Boot 教程](https://dunwu.github.io/spring-boot-tutorial/) 是一个 Spring Boot 实战教程。 +- [Tomcat 快速入门](docs/02.JavaEE/02.服务器/01.Tomcat/01.Tomcat快速入门.md) +- [Tomcat 连接器](docs/02.JavaEE/02.服务器/01.Tomcat/02.Tomcat连接器.md) +- [Tomcat 容器](docs/02.JavaEE/02.服务器/01.Tomcat/03.Tomcat容器.md) +- [Tomcat 优化](docs/02.JavaEE/02.服务器/01.Tomcat/04.Tomcat优化.md) +- [Tomcat 和 Jetty](docs/02.JavaEE/02.服务器/01.Tomcat/05.Tomcat和Jetty.md) +- [Jetty](docs/02.JavaEE/02.服务器/02.Jetty.md) -## javatool +### Java 软件 -### 构建 +#### Java 构建 -> Java 项目需要通过 [**构建工具**](docs/javatool/build) 来管理项目依赖,完成编译、打包、发布、生成 JavaDoc 等任务。 +> Java 项目需要通过 [**构建工具**](docs/11.软件/01.构建) 来管理项目依赖,完成编译、打包、发布、生成 JavaDoc 等任务。 > > - 目前最主流的构建工具是 Maven,它的功能非常强大。 > - Gradle 号称是要替代 Maven 等构件工具,它的版本管理确实简洁,但是需要学习 Groovy,学习成本比 Maven 高。 > - Ant 功能比 Maven 和 Gradle 要弱,现代 Java 项目基本不用了,但也有一些传统的 Java 项目还在使用。 -- [Maven](docs/javatool/build/maven) 📚 - - [Maven 入门指南](docs/javatool/build/maven/maven-quickstart.md) - - [Maven 教程之 pom.xml 详解](docs/javatool/build/maven/maven-pom.md) - - [Maven 教程之 settings.xml 详解](docs/javatool/build/maven/maven-settings.md) - - [Maven 实战问题和最佳实践](docs/javatool/build/maven/maven-action.md) - - [Maven 教程之发布 jar 到私服或中央仓库](docs/javatool/build/maven/maven-deploy.md) - - [Maven 插件之代码检查](docs/javatool/build/maven/maven-checkstyle-plugin.md) -- [Ant 简易教程](docs/javatool/build/ant.md) +- [Maven](docs/11.软件/01.构建/01.Maven) 📚 + - [Maven 快速入门](docs/11.软件/01.构建/01.Maven/01.Maven快速入门.md) + - [Maven 教程之 pom.xml 详解](docs/11.软件/01.构建/01.Maven/02.Maven教程之pom.xml详解.md) + - [Maven 教程之 settings.xml 详解](docs/11.软件/01.构建/01.Maven/03.Maven教程之settings.xml详解.md) + - [Maven 实战问题和最佳实践](docs/11.软件/01.构建/01.Maven/04.Maven实战问题和最佳实践.md) + - [Maven 教程之发布 jar 到私服或中央仓库](docs/11.软件/01.构建/01.Maven/05.Maven教程之发布jar到私服或中央仓库.md) + - [Maven 插件之代码检查](docs/11.软件/01.构建/01.Maven/06.Maven插件之代码检查.md) +- [Ant 简易教程](docs/11.软件/01.构建/02.Ant.md) + +#### Java IDE + +> 自从有了 [**IDE**](docs/11.软件/02.IDE),写代码从此就告别了刀耕火种的蛮荒时代。 +> +> - [Eclipse](docs/11.软件/02.IDE/02.Eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 +> - 曾经抗拒从转 [Intellij Idea](docs/11.软件/02.IDE/01.Intellij.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 +> - 你可以在 [vscode](docs/11.软件/02.IDE/03.VsCode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 + +- [Intellij Idea](docs/11.软件/02.IDE/01.Intellij.md) +- [Eclipse](docs/11.软件/02.IDE/02.Eclipse.md) +- [vscode](docs/11.软件/02.IDE/03.VsCode.md) + +#### Java 监控诊断 + +> [监控/诊断](docs/11.软件/03.监控诊断) 工具主要用于 Java 应用的运维。通过采集、分析、存储、可视化应用的有效数据,帮助开发者、使用者快速定位问题,找到性能瓶颈。 + +- [监控工具对比](docs/11.软件/03.监控诊断/01.监控工具对比.md) +- [CAT](docs/11.软件/03.监控诊断/02.CAT.md) +- [Zipkin](docs/11.软件/03.监控诊断/03.Zipkin.md) +- [SkyWalking](docs/11.软件/03.监控诊断/04.Skywalking.md) +- [Arthas](docs/11.软件/03.监控诊断/05.Arthas.md) + +### Java 工具 + +#### Java IO + +- [JSON 序列化](docs/12.工具/01.IO/01.JSON序列化.md) - [fastjson](https://github.com/alibaba/fastjson)、[Jackson](https://github.com/FasterXML/jackson)、[Gson](https://github.com/google/gson) +- [二进制序列化](docs/12.工具/01.IO/02.二进制序列化.md) - [Protobuf](https://developers.google.com/protocol-buffers)、[Thrift](https://thrift.apache.org/)、[Hessian](http://hessian.caucho.com/)、[Kryo](https://github.com/EsotericSoftware/kryo)、[FST](https://github.com/RuedigerMoeller/fast-serialization) + +#### JavaBean 工具 + +- [Lombok](docs/12.工具/02.JavaBean/01.Lombok.md) +- [Dozer](docs/12.工具/02.JavaBean/02.Dozer.md) + +#### Java 模板引擎 + +- [Freemark](docs/12.工具/03.模板引擎/01.Freemark.md) +- [Velocity](docs/12.工具/03.模板引擎/03.Velocity.md) +- [Thymeleaf](docs/12.工具/03.模板引擎/02.Thymeleaf.md) + +#### Java 测试工具 -### IDE +- [Junit](docs/12.工具/04.测试/01.Junit.md) +- [Mockito](docs/12.工具/04.测试/02.Mockito.md) +- [Jmeter](docs/12.工具/04.测试/03.Jmeter.md) +- [JMH](docs/12.工具/04.测试/04.JMH.md) -> 自从有了 [**IDE**](docs/javatool/ide),写代码从此就告别了刀耕火种的蛮荒时代。 +#### 其他 + +- [Java 日志](docs/12.工具/99.其他/01.Java日志.md) +- [Java 工具包](docs/12.工具/99.其他/02.Java工具包.md) +- [Reflections](docs/12.工具/99.其他/03.Reflections.md) +- [JavaMail](docs/12.工具/99.其他/04.JavaMail.md) +- [Jsoup](docs/12.工具/99.其他/05.Jsoup.md) +- [Thumbnailator](docs/12.工具/99.其他/06.Thumbnailator.md) +- [Zxing](docs/12.工具/99.其他/07.Zxing.md) + +### Java 框架 + +#### ORM + +- [Mybatis 快速入门](docs/13.框架/11.ORM/01.Mybatis快速入门.md) +- [Mybatis 原理](docs/13.框架/11.ORM/02.Mybatis原理.md) + +#### Spring + +📚 [spring-tutorial](https://dunwu.github.io/spring-tutorial/) 是一个 Spring 实战教程。 + +#### Spring Boot + +📚 [Spring Boot 教程](https://dunwu.github.io/spring-boot-tutorial/) 是一个 Spring Boot 实战教程。 + +#### 安全 + +> Java 领域比较流行的安全框架就是 shiro 和 spring-security。 +> +> shiro 更为简单、轻便,容易理解,能满足大多数基本安全场景下的需要。 > -> - [Eclipse](docs/javatool/ide/eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 -> - 曾经抗拒从转 [Intellij Idea](docs/javatool/ide/intellij-idea.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 -> - 你可以在 [vscode](docs/javatool/ide/vscode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 +> spring-security 功能更丰富,也比 shiro 更复杂。值得一提的是由于 spring-security 是 spring 团队开发,所以集成 spring 和 spring-boot 框架更容易。 -- [Intellij Idea](docs/javatool/ide/intellij-idea.md) -- [Eclipse](docs/javatool/ide/eclipse.md) -- [vscode](docs/javatool/ide/vscode.md) +- [Shiro](docs/13.框架/12.安全/01.Shiro.md) +- [SpringSecurity](docs/13.框架/12.安全/02.SpringSecurity.md) -### 监控/诊断 +#### IO -> [监控/诊断](docs/javatool/monitor) 工具主要用于 Java 应用的运维。通过采集、分析、存储、可视化应用的有效数据,帮助开发者、使用者快速定位问题,找到性能瓶颈。 +- [Shiro](docs/13.框架/13.IO/01.Netty.md) -- [监控工具对比](docs/javatool/monitor/monitor-summary.md) -- [CAT](docs/javatool/monitor/cat.md) -- [Zipkin](docs/javatool/monitor/zipkin.md) -- [SkyWalking](docs/javatool/monitor/skywalking.md) -- [Arthas](docs/javatool/monitor/arthas.md) +#### 微服务 ---- +- [Dubbo](docs/13.框架/14.微服务/01.Dubbo.md) -## 其他技术栈 +### Java 中间件 -- [db-tutorial](https://dunwu.github.io/db-tutorial/) - 是对数据库领域开发经验的总结。内容包含:关系型数据库和 Nosql 理论、Mysql、Redis 等。 -- [algorithm-tutorial](https://dunwu.github.io/algorithm-tutorial/) - 是对数据结构和算法的总结。内容包含:一些基本的数据结构、算法。 -- [linux-tutorial](https://github.com/dunwu/linux-tutorial) - 是对 Linux 操作系统的经验总结。内容包含:Linux 常用命令;各种常见软件的 Linux 环境安装配置;运维、部署脚本;Shell、Python 语法教程;Git、Docker 教程。 -- [frontend-tutorial](https://github.com/dunwu/frontend-tutorial) - 前端教程 +#### MQ + +> 消息队列(Message Queue,简称 MQ)技术是分布式应用间交换信息的一种技术。 +> +> 消息队列主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 +> +> 如果想深入学习各种消息队列产品,建议先了解一下 [消息队列基本原理](https://dunwu.github.io/blog/pages/1fd240/) ,有助于理解消息队列特性的实现和设计思路。 ---- +- [消息队列面试](docs/14.中间件/01.MQ/01.消息队列面试.md) +- [消息队列基本原理](docs/14.中间件/01.MQ/02.消息队列基本原理.md) +- [RocketMQ](docs/14.中间件/01.MQ/03.RocketMQ.md) +- [ActiveMQ](docs/14.中间件/01.MQ/04.ActiveMQ.md) -## 学习资源 +#### 缓存 + +> 缓存可以说是优化系统性能的第一手段,在各种技术中都会有缓存的应用。 +> +> 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://dunwu.github.io/design/distributed/分布式缓存.html),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 + +- [缓存面试题](docs/14.中间件/02.缓存/01.缓存面试题.md) +- [Java 缓存中间件](docs/14.中间件/02.缓存/02.Java缓存中间件.md) +- [Memcached 快速入门](docs/14.中间件/02.缓存/03.Memcached.md) +- [Ehcache 快速入门](docs/14.中间件/02.缓存/04.Ehcache.md) +- [Java 进程内缓存](docs/14.中间件/02.缓存/05.Java进程内缓存.md) +- [Http 缓存](docs/14.中间件/02.缓存/06.Http缓存.md) + +#### 流量控制 + +- [Hystrix](docs/14.中间件/03.流量控制/01.Hystrix.md) + +### [大数据](https://dunwu.github.io/bigdata-tutorial) + +> 大数据技术点以归档在:[bigdata-tutorial](https://dunwu.github.io/bigdata-tutorial) + +- [Hdfs](https://dunwu.github.io/bigdata-tutorial/hdfs) 📚 +- [Hbase](https://dunwu.github.io/bigdata-tutorial/hbase) 📚 +- [Hive](https://dunwu.github.io/bigdata-tutorial/hive) 📚 +- [MapReduce](https://dunwu.github.io/bigdata-tutorial/mapreduce) +- [Yarn](https://dunwu.github.io/bigdata-tutorial/yarn) +- [ZooKeeper](https://dunwu.github.io/bigdata-tutorial/zookeeper) 📚 +- [Kafka](https://dunwu.github.io/bigdata-tutorial/kafka) 📚 +- Spark +- Storm +- [Flink](https://dunwu.github.io/bigdata-tutorial/tree/master/docs/flink) + +## 📚 资料 - Java 经典书籍 - [《Effective Java 中文版》](https://item.jd.com/12507084.html) - 本书介绍了在 Java 编程中 78 条极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。同推荐《重构 : 改善既有代码的设计》、《代码整洁之道》、《代码大全》,有一定的内容重叠。 @@ -107,3 +236,18 @@ - [《HTTP 权威指南》](https://item.jd.com/11056556.html) - 本书尝试着将 HTTP 中一些互相关联且常被误解的规则梳理清楚,并编写了一系列基于各种主题的章节,对 HTTP 各方面的特性进行了介绍。纵观全书,对 HTTP“为什么”这样做进行了详细的解释,而不仅仅停留在它是“怎么做”的。 - [《TCP/IP 详解 系列》](https://item.jd.com/11966296.html) - 完整而详细的 TCP/IP 协议指南。针对任何希望理解 TCP/IP 协议是如何实现的读者设计。 - [《剑指 Offer:名企面试官精讲典型编程题》](https://item.jd.com/12163054.html) - 剖析了 80 个典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这 5 个面试要点。 + +## 🚪 传送 + +◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ + +> 你可能会感兴趣: + +- [Java 教程](https://github.com/dunwu/java-tutorial) 📚 +- [JavaCore 教程](https://dunwu.github.io/javacore/) 📚 +- [Spring 教程](https://dunwu.github.io/spring-tutorial/) 📚 +- [Spring Boot 教程](https://dunwu.github.io/spring-boot-tutorial/) 📚 +- [数据库教程](https://dunwu.github.io/db-tutorial/) 📚 +- [数据结构和算法教程](https://dunwu.github.io/algorithm-tutorial/) 📚 +- [Linux 教程](https://dunwu.github.io/linux-tutorial/) 📚 +- [Nginx 教程](https://github.com/dunwu/nginx-tutorial/) 📚 diff --git "a/assets/Java\344\270\232\345\212\241\351\227\256\351\242\230.xmind" "b/assets/Java\344\270\232\345\212\241\351\227\256\351\242\230.xmind" deleted file mode 100644 index 812b8fd2..00000000 Binary files "a/assets/Java\344\270\232\345\212\241\351\227\256\351\242\230.xmind" and /dev/null differ diff --git "a/assets/Java\346\200\247\350\203\275\350\260\203\344\274\230.xmind" "b/assets/Java\346\200\247\350\203\275\350\260\203\344\274\230.xmind" deleted file mode 100644 index a142fc71..00000000 Binary files "a/assets/Java\346\200\247\350\203\275\350\260\203\344\274\230.xmind" and /dev/null differ diff --git a/assets/TroubleShooting.eddx b/assets/TroubleShooting.eddx deleted file mode 100644 index 7b586935..00000000 Binary files a/assets/TroubleShooting.eddx and /dev/null differ diff --git a/assets/TroubleShooting.xmind b/assets/TroubleShooting.xmind deleted file mode 100644 index 8f3a7464..00000000 Binary files a/assets/TroubleShooting.xmind and /dev/null differ diff --git "a/assets/javatool/monitor/\347\233\221\346\216\247\345\267\245\345\205\267.eddx" "b/assets/javatool/monitor/\347\233\221\346\216\247\345\267\245\345\205\267.eddx" deleted file mode 100644 index e963035d..00000000 Binary files "a/assets/javatool/monitor/\347\233\221\346\216\247\345\267\245\345\205\267.eddx" and /dev/null differ diff --git "a/assets/javatool/monitor/\347\233\221\346\216\247\345\267\245\345\205\267\346\257\224\345\257\271.xlsx" "b/assets/javatool/monitor/\347\233\221\346\216\247\345\267\245\345\205\267\346\257\224\345\257\271.xlsx" deleted file mode 100644 index 21317a10..00000000 Binary files "a/assets/javatool/monitor/\347\233\221\346\216\247\345\267\245\345\205\267\346\257\224\345\257\271.xlsx" and /dev/null differ diff --git a/assets/tomcat.eddx b/assets/tomcat.eddx deleted file mode 100644 index aab46c12..00000000 Binary files a/assets/tomcat.eddx and /dev/null differ diff --git a/codes/java-distributed/java-distributed-id/pom.xml b/codes/java-distributed/java-distributed-id/pom.xml new file mode 100644 index 00000000..5f72fc0d --- /dev/null +++ b/codes/java-distributed/java-distributed-id/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + + io.github.dunwu.distributed + java-distributed + 1.0.0 + + + io.github.dunwu.javatech + java-distributed-id + 1.0.0 + jar + ${project.artifactId} + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + org.apache.zookeeper + zookeeper + + + org.apache.curator + curator-recipes + + + redis.clients + jedis + + + cn.hutool + hutool-all + + + org.projectlombok + lombok + + + ch.qos.logback + logback-classic + true + + + diff --git a/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId.java b/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId.java new file mode 100644 index 00000000..06d7092c --- /dev/null +++ b/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId.java @@ -0,0 +1,55 @@ +package io.github.dunwu.distributed.id; + +import cn.hutool.core.collection.CollectionUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.apache.zookeeper.CreateMode; + +import java.util.List; + +/** + * ZK 分布式 ID + * + * @author Zhang Peng + * @date 2024-12-20 + */ +@Slf4j +public class ZookeeperDistributedId { + + public static void main(String[] args) throws Exception { + + // 获取客户端 + RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); + CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy); + + // 开启会话 + client.start(); + + String id1 = client.create() + .creatingParentsIfNeeded() + .withMode(CreateMode.PERSISTENT_SEQUENTIAL) + .forPath("/zkid/id_"); + log.info("id: {}", id1); + + String id2 = client.create() + .creatingParentsIfNeeded() + .withMode(CreateMode.PERSISTENT_SEQUENTIAL) + .forPath("/zkid/id_"); + log.info("id: {}", id2); + + List children = client.getChildren().forPath("/zkid"); + if (CollectionUtil.isNotEmpty(children)) { + for (String child : children) { + client.delete().forPath("/zkid/" + child); + } + } + client.delete().forPath("/zkid"); + + // 关闭客户端 + client.close(); + } + +} diff --git a/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId2.java b/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId2.java new file mode 100644 index 00000000..69d5d543 --- /dev/null +++ b/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId2.java @@ -0,0 +1,46 @@ +package io.github.dunwu.distributed.id; + +import lombok.extern.slf4j.Slf4j; +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.framework.recipes.atomic.AtomicValue; +import org.apache.curator.framework.recipes.atomic.DistributedAtomicLong; +import org.apache.curator.retry.ExponentialBackoffRetry; + +/** + * ZK 分布式 ID + *

+ * 基于原子计数器生成 ID + * + * @author Zhang Peng + * @date 2024-12-20 + */ +@Slf4j +public class ZookeeperDistributedId2 { + + public static void main(String[] args) throws Exception { + + // 获取客户端 + RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); + CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy); + DistributedAtomicLong atomicLong = new DistributedAtomicLong(client, "/zkid", retryPolicy); + + // 开启会话 + client.start(); + + // 基于原子计数器生成 ID + AtomicValue id1 = atomicLong.increment(); + log.info("id: {}", id1.postValue()); + + AtomicValue id2 = atomicLong.increment(); + log.info("id: {}", id2.postValue()); + + // 清理节点 + client.delete().forPath("/zkid"); + + // 关闭客户端 + client.close(); + } + +} diff --git a/codes/java-distributed/java-distributed-id/src/main/resources/scripts/fixed_window_rate_limit.lua b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/fixed_window_rate_limit.lua new file mode 100644 index 00000000..e0c9ad00 --- /dev/null +++ b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/fixed_window_rate_limit.lua @@ -0,0 +1,21 @@ +-- 缓存 Key +local key = KEYS[1] +-- 访问请求数 +local permits = tonumber(ARGV[1]) +-- 过期时间 +local seconds = tonumber(ARGV[2]) +-- 限流阈值 +local limit = tonumber(ARGV[3]) + +-- 获取统计值 +local count = tonumber(redis.call('GET', key) or "0") + +if count + permits > limit then + -- 请求拒绝 + return -1 +else + -- 请求通过 + redis.call('INCRBY', key, permits) + redis.call('EXPIRE', key, seconds) + return count + permits +end \ No newline at end of file diff --git a/codes/java-distributed/java-distributed-id/src/main/resources/scripts/token_bucket_rate_limit.lua b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/token_bucket_rate_limit.lua new file mode 100644 index 00000000..541d70c9 --- /dev/null +++ b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/token_bucket_rate_limit.lua @@ -0,0 +1,39 @@ +local tokenKey = KEYS[1] +local timeKey = KEYS[2] + +-- 申请令牌数 +local permits = tonumber(ARGV[1]) +-- QPS +local qps = tonumber(ARGV[2]) +-- 桶的容量 +local capacity = tonumber(ARGV[3]) +-- 当前时间(单位:毫秒) +local nowMillis = tonumber(ARGV[4]) +-- 填满令牌桶所需要的时间 +local fillTime = capacity / qps +local ttl = math.min(capacity, math.floor(fillTime * 2)) + +local currentTokenNum = tonumber(redis.call("GET", tokenKey)) +if currentTokenNum == nil then + currentTokenNum = capacity +end + +local endTimeMillis = tonumber(redis.call("GET", timeKey)) +if endTimeMillis == nil then + endTimeMillis = 0 +end + +local gap = nowMillis - endTimeMillis +local newTokenNum = math.max(0, gap * qps / 1000) +local currentTokenNum = math.min(capacity, currentTokenNum + newTokenNum) + +if currentTokenNum < permits then + -- 请求拒绝 + return -1 +else + -- 请求通过 + local finalTokenNum = currentTokenNum - permits + redis.call("SETEX", tokenKey, ttl, finalTokenNum) + redis.call("SETEX", timeKey, ttl, nowMillis) + return finalTokenNum +end diff --git a/codes/java-distributed/java-load-balance/pom.xml b/codes/java-distributed/java-load-balance/pom.xml index 5f14ae1a..b4e73f0f 100644 --- a/codes/java-distributed/java-load-balance/pom.xml +++ b/codes/java-distributed/java-load-balance/pom.xml @@ -3,11 +3,10 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - io.github.dunwu.javatech + io.github.dunwu.distributed java-load-balance 1.0.0 jar - ${project.artifactId} UTF-8 diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/BaseLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/BaseLoadBalance.java similarity index 96% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/BaseLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/BaseLoadBalance.java index e4f03ab2..ebb317a8 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/BaseLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/BaseLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import cn.hutool.core.collection.CollectionUtil; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/ConsistentHashLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/ConsistentHashLoadBalance.java similarity index 99% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/ConsistentHashLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/ConsistentHashLoadBalance.java index 42394a12..c67558a2 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/ConsistentHashLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/ConsistentHashLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/IpHashLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/IpHashLoadBalance.java similarity index 94% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/IpHashLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/IpHashLoadBalance.java index 00be0c93..3d71cbb7 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/IpHashLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/IpHashLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import cn.hutool.core.util.HashUtil; import cn.hutool.core.util.StrUtil; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LeastActiveLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LeastActiveLoadBalance.java similarity index 98% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LeastActiveLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LeastActiveLoadBalance.java index 25dc6f13..23a7f03c 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LeastActiveLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LeastActiveLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.util.List; import java.util.Random; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalance.java similarity index 86% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalance.java index 046734af..f2d0c561 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.util.List; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalanceDemo.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalanceDemo.java similarity index 97% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalanceDemo.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalanceDemo.java index 1077f48e..57dc61f1 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/LoadBalanceDemo.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalanceDemo.java @@ -1,6 +1,10 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.TreeMap; /** * 负载均衡算法测试例 diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/Node.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/Node.java similarity index 97% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/Node.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/Node.java index ad0c8ed4..2fc9c712 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/Node.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/Node.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.util.Objects; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RandomLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RandomLoadBalance.java similarity index 94% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RandomLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RandomLoadBalance.java index 8f4b7d8d..5b775dd2 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RandomLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RandomLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.util.List; import java.util.Random; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RoundRobinLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RoundRobinLoadBalance.java similarity index 94% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RoundRobinLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RoundRobinLoadBalance.java index d14b251d..c0858152 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/RoundRobinLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RoundRobinLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/StatisticsUtil.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/StatisticsUtil.java similarity index 95% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/StatisticsUtil.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/StatisticsUtil.java index 50eaa59a..cbb66d13 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/StatisticsUtil.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/StatisticsUtil.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; public class StatisticsUtil { diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/WeightRandomLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRandomLoadBalance.java similarity index 96% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/WeightRandomLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRandomLoadBalance.java index a3f1c853..c7135667 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/WeightRandomLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRandomLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.util.List; import java.util.Random; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/WeightRoundRobinLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRoundRobinLoadBalance.java similarity index 99% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/WeightRoundRobinLoadBalance.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRoundRobinLoadBalance.java index 3180958d..3f71a573 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/WeightRoundRobinLoadBalance.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRoundRobinLoadBalance.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; import java.util.List; import java.util.concurrent.ConcurrentHashMap; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/package-info.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/package-info.java similarity index 76% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/package-info.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/package-info.java index 8633bb5a..4d8b7a26 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/package-info.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/package-info.java @@ -4,4 +4,4 @@ * @author Zhang Peng * @since 2020-01-22 */ -package io.github.dunwu.javatech; +package io.github.dunwu.distributed; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/CRCHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/CRCHashStrategy.java similarity index 98% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/CRCHashStrategy.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/CRCHashStrategy.java index 8622bb51..5c732a5b 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/CRCHashStrategy.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/CRCHashStrategy.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech.support; +package io.github.dunwu.distributed.support; import java.nio.charset.StandardCharsets; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/FnvHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/FnvHashStrategy.java similarity index 92% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/FnvHashStrategy.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/FnvHashStrategy.java index 807f3c33..fc7cea13 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/FnvHashStrategy.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/FnvHashStrategy.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech.support; +package io.github.dunwu.distributed.support; public class FnvHashStrategy implements HashStrategy { diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/HashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/HashStrategy.java similarity index 59% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/HashStrategy.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/HashStrategy.java index 2ad6deef..f574c86e 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/HashStrategy.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/HashStrategy.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech.support; +package io.github.dunwu.distributed.support; public interface HashStrategy { diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/JdkHashCodeStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/JdkHashCodeStrategy.java similarity index 77% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/JdkHashCodeStrategy.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/JdkHashCodeStrategy.java index f4a8389c..6541e2a4 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/JdkHashCodeStrategy.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/JdkHashCodeStrategy.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech.support; +package io.github.dunwu.distributed.support; public class JdkHashCodeStrategy implements HashStrategy { diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/KetamaHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/KetamaHashStrategy.java similarity index 96% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/KetamaHashStrategy.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/KetamaHashStrategy.java index 8e98ef1f..ef479aaf 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/KetamaHashStrategy.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/KetamaHashStrategy.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech.support; +package io.github.dunwu.distributed.support; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/MurmurHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/MurmurHashStrategy.java similarity index 96% rename from codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/MurmurHashStrategy.java rename to codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/MurmurHashStrategy.java index a100f3d3..82ee1620 100644 --- a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/javatech/support/MurmurHashStrategy.java +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/MurmurHashStrategy.java @@ -1,4 +1,4 @@ -package io.github.dunwu.javatech.support; +package io.github.dunwu.distributed.support; import java.nio.ByteBuffer; import java.nio.ByteOrder; diff --git a/codes/java-distributed/java-rate-limit/pom.xml b/codes/java-distributed/java-rate-limit/pom.xml new file mode 100644 index 00000000..312d7f21 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + io.github.dunwu.distributed + java-rate-limit + 1.0.0 + jar + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + redis.clients + jedis + 5.1.0 + + + cn.hutool + hutool-all + 5.8.25 + + + org.projectlombok + lombok + 1.18.30 + + + ch.qos.logback + logback-classic + 1.2.3 + true + + + diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/FixedWindowRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/FixedWindowRateLimiter.java new file mode 100644 index 00000000..0af8d142 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/FixedWindowRateLimiter.java @@ -0,0 +1,59 @@ +package io.github.dunwu.distributed.ratelimit; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 固定时间窗口限流算法 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public class FixedWindowRateLimiter implements RateLimiter { + + /** + * 允许的最大请求数 + */ + private final long maxPermits; + + /** + * 窗口期时长 + */ + private final long periodMillis; + + /** + * 窗口期截止时间 + */ + private long lastPeriodMillis; + + /** + * 请求计数 + */ + private AtomicLong count = new AtomicLong(0); + + public FixedWindowRateLimiter(long qps) { + this(qps, 1000, TimeUnit.MILLISECONDS); + } + + public FixedWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit) { + this.maxPermits = maxPermits; + this.periodMillis = timeUnit.toMillis(period); + this.lastPeriodMillis = System.currentTimeMillis() + this.periodMillis; + } + + @Override + public synchronized boolean tryAcquire(int permits) { + long now = System.currentTimeMillis(); + if (lastPeriodMillis <= now) { + this.lastPeriodMillis = now + this.periodMillis; + count = new AtomicLong(0); + } + if (count.get() + permits <= maxPermits) { + count.addAndGet(permits); + return true; + } else { + return false; + } + } + +} \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/LeakyBucketRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/LeakyBucketRateLimiter.java new file mode 100644 index 00000000..0d99a227 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/LeakyBucketRateLimiter.java @@ -0,0 +1,64 @@ +package io.github.dunwu.distributed.ratelimit; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * 漏桶限流算法 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public class LeakyBucketRateLimiter implements RateLimiter { + + /** + * QPS + */ + private final int qps; + + /** + * 桶的容量 + */ + private final long capacity; + + /** + * 计算的起始时间 + */ + private long beginTimeMillis; + + /** + * 桶中当前的水量 + */ + private final AtomicLong waterNum = new AtomicLong(0); + + public LeakyBucketRateLimiter(int qps, int capacity) { + this.qps = qps; + this.capacity = capacity; + } + + @Override + public synchronized boolean tryAcquire(int permits) { + + // 如果桶中没有水,直接放行 + if (waterNum.get() == 0) { + beginTimeMillis = System.currentTimeMillis(); + waterNum.addAndGet(permits); + return true; + } + + // 计算水量 + long leakedWaterNum = ((System.currentTimeMillis() - beginTimeMillis) / 1000) * qps; + long currentWaterNum = waterNum.get() - leakedWaterNum; + waterNum.set(Math.max(0, currentWaterNum)); + + // 重置时间 + beginTimeMillis = System.currentTimeMillis(); + + if (waterNum.get() + permits < capacity) { + waterNum.addAndGet(permits); + return true; + } else { + return false; + } + } + +} \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiter.java new file mode 100644 index 00000000..4fbc9646 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiter.java @@ -0,0 +1,13 @@ +package io.github.dunwu.distributed.ratelimit; + +/** + * 限流器 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public interface RateLimiter { + + boolean tryAcquire(int permits); + +} diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiterDemo.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiterDemo.java new file mode 100644 index 00000000..e4a50641 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiterDemo.java @@ -0,0 +1,95 @@ +package io.github.dunwu.distributed.ratelimit; + +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.RandomUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 限流器示例 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +@Slf4j +public class RateLimiterDemo { + + public static void main(String[] args) { + + // ============================================================================ + + int qps = 20; + + System.out.println("======================= 固定时间窗口限流算法 ======================="); + FixedWindowRateLimiter fixedWindowRateLimiter = new FixedWindowRateLimiter(qps); + testRateLimit(fixedWindowRateLimiter, qps); + + System.out.println("======================= 滑动时间窗口限流算法 ======================="); + SlidingWindowRateLimiter slidingWindowRateLimiter = new SlidingWindowRateLimiter(qps, 10); + testRateLimit(slidingWindowRateLimiter, qps); + + System.out.println("======================= 漏桶限流算法 ======================="); + LeakyBucketRateLimiter leakyBucketRateLimiter = new LeakyBucketRateLimiter(qps, 100); + testRateLimit(leakyBucketRateLimiter, qps); + + System.out.println("======================= 令牌桶限流算法 ======================="); + TokenBucketRateLimiter tokenBucketRateLimiter = new TokenBucketRateLimiter(qps, 100); + testRateLimit(tokenBucketRateLimiter, qps); + } + + private static void testRateLimit(RateLimiter rateLimiter, int qps) { + + AtomicInteger okNum = new AtomicInteger(0); + AtomicInteger limitNum = new AtomicInteger(0); + ExecutorService executorService = ThreadUtil.newFixedExecutor(10, "限流测试", true); + long beginTime = System.currentTimeMillis(); + + int threadNum = 4; + final CountDownLatch latch = new CountDownLatch(threadNum); + for (int i = 0; i < threadNum; i++) { + executorService.submit(() -> { + try { + batchRequest(rateLimiter, okNum, limitNum, 1000); + } catch (Exception e) { + log.error("发生异常!", e); + } finally { + latch.countDown(); + } + }); + } + + try { + latch.await(10, TimeUnit.SECONDS); + long endTime = System.currentTimeMillis(); + long gap = endTime - beginTime; + log.info("限流 QPS: {} -> 实际结果:耗时 {} ms,{} 次请求成功,{} 次请求被限流,实际 QPS: {}", + qps, gap, okNum.get(), limitNum.get(), okNum.get() * 1000 / gap); + if (okNum.get() == qps) { + log.info("限流符合预期"); + } + } catch (Exception e) { + log.error("发生异常!", e); + } finally { + executorService.shutdown(); + } + } + + private static void batchRequest(RateLimiter rateLimiter, AtomicInteger okNum, AtomicInteger limitNum, int num) + throws InterruptedException { + for (int j = 0; j < num; j++) { + if (rateLimiter.tryAcquire(1)) { + log.info("请求成功"); + okNum.getAndIncrement(); + } else { + log.info("请求限流"); + limitNum.getAndIncrement(); + } + TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(0, 10)); + } + } + +} diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisFixedWindowRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisFixedWindowRateLimiter.java new file mode 100644 index 00000000..ec5d77d9 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisFixedWindowRateLimiter.java @@ -0,0 +1,100 @@ +package io.github.dunwu.distributed.ratelimit; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 基于 Redis + Lua 实现的固定时间窗口限流算法 + * + * @author Zhang Peng + * @date 2024-01-23 + */ +public class RedisFixedWindowRateLimiter implements RateLimiter { + + private static final String REDIS_HOST = "localhost"; + + private static final int REDIS_PORT = 6379; + + private static final Jedis JEDIS; + + public static final String SCRIPT; + + static { + // Jedis 有多种构造方法,这里选用最简单的一种情况 + JEDIS = new Jedis(REDIS_HOST, REDIS_PORT); + + // 触发 ping 命令 + try { + JEDIS.ping(); + System.out.println("jedis 连接成功"); + } catch (JedisConnectionException e) { + e.printStackTrace(); + } + + SCRIPT = FileUtil.readString(ResourceUtil.getResource("scripts/fixed_window_rate_limit.lua"), + StandardCharsets.UTF_8); + } + + private final long maxPermits; + private final long periodSeconds; + private final String key; + + public RedisFixedWindowRateLimiter(long qps, String key) { + this(qps * 60, 60, TimeUnit.SECONDS, key); + } + + public RedisFixedWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit, String key) { + this.maxPermits = maxPermits; + this.periodSeconds = timeUnit.toSeconds(period); + this.key = key; + } + + @Override + public boolean tryAcquire(int permits) { + List keys = Collections.singletonList(key); + List args = CollectionUtil.newLinkedList(String.valueOf(permits), String.valueOf(periodSeconds), + String.valueOf(maxPermits)); + Object eval = JEDIS.eval(SCRIPT, keys, args); + long value = (long) eval; + return value != -1; + } + + public static void main(String[] args) throws InterruptedException { + + int qps = 20; + RateLimiter jedisFixedWindowRateLimiter = new RedisFixedWindowRateLimiter(qps, "rate:limit:20240122210000"); + + // 模拟在一分钟内,不断收到请求,限流是否有效 + int seconds = 60; + long okNum = 0L; + long total = 0L; + long beginTime = System.currentTimeMillis(); + int num = RandomUtil.randomInt(qps, 100); + for (int second = 0; second < seconds; second++) { + for (int i = 0; i < num; i++) { + total++; + if (jedisFixedWindowRateLimiter.tryAcquire(1)) { + okNum++; + System.out.println("请求成功"); + } else { + System.out.println("请求限流"); + } + } + TimeUnit.SECONDS.sleep(1); + } + long endTime = System.currentTimeMillis(); + long time = (endTime - beginTime) / 1000; + System.out.println(StrUtil.format("请求通过数:{},总请求数:{},实际 QPS:{}", okNum, total, okNum / time)); + } + +} diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisTokenBucketRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisTokenBucketRateLimiter.java new file mode 100644 index 00000000..9dd219df --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisTokenBucketRateLimiter.java @@ -0,0 +1,104 @@ +package io.github.dunwu.distributed.ratelimit; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 基于 Redis + Lua 实现的令牌桶限流算法 + * + * @author Zhang Peng + * @date 2024-01-23 + */ +public class RedisTokenBucketRateLimiter implements RateLimiter { + + private static final String REDIS_HOST = "localhost"; + + private static final int REDIS_PORT = 6379; + + private static final Jedis JEDIS; + + public static final String SCRIPT; + + static { + // Jedis 有多种构造方法,这里选用最简单的一种情况 + JEDIS = new Jedis(REDIS_HOST, REDIS_PORT); + + // 触发 ping 命令 + try { + JEDIS.ping(); + System.out.println("jedis 连接成功"); + } catch (JedisConnectionException e) { + e.printStackTrace(); + } + + SCRIPT = FileUtil.readString(ResourceUtil.getResource("scripts/token_bucket_rate_limit.lua"), + StandardCharsets.UTF_8); + } + + private final long qps; + private final long capacity; + private final String tokenKey; + private final String timeKey; + + public RedisTokenBucketRateLimiter(long qps, long capacity, String tokenKey, String timeKey) { + this.qps = qps; + this.capacity = capacity; + this.tokenKey = tokenKey; + this.timeKey = timeKey; + } + + @Override + public boolean tryAcquire(int permits) { + long now = System.currentTimeMillis(); + List keys = CollectionUtil.newLinkedList(tokenKey, timeKey); + List args = CollectionUtil.newLinkedList(String.valueOf(permits), String.valueOf(qps), + String.valueOf(capacity), String.valueOf(now)); + Object eval = JEDIS.eval(SCRIPT, keys, args); + long value = (long) eval; + return value != -1; + } + + public static void main(String[] args) throws InterruptedException { + + int qps = 20; + int bucket = 100; + RedisTokenBucketRateLimiter redisTokenBucketRateLimiter = + new RedisTokenBucketRateLimiter(qps, bucket, "token:rate:limit", "token:rate:limit:time"); + + // 先将令牌桶预热令牌申请完,后续才能真实反映限流 QPS + redisTokenBucketRateLimiter.tryAcquire(bucket); + TimeUnit.SECONDS.sleep(1); + + // 模拟在一分钟内,不断收到请求,限流是否有效 + int seconds = 60; + long okNum = 0L; + long total = 0L; + long beginTime = System.currentTimeMillis(); + for (int second = 0; second < seconds; second++) { + int num = RandomUtil.randomInt(qps, 100); + for (int i = 0; i < num; i++) { + total++; + if (redisTokenBucketRateLimiter.tryAcquire(1)) { + okNum++; + System.out.println("请求成功"); + } else { + System.out.println("请求限流"); + } + } + TimeUnit.SECONDS.sleep(1); + } + long endTime = System.currentTimeMillis(); + long time = (endTime - beginTime) / 1000; + System.out.println(StrUtil.format("请求通过数:{},总请求数:{},实际 QPS:{}", okNum, total, okNum / time)); + } + +} diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/SlidingWindowRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/SlidingWindowRateLimiter.java new file mode 100644 index 00000000..a93613a2 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/SlidingWindowRateLimiter.java @@ -0,0 +1,87 @@ +package io.github.dunwu.distributed.ratelimit; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 滑动时间窗口限流算法 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public class SlidingWindowRateLimiter implements RateLimiter { + + /** + * 允许的最大请求数 + */ + private final long maxPermits; + + /** + * 窗口期时长 + */ + private final long periodMillis; + + /** + * 分片窗口期时长 + */ + private final long shardPeriodMillis; + + /** + * 窗口期截止时间 + */ + private long lastPeriodMillis; + + /** + * 分片窗口数 + */ + private final int shardNum; + + /** + * 请求总计数 + */ + private final AtomicLong totalCount = new AtomicLong(0); + + /** + * 分片窗口计数列表 + */ + private final List countList = new LinkedList<>(); + + public SlidingWindowRateLimiter(long qps, int shardNum) { + this(qps, 1000, TimeUnit.MILLISECONDS, shardNum); + } + + public SlidingWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit, int shardNum) { + this.maxPermits = maxPermits; + this.periodMillis = timeUnit.toMillis(period); + this.lastPeriodMillis = System.currentTimeMillis(); + this.shardPeriodMillis = timeUnit.toMillis(period) / shardNum; + this.shardNum = shardNum; + for (int i = 0; i < shardNum; i++) { + countList.add(new AtomicLong(0)); + } + } + + @Override + public synchronized boolean tryAcquire(int permits) { + long now = System.currentTimeMillis(); + if (now > lastPeriodMillis) { + for (int shardId = 0; shardId < shardNum; shardId++) { + long shardCount = countList.get(shardId).get(); + totalCount.addAndGet(-shardCount); + countList.set(shardId, new AtomicLong(0)); + lastPeriodMillis += shardPeriodMillis; + } + } + int shardId = (int) (now % periodMillis / shardPeriodMillis); + if (totalCount.get() + permits <= maxPermits) { + countList.get(shardId).addAndGet(permits); + totalCount.addAndGet(permits); + return true; + } else { + return false; + } + } + +} \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/TokenBucketRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/TokenBucketRateLimiter.java new file mode 100644 index 00000000..e03e4c7d --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/TokenBucketRateLimiter.java @@ -0,0 +1,59 @@ +package io.github.dunwu.distributed.ratelimit; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * 令牌桶限流算法 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public class TokenBucketRateLimiter implements RateLimiter { + + /** + * QPS + */ + private final long qps; + + /** + * 桶的容量 + */ + private final long capacity; + + /** + * 上一次令牌发放时间 + */ + private long endTimeMillis; + + /** + * 桶中当前的令牌数量 + */ + private final AtomicLong tokenNum = new AtomicLong(0); + + public TokenBucketRateLimiter(long qps, long capacity) { + this.qps = qps; + this.capacity = capacity; + this.endTimeMillis = System.currentTimeMillis(); + } + + @Override + public synchronized boolean tryAcquire(int permits) { + + long now = System.currentTimeMillis(); + long gap = now - endTimeMillis; + + // 计算令牌数 + long newTokenNum = (gap * qps / 1000); + long currentTokenNum = tokenNum.get() + newTokenNum; + tokenNum.set(Math.min(capacity, currentTokenNum)); + + if (tokenNum.get() < permits) { + return false; + } else { + tokenNum.addAndGet(-permits); + endTimeMillis = now; + return true; + } + } + +} \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/resources/scripts/fixed_window_rate_limit.lua b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/fixed_window_rate_limit.lua new file mode 100644 index 00000000..e0c9ad00 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/fixed_window_rate_limit.lua @@ -0,0 +1,21 @@ +-- 缓存 Key +local key = KEYS[1] +-- 访问请求数 +local permits = tonumber(ARGV[1]) +-- 过期时间 +local seconds = tonumber(ARGV[2]) +-- 限流阈值 +local limit = tonumber(ARGV[3]) + +-- 获取统计值 +local count = tonumber(redis.call('GET', key) or "0") + +if count + permits > limit then + -- 请求拒绝 + return -1 +else + -- 请求通过 + redis.call('INCRBY', key, permits) + redis.call('EXPIRE', key, seconds) + return count + permits +end \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/resources/scripts/token_bucket_rate_limit.lua b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/token_bucket_rate_limit.lua new file mode 100644 index 00000000..541d70c9 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/token_bucket_rate_limit.lua @@ -0,0 +1,39 @@ +local tokenKey = KEYS[1] +local timeKey = KEYS[2] + +-- 申请令牌数 +local permits = tonumber(ARGV[1]) +-- QPS +local qps = tonumber(ARGV[2]) +-- 桶的容量 +local capacity = tonumber(ARGV[3]) +-- 当前时间(单位:毫秒) +local nowMillis = tonumber(ARGV[4]) +-- 填满令牌桶所需要的时间 +local fillTime = capacity / qps +local ttl = math.min(capacity, math.floor(fillTime * 2)) + +local currentTokenNum = tonumber(redis.call("GET", tokenKey)) +if currentTokenNum == nil then + currentTokenNum = capacity +end + +local endTimeMillis = tonumber(redis.call("GET", timeKey)) +if endTimeMillis == nil then + endTimeMillis = 0 +end + +local gap = nowMillis - endTimeMillis +local newTokenNum = math.max(0, gap * qps / 1000) +local currentTokenNum = math.min(capacity, currentTokenNum + newTokenNum) + +if currentTokenNum < permits then + -- 请求拒绝 + return -1 +else + -- 请求通过 + local finalTokenNum = currentTokenNum - permits + redis.call("SETEX", tokenKey, ttl, finalTokenNum) + redis.call("SETEX", timeKey, ttl, nowMillis) + return finalTokenNum +end diff --git a/codes/java-distributed/java-task/pom.xml b/codes/java-distributed/java-task/pom.xml new file mode 100644 index 00000000..8d7cc462 --- /dev/null +++ b/codes/java-distributed/java-task/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + io.github.dunwu.distributed + java-distributed + 1.0.0 + + + io.github.dunwu.distributed + java-task + 1.0.0 + jar + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + cn.hutool + hutool-all + + + org.projectlombok + lombok + + + ch.qos.logback + logback-classic + 1.2.3 + true + + + diff --git a/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/DelayQueueExample.java b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/DelayQueueExample.java new file mode 100644 index 00000000..d510eed1 --- /dev/null +++ b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/DelayQueueExample.java @@ -0,0 +1,52 @@ +package io.github.dunwu.local.task; + +import cn.hutool.core.date.DateUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class DelayQueueExample { + + public static void main(String[] args) throws InterruptedException { + BlockingQueue delayQueue = new DelayQueue<>(); + long now = System.currentTimeMillis(); + delayQueue.put(new SampleTask(now + 1000)); + delayQueue.put(new SampleTask(now + 2000)); + delayQueue.put(new SampleTask(now + 3000)); + for (int i = 0; i < 3; i++) { + log.info("task 执行时间:{}", DateUtil.format(new Date(delayQueue.take().getTime()), "yyyy-MM-dd HH:mm:ss")); + } + } + + static class SampleTask implements Delayed { + + long time; + + public SampleTask(long time) { + this.time = time; + } + + public long getTime() { + return time; + } + + @Override + public int compareTo(Delayed o) { + return Long.compare(this.getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS)); + } + + @Override + public long getDelay(TimeUnit unit) { + return unit.convert(time - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + } + +} + + diff --git a/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/ScheduledExecutorServiceExample.java b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/ScheduledExecutorServiceExample.java new file mode 100644 index 00000000..78e8f5bd --- /dev/null +++ b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/ScheduledExecutorServiceExample.java @@ -0,0 +1,37 @@ +package io.github.dunwu.local.task; + +import cn.hutool.core.date.DateUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class ScheduledExecutorServiceExample { + + public static void main(String[] args) { + // 创建一个 ScheduledExecutorService 对象,它将使用一个线程池来执行任务 + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + + // 创建一个 Runnable 对象,这个任务将在 2 秒后执行,并且每 1 秒重复执行一次 + Runnable task = () -> { + log.info("task 执行时间:{}", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss")); + }; + + // 安排任务在 2 秒后执行,并且每 1 秒重复执行一次 + executor.scheduleAtFixedRate(task, 2, 1, TimeUnit.SECONDS); + + // 主线程等待 10 秒后结束 + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // 关闭 executor,这将停止所有正在执行的任务,并拒绝新任务的提交 + executor.shutdown(); + } + +} diff --git a/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/TimerExample.java b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/TimerExample.java new file mode 100644 index 00000000..ce1d7756 --- /dev/null +++ b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/TimerExample.java @@ -0,0 +1,39 @@ +package io.github.dunwu.local.task; + +import cn.hutool.core.date.DateUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.Timer; +import java.util.TimerTask; + +@Slf4j +public class TimerExample { + + public static void main(String[] args) { + // 创建一个 Timer 对象 + Timer timer = new Timer(); + + // 创建一个 TimerTask 对象,这个任务将在 2 秒后执行,并且每 1 秒重复执行一次 + TimerTask task = new TimerTask() { + @Override + public void run() { + log.info("task 执行时间:{}", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss")); + } + }; + + // 安排任务在 2 秒后执行,并且每 1 秒重复执行一次 + timer.schedule(task, 2000, 1000); + + // 主线程等待 10 秒后结束 + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // 取消定时器和所有已安排的任务 + timer.cancel(); + } + +} diff --git a/codes/java-distributed/pom.xml b/codes/java-distributed/pom.xml index 35fd5860..aa88d15d 100644 --- a/codes/java-distributed/pom.xml +++ b/codes/java-distributed/pom.xml @@ -3,15 +3,52 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - io.github.dunwu.javatech + io.github.dunwu.distributed java-distributed 1.0.0 pom - JAVA-DISTRIBUTED - JAVA-DISTRIBUTED 示例源码 java-load-balance + java-rate-limit + java-distributed-id + java-task + + + + org.apache.zookeeper + zookeeper + 3.9.2 + + + org.apache.curator + curator-recipes + 4.3.0 + + + redis.clients + jedis + 5.1.0 + + + cn.hutool + hutool-all + 5.8.34 + + + org.projectlombok + lombok + 1.18.30 + + + ch.qos.logback + logback-classic + 1.4.12 + true + + + + diff --git a/codes/javatech/javatech-cache/pom.xml b/codes/javatech/javatech-cache/pom.xml new file mode 100644 index 00000000..4503c60c --- /dev/null +++ b/codes/javatech/javatech-cache/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.2.1.RELEASE + + + io.github.dunwu.javatech + javatech-cache + 1.0.0 + jar + JAVATECH-缓存示例 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-cache + + + net.sf.ehcache + ehcache + + + com.github.ben-manes.caffeine + caffeine + + + net.spy + spymemcached + 2.12.2 + + + com.google.guava + guava + 29.0-jre + + + org.springframework.boot + spring-boot-starter-test + + + org.projectlombok + lombok + + + mysql + mysql-connector-java + + + com.h2database + h2 + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/SpringBootDataCacheApplication.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/SpringBootDataCacheApplication.java new file mode 100644 index 00000000..4a5220d3 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/SpringBootDataCacheApplication.java @@ -0,0 +1,87 @@ +package io.github.dunwu.javatech; + +import io.github.dunwu.javatech.data.User; +import io.github.dunwu.javatech.data.UserDao; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; + +/** + * @author Zhang Peng + * @since 2019-10-14 + */ +@EnableCaching +@SpringBootApplication +public class SpringBootDataCacheApplication implements CommandLineRunner { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private final UserDao userDao; + + public SpringBootDataCacheApplication(UserDao userDao) { + this.userDao = userDao; + } + + public static void main(String[] args) { + SpringApplication.run(SpringBootDataCacheApplication.class, args); + } + + @Override + public void run(String... args) throws Exception { + + if (userDao != null) { + printDataSourceInfo(userDao.getJdbcTemplate()); + log.info("连接数据源成功!"); + } else { + log.error("连接数据源失败!"); + return; + } + + for (int i = 1; i <= 3; i++) { + User user = userDao.queryByName("张三"); + log.info("第 {} 次查询 name = {}", i, user.toString()); + } + + for (int i = 1; i <= 3; i++) { + User user = userDao.queryByName("李四"); + log.info("第 {} 次查询 name = {}", i, user.toString()); + } + + User result = userDao.queryByName("张三"); + result.setAddress("深圳"); + userDao.update(result); + + for (int i = 1; i <= 3; i++) { + User user = userDao.queryByName("张三"); + log.info("第 {} 次查询 name = {}", i, user.toString()); + } + } + + public void printDataSourceInfo(JdbcTemplate jdbcTemplate) throws SQLException { + + DataSource dataSource = jdbcTemplate.getDataSource(); + + Connection connection; + if (dataSource != null) { + connection = dataSource.getConnection(); + } else { + log.error("获取 DataSource 失败"); + return; + } + + if (connection != null) { + log.info("DB URL: {}", connection.getMetaData().getURL()); + } else { + log.error("获取 Connection 失败"); + } + } + +} diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/CaffeineDemo.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/CaffeineDemo.java new file mode 100644 index 00000000..484259a6 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/CaffeineDemo.java @@ -0,0 +1,23 @@ +package io.github.dunwu.javatech.cache; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import java.util.concurrent.TimeUnit; + +/** + * @author Zhang Peng + * @since 2020-07-09 + */ +public class CaffeineDemo { + + public static void main(String[] args) { + Cache cache = Caffeine.newBuilder() + .expireAfterWrite(1, TimeUnit.SECONDS) + .expireAfterAccess(1, TimeUnit.SECONDS) + .maximumSize(10) + .build(); + cache.put("hello", "hello"); + } + +} diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/GuavaCacheDemo.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/GuavaCacheDemo.java new file mode 100644 index 00000000..1cdb585e --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/GuavaCacheDemo.java @@ -0,0 +1,55 @@ +package io.github.dunwu.javatech.cache; + +import com.google.common.cache.*; + +import java.util.concurrent.TimeUnit; + +/** + * @author Zhang Peng + * @since 2020-07-09 + */ +public class GuavaCacheDemo { + + public static void main(String[] args) { + CacheLoader loader = new CacheLoader() { + @Override + public String load(String key) throws Exception { + Thread.sleep(1000); + if ("key".equals(key)) { + return null; + } + System.out.println(key + " is loaded from a cacheLoader!"); + return key + "'s value"; + } + }; + + RemovalListener removalListener = new RemovalListener() { + @Override + public void onRemoval(RemovalNotification removal) { + System.out.println("[" + removal.getKey() + ":" + removal.getValue() + "] is evicted!"); + } + }; + + LoadingCache testCache = CacheBuilder.newBuilder() + .maximumSize(7) + .expireAfterWrite(10, TimeUnit.MINUTES) + .removalListener(removalListener) + .build(loader); + + for (int i = 0; i < 10; i++) { + String key = "key" + i; + String value = "value" + i; + testCache.put(key, value); + System.out.println("[" + key + ":" + value + "] is put into cache!"); + } + + System.out.println(testCache.getIfPresent("key6")); + + try { + System.out.println(testCache.get("key")); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/LRUCache.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/LRUCache.java new file mode 100644 index 00000000..3d38eae3 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/LRUCache.java @@ -0,0 +1,60 @@ +package io.github.dunwu.javatech.cache; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 通过继承 LinkedHashMap 来实现一个简单的 LRUHashMap + * + * 核心思想就是:LRU (最近最少使用)算法 + * + * @author Zhang Peng + * @since 2020-01-18 + */ +class LRUCache extends LinkedHashMap { + + private final int max; + private Object lock; + + public LRUCache(int max) { + //无需扩容 + super((int) (max * 1.4f), 0.75f, true); + this.max = max; + this.lock = new Object(); + } + + /** + * 重写LinkedHashMap的removeEldestEntry方法即可 在Put的时候判断,如果为true,就会删除最老的 + * + * @param eldest + * @return + */ + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > max; + } + + public Object getValue(Object key) { + synchronized (lock) { + return get(key); + } + } + + public void putValue(Object key, Object value) { + synchronized (lock) { + put(key, value); + } + } + + public boolean removeValue(Object key) { + synchronized (lock) { + return remove(key) != null; + } + } + + public boolean removeAll() { + clear(); + return true; + } + +} diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/MemcachedDemo.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/MemcachedDemo.java new file mode 100644 index 00000000..9399b12d --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/MemcachedDemo.java @@ -0,0 +1,294 @@ +package io.github.dunwu.javatech.cache; + +import net.spy.memcached.CASResponse; +import net.spy.memcached.CASValue; +import net.spy.memcached.MemcachedClient; + +import java.net.InetSocketAddress; +import java.util.concurrent.Future; + +/** + * Memcached 客户端连接示例 + * + * @author Zhang Peng + * @since 2020-07-10 + */ +public class MemcachedDemo { + + public static final String URL = "127.0.0.1"; + public static final int PORT = 11211; + + public static void main(String[] args) { + add(); + remove(); + append(); + prepend(); + cas(); + get(); + delete(); + incrAndDecr(); + } + + public static void add() { + try { + + // 连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加数据 + Future fo = mcc.set("MyKey", 900, "Free Education"); + + // 打印状态 + System.out.println("set status:" + fo.get()); + + // 输出 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 添加 + fo = mcc.add("MyKey", 900, "memcached"); + + // 打印状态 + System.out.println("add status:" + fo.get()); + + // 添加新key + fo = mcc.add("codingground", 900, "All Free Compilers"); + + // 打印状态 + System.out.println("add status:" + fo.get()); + + // 输出 + System.out.println("codingground value in cache - " + mcc.get("codingground")); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + + public static void remove() { + + try { + //连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加第一个 key=》value 对 + Future fo = mcc.set("MyKey", 900, "Free Education"); + + // 输出执行 add 方法后的状态 + System.out.println("add status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 添加新的 key + fo = mcc.replace("MyKey", 900, "Largest Tutorials' Library"); + + // 输出执行 set 方法后的状态 + System.out.println("replace status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + + public static void append() { + + try { + + // 连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加数据 + Future fo = mcc.set("MyKey", 900, "Free Education"); + + // 输出执行 set 方法后的状态 + System.out.println("set status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 对存在的key进行数据添加操作 + fo = mcc.append(900, "MyKey", " for All"); + + // 输出执行 set 方法后的状态 + System.out.println("append status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("codingground")); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + + public static void prepend() { + + try { + + // 连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加数据 + Future fo = mcc.set("MyKey", 900, "Education for All"); + + // 输出执行 set 方法后的状态 + System.out.println("set status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 对存在的key进行数据添加操作 + fo = mcc.prepend(900, "MyKey", "Free "); + + // 输出执行 set 方法后的状态 + System.out.println("prepend status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("codingground")); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + + public static void cas() { + + try { + + // 连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加数据 + Future fo = mcc.set("MyKey", 900, "Free Education"); + + // 输出执行 set 方法后的状态 + System.out.println("set status:" + fo.get()); + + // 使用 get 方法获取数据 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 通过 gets 方法获取 CAS token(令牌) + CASValue casValue = mcc.gets("MyKey"); + + // 输出 CAS token(令牌) 值 + System.out.println("CAS token - " + casValue); + + // 尝试使用cas方法来更新数据 + CASResponse casresp = mcc.cas("MyKey", casValue.getCas(), 900, "Largest Tutorials-Library"); + + // 输出 CAS 响应信息 + System.out.println("CAS Response - " + casresp); + + // 输出值 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + + public static void get() { + + try { + + // 连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加数据 + Future fo = mcc.set("MyKey", 900, "Free Education"); + + // 输出执行 set 方法后的状态 + System.out.println("set status:" + fo.get()); + + // 使用 get 方法获取数据 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + + public static void delete() { + + try { + + // 连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加数据 + Future fo = mcc.set("MyKey", 900, "World's largest online tutorials library"); + + // 输出执行 set 方法后的状态 + System.out.println("set status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 对存在的key进行数据添加操作 + fo = mcc.delete("MyKey"); + + // 输出执行 delete 方法后的状态 + System.out.println("delete status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("codingground")); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + + public static void incrAndDecr() { + + try { + + // 连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加数字值 + Future fo = mcc.set("number", 900, "1000"); + + // 输出执行 set 方法后的状态 + System.out.println("set status:" + fo.get()); + + // 获取键对应的值 + System.out.println("value in cache - " + mcc.get("number")); + + // 自增并输出 + System.out.println("value in cache after increment - " + mcc.incr("number", 111)); + + // 自减并输出 + System.out.println("value in cache after decrement - " + mcc.decr("number", 112)); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + +} diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/User.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/User.java new file mode 100644 index 00000000..758e3c9a --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/User.java @@ -0,0 +1,38 @@ +package io.github.dunwu.javatech.data; + +import lombok.Data; +import lombok.ToString; + +import java.io.Serializable; + +@Data +@ToString +public class User implements Serializable { + + private static final long serialVersionUID = 4142994984277644695L; + + private Long id; + + private String name; + + private Integer age; + + private String address; + + private String email; + + public User() {} + + public User(Long id, String name) { + this.id = id; + this.name = name; + } + + public User(String name, Integer age, String address, String email) { + this.name = name; + this.age = age; + this.address = address; + this.email = email; + } + +} diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDao.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDao.java new file mode 100644 index 00000000..370147cd --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDao.java @@ -0,0 +1,33 @@ +package io.github.dunwu.javatech.data; + +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.List; + +public interface UserDao { + + void batchInsert(List users); + + Integer count(); + + @CacheEvict(value = "dunwu:users", key = "#name") + int deleteByName(String name); + + void insert(User user); + + List list(); + + @Cacheable(value = "dunwu:users", key = "#name") + User queryByName(String name); + + void recreateTable(); + + @CachePut(value = "dunwu:users", key = "#user.name") + User update(User user); + + JdbcTemplate getJdbcTemplate(); + +} diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDaoImpl.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDaoImpl.java new file mode 100644 index 00000000..a69ec012 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDaoImpl.java @@ -0,0 +1,105 @@ +package io.github.dunwu.javatech.data; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class UserDaoImpl implements UserDao { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private final JdbcTemplate jdbcTemplate; + + public UserDaoImpl(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchInsert(List users) { + String sql = "INSERT INTO user(name, age, address, email) VALUES(?, ?, ?, ?)"; + + List params = new ArrayList<>(); + + users.forEach(item -> { + params.add(new Object[] { item.getName(), item.getAge(), item.getAddress(), item.getEmail() }); + }); + jdbcTemplate.batchUpdate(sql, params); + } + + @Override + public Integer count() { + try { + return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM user", Integer.class); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public int deleteByName(String name) { + int result = jdbcTemplate.update("DELETE FROM user WHERE name = ?", name); + log.info("[Delete] name = {}", name); + return result; + } + + @Override + public void insert(User user) { + jdbcTemplate.update("INSERT INTO user(name, age, address, email) VALUES(?, ?, ?, ?)", user.getName(), + user.getAge(), user.getAddress(), user.getEmail()); + } + + @Override + public List list() { + return jdbcTemplate.query("select * from USER", new BeanPropertyRowMapper(User.class)); + } + + @Override + public User queryByName(String name) { + + try { + User user = jdbcTemplate.queryForObject("SELECT * FROM user WHERE name = ?", + new BeanPropertyRowMapper<>(User.class), name); + log.info("[Query] name = {}, result = {}", name, user); + return user; + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void recreateTable() { + jdbcTemplate.execute("DROP TABLE IF EXISTS user"); + + String sqlStatement = + "CREATE TABLE user (\n" + " id BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Id',\n" + + " name VARCHAR(10) NOT NULL DEFAULT '' COMMENT '用户名',\n" + + " age TINYINT(3) NOT NULL DEFAULT 0 COMMENT '年龄',\n" + + " address VARCHAR(32) NOT NULL DEFAULT '' COMMENT '地址',\n" + + " email VARCHAR(32) NOT NULL DEFAULT '' COMMENT '邮件',\n" + " PRIMARY KEY (id)\n" + + ") COMMENT = '用户表';"; + jdbcTemplate.execute(sqlStatement); + } + + @Override + public User update(User user) { + jdbcTemplate.update("UPDATE USER SET name=?, age=?, address=?, email=? WHERE id=?", user.getName(), + user.getAge(), user.getAddress(), user.getEmail(), user.getId()); + return user; + } + + @Override + public JdbcTemplate getJdbcTemplate() { + return jdbcTemplate; + } + +} diff --git a/codes/javatech/javatech-cache/src/main/resources/application.properties b/codes/javatech/javatech-cache/src/main/resources/application.properties new file mode 100644 index 00000000..63306cc8 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/resources/application.properties @@ -0,0 +1,15 @@ +spring.datasource.url = jdbc:mysql://localhost:3306/spring_boot_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false +spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver +spring.datasource.username = root +spring.datasource.password = root +# 强制每次启动使用 sql 初始化数据,本项目仅为了演示方便,真实环境应避免这种模式 +spring.datasource.initialization-mode = ALWAYS +spring.datasource.schema = classpath:sql/schema.sql +spring.datasource.data = classpath:sql/data.sql +#spring.redis.database = 0 +#spring.redis.host = localhost +#spring.redis.port = 6379 +#spring.redis.password = +spring.cache.type = simple +spring.cache.cache-names = dunwu +spring.cache.redis.time-to-live = 60s diff --git a/codes/javatech/javatech-cache/src/main/resources/banner.txt b/codes/javatech/javatech-cache/src/main/resources/banner.txt new file mode 100644 index 00000000..449413d5 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/resources/banner.txt @@ -0,0 +1,12 @@ +${AnsiColor.BRIGHT_YELLOW}${AnsiStyle.BOLD} + ________ ___ ___ ________ ___ __ ___ ___ +|\ ___ \|\ \|\ \|\ ___ \|\ \ |\ \|\ \|\ \ +\ \ \_|\ \ \ \\\ \ \ \\ \ \ \ \ \ \ \ \ \\\ \ + \ \ \ \\ \ \ \\\ \ \ \\ \ \ \ \ __\ \ \ \ \\\ \ + \ \ \_\\ \ \ \\\ \ \ \\ \ \ \ \|\__\_\ \ \ \\\ \ + \ \_______\ \_______\ \__\\ \__\ \____________\ \_______\ + \|_______|\|_______|\|__| \|__|\|____________|\|_______| +${AnsiColor.CYAN}${AnsiStyle.BOLD} +:: Java :: (v${java.version}) +:: Spring Boot :: (v${spring-boot.version}) +${AnsiStyle.NORMAL} diff --git a/codes/javatech/javatech-cache/src/main/resources/logback.xml b/codes/javatech/javatech-cache/src/main/resources/logback.xml new file mode 100644 index 00000000..240ee4c6 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + %d{HH:mm:ss.SSS} [%boldYellow(%thread)] [%highlight(%-5level)] %boldGreen(%c{36}.%M) - %boldBlue(%m%n) + + + + + + + + + + diff --git a/codes/javatech/javatech-cache/src/main/resources/sql/data.sql b/codes/javatech/javatech-cache/src/main/resources/sql/data.sql new file mode 100644 index 00000000..694c3472 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/resources/sql/data.sql @@ -0,0 +1,8 @@ +-- ------------------------------------------- +-- 运行本项目的 DML 脚本 +-- ------------------------------------------- + +INSERT INTO user (name, age, address, email) +VALUES ('张三', 18, '北京', 'xxx@163.com'); +INSERT INTO user (name, age, address, email) +VALUES ('李四', 19, '上海', 'xxx@163.com'); diff --git a/codes/javatech/javatech-cache/src/main/resources/sql/schema.sql b/codes/javatech/javatech-cache/src/main/resources/sql/schema.sql new file mode 100644 index 00000000..247bdc1e --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/resources/sql/schema.sql @@ -0,0 +1,13 @@ +-- ------------------------------------------- +-- 运行本项目的 DDL 脚本 +-- ------------------------------------------- + +-- 创建数据表 user +CREATE TABLE IF NOT EXISTS user ( + id BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Id', + name VARCHAR(10) NOT NULL DEFAULT '' COMMENT '用户名', + age TINYINT(3) NOT NULL DEFAULT 0 COMMENT '年龄', + address VARCHAR(32) NOT NULL DEFAULT '' COMMENT '地址', + email VARCHAR(32) NOT NULL DEFAULT '' COMMENT '邮件', + PRIMARY KEY (id) +) COMMENT = '用户表'; diff --git a/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/EhcacheApiTest.java b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/EhcacheApiTest.java new file mode 100644 index 00000000..d8d2a356 --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/EhcacheApiTest.java @@ -0,0 +1,235 @@ +package io.github.dunwu.javatech.cache; + +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.Element; +import net.sf.ehcache.config.CacheConfiguration; +import net.sf.ehcache.config.PersistenceConfiguration; +import net.sf.ehcache.store.MemoryStoreEvictionPolicy; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; + +/** + * Ehcache API 测试(没有和任何框架集成的情况) + * + * @author Zhang Peng + * @since 2019-09-04 + */ +public class EhcacheApiTest { + + /** + * 使用Ehcache默认配置(classpath下的ehcache.xml)获取单例的CacheManager实例 + */ + @Test + public void operation() { + CacheManager manager = CacheManager.newInstance("src/test/resources/ehcache/ehcache.xml"); + + // 获得Cache的引用 + Cache cache = manager.getCache("users"); + + // 将一个Element添加到Cache + cache.put(new Element("key1", "value1")); + + // 获取Element,Element类支持序列化,所以下面两种方法都可以用 + Element element1 = cache.get("key1"); + // 获取非序列化的值 + System.out.println("key=" + element1.getObjectKey() + ", value=" + element1.getObjectValue()); + // 获取序列化的值 + System.out.println("key=" + element1.getKey() + ", value=" + element1.getValue()); + + // 更新Cache中的Element + cache.put(new Element("key1", "value2")); + Element element2 = cache.get("key1"); + System.out.println("key=" + element2.getObjectKey() + ", value=" + element2.getObjectValue()); + + // 获取Cache的元素数 + System.out.println("cache size:" + cache.getSize()); + + // 获取MemoryStore的元素数 + System.out.println("MemoryStoreSize:" + cache.getMemoryStoreSize()); + + // 获取DiskStore的元素数 + System.out.println("DiskStoreSize:" + cache.getDiskStoreSize()); + + // 移除Element + cache.remove("key1"); + System.out.println("cache size:" + cache.getSize()); + + // 关闭当前CacheManager对象 + manager.shutdown(); + + // 关闭CacheManager单例实例 + CacheManager.getInstance().shutdown(); + } + + /** + * 使用Ehcache默认配置(classpath下的ehcache.xml)获取单例的CacheManager实例 + */ + @Test + public void create01() { + CacheManager cacheManager = CacheManager.create(); + + String[] cacheNames = cacheManager.getCacheNames(); + for (String name : cacheNames) { + System.out.println("name:" + name); + } + + cacheManager.shutdown(); + } + + /** + * 使用Ehcache默认配置(classpath下的ehcache.xml)新建一个单例的CacheManager实例 + */ + @Test + public void create02() { + CacheManager.newInstance(); + + String[] cacheNames = CacheManager.getInstance().getCacheNames(); + for (String name : cacheNames) { + System.out.println("name:" + name); + } + + // 关闭CacheManager单例实例 + CacheManager.getInstance().shutdown(); + } + + /** + * 使用不同的配置文件分别创建一个CacheManager实例 + */ + @Test + public void create03() { + CacheManager manager1 = CacheManager.newInstance("src/test/resources/ehcache/ehcache1.xml"); + CacheManager manager2 = CacheManager.newInstance("src/test/resources/ehcache/ehcache1.xml"); + String[] cacheNamesForManager1 = manager1.getCacheNames(); + String[] cacheNamesForManager2 = manager2.getCacheNames(); + + for (String name : cacheNamesForManager1) { + System.out.println("[ehcache1.xml]name:" + name); + } + + for (String name : cacheNamesForManager2) { + System.out.println("[ehcache2.xml]name:" + name); + } + + manager1.shutdown(); + manager2.shutdown(); + } + + /** + * 基于classpath下的配置文件创建CacheManager实例 + */ + @Test + public void create04() { + URL url = getClass().getResource("/ehcache/ehcache.xml"); + CacheManager manager = CacheManager.newInstance(url); + String[] cacheNames = manager.getCacheNames(); + + for (String name : cacheNames) { + System.out.println("[ehcache.xml]name:" + name); + } + + manager.shutdown(); + } + + /** + * 基于IO流得到配置文件,并创建CacheManager实例 + */ + @Test + public void create05() throws Exception { + InputStream fis = new FileInputStream(new File("src/test/resources/ehcache/ehcache.xml").getAbsolutePath()); + CacheManager manager = CacheManager.newInstance(fis); + fis.close(); + String[] cacheNames = manager.getCacheNames(); + + for (String name : cacheNames) { + System.out.println("[ehcache.xml]name:" + name); + } + + manager.shutdown(); + } + + /** + * 使用默认配置(classpath下的ehcache.xml)添加缓存 + */ + @Test + public void addAndRemove01() { + CacheManager singletonManager = CacheManager.create(); + + // 添加缓存 + singletonManager.addCache("testCache"); + + // 打印配置信息和状态 + Cache test = singletonManager.getCache("testCache"); + System.out.println("cache name:" + test.getCacheConfiguration().getName()); + System.out.println("cache status:" + test.getStatus().toString()); + System.out.println("maxElementsInMemory:" + test.getCacheConfiguration().getMaxElementsInMemory()); + System.out.println("timeToIdleSeconds:" + test.getCacheConfiguration().getTimeToIdleSeconds()); + System.out.println("timeToLiveSeconds:" + test.getCacheConfiguration().getTimeToLiveSeconds()); + + // 删除缓存 + singletonManager.removeCache("testCache"); + System.out.println("cache status:" + test.getStatus().toString()); + + singletonManager.shutdown(); + } + + /** + * 使用自定义配置添加缓存,注意缓存未添加进CacheManager之前并不可用 + */ + @Test + public void addAndRemove02() { + CacheManager singletonManager = CacheManager.create(); + + // 添加缓存 + Cache memoryOnlyCache = new Cache("testCache2", 5000, false, false, 5, 2); + singletonManager.addCache(memoryOnlyCache); + + // 打印配置信息和状态 + Cache test = singletonManager.getCache("testCache2"); + System.out.println("cache name:" + test.getCacheConfiguration().getName()); + System.out.println("cache status:" + test.getStatus().toString()); + System.out.println("maxElementsInMemory:" + test.getCacheConfiguration().getMaxElementsInMemory()); + System.out.println("timeToIdleSeconds:" + test.getCacheConfiguration().getTimeToIdleSeconds()); + System.out.println("timeToLiveSeconds:" + test.getCacheConfiguration().getTimeToLiveSeconds()); + + // 删除缓存 + singletonManager.removeCache("testCache2"); + System.out.println("cache status:" + test.getStatus().toString()); + + singletonManager.shutdown(); + } + + /** + * 使用特定的配置添加缓存 + */ + @Test + public void addAndRemove03() { + CacheManager manager = CacheManager.create(); + + // 添加缓存 + Cache testCache = new Cache(new CacheConfiguration("testCache3", 5000) + .memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LFU).eternal(false).timeToLiveSeconds(60) + .timeToIdleSeconds(30).diskExpiryThreadIntervalSeconds(0) + .persistence(new PersistenceConfiguration().strategy(PersistenceConfiguration.Strategy.LOCALTEMPSWAP))); + manager.addCache(testCache); + + // 打印配置信息和状态 + Cache test = manager.getCache("testCache3"); + System.out.println("cache name:" + test.getCacheConfiguration().getName()); + System.out.println("cache status:" + test.getStatus().toString()); + System.out.println("maxElementsInMemory:" + test.getCacheConfiguration().getMaxElementsInMemory()); + System.out.println("timeToIdleSeconds:" + test.getCacheConfiguration().getTimeToIdleSeconds()); + System.out.println("timeToLiveSeconds:" + test.getCacheConfiguration().getTimeToLiveSeconds()); + + // 删除缓存 + manager.removeCache("testCache3"); + System.out.println("cache status:" + test.getStatus().toString()); + + manager.shutdown(); + } + +} diff --git a/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/LRUCacheTest.java b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/LRUCacheTest.java new file mode 100644 index 00000000..21bfbc81 --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/LRUCacheTest.java @@ -0,0 +1,25 @@ +package io.github.dunwu.javatech.cache; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * @author Zhang Peng + * @since 2020-01-18 + */ +public class LRUCacheTest { + + @Test + public void test() { + LRUCache cache = new LRUCache(2); + Assertions.assertNull(cache.get(2)); + cache.put(2, "B"); + Assertions.assertNull(cache.get(1)); + cache.put(1, "A"); + cache.put(3, "C"); + Assertions.assertEquals("A", cache.get(1)); + Assertions.assertEquals(null, cache.get(2)); + Assertions.assertEquals("C", cache.get(3)); + } + +} diff --git a/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCacheDemo.java b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCacheDemo.java new file mode 100644 index 00000000..837744ab --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCacheDemo.java @@ -0,0 +1,142 @@ +package io.github.dunwu.javatech.cache.spring; + +import io.github.dunwu.javatech.data.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; + +/** + * Spring 缓存接口测试类 + * + * @author Zhang Peng + * @since 2019-09-04 + */ +@Component +public class SpringCacheDemo { + + @Autowired + private UserService userService; + + @Autowired + private CacheManager cacheManager; + + /** + * 测试当前真实工作的 CacheManager 是什么 + */ + public void getCacheManager() { + System.out.println("当前 CacheManager 类:" + cacheManager.getClass()); + System.out.println(cacheManager.getCacheNames()); + } + + /** + * 测试@Cacheable + */ + public void testFindUser() throws InterruptedException { + // 设置查询条件 + User user1 = new User(1L, null); + User user2 = new User(2L, null); + User user3 = new User(3L, null); + User user4 = new User(3L, null); + + System.out.println("第一次查询"); + System.out.println(userService.findUser(user1)); + System.out.println(userService.findUser(user2)); + System.out.println(userService.findUser(user3)); + System.out.println(userService.findUser(user4)); + + // 如果缓存有效,应该不会打印 查找数据库 id = %d 成功 这样的信息 + System.out.println("\n第二次查询"); + System.out.println(userService.findUser(user1)); + System.out.println(userService.findUser(user2)); + System.out.println(userService.findUser(user3)); + System.out.println(userService.findUser(user4)); + + // 在classpath:ehcache/ehcache.xml中,设置了userCache的缓存时间为3000 ms, 这里设置等待 + Thread.sleep(3000); + + System.out.println("\n缓存过期,再次查询"); + System.out.println(userService.findUser(user1)); + System.out.println(userService.findUser(user2)); + System.out.println(userService.findUser(user3)); + } + + /** + * 测试@Cacheable设置Spring SpEL条件限制 + */ + public void testFindUserInLimit() throws InterruptedException { + // 设置查询条件 + User user1 = new User(1L, null); + User user2 = new User(2L, null); + User user3 = new User(3L, null); + + System.out.println("第一次查询user info"); + System.out.println(userService.findUserInLimit(user1)); + System.out.println(userService.findUserInLimit(user2)); + System.out.println(userService.findUserInLimit(user3)); + + System.out.println("\n第二次查询user info"); + System.out.println(userService.findUserInLimit(user1)); + System.out.println(userService.findUserInLimit(user2)); + System.out.println(userService.findUserInLimit(user3)); // 超过限制条件,不会从缓存中读数据 + + // 在classpath:ehcache/ehcache.xml中,设置了userCache的缓存时间为3000 ms, 这里设置等待 + Thread.sleep(3000); + + System.out.println("\n缓存过期,再次查询"); + System.out.println(userService.findUserInLimit(user1)); + System.out.println(userService.findUserInLimit(user2)); + System.out.println(userService.findUserInLimit(user3)); + } + + /** + * 测试@CachePut + */ + public void testUpdateUser() { + // 设置查询条件 + User user1 = new User(2L, null); + User user2 = new User(2L, null); + + System.out.println(userService.findUser(user1)); + System.out.println(userService.findUser(user2)); + userService.updateUser(new User(2L, "尼古拉斯.赵四")); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(userService.findUser(user1)); + System.out.println(userService.findUser(user2)); + } + + /** + * 测试@CacheEvict删除指定缓存 + */ + public void testRemoveUser() { + // 设置查询条件 + User user1 = new User(1L, null); + + System.out.println("数据删除前:"); + System.out.println(userService.findUser(user1)); + + userService.removeUser(user1); + System.out.println("数据删除后:"); + System.out.println(userService.findUser(user1)); + } + + /** + * 测试@CacheEvict删除所有缓存 + */ + public void testClear() { + System.out.println("数据清空前:"); + System.out.println(userService.findUser(new User(1L, null))); + System.out.println(userService.findUser(new User(2L, null))); + System.out.println(userService.findUser(new User(3L, null))); + + userService.clear(); + System.out.println("\n数据清空后:"); + System.out.println(userService.findUser(new User(1L, null))); + System.out.println(userService.findUser(new User(2L, null))); + System.out.println(userService.findUser(new User(3L, null))); + } + +} diff --git a/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCaffeineCacheTest.java b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCaffeineCacheTest.java new file mode 100644 index 00000000..ba1445ee --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCaffeineCacheTest.java @@ -0,0 +1,72 @@ +package io.github.dunwu.javatech.cache.spring; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * 使用 Caffeine 作为 Spring 缓存测试 + *

+ * 配置内容见:spring/spring-caffeine.xml + * + * @author Zhang Peng + * @since 2019-09-04 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = { "classpath:spring/spring-caffeine.xml" }) +public class SpringCaffeineCacheTest { + + @Autowired + private SpringCacheDemo springCacheDemo; + + /** + * 测试当前真实工作的 CacheManager 是什么 + */ + @Test + public void getCacheManager() { + springCacheDemo.getCacheManager(); + } + + /** + * 测试@Cacheable + */ + @Test + public void testFindUser() throws InterruptedException { + springCacheDemo.testFindUser(); + } + + /** + * 测试@Cacheable设置Spring SpEL条件限制 + */ + @Test + public void testFindUserInLimit() throws InterruptedException { + springCacheDemo.testFindUserInLimit(); + } + + /** + * 测试@CachePut + */ + @Test + public void testUpdateUser() { + springCacheDemo.testUpdateUser(); + } + + /** + * 测试@CacheEvict删除指定缓存 + */ + @Test + public void testRemoveUser() { + springCacheDemo.testRemoveUser(); + } + + /** + * 测试@CacheEvict删除所有缓存 + */ + @Test + public void testClear() { + springCacheDemo.testClear(); + } + +} diff --git a/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringConcurrentHashMapCacheTest.java b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringConcurrentHashMapCacheTest.java new file mode 100644 index 00000000..d63d03d1 --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringConcurrentHashMapCacheTest.java @@ -0,0 +1,72 @@ +package io.github.dunwu.javatech.cache.spring; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * 使用 ConcurrentHashMap 作为 Spring 缓存测试 + *

+ * 配置内容见:spring/spring-hashmap.xml + * + * @author Zhang Peng + * @since 2019-09-04 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = { "classpath:spring/spring-hashmap.xml" }) +public class SpringConcurrentHashMapCacheTest { + + @Autowired + private SpringCacheDemo springCacheDemo; + + /** + * 测试当前真实工作的 CacheManager 是什么 + */ + @Test + public void getCacheManager() { + springCacheDemo.getCacheManager(); + } + + /** + * 测试@Cacheable + */ + @Test + public void testFindUser() throws InterruptedException { + springCacheDemo.testFindUser(); + } + + /** + * 测试@Cacheable设置Spring SpEL条件限制 + */ + @Test + public void testFindUserInLimit() throws InterruptedException { + springCacheDemo.testFindUserInLimit(); + } + + /** + * 测试@CachePut + */ + @Test + public void testUpdateUser() { + springCacheDemo.testUpdateUser(); + } + + /** + * 测试@CacheEvict删除指定缓存 + */ + @Test + public void testRemoveUser() { + springCacheDemo.testRemoveUser(); + } + + /** + * 测试@CacheEvict删除所有缓存 + */ + @Test + public void testClear() { + springCacheDemo.testClear(); + } + +} diff --git a/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringEhcacheCacheTest.java b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringEhcacheCacheTest.java new file mode 100644 index 00000000..d39e9d86 --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringEhcacheCacheTest.java @@ -0,0 +1,72 @@ +package io.github.dunwu.javatech.cache.spring; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * 使用 Ehcache 作为 Spring 缓存测试 + *

+ * 配置内容见:spring/spring-ehcache.xml + * + * @author Zhang Peng + * @since 2019-09-04 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = { "classpath:spring/spring-ehcache.xml" }) +public class SpringEhcacheCacheTest { + + @Autowired + private SpringCacheDemo springCacheDemo; + + /** + * 测试当前真实工作的 CacheManager 是什么 + */ + @Test + public void getCacheManager() { + springCacheDemo.getCacheManager(); + } + + /** + * 测试@Cacheable + */ + @Test + public void testFindUser() throws InterruptedException { + springCacheDemo.testFindUser(); + } + + /** + * 测试@Cacheable设置Spring SpEL条件限制 + */ + @Test + public void testFindUserInLimit() throws InterruptedException { + springCacheDemo.testFindUserInLimit(); + } + + /** + * 测试@CachePut + */ + @Test + public void testUpdateUser() { + springCacheDemo.testUpdateUser(); + } + + /** + * 测试@CacheEvict删除指定缓存 + */ + @Test + public void testRemoveUser() { + springCacheDemo.testRemoveUser(); + } + + /** + * 测试@CacheEvict删除所有缓存 + */ + @Test + public void testClear() { + springCacheDemo.testClear(); + } + +} diff --git a/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/UserService.java b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/UserService.java new file mode 100644 index 00000000..588b6d27 --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/UserService.java @@ -0,0 +1,87 @@ +package io.github.dunwu.javatech.cache.spring; + +import io.github.dunwu.javatech.data.User; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.stereotype.Service; + +import java.util.concurrent.ConcurrentHashMap; + +@Service +public class UserService { + + private ConcurrentHashMap map; + + public UserService() { + // 模拟应用启动时加载缓存 + map = new ConcurrentHashMap<>(); + User user1 = new User(1L, "张三"); + User user2 = new User(2L, "赵四"); + User user3 = new User(3L, "王五"); + map.put(user1.getId(), user1); + map.put(user2.getId(), user2); + map.put(user3.getId(), user3); + } + + @Cacheable(value = "users", key = "#user.id") + public User findUser(User user) { + return findUserInDb(user.getId()); + } + + /** + * 模拟数据库查询操作 + */ + private User findUserInDb(Long id) { + User user = map.get(id); + if (user != null) { + System.out.println("查找数据库 id = " + id + " 成功"); + return user; + } + return null; + } + + @Cacheable(value = "users", condition = "#user.getId() <= 2") + public User findUserInLimit(User user) { + return findUserInDb(user.getId()); + } + + @CachePut(value = "users", key = "#user.getId()") + public void updateUser(User user) { + updateUserInDb(user); + } + + /** + * 模拟数据库更新操作 + */ + private void updateUserInDb(User user) { + User old = map.get(user.getId()); + if (old != null) { + System.out.println("更新数据库" + old + " -> " + user); + old.setName(user.getName()); + } + } + + @CacheEvict(value = "users", key = "#user.getId()") + public void removeUser(User user) { + removeUserInDb(user.getId()); + } + + /** + * 模拟数据库删除操作 + */ + private void removeUserInDb(Long id) { + map.remove(id); + System.out.println("从数据库移除 id = " + id + " 的数据"); + } + + @CacheEvict(value = "users", allEntries = true) + public void clear() { + removeAllInDb(); + } + + private void removeAllInDb() { + map.clear(); + } + +} diff --git a/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/package-info.java b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/package-info.java new file mode 100644 index 00000000..21c5686c --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/package-info.java @@ -0,0 +1,7 @@ +/** + * Spring 集成各类缓存库测试 + * + * @author Zhang Peng + * @since 2020-07-10 + */ +package io.github.dunwu.javatech.cache.spring; diff --git a/codes/javatech/javatech-cache/src/test/resources/ehcache/ehcache.xml b/codes/javatech/javatech-cache/src/test/resources/ehcache/ehcache.xml new file mode 100644 index 00000000..9bf4c661 --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/resources/ehcache/ehcache.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + diff --git a/codes/javatech/javatech-cache/src/test/resources/ehcache/ehcache1.xml b/codes/javatech/javatech-cache/src/test/resources/ehcache/ehcache1.xml new file mode 100644 index 00000000..690eebbd --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/resources/ehcache/ehcache1.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/codes/javatech/javatech-cache/src/test/resources/ehcache/ehcache2.xml b/codes/javatech/javatech-cache/src/test/resources/ehcache/ehcache2.xml new file mode 100644 index 00000000..bb41206a --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/resources/ehcache/ehcache2.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/codes/javatech/javatech-cache/src/test/resources/spring/spring-caffeine.xml b/codes/javatech/javatech-cache/src/test/resources/spring/spring-caffeine.xml new file mode 100644 index 00000000..1570bba2 --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/resources/spring/spring-caffeine.xml @@ -0,0 +1,19 @@ + + + + 使用 Caffeine 作为 Spring 缓存 + + + + + + + + + diff --git a/codes/javatech/javatech-cache/src/test/resources/spring/spring-ehcache.xml b/codes/javatech/javatech-cache/src/test/resources/spring/spring-ehcache.xml new file mode 100644 index 00000000..81af38dd --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/resources/spring/spring-ehcache.xml @@ -0,0 +1,25 @@ + + + + 使用 EhCache 作为 Spring 缓存 + + + + + + + + + + + + + + + diff --git a/codes/javatech/javatech-cache/src/test/resources/spring/spring-hashmap.xml b/codes/javatech/javatech-cache/src/test/resources/spring/spring-hashmap.xml new file mode 100644 index 00000000..1620e0fe --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/resources/spring/spring-hashmap.xml @@ -0,0 +1,26 @@ + + + + 使用 ConcurrentHashMap 作为 Spring 缓存 + + + + + + + + + + + + + + + + diff --git a/codes/javatech/javatech-lib/pom.xml b/codes/javatech/javatech-lib/pom.xml new file mode 100644 index 00000000..7c6da10b --- /dev/null +++ b/codes/javatech/javatech-lib/pom.xml @@ -0,0 +1,173 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 1.0.8 + + + io.github.dunwu.javatech + javatech-lib + 1.0.0 + JAVATECH-工具包示例 + + + 5.2.2 + 1.22 + + + + + cn.hutool + hutool-all + + + org.projectlombok + lombok + + + + + com.alibaba + fastjson + + + com.fasterxml.jackson.core + jackson-databind + + + com.google.code.gson + gson + + + + + de.ruedigermoeller + fst + 2.56 + + + com.esotericsoftware + kryo + 5.3.0 + + + + + ch.qos.logback + logback-classic + + + + + org.reflections + reflections + 0.10.2 + + + + + com.squareup.okhttp3 + okhttp + 4.10.0 + + + + + org.apache.poi + poi + ${poi.version} + + + org.apache.poi + poi-ooxml + ${poi.version} + + + org.apache.poi + poi-scratchpad + ${poi.version} + + + + + com.alibaba + easyexcel + 3.1.1 + + + + + org.dom4j + dom4j + 2.1.3 + + + + + com.github.javaparser + javaparser-symbol-solver-core + 3.24.2 + + + + + com.github.promeg + tinypinyin + 2.0.3 + + + com.github.promeg + tinypinyin-lexicons-java-cncity + 2.0.3 + + + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + + + + + junit + junit + test + + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + org.hamcrest + hamcrest + 2.2 + test + + + + diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/BuilderDemo01.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/BuilderDemo01.java new file mode 100644 index 00000000..9e4104fe --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/BuilderDemo01.java @@ -0,0 +1,28 @@ +package io.github.dunwu.javatech.bean.lombok; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Builder; +import lombok.Data; + +/** + * 使用 @Builder 不当导致 json 反序列化失败 + * + * @author Zhang Peng + * @date 2020/12/3 + */ +@Data +@Builder +public class BuilderDemo01 { + + private String name; + + public static void main(String[] args) throws JsonProcessingException { + BuilderDemo01 demo01 = BuilderDemo01.builder().name("demo01").build(); + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(demo01); + BuilderDemo01 expectDemo01 = mapper.readValue(json, BuilderDemo01.class); + System.out.println(expectDemo01.toString()); + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/BuilderDemo02.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/BuilderDemo02.java new file mode 100644 index 00000000..0820b063 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/BuilderDemo02.java @@ -0,0 +1,33 @@ +package io.github.dunwu.javatech.bean.lombok; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 修正 {@link BuilderDemo01} 使用不当的问题 + * + * @author Zhang Peng + * @date 2020/12/3 + * @see BuilderDemo01 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BuilderDemo02 { + + private String name; + + public static void main(String[] args) throws JsonProcessingException { + BuilderDemo02 demo02 = BuilderDemo02.builder().name("demo01").build(); + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(demo02); + BuilderDemo02 expectDemo02 = mapper.readValue(json, BuilderDemo02.class); + System.out.println(expectDemo02.toString()); + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/DataDemo.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/DataDemo.java new file mode 100644 index 00000000..a35811c0 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/DataDemo.java @@ -0,0 +1,25 @@ +package io.github.dunwu.javatech.bean.lombok; + +import lombok.Data; +import lombok.ToString; + +import java.util.List; + +/** + * @Data 示例 + * + * @author Zhang Peng + * @see @Data + * @since 2019-11-22 + */ +@Data(staticConstructor = "of") +@ToString +public class DataDemo { + + private final Person founder; + + protected List employees; + + private String name; + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/EqualsAndHashCodeDemo.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/EqualsAndHashCodeDemo.java new file mode 100644 index 00000000..c342ab4d --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/EqualsAndHashCodeDemo.java @@ -0,0 +1,55 @@ +package io.github.dunwu.javatech.bean.lombok; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NonNull; + +/** + * @EqualsAndHashCode 示例 + * + * @author Zhang Peng + * @see @EqualsAndHashCode + * @since 2019-11-22 + */ +@Data +@EqualsAndHashCode(callSuper = true, exclude = { "address", "city", "state", "zip" }) +public class EqualsAndHashCodeDemo extends Person { + + @NonNull + private String name; + + @NonNull + private Gender gender; + + private String ssn; + + private String address; + + private String city; + + private String state; + + private String zip; + + public EqualsAndHashCodeDemo(@NonNull String name, @NonNull Gender gender) { + this.name = name; + this.gender = gender; + } + + public EqualsAndHashCodeDemo(@NonNull String name, @NonNull Gender gender, + String ssn, String address, String city, String state, String zip) { + this.name = name; + this.gender = gender; + this.ssn = ssn; + this.address = address; + this.city = city; + this.state = state; + this.zip = zip; + } + + public enum Gender { + Male, + Female + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/GetterAndSetterDemo.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/GetterAndSetterDemo.java new file mode 100644 index 00000000..e4365b6e --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/GetterAndSetterDemo.java @@ -0,0 +1,23 @@ +package io.github.dunwu.javatech.bean.lombok; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; + +/** + * @Getter@Setter 示例 + * + * @author Zhang Peng + * @see @Getter and @Setter + * @since 2019-11-22 + */ +public class GetterAndSetterDemo { + + @Getter + @Setter + private boolean employed = true; + + @Setter(AccessLevel.PROTECTED) + private String name; + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/NonNullDemo.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/NonNullDemo.java new file mode 100644 index 00000000..266e3ad8 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/NonNullDemo.java @@ -0,0 +1,23 @@ +package io.github.dunwu.javatech.bean.lombok; + +import lombok.Getter; +import lombok.NonNull; +import lombok.Setter; + +import java.util.List; + +/** + * @NonNull 示例 + * + * @author Zhang Peng + * @see @NonNull + * @since 2019-11-22 + */ +public class NonNullDemo { + + @Getter + @Setter + @NonNull + private List members; + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/Person.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/Person.java new file mode 100644 index 00000000..30f1519c --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/Person.java @@ -0,0 +1,26 @@ +package io.github.dunwu.javatech.bean.lombok; + +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.ToString; + +/** + * @Data@ToString@EqualsAndHashCode 示例 + * + * @author Zhang Peng + * @see @Data + * 、@ToString@EqualsAndHashCode + * @since 2019-11-22 + */ +@Data +@ToString(exclude = "age") +@EqualsAndHashCode(exclude = { "age", "sex" }) +public class Person { + + protected String name; + + protected Integer age; + + protected String sex; + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/ToStringDemo.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/ToStringDemo.java new file mode 100644 index 00000000..bd083e91 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/ToStringDemo.java @@ -0,0 +1,24 @@ +package io.github.dunwu.javatech.bean.lombok; + +import lombok.ToString; + +/** + * @author Zhang Peng + * @since 2019-11-22 + */ +@ToString(callSuper = true, exclude = "someExcludedField") +public class ToStringDemo { + + private boolean someBoolean = true; + + private String someStringField; + + private float someExcludedField; + + public ToStringDemo(boolean someBoolean, String someStringField, float someExcludedField) { + this.someBoolean = someBoolean; + this.someStringField = someStringField; + this.someExcludedField = someExcludedField; + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/User.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/User.java new file mode 100644 index 00000000..093b010a --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/User.java @@ -0,0 +1,24 @@ +package io.github.dunwu.javatech.bean.lombok; + +import lombok.Data; +import lombok.ToString; + +/** + * @Data@ToString@EqualsAndHashCode 示例 + * + * @author Zhang Peng + * @see @Data + * 、@ToString@EqualsAndHashCode + * @since 2019-11-22 + */ +@Data +@ToString +public class User { + + private String name; + + private Integer age; + + private String sex; + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/package-info.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/package-info.java new file mode 100644 index 00000000..c0f2a966 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/lombok/package-info.java @@ -0,0 +1,8 @@ +/** + * 本 package 下所有代码均为 Lombok 示例 + * + * @author Zhang Peng + * @see Reducing Boilerplate Code with Project Lombok + * @since 2019-11-22 + */ +package io.github.dunwu.javatech.bean.lombok; diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/Group.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/Group.java new file mode 100644 index 00000000..f07b3e08 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/Group.java @@ -0,0 +1,42 @@ +package io.github.dunwu.javatech.bean.sample; + +import java.util.ArrayList; +import java.util.List; + +public class Group { + + private Long id; + + private String name; + + private List users = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } + + public void addUser(User user) { + this.users.add(user); + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/Person.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/Person.java new file mode 100644 index 00000000..8d87dddd --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/Person.java @@ -0,0 +1,42 @@ +package io.github.dunwu.javatech.bean.sample; + +import java.io.Serializable; + +public class Person implements Serializable { + + private static final long serialVersionUID = -210388541252854256L; + + private String name; + + private int age; + + public Person() { + } + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + @Override + public String toString() { + return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/TestBean.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/TestBean.java new file mode 100644 index 00000000..b53be676 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/TestBean.java @@ -0,0 +1,59 @@ +package io.github.dunwu.javatech.bean.sample; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 定义一个满足大多数情况的 Bean 结构(含 JDK8 数据类型),使得各种 Json 库测试性能时能相对公平 + * + * @author Zhang Peng + * @since 2019-11-22 + */ +@Data +@ToString +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +public class TestBean implements Serializable { + + private static final long serialVersionUID = -6473181683996762084L; + + private int i1; + + private Integer i2; + + private float f1; + + private Double d1; + + private Date date1; + + private LocalDateTime date2; + + private LocalDate date3; + + private Color color; + + private String[] strArray; + + private List intList; + + private Map map; + + public static enum Color { + RED, + YELLOW, + BLUE + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/TestBean2.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/TestBean2.java new file mode 100644 index 00000000..ac3a80e7 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/TestBean2.java @@ -0,0 +1,50 @@ +package io.github.dunwu.javatech.bean.sample; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 定义一个满足大多数情况的 Bean 结构(不含 JDK8 数据类型),使得各种 Json 库测试性能时能相对公平 + * + * @author Zhang Peng + * @since 2019-11-22 + */ +@Data +@ToString +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +public class TestBean2 { + + private int i1; + + private Integer i2; + + private float f1; + + private Double d1; + + private Date date1; + + private Color color; + + private String[] strArray; + + private List intList; + + private Map map; + + public static enum Color { + RED, + YELLOW, + BLUE + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/User.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/User.java new file mode 100644 index 00000000..b2418eb9 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/bean/sample/User.java @@ -0,0 +1,35 @@ +package io.github.dunwu.javatech.bean.sample; + +public class User { + + private Long id; + + private String name; + + public User() { + } + + public User(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public User setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public User setName(String name) { + this.name = name; + return this; + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/jmh/JMHSample_34_SafeLooping.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/jmh/JMHSample_34_SafeLooping.java new file mode 100644 index 00000000..ce44dbb9 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/jmh/JMHSample_34_SafeLooping.java @@ -0,0 +1,159 @@ +package io.github.dunwu.javatech.jmh; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.concurrent.TimeUnit; + +@State(Scope.Thread) +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) +@Fork(3) +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +public class JMHSample_34_SafeLooping { + + /* + * JMHSample_11_Loops warns about the dangers of using loops in @Benchmark methods. + * Sometimes, however, one needs to traverse through several elements in a dataset. + * This is hard to do without loops, and therefore we need to devise a scheme for + * safe looping. + */ + + /* + * Suppose we want to measure how much it takes to execute work() with different + * arguments. This mimics a frequent use case when multiple instances with the same + * implementation, but different data, is measured. + */ + + static final int BASE = 42; + + static int work(int x) { + return BASE + x; + } + + /* + * Every benchmark requires control. We do a trivial control for our benchmarks + * by checking the benchmark costs are growing linearly with increased task size. + * If it doesn't, then something wrong is happening. + */ + + @Param({ "1", "10", "100", "1000" }) + int size; + + int[] xs; + + @Setup + public void setup() { + xs = new int[size]; + for (int c = 0; c < size; c++) { + xs[c] = c; + } + } + + /* + * First, the obviously wrong way: "saving" the result into a local variable would not + * work. A sufficiently smart compiler will inline work(), and figure out only the last + * work() call needs to be evaluated. Indeed, if you run it with varying $size, the score + * will stay the same! + */ + + @Benchmark + public int measureWrong_1() { + int acc = 0; + for (int x : xs) { + acc = work(x); + } + return acc; + } + + /* + * Second, another wrong way: "accumulating" the result into a local variable. While + * it would force the computation of each work() method, there are software pipelining + * effects in action, that can merge the operations between two otherwise distinct work() + * bodies. This will obliterate the benchmark setup. + * + * In this example, HotSpot does the unrolled loop, merges the $BASE operands into a single + * addition to $acc, and then does a bunch of very tight stores of $x-s. The final performance + * depends on how much of the loop unrolling happened *and* how much data is available to make + * the large strides. + */ + + @Benchmark + public int measureWrong_2() { + int acc = 0; + for (int x : xs) { + acc += work(x); + } + return acc; + } + + /* + * Now, let's see how to measure these things properly. A very straight-forward way to + * break the merging is to sink each result to Blackhole. This will force runtime to compute + * every work() call in full. (We would normally like to care about several concurrent work() + * computations at once, but the memory effects from Blackhole.consume() prevent those optimization + * on most runtimes). + */ + + @Benchmark + public void measureRight_1(Blackhole bh) { + for (int x : xs) { + bh.consume(work(x)); + } + } + + /* + * DANGEROUS AREA, PLEASE READ THE DESCRIPTION BELOW. + * + * Sometimes, the cost of sinking the value into a Blackhole is dominating the nano-benchmark score. + * In these cases, one may try to do a make-shift "sinker" with non-inlineable method. This trick is + * *very* VM-specific, and can only be used if you are verifying the generated code (that's a good + * strategy when dealing with nano-benchmarks anyway). + * + * You SHOULD NOT use this trick in most cases. Apply only where needed. + */ + + @Benchmark + public void measureRight_2() { + for (int x : xs) { + sink(work(x)); + } + } + + @CompilerControl(CompilerControl.Mode.DONT_INLINE) + public static void sink(int v) { + // IT IS VERY IMPORTANT TO MATCH THE SIGNATURE TO AVOID AUTOBOXING. + // The method intentionally does nothing. + } + + + /* + * ============================== HOW TO RUN THIS TEST: ==================================== + * + * You might notice measureWrong_1 does not depend on $size, measureWrong_2 has troubles with + * linearity, and otherwise much faster than both measureRight_*. You can also see measureRight_2 + * is marginally faster than measureRight_1. + * + * You can run this test: + * + * a) Via the command line: + * $ mvn clean install + * $ java -jar target/benchmarks.jar JMHSample_34 + * + * b) Via the Java API: + * (see the JMH homepage for possible caveats when running from IDE: + * http://openjdk.java.net/projects/code-tools/jmh/) + */ + + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder().include(JMHSample_34_SafeLooping.class.getSimpleName()).forks(3).build(); + + new Runner(opt).run(); + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/jmh/JmhQuickStart.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/jmh/JmhQuickStart.java new file mode 100644 index 00000000..7b16bcc1 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/jmh/JmhQuickStart.java @@ -0,0 +1,40 @@ +package io.github.dunwu.javatech.jmh; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 3) +@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class JmhQuickStart { + + public static void main(String[] args) throws RunnerException { + Options opt = new OptionsBuilder().include(JmhQuickStart.class.getSimpleName()).forks(1).build(); + new Runner(opt).run(); + } + + @Benchmark + public String testStringAdd() { + String a = ""; + for (int i = 0; i < 10; i++) { + a += i; + } + return a; + } + + @Benchmark + public String testStringBuilderAdd() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; i++) { + sb.append(i); + } + return sb.toString(); + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/jmh/StringBuilderBenchmark.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/jmh/StringBuilderBenchmark.java new file mode 100644 index 00000000..00ee2ea4 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/jmh/StringBuilderBenchmark.java @@ -0,0 +1,44 @@ +package io.github.dunwu.javatech.jmh; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.RunnerException; +import org.openjdk.jmh.runner.options.Options; +import org.openjdk.jmh.runner.options.OptionsBuilder; + +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 3) +@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS) +@Threads(8) +@Fork(2) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class StringBuilderBenchmark { + + public static void main(String[] args) throws RunnerException { + Options options = new OptionsBuilder().include(StringBuilderBenchmark.class.getSimpleName()) + .output("d:/Benchmark.log") + .build(); + new Runner(options).run(); + } + + @Benchmark + public void testStringAdd() { + String str = ""; + for (int i = 0; i < 10; i++) { + str += i; + } + System.out.println(str); + } + + @Benchmark + public void testStringBuilderAdd() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; i++) { + sb.append(i); + } + System.out.println(sb.toString()); + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/poi/excel/ExcelUtil.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/poi/excel/ExcelUtil.java new file mode 100644 index 00000000..e75cd267 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/poi/excel/ExcelUtil.java @@ -0,0 +1,100 @@ +package io.github.dunwu.javatech.poi.excel; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.alibaba.excel.event.SyncReadListener; +import com.alibaba.excel.read.listener.ReadListener; +import com.alibaba.fastjson.JSON; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * @author Zhang Peng + * @since 2020-07-01 + */ +public class ExcelUtil { + + private static final Logger LOGGER = LoggerFactory.getLogger(ExcelUtil.class); + + public static List> readAsync(InputStream inputStream) { + List> list = new ArrayList<>(); + EasyExcel.read(inputStream, new NoModelDataListener(list)).sheet().doRead(); + return list; + } + + /** + * 异步方式,根据 InputStream 读取 Excel 内容。 + *

+ * 返回结果为指定的 Class 类型列表 + *

+ * 需要指定读取结束后,负责处理数据的监听器 + *

+ * 注意:Class 类型中不能使用枚举类型 + */ + public static void readAsync(InputStream inputStream, Class clazz, ReadListener listener) { + EasyExcel.read(inputStream, clazz, listener).sheet().doRead(); + } + + /** + * 同步方式,根据 InputStream 读取 Excel 内容。 + *

+ * 返回结果为指定的 Class 类型列表 + *

+ * 注意:Class 类型中不能使用枚举类型 + */ + public static List readSync(InputStream inputStream, Class clazz) { + SyncReadListener listener = new SyncReadListener(); + EasyExcel.read(inputStream, clazz, listener).sheet().doRead(); + List list = listener.getList(); + List dtoList = list.stream().map(i -> (T) i).collect(Collectors.toList()); + System.out.println(dtoList); + return dtoList; + } + + static class NoModelDataListener extends AnalysisEventListener> { + + /** + * 每隔5条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收 + */ + private static final int BATCH_COUNT = 1000; + + List> list; + + public NoModelDataListener(List> list) { + this.list = list; + } + + @Override + public void invoke(Map data, AnalysisContext context) { + LOGGER.info("解析到一条数据:{}", JSON.toJSONString(data)); + list.add(data); + if (list.size() >= BATCH_COUNT) { + saveData(); + list.clear(); + } + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + saveData(); + LOGGER.info("所有数据解析完成!"); + } + + /** + * 加上存储数据库 + */ + private void saveData() { + LOGGER.info("{}条数据,开始存储数据库!", list.size()); + LOGGER.info("存储数据库成功!"); + } + + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/poi/word/WordUtil.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/poi/word/WordUtil.java new file mode 100644 index 00000000..bda492cb --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/poi/word/WordUtil.java @@ -0,0 +1,263 @@ +package io.github.dunwu.javatech.poi.word; + +import org.apache.poi.hpsf.DocumentSummaryInformation; +import org.apache.poi.hpsf.SummaryInformation; +import org.apache.poi.hwpf.HWPFDocument; +import org.apache.poi.ooxml.POIXMLProperties; +import org.apache.poi.xwpf.extractor.XWPFWordExtractor; +import org.apache.poi.xwpf.usermodel.*; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * @author Zhang Peng + * @see https://poi.apache.org/ + * @see https://www.w3cschool.cn/apache_poi_word/apache_poi_word_overview.html + * @since 2018-11-08 + */ +public class WordUtil { + + /** + * 创建空白文档 + * + * @param filename + * @throws IOException + */ + public static void create(String filename) throws IOException { + // Blank Document + XWPFDocument document = new XWPFDocument(); + // Write the Document in file system + FileOutputStream out = new FileOutputStream(new File(filename)); + document.write(out); + out.close(); + System.out.printf("create %s written successully\n", filename); + } + + /** + * 创建 *.docx 文档,包含 content 内容 + * + * @param filename + * @throws IOException + */ + public static void create(String filename, String content) throws IOException { + // Blank Document + XWPFDocument document = new XWPFDocument(); + // Write the Document in file system + FileOutputStream out = new FileOutputStream(new File(filename)); + + // create Paragraph + XWPFParagraph paragraph = document.createParagraph(); + XWPFRun run = paragraph.createRun(); + run.setText(content); + document.write(out); + out.close(); + System.out.printf("create %s written successully\n", filename); + } + + /** + * 创建 *.docx 文档,包含 content 内容,content 内容置于边框中 + * + * @param filename + * @throws IOException + */ + public static void createWithBorders(String filename, String content) throws IOException { + // Blank Document + XWPFDocument document = new XWPFDocument(); + + // Write the Document in file system + FileOutputStream out = new FileOutputStream(new File(filename)); + + // create paragraph + XWPFParagraph paragraph = document.createParagraph(); + + // Set bottom border to paragraph + paragraph.setBorderBottom(Borders.BASIC_BLACK_DASHES); + + // Set left border to paragraph + paragraph.setBorderLeft(Borders.BASIC_BLACK_DASHES); + + // Set right border to paragraph + paragraph.setBorderRight(Borders.BASIC_BLACK_DASHES); + + // Set top border to paragraph + paragraph.setBorderTop(Borders.BASIC_BLACK_DASHES); + + XWPFRun run = paragraph.createRun(); + run.setText(content); + + document.write(out); + out.close(); + System.out.printf("create %s written successully\n", filename); + } + + /** + * 表格 + * + * @param filename + * @throws IOException + */ + public static void createWithTable(String filename) throws IOException { + // Blank Document + XWPFDocument document = new XWPFDocument(); + + // Write the Document in file system + FileOutputStream out = new FileOutputStream(new File(filename)); + + // create table + XWPFTable table = document.createTable(); + // create first row + XWPFTableRow tableRowOne = table.getRow(0); + tableRowOne.getCell(0).setText("col one, row one"); + tableRowOne.addNewTableCell().setText("col two, row one"); + tableRowOne.addNewTableCell().setText("col three, row one"); + // create second row + XWPFTableRow tableRowTwo = table.createRow(); + tableRowTwo.getCell(0).setText("col one, row two"); + tableRowTwo.getCell(1).setText("col two, row two"); + tableRowTwo.getCell(2).setText("col three, row two"); + // create third row + XWPFTableRow tableRowThree = table.createRow(); + tableRowThree.getCell(0).setText("col one, row three"); + tableRowThree.getCell(1).setText("col two, row three"); + tableRowThree.getCell(2).setText("col three, row three"); + + document.write(out); + out.close(); + System.out.printf("create %s written successully\n", filename); + } + + /** + * 字体样式 + * + * @param filename + * @throws IOException + */ + public static void createWithFontStyle(String filename) throws IOException { + // Blank Document + XWPFDocument document = new XWPFDocument(); + + // Write the Document in file system + FileOutputStream out = new FileOutputStream(new File(filename)); + + // create paragraph + XWPFParagraph paragraph = document.createParagraph(); + + // Set Bold an Italic + XWPFRun paragraphOneRunOne = paragraph.createRun(); + paragraphOneRunOne.setBold(true); + paragraphOneRunOne.setItalic(true); + paragraphOneRunOne.setText("Font Style"); + paragraphOneRunOne.addBreak(); + + // Set text Position + XWPFRun paragraphOneRunTwo = paragraph.createRun(); + paragraphOneRunTwo.setText("Font Style two"); + paragraphOneRunTwo.setTextPosition(100); + + // Set Strike through and Font Size and Subscript + XWPFRun paragraphOneRunThree = paragraph.createRun(); + paragraphOneRunThree.setStrike(true); + paragraphOneRunThree.setFontSize(20); + paragraphOneRunThree.setSubscript(VerticalAlign.SUBSCRIPT); + paragraphOneRunThree.setText(" Different Font Styles"); + + document.write(out); + out.close(); + System.out.printf("create %s written successully\n", filename); + } + + /** + * 对齐方式 + * + * @param filename + * @throws IOException + */ + public static void createWithAlign(String filename) throws IOException { + // Blank Document + XWPFDocument document = new XWPFDocument(); + + // Write the Document in file system + FileOutputStream out = new FileOutputStream(new File(filename)); + + // create paragraph + XWPFParagraph paragraph = document.createParagraph(); + + // Set alignment paragraph to RIGHT + paragraph.setAlignment(ParagraphAlignment.RIGHT); + XWPFRun run = paragraph.createRun(); + run.setText("At tutorialspoint.com, we strive hard to " + "provide quality tutorials for self-learning " + + "purpose in the domains of Academics, Information " + + "Technology, Management and Computer Programming " + "Languages."); + + // Create Another paragraph + paragraph = document.createParagraph(); + + // Set alignment paragraph to CENTER + paragraph.setAlignment(ParagraphAlignment.CENTER); + run = paragraph.createRun(); + run.setText("The endeavour started by Mohtashim, an AMU " + + "alumni, who is the founder and the managing director " + + "of Tutorials Point (I) Pvt. Ltd. He came up with the " + + "website tutorialspoint.com in year 2006 with the help" + + "of handpicked freelancers, with an array of tutorials" + " for computer programming languages. "); + document.write(out); + out.close(); + System.out.printf("create %s written successully\n", filename); + } + + /** + * 文本提取 + * + * @param filename + * @throws IOException + */ + public static void extractor(String filename) throws IOException { + XWPFDocument docx = new XWPFDocument(new FileInputStream(filename)); + // using XWPFWordExtractor Class + XWPFWordExtractor we = new XWPFWordExtractor(docx); + System.out.println(we.getText()); + } + + public static void setDocxProperties(String filename) throws IOException { + FileInputStream fis = new FileInputStream(new File(filename)); + XWPFDocument doc = new XWPFDocument(fis); + + POIXMLProperties properties = doc.getProperties(); + POIXMLProperties.CoreProperties coreProperties = properties.getCoreProperties(); + coreProperties.setCreator("星环信息科技有限公司"); + coreProperties.setLastModifiedByUser("星环信息科技有限公司"); + POIXMLProperties.ExtendedProperties extendedProperties = properties.getExtendedProperties(); + extendedProperties.getUnderlyingProperties().setCompany("星环信息科技有限公司"); + + FileOutputStream fos = new FileOutputStream(new File(filename)); + doc.write(fos); + + fos.close(); + doc.close(); + fis.close(); + } + + public static void setDocProperties(String filename) throws IOException { + System.out.println("filename = [" + filename + "]"); + FileInputStream fis = new FileInputStream(new File(filename)); + HWPFDocument doc = new HWPFDocument(fis); + + SummaryInformation summaryInformation = doc.getSummaryInformation(); + summaryInformation.setAuthor("张鹏"); + summaryInformation.setLastAuthor("张鹏"); + DocumentSummaryInformation documentSummaryInformation = doc.getDocumentSummaryInformation(); + documentSummaryInformation.setCompany("张鹏"); + documentSummaryInformation.setDocumentVersion("1"); + + FileOutputStream fos = new FileOutputStream(new File(filename)); + doc.write(fos); + + fos.close(); + doc.close(); + fis.close(); + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/FstDemo.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/FstDemo.java new file mode 100644 index 00000000..05af588a --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/FstDemo.java @@ -0,0 +1,73 @@ +package io.github.dunwu.javatech.seriralize; + +import org.nustaq.serialization.FSTConfiguration; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * FST 序列化/反序列化示例 + * + * @author Zhang Peng + * @see FST + * @since 2019-11-22 + */ +public class FstDemo { + + private static FSTConfiguration DEFAULT_CONFIG = FSTConfiguration.createDefaultConfiguration(); + + /** + * 将字符串反序列化为原对象,先使用 Base64 解码 + * + * @param str {@link #writeToString} 方法序列化后的字符串 + * @param clazz 原对象的类型 + * @param 原对象的类型 + * @return 原对象 + */ + public static T readFromString(String str, Class clazz) throws IOException { + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + return readFromBytes(Base64.getDecoder().decode(bytes), clazz); + } + + /** + * 将 byte 数组反序列化为原对象 + * + * @param bytes {@link #writeToBytes} 方法序列化后的 byte 数组 + * @param clazz 原对象的类型 + * @param 原对象的类型 + * @return 原对象 + */ + public static T readFromBytes(byte[] bytes, Class clazz) throws IOException { + Object obj = DEFAULT_CONFIG.asObject(bytes); + if (clazz.isInstance(obj)) { + return (T) obj; + } else { + throw new IOException("derialize failed"); + } + } + + /** + * 将对象序列化为 byte 数组后,再使用 Base64 编码 + * + * @param obj 任意对象 + * @param 对象的类型 + * @return 序列化后的字符串 + */ + public static String writeToString(T obj) { + byte[] bytes = writeToBytes(obj); + return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8); + } + + /** + * 将对象序列化为 byte 数组 + * + * @param obj 任意对象 + * @param 对象的类型 + * @return 序列化后的 byte 数组 + */ + public static byte[] writeToBytes(T obj) { + return DEFAULT_CONFIG.asByteArray(obj); + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/JdkSerializeDemo.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/JdkSerializeDemo.java new file mode 100644 index 00000000..0043a5c7 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/JdkSerializeDemo.java @@ -0,0 +1,78 @@ +package io.github.dunwu.javatech.seriralize; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * JDK 默认序列化、反序列化机制示例 + * + * @author Zhang Peng + * @since 2019-11-22 + */ +public class JdkSerializeDemo { + + /** + * 将字符串反序列化为原对象,先使用 Base64 解码 + * + * @param str {@link #writeToString} 方法序列化后的字符串 + * @param clazz 原对象的类型 + * @param 原对象的类型 + * @return 原对象 + */ + public static T readFromString(String str, Class clazz) throws IOException, ClassNotFoundException { + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + return readFromBytes(Base64.getDecoder().decode(bytes), clazz); + } + + /** + * 将 byte 数组反序列化为原对象 + * + * @param bytes {@link #writeToBytes} 方法序列化后的 byte 数组 + * @param clazz 原对象的类型 + * @param 原对象的类型 + * @return 原对象 + */ + public static T readFromBytes(byte[] bytes, Class clazz) throws IOException, ClassNotFoundException { + ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais); + Object obj = ois.readObject(); + bais.close(); + ois.close(); + if (clazz.isInstance(obj)) { + return (T) obj; + } else { + throw new IOException("derialize failed"); + } + } + + /** + * 将对象序列化为 byte 数组后,再使用 Base64 编码 + * + * @param obj 任意对象 + * @param 对象的类型 + * @return 序列化后的字符串 + */ + public static String writeToString(T obj) throws IOException { + byte[] bytes = writeToBytes(obj); + return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8); + } + + /** + * 将对象序列化为 byte 数组 + * + * @param obj 任意对象 + * @param 对象的类型 + * @return 序列化后的 byte 数组 + */ + public static byte[] writeToBytes(T obj) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(obj); + byte[] bytes = baos.toByteArray(); + baos.close(); + oos.close(); + return bytes; + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/KryoDemo.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/KryoDemo.java new file mode 100644 index 00000000..29d45909 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/KryoDemo.java @@ -0,0 +1,114 @@ +package io.github.dunwu.javatech.seriralize; + +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; +import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy; +import org.objenesis.strategy.StdInstantiatorStrategy; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Kyro 序列化/反序列化示例 + * + * @author Zhang Peng + * @author Kryo 应用指南 + * @since 2019-11-26 + */ +public class KryoDemo { + + // 每个线程的 Kryo 实例 + private static final ThreadLocal kryoLocal = ThreadLocal.withInitial(() -> { + Kryo kryo = new Kryo(); + + /** + * 不要轻易改变这里的配置!更改之后,序列化的格式就会发生变化, + * 上线的同时就必须清除 Redis 里的所有缓存, + * 否则那些缓存再回来反序列化的时候,就会报错 + */ + //支持对象循环引用(否则会栈溢出) + kryo.setReferences(true); //默认值就是 true,添加此行的目的是为了提醒维护者,不要改变这个配置 + + //不强制要求注册类(注册行为无法保证多个 JVM 内同一个类的注册编号相同;而且业务系统中大量的 Class 也难以一一注册) + kryo.setRegistrationRequired(false); //默认值就是 false,添加此行的目的是为了提醒维护者,不要改变这个配置 + + //Fix the NPE bug when deserializing Collections. + ((DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy()) + .setFallbackInstantiatorStrategy(new StdInstantiatorStrategy()); + + return kryo; + }); + + /** + * 将字符串反序列化为原对象,先使用 Base64 解码 + * + * @param str {@link #writeToString} 方法序列化后的字符串 + * @param clazz 原对象的类型 + * @param 原对象的类型 + * @return 原对象 + */ + public static T readFromString(String str, Class clazz) { + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + return readFromBytes(Base64.getDecoder().decode(bytes), clazz); + } + + /** + * 将 byte 数组反序列化为原对象 + * + * @param bytes {@link #writeToBytes} 方法序列化后的 byte 数组 + * @param clazz 原对象的类型 + * @param 原对象的类型 + * @return 原对象 + */ + @SuppressWarnings("unchecked") + public static T readFromBytes(byte[] bytes, Class clazz) { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + Input input = new Input(byteArrayInputStream); + + Kryo kryo = getInstance(); + return (T) kryo.readObject(input, clazz); + } + + /** + * 获得当前线程的 Kryo 实例 + * + * @return 当前线程的 Kryo 实例 + */ + public static Kryo getInstance() { + return kryoLocal.get(); + } + + /** + * 将对象序列化为 byte 数组后,再使用 Base64 编码 + * + * @param obj 任意对象 + * @param 对象的类型 + * @return 序列化后的字符串 + */ + public static String writeToString(T obj) { + byte[] bytes = writeToBytes(obj); + return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8); + } + + /** + * 将对象序列化为 byte 数组 + * + * @param obj 任意对象 + * @param 对象的类型 + * @return 序列化后的 byte 数组 + */ + public static byte[] writeToBytes(T obj) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + Output output = new Output(byteArrayOutputStream); + + Kryo kryo = getInstance(); + kryo.writeObject(output, obj); + output.flush(); + + return byteArrayOutputStream.toByteArray(); + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/BeanUtils.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/BeanUtils.java new file mode 100644 index 00000000..33499166 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/BeanUtils.java @@ -0,0 +1,68 @@ +package io.github.dunwu.javatech.seriralize.util; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +/** + * @author Zhang Peng + * @since 2019-11-22 + */ +public class BeanUtils { + + public static Group initGroupBean() { + Group group = new Group(); + group.setId(0L); + group.setName("admin"); + + User guestUser = new User(); + guestUser.setId(2L); + guestUser.setName("guest"); + + User rootUser = new User(); + rootUser.setId(3L); + rootUser.setName("root"); + + group.addUser(guestUser); + group.addUser(rootUser); + + return group; + } + + public static TestBean initJdk8Bean() { + String[] strArray = { "a", "b", "c" }; + Integer[] intArray = { 1, 2, 3, 4, 5 }; + List intList = new ArrayList<>(); + intList.addAll(Arrays.asList(intArray)); + Map map = new HashMap<>(); + map.put("name", "jack"); + map.put("age", 18); + map.put("length", 175.3f); + TestBean bean = new TestBean(); + java.sql.Date date = java.sql.Date.valueOf("2019-11-22"); + LocalDateTime localDateTime = LocalDateTime.of(2000, 1, 1, 12, 0, 0); + LocalDate localDate = LocalDate.of(1949, 10, 1); + bean.setI1(10).setI2(1024).setF1(0.5f).setD1(100.0) + .setDate1(date).setDate2(localDateTime).setDate3(localDate) + .setColor(TestBean.Color.BLUE).setStrArray(strArray).setIntList(intList).setMap(map); + return bean; + } + + public static TestBean2 initNotJdk8Bean() { + String[] strArray = { "a", "b", "c" }; + Integer[] intArray = { 1, 2, 3, 4, 5 }; + List intList = new ArrayList<>(); + intList.addAll(Arrays.asList(intArray)); + Map map = new HashMap<>(); + map.put("name", "jack"); + map.put("age", 18); + map.put("length", 175.3f); + TestBean2 bean = new TestBean2(); + java.sql.Date date = java.sql.Date.valueOf("2019-11-22"); + bean.setI1(10).setI2(1024).setF1(0.5f).setD1(100.0) + .setDate1(date) + .setColor(TestBean2.Color.BLUE).setStrArray(strArray).setIntList(intList).setMap(map); + return bean; + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/DateUtil.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/DateUtil.java new file mode 100644 index 00000000..31a92651 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/DateUtil.java @@ -0,0 +1,120 @@ +package io.github.dunwu.javatech.seriralize.util; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.Date; + +/** + * @author Zhang Peng + * @since 2020-04-08 + */ +public class DateUtil extends cn.hutool.core.date.DateUtil { + + /** + * 将 {@link Calendar} 转化为 {@link Date} + * + * @param calendar {@link Calendar} + * @return {@link Date} + */ + public static Date toDate(final Calendar calendar) { + return calendar.getTime(); + } + + /** + * 将 {@link LocalDateTime} 转化为 {@link Date} + * + * @param localDateTime {@link LocalDateTime} + * @return {@link Date} + */ + public static Date toDate(final LocalDateTime localDateTime) { + ZoneId zoneId = ZoneId.systemDefault(); + ZonedDateTime zdt = localDateTime.atZone(zoneId); + return Date.from(zdt.toInstant()); + } + + public static String formatDurationString(Duration duration) { + Duration temp = duration; + StringBuilder sb = new StringBuilder(); + + long days = temp.toDays(); + if (days > 0) { + sb.append(days + "d "); + } + temp = temp.minusDays(days); + + long hours = temp.toHours(); + if (hours > 0) { + sb.append(hours + "h "); + } + temp = temp.minusHours(hours); + + long minutes = temp.toMinutes(); + if (minutes > 0) { + sb.append(minutes + "m "); + } + temp = temp.minusMinutes(minutes); + + long seconds = temp.getSeconds(); + if (seconds > 0) { + sb.append(minutes + "s "); + } + temp = temp.minusSeconds(seconds); + + long millis = temp.toMillis(); + if (millis > 0) { + sb.append(millis + "ms "); + } + temp = temp.minusMillis(millis); + + long nanos = temp.toNanos(); + if (nanos > 0) { + sb.append(nanos + "ns "); + } + return sb.toString(); + } + + public static String formatDurationChineseString(Duration duration) { + Duration temp = duration; + StringBuilder sb = new StringBuilder(); + + long days = temp.toDays(); + if (days > 0) { + sb.append(days + "天 "); + } + temp = temp.minusDays(days); + + long hours = temp.toHours(); + if (hours > 0) { + sb.append(hours + "时 "); + } + temp = temp.minusHours(hours); + + long minutes = temp.toMinutes(); + if (minutes > 0) { + sb.append(minutes + "分 "); + } + temp = temp.minusMinutes(minutes); + + long seconds = temp.getSeconds(); + if (seconds > 0) { + sb.append(minutes + "秒 "); + } + temp = temp.minusSeconds(seconds); + + long millis = temp.toMillis(); + if (millis > 0) { + sb.append(millis + "毫秒 "); + } + temp = temp.minusMillis(millis); + + long nanos = temp.toNanos(); + if (nanos > 0) { + sb.append(nanos + "纳秒 "); + } + return sb.toString(); + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/Group.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/Group.java new file mode 100644 index 00000000..43343474 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/Group.java @@ -0,0 +1,42 @@ +package io.github.dunwu.javatech.seriralize.util; + +import java.util.ArrayList; +import java.util.List; + +public class Group { + + private Long id; + + private String name; + + private List users = new ArrayList<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } + + public void addUser(User user) { + this.users.add(user); + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/Person.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/Person.java new file mode 100644 index 00000000..8ca18c91 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/Person.java @@ -0,0 +1,42 @@ +package io.github.dunwu.javatech.seriralize.util; + +import java.io.Serializable; + +public class Person implements Serializable { + + private static final long serialVersionUID = -210388541252854256L; + + private String name; + + private int age; + + public Person() { + } + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + @Override + public String toString() { + return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/TestBean.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/TestBean.java new file mode 100644 index 00000000..5a3afe11 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/TestBean.java @@ -0,0 +1,59 @@ +package io.github.dunwu.javatech.seriralize.util; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 定义一个满足大多数情况的 Bean 结构(含 JDK8 数据类型),使得各种 Json 库测试性能时能相对公平 + * + * @author Zhang Peng + * @since 2019-11-22 + */ +@Data +@ToString +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +public class TestBean implements Serializable { + + private static final long serialVersionUID = -6473181683996762084L; + + private int i1; + + private Integer i2; + + private float f1; + + private Double d1; + + private Date date1; + + private LocalDateTime date2; + + private LocalDate date3; + + private Color color; + + private String[] strArray; + + private List intList; + + private Map map; + + public static enum Color { + RED, + YELLOW, + BLUE + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/TestBean2.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/TestBean2.java new file mode 100644 index 00000000..7f2881e5 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/TestBean2.java @@ -0,0 +1,50 @@ +package io.github.dunwu.javatech.seriralize.util; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.Date; +import java.util.List; +import java.util.Map; + +/** + * 定义一个满足大多数情况的 Bean 结构(不含 JDK8 数据类型),使得各种 Json 库测试性能时能相对公平 + * + * @author Zhang Peng + * @since 2019-11-22 + */ +@Data +@ToString +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +public class TestBean2 { + + private int i1; + + private Integer i2; + + private float f1; + + private Double d1; + + private Date date1; + + private Color color; + + private String[] strArray; + + private List intList; + + private Map map; + + public static enum Color { + RED, + YELLOW, + BLUE + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/User.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/User.java new file mode 100644 index 00000000..785788f0 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/seriralize/util/User.java @@ -0,0 +1,35 @@ +package io.github.dunwu.javatech.seriralize.util; + +public class User { + + private Long id; + + private String name; + + public User() { + } + + public User(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public User setId(Long id) { + this.id = id; + return this; + } + + public String getName() { + return name; + } + + public User setName(String name) { + this.name = name; + return this; + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/util/BeanUtils.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/util/BeanUtils.java new file mode 100644 index 00000000..72a05d1f --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/util/BeanUtils.java @@ -0,0 +1,73 @@ +package io.github.dunwu.javatech.util; + +import io.github.dunwu.javatech.bean.sample.Group; +import io.github.dunwu.javatech.bean.sample.TestBean; +import io.github.dunwu.javatech.bean.sample.TestBean2; +import io.github.dunwu.javatech.bean.sample.User; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +/** + * @author Zhang Peng + * @since 2019-11-22 + */ +public class BeanUtils { + + public static Group initGroupBean() { + Group group = new Group(); + group.setId(0L); + group.setName("admin"); + + User guestUser = new User(); + guestUser.setId(2L); + guestUser.setName("guest"); + + User rootUser = new User(); + rootUser.setId(3L); + rootUser.setName("root"); + + group.addUser(guestUser); + group.addUser(rootUser); + + return group; + } + + public static TestBean initJdk8Bean() { + String[] strArray = { "a", "b", "c" }; + Integer[] intArray = { 1, 2, 3, 4, 5 }; + List intList = new ArrayList<>(); + intList.addAll(Arrays.asList(intArray)); + Map map = new HashMap<>(); + map.put("name", "jack"); + map.put("age", 18); + map.put("length", 175.3f); + TestBean bean = new TestBean(); + java.sql.Date date = java.sql.Date.valueOf("2019-11-22"); + LocalDateTime localDateTime = LocalDateTime.of(2000, 1, 1, 12, 0, 0); + LocalDate localDate = LocalDate.of(1949, 10, 1); + bean.setI1(10).setI2(1024).setF1(0.5f).setD1(100.0) + .setDate1(date).setDate2(localDateTime).setDate3(localDate) + .setColor(TestBean.Color.BLUE).setStrArray(strArray).setIntList(intList).setMap(map); + return bean; + } + + public static TestBean2 initNotJdk8Bean() { + String[] strArray = { "a", "b", "c" }; + Integer[] intArray = { 1, 2, 3, 4, 5 }; + List intList = new ArrayList<>(); + intList.addAll(Arrays.asList(intArray)); + Map map = new HashMap<>(); + map.put("name", "jack"); + map.put("age", 18); + map.put("length", 175.3f); + TestBean2 bean = new TestBean2(); + java.sql.Date date = java.sql.Date.valueOf("2019-11-22"); + bean.setI1(10).setI2(1024).setF1(0.5f).setD1(100.0) + .setDate1(date) + .setColor(TestBean2.Color.BLUE).setStrArray(strArray).setIntList(intList).setMap(map); + return bean; + } + +} diff --git a/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/util/DateUtil.java b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/util/DateUtil.java new file mode 100644 index 00000000..90c70d98 --- /dev/null +++ b/codes/javatech/javatech-lib/src/main/java/io/github/dunwu/javatech/util/DateUtil.java @@ -0,0 +1,120 @@ +package io.github.dunwu.javatech.util; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Calendar; +import java.util.Date; + +/** + * @author Zhang Peng + * @since 2020-04-08 + */ +public class DateUtil extends cn.hutool.core.date.DateUtil { + + /** + * 将 {@link Calendar} 转化为 {@link Date} + * + * @param calendar {@link Calendar} + * @return {@link Date} + */ + public static Date toDate(final Calendar calendar) { + return calendar.getTime(); + } + + /** + * 将 {@link LocalDateTime} 转化为 {@link Date} + * + * @param localDateTime {@link LocalDateTime} + * @return {@link Date} + */ + public static Date toDate(final LocalDateTime localDateTime) { + ZoneId zoneId = ZoneId.systemDefault(); + ZonedDateTime zdt = localDateTime.atZone(zoneId); + return Date.from(zdt.toInstant()); + } + + public static String formatDurationString(Duration duration) { + Duration temp = duration; + StringBuilder sb = new StringBuilder(); + + long days = temp.toDays(); + if (days > 0) { + sb.append(days + "d "); + } + temp = temp.minusDays(days); + + long hours = temp.toHours(); + if (hours > 0) { + sb.append(hours + "h "); + } + temp = temp.minusHours(hours); + + long minutes = temp.toMinutes(); + if (minutes > 0) { + sb.append(minutes + "m "); + } + temp = temp.minusMinutes(minutes); + + long seconds = temp.getSeconds(); + if (seconds > 0) { + sb.append(minutes + "s "); + } + temp = temp.minusSeconds(seconds); + + long millis = temp.toMillis(); + if (millis > 0) { + sb.append(millis + "ms "); + } + temp = temp.minusMillis(millis); + + long nanos = temp.toNanos(); + if (nanos > 0) { + sb.append(nanos + "ns "); + } + return sb.toString(); + } + + public static String formatDurationChineseString(Duration duration) { + Duration temp = duration; + StringBuilder sb = new StringBuilder(); + + long days = temp.toDays(); + if (days > 0) { + sb.append(days + "天 "); + } + temp = temp.minusDays(days); + + long hours = temp.toHours(); + if (hours > 0) { + sb.append(hours + "时 "); + } + temp = temp.minusHours(hours); + + long minutes = temp.toMinutes(); + if (minutes > 0) { + sb.append(minutes + "分 "); + } + temp = temp.minusMinutes(minutes); + + long seconds = temp.getSeconds(); + if (seconds > 0) { + sb.append(minutes + "秒 "); + } + temp = temp.minusSeconds(seconds); + + long millis = temp.toMillis(); + if (millis > 0) { + sb.append(millis + "毫秒 "); + } + temp = temp.minusMillis(millis); + + long nanos = temp.toNanos(); + if (nanos > 0) { + sb.append(nanos + "纳秒 "); + } + return sb.toString(); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/bean/BeanConvertTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/bean/BeanConvertTest.java new file mode 100644 index 00000000..9efadb20 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/bean/BeanConvertTest.java @@ -0,0 +1,107 @@ +package io.github.dunwu.javatech.bean; + +import cn.hutool.core.bean.BeanUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * @author Zhang Peng + * @since 2020-05-15 + */ +public class BeanConvertTest { + + @Test + @DisplayName("Bean 转换测试") + public void convertTest() { + Person person = new Person(); + person.setName("Jack").setAge(20).setSex(Sex.MALE); + // Bean 转换 + User user = BeanUtil.toBean(person, User.class); + Assertions.assertEquals(person.getName(), user.getName()); + Assertions.assertEquals(person.getAge(), user.getAge()); + Assertions.assertEquals(person.getSex().name(), user.getSex()); + } + + public enum Sex { + MALE, + FEMALE + } + + + static class User { + + private String name; + + private Integer age; + + private String sex; + + public String getName() { + return name; + } + + public User setName(String name) { + this.name = name; + return this; + } + + public Integer getAge() { + return age; + } + + public User setAge(Integer age) { + this.age = age; + return this; + } + + public String getSex() { + return sex; + } + + public User setSex(String sex) { + this.sex = sex; + return this; + } + + } + + + static class Person { + + private String name; + + private Integer age; + + private Sex sex; + + public String getName() { + return name; + } + + public Person setName(String name) { + this.name = name; + return this; + } + + public Integer getAge() { + return age; + } + + public Person setAge(Integer age) { + this.age = age; + return this; + } + + public Sex getSex() { + return sex; + } + + public Person setSex(Sex sex) { + this.sex = sex; + return this; + } + + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/bean/lombok/LombokTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/bean/lombok/LombokTest.java new file mode 100644 index 00000000..f0988e7f --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/bean/lombok/LombokTest.java @@ -0,0 +1,142 @@ +package io.github.dunwu.javatech.bean.lombok; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Cleanup; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Lombok 单元测试 + * + * @author Zhang Peng + * @see Reducing Boilerplate Code with Project Lombok + */ +public class LombokTest { + + @Test + @DisplayName("测试 @Getter / @Setter") + public void testGetterAndSetterDemo() { + GetterAndSetterDemo demo = new GetterAndSetterDemo(); + demo.setEmployed(true); + demo.setName("xxx"); + Assertions.assertTrue(demo.isEmployed()); + } + + @Test + @DisplayName("测试 @ToString") + public void testToStringDemo() { + ToStringDemo demo = new ToStringDemo(true, "abc", 0.5f); + System.out.println(demo.toString()); + + String str = demo.toString(); + Assertions.assertTrue(str.contains("someBoolean=true, someStringField=abc")); + + Person person = new Person(); + person.setName("张三"); + person.setAge(20); + person.setSex("男"); + System.out.println(person.toString()); + Assertions.assertEquals("Person(name=张三, sex=男)", person.toString()); + } + + @Test + @DisplayName("测试 @EqualsAndHashCode") + public void testEqualsAndHashCodeDemo() { + EqualsAndHashCodeDemo demo1 = + new EqualsAndHashCodeDemo("name1", EqualsAndHashCodeDemo.Gender.Female, "ssn", "xxx", "xxx", "xxx", "xxx"); + EqualsAndHashCodeDemo demo2 = + new EqualsAndHashCodeDemo("name1", EqualsAndHashCodeDemo.Gender.Female, "ssn", "ooo", "ooo", "ooo", "ooo"); + Assertions.assertEquals(demo1, demo2); + + Person person = new Person(); + person.setName("张三"); + person.setAge(20); + person.setSex("男"); + + Person person2 = new Person(); + person2.setName("张三"); + person2.setAge(18); + person2.setSex("男"); + + Person person3 = new Person(); + person3.setName("李四"); + person3.setAge(20); + person3.setSex("男"); + + Assertions.assertEquals(person2, person); + Assertions.assertNotEquals(person3, person); + } + + @Test + @DisplayName("测试 @Data") + public void testDataDemo() { + Person huangshiren = new Person(); + huangshiren.setName("黄世仁"); + huangshiren.setAge(30); + huangshiren.setSex("男"); + Person yangbailao = new Person(); + yangbailao.setName("杨白劳"); + yangbailao.setAge(50); + yangbailao.setSex("男"); + Person xiaobaicai = new Person(); + xiaobaicai.setName("小白菜"); + xiaobaicai.setAge(20); + xiaobaicai.setSex("女"); + + List personList = new ArrayList<>(); + personList.add(yangbailao); + personList.add(xiaobaicai); + + DataDemo demo = DataDemo.of(huangshiren); + demo.setName("黑心农产品公司"); + demo.setEmployees(personList); + Assertions.assertEquals("黑心农产品公司", demo.getName()); + Assertions.assertEquals(huangshiren, demo.getFounder()); + + System.out.println("公司名:" + demo.getName()); + System.out.println("创始人:" + demo.getFounder()); + System.out.println("员工信息"); + + List employees = demo.getEmployees(); + Assertions.assertTrue(employees.containsAll(Arrays.asList(yangbailao, xiaobaicai))); + demo.getEmployees().forEach(person -> { + System.out.println(person.toString()); + }); + } + + @Test + @DisplayName("测试 @Cleanup") + public void testCleanUp() { + try { + @Cleanup + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(new byte[] { 'Y', 'e', 's' }); + System.out.println(baos.toString()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + @DisplayName("测试 @Builder") + public void testBuilder() { + BuilderDemo01 demo01 = BuilderDemo01.builder().name("demo01").build(); + ObjectMapper mapper = new ObjectMapper(); + String json = null; + try { + json = mapper.writeValueAsString(demo01); + BuilderDemo01 expectDemo01 = mapper.readValue(json, BuilderDemo01.class); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/http/OkHttpTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/http/OkHttpTests.java new file mode 100644 index 00000000..f5f66c12 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/http/OkHttpTests.java @@ -0,0 +1,63 @@ +package io.github.dunwu.javatech.http; + +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +/** + * okhttp API 测试 + * + * @author Zhang Peng + * @date 2022-06-09 + */ +@Slf4j +public class OkHttpTests { + + OkHttpClient client = new OkHttpClient(); + + @Test + @DisplayName("OkHttp 同步 Get 请求") + public void testSyncGet() throws IOException { + String ip = "127.0.0.1"; + String url = "http://realip.cc/?ip=" + ip; + Request request = new Request.Builder().url(url).build(); + + try (Response response = client.newCall(request).execute()) { + String json = response.body().string(); + System.out.println("请求结果:" + json); + } + } + + @Test + @DisplayName("OkHttp 异步 Get 请求") + public void testAsyncGet() { + String ip = "127.0.0.1"; + String url = "http://realip.cc/?ip=" + ip; + Request request = new Request.Builder().url(url).build(); + + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(Call call, IOException e) { + e.printStackTrace(); + } + + @Override + public void onResponse(Call call, Response response) throws IOException { + if (!response.isSuccessful()) + throw new IOException("Unexpected code " + response); + + Headers responseHeaders = response.headers(); + for (int i = 0, size = responseHeaders.size(); i < size; i++) { + System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i)); + } + + String json = response.body().string(); + System.out.println("请求结果:" + json); + } + }); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserCommentReporterTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserCommentReporterTest.java new file mode 100644 index 00000000..be288dbf --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserCommentReporterTest.java @@ -0,0 +1,72 @@ +package io.github.dunwu.javatech.java; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.comments.Comment; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.List; +import java.util.stream.Collectors; + +/** + * JavaParser 解析java文件中的注释 + * + * @author Zhang Peng + * @date 2022-02-10 + */ +public class JavaParserCommentReporterTest { + + private static final String FILE_PATH = + "src/test/java/io/github/dunwu/javatech/java/samples/ReversePolishNotation.java"; + + @Test + @DisplayName("解析java文件中的注释 - 测试一") + public void commentReporterStarter() throws FileNotFoundException { + CompilationUnit cu = StaticJavaParser.parse(new FileInputStream(FILE_PATH)); + List comments = cu.getAllContainedComments(); + comments.forEach(System.out::println); + } + + @Test + @DisplayName("解析java文件中的注释 - 测试二") + public void commentReporterComplete() throws FileNotFoundException { + + CompilationUnit cu = StaticJavaParser.parse(new File(FILE_PATH)); + + List comments = cu.getAllContainedComments() + .stream() + .map(p -> new CommentReportEntry(p.getClass().getSimpleName(), + p.getContent(), + p.getRange().map(r -> r.begin.line).orElse(-1), + !p.getCommentedNode().isPresent())) + .collect(Collectors.toList()); + + comments.forEach(System.out::println); + } + + private static class CommentReportEntry { + + private String type; + private String text; + private int lineNumber; + private boolean isOrphan; + + CommentReportEntry(String type, String text, int lineNumber, boolean isOrphan) { + this.type = type; + this.text = text; + this.lineNumber = lineNumber; + this.isOrphan = isOrphan; + } + + @Override + public String toString() { + return lineNumber + "|" + type + "|" + isOrphan + "|" + text.replaceAll("\\n", "").trim(); + } + + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserLexicalPreservationTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserLexicalPreservationTest.java new file mode 100644 index 00000000..75729ab0 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserLexicalPreservationTest.java @@ -0,0 +1,57 @@ +package io.github.dunwu.javatech.java; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Modifier; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.printer.lexicalpreservation.LexicalPreservingPrinter; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * @author Zhang Peng + * @date 2022-02-10 + */ +public class JavaParserLexicalPreservationTest { + + @Test + @DisplayName("JavaParser 词汇保存1") + public void lexicalPreservationStarter() { + String code = "class A { }"; + CompilationUnit cu = StaticJavaParser.parse(code); + LexicalPreservingPrinter.setup(cu); + System.out.println(LexicalPreservingPrinter.print(cu)); + } + + @Test + @DisplayName("JavaParser 词汇保存2") + public void lexicalPreservationComplete() { + String code = "// Hey, this is a comment\n\n\n// Another one\n\nclass A { }"; + CompilationUnit cu = StaticJavaParser.parse(code); + LexicalPreservingPrinter.setup(cu); + + System.out.println(LexicalPreservingPrinter.print(cu)); + + System.out.println("----------------"); + + ClassOrInterfaceDeclaration myClass = cu.getClassByName("A").get(); + myClass.setName("MyNewClassName"); + System.out.println(LexicalPreservingPrinter.print(cu)); + + System.out.println("----------------"); + + myClass = cu.getClassByName("MyNewClassName").get(); + myClass.setName("MyNewClassName"); + myClass.addModifier(Modifier.Keyword.PUBLIC); + System.out.println(LexicalPreservingPrinter.print(cu)); + + System.out.println("----------------"); + + myClass = cu.getClassByName("MyNewClassName").get(); + myClass.setName("MyNewClassName"); + myClass.addModifier(Modifier.Keyword.PUBLIC); + cu.setPackageDeclaration("io.github.dunwu.javatech.java.samples"); + System.out.println(LexicalPreservingPrinter.print(cu)); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserModifyingVisitorTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserModifyingVisitorTest.java new file mode 100644 index 00000000..0e6430a6 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserModifyingVisitorTest.java @@ -0,0 +1,67 @@ +package io.github.dunwu.javatech.java; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.visitor.ModifierVisitor; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.regex.Pattern; + +/** + * JavaParser 美化打印测试 + * + * @author Zhang Peng + * @date 2022-02-07 + */ +public class JavaParserModifyingVisitorTest { + + private static final String FILE_PATH = + "src/test/java/io/github/dunwu/javatech/java/samples/ReversePolishNotation.java"; + private static final Pattern LOOK_AHEAD_THREE = Pattern.compile("(\\d)(?=(\\d{3})+$)"); + + @Test + @DisplayName("JavaParser VoidVisitor 测试 1") + public void voidVisitorStarter() throws FileNotFoundException { + CompilationUnit cu = StaticJavaParser.parse(new FileInputStream(FILE_PATH)); + System.out.println(cu.toString()); + } + + @Test + @DisplayName("JavaParser VoidVisitor 测试 2") + public void prettyPrintComplete() throws FileNotFoundException { + + CompilationUnit cu = StaticJavaParser.parse(new FileInputStream(FILE_PATH)); + + ModifierVisitor numericLiteralVisitor = new IntegerLiteralModifier(); + numericLiteralVisitor.visit(cu, null); + + System.out.println(cu.toString()); + } + + private static class IntegerLiteralModifier extends ModifierVisitor { + + @Override + public FieldDeclaration visit(FieldDeclaration fd, Void arg) { + super.visit(fd, arg); + fd.getVariables().forEach(v -> + v.getInitializer().ifPresent(i -> + i.ifIntegerLiteralExpr(il -> + v.setInitializer(formatWithUnderscores(il.getValue())) + ) + ) + ); + return fd; + } + + } + + static String formatWithUnderscores(String value) { + String withoutUnderscores = value.replaceAll("_", ""); + return LOOK_AHEAD_THREE.matcher(withoutUnderscores).replaceAll("$1_"); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserPerttyPrintTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserPerttyPrintTest.java new file mode 100644 index 00000000..de1540cf --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserPerttyPrintTest.java @@ -0,0 +1,45 @@ +package io.github.dunwu.javatech.java; + +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.comments.LineComment; +import com.github.javaparser.printer.PrettyPrinter; +import com.github.javaparser.printer.configuration.Indentation; +import com.github.javaparser.printer.configuration.PrettyPrinterConfiguration; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * JavaParser 美化打印测试 + * + * @author Zhang Peng + * @date 2022-02-07 + */ +public class JavaParserPerttyPrintTest { + + @Test + @DisplayName("JavaParser 美化打印1") + public void prettyPrintStarter() { + ClassOrInterfaceDeclaration myClass = new ClassOrInterfaceDeclaration(); + myClass.setComment(new LineComment("A very cool class!")); + myClass.setName("MyClass"); + myClass.addField("String", "foo"); + System.out.println(myClass); + } + + @Test + @DisplayName("JavaParser 美化打印2") + public void prettyPrintComplete() { + ClassOrInterfaceDeclaration myClass = new ClassOrInterfaceDeclaration(); + myClass.setComment(new LineComment("A very cool class!")); + myClass.setName("MyClass"); + myClass.addField("String", "foo"); + + PrettyPrinterConfiguration conf = new PrettyPrinterConfiguration(); + conf.setIndentSize(1); + conf.setIndentType(Indentation.IndentType.SPACES); + conf.setPrintComments(false); + PrettyPrinter prettyPrinter = new PrettyPrinter(conf); + System.out.println(prettyPrinter.print(myClass)); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserTest.java new file mode 100644 index 00000000..8e5766e0 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserTest.java @@ -0,0 +1,222 @@ +package io.github.dunwu.javatech.java; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.NodeList; +import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration; +import com.github.javaparser.ast.body.EnumDeclaration; +import com.github.javaparser.ast.comments.JavadocComment; +import com.github.javaparser.ast.comments.LineComment; +import com.github.javaparser.ast.expr.AssignExpr; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.MethodCallExpr; +import com.github.javaparser.ast.type.ClassOrInterfaceType; +import com.github.javaparser.printer.PrettyPrinter; +import com.github.javaparser.printer.configuration.Indentation; +import com.github.javaparser.printer.configuration.PrettyPrinterConfiguration; +import com.github.javaparser.resolution.declarations.ResolvedReferenceTypeDeclaration; +import com.github.javaparser.resolution.types.ResolvedType; +import com.github.javaparser.symbolsolver.JavaSymbolSolver; +import com.github.javaparser.symbolsolver.model.resolution.TypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver; +import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.io.File; +import java.io.FileNotFoundException; +import java.util.List; + +/** + * @author Zhang Peng + * @date 2022-02-07 + */ +public class JavaParserTest { + + final String FILE_PATH = "src/test/java/io/github/dunwu/javatech/java/samples/ReversePolishNotation.java"; + + @Test + @DisplayName("获得 import") + public void testGetImports() { + try { + CompilationUnit compilationUnit = StaticJavaParser.parse(new File(FILE_PATH)); + Assertions.assertThat(CollectionUtil.isEmpty(compilationUnit.getImports())); + compilationUnit.getImports().forEach(i -> { + System.out.println(i.getName()); + }); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + + @Test + @DisplayName("获得类型") + public void testGetTypes() { + try { + CompilationUnit compilationUnit = StaticJavaParser.parse(new File(FILE_PATH)); + Assertions.assertThat(CollectionUtil.isEmpty(compilationUnit.getImports())); + compilationUnit.findAll(ClassOrInterfaceDeclaration.class).stream() + .filter(c -> !c.isInterface() && !c.isAbstract()) + .forEach(c -> { + System.out.println(c.getFullyQualifiedName().get()); + NodeList eList = c.getExtendedTypes(); + for (ClassOrInterfaceType e : eList) { + System.out.println("\t" + e.asString()); + } + }); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + + @DisplayName("获得枚举属性") + @ParameterizedTest(name = "{0}") + @CsvSource({ "src/test/java/io/github/dunwu/javatech/java/samples/CodeEnum.java", + "src/test/java/io/github/dunwu/javatech/java/samples/WebSocketMsgType.java", + "src/test/java/io/github/dunwu/javatech/java/samples/CaptchaTypeEnum.java", + "src/test/java/io/github/dunwu/javatech/java/samples/CodeBiEnum.java" }) + void testGetEnums(String path) { + File file = new File(path); + try { + CompilationUnit compilationUnit = StaticJavaParser.parse(file); + Assertions.assertThat(CollectionUtil.isEmpty(compilationUnit.getImports())); + compilationUnit.findAll(EnumDeclaration.class) + .forEach(c -> { + c.getFullyQualifiedName().ifPresent(i -> { + System.out.printf("枚举类型:%s, ", i); + }); + c.getJavadocComment().ifPresent(i -> { + System.out.printf("枚举类型注释:%s", getFilteredCommentString(i)); + }); + System.out.println(); + + c.getEntries().forEach(e -> { + System.out.printf("枚举 Entry:%s, ", e.getName()); + e.getJavadocComment().ifPresent(i -> { + System.out.printf("枚举 Entry 注释:%s", getFilteredCommentString(i)); + }); + System.out.println(); + + NodeList arguments = e.getArguments(); + if (CollectionUtil.isNotEmpty(arguments)) { + for (int i = 0; i < arguments.size(); i++) { + System.out.printf("\t参数 %d:%s", i + 1, + arguments.get(i).asLiteralStringValueExpr().getValue()); + } + System.out.println(); + } + }); + }); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + } + + public static String getFilteredCommentString(JavadocComment comment) { + + if (comment == null) { + return null; + } + + List lines = StrUtil.split(comment.getContent(), '\n'); + if (CollectionUtil.isEmpty(lines)) { + return null; + } + + String[] finalCommentLines = lines.stream() + .map(line -> { + if (StrUtil.isBlank(line)) { + return line; + } + // 去除所有 html 标签 + line = line.replaceAll("<[^>]*>", ""); + line = line.trim(); + if (line.startsWith("*")) { + line = line.substring(1).trim(); + } + return line; + }) + .filter(StrUtil::isNotBlank) + .filter(line -> !line.startsWith("@")) + .toArray(String[]::new); + return StrUtil.concat(false, finalCommentLines); + } + + @Test + public void testGenerateSimpleClass() { + ClassOrInterfaceDeclaration myClass = new ClassOrInterfaceDeclaration(); + myClass.setComment(new LineComment("A very cool class!")); + myClass.setName("MyClass"); + myClass.addField("String", "foo"); + + PrettyPrinterConfiguration conf = new PrettyPrinterConfiguration(); + conf.setIndentSize(1); + conf.setIndentType(Indentation.IndentType.SPACES); + conf.setPrintComments(false); + PrettyPrinter prettyPrinter = new PrettyPrinter(conf); + System.out.println(prettyPrinter.print(myClass)); + } + + @Test + public void getTypeOfReference() throws FileNotFoundException { + final String FILE_PATH = "src/test/java/io/github/dunwu/javatech/java/samples/Bar.java"; + + TypeSolver typeSolver = new CombinedTypeSolver(); + + JavaSymbolSolver symbolSolver = new JavaSymbolSolver(typeSolver); + StaticJavaParser + .getConfiguration() + .setSymbolResolver(symbolSolver); + + CompilationUnit cu = StaticJavaParser.parse(new File(FILE_PATH)); + + cu.findAll(AssignExpr.class).forEach(ae -> { + ResolvedType resolvedType = ae.calculateResolvedType(); + System.out.println(ae.toString() + " is a: " + resolvedType); + }); + } + + @Test + public void resolveMethodCalls() throws FileNotFoundException { + final String FILE_PATH = "src/test/java/io/github/dunwu/javatech/java/samples/A.java"; + TypeSolver typeSolver = new ReflectionTypeSolver(); + + JavaSymbolSolver symbolSolver = new JavaSymbolSolver(typeSolver); + StaticJavaParser + .getConfiguration() + .setSymbolResolver(symbolSolver); + + CompilationUnit cu = StaticJavaParser.parse(new File(FILE_PATH)); + + cu.findAll(MethodCallExpr.class).forEach(mce -> + System.out.println(mce.resolve().getQualifiedSignature())); + } + + @Test + public void usingTypeSolver() { + TypeSolver typeSolver = new ReflectionTypeSolver(); + + showReferenceTypeDeclaration(typeSolver.solveType("java.lang.Object")); + showReferenceTypeDeclaration(typeSolver.solveType("java.lang.String")); + showReferenceTypeDeclaration(typeSolver.solveType("java.util.List")); + } + + private static void showReferenceTypeDeclaration( + ResolvedReferenceTypeDeclaration resolvedReferenceTypeDeclaration) { + + System.out.println(String.format("== %s ==", resolvedReferenceTypeDeclaration.getQualifiedName())); + System.out.println(" fields:"); + resolvedReferenceTypeDeclaration.getAllFields() + .forEach(f -> System.out.println(String.format(" %s %s", f.getType(), f.getName()))); + System.out.println(" methods:"); + resolvedReferenceTypeDeclaration.getAllMethods() + .forEach(m -> System.out.println(String.format(" %s", m.getQualifiedSignature()))); + System.out.println(); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserVoidVisitorTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserVoidVisitorTest.java new file mode 100644 index 00000000..6824e3d9 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/JavaParserVoidVisitorTest.java @@ -0,0 +1,68 @@ +package io.github.dunwu.javatech.java; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.visitor.VoidVisitor; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.util.ArrayList; +import java.util.List; + +/** + * JavaParser 美化打印测试 + * + * @author Zhang Peng + * @date 2022-02-07 + */ +public class JavaParserVoidVisitorTest { + + private static final String FILE_PATH = + "src/test/java/io/github/dunwu/javatech/java/samples/ReversePolishNotation.java"; + + @Test + @DisplayName("JavaParser VoidVisitor 测试 1") + public void voidVisitorStarter() throws FileNotFoundException { + CompilationUnit cu = StaticJavaParser.parse(new FileInputStream(FILE_PATH)); + System.out.println(cu.toString()); + } + + @Test + @DisplayName("JavaParser VoidVisitor 测试 2") + public void prettyPrintComplete() throws FileNotFoundException { + + CompilationUnit cu = StaticJavaParser.parse(new FileInputStream(FILE_PATH)); + + VoidVisitor methodNameVisitor = new MethodNamePrinter(); + methodNameVisitor.visit(cu, null); + List methodNames = new ArrayList<>(); + VoidVisitor> methodNameCollector = new MethodNameCollector(); + methodNameCollector.visit(cu, methodNames); + methodNames.forEach(n -> System.out.println("Method Name Collected: " + n)); + } + + private static class MethodNamePrinter extends VoidVisitorAdapter { + + @Override + public void visit(MethodDeclaration md, Void arg) { + super.visit(md, arg); + System.out.println("Method Name Printed: " + md.getName()); + } + + } + + private static class MethodNameCollector extends VoidVisitorAdapter> { + + @Override + public void visit(MethodDeclaration md, List collector) { + super.visit(md, collector); + collector.add(md.getNameAsString()); + } + + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/A.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/A.java new file mode 100644 index 00000000..ffa79bf7 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/A.java @@ -0,0 +1,11 @@ +package io.github.dunwu.javatech.java.samples; + +class A { + + public void foo(Object param) { + System.out.println(1); + System.out.println("hi"); + System.out.println(param); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/Bar.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/Bar.java new file mode 100644 index 00000000..0debe513 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/Bar.java @@ -0,0 +1,14 @@ +package io.github.dunwu.javatech.java.samples; + +class Bar { + + private String a; + + void aMethod() { + while (true) { + int a = 0; + a = a + 1; + } + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CaptchaTypeEnum.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CaptchaTypeEnum.java new file mode 100644 index 00000000..8771804c --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CaptchaTypeEnum.java @@ -0,0 +1,40 @@ +package io.github.dunwu.javatech.java.samples; + +/** + * 验证码类型枚举 + * + * @author peng.zhang + * @date 2021-09-24 + */ +public enum CaptchaTypeEnum { + /** + * 算数 + */ + ARITHMETIC(1), + /** + * 中文 + */ + CHINESE(2), + /** + * 中文闪图 + */ + CHINESE_GIF(3), + /** + * 闪图 + */ + GIF(4), + /** + * 数字大写字母 + */ + SPEC(5); + + private final int code; + + CaptchaTypeEnum(int code) { + this.code = code; + } + + public int getCode() { + return code; + } +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CodeBiEnum.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CodeBiEnum.java new file mode 100644 index 00000000..f1defc63 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CodeBiEnum.java @@ -0,0 +1,30 @@ +package io.github.dunwu.javatech.java.samples; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 验证码业务场景 + */ +@Getter +@AllArgsConstructor +public enum CodeBiEnum { + + /* 旧邮箱修改邮箱 */ + ONE(1, "旧邮箱修改邮箱"), + + /* 通过邮箱修改密码 */ + TWO(2, "通过邮箱修改密码"); + + private final Integer code; + private final String description; + + public static CodeBiEnum find(Integer code) { + for (CodeBiEnum value : CodeBiEnum.values()) { + if (code.equals(value.getCode())) { + return value; + } + } + return null; + } +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CodeEnum.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CodeEnum.java new file mode 100644 index 00000000..fa5ef2e9 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CodeEnum.java @@ -0,0 +1,40 @@ +package io.github.dunwu.javatech.java.samples; + +/** + *

+ * 验证码业务场景对应的 Redis 中的 key + *

+ * + * @author Zheng Jie + * @date 2020-05-02 + */ +public enum CodeEnum { + + /* 通过手机号码重置邮箱 */ + PHONE_RESET_EMAIL_CODE("phone_reset_email_code_", "通过手机号码重置邮箱"), + + /* 通过旧邮箱重置邮箱 */ + EMAIL_RESET_EMAIL_CODE("email_reset_email_code_", "通过旧邮箱重置邮箱"), + + /* 通过手机号码重置密码 */ + PHONE_RESET_PWD_CODE("phone_reset_pwd_code_", "通过手机号码重置密码"), + + /* 通过邮箱重置密码 */ + EMAIL_RESET_PWD_CODE("email_reset_pwd_code_", "通过邮箱重置密码"); + + private final String key; + private final String description; + + CodeEnum(String key, String description) { + this.key = key; + this.description = description; + } + + public String getKey() { + return key; + } + + public String getDescription() { + return description; + } +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CommentGenerator.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CommentGenerator.java new file mode 100644 index 00000000..139c7b03 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CommentGenerator.java @@ -0,0 +1,57 @@ +package io.github.dunwu.javatech.java.samples; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.visitor.VoidVisitorAdapter; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class CommentGenerator { + + private static final String FILE_PATH = + "src/test/java/io/github/dunwu/javatech/java/samples/ReversePolishNotation.java"; + + private static final Pattern FIND_UPPERCASE = Pattern.compile("(.)(\\p{Upper})"); + + public static void main(String[] args) throws Exception { + + CompilationUnit cu = StaticJavaParser.parse(new File(FILE_PATH)); + + List methodDeclarations = new ArrayList<>(); + VoidVisitorAdapter> unDocumentedMethodCollector = new UnDocumentedMethodCollector(); + unDocumentedMethodCollector.visit(cu, methodDeclarations); + + cu.findAll(MethodDeclaration.class).stream() + .filter(md -> !md.getJavadoc().isPresent()) + .forEach(md -> md.setJavadocComment(generateJavaDoc(md))); + + System.out.println(cu.toString()); + } + + private static class UnDocumentedMethodCollector extends VoidVisitorAdapter> { + + @Override + public void visit(MethodDeclaration md, List collector) { + super.visit(md, collector); + // value == null + if (!md.getJavadoc().isPresent()) { + collector.add(md); + } + } + + } + + private static String generateJavaDoc(MethodDeclaration md) { + return " " + camelCaseToTitleFormat(md.getNameAsString()) + " "; + } + + private static String camelCaseToTitleFormat(String text) { + String split = FIND_UPPERCASE.matcher(text).replaceAll("$1 $2"); + return split.substring(0, 1).toUpperCase() + split.substring(1); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CommentRemover.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CommentRemover.java new file mode 100644 index 00000000..cd6989dd --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/CommentRemover.java @@ -0,0 +1,30 @@ +package io.github.dunwu.javatech.java.samples; + +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.comments.Comment; + +import java.io.File; +import java.util.List; +import java.util.stream.Collectors; + +public class CommentRemover { + + private static final String FILE_PATH = + "src/test/java/io/github/dunwu/javatech/java/samples/ReversePolishNotation.java"; + + public static void main(String[] args) throws Exception { + CompilationUnit cu = StaticJavaParser.parse(new File(FILE_PATH)); + + List comments = cu.getAllContainedComments(); + List unwantedComments = comments + .stream() + .filter(p -> !p.getCommentedNode().isPresent() || p.isLineComment()) + .collect(Collectors.toList()); + unwantedComments.forEach(Node::remove); + + System.out.println(cu.toString()); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/Foo.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/Foo.java new file mode 100644 index 00000000..3670c4f5 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/Foo.java @@ -0,0 +1,7 @@ +package io.github.dunwu.javatech.java.samples; + +class Foo { + + Bar bar; + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/LexicalPreservation.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/LexicalPreservation.java new file mode 100644 index 00000000..252a115a --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/LexicalPreservation.java @@ -0,0 +1,9 @@ +package io.github.dunwu.javatech.java.samples; + +// Hey, this is a comment + +// Another one +public class LexicalPreservation { + + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/ReversePolishNotation.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/ReversePolishNotation.java new file mode 100644 index 00000000..d7701672 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/ReversePolishNotation.java @@ -0,0 +1,81 @@ +package io.github.dunwu.javatech.java.samples; + +import java.util.Stack; +import java.util.stream.Stream; + + +/** + * A Simple Reverse Polish Notation calculator with memory function. + */ +public class ReversePolishNotation { + + // What does this do? + public static int ONE_BILLION = 1000000000; + + private double memory = 0; + + /** + * Takes reverse polish notation style string and returns the resulting calculation. + * + * @param input mathematical expression in the reverse Polish notation format + * @return the calculation result + */ + public Double calc(String input) { + + String[] tokens = input.split(" "); + Stack numbers = new Stack<>(); + + Stream.of(tokens).forEach(t -> { + double a; + double b; + switch(t){ + case "+": + b = numbers.pop(); + a = numbers.pop(); + numbers.push(a + b); + break; + case "/": + b = numbers.pop(); + a = numbers.pop(); + numbers.push(a / b); + break; + case "-": + b = numbers.pop(); + a = numbers.pop(); + numbers.push(a - b); + break; + case "*": + b = numbers.pop(); + a = numbers.pop(); + numbers.push(a * b); + break; + default: + numbers.push(Double.valueOf(t)); + } + }); + return numbers.pop(); + } + + /** + * Memory Recall uses the number in stored memory, defaulting to 0. + * + * @return the double + */ + public double memoryRecall(){ + return memory; + } + + /** + * Memory Clear sets the memory to 0. + */ + public void memoryClear(){ + memory = 0; + } + + + public void memoryStore(double value){ + memory = value; + } + +} +/* EOF */ diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/WebSocketMsgType.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/WebSocketMsgType.java new file mode 100644 index 00000000..afa3c059 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/java/samples/WebSocketMsgType.java @@ -0,0 +1,15 @@ +package io.github.dunwu.javatech.java.samples; + +/** + * WebSocket 消息类型 + */ +public enum WebSocketMsgType { + /** 连接 */ + CONNECT, + /** 关闭 */ + CLOSE, + /** 信息 */ + INFO, + /** 错误 */ + ERROR +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/pinyin/PinyinParserTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/pinyin/PinyinParserTest.java new file mode 100644 index 00000000..43b0a8d8 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/pinyin/PinyinParserTest.java @@ -0,0 +1,52 @@ +package io.github.dunwu.javatech.pinyin; + +import com.github.promeg.pinyinhelper.Pinyin; +import com.github.promeg.pinyinhelper.PinyinMapDict; +import com.github.promeg.tinypinyin.lexicons.java.cncity.CnCityDict; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +/** + * 汉字转拼音解析测试 + * + * @author Zhang Peng + * @date 2022-02-07 + */ +public class PinyinParserTest { + + @Test + @DisplayName("测试一段文本") + public void testText() { + String text = "测试一段文本"; + String content = Pinyin.toPinyin(text, " "); + System.out.println(content); + Assertions.assertEquals("CE SHI YI DUAN WEN BEN", content); + } + + @Test + @DisplayName("测试中文城市词典拼音") + public void testCityDict() { + // 添加中文城市词典 + Pinyin.init(Pinyin.newConfig().with(CnCityDict.getInstance())); + + // 添加自定义词典 + Pinyin.init(Pinyin.newConfig() + .with(new PinyinMapDict() { + @Override + public Map mapping() { + HashMap map = new HashMap<>(); + map.put("重庆", new String[] { "CONG", "QIN" }); + return map; + } + })); + + String content = Pinyin.toPinyin("欢迎来重庆", " "); + System.out.println(content); + Assertions.assertEquals("HUAN YING LAI CONG QIN", content); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/poi/excel/ExcelTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/poi/excel/ExcelTest.java new file mode 100644 index 00000000..c5e03cfc --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/poi/excel/ExcelTest.java @@ -0,0 +1,127 @@ +package io.github.dunwu.javatech.poi.excel; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.annotation.ExcelProperty; +import lombok.Data; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author Zhang Peng + * @since 2020-07-01 + */ +@Slf4j +public class ExcelTest { + + public static final String OUT_FILE = "d:\\temp.xlsx"; + + @Test + public void simpleWrite() { + // 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭 + // 如果这里想使用03 则 传入excelType参数即可 + BeanDemo demo = new BeanDemo(); + demo.setA("分析维度1"); + demo.setB("分析维度2"); + demo.setC("分析维度3"); + demo.setD("分析维度4"); + EasyExcel.write(OUT_FILE, BeanDemo.class).sheet("模板").doWrite(Collections.singletonList(demo)); + } + + /** + * 动态头,实时生成头写入 + *

+ * 思路是这样子的,先创建List头格式的sheet仅仅写入头,然后通过table 不写入头的方式 去写入数据 + * + *

+ * 1. 创建excel对应的实体对象 + *

+ * 2. 然后写入table即可 + */ + @Test + public void dynamicHeadWrite() { + BeanDemo demo = new BeanDemo(); + demo.setA("分析维度1"); + demo.setB("分析维度2"); + demo.setC("分析维度3"); + demo.setD("分析维度4"); + + EasyExcel.write(OUT_FILE) + // 这里放入动态头 + .head(head()).sheet("模板") + // 当然这里数据也可以用 List> 去传入 + .doWrite(Collections.singletonList(demo)); + } + + private List> head() { + List> list = new ArrayList>(); + List head0 = new ArrayList(); + head0.add("是否分析维度"); + List head1 = new ArrayList(); + head1.add("默认值"); + List head2 = new ArrayList(); + head2.add("校验规则"); + List head3 = new ArrayList(); + head3.add("衍生函数名称"); + list.add(head0); + list.add(head1); + list.add(head2); + list.add(head3); + return list; + } + + @Data + @Accessors(chain = true) + public static class BeanDemo { + + @ExcelProperty("是否分析维度") + private String a; + @ExcelProperty("默认值") + private String b; + @ExcelProperty("校验规则") + private String c; + @ExcelProperty("衍生函数名称") + private String d; + @ExcelProperty("衍生函数实例参数") + private String e; + @ExcelProperty("参数配置示例") + private String f; + + } + + @Test + public void syncRead() throws IOException { + File file = new File(OUT_FILE); + InputStream inputStream = new FileInputStream(file); + List eventAttrDefExcelDTOS = ExcelUtil.readSync(inputStream, BeanDemo.class); + inputStream.close(); + System.out.println(eventAttrDefExcelDTOS); + } + + @Test + public void syncRead2() throws IOException { + File file = new File(OUT_FILE); + InputStream inputStream = new FileInputStream(file); + List list = ExcelUtil.readSync(inputStream, BeanDemo.class); + inputStream.close(); + System.out.println(list); + } + + @Test + public void asyncReadByCustom() throws IOException { + File file = new File(OUT_FILE); + InputStream inputStream = new FileInputStream(file); + List list = ExcelUtil.readSync(inputStream, BeanDemo.class); + inputStream.close(); + System.out.println(list); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/poi/word/WordUtilTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/poi/word/WordUtilTest.java new file mode 100644 index 00000000..9e49ca07 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/poi/word/WordUtilTest.java @@ -0,0 +1,111 @@ +package io.github.dunwu.javatech.poi.word; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +/** + * @author Zhang Peng + * @since 2018-11-08 + */ +public class WordUtilTest { + + @Test + public void testCreateDocx() { + try { + WordUtil.create("d://temp.docx"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + public void testCreateDocxWithContent() { + StringBuilder sb = new StringBuilder(); + sb.append("At tutorialspoint.com, we strive hard to "); + sb.append("provide quality tutorials for self-learning "); + sb.append("purpose in the domains of Academics, Information "); + sb.append("Technology, Management and Computer Programming Languages."); + + try { + WordUtil.create("d://temp2.docx", sb.toString()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + public void testCreateDocxWithBorder() { + StringBuilder sb = new StringBuilder(); + sb.append("At tutorialspoint.com, we strive hard to "); + sb.append("provide quality tutorials for self-learning "); + sb.append("purpose in the domains of Academics, Information "); + sb.append("Technology, Management and Computer Programming Languages."); + + try { + WordUtil.createWithBorders("d://temp3.docx", sb.toString()); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + public void testCreateDocxWithTable() { + try { + WordUtil.createWithTable("d://temp4.docx"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + public void testCreateDocxWithFontStyle() { + try { + WordUtil.createWithFontStyle("d://temp5.docx"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + public void testCreateDocxWithAlign() { + try { + WordUtil.createWithAlign("d://temp6.docx"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + public void testExtractor() { + try { + WordUtil.extractor("d://temp6.docx"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Test + public void test() { + try { + WordUtil.setDocxProperties("d://temp6.docx"); + } catch (IOException e) { + e.printStackTrace(); + } + } + + // @Test + // public void test2() { + // File dir = new File("D:\\Docs\\ZP\\notes\\软件工程\\软件工程文档标准模板"); + // File[] files = dir.listFiles(); + // for (File file : files) { + // if (!file.isDirectory()) { + // try { + // WordUtil.setDocProperties(file.getAbsolutePath()); + // } catch (IOException e) { + // e.printStackTrace(); + // } + // } + // } + // } +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ClasspathHelperTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ClasspathHelperTest.java new file mode 100644 index 00000000..3cf0c169 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ClasspathHelperTest.java @@ -0,0 +1,41 @@ +package io.github.dunwu.javatech.reflections; + +import org.junit.jupiter.api.Test; +import org.reflections.util.ClasspathHelper; + +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +/** + * Test ClasspathHelper utility class + */ +public final class ClasspathHelperTest { + + @Test + public void testForClassLoaderShouldntReorderUrls() throws MalformedURLException { + // testing same URL set with different order to not fall into the case when HashSet orders elements in the same order as we do + final URL[] urls1 = { new URL("file", "foo", 1111, "foo"), new URL("file", "bar", 1111, "bar"), + new URL("file", "baz", 1111, "baz") }; + final List urlsList2 = Arrays.asList(urls1); + Collections.reverse(urlsList2); + final URL[] urls2 = urlsList2.toArray(new URL[0]); + + final URLClassLoader urlClassLoader1 = new URLClassLoader(urls1, null); + final URLClassLoader urlClassLoader2 = new URLClassLoader(urls2, null); + final Collection resultUrls1 = ClasspathHelper.forClassLoader(urlClassLoader1); + final Collection resultUrls2 = ClasspathHelper.forClassLoader(urlClassLoader2); + + assertArrayEquals(urls1, resultUrls1.toArray(), + "URLs returned from forClassLoader should be in the same order as source URLs"); + assertArrayEquals(urls2, resultUrls2.toArray(), + "URLs returned from forClassLoader should be in the same order as source URLs"); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/CombinedTestModel.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/CombinedTestModel.java new file mode 100644 index 00000000..10b0fc93 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/CombinedTestModel.java @@ -0,0 +1,77 @@ +package io.github.dunwu.javatech.reflections; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +interface CombinedTestModel { + + @Retention(RUNTIME) + @interface Alias { + + String value(); + + } + + @Retention(RUNTIME) + @interface Requests { + + Request[] value(); + + } + + @Retention(RUNTIME) + @Repeatable(Requests.class) + @interface Request { + + @Alias("path") String value() default ""; + + String method() default ""; + + } + + @Retention(RUNTIME) + @Request(method = "Get") + @interface Get { + + String value(); + + } + + @Retention(RUNTIME) + @Request(method = "Post") + @interface Post { + + String value(); + + } + + @Request("/base") + interface Controller { + + @Get("/get") + void get(); + + @Post("/post") + void post(Object object); + + } + + abstract class Abstract implements Controller { + + @Override + public void get() {} + + } + + class Impl extends Abstract { + + @Requests({ @Request(method = "PUT", value = "/another"), + @Request(method = "PATCH", value = "/another") }) + @Override + public void post(Object object) {} + + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ConfigurationBuilderTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ConfigurationBuilderTest.java new file mode 100644 index 00000000..ebf09531 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ConfigurationBuilderTest.java @@ -0,0 +1,51 @@ +package io.github.dunwu.javatech.reflections; + +import org.junit.jupiter.api.Test; +import org.reflections.ReflectionsException; +import org.reflections.scanners.Scanners; +import org.reflections.util.ClasspathHelper; +import org.reflections.util.ConfigurationBuilder; +import org.reflections.util.FilterBuilder; + +import java.net.URL; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.function.Predicate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ConfigurationBuilderTest { + + @Test + public void buildForConfig() { + assertConfig(ConfigurationBuilder.build("io.github.dunwu.javatech.reflections"), + ClasspathHelper.forPackage("io.github.dunwu.javatech.reflections"), + new FilterBuilder().includePackage("io.github.dunwu.javatech.reflections")); + + assertConfig(ConfigurationBuilder.build("io"), + ClasspathHelper.forPackage("io"), + new FilterBuilder().includePackage("io")); + } + + @Test + public void buildFor() { + assertThrows(ReflectionsException.class, () -> ConfigurationBuilder.build("")); + + assertConfig(ConfigurationBuilder.build(), + ClasspathHelper.forClassLoader(), + new FilterBuilder()); + + assertConfig(ConfigurationBuilder.build("not.exist"), + ClasspathHelper.forClassLoader(), + new FilterBuilder().includePackage("not.exist")); + } + + private void assertConfig(ConfigurationBuilder config, Collection urls, Predicate inputsFilter) { + assertEquals(config.getUrls(), new HashSet<>(urls)); + assertEquals(config.getInputsFilter(), inputsFilter); + assertEquals(config.getScanners(), new HashSet<>(Arrays.asList(Scanners.SubTypes, Scanners.TypesAnnotated))); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/FilterBuilderTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/FilterBuilderTest.java new file mode 100644 index 00000000..a8276f51 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/FilterBuilderTest.java @@ -0,0 +1,54 @@ +package io.github.dunwu.javatech.reflections; + +import org.junit.jupiter.api.Test; +import org.reflections.util.FilterBuilder; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class FilterBuilderTest { + + @Test + public void includeExcludePackage() { + FilterBuilder filter = new FilterBuilder() + .includePackage("io.github.dunwu.javatech.reflections") + .excludePackage("io.github.dunwu.javatech.reflections.exclude") + .includePackage("io.foo"); + + doAssert(filter); + } + + @Test + public void parsePackages() { + FilterBuilder filter = FilterBuilder + .parsePackages("+io.github.dunwu.javatech.reflections , -io.github.dunwu.javatech.reflections.exclude,+io.foo"); // not trimmed + + doAssert(filter); + } + + @Test + public void includeExcludePattern() { + FilterBuilder filter = new FilterBuilder() + .includePattern("io\\.github\\.dunwu\\.javatech\\.reflections\\..*") + .excludePattern("io\\.github\\.dunwu\\.javatech\\.reflections\\.exclude\\..*") + .includePattern("io\\.foo\\..*"); + + doAssert(filter); + } + + private void doAssert(FilterBuilder filter) { + assertFalse(filter.test("")); + assertFalse(filter.test("io")); + assertFalse(filter.test("io.")); + assertFalse(filter.test("io.github.dunwu.javatech.reflections")); + assertTrue(filter.test("io.github.dunwu.javatech.reflections.")); + assertTrue(filter.test("io.github.dunwu.javatech.reflections.Reflections")); + assertTrue(filter.test("io.github.dunwu.javatech.reflections.foo.Reflections")); + assertFalse(filter.test("io.github.dunwu.javatech.reflections.exclude.it")); + assertFalse(filter.test("io.foo")); + assertTrue(filter.test("io.foo.")); + assertTrue(filter.test("io.foo.bar")); + assertFalse(filter.test("io.bar.Reflections")); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/JavaCodeSerializerTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/JavaCodeSerializerTest.java new file mode 100644 index 00000000..7da8c666 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/JavaCodeSerializerTest.java @@ -0,0 +1,31 @@ +package io.github.dunwu.javatech.reflections; + +import org.junit.jupiter.api.Test; +import org.reflections.Reflections; +import org.reflections.scanners.TypeElementsScanner; +import org.reflections.serializers.JavaCodeSerializer; +import org.reflections.util.FilterBuilder; +import org.reflections.util.NameHelper; + +public class JavaCodeSerializerTest implements NameHelper { + + public JavaCodeSerializerTest() { + FilterBuilder filterBuilder = new FilterBuilder().includePattern("io\\.github\\.dunwu\\.javatech\\.reflections\\.TestModel\\$.*"); + Reflections reflections = new Reflections( + TestModel.class, + new TypeElementsScanner().filterResultsBy(filterBuilder), + filterBuilder); + + String filename = ReflectionsTest.getUserDir() + "/src/test/java/io.github.dunwu.javatech.reflections.MyTestModelStore"; + reflections.save(filename, new JavaCodeSerializer()); + } + + @Test + public void check() { + // MyTestModelStore contains TestModel type elements + Class c1 = MyTestModelStore.io.github.dunwu.javatech.reflections.TestModel$C1.class; + Class ac1 = MyTestModelStore.io.github.dunwu.javatech.reflections.TestModel$C1.annotations.io_github_dunwu_javatech_reflections_TestModel$AC1.class; + Class f1 = MyTestModelStore.io.github.dunwu.javatech.reflections.TestModel$C4.fields.f1.class; + Class m1 = MyTestModelStore.io.github.dunwu.javatech.reflections.TestModel$C4.methods.m1.class; + } +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/JdkTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/JdkTests.java new file mode 100644 index 00000000..4f4cb69e --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/JdkTests.java @@ -0,0 +1,273 @@ +package io.github.dunwu.javatech.reflections; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.reflections.ReflectionUtils; +import org.reflections.Reflections; +import org.reflections.ReflectionsException; +import org.reflections.scanners.Scanner; +import org.reflections.scanners.Scanners; +import org.reflections.util.ClasspathHelper; +import org.reflections.util.ConfigurationBuilder; +import org.reflections.util.UtilQueryBuilder; +import org.reflections.vfs.Vfs; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URL; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.reflections.ReflectionUtils.get; + +/** + * test reflection symmetry between jrt scanned metadata (Scanners) and java reflection accessibility (ReflectionUtils + * functions). + *

except for known differences per jdk version, these pairs should access similar metadata: + * SubTypes/SuperTypes, TypesAnnotated/AnnotatedTypes, MethodsAnnotated/AnnotatedTypes, Resources etc... + *

tested with AdoptOpenJDK + */ +@SuppressWarnings({ "ArraysAsListWithZeroOrOneArgument" }) +public class JdkTests { + + private static Reflections reflections; + + @BeforeAll + static void init() { + if (!Vfs.getDefaultUrlTypes().get(0).getClass().equals(JrtUrlType.class)) { + Vfs.addDefaultURLTypes(new JrtUrlType()); + } + URL urls = ClasspathHelper.forClass(Object.class); + measure("before"); + + reflections = new Reflections( + new ConfigurationBuilder() + .addUrls(urls) + .setScanners(Scanners.values())); + + measure("scan"); + } + + @AfterAll + static void cleanup() { + if (Vfs.getDefaultUrlTypes().get(0).getClass().equals(JrtUrlType.class)) { + Vfs.getDefaultUrlTypes().remove(0); + } + reflections.getStore().clear(); + measure("cleanup"); + } + + @Test + public void checkSubTypes() { + Map> diff = reflect( + Scanners.SubTypes, + ReflectionUtils.SuperTypes, + Class.class); + + assertEquals(diff, Collections.emptyMap()); + } + + @Test + public void checkTypesAnnotated() { + Map> diff = reflect( + Scanners.TypesAnnotated, + ReflectionUtils.AnnotationTypes, + Class.class); + + Arrays.asList("jdk.internal.PreviewFeature", // jdk 15 + "jdk.internal.javac.PreviewFeature") // jdk 17 + .forEach(diff::remove); + assertEquals(diff, Collections.emptyMap()); + } + + @Test + public void checkMethodsAnnotated() { + Map> diff = reflect( + Scanners.MethodsAnnotated, + ReflectionUtils.AnnotationTypes, + Method.class); + + // todo fix differences @A2 such as - @A1 public @A2 result method(...) + Arrays.asList("com.sun.istack.internal.NotNull", // jdk 8 + "com.sun.istack.internal.Nullable", + "sun.reflect.CallerSensitive", + "java.lang.invoke.LambdaForm$Hidden", + "jdk.internal.reflect.CallerSensitive", // jdk 11, 13, 15 + "jdk.internal.PreviewFeature") // jdk 15 + .forEach(diff::remove); + assertEquals(diff, Collections.emptyMap()); + } + + @Test + public void checkConstructorsAnnotated() { + Map> diff = reflect( + Scanners.ConstructorsAnnotated, + ReflectionUtils.AnnotationTypes, + Constructor.class); + + assertEquals(diff, Collections.emptyMap()); + } + + @Test + public void checkFieldsAnnotated() { + Map> diff = reflect( + Scanners.FieldsAnnotated, + ReflectionUtils.AnnotationTypes, + Field.class); + + Arrays.asList("com.sun.istack.internal.NotNull", // jdk 8 + "com.sun.istack.internal.Nullable", + "jdk.internal.PreviewFeature", // jdk 15 + "jdk.internal.vm.annotation.Stable") // jdk 17 + .forEach(diff::remove); + assertEquals(diff, Collections.emptyMap()); + } + + @Test + public void checkResources() { + Set diff = new HashSet<>(); + Map> mmap = reflections.getStore().get(Scanners.Resources.index()); + mmap.values().forEach(resources -> + resources.forEach(resource -> { + Set urls = get(ReflectionUtils.Resources.get(resource)); + // if (urls == null || urls.isEmpty()) diff.add(resource); + for (URL url : urls) { + try { if (!Files.exists(JrtUrlType.getJrtRealPath(url))) diff.add(resource); } catch (Exception e) { + diff.add(resource); + } + } + })); + System.out.println(Scanners.Resources.index() + + ": " + + mmap.values().stream().mapToInt(Set::size).sum() + + ", missing: " + + diff.size()); + + Arrays.asList("META-INF/MANIFEST.MF") // jdk 8 + .forEach(diff::remove); + assertEquals(diff, Collections.emptySet()); + } + + @Test + public void checkMethodsSignature() { + // Map> diffMethodSignature = + // findDiff(reflections, Scanners.MethodsSignature, ReflectionUtils.MethodSignature, Field.class); + // assertEquals(diffMethodSignature, Collections.emptyMap()); } + } + + private Map> reflect( + Scanner scanner, UtilQueryBuilder utilQueryBuilder, Class resultType) { + Map> mmap = reflections.getStore().get(scanner.index()); + Map> missing = new HashMap<>(); + mmap.forEach((key, strings) -> + strings.forEach(string -> { + //noinspection unchecked + F element = (F) reflections.forName(string, resultType); + if (element == null || !reflections.toNames(get(utilQueryBuilder.get(element))).contains(key)) { + missing.computeIfAbsent(key, k -> new HashSet<>()).add(string); + } + })); + System.out.println( + scanner.index() + ": " + mmap.values().stream().mapToInt(Set::size).sum() + ", missing: " + missing.values() + .stream() + .mapToInt( + Set::size) + .sum()); + return missing; + } + + private static void measure(String s) { + System.out.printf("-> %s %s ", s, mb(mem())); + gc(); + System.out.printf("(gc -> %s)%n", mb(mem())); + } + + private static void gc() { + for (int i = 0; i < 3; i++) { + Runtime.getRuntime().gc(); + System.runFinalization(); + try { + Thread.sleep(100); + } catch (InterruptedException e) { /*java sucks*/ } + } + } + + private static long mem() { + return Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(); + } + + private static String mb(long mem2) { + return (mem2 / 1024 / 1024) + "mb"; + } + + public static class JrtUrlType implements Vfs.UrlType { + + @Override + public boolean matches(URL url) throws Exception { + return url.getProtocol().equals("jrt"); + } + + @Override + public Vfs.Dir createDir(URL url) throws Exception { + final Path realPath = getJrtRealPath(url); + return new Vfs.Dir() { + @Override + public String getPath() { + return url.getPath(); + } + + @Override + public Iterable getFiles() { + return () -> { + try { + return Files.walk(realPath) + .filter(Files::isRegularFile) + .map(p -> (Vfs.File) new Vfs.File() { + @Override + public String getName() { + return p.toString(); + } + + @Override + public String getRelativePath() { + return p.startsWith(realPath) ? p.toString() + .substring( + realPath.toString().length()) + : p.toString(); + } + + @Override + public InputStream openInputStream() throws IOException { + return Files.newInputStream(p); + } + }) + .iterator(); + } catch (Exception e) { + throw new ReflectionsException(e); + } + }; + } + }; + } + + /** + * jdk 11 workaround for {@code Paths.get().toRealPath()} + */ + public static Path getJrtRealPath(URL url) throws IOException { + // jdk 11 workaround + return FileSystems.getFileSystem(URI.create("jrt:/")).getPath("modules", url.getPath()) + .toRealPath(); + } + + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/MoreTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/MoreTests.java new file mode 100644 index 00000000..5c4596b8 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/MoreTests.java @@ -0,0 +1,110 @@ +package io.github.dunwu.javatech.reflections; + +import org.junit.jupiter.api.Test; +import org.reflections.ReflectionUtils; +import org.reflections.Reflections; +import org.reflections.scanners.MethodParameterNamesScanner; +import org.reflections.scanners.Scanners; +import org.reflections.util.ClasspathHelper; +import org.reflections.util.ConfigurationBuilder; + +import java.lang.annotation.Annotation; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static io.github.dunwu.javatech.reflections.MoreTestsModel.*; +import static io.github.dunwu.javatech.reflections.ReflectionsTest.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.reflections.ReflectionUtils.Annotations; +import static org.reflections.scanners.Scanners.Resources; +import static org.reflections.scanners.Scanners.SubTypes; + +public class MoreTests { + + @Test + public void test_cyclic_annotation() { + Reflections reflections = new Reflections(MoreTestsModel.class); + assertThat(reflections.getTypesAnnotatedWith(CyclicAnnotation.class), + equalTo(CyclicAnnotation.class)); + } + + @Test + public void no_exception_when_configured_scanner_store_is_empty() { + Reflections reflections = new Reflections( + new ConfigurationBuilder() + .setUrls(ClasspathHelper.forClass(TestModel.class)) + .setScanners()); + + assertNull(reflections.getStore().get(SubTypes.index())); + assertTrue(reflections.getSubTypesOf(TestModel.C1.class).isEmpty()); + assertTrue(reflections.get(SubTypes.of(TestModel.C1.class)).isEmpty()); + assertTrue(reflections.get(Resources.with(".*")).isEmpty()); + } + + @Test + public void getAllAnnotated_returns_meta_annotations() { + Reflections reflections = new Reflections(MoreTestsModel.class); + for (Class type : reflections.getTypesAnnotatedWith(Meta.class)) { + Set allAnnotations = ReflectionUtils.get(Annotations.of(type)); + List> collect = + allAnnotations.stream().map(Annotation::annotationType).collect(Collectors.toList()); + assertTrue(collect.contains(Meta.class)); + } + + Meta meta = new Meta() { + @Override + public String value() { return "a"; } + + @Override + public Class annotationType() { return Meta.class; } + }; + for (Class type : reflections.getTypesAnnotatedWith(meta)) { + Set allAnnotations = ReflectionUtils.get(Annotations.of(type)); + List> collect = + allAnnotations.stream().map(Annotation::annotationType).collect(Collectors.toList()); + assertTrue(collect.contains(Meta.class)); + } + } + + @Test + public void resources_scanner_filters_classes() { + Reflections reflections = new Reflections(Scanners.Resources); + Collection resources = reflections.getResources(".*"); + assertTrue(resources.stream().noneMatch(res -> res.endsWith(".class"))); + } + + @Test + public void test_repeatable() { + Reflections ref = new Reflections(MoreTestsModel.class); + Collection> clazzes = ref.getTypesAnnotatedWith(Name.class); + assertTrue(clazzes.contains(SingleName.class)); + assertFalse(clazzes.contains(MultiName.class)); + + clazzes = ref.getTypesAnnotatedWith(Names.class); + assertFalse(clazzes.contains(SingleName.class)); + assertTrue(clazzes.contains(MultiName.class)); + } + + @Test + public void test_method_param_names_not_local_vars() throws NoSuchMethodException { + Reflections reflections = new Reflections(MoreTestsModel.class, new MethodParameterNamesScanner()); + + Class clazz = ParamNames.class; + assertEquals(reflections.getMemberParameterNames(clazz.getConstructor(String.class)).toString(), + "[param1]"); + assertEquals( + reflections.getMemberParameterNames(clazz.getMethod("test", String.class, String.class)).toString(), + "[testParam1, testParam2]"); + assertEquals(reflections.getMemberParameterNames(clazz.getMethod("test", String.class)).toString(), + "[testParam]"); + assertEquals(reflections.getMemberParameterNames(clazz.getMethod("test2", String.class)).toString(), + "[testParam]"); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/MoreTestsModel.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/MoreTestsModel.java new file mode 100644 index 00000000..1897218c --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/MoreTestsModel.java @@ -0,0 +1,107 @@ +package io.github.dunwu.javatech.reflections; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +public class MoreTestsModel { + + @CyclicAnnotation + @Retention(RUNTIME) + public @interface CyclicAnnotation {} + + @Target(ElementType.TYPE) + @Retention(RUNTIME) + @interface Meta { + + String value(); + + } + + @Meta("a") + @Retention(RUNTIME) + @interface A {} + + @Meta("b") + @Retention(RUNTIME) + @interface B {} + + @A + class A1 {} + + @B + class B1 {} + + @A + class A2 {} + + @Retention(RUNTIME) + public @interface TestAnnotation { + + String value(); + + } + + @TestAnnotation("foo foo foo") + public class ActualFunctionalityClass { + + @TestAnnotation("bar bar bar") + class Thing {} + + } + + // repeatable + @Repeatable(Names.class) + @Retention(RUNTIME) + @Target({ ElementType.TYPE }) + public @interface Name { + + String name(); + + } + + @Name(name = "foo") + @Name(name = "bar") + public static class MultiName {} + + @Retention(RUNTIME) + @Target({ ElementType.TYPE }) + public @interface Names { + + Name[] value() default {}; + + } + + @Name(name = "foo") + public static class SingleName {} + + // + public static class ParamNames { + + public ParamNames() { + String testLocal = "local"; + } + + public ParamNames(String param1) { + String testLocal = "local"; + } + + public void test(String testParam) { + String testLocal = "local"; + } + + public void test(String testParam1, String testParam2) { + String testLocal1 = "local"; + String testLocal2 = "local"; + } + + public static void test2(String testParam) { + String testLocal = "local"; + } + + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/MyTestModelStore.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/MyTestModelStore.java new file mode 100644 index 00000000..63a145fa --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/MyTestModelStore.java @@ -0,0 +1,106 @@ +//generated using Reflections JavaCodeSerializer [Thu Jun 09 20:34:19 CST 2022] +package io.github.dunwu.javatech.reflections; + +public interface MyTestModelStore { + + interface io { + interface github { + interface dunwu { + interface javatech { + interface reflections { + interface TestModel$AC1 { + } + interface TestModel$AC1n { + } + interface TestModel$AC2 { + interface methods { + interface value {} + } + } + interface TestModel$AC3 { + interface annotations { + interface io_github_dunwu_javatech_reflections_TestModel$AC2 {} + } + } + interface TestModel$AF1 { + interface methods { + interface value {} + } + } + interface TestModel$AI1 { + interface annotations { + interface io_github_dunwu_javatech_reflections_TestModel$MAI1 {} + } + } + interface TestModel$AI2 { + } + interface TestModel$AM1 { + interface methods { + interface value {} + } + } + interface TestModel$AM2 { + } + interface TestModel$C1 { + interface annotations { + interface io_github_dunwu_javatech_reflections_TestModel$AC1 {} + interface io_github_dunwu_javatech_reflections_TestModel$AC1n {} + } + } + interface TestModel$C2 { + interface annotations { + interface io_github_dunwu_javatech_reflections_TestModel$AC2 {} + } + } + interface TestModel$C3 { + interface annotations { + interface io_github_dunwu_javatech_reflections_TestModel$AC2 {} + } + } + interface TestModel$C4 { + interface fields { + interface f1 {} + interface f2 {} + interface f3 {} + } + interface methods { + interface add {} + interface c2toC3 {} + interface m1 {} + interface m1_int$$$$__java_lang_String$$$$ {} + interface m3 {} + interface m4 {} + } + } + interface TestModel$C5 { + } + interface TestModel$C6 { + } + interface TestModel$C7 { + interface annotations { + interface io_github_dunwu_javatech_reflections_TestModel$AC3 {} + } + } + interface TestModel$I1 { + interface annotations { + interface io_github_dunwu_javatech_reflections_TestModel$AI1 {} + } + } + interface TestModel$I2 { + interface annotations { + interface io_github_dunwu_javatech_reflections_TestModel$AI2 {} + } + } + interface TestModel$I3 { + interface annotations { + interface io_github_dunwu_javatech_reflections_TestModel$AC2 {} + } + } + interface TestModel$MAI1 { + } + } + } + } + } + } +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/NameHelperTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/NameHelperTest.java new file mode 100644 index 00000000..51c7dc5b --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/NameHelperTest.java @@ -0,0 +1,73 @@ +package io.github.dunwu.javatech.reflections; + +import org.junit.jupiter.api.Test; +import org.reflections.util.NameHelper; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@SuppressWarnings({"unchecked"}) +public class NameHelperTest implements NameHelper { + + @Test + public void testClass() { + assertToFor(String.class, this::toName, this::forClass); + assertToFor(String[].class, this::toName, this::forClass); + assertToFor(boolean.class, this::toName, this::forClass); + assertNull(forClass("no.exist")); + } + + @Test + public void testConstructor() throws NoSuchMethodException { + assertToFor(String.class.getDeclaredConstructor(), this::toName, this::forConstructor); + assertToFor(String.class.getDeclaredConstructor(String.class), this::toName, this::forConstructor); + } + + @Test + public void testMethod() throws NoSuchMethodException { + assertToFor(String.class.getDeclaredMethod("length"), this::toName, this::forMethod); + assertToFor(String.class.getDeclaredMethod("charAt", int.class), this::toName, this::forMethod); + } + + @Test + public void testField() throws NoSuchFieldException { + assertToFor(String.class.getDeclaredField("value"), this::toName, this::forField); + } + + @Test + public void testToForNames() throws NoSuchFieldException, NoSuchMethodException { + Class CLASS = String.class; + Constructor CONST = CLASS.getDeclaredConstructor(); + Method METHOD = CLASS.getDeclaredMethod("length"); + Field FIELD = CLASS.getDeclaredField("value"); + + Set elements = set(CLASS, CONST, METHOD, FIELD); + Collection names = toNames(elements); + + assertEquals(set(CLASS), forNames(names)); + assertEquals(set(CLASS), forNames(names, Class.class)); + assertEquals(set(CONST), forNames(names, Constructor.class)); + assertEquals(set(METHOD), forNames(names, Method.class)); + assertEquals(set(FIELD), forNames(names, Field.class)); + assertEquals(set(CONST, METHOD, FIELD), forNames(names, Member.class)); + } + + void assertToFor(T type, Function toName, Function forName) { + assertEquals(forName.apply(toName.apply(type)), type); + } + + private Set set(T... ts) { + return new HashSet<>(Arrays.asList(ts)); + } +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionUtilsQueryTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionUtilsQueryTest.java new file mode 100644 index 00000000..0bda92a1 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionUtilsQueryTest.java @@ -0,0 +1,245 @@ +package io.github.dunwu.javatech.reflections; + +import org.junit.jupiter.api.Test; +import org.reflections.ReflectionUtils; +import org.reflections.Reflections; +import org.reflections.Store; +import org.reflections.scanners.Scanners; +import org.reflections.util.AnnotationMergeCollector; +import org.reflections.util.QueryFunction; + +import java.lang.annotation.*; +import java.lang.reflect.Method; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static io.github.dunwu.javatech.reflections.ReflectionsQueryTest.equalTo; +import static io.github.dunwu.javatech.reflections.TestModel.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.reflections.ReflectionUtils.*; +import static org.reflections.scanners.Scanners.MethodsAnnotated; +import static org.reflections.scanners.Scanners.TypesAnnotated; + +public class ReflectionUtilsQueryTest { + + @Test + public void testTypes() throws NoSuchMethodException { + assertThat( + get(SuperTypes.of(C3.class)), + equalTo(C1.class, I2.class, I1.class)); + + assertThat( + get(SuperTypes.of(C3.class) + .filter(withAnnotation(AI1.class))), + equalTo(I1.class)); + + assertThat( + get(Interfaces.get(C1.class)), + equalTo(I2.class)); + + assertThat( + get(Interfaces.of(C3.class)), + equalTo(I2.class, I1.class)); + + assertThat( + get(SuperClass.of(C5.class)), + equalTo(C3.class, C1.class)); + + assertThat( + get(Annotations.of(C3.class) + .map(Annotation::annotationType)), + equalTo( + Retention.class, Target.class, Documented.class, Inherited.class, + AC1.class, AC1n.class, AC2.class, AI1.class, AI2.class, MAI1.class)); + + assertThat( + get(AnnotationTypes.of(C3.class) + .filter(a -> !a.getName().startsWith("java."))), + equalTo( + AC1.class, AC1n.class, AC2.class, AI1.class, AI2.class, MAI1.class)); + + assertThat( + get(Annotations.of(C4.class.getDeclaredMethod("m4", String.class)) + .map(Annotation::annotationType)), + equalTo()); + } + + @Test + public void testMembers() throws NoSuchMethodException, NoSuchFieldException { + assertThat( + get(Methods.of(C4.class, withName("m4"))), + equalTo(C4.class.getDeclaredMethod("m4", String.class))); + + assertThat( + get(Methods.of(C4.class, withParameters(String.class))), + equalTo(C4.class.getDeclaredMethod("m4", String.class))); + + assertThat( + get(Methods.of(C4.class) + .filter(withPattern("public.*.void .*")) + .map(Method::getName)), + equalTo("m1")); + + assertThat( + get(Methods.of(C4.class, withAnyParameterAnnotation(AM1.class))), + equalTo(C4.class.getDeclaredMethod("m4", String.class))); + + assertThat( + get(Methods.of(Class.class) + .filter(withReturnType(Method.class).and(withPublic())) + .map(Method::getName)), + equalTo("getMethod", "getDeclaredMethod", "getEnclosingMethod")); + + assertThat( + get(Fields.of(C4.class, withAnnotation(AF1.class))), + equalTo(C4.class.getDeclaredField("f1"), + C4.class.getDeclaredField("f2"))); + + AF1 af12 = new AF1() { + public String value() { return "2"; } + + public Class annotationType() { return AF1.class; } + }; + assertThat( + get(Fields.of(C4.class) + .filter(withAnnotation(af12))), + equalTo(C4.class.getDeclaredField("f2"))); + + assertThat( + get(Fields.of(C4.class) + .filter(withTypeAssignableTo(String.class))), + equalTo(C4.class.getDeclaredField("f1"), + C4.class.getDeclaredField("f2"), + C4.class.getDeclaredField("f3"))); + + assertThat( + get(Constructors.of(C4.class) + .filter(withParametersCount(0))), + equalTo(C4.class.getDeclaredConstructor())); + } + + @Test + public void nestedQuery() { + Set> annotations = + get(AnnotationTypes.of( + Methods.of(C4.class)) + .filter(withNamePrefix("io.github.dunwu.javatech.reflections"))); + + assertThat(annotations, + equalTo(AM1.class)); + } + + @Test + public void addQuery() { + Set> annotations = + get(AnnotationTypes.of(C1.class) + .add(AnnotationTypes.of(C2.class))); + + assertThat(annotations, + equalTo( + Retention.class, Target.class, Documented.class, Inherited.class, + AC1.class, AC2.class, AC1n.class, AI2.class, AI1.class, MAI1.class)); + } + + @Test + public void singleQuery() { + QueryFunction> single = + QueryFunction.single(CombinedTestModel.Impl.class); + assertThat(single.apply(null), + equalTo(CombinedTestModel.Impl.class)); + + QueryFunction> second = + single.add( + QueryFunction.single(CombinedTestModel.Controller.class)); + assertThat(second.apply(null), + equalTo(CombinedTestModel.Impl.class, CombinedTestModel.Controller.class)); + } + + @Test + public void getAllQuery() { + QueryFunction> single = + QueryFunction.single(CombinedTestModel.Impl.class); + + QueryFunction> allIncluding = + single.add( + single.getAll(SuperTypes::get)); + assertThat(allIncluding.apply(null), + equalTo(CombinedTestModel.Impl.class, CombinedTestModel.Abstract.class, + CombinedTestModel.Controller.class)); + } + + @Test + public void flatMapQuery() throws NoSuchMethodException { + Set query = + get(Annotations.of( + Methods.of(CombinedTestModel.Impl.class)) + .flatMap(annotation -> + Methods.of(annotation.annotationType()))); + + Set query1 = + get(AnnotationTypes.of(Methods.of(CombinedTestModel.Impl.class)).flatMap(Methods::of)); + + assertThat(query, + equalTo( + CombinedTestModel.Post.class.getDeclaredMethod("value"), + CombinedTestModel.Requests.class.getDeclaredMethod("value"), + CombinedTestModel.Get.class.getDeclaredMethod("value"))); + + assertEquals(query, query1); + } + + @Test + public void annotationToMap() { + Set> valueMaps = + get(Annotations.of( + Methods.of(CombinedTestModel.Impl.class)) + .filter(withNamePrefix("io.github.dunwu.javatech.reflections")) + .map(ReflectionUtils::toMap)); + + // todo proper assert + Set collect = + valueMaps.stream().map(Object::toString).sorted().collect(Collectors.toCollection(LinkedHashSet::new)); + assertThat(collect, + equalTo( + "{value=/get}", + "{value=/post}", + "{value=[" + + "{method=PUT, value=/another}, " + + "{method=PATCH, value=/another}]}" + )); + } + + @Test + public void mergedAnnotations() { + Class metaAnnotation = CombinedTestModel.Request.class; + + Reflections reflections = new Reflections(metaAnnotation, Scanners.values()); + + Set> metaAnnotations = + reflections.get(TypesAnnotated.getAllIncluding(metaAnnotation.getName()).asClass()); + + QueryFunction mergedAnnotations = + MethodsAnnotated.with(metaAnnotations) + .as(Method.class) + .map(method -> + get(Annotations.of(method.getDeclaringClass()) + .add(Annotations.of(method)) + .filter(a -> metaAnnotations.contains(a.annotationType()))) + .stream() + .collect(new AnnotationMergeCollector(method))) + .map(map -> ReflectionUtils.toAnnotation(map, metaAnnotation)); + + assertThat( + reflections.get(mergedAnnotations.map(CombinedTestModel.Request::value)), + equalTo("/base/post", "/base/get")); + + assertThat( + reflections.get(mergedAnnotations.map(CombinedTestModel.Request::method)), + equalTo("Post", "Get")); + } + +} + diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionUtilsTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionUtilsTest.java new file mode 100644 index 00000000..39116de1 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionUtilsTest.java @@ -0,0 +1,165 @@ +package io.github.dunwu.javatech.reflections; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.junit.jupiter.api.Test; +import org.reflections.Reflections; +import org.reflections.scanners.Scanners; + +import java.lang.annotation.*; +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.reflections.ReflectionUtils.*; +import static io.github.dunwu.javatech.reflections.ReflectionsTest.equalTo; + +@SuppressWarnings("unchecked") +public class ReflectionUtilsTest { + + @Test + public void getAllTest() { + assertThat(getAllSuperTypes(TestModel.C3.class, withAnnotation(TestModel.AI1.class)), + equalTo(TestModel.I1.class)); + + Set allMethods = + getAllMethods(TestModel.C4.class, withModifier(Modifier.PUBLIC), withReturnType(void.class)); + Set allMethods1 = getAllMethods(TestModel.C4.class, withPattern("public.*.void .*")); + + assertTrue(allMethods.containsAll(allMethods1) && allMethods1.containsAll(allMethods)); + assertThat(allMethods1, names("m1")); + + assertThat(getAllMethods(TestModel.C4.class, withAnyParameterAnnotation(TestModel.AM1.class)), names("m4")); + + assertThat(getAllFields(TestModel.C4.class, withAnnotation(TestModel.AF1.class)), names("f1", "f2")); + + assertThat(getAllFields(TestModel.C4.class, withAnnotation(new TestModel.AF1() { + public String value() {return "2";} + + public Class annotationType() {return TestModel.AF1.class;} + })), + names("f2")); + + assertThat(getAllFields(TestModel.C4.class, withTypeAssignableTo(String.class)), names("f1", "f2", "f3")); + + assertThat(getAllConstructors(TestModel.C4.class, withParametersCount(0)), names(TestModel.C4.class.getName())); + + Set allAnnotations = getAllAnnotations(TestModel.C3.class); + assertThat(allAnnotations.stream().map(Annotation::annotationType).collect(Collectors.toSet()), + equalTo(Documented.class, Inherited.class, Retention.class, Target.class, + TestModel.MAI1.class, TestModel.AI1.class, TestModel.AI2.class, + TestModel.AC1.class, TestModel.AC1n.class, TestModel.AC2.class)); + + Method m4 = getMethods(TestModel.C4.class, withName("m4")).iterator().next(); + assertEquals(m4.getName(), "m4"); + assertTrue(getAnnotations(m4).isEmpty()); + } + + @Test + public void withParameter() throws Exception { + Class target = Collections.class; + Object arg1 = Arrays.asList(1, 2, 3); + + Set allMethods = new HashSet<>(); + for (Class type : getAllSuperTypes(arg1.getClass())) { + allMethods.addAll(getAllMethods(target, withModifier(Modifier.STATIC), withParameters(type))); + } + + Set allMethods1 = + getAllMethods(target, withModifier(Modifier.STATIC), withParametersAssignableTo(arg1.getClass())); + + assertEquals(allMethods, allMethods1); + + for (Method method : allMethods) { //effectively invokable + //noinspection UnusedDeclaration + Object invoke = method.invoke(null, arg1); + } + } + + @Test + public void withParametersAssignableFromTest() throws Exception { + //Check for null safe + getAllMethods(Collections.class, withModifier(Modifier.STATIC), withParametersAssignableFrom()); + + Class target = Collections.class; + Object arg1 = Arrays.asList(1, 2, 3); + + Set allMethods = new HashSet<>(); + for (Class type : getAllSuperTypes(arg1.getClass())) { + allMethods.addAll(getAllMethods(target, withModifier(Modifier.STATIC), withParameters(type))); + } + + Set allMethods1 = + getAllMethods(target, withModifier(Modifier.STATIC), withParametersAssignableFrom(Iterable.class), + withParametersAssignableTo(arg1.getClass())); + + assertEquals(allMethods, allMethods1); + + for (Method method : allMethods) { //effectively invokable + //noinspection UnusedDeclaration + Object invoke = method.invoke(null, arg1); + } + } + + @Test + public void withReturn() { + Set returnMember = getAllMethods(Class.class, withReturnTypeAssignableFrom(Member.class)); + Set returnsAssignableToMember = getAllMethods(Class.class, withReturnType(Method.class)); + + assertTrue(returnMember.containsAll(returnsAssignableToMember)); + assertFalse(returnsAssignableToMember.containsAll(returnMember)); + + returnsAssignableToMember = getAllMethods(Class.class, withReturnType(Field.class)); + assertTrue(returnMember.containsAll(returnsAssignableToMember)); + assertFalse(returnsAssignableToMember.containsAll(returnMember)); + } + + @Test + public void getAllAndReflections() { + Reflections reflections = new Reflections(TestModel.class, Scanners.FieldsAnnotated); + + Set allFields = reflections.getFieldsAnnotatedWith(TestModel.AF1.class) + .stream() + .filter(withModifier(Modifier.PROTECTED)) + .collect(Collectors.toSet()); + assertEquals(1, allFields.size()); + assertThat(allFields, names("f2")); + } + + private Set names(Set o) { + return o.stream().map(Member::getName).collect(Collectors.toSet()); + } + + private BaseMatcher> names(final String... namesArray) { + return new BaseMatcher>() { + + public boolean matches(Object o) { + Collection transform = names((Set) o); + final Collection names = Arrays.asList(namesArray); + return transform.containsAll(names) && names.containsAll(transform); + } + + public void describeTo(Description description) { + } + }; + } + + public static String toStringSorted(Collection set) { + return set.stream() + .map(o -> o.toString() + .replace("[", "") + .replace("]", "") + .replace("{", "") + .replace("}", "") + .replace("\"", "")) + .sorted().collect(Collectors.toList()).toString(); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionsExpandSupertypesTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionsExpandSupertypesTest.java new file mode 100644 index 00000000..eb1f4930 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionsExpandSupertypesTest.java @@ -0,0 +1,103 @@ +package io.github.dunwu.javatech.reflections; + +import org.junit.jupiter.api.Test; +import org.reflections.Reflections; +import org.reflections.util.ConfigurationBuilder; +import org.reflections.util.FilterBuilder; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import static io.github.dunwu.javatech.reflections.ReflectionsQueryTest.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.reflections.scanners.Scanners.SubTypes; + +public class ReflectionsExpandSupertypesTest { + + private final FilterBuilder inputsFilter = new FilterBuilder() + .includePattern("io\\.github\\.dunwu\\.javatech\\.reflections\\.ReflectionsExpandSupertypesTest\\$ExpandTestModel\\$Scanned\\$.*"); + + @SuppressWarnings("unused") + public interface ExpandTestModel { + + interface NotScanned { + + @Retention(RetentionPolicy.RUNTIME) + @interface MetaAnnotation {} // outside of scanned scope + + @Retention(RetentionPolicy.RUNTIME) + @Inherited + @MetaAnnotation + @interface TestAnnotation {} // outside of scanned scope, but immediate annotation + + interface BaseInterface {} // outside of scanned scope + + @TestAnnotation + class BaseClass implements BaseInterface {} // outside of scanned scope, but immediate supertype + + } + + interface Scanned { + + class ChildrenClass extends NotScanned.BaseClass {} + + } + + } + + @Test + public void testExpandSupertypes() { + ConfigurationBuilder configuration = new ConfigurationBuilder() + .forPackage("io.github.dunwu.javatech.reflections") + .filterInputsBy(inputsFilter); + + Reflections reflections = new Reflections(configuration); + assertThat(reflections.get(SubTypes.of(ExpandTestModel.NotScanned.BaseInterface.class).asClass()), + equalTo( + ExpandTestModel.NotScanned.BaseClass.class, + ExpandTestModel.Scanned.ChildrenClass.class)); + + Reflections refNoExpand = new Reflections(configuration.setExpandSuperTypes(false)); + assertThat(refNoExpand.get(SubTypes.of(ExpandTestModel.NotScanned.BaseInterface.class).asClass()), + equalTo()); + } + + @Test + void testDetectInheritedAnnotations() { + ConfigurationBuilder configuration = new ConfigurationBuilder() + .forPackage("io.github.dunwu.javatech.reflections") + .filterInputsBy(inputsFilter); + + Reflections reflections = new Reflections(configuration); + assertThat(reflections.getTypesAnnotatedWith(ExpandTestModel.NotScanned.TestAnnotation.class), + equalTo( + ExpandTestModel.NotScanned.BaseClass.class, + ExpandTestModel.Scanned.ChildrenClass.class)); + + Reflections refNoExpand = new Reflections(configuration.setExpandSuperTypes(false)); + assertThat(refNoExpand.getTypesAnnotatedWith(ExpandTestModel.NotScanned.TestAnnotation.class), + equalTo()); + } + + @Test + void testExpandMetaAnnotations() { + ConfigurationBuilder configuration = new ConfigurationBuilder() + .forPackage("io.github.dunwu.javatech.reflections") + .filterInputsBy(inputsFilter); + + Reflections reflections = new Reflections(configuration); + assertThat(reflections.getTypesAnnotatedWith(ExpandTestModel.NotScanned.MetaAnnotation.class), + equalTo()); + // todo fix, support expansion of meta annotations outside of scanned scope + // equalTo( + // NotScanned.TestAnnotation.class, + // NotScanned.BaseClass.class, + // Scanned.ChildrenClass.class)); + + Reflections refNoExpand = new Reflections(configuration.setExpandSuperTypes(false)); + assertThat(refNoExpand.getTypesAnnotatedWith(ExpandTestModel.NotScanned.MetaAnnotation.class), + equalTo()); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionsQueryTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionsQueryTest.java new file mode 100644 index 00000000..a787e6bc --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionsQueryTest.java @@ -0,0 +1,339 @@ +package io.github.dunwu.javatech.reflections; + +import org.hamcrest.Matcher; +import org.hamcrest.core.IsEqual; +import org.junit.jupiter.api.Test; +import org.reflections.Reflections; +import org.reflections.scanners.Scanners; +import org.reflections.util.ConfigurationBuilder; +import org.reflections.util.FilterBuilder; +import org.reflections.util.NameHelper; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashSet; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.reflections.ReflectionUtils.withAnnotation; +import static org.reflections.ReflectionUtils.withAnyParameterAnnotation; +import static io.github.dunwu.javatech.reflections.TestModel.*; +import static org.reflections.scanners.Scanners.*; + +public class ReflectionsQueryTest implements NameHelper { + static Reflections reflections; + + public ReflectionsQueryTest() { + reflections = new Reflections( + new ConfigurationBuilder() + .forPackage("io.github.dunwu.javatech.reflections") + .filterInputsBy(new FilterBuilder() + .includePattern("io\\.github\\.dunwu\\.javatech\\.reflections\\.TestModel\\$.*") + .or(s -> s.endsWith(".xml"))) + .setScanners(Scanners.values())); + } + + @Test + public void testSubTypes() { + assertThat("direct subtypes of interface", + reflections.get(SubTypes.get(I1.class)), + equalToNames(I2.class)); + + assertThat("direct subtypes of class", + reflections.get(SubTypes.get(C1.class).asClass()), + equalTo(C2.class, C3.class)); + + assertThat("transitive subtypes of interface", + reflections.get(SubTypes.of(I1.class)), + equalToNames(I2.class, C1.class, C2.class, C3.class, C5.class)); + + assertThat("transitive subtypes of class", + reflections.get(SubTypes.of(C1.class).asClass()), + equalTo(C2.class, C3.class, C5.class)); + } + + @Test + public void testTypesAnnotated() { + assertThat("direct types annotated with meta annotation", + reflections.get(TypesAnnotated.get(MAI1.class).asClass()), + equalTo(AI1.class)); + + assertThat("transitive types annotated with meta annotation", + reflections.get(TypesAnnotated.of(MAI1.class).asClass()), + equalTo(AI1.class, I1.class)); + + assertThat("transitive subtypes of types annotated with meta annotation, including", + reflections.get(SubTypes.of(TypesAnnotated.with(MAI1.class)).asClass()), + equalTo(AI1.class, I1.class, I2.class, C1.class, C2.class, C3.class, C5.class)); + + assertThat("direct types annotated with annotation", + reflections.get(TypesAnnotated.get(AI1.class)), + equalToNames(I1.class)); + + assertThat("transitive types annotated with annotation", + reflections.get(TypesAnnotated.of(AI1.class)), + equalToNames(I1.class)); + + assertThat("transitive subtypes of types annotated with annotation", + reflections.get(SubTypes.of(TypesAnnotated.with(AI1.class))), + equalToNames(I1.class, I2.class, C1.class, C2.class, C3.class, C5.class)); + } + + @Test + public void testTypesAnnotatedWithMemberMatching() { + assertThat("direct types annotated with annotation", + reflections.get(TypesAnnotated.get(AC2.class).asClass()), + equalTo(C2.class, C3.class, I3.class, AC3.class)); + + assertThat("transitive types annotated with annotation", + reflections.get(TypesAnnotated.with(AC2.class).asClass()), + equalTo(C2.class, C3.class, I3.class, AC3.class, C7.class)); + + assertThat("direct types annotated with annotation with Retention(CLASS)", + reflections.get(TypesAnnotated.get(AC3.class).asClass()), + equalTo(C7.class)); + + assertThat("transitive subtypes of types annotated with annotation", + reflections.get(SubTypes.of(TypesAnnotated.with(AC2.class)).asClass()), + equalTo(C2.class, C3.class, I3.class, AC3.class, C7.class, C5.class, C6.class)); + + AC2 ac2 = new AC2() { + public String value() { return "ac2"; } + public Class annotationType() { return AC2.class; } + }; + + assertThat("transitive types annotated with annotation filter by member matching", + reflections.get(TypesAnnotated.with(AC2.class).asClass().filter(withAnnotation(ac2))), + equalTo(C3.class, I3.class, AC3.class)); + + assertThat("transitive subtypes of types annotated with annotation filter by member matching", + reflections.get(SubTypes.of(TypesAnnotated.with(AC2.class).filter(a -> withAnnotation(ac2).test(forClass(a))))), + equalToNames(C3.class, I3.class, AC3.class, C5.class, C6.class)); + } + + @Test + public void testMethodsAnnotated() throws NoSuchMethodException { + assertThat("methods annotated with annotation", + reflections.get(MethodsAnnotated.with(AM1.class)), + equalToNames( + C4.class.getDeclaredMethod("m1"), + C4.class.getDeclaredMethod("m1", int.class, String[].class), + C4.class.getDeclaredMethod("m1", int[][].class, String[][].class), + C4.class.getDeclaredMethod("m3"))); + + AM1 am11 = new AM1() { + public String value() { + return "1"; + } + public Class annotationType() { + return AM1.class; + } + }; + + assertThat("methods annotated with annotation filter by member matching", + reflections.get(MethodsAnnotated.with(AM1.class).as(Method.class).filter(withAnnotation(am11))), + equalTo( + C4.class.getDeclaredMethod("m1"), + C4.class.getDeclaredMethod("m1", int.class, String[].class), + C4.class.getDeclaredMethod("m1", int[][].class, String[][].class))); + } + + @Test + public void testConstructorsAnnotated() throws NoSuchMethodException { + assertThat("constructors annotated with annotation", + reflections.get(ConstructorsAnnotated.with(AM1.class)), + equalToNames(C4.class.getDeclaredConstructor(String.class))); + + AM1 am12 = new AM1() { + public String value() { + return "2"; + } + public Class annotationType() { + return AM1.class; + } + }; + + assertThat("constructors annotated with annotation filter by member matching", + reflections.get(ConstructorsAnnotated.with(AM1.class) + .as(Constructor.class).filter(withAnnotation(am12))), + equalTo()); + } + + @Test + public void testFieldsAnnotated() throws NoSuchFieldException { + assertThat("fields annotated with annotation", + reflections.get(FieldsAnnotated.with(AF1.class)), + equalToNames( + C4.class.getDeclaredField("f1"), + C4.class.getDeclaredField("f2"))); + + AF1 af12 = new AF1() { + public String value() { + return "2"; + } + public Class annotationType() { + return AF1.class; + } + }; + + assertThat("fields annotated with annotation filter by member matching", + reflections.get(FieldsAnnotated.with(AF1.class) + .as(Field.class).filter(withAnnotation(af12))), + equalTo(C4.class.getDeclaredField("f2"))); + } + + @Test + public void testMethods() throws NoSuchMethodException { + assertThat("methods with any parameter", + reflections.get(MethodsParameter.with(String.class)), + equalToNames(C4.class.getDeclaredMethod("m4", String.class))); + + assertThat("methods with any parameter", + reflections.get(MethodsParameter.with(int.class)), + equalToNames( + C4.class.getDeclaredMethod("m1", int.class, String[].class), + C4.class.getDeclaredMethod("add", int.class, int.class))); + + assertThat("methods with signature single parameter", + reflections.get(MethodsSignature.with(String.class)), + equalToNames(C4.class.getDeclaredMethod("m4", String.class))); + + assertThat("methods with signature", + reflections.get(MethodsSignature.with(int.class, String[].class)), + equalToNames(C4.class.getDeclaredMethod("m1", int.class, String[].class))); + + assertThat("methods with signature no parameters", + reflections.get(MethodsSignature.with()), + equalToNames( + C4.class.getDeclaredMethod("m1"), + C4.class.getDeclaredMethod("m3"), + AC2.class.getMethod("value"), + AF1.class.getMethod("value"), + AM1.class.getMethod("value"))); + + assertThat("methods with return type", + reflections.get(MethodsReturn.of(String.class)), + equalToNames( + C4.class.getDeclaredMethod("m3"), + C4.class.getDeclaredMethod("m4", String.class), + AC2.class.getMethod("value"), + AF1.class.getMethod("value"), + AM1.class.getMethod("value"))); + + assertThat("methods with return type void", + reflections.get(MethodsReturn.of(void.class)), + equalToNames( + C4.class.getDeclaredMethod("m1"), + C4.class.getDeclaredMethod("m1", int.class, String[].class), + C4.class.getDeclaredMethod("m1", int[][].class, String[][].class))); + + assertThat("methods with parameter annotation", + reflections.get(MethodsParameter.with(AM1.class)), + equalToNames(C4.class.getDeclaredMethod("m4", String.class))); + + AM1 am1 = new AM1() { + public String value() { + return "2"; + } + public Class annotationType() { + return AM1.class; + } + }; + + assertThat("methods with parameter annotation filter by member matching", + reflections.get(MethodsParameter.with(AM1.class).as(Method.class).filter(withAnyParameterAnnotation(am1))), + equalTo(C4.class.getDeclaredMethod("m4", String.class))); + + assertThat("methods with parameter annotation visible/invisible", + reflections.get(MethodsParameter.with(AM2.class)), + equalToNames( + C4.class.getDeclaredMethod("m4", String.class), + C4.class.getDeclaredMethod("m1", int.class, String[].class))); + } + + @Test + public void testConstructorParameter() throws NoSuchMethodException { + assertThat("constructors with parameter", + reflections.get(ConstructorsParameter.with(String.class)), + equalToNames(C4.class.getDeclaredConstructor(String.class))); + + assertThat("constructors with signature no parameters", + reflections.get(ConstructorsSignature.with()), + equalToNames( + C1.class.getDeclaredConstructor(), + C2.class.getDeclaredConstructor(), + C3.class.getDeclaredConstructor(), + C4.class.getDeclaredConstructor(), + C5.class.getDeclaredConstructor(), + C6.class.getDeclaredConstructor(), + C7.class.getDeclaredConstructor())); + + assertThat("constructors with parameter annotation", + reflections.get(ConstructorsParameter.with(AM1.class)), + equalToNames(C4.class.getDeclaredConstructor(String.class))); + + AM1 am1 = new AM1() { + public String value() { + return "1"; + } + public Class annotationType() { + return AM1.class; + } + }; + + assertThat("constructors with parameter annotation filter by member values", + reflections.get(ConstructorsParameter.with(AM1.class) + .as(Constructor.class) + .filter(withAnnotation(am1))), + equalTo(C4.class.getDeclaredConstructor(String.class))); + } + + @Test + public void testResourcesScanner() { + assertThat("resources matching pattern", + reflections.get(Resources.with(".*resource1-reflections\\.xml")), + equalTo("META-INF/reflections/resource1-reflections.xml")); + + assertThat("resources matching pattern any", + reflections.get(Resources.with(".*")), + equalTo( + "META-INF/reflections/testModel-reflections.xml", + "META-INF/reflections/saved-testModel-reflections.xml", + "META-INF/reflections/resource1-reflections.xml", + "META-INF/reflections/inner/resource2-reflections.xml")); + } + + @Test + public void testGetAll() { + reflections = new Reflections( + new ConfigurationBuilder() + .forPackage("io.github.dunwu.javatech.reflections") + .filterInputsBy(new FilterBuilder().includePattern("io\\.github\\.dunwu\\.javatech\\.reflections\\.TestModel\\$.*")) + .setScanners(Scanners.SubTypes.filterResultsBy(t -> true))); + + assertThat("all (sub) types", + reflections.getAll(SubTypes), + equalTo("java.lang.Object", "java.lang.annotation.Annotation", + "io.github.dunwu.javatech.reflections.TestModel$MAI1", "io.github.dunwu.javatech.reflections.TestModel$AI1", "io.github.dunwu.javatech.reflections.TestModel$AI2", + "io.github.dunwu.javatech.reflections.TestModel$I1", "io.github.dunwu.javatech.reflections.TestModel$I2", "io.github.dunwu.javatech.reflections.TestModel$I3", + "io.github.dunwu.javatech.reflections.TestModel$AF1", "io.github.dunwu.javatech.reflections.TestModel$AM1", "io.github.dunwu.javatech.reflections.TestModel$AM2", + "io.github.dunwu.javatech.reflections.TestModel$AC1", "io.github.dunwu.javatech.reflections.TestModel$AC1n", "io.github.dunwu.javatech.reflections.TestModel$AC2", "io.github.dunwu.javatech.reflections.TestModel$AC3", + "io.github.dunwu.javatech.reflections.TestModel$C1", "io.github.dunwu.javatech.reflections.TestModel$C2", "io.github.dunwu.javatech.reflections.TestModel$C3", "io.github.dunwu.javatech.reflections.TestModel$C4", + "io.github.dunwu.javatech.reflections.TestModel$C5", "io.github.dunwu.javatech.reflections.TestModel$C6", "io.github.dunwu.javatech.reflections.TestModel$C7")); + } + + // + @SafeVarargs + public static Matcher> equalTo(T... operand) { + return IsEqual.equalTo(new LinkedHashSet<>(Arrays.asList(operand))); + } + + @SafeVarargs + public final Matcher> equalToNames(T... operand) { + return IsEqual.equalTo(new LinkedHashSet<>(toNames(operand))); + } +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionsTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionsTest.java new file mode 100644 index 00000000..f39a8c44 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/ReflectionsTest.java @@ -0,0 +1,343 @@ +package io.github.dunwu.javatech.reflections; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.hamcrest.core.IsEqual; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.reflections.Reflections; +import org.reflections.scanners.*; +import org.reflections.util.ClasspathHelper; +import org.reflections.util.ConfigurationBuilder; +import org.reflections.util.FilterBuilder; +import org.reflections.util.NameHelper; + +import java.io.File; +import java.lang.annotation.Annotation; +import java.util.*; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static io.github.dunwu.javatech.reflections.TestModel.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@SuppressWarnings("unchecked") +public class ReflectionsTest implements NameHelper { + + private static final FilterBuilder TestModelFilter = new FilterBuilder() + .includePattern("io\\.github\\.dunwu\\.javatech\\.reflections\\.TestModel\\$.*") + .includePattern("io\\.github\\.dunwu\\.javatech\\.reflections\\.UsageTestModel\\$.*"); + + static Reflections reflections; + + @BeforeAll + public static void init() { + //noinspection deprecation + reflections = new Reflections(new ConfigurationBuilder() + .setUrls(Collections.singletonList(ClasspathHelper.forClass(TestModel.class))) + .filterInputsBy(TestModelFilter) + .setScanners( + new SubTypesScanner(), + new TypeAnnotationsScanner(), + new MethodAnnotationsScanner(), + new FieldAnnotationsScanner(), + Scanners.ConstructorsAnnotated, + Scanners.MethodsParameter, + Scanners.MethodsSignature, + Scanners.MethodsReturn, + Scanners.ConstructorsParameter, + Scanners.ConstructorsSignature, + new ResourcesScanner(), + new MethodParameterNamesScanner(), + new MemberUsageScanner())); + } + + @Test + public void testSubTypesOf() { + assertThat(reflections.getSubTypesOf(I1.class), are(I2.class, C1.class, C2.class, C3.class, C5.class)); + assertThat(reflections.getSubTypesOf(C1.class), are(C2.class, C3.class, C5.class)); + + assertFalse(reflections.getAllTypes().isEmpty(), + "getAllTypes should not be empty when Reflections is configured with SubTypesScanner(false)"); + } + + @Test + public void testTypesAnnotatedWith() { + assertThat(reflections.getTypesAnnotatedWith(MAI1.class, true), are(AI1.class)); + assertThat(reflections.getTypesAnnotatedWith(MAI1.class, true), annotatedWith(MAI1.class)); + + assertThat(reflections.getTypesAnnotatedWith(AI2.class, true), are(I2.class)); + assertThat(reflections.getTypesAnnotatedWith(AI2.class, true), annotatedWith(AI2.class)); + + assertThat(reflections.getTypesAnnotatedWith(AC1.class, true), are(C1.class, C2.class, C3.class, C5.class)); + assertThat(reflections.getTypesAnnotatedWith(AC1.class, true), annotatedWith(AC1.class)); + + assertThat(reflections.getTypesAnnotatedWith(AC1n.class, true), are(C1.class)); + assertThat(reflections.getTypesAnnotatedWith(AC1n.class, true), annotatedWith(AC1n.class)); + assertThat(reflections.getTypesAnnotatedWith(MAI1.class), + are(AI1.class, I1.class, I2.class, C1.class, C2.class, C3.class, C5.class)); + assertThat(reflections.getTypesAnnotatedWith(AI1.class), + are(I1.class, I2.class, C1.class, C2.class, C3.class, C5.class)); + assertThat(reflections.getTypesAnnotatedWith(AI2.class), are(I2.class, C1.class, C2.class, C3.class, C5.class)); + + assertThat(reflections.getTypesAnnotatedWith(AM1.class), isEmpty); + + //annotation member value matching + AC2 ac2 = new AC2() { + public String value() {return "ac2";} + + public Class annotationType() {return AC2.class;} + }; + + assertThat(reflections.getTypesAnnotatedWith(ac2), + are(C3.class, C5.class, I3.class, C6.class, AC3.class, C7.class)); + + assertThat(reflections.getTypesAnnotatedWith(ac2, true), are(C3.class, I3.class, AC3.class)); + } + + @Test + public void testMethodsAnnotatedWith() throws NoSuchMethodException { + assertThat(reflections.getMethodsAnnotatedWith(AM1.class), + are(C4.class.getDeclaredMethod("m1"), + C4.class.getDeclaredMethod("m1", int.class, String[].class), + C4.class.getDeclaredMethod("m1", int[][].class, String[][].class), + C4.class.getDeclaredMethod("m3"))); + + AM1 am1 = new AM1() { + public String value() {return "1";} + + public Class annotationType() {return AM1.class;} + }; + assertThat(reflections.getMethodsAnnotatedWith(am1), + are(C4.class.getDeclaredMethod("m1"), + C4.class.getDeclaredMethod("m1", int.class, String[].class), + C4.class.getDeclaredMethod("m1", int[][].class, String[][].class))); + } + + @Test + public void testConstructorsAnnotatedWith() throws NoSuchMethodException { + assertThat(reflections.getConstructorsAnnotatedWith(AM1.class), + are(C4.class.getDeclaredConstructor(String.class))); + + AM1 am1 = new AM1() { + public String value() {return "1";} + + public Class annotationType() {return AM1.class;} + }; + assertThat(reflections.getConstructorsAnnotatedWith(am1), + are(C4.class.getDeclaredConstructor(String.class))); + } + + @Test + public void testFieldsAnnotatedWith() throws NoSuchFieldException { + assertThat(reflections.getFieldsAnnotatedWith(AF1.class), + are(C4.class.getDeclaredField("f1"), + C4.class.getDeclaredField("f2") + )); + + assertThat(reflections.getFieldsAnnotatedWith(new AF1() { + public String value() {return "2";} + + public Class annotationType() {return AF1.class;} + }), + are(C4.class.getDeclaredField("f2"))); + } + + @Test + public void testMethodParameter() throws NoSuchMethodException { + assertThat(reflections.getMethodsWithParameter(String.class), + are(C4.class.getDeclaredMethod("m4", String.class), + UsageTestModel.C1.class.getDeclaredMethod("method", String.class))); + + assertThat(reflections.getMethodsWithSignature(), + are(C4.class.getDeclaredMethod("m1"), C4.class.getDeclaredMethod("m3"), + AC2.class.getMethod("value"), AF1.class.getMethod("value"), AM1.class.getMethod("value"), + UsageTestModel.C1.class.getDeclaredMethod("method"), + UsageTestModel.C2.class.getDeclaredMethod("method"))); + + assertThat(reflections.getMethodsWithSignature(int[][].class, String[][].class), + are(C4.class.getDeclaredMethod("m1", int[][].class, String[][].class))); + + assertThat(reflections.getMethodsReturn(int.class), + are(C4.class.getDeclaredMethod("add", int.class, int.class))); + + assertThat(reflections.getMethodsReturn(String.class), + are(C4.class.getDeclaredMethod("m3"), C4.class.getDeclaredMethod("m4", String.class), + AC2.class.getMethod("value"), AF1.class.getMethod("value"), AM1.class.getMethod("value"))); + + assertThat(reflections.getMethodsReturn(void.class), + are(C4.class.getDeclaredMethod("m1"), C4.class.getDeclaredMethod("m1", int.class, String[].class), + C4.class.getDeclaredMethod("m1", int[][].class, String[][].class), + UsageTestModel.C1.class.getDeclaredMethod("method"), + UsageTestModel.C1.class.getDeclaredMethod("method", String.class), + UsageTestModel.C2.class.getDeclaredMethod("method"))); + + assertThat(reflections.getMethodsWithParameter(AM1.class), + are(C4.class.getDeclaredMethod("m4", String.class))); + + assertThat(reflections.getMethodsWithParameter(AM2.class), + are(C4.class.getDeclaredMethod("m4", String.class), + C4.class.getDeclaredMethod("m1", int.class, String[].class))); + } + + @Test + public void testConstructorParameter() throws NoSuchMethodException { + assertThat(reflections.getConstructorsWithParameter(String.class), + are(C4.class.getDeclaredConstructor(String.class))); + + assertThat(reflections.getConstructorsWithSignature(), + are(C1.class.getDeclaredConstructor(), C2.class.getDeclaredConstructor(), C3.class.getDeclaredConstructor(), + C4.class.getDeclaredConstructor(), C5.class.getDeclaredConstructor(), C6.class.getDeclaredConstructor(), + C7.class.getDeclaredConstructor(), UsageTestModel.C1.class.getDeclaredConstructor(), + UsageTestModel.C2.class.getDeclaredConstructor())); + + assertThat(reflections.getConstructorsWithParameter(AM1.class), + are(C4.class.getDeclaredConstructor(String.class))); + } + + @Test + public void testResourcesScanner() { + Predicate filter = + new FilterBuilder().includePattern(".*\\.xml").excludePattern(".*testModel-reflections\\.xml"); + Reflections reflections = new Reflections(new ConfigurationBuilder() + .filterInputsBy(filter) + .setScanners(Scanners.Resources) + .setUrls(Collections.singletonList(ClasspathHelper.forClass(TestModel.class)))); + + Collection resolved = reflections.getResources(Pattern.compile(".*resource1-reflections\\.xml")); + assertThat(resolved, are("META-INF/reflections/resource1-reflections.xml")); + + Collection resources = reflections.getResources(".*"); + assertThat(resources, are("META-INF/reflections/resource1-reflections.xml", + "META-INF/reflections/inner/resource2-reflections.xml")); + } + + @Test + public void testMethodParameterNames() throws NoSuchMethodException { + assertEquals(reflections.getMemberParameterNames(C4.class.getDeclaredMethod("m3")), + Collections.emptyList()); + + assertEquals(reflections.getMemberParameterNames(C4.class.getDeclaredMethod("m4", String.class)), + Collections.singletonList("string")); + + assertEquals(reflections.getMemberParameterNames(C4.class.getDeclaredMethod("add", int.class, int.class)), + Arrays.asList("i1", "i2")); + + assertEquals(reflections.getMemberParameterNames(C4.class.getDeclaredConstructor(String.class)), + Collections.singletonList("f1")); + } + + @Test + public void testMemberUsageScanner() throws NoSuchFieldException, NoSuchMethodException { + //field usage + assertThat(reflections.getMemberUsage(UsageTestModel.C1.class.getDeclaredField("c2")), + are(UsageTestModel.C1.class.getDeclaredConstructor(), + UsageTestModel.C1.class.getDeclaredConstructor(UsageTestModel.C2.class), + UsageTestModel.C1.class.getDeclaredMethod("method"), + UsageTestModel.C1.class.getDeclaredMethod("method", String.class))); + + //method usage + assertThat(reflections.getMemberUsage(UsageTestModel.C1.class.getDeclaredMethod("method")), + are(UsageTestModel.C2.class.getDeclaredMethod("method"))); + + assertThat(reflections.getMemberUsage(UsageTestModel.C1.class.getDeclaredMethod("method", String.class)), + are(UsageTestModel.C2.class.getDeclaredMethod("method"))); + + //constructor usage + assertThat(reflections.getMemberUsage(UsageTestModel.C1.class.getDeclaredConstructor()), + are(UsageTestModel.C2.class.getDeclaredConstructor(), + UsageTestModel.C2.class.getDeclaredMethod("method"))); + + assertThat(reflections.getMemberUsage(UsageTestModel.C1.class.getDeclaredConstructor(UsageTestModel.C2.class)), + are(UsageTestModel.C2.class.getDeclaredMethod("method"))); + } + + @Test + public void testScannerNotConfigured() throws NoSuchMethodException { + Reflections reflections = new Reflections(new ConfigurationBuilder() + .setUrls(Collections.singletonList(ClasspathHelper.forClass(TestModel.class))) + .filterInputsBy(TestModelFilter.includePackage("io\\.github\\.dunwu\\.javatech\\.reflections\\.UsageTestModel\\$.*")) + .setScanners()); + + assertTrue(reflections.getSubTypesOf(C1.class).isEmpty()); + assertTrue(reflections.getTypesAnnotatedWith(AC1.class).isEmpty()); + assertTrue(reflections.getMethodsAnnotatedWith(AC1.class).isEmpty()); + assertTrue(reflections.getMethodsWithSignature().isEmpty()); + assertTrue(reflections.getMethodsWithParameter(String.class).isEmpty()); + assertTrue(reflections.getMethodsReturn(String.class).isEmpty()); + assertTrue(reflections.getConstructorsAnnotatedWith(AM1.class).isEmpty()); + assertTrue(reflections.getConstructorsWithSignature().isEmpty()); + assertTrue(reflections.getConstructorsWithParameter(String.class).isEmpty()); + assertTrue(reflections.getFieldsAnnotatedWith(AF1.class).isEmpty()); + assertTrue(reflections.getResources(".*").isEmpty()); + assertTrue(reflections.getMemberParameterNames(C4.class.getDeclaredMethod("m4", String.class)).isEmpty()); + assertTrue(reflections.getMemberUsage(UsageTestModel.C1.class.getDeclaredConstructor()).isEmpty()); + assertTrue(reflections.getAllTypes().isEmpty()); + } + + // + public static String getUserDir() { + File file = new File(System.getProperty("user.dir")); + //a hack to fix user.dir issue(?) in surfire + if (Arrays.asList(file.list()).contains("reflections")) { + file = new File(file, "reflections"); + } + return file.getAbsolutePath(); + } + + private final BaseMatcher>> isEmpty = new BaseMatcher>>() { + public boolean matches(Object o) { + return ((Collection) o).isEmpty(); + } + + public void describeTo(Description description) { + description.appendText("empty collection"); + } + }; + + private abstract static class Match extends BaseMatcher { + + public void describeTo(Description description) { } + + } + + public static Matcher> are(final T... ts) { + final Collection c1 = Arrays.asList(ts); + return new Match>() { + public boolean matches(Object o) { + Collection c2 = (Collection) o; + return c1.containsAll(c2) && c2.containsAll(c1); + } + + @Override + public void describeTo(Description description) { + description.appendText(Arrays.toString(ts)); + } + }; + } + + @SafeVarargs + public static Matcher> equalTo(T... operand) { + return IsEqual.equalTo(new HashSet<>(Arrays.asList(operand))); + } + + private Matcher>> annotatedWith(final Class annotation) { + return new Match>>() { + public boolean matches(Object o) { + for (Class c : (Iterable>) o) { + List> annotationTypes = + Stream.of(c.getAnnotations()).map(Annotation::annotationType).collect(Collectors.toList()); + if (!annotationTypes.contains(annotation)) return false; + } + return true; + } + }; + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/TestModel.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/TestModel.java new file mode 100644 index 00000000..3045b508 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/TestModel.java @@ -0,0 +1,115 @@ +package io.github.dunwu.javatech.reflections; + +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@SuppressWarnings({ "ALL" }) +public interface TestModel { + + public @Retention(RUNTIME) + @Inherited + @interface MAI1 {} + + public @Retention(RUNTIME) + @MAI1 + @interface AI1 {} + + public @AI1 + interface I1 {} + + public @Retention(RUNTIME) + @Inherited + @interface AI2 {} + + public @AI2 + interface I2 extends I1 {} + + public @Retention(RUNTIME) + @Inherited + @interface AC1 {} + + public @Retention(RUNTIME) + @interface AC1n {} + + public @AC1 + @AC1n + class C1 implements I2 {} + + public @Retention(RUNTIME) + @interface AC2 { + + public abstract String value(); + + } + + public @AC2("") + class C2 extends C1 {} + + public @AC2("ac2") + class C3 extends C1 {} + + public @Retention(RUNTIME) + @interface AM1 { + + public abstract String value(); + + } + + public @interface AM2 {} + + public @Retention(RUNTIME) + @interface AF1 { + + public abstract String value(); + + } + + public class C4 { + + @AF1("1") + private String f1; + @AF1("2") + protected String f2; + protected String f3; + + public C4() { } + + @AM1("1") + public C4(@AM1("1") String f1) { this.f1 = f1; } + + @AM1("1") + protected void m1() {} + + @AM1("1") + public void m1(int integer, @AM2 String... strings) {} + + @AM1("1") + public void m1(int[][] integer, String[][] strings) {} + + @AM1("2") + public String m3() {return null;} + + public String m4(@AM1("2") @AM2 String string) {return null;} + + public C3 c2toC3(C2 c2) {return null;} + + public int add(int i1, int i2) { return i1 + i2; } + + } + + public class C5 extends C3 {} + + public @AC2("ac2") + interface I3 {} + + public class C6 implements I3 {} + + public @AC2("ac2") + @interface AC3 {} // not @Retention(RUNTIME) + + public @AC3 + class C7 {} + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/UsageTestModel.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/UsageTestModel.java new file mode 100644 index 00000000..ae1bd5b0 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/UsageTestModel.java @@ -0,0 +1,32 @@ +package io.github.dunwu.javatech.reflections; + +public interface UsageTestModel { + + class C1 { + + C2 c2 = new C2(); + + public C1() {} + + public C1(C2 c2) {this.c2 = c2;} + + public void method() {c2.method();} + + public void method(String string) {c2.method();} + + } + + class C2 { + + C1 c1 = new C1(); + + public void method() { + c1 = new C1(); + c1 = new C1(this); + c1.method(); + c1.method(""); + } + + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/VfsTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/VfsTest.java new file mode 100644 index 00000000..b3cf9e07 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/reflections/VfsTest.java @@ -0,0 +1,132 @@ +package io.github.dunwu.javatech.reflections; + +import javassist.bytecode.ClassFile; +import org.junit.jupiter.api.Test; +import org.reflections.ReflectionsException; +import org.reflections.util.ClasspathHelper; +import org.reflections.vfs.SystemDir; +import org.reflections.vfs.Vfs; +import org.slf4j.Logger; + +import java.io.BufferedInputStream; +import java.io.DataInputStream; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static java.text.MessageFormat.format; +import static org.junit.jupiter.api.Assertions.*; + +public class VfsTest { + + @Test + public void testJarFile() throws Exception { + URL url = new URL(ClasspathHelper.forClass(Logger.class).toExternalForm().replace("jar:", "")); + assertTrue(url.toString().startsWith("file:")); + assertTrue(url.toString().contains(".jar")); + + assertTrue(Vfs.DefaultUrlTypes.jarFile.matches(url)); + assertFalse(Vfs.DefaultUrlTypes.jarUrl.matches(url)); + assertFalse(Vfs.DefaultUrlTypes.directory.matches(url)); + + Vfs.Dir dir = Vfs.DefaultUrlTypes.jarFile.createDir(url); + testVfsDir(dir); + } + + @Test + public void testJarUrl() throws Exception { + URL url = ClasspathHelper.forClass(Logger.class); + assertTrue(url.toString().startsWith("jar:file:")); + assertTrue(url.toString().contains(".jar!")); + + assertFalse(Vfs.DefaultUrlTypes.jarFile.matches(url)); + assertTrue(Vfs.DefaultUrlTypes.jarUrl.matches(url)); + assertFalse(Vfs.DefaultUrlTypes.directory.matches(url)); + + Vfs.Dir dir = Vfs.DefaultUrlTypes.jarUrl.createDir(url); + testVfsDir(dir); + } + + @Test + public void testDirectory() throws Exception { + URL url = ClasspathHelper.forClass(getClass()); + assertTrue(url.toString().startsWith("file:")); + assertFalse(url.toString().contains(".jar")); + + assertFalse(Vfs.DefaultUrlTypes.jarFile.matches(url)); + assertFalse(Vfs.DefaultUrlTypes.jarUrl.matches(url)); + assertTrue(Vfs.DefaultUrlTypes.directory.matches(url)); + + Vfs.Dir dir = Vfs.DefaultUrlTypes.directory.createDir(url); + testVfsDir(dir); + } + + @Test + public void testJarInputStream() throws Exception { + URL url = ClasspathHelper.forClass(Logger.class); + assertTrue(Vfs.DefaultUrlTypes.jarInputStream.matches(url)); + try { + testVfsDir(Vfs.DefaultUrlTypes.jarInputStream.createDir(url)); + fail(); + } catch (ReflectionsException e) { + // expected + } + + url = new URL( + ClasspathHelper.forClass(Logger.class).toExternalForm().replace("jar:", "").replace(".jar!", ".jar")); + assertTrue(Vfs.DefaultUrlTypes.jarInputStream.matches(url)); + testVfsDir(Vfs.DefaultUrlTypes.jarInputStream.createDir(url)); + + url = ClasspathHelper.forClass(getClass()); + assertFalse(Vfs.DefaultUrlTypes.jarInputStream.matches(url)); + try { + testVfsDir(Vfs.DefaultUrlTypes.jarInputStream.createDir(url)); + fail(); + } catch (AssertionError e) { + // expected + } + } + + @Test + public void dirWithSpaces() { + Collection urls = ClasspathHelper.forPackage("dir+with spaces"); + assertFalse(urls.isEmpty()); + for (URL url : urls) { + Vfs.Dir dir = Vfs.fromURL(url); + assertNotNull(dir); + assertNotNull(dir.getFiles().iterator().next()); + } + } + + @Test + public void vfsFromDirWithJarInName() throws MalformedURLException { + String tmpFolder = System.getProperty("java.io.tmpdir"); + tmpFolder = tmpFolder.endsWith(File.separator) ? tmpFolder : tmpFolder + File.separator; + String dirWithJarInName = tmpFolder + "tony.jarvis"; + File newDir = new File(dirWithJarInName); + newDir.mkdir(); + + try { + Vfs.Dir dir = Vfs.fromURL(new URL(format("file:{0}", dirWithJarInName))); + + assertEquals(dirWithJarInName.replace("\\", "/"), dir.getPath()); + assertEquals(SystemDir.class, dir.getClass()); + } finally { + newDir.delete(); + } + } + + private void testVfsDir(Vfs.Dir dir) { + List files = new ArrayList<>(); + for (Vfs.File file : dir.getFiles()) { + files.add(file); + } + assertFalse(files.isEmpty()); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/binary/BinarySerializePerformanceTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/binary/BinarySerializePerformanceTest.java new file mode 100644 index 00000000..cb0a67d6 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/binary/BinarySerializePerformanceTest.java @@ -0,0 +1,66 @@ +package io.github.dunwu.javatech.seriralize.binary; + +import io.github.dunwu.javatech.bean.sample.TestBean; +import io.github.dunwu.javatech.seriralize.FstDemo; +import io.github.dunwu.javatech.seriralize.JdkSerializeDemo; +import io.github.dunwu.javatech.seriralize.KryoDemo; +import io.github.dunwu.javatech.util.BeanUtils; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 序列化、反序列化性能测试 + * + * @author Zhang Peng + * @since 2019-11-22 + */ +public class BinarySerializePerformanceTest { + + private static final int BATCH_SIZE = 100000; + + @Test + public void testJdkSerialize() throws IOException, ClassNotFoundException { + long begin = System.currentTimeMillis(); + for (int i = 0; i < BATCH_SIZE; i++) { + TestBean oldBean = BeanUtils.initJdk8Bean(); + byte[] bytes = JdkSerializeDemo.writeToBytes(oldBean); + assertThat(bytes).isNotEmpty(); + TestBean newBean = JdkSerializeDemo.readFromBytes(bytes, TestBean.class); + assertThat(newBean).isNotNull(); + } + long end = System.currentTimeMillis(); + System.out.printf("JDK 默认序列化/反序列化耗时:%s", (end - begin)); + } + + @Test + public void testFst() throws IOException { + long begin = System.currentTimeMillis(); + for (int i = 0; i < BATCH_SIZE; i++) { + TestBean oldBean = BeanUtils.initJdk8Bean(); + byte[] bytes = FstDemo.writeToBytes(oldBean); + assertThat(bytes).isNotEmpty(); + TestBean newBean = FstDemo.readFromBytes(bytes, TestBean.class); + assertThat(newBean).isNotNull(); + } + long end = System.currentTimeMillis(); + System.out.printf("FST 序列化/反序列化耗时:%s", (end - begin)); + } + + @Test + public void testKryo() throws IOException { + long begin = System.currentTimeMillis(); + for (int i = 0; i < BATCH_SIZE; i++) { + TestBean oldBean = BeanUtils.initJdk8Bean(); + byte[] bytes = KryoDemo.writeToBytes(oldBean); + assertThat(bytes).isNotEmpty(); + TestBean newBean = KryoDemo.readFromBytes(bytes, TestBean.class); + assertThat(newBean).isNotNull(); + } + long end = System.currentTimeMillis(); + System.out.printf("Kryo 序列化/反序列化耗时:%s", (end - begin)); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/fastjson/FastjsonAnnotationBean.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/fastjson/FastjsonAnnotationBean.java new file mode 100644 index 00000000..836a0793 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/fastjson/FastjsonAnnotationBean.java @@ -0,0 +1,175 @@ +package io.github.dunwu.javatech.seriralize.json.fastjson; + +import com.alibaba.fastjson.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.Objects; + +/** + * @JSONField 的使用 + *

+ * @JSONField 可以配置在 getter/setter 方法上 + *

+ * @JSONField 可以配置在字段上,但是要求字段必须是 public + * + * @author Zhang Peng + * @see @JSONField + * @since 2019-03-18 + */ +public class FastjsonAnnotationBean implements Serializable { + + private int id; + + // 配置date序列化和反序列使用yyyyMMdd日期格式 + @JSONField(format = "yyyy-MM-dd") + private Date date1; + + @JsonIgnore + private Date date2; + + // 不序列化 + @JSONField(serialize = false, format = "yyyy-MM-dd hh:mm:ss") + private LocalDate date3; + + // 不反序列化 + @JSONField(deserialize = false, format = "yyyy-MM-dd") + private LocalDateTime date4; + + @JSONField(ordinal = 1) + private Double d1; + + // 按ordinal排序 + @JSONField(ordinal = 2) + private float f1; + + @JSONField(ordinal = 1) + private int f2; + + public FastjsonAnnotationBean() { + } + + public FastjsonAnnotationBean(int id, Date date1, Date date2, LocalDate date3, LocalDateTime date4, Double d1, + float f1, + int f2) { + this.id = id; + this.date1 = date1; + this.date2 = date2; + this.date3 = date3; + this.date4 = date4; + this.d1 = d1; + this.f1 = f1; + this.f2 = f2; + } + + @Override + public int hashCode() { + return Objects.hash(id, date1, date2, date3, date4, d1, f1, f2); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FastjsonAnnotationBean)) return false; + FastjsonAnnotationBean that = (FastjsonAnnotationBean) o; + return id == that.id && + Float.compare(that.f1, f1) == 0 && + f2 == that.f2 && + Objects.equals(date1, that.date1) && + Objects.equals(date2, that.date2) && + Objects.equals(date3, that.date3) && + Objects.equals(date4, that.date4) && + Objects.equals(d1, that.d1); + } + + @Override + public String toString() { + return "FastjsonAnnotationBean{" + + "id=" + id + + ", date1=" + date1 + + ", date2=" + date2 + + ", date3=" + date3 + + ", date4=" + date4 + + ", d1=" + d1 + + ", f1=" + f1 + + ", f2=" + f2 + + '}'; + } + + @JSONField(name = "ID") + public int getId() { + return id; + } + + public FastjsonAnnotationBean setId(int id) { + this.id = id; + return this; + } + + public Date getDate1() { + return date1; + } + + public FastjsonAnnotationBean setDate1(Date date1) { + this.date1 = date1; + return this; + } + + public Date getDate2() { + return date2; + } + + public FastjsonAnnotationBean setDate2(Date date2) { + this.date2 = date2; + return this; + } + + public LocalDate getDate3() { + return date3; + } + + public FastjsonAnnotationBean setDate3(LocalDate date3) { + this.date3 = date3; + return this; + } + + public LocalDateTime getDate4() { + return date4; + } + + public FastjsonAnnotationBean setDate4(LocalDateTime date4) { + this.date4 = date4; + return this; + } + + public Double getD1() { + return d1; + } + + public FastjsonAnnotationBean setD1(Double d1) { + this.d1 = d1; + return this; + } + + public float getF1() { + return f1; + } + + public FastjsonAnnotationBean setF1(float f1) { + this.f1 = f1; + return this; + } + + public int getF2() { + return f2; + } + + public FastjsonAnnotationBean setF2(int f2) { + this.f2 = f2; + return this; + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/fastjson/FastjsonCaseTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/fastjson/FastjsonCaseTests.java new file mode 100644 index 00000000..66a7d131 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/fastjson/FastjsonCaseTests.java @@ -0,0 +1,59 @@ +package io.github.dunwu.javatech.seriralize.json.fastjson; + +import com.alibaba.fastjson.JSON; +import io.github.dunwu.javatech.bean.sample.Group; +import io.github.dunwu.javatech.util.DateUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Date; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Zhang Peng + * @since 2019-11-22 + */ +public class FastjsonCaseTests { + + @Test + @DisplayName("测试 Fastjson 默认序列化、反序列化") + public void defaultTest() { + Group oldGroup = new Group(); + String jsonString = JSON.toJSONString(oldGroup); + + System.out.println(jsonString); + + Group newGroup = JSON.parseObject(jsonString, Group.class); + assertThat(newGroup).isNotNull(); + } + + /** + * 序列化测试 + */ + @Test + @DisplayName("测试 JavaBean 上使用 @JSONField 注解后的序列化、反序列化") + public void serializeTest() { + LocalDate localDate = LocalDate.of(1949, 10, 1); + LocalDateTime localDateTime = LocalDateTime.of(2000, 1, 1, 12, 0, 0); + Date date = DateUtil.toDate(localDateTime); + + FastjsonAnnotationBean originBean = + new FastjsonAnnotationBean(1, date, date, localDate, localDateTime, 100.0, 0.5f, 1000); + final String expectJson = + "{\"ID\":1,\"date1\":\"2000-01-01\",\"date2\":946699200000,\"date4\":\"2000-01-01\",\"d1\":100.0,\"f2\":1000,\"f1\":0.5}"; + String json = JSON.toJSONString(originBean); + Assertions.assertEquals(expectJson, json); + System.out.println("json = [" + json + "]"); + + FastjsonAnnotationBean targetBean = JSON.parseObject(json, FastjsonAnnotationBean.class); + FastjsonAnnotationBean expectBean = new FastjsonAnnotationBean(); + expectBean.setId(1).setDate1(date).setDate4(localDateTime).setD1(100.0).setF1(0.5f); + System.out.printf("deserialize result: %s", targetBean.toString()); + Assertions.assertNotEquals(originBean, targetBean); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/fastjson/FastjsonPerformanceTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/fastjson/FastjsonPerformanceTests.java new file mode 100644 index 00000000..4911e3b8 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/fastjson/FastjsonPerformanceTests.java @@ -0,0 +1,50 @@ +package io.github.dunwu.javatech.seriralize.json.fastjson; + +import com.alibaba.fastjson.JSON; +import io.github.dunwu.javatech.bean.sample.TestBean2; +import io.github.dunwu.javatech.util.BeanUtils; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Fastjson 性能测试 + * + * @author Zhang Peng + * @since 2019-03-18 + */ +public class FastjsonPerformanceTests { + + private static final int BATCH_SIZE = 100000; + + /** + * 测试十次,每次序列化、反序列化 100000 条数据,平均耗时约 380 ms + */ + @Test + public void testPerformance() { + long time = 0L; + for (int i = 0; i < 10; i++) { + time += donSerializeAndDeserialize(); + } + System.out.println(String.format("time: %d ms", TimeUnit.NANOSECONDS.toMillis(time / 10))); + } + + /** + * 循环序列化、反序列 {@link #BATCH_SIZE} 条数据,测试性能 + */ + private long donSerializeAndDeserialize() { + TestBean2 bean = BeanUtils.initNotJdk8Bean(); + long begin = System.nanoTime(); + for (int i = 0; i < BATCH_SIZE; i++) { + String json = JSON.toJSONString(bean); + assertThat(json).isNotBlank(); + TestBean2 newBean = JSON.parseObject(json, TestBean2.class); + assertThat(newBean).isNotNull(); + } + long end = System.nanoTime(); + return (end - begin); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/gson/GsonAnnotationBean.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/gson/GsonAnnotationBean.java new file mode 100644 index 00000000..a043da79 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/gson/GsonAnnotationBean.java @@ -0,0 +1,52 @@ +package io.github.dunwu.javatech.seriralize.json.gson; + +import com.google.gson.annotations.SerializedName; + +import java.util.Objects; + +/** + * @author Zhang Peng + * @since 2019-11-24 + */ +public class GsonAnnotationBean { + + @SerializedName("custom_naming") + private String someField; + + private String someOtherField; + + @Override + public int hashCode() { + return Objects.hash(someField, someOtherField); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof GsonAnnotationBean)) { + return false; + } + GsonAnnotationBean that = (GsonAnnotationBean) o; + return Objects.equals(someField, that.someField) && + Objects.equals(someOtherField, that.someOtherField); + } + + public String getSomeField() { + return someField; + } + + public void setSomeField(String someField) { + this.someField = someField; + } + + public String getSomeOtherField() { + return someOtherField; + } + + public void setSomeOtherField(String someOtherField) { + this.someOtherField = someOtherField; + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/gson/GsonCaseTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/gson/GsonCaseTests.java new file mode 100644 index 00000000..324c3c31 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/gson/GsonCaseTests.java @@ -0,0 +1,80 @@ +package io.github.dunwu.javatech.seriralize.json.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Modifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Zhang Peng + * @since 2019-11-24 + */ +public class GsonCaseTests { + + private Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create(); + + private Gson gson2 = new GsonBuilder() + .setVersion(1.0) + .setPrettyPrinting() + .setDateFormat("yyyy-MM-dd HH:mm:ss") + .excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT, Modifier.VOLATILE) + .create(); + + @Test + public void test() { + // Serialization + Gson gson = new Gson(); + gson.toJson(1); // ==> 1 + gson.toJson("abcd"); // ==> "abcd" + gson.toJson(10L); // ==> 10 + int[] values = { 1 }; + gson.toJson(values); // ==> [1] + + // Deserialization + int i1 = gson.fromJson("1", int.class); + Integer i2 = gson.fromJson("1", Integer.class); + Long l1 = gson.fromJson("1", Long.class); + Boolean b1 = gson.fromJson("false", Boolean.class); + String str = gson.fromJson("\"abc\"", String.class); + String[] anotherStr = gson.fromJson("[\"abc\"]", String[].class); + + assertThat(i1).isEqualTo(1); + assertThat(i2).isEqualTo(1); + assertThat(l1).isEqualTo(1L); + assertThat(b1).isFalse(); + assertThat(str).isEqualTo("abc"); + } + + @Test + public void testAnnotation() { + GsonAnnotationBean oldBean = new GsonAnnotationBean(); + oldBean.setSomeField("hello"); + oldBean.setSomeOtherField("world"); + + String expectStr = "{\"custom_naming\":\"hello\",\"someOtherField\":\"world\"}"; + String json = gson.toJson(oldBean); + assertThat(json).isEqualTo(expectStr); + + GsonAnnotationBean newBean = gson.fromJson(expectStr, GsonAnnotationBean.class); + assertThat(newBean).isEqualTo(oldBean); + } + + @Test + public void testVersionedClass() { + VersionedClass versionedObject = new VersionedClass(); + String jsonOutput = gson2.toJson(versionedObject); + System.out.println(jsonOutput); + assertThat(jsonOutput).isEqualTo("{\n" + + " \"newField\": \"new\",\n" + + " \"field\": \"old\"\n" + + "}"); + + jsonOutput = gson.toJson(versionedObject); + System.out.println(jsonOutput); + assertThat(jsonOutput).isEqualTo("{\"newerField\":\"newer\",\"newField\":\"new\",\"field\":\"old\"}"); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/gson/GsonPerformanceTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/gson/GsonPerformanceTests.java new file mode 100644 index 00000000..dcc35a5d --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/gson/GsonPerformanceTests.java @@ -0,0 +1,53 @@ +package io.github.dunwu.javatech.seriralize.json.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.github.dunwu.javatech.bean.sample.TestBean2; +import io.github.dunwu.javatech.util.BeanUtils; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Gson 性能测试 + * + * @author Zhang Peng + * @since 2019-11-22 + */ +public class GsonPerformanceTests { + + private static final int BATCH_SIZE = 100000; + + private Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create(); + + /** + * 测试十次,每次序列化、反序列化 100000 条数据,平均耗时约 704 ms + */ + @Test + public void testPerformance() { + long time = 0L; + for (int i = 0; i < 10; i++) { + time += donSerializeAndDeserialize(); + } + System.out.println(String.format("time: %d ms", TimeUnit.NANOSECONDS.toMillis(time / 10))); + } + + /** + * 循环序列化、反序列 {@link #BATCH_SIZE} 条数据,测试性能 + */ + private long donSerializeAndDeserialize() { + TestBean2 bean = BeanUtils.initNotJdk8Bean(); + long begin = System.nanoTime(); + for (int i = 0; i < BATCH_SIZE; i++) { + String json = gson.toJson(bean); + assertThat(json).isNotBlank(); + TestBean2 newBean = gson.fromJson(json, TestBean2.class); + assertThat(newBean).isNotNull(); + } + long end = System.nanoTime(); + return (end - begin); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/gson/VersionedClass.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/gson/VersionedClass.java new file mode 100644 index 00000000..cf4878a5 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/gson/VersionedClass.java @@ -0,0 +1,21 @@ +package io.github.dunwu.javatech.seriralize.json.gson; + +import com.google.gson.annotations.Since; + +public class VersionedClass { + + @Since(1.1) + private final String newerField; + + @Since(1.0) + private final String newField; + + private final String field; + + public VersionedClass() { + this.newerField = "newer"; + this.newField = "new"; + this.field = "old"; + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/jackson/JacksonAnnotationBean.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/jackson/JacksonAnnotationBean.java new file mode 100644 index 00000000..4777ca61 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/jackson/JacksonAnnotationBean.java @@ -0,0 +1,55 @@ +package io.github.dunwu.javatech.seriralize.json.jackson; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +/** + * @author Zhang Peng + * @since 2019-03-18 + */ +@JsonPropertyOrder(alphabetic = true) +public class JacksonAnnotationBean { + + private String Name; + + private int Age; + + @JsonIgnore + private String Sex; + + public JacksonAnnotationBean() { + } + + public JacksonAnnotationBean(String name, int age, String sex) { + Name = name; + Age = age; + Sex = sex; + } + + @JsonProperty("username") + public String getName() { + return Name; + } + + public void setName(String name) { + Name = name; + } + + public int getAge() { + return Age; + } + + public void setAge(int age) { + Age = age; + } + + public String getSex() { + return Sex; + } + + public void setSex(String sex) { + Sex = sex; + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/jackson/JacksonPerformanceTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/jackson/JacksonPerformanceTests.java new file mode 100644 index 00000000..e8efe4f8 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/jackson/JacksonPerformanceTests.java @@ -0,0 +1,53 @@ +package io.github.dunwu.javatech.seriralize.json.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.dunwu.javatech.bean.sample.TestBean2; +import io.github.dunwu.javatech.util.BeanUtils; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Jackson 性能测试 + * + * @author Zhang Peng + * @since 2019-03-18 + */ +public class JacksonPerformanceTests { + + private static final int BATCH_SIZE = 100000; + + private final ObjectMapper mapper = new ObjectMapper(); + + /** + * 测试十次,每次序列化、反序列化 100000 条数据,平均耗时约 334 ms + */ + @Test + public void testPerformance() throws IOException { + long time = 0L; + for (int i = 0; i < 10; i++) { + time += donSerializeAndDeserialize(); + } + System.out.println(String.format("time: %d ms", TimeUnit.NANOSECONDS.toMillis(time / 10))); + } + + /** + * 循环序列化、反序列 {@link #BATCH_SIZE} 条数据,测试性能 + */ + private long donSerializeAndDeserialize() throws IOException { + TestBean2 bean = BeanUtils.initNotJdk8Bean(); + long begin = System.nanoTime(); + for (int i = 0; i < BATCH_SIZE; i++) { + String json = mapper.writeValueAsString(bean); + assertThat(json).isNotBlank(); + TestBean2 newBean = mapper.readValue(json, TestBean2.class); + assertThat(newBean).isNotNull(); + } + long end = System.nanoTime(); + return (end - begin); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/jackson/JacksonTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/jackson/JacksonTests.java new file mode 100644 index 00000000..884496db --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/seriralize/json/jackson/JacksonTests.java @@ -0,0 +1,101 @@ +package io.github.dunwu.javatech.seriralize.json.jackson; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.dunwu.javatech.bean.sample.Person; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +/** + * Jackson 使用示例 + * + * @author Zhang Peng + * @since 2019-03-18 + */ +public class JacksonTests { + + final ObjectMapper mapper = new ObjectMapper(); + + /** + * 序列化测试 + */ + @Test + public void serialize() { + Person p = new Person("Tom", 20); + String json = null; + try { + json = mapper.writeValueAsString(p); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + Assertions.assertNotNull(json); + System.out.println("json = [" + json + "]"); + } + + /** + * 反序列化测试 + */ + @Test + public void deserialize() { + final String json = "{\"age\":20,\"name\":\"Tom\"}"; + Person p = null; + try { + p = mapper.readValue(json, Person.class); + } catch (IOException e) { + e.printStackTrace(); + } + Assertions.assertNotNull(p); + System.out.println("p = [" + p + "]"); + } + + /** + * 序列化测试 + */ + @Test + public void serialize2() { + Person p = new Person("Tom", 20); + Person p2 = new Person("Jack", 22); + Person p3 = new Person("Mary", 18); + + List persons = new LinkedList<>(); + persons.add(p); + persons.add(p2); + persons.add(p3); + + Map map = new HashMap<>(); + map.put("persons", persons); + + String json = null; + try { + json = mapper.writeValueAsString(map); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + + Assertions.assertNotNull(json); + System.out.println("json = [" + json + "]"); + } + + /** + * 序列化测试 + */ + @Test + public void serialize3() { + JacksonAnnotationBean jacksonAnnotationBean = new JacksonAnnotationBean("jack", 19, "男"); + String json = null; + try { + json = mapper.writeValueAsString(jacksonAnnotationBean); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + Assertions.assertNotNull(json); + System.out.println("json = [" + json + "]"); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit4/JUnitTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit4/JUnitTest.java new file mode 100644 index 00000000..ef852879 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit4/JUnitTest.java @@ -0,0 +1,71 @@ +package io.github.dunwu.javatech.test.junit4; + +import org.junit.*; +import org.junit.runners.MethodSorters; + +/** + * JUnit 使用示例。 请注意各个方法的执行顺序。 + * + * @author Zhang Peng + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class JUnitTest { + + /** + * @BeforeClass 注解指出这是附着在静态方法必须执行一次并在类的所有测试之前。 一般用于共享配置方法(如连接到数据库)。 + */ + @BeforeClass + public static void beforeClass() { + System.out.println("call @BeforeClass"); + } + + /** + * 当需要执行所有的测试在JUnit测试用例类后执行,@AfterClass 注解可以使用以清理建立方法,(从数据库如断开连接)。 注意:附有此批注(类似于BeforeClass)的方法必须定义为静态。 + */ + @AfterClass + public static void afterClass() { + System.out.println("call @AfterClass"); + } + + @Test + public void testA() { + System.out.println("call @Test testA"); + int sum = 1 + 2 + 3; + Assert.assertEquals(6, sum); + } + + @Test + public void testC() { + System.out.println("call @Test testC"); + } + + @Test + public void testB() { + System.out.println("call @Test testB"); + } + + /** + * @Before 注解修饰的方法必须在类中的每个测试之前执行,以便执行测试某些必要的先决条件。 + */ + @Before + public void before() { + System.out.println("call @Before"); + } + + /** + * @After 注解修饰的方法在执行每项测试后执行(如执行每一个测试后重置某些变量 , 删除临时变量等) + */ + @After + public void after() { + System.out.println("call @After"); + } + + /** + * 当想暂时禁用特定的测试执行可以使用忽略注释。每个被注解为 @Ignore 的方法将不被执行。 + */ + @Ignore + public void ignore() { + System.out.println("call @Ignore"); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/AassertjAssertionsTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/AassertjAssertionsTests.java new file mode 100644 index 00000000..7abd1e3a --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/AassertjAssertionsTests.java @@ -0,0 +1,26 @@ +package io.github.dunwu.javatech.test.junit5; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * assertj Assertions 示例 + * @author Zhang Peng + * @date 2022-07-29 + */ +public class AassertjAssertionsTests { + + @Test + void exceptionTesting() { + Throwable exception = Assertions.catchThrowable(() -> { + throw new IllegalArgumentException("a message"); + }); + Assertions.assertThat(exception.getMessage()).isEqualTo("a message"); + } + + @Test + void standardAssertions() { + Assertions.assertThat(2).isEqualTo(2); + Assertions.assertThat('a').isLessThan('b'); + } +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/AssertionsTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/AssertionsTests.java new file mode 100644 index 00000000..d5b17dfe --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/AssertionsTests.java @@ -0,0 +1,151 @@ +package io.github.dunwu.javatech.test.junit5; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static java.time.Duration.ofMillis; +import static java.time.Duration.ofMinutes; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Junit5 断言示例 + * + * @author Zhang Peng + * @since 2018-11-29 + */ +@Disabled +class AssertionsTests { + + private static Person person; + + @BeforeAll + static void beforeAll() { + person = new Person("John", "Doe"); + } + + @Test + void dependentAssertions() { + // Within a code block, if an assertion fails the + // subsequent code in the same block will be skipped. + assertAll("properties", () -> { + String firstName = person.getFirstName(); + assertNotNull(firstName); + + // Executed only if the previous assertion is valid. + assertAll("first name", () -> assertTrue(firstName.startsWith("J")), + () -> assertTrue(firstName.endsWith("n"))); + }, () -> { + // Grouped assertion, so processed independently + // of results of first name assertions. + String lastName = person.getLastName(); + assertNotNull(lastName); + + // Executed only if the previous assertion is valid. + assertAll("last name", () -> assertTrue(lastName.startsWith("D")), + () -> assertTrue(lastName.endsWith("e"))); + }); + } + + @Test + void exceptionTesting() { + Throwable exception = assertThrows(IllegalArgumentException.class, () -> { + throw new IllegalArgumentException("a message"); + }); + assertEquals("a message", exception.getMessage()); + } + + @Test + void groupedAssertions() { + // In a grouped assertion all assertions are executed, and any + // failures will be reported together. + assertAll("person", () -> assertEquals("John", person.getFirstName()), + () -> assertEquals("Doe", person.getLastName())); + } + + @Test + void standardAssertions() { + assertEquals(2, 2); + assertEquals(4, 4, "The optional assertion message is now the last parameter."); + assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- " + + "to avoid constructing complex messages unnecessarily."); + } + + @Test + void timeoutExceeded() { + // The following assertion fails with an error message similar to: + // execution exceeded timeout of 10 ms by 91 ms + assertTimeout(ofMillis(10), () -> { + // Simulate task that takes more than 10 ms. + Thread.sleep(100); + }); + } + + @Test + void timeoutExceededWithPreemptiveTermination() { + // The following assertion fails with an error message similar to: + // execution timed out after 10 ms + assertTimeoutPreemptively(ofMillis(10), () -> { + // Simulate task that takes more than 10 ms. + Thread.sleep(100); + }); + } + + @Test + void timeoutNotExceeded() { + // The following assertion succeeds. + assertTimeout(ofMinutes(2), () -> { + // Perform task that takes less than 2 minutes. + }); + } + + @Test + void timeoutNotExceededWithMethod() { + // The following assertion invokes a method reference and returns an object. + String actualGreeting = assertTimeout(ofMinutes(2), AssertionsTests::greeting); + assertEquals("Hello, World!", actualGreeting); + } + + private static String greeting() { + return "Hello, World!"; + } + + @Test + void timeoutNotExceededWithResult() { + // The following assertion succeeds, and returns the supplied object. + String actualResult = assertTimeout(ofMinutes(2), () -> { + return "a result"; + }); + assertEquals("a result", actualResult); + } + + static class Person { + + private String firstName; + + private String lastName; + + Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + String getFirstName() { + return firstName; + } + + void setFirstName(String firstName) { + this.firstName = firstName; + } + + String getLastName() { + return lastName; + } + + void setLastName(String lastName) { + this.lastName = lastName; + } + + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/AssumptionsTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/AssumptionsTests.java new file mode 100644 index 00000000..01527245 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/AssumptionsTests.java @@ -0,0 +1,42 @@ +package io.github.dunwu.javatech.test.junit5; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.Assumptions.assumingThat; + +/** + * Junit5 断言示例 + * + * @author Zhang Peng + * @since 2018-11-29 + */ +@Disabled +class AssumptionsTests { + + @Test + void testInAllEnvironments() { + assumingThat("CI".equals(System.getenv("ENV")), () -> { + // perform these assertions only on the CI server + assertEquals(2, 2); + }); + + // perform these assertions in all environments + assertEquals("a string", "a string"); + } + + @Test + void testOnlyOnCiServer() { + assumeTrue("CI".equals(System.getenv("ENV"))); + // remainder of test + } + + @Test + void testOnlyOnDeveloperWorkstation() { + assumeTrue("DEV".equals(System.getenv("ENV")), () -> "Aborting test: not on developer workstation"); + // remainder of test + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/DisplayNameTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/DisplayNameTests.java new file mode 100644 index 00000000..b85bcd47 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/DisplayNameTests.java @@ -0,0 +1,32 @@ +package io.github.dunwu.javatech.test.junit5; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** + * Junit5 定制测试类和方法的显示名称 + * + * @author Zhang Peng + * @since 2018-11-29 + */ +@Disabled +@DisplayName("A special test case") +class DisplayNameTests { + + @Test + @DisplayName("😱") + void testWithDisplayNameContainingEmoji() { + } + + @Test + @DisplayName("Custom test name containing spaces") + void testWithDisplayNameContainingSpaces() { + } + + @Test + @DisplayName("╯°□°)╯") + void testWithDisplayNameContainingSpecialCharacters() { + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/DynamicTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/DynamicTests.java new file mode 100644 index 00000000..b464d840 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/DynamicTests.java @@ -0,0 +1,111 @@ +package io.github.dunwu.javatech.test.junit5; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.function.ThrowingConsumer; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +@Disabled +class DynamicTests { + + @TestFactory + DynamicTest[] dynamicTestsFromArray() { + return new DynamicTest[] { dynamicTest("7th dynamic test", () -> assertTrue(true)), + dynamicTest("8th dynamic test", () -> assertEquals(4, 2 * 2)) }; + } + + @TestFactory + Collection dynamicTestsFromCollection() { + return Arrays.asList(dynamicTest("1st dynamic test", () -> assertTrue(true)), + dynamicTest("2nd dynamic test", () -> assertEquals(4, 2 * 2))); + } + + @TestFactory + Stream dynamicTestsFromIntStream() { + // Generates tests for the first 10 even integers. + return IntStream.iterate(0, n -> n + 2) + .limit(10) + .mapToObj(n -> dynamicTest("test" + n, () -> assertEquals(0, n % 2))); + } + + @TestFactory + Iterable dynamicTestsFromIterable() { + return Arrays.asList(dynamicTest("3rd dynamic test", () -> assertTrue(true)), + dynamicTest("4th dynamic test", () -> assertEquals(4, 2 * 2))); + } + + @TestFactory + Iterator dynamicTestsFromIterator() { + return Arrays.asList(dynamicTest("5th dynamic test", () -> assertTrue(true)), + dynamicTest("6th dynamic test", () -> assertEquals(4, 2 * 2))).iterator(); + } + + @TestFactory + Stream dynamicTestsFromStream() { + return Stream.of("A", "B", "C").map(str -> dynamicTest("test" + str, () -> { + /* ... */ + })); + } + + @TestFactory + Stream dynamicTestsWithContainers() { + return Stream.of("A", "B", "C") + .map(input -> dynamicContainer("Container " + input, + Stream.of(dynamicTest("not null", () -> assertNotNull(input)), + dynamicContainer("properties", Stream.of( + dynamicTest("length > 0", + () -> assertTrue(input.length() > 0)), + dynamicTest("not empty", + () -> assertFalse(input.isEmpty()))))))); + } + + // This will result in a JUnitException! + @TestFactory + List dynamicTestsWithInvalidReturnType() { + return Arrays.asList("Hello"); + } + + @TestFactory + Stream generateRandomNumberOfTests() { + + // Generates random positive integers between 0 and 100 until + // a number evenly divisible by 7 is encountered. + Iterator inputGenerator = new Iterator() { + + Random random = new Random(); + + int current; + + @Override + public boolean hasNext() { + current = random.nextInt(100); + return current % 7 != 0; + } + + @Override + public Integer next() { + return current; + } + }; + + // Generates display names like: input:5, input:37, input:85, etc. + Function displayNameGenerator = (input) -> "input:" + input; + + // Executes tests based on the current input value. + ThrowingConsumer testExecutor = (input) -> assertTrue(input % 7 != 0); + + // Returns a stream of dynamic tests. + return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/NestedTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/NestedTests.java new file mode 100644 index 00000000..9a912779 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/NestedTests.java @@ -0,0 +1,84 @@ +package io.github.dunwu.javatech.test.junit5; + +import org.junit.jupiter.api.*; + +import java.util.EmptyStackException; +import java.util.Stack; + +import static org.junit.jupiter.api.Assertions.*; + +@Disabled +@DisplayName("A stack") +class NestedTests { + + Stack stack; + + @Test + @DisplayName("is instantiated with new Stack()") + void isInstantiatedWithNew() { + new Stack<>(); + } + + @Nested + @DisplayName("when new") + class WhenNew { + + @BeforeEach + void createNewStack() { + stack = new Stack<>(); + } + + @Test + @DisplayName("is empty") + void isEmpty() { + assertTrue(stack.isEmpty()); + } + + @Test + @DisplayName("throws EmptyStackException when peeked") + void throwsExceptionWhenPeeked() { + assertThrows(EmptyStackException.class, () -> stack.peek()); + } + + @Test + @DisplayName("throws EmptyStackException when popped") + void throwsExceptionWhenPopped() { + assertThrows(EmptyStackException.class, () -> stack.pop()); + } + + @Nested + @DisplayName("after pushing an element") + class AfterPushing { + + String anElement = "an element"; + + @Test + @DisplayName("it is no longer empty") + void isNotEmpty() { + assertFalse(stack.isEmpty()); + } + + @BeforeEach + void pushAnElement() { + stack.push(anElement); + } + + @Test + @DisplayName("returns the element when peeked but remains not empty") + void returnElementWhenPeeked() { + assertEquals(anElement, stack.peek()); + assertFalse(stack.isEmpty()); + } + + @Test + @DisplayName("returns the element when popped and is empty") + void returnElementWhenPopped() { + assertEquals(anElement, stack.pop()); + assertTrue(stack.isEmpty()); + } + + } + + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/ParameterizedTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/ParameterizedTests.java new file mode 100644 index 00000000..860cb28d --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/ParameterizedTests.java @@ -0,0 +1,32 @@ +package io.github.dunwu.javatech.test.junit5; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Zhang Peng + * @since 2018-11-29 + */ +@Disabled +class ParameterizedTests { + + @ParameterizedTest(name = "{0} + {1} = {2}") + @CsvSource({ "0, 1, 1", "1, 2, 3", "49, 51, 100", "1, 100, 101" }) + void add(int first, int second, int expectedResult) { + Calculator calculator = new Calculator(); + assertEquals(expectedResult, calculator.add(first, second), + () -> first + " + " + second + " should equal " + expectedResult); + } + + class Calculator { + + public int add(int a, int b) { + return a + b; + } + + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/RepeatedTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/RepeatedTests.java new file mode 100644 index 00000000..99072fe5 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/RepeatedTests.java @@ -0,0 +1,46 @@ +package io.github.dunwu.javatech.test.junit5; + +import org.junit.jupiter.api.*; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Disabled +class RepeatedTests { + + @BeforeEach + void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) { + int currentRepetition = repetitionInfo.getCurrentRepetition(); + int totalRepetitions = repetitionInfo.getTotalRepetitions(); + String methodName = testInfo.getTestMethod().get().getName(); + System.out.printf("About to execute repetition %d of %d for %s%n", // + currentRepetition, totalRepetitions, methodName); + } + + @RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}") + @DisplayName("Repeat!") + void customDisplayName(TestInfo testInfo) { + assertEquals(testInfo.getDisplayName(), "Repeat! 1/1"); + } + + @RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME) + @DisplayName("Details...") + void customDisplayNameWithLongPattern(TestInfo testInfo) { + assertEquals(testInfo.getDisplayName(), "Details... :: repetition 1 of 1"); + } + + @RepeatedTest(10) + void repeatedTest() { + // ... + } + + @RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}") + void repeatedTestInGerman() { + // ... + } + + @RepeatedTest(5) + void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) { + assertEquals(5, repetitionInfo.getTotalRepetitions()); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/StandardTests.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/StandardTests.java new file mode 100644 index 00000000..e11aa491 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5/StandardTests.java @@ -0,0 +1,52 @@ +package io.github.dunwu.javatech.test.junit5; + +import org.junit.jupiter.api.*; + +/** + * Junit5 标准测试 + * + * @author Zhang Peng + * @since 2018-11-29 + */ +@Disabled +class StandardTests { + + @AfterAll + static void afterAll() { + System.out.println("call afterAll()"); + } + + @BeforeAll + static void beforeAll() { + System.out.println("call beforeAll()"); + } + + @AfterEach + void afterEach() { + System.out.println("call afterEach()"); + } + + @BeforeEach + void beforeEach() { + System.out.println("call beforeEach()"); + } + + @Test + void failingTest() { + System.out.println("call failingTest()"); + // fail("a failing test"); + } + + @Test + @Disabled("for demonstration purposes") + void skippedTest() { + System.out.println("call skippedTest()"); + // not executed + } + + @Test + void succeedingTest() { + System.out.println("call succeedingTest()"); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/mockito/MockitoTest.java b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/mockito/MockitoTest.java new file mode 100644 index 00000000..ddc87549 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/mockito/MockitoTest.java @@ -0,0 +1,225 @@ +package io.github.dunwu.javatech.test.mockito; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +import java.util.LinkedList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.*; + +class MockitoTest { + + // 模拟 LinkedList 的一个对象 + LinkedList mockedList = mock(LinkedList.class); + + @BeforeEach + void beforeEach() { + mockedList.clear(); + } + + @Test + void test() { + // using mock object - it does not throw any "unexpected interaction" exception + mockedList.add("one"); + // selective, explicit, highly readable verification + verify(mockedList).add("one"); + } + + /** + * 模拟对象 + */ + @Test + void test01() { + // 此时调用get方法,会返回null,因为还没有对方法调用的返回值做模拟 + System.out.println(mockedList.get(0)); + } + + /** + * 模拟方法调用的返回值 + */ + @Test + void test02() { + // 模拟获取第一个元素时,返回字符串first。给特定的方法调用返回固定值在官方说法中称为stub。 + when(mockedList.get(0)).thenReturn("first"); + // 此时打印输出first + System.out.println(mockedList.get(0)); + } + + /** + * 模拟方法调用抛出异常 + */ + @Test + void test03() { + // 模拟获取第二个元素时,抛出RuntimeException + when(mockedList.get(1)).thenThrow(new RuntimeException()); + try { + // 此时将会抛出RuntimeException + System.out.println(mockedList.get(1)); + } catch (RuntimeException e) { + System.err.println(e.getMessage()); + assertThat(e.getMessage()).isEqualTo(null); + } + } + + /** + * 模拟方法调用抛出异常2 + */ + @Test + void test04() { + doThrow(new RuntimeException("clear exception")).when(mockedList).clear(); + try { + mockedList.clear(); + } catch (RuntimeException e) { + System.err.println(e.getMessage()); + assertThat(e.getMessage()).contains("clear exception"); + } + } + + /** + * 模拟调用方法时的参数匹配 + */ + @Test + void test05() { + // anyInt()匹配任何int参数,这意味着参数为任意值,其返回值均是element + when(mockedList.get(anyInt())).thenReturn("element"); + // 此时打印是element + System.out.println(mockedList.get(999)); + } + + /** + * 模拟方法调用次数 + */ + @Test + void test06() { + // 调用add一次 + mockedList.add("once"); + // 下面两个写法验证效果一样,均验证add方法是否被调用了一次 + verify(mockedList).add("once"); + verify(mockedList, times(1)).add("once"); + } + + /** + * 校验行为 + */ + @Test + void test07() { + // using mock object + mockedList.add("one"); + // verification + verify(mockedList).add("one"); + verify(mockedList).clear(); + } + + /** + * 模拟方法调用(Stubbing) + */ + @Test + void test08() { + // stubbing + when(mockedList.get(0)).thenReturn("first"); + when(mockedList.get(1)).thenThrow(new RuntimeException()); + // following prints "first" + System.out.println(mockedList.get(0)); + try { + // following throws runtime exception + System.out.println(mockedList.get(1)); + } catch (RuntimeException e) { + System.out.println(e.getMessage()); + } + // following prints "null" because get(999) was not stubbed + System.out.println(mockedList.get(999)); + + verify(mockedList).get(0); + } + + /** + * 校验方法调用次数 + */ + @Test + void test09() { + // using mock + mockedList.add("once"); + + mockedList.add("twice"); + mockedList.add("twice"); + + mockedList.add("three"); + mockedList.add("three"); + mockedList.add("three"); + // following two verifications work exactly the same - times(1) is used by default + verify(mockedList).add("once"); + verify(mockedList, times(1)).add("once"); + // exact number of invocations verification + verify(mockedList, times(2)).add("twice"); + verify(mockedList, times(3)).add("three"); + // verification using never(). never() is an alias to times(0) + verify(mockedList, never()).add("never happened"); + // verification using atLeast()/atMost() + verify(mockedList, atLeastOnce()).add("three"); + verify(mockedList, atLeast(2)).add("twice"); + verify(mockedList, atMost(5)).add("three"); + } + + /** + * 校验方法调用顺序 + */ + @Test + void test10() { + // A. Single mock whose methods must be invoked in a particular order + List singleMock = mock(List.class); + // using a single mock + singleMock.add("was added first"); + singleMock.add("was added second"); + // create an inOrder verifier for a single mock + InOrder inOrder = inOrder(singleMock); + // following will make sure that add is first called with "was added first, then + // with "was added second" + inOrder.verify(singleMock).add("was added first"); + inOrder.verify(singleMock).add("was added second"); + + // B. Multiple mocks that must be used in a particular order + List firstMock = mock(List.class); + List secondMock = mock(List.class); + // using mocks + firstMock.add("was called first"); + secondMock.add("was called second"); + // create inOrder object passing any mocks that need to be verified in order + inOrder = inOrder(firstMock, secondMock); + // following will make sure that firstMock was called before secondMock + inOrder.verify(firstMock).add("was called first"); + inOrder.verify(secondMock).add("was called second"); + // Oh, and A + B can be mixed together at will + } + + /** + * 校验方法是否从未调用 + */ + @Test + void test11() { + List mockOne = mock(List.class); + List mockTwo = mock(List.class); + List mockThree = mock(List.class); + // using mocks - only mockOne is interacted + mockOne.add("one"); + // ordinary verification + verify(mockOne).add("one"); + // verify that method was never called on a mock + verify(mockOne, never()).add("two"); + // verify that other mocks were not interacted + verifyZeroInteractions(mockTwo, mockThree); + } + + /** + * 重置Mock + */ + void test12() { + List mock = mock(List.class); + when(mock.size()).thenReturn(10); + mock.add(1); + reset(mock); + } + +} diff --git a/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/inner/resource2-reflections.xml b/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/inner/resource2-reflections.xml new file mode 100644 index 00000000..919f565e --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/inner/resource2-reflections.xml @@ -0,0 +1,13 @@ + + + + + + io.github.dunwu.javatech.reflections.TestModel$AF1 + + io.github.dunwu.javatech.reflections.TestModel$C4.f1 + io.github.dunwu.javatech.reflections.TestModel$C4.f2 + + + + diff --git a/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/resource1-reflections.xml b/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/resource1-reflections.xml new file mode 100644 index 00000000..e2154b4a --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/resource1-reflections.xml @@ -0,0 +1,16 @@ + + + + + + io.github.dunwu.javatech.reflections.TestModel$AM1 + + io.github.dunwu.javatech.reflections.TestModel$C4.<init>(java.lang.String) + io.github.dunwu.javatech.reflections.TestModel$C4.m3() + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][]) + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[]) + io.github.dunwu.javatech.reflections.TestModel$C4.m1() + + + + diff --git a/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/saved-testModel-reflections.json b/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/saved-testModel-reflections.json new file mode 100644 index 00000000..fa047d1c --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/saved-testModel-reflections.json @@ -0,0 +1,200 @@ +{ + "store": { + "TypesAnnotated": { + "io.github.dunwu.javatech.reflections.TestModel$AC3": [ + "io.github.dunwu.javatech.reflections.TestModel$C7" + ], + "io.github.dunwu.javatech.reflections.TestModel$MAI1": [ + "io.github.dunwu.javatech.reflections.TestModel$AI1" + ], + "io.github.dunwu.javatech.reflections.TestModel$AC2": [ + "io.github.dunwu.javatech.reflections.TestModel$C3", + "io.github.dunwu.javatech.reflections.TestModel$C2", + "io.github.dunwu.javatech.reflections.TestModel$AC3", + "io.github.dunwu.javatech.reflections.TestModel$I3" + ], + "io.github.dunwu.javatech.reflections.TestModel$AC1n": [ + "io.github.dunwu.javatech.reflections.TestModel$C1" + ], + "io.github.dunwu.javatech.reflections.TestModel$AC1": [ + "io.github.dunwu.javatech.reflections.TestModel$C1" + ], + "io.github.dunwu.javatech.reflections.TestModel$AI2": [ + "io.github.dunwu.javatech.reflections.TestModel$I2" + ], + "java.lang.annotation.Inherited": [ + "io.github.dunwu.javatech.reflections.TestModel$MAI1", + "io.github.dunwu.javatech.reflections.TestModel$AC1", + "io.github.dunwu.javatech.reflections.TestModel$AI2" + ], + "io.github.dunwu.javatech.reflections.TestModel$AI1": [ + "io.github.dunwu.javatech.reflections.TestModel$I1" + ], + "java.lang.annotation.Retention": [ + "io.github.dunwu.javatech.reflections.TestModel$AC3", + "io.github.dunwu.javatech.reflections.TestModel$MAI1", + "io.github.dunwu.javatech.reflections.TestModel$AC1n", + "io.github.dunwu.javatech.reflections.TestModel$AC2", + "io.github.dunwu.javatech.reflections.TestModel$AC1", + "io.github.dunwu.javatech.reflections.TestModel$AI2", + "io.github.dunwu.javatech.reflections.TestModel$AI1", + "io.github.dunwu.javatech.reflections.TestModel$AF1", + "io.github.dunwu.javatech.reflections.TestModel$AM1" + ] + }, + "MethodsSignature": { + "[io.github.dunwu.javatech.reflections.TestModel$C2]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.c2toC3(io.github.dunwu.javatech.reflections.TestModel$C2)" + ], + "[]": [ + "io.github.dunwu.javatech.reflections.TestModel$C7.\u003cinit\u003e()", + "io.github.dunwu.javatech.reflections.TestModel$AF1.value()", + "io.github.dunwu.javatech.reflections.TestModel$C4.m1()", + "io.github.dunwu.javatech.reflections.TestModel$C4.\u003cinit\u003e()", + "io.github.dunwu.javatech.reflections.TestModel$C2.\u003cinit\u003e()", + "io.github.dunwu.javatech.reflections.TestModel$C4.m3()", + "io.github.dunwu.javatech.reflections.TestModel$C3.\u003cinit\u003e()", + "io.github.dunwu.javatech.reflections.TestModel$C6.\u003cinit\u003e()", + "io.github.dunwu.javatech.reflections.TestModel$C5.\u003cinit\u003e()", + "io.github.dunwu.javatech.reflections.TestModel$AM1.value()", + "io.github.dunwu.javatech.reflections.TestModel$AC2.value()", + "io.github.dunwu.javatech.reflections.TestModel$C1.\u003cinit\u003e()" + ], + "[int, java.lang.String[]]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[])" + ], + "[int, int]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.add(int, int)" + ], + "[java.lang.String]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String)", + "io.github.dunwu.javatech.reflections.TestModel$C4.\u003cinit\u003e(java.lang.String)" + ], + "[int[][], java.lang.String[][]]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][])" + ] + }, + "MethodsParameter": { + "io.github.dunwu.javatech.reflections.TestModel$C2": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.c2toC3(io.github.dunwu.javatech.reflections.TestModel$C2)" + ], + "int[][]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][])" + ], + "java.lang.String[][]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][])" + ], + "java.lang.String": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String)", + "io.github.dunwu.javatech.reflections.TestModel$C4.\u003cinit\u003e(java.lang.String)" + ], + "java.lang.String[]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[])" + ], + "io.github.dunwu.javatech.reflections.TestModel$AM1": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String)", + "io.github.dunwu.javatech.reflections.TestModel$C4.\u003cinit\u003e(java.lang.String)" + ], + "int": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[])", + "io.github.dunwu.javatech.reflections.TestModel$C4.add(int, int)" + ] + }, + "SubTypes": { + "io.github.dunwu.javatech.reflections.TestModel$C3": [ + "io.github.dunwu.javatech.reflections.TestModel$C5" + ], + "io.github.dunwu.javatech.reflections.TestModel$C1": [ + "io.github.dunwu.javatech.reflections.TestModel$C3", + "io.github.dunwu.javatech.reflections.TestModel$C2" + ], + "java.lang.annotation.Annotation": [ + "io.github.dunwu.javatech.reflections.TestModel$AC3", + "io.github.dunwu.javatech.reflections.TestModel$MAI1", + "io.github.dunwu.javatech.reflections.TestModel$AC2", + "io.github.dunwu.javatech.reflections.TestModel$AC1n", + "io.github.dunwu.javatech.reflections.TestModel$AC1", + "io.github.dunwu.javatech.reflections.TestModel$AI2", + "io.github.dunwu.javatech.reflections.TestModel$AI1", + "io.github.dunwu.javatech.reflections.TestModel$AF1", + "io.github.dunwu.javatech.reflections.TestModel$AM1" + ], + "io.github.dunwu.javatech.reflections.TestModel$I1": [ + "io.github.dunwu.javatech.reflections.TestModel$I2" + ], + "io.github.dunwu.javatech.reflections.TestModel$I3": [ + "io.github.dunwu.javatech.reflections.TestModel$C6" + ], + "java.lang.Object": [ + "io.github.dunwu.javatech.reflections.TestModel$AI2", + "io.github.dunwu.javatech.reflections.TestModel$AI1", + "io.github.dunwu.javatech.reflections.TestModel$AF1", + "io.github.dunwu.javatech.reflections.TestModel$C7", + "io.github.dunwu.javatech.reflections.TestModel$AM1", + "io.github.dunwu.javatech.reflections.TestModel$C6", + "io.github.dunwu.javatech.reflections.TestModel$AC3", + "io.github.dunwu.javatech.reflections.TestModel$C4", + "io.github.dunwu.javatech.reflections.TestModel$MAI1", + "io.github.dunwu.javatech.reflections.TestModel$AC2", + "io.github.dunwu.javatech.reflections.TestModel$C1", + "io.github.dunwu.javatech.reflections.TestModel$AC1n", + "io.github.dunwu.javatech.reflections.TestModel$AC1", + "io.github.dunwu.javatech.reflections.TestModel$I1", + "io.github.dunwu.javatech.reflections.TestModel$I3", + "io.github.dunwu.javatech.reflections.TestModel$I2" + ], + "io.github.dunwu.javatech.reflections.TestModel$I2": [ + "io.github.dunwu.javatech.reflections.TestModel$C1" + ] + }, + "MethodsReturn": { + "io.github.dunwu.javatech.reflections.TestModel$C3": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.c2toC3(io.github.dunwu.javatech.reflections.TestModel$C2)" + ], + "void": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[])", + "io.github.dunwu.javatech.reflections.TestModel$C4.m1()", + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][])" + ], + "java.lang.String": [ + "io.github.dunwu.javatech.reflections.TestModel$AF1.value()", + "io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String)", + "io.github.dunwu.javatech.reflections.TestModel$C4.m3()", + "io.github.dunwu.javatech.reflections.TestModel$AM1.value()", + "io.github.dunwu.javatech.reflections.TestModel$AC2.value()" + ], + "int": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.add(int, int)" + ] + }, + "MethodsAnnotated": { + "io.github.dunwu.javatech.reflections.TestModel$AM1": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[])", + "io.github.dunwu.javatech.reflections.TestModel$C4.m1()", + "io.github.dunwu.javatech.reflections.TestModel$C4.m3()", + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][])", + "io.github.dunwu.javatech.reflections.TestModel$C4.\u003cinit\u003e(java.lang.String)" + ] + }, + "FieldsAnnotated": { + "io.github.dunwu.javatech.reflections.TestModel$AF1": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.f1", + "io.github.dunwu.javatech.reflections.TestModel$C4.f2" + ] + }, + "Resources": { + "resource1-reflections.xml": [ + "META-INF/reflections/resource1-reflections.xml" + ], + "saved-testModel-reflections.xml": [ + "META-INF/reflections/saved-testModel-reflections.xml" + ], + "resource2-reflections.xml": [ + "META-INF/reflections/inner/resource2-reflections.xml" + ], + "testModel-reflections.xml": [ + "META-INF/reflections/testModel-reflections.xml" + ] + } + } +} diff --git a/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/saved-testModel-reflections.xml b/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/saved-testModel-reflections.xml new file mode 100644 index 00000000..4acbc004 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/saved-testModel-reflections.xml @@ -0,0 +1,317 @@ + + + + + + io.github.dunwu.javatech.reflections.TestModel$AC3 + + io.github.dunwu.javatech.reflections.TestModel$C7 + + + + io.github.dunwu.javatech.reflections.TestModel$MAI1 + + io.github.dunwu.javatech.reflections.TestModel$AI1 + + + + io.github.dunwu.javatech.reflections.TestModel$AC2 + + io.github.dunwu.javatech.reflections.TestModel$C3 + io.github.dunwu.javatech.reflections.TestModel$C2 + io.github.dunwu.javatech.reflections.TestModel$AC3 + io.github.dunwu.javatech.reflections.TestModel$I3 + + + + io.github.dunwu.javatech.reflections.TestModel$AC1n + + io.github.dunwu.javatech.reflections.TestModel$C1 + + + + io.github.dunwu.javatech.reflections.TestModel$AC1 + + io.github.dunwu.javatech.reflections.TestModel$C1 + + + + io.github.dunwu.javatech.reflections.TestModel$AI2 + + io.github.dunwu.javatech.reflections.TestModel$I2 + + + + java.lang.annotation.Inherited + + io.github.dunwu.javatech.reflections.TestModel$MAI1 + io.github.dunwu.javatech.reflections.TestModel$AC1 + io.github.dunwu.javatech.reflections.TestModel$AI2 + + + + io.github.dunwu.javatech.reflections.TestModel$AI1 + + io.github.dunwu.javatech.reflections.TestModel$I1 + + + + java.lang.annotation.Retention + + io.github.dunwu.javatech.reflections.TestModel$AC3 + io.github.dunwu.javatech.reflections.TestModel$MAI1 + io.github.dunwu.javatech.reflections.TestModel$AC1n + io.github.dunwu.javatech.reflections.TestModel$AC2 + io.github.dunwu.javatech.reflections.TestModel$AC1 + io.github.dunwu.javatech.reflections.TestModel$AI2 + io.github.dunwu.javatech.reflections.TestModel$AI1 + io.github.dunwu.javatech.reflections.TestModel$AF1 + io.github.dunwu.javatech.reflections.TestModel$AM1 + + + + + + [io.github.dunwu.javatech.reflections.TestModel$C2] + + io.github.dunwu.javatech.reflections.TestModel$C4.c2toC3(io.github.dunwu.javatech.reflections.TestModel$C2) + + + + [] + + io.github.dunwu.javatech.reflections.TestModel$C7.<init>() + io.github.dunwu.javatech.reflections.TestModel$AF1.value() + io.github.dunwu.javatech.reflections.TestModel$C4.m1() + io.github.dunwu.javatech.reflections.TestModel$C4.<init>() + io.github.dunwu.javatech.reflections.TestModel$C2.<init>() + io.github.dunwu.javatech.reflections.TestModel$C4.m3() + io.github.dunwu.javatech.reflections.TestModel$C3.<init>() + io.github.dunwu.javatech.reflections.TestModel$C6.<init>() + io.github.dunwu.javatech.reflections.TestModel$C5.<init>() + io.github.dunwu.javatech.reflections.TestModel$AM1.value() + io.github.dunwu.javatech.reflections.TestModel$AC2.value() + io.github.dunwu.javatech.reflections.TestModel$C1.<init>() + + + + [int, java.lang.String[]] + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[]) + + + + [int, int] + + io.github.dunwu.javatech.reflections.TestModel$C4.add(int, int) + + + + [java.lang.String] + + io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String) + io.github.dunwu.javatech.reflections.TestModel$C4.<init>(java.lang.String) + + + + [int[][], java.lang.String[][]] + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][]) + + + + + + io.github.dunwu.javatech.reflections.TestModel$C2 + + io.github.dunwu.javatech.reflections.TestModel$C4.c2toC3(io.github.dunwu.javatech.reflections.TestModel$C2) + + + + int[][] + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][]) + + + + java.lang.String[][] + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][]) + + + + java.lang.String + + io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String) + io.github.dunwu.javatech.reflections.TestModel$C4.<init>(java.lang.String) + + + + java.lang.String[] + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[]) + + + + io.github.dunwu.javatech.reflections.TestModel$AM1 + + io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String) + io.github.dunwu.javatech.reflections.TestModel$C4.<init>(java.lang.String) + + + + int + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[]) + io.github.dunwu.javatech.reflections.TestModel$C4.add(int, int) + + + + + + io.github.dunwu.javatech.reflections.TestModel$C3 + + io.github.dunwu.javatech.reflections.TestModel$C5 + + + + io.github.dunwu.javatech.reflections.TestModel$C1 + + io.github.dunwu.javatech.reflections.TestModel$C3 + io.github.dunwu.javatech.reflections.TestModel$C2 + + + + java.lang.annotation.Annotation + + io.github.dunwu.javatech.reflections.TestModel$AC3 + io.github.dunwu.javatech.reflections.TestModel$MAI1 + io.github.dunwu.javatech.reflections.TestModel$AC2 + io.github.dunwu.javatech.reflections.TestModel$AC1n + io.github.dunwu.javatech.reflections.TestModel$AC1 + io.github.dunwu.javatech.reflections.TestModel$AI2 + io.github.dunwu.javatech.reflections.TestModel$AI1 + io.github.dunwu.javatech.reflections.TestModel$AF1 + io.github.dunwu.javatech.reflections.TestModel$AM1 + + + + io.github.dunwu.javatech.reflections.TestModel$I1 + + io.github.dunwu.javatech.reflections.TestModel$I2 + + + + io.github.dunwu.javatech.reflections.TestModel$I3 + + io.github.dunwu.javatech.reflections.TestModel$C6 + + + + java.lang.Object + + io.github.dunwu.javatech.reflections.TestModel$AI2 + io.github.dunwu.javatech.reflections.TestModel$AI1 + io.github.dunwu.javatech.reflections.TestModel$AF1 + io.github.dunwu.javatech.reflections.TestModel$C7 + io.github.dunwu.javatech.reflections.TestModel$AM1 + io.github.dunwu.javatech.reflections.TestModel$C6 + io.github.dunwu.javatech.reflections.TestModel$AC3 + io.github.dunwu.javatech.reflections.TestModel$C4 + io.github.dunwu.javatech.reflections.TestModel$MAI1 + io.github.dunwu.javatech.reflections.TestModel$AC2 + io.github.dunwu.javatech.reflections.TestModel$C1 + io.github.dunwu.javatech.reflections.TestModel$AC1n + io.github.dunwu.javatech.reflections.TestModel$AC1 + io.github.dunwu.javatech.reflections.TestModel$I1 + io.github.dunwu.javatech.reflections.TestModel$I3 + io.github.dunwu.javatech.reflections.TestModel$I2 + + + + io.github.dunwu.javatech.reflections.TestModel$I2 + + io.github.dunwu.javatech.reflections.TestModel$C1 + + + + + + io.github.dunwu.javatech.reflections.TestModel$C3 + + io.github.dunwu.javatech.reflections.TestModel$C4.c2toC3(io.github.dunwu.javatech.reflections.TestModel$C2) + + + + void + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[]) + io.github.dunwu.javatech.reflections.TestModel$C4.m1() + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][]) + + + + java.lang.String + + io.github.dunwu.javatech.reflections.TestModel$AF1.value() + io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String) + io.github.dunwu.javatech.reflections.TestModel$C4.m3() + io.github.dunwu.javatech.reflections.TestModel$AM1.value() + io.github.dunwu.javatech.reflections.TestModel$AC2.value() + + + + int + + io.github.dunwu.javatech.reflections.TestModel$C4.add(int, int) + + + + + + io.github.dunwu.javatech.reflections.TestModel$AM1 + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[]) + io.github.dunwu.javatech.reflections.TestModel$C4.m1() + io.github.dunwu.javatech.reflections.TestModel$C4.m3() + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][]) + io.github.dunwu.javatech.reflections.TestModel$C4.<init>(java.lang.String) + + + + + + io.github.dunwu.javatech.reflections.TestModel$AF1 + + io.github.dunwu.javatech.reflections.TestModel$C4.f1 + io.github.dunwu.javatech.reflections.TestModel$C4.f2 + + + + + + resource1-reflections.xml + + META-INF/reflections/resource1-reflections.xml + + + + saved-testModel-reflections.xml + + META-INF/reflections/saved-testModel-reflections.xml + + + + resource2-reflections.xml + + META-INF/reflections/inner/resource2-reflections.xml + + + + testModel-reflections.xml + + META-INF/reflections/testModel-reflections.xml + + + + diff --git a/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/testModel-reflections.json b/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/testModel-reflections.json new file mode 100644 index 00000000..fa047d1c --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/testModel-reflections.json @@ -0,0 +1,200 @@ +{ + "store": { + "TypesAnnotated": { + "io.github.dunwu.javatech.reflections.TestModel$AC3": [ + "io.github.dunwu.javatech.reflections.TestModel$C7" + ], + "io.github.dunwu.javatech.reflections.TestModel$MAI1": [ + "io.github.dunwu.javatech.reflections.TestModel$AI1" + ], + "io.github.dunwu.javatech.reflections.TestModel$AC2": [ + "io.github.dunwu.javatech.reflections.TestModel$C3", + "io.github.dunwu.javatech.reflections.TestModel$C2", + "io.github.dunwu.javatech.reflections.TestModel$AC3", + "io.github.dunwu.javatech.reflections.TestModel$I3" + ], + "io.github.dunwu.javatech.reflections.TestModel$AC1n": [ + "io.github.dunwu.javatech.reflections.TestModel$C1" + ], + "io.github.dunwu.javatech.reflections.TestModel$AC1": [ + "io.github.dunwu.javatech.reflections.TestModel$C1" + ], + "io.github.dunwu.javatech.reflections.TestModel$AI2": [ + "io.github.dunwu.javatech.reflections.TestModel$I2" + ], + "java.lang.annotation.Inherited": [ + "io.github.dunwu.javatech.reflections.TestModel$MAI1", + "io.github.dunwu.javatech.reflections.TestModel$AC1", + "io.github.dunwu.javatech.reflections.TestModel$AI2" + ], + "io.github.dunwu.javatech.reflections.TestModel$AI1": [ + "io.github.dunwu.javatech.reflections.TestModel$I1" + ], + "java.lang.annotation.Retention": [ + "io.github.dunwu.javatech.reflections.TestModel$AC3", + "io.github.dunwu.javatech.reflections.TestModel$MAI1", + "io.github.dunwu.javatech.reflections.TestModel$AC1n", + "io.github.dunwu.javatech.reflections.TestModel$AC2", + "io.github.dunwu.javatech.reflections.TestModel$AC1", + "io.github.dunwu.javatech.reflections.TestModel$AI2", + "io.github.dunwu.javatech.reflections.TestModel$AI1", + "io.github.dunwu.javatech.reflections.TestModel$AF1", + "io.github.dunwu.javatech.reflections.TestModel$AM1" + ] + }, + "MethodsSignature": { + "[io.github.dunwu.javatech.reflections.TestModel$C2]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.c2toC3(io.github.dunwu.javatech.reflections.TestModel$C2)" + ], + "[]": [ + "io.github.dunwu.javatech.reflections.TestModel$C7.\u003cinit\u003e()", + "io.github.dunwu.javatech.reflections.TestModel$AF1.value()", + "io.github.dunwu.javatech.reflections.TestModel$C4.m1()", + "io.github.dunwu.javatech.reflections.TestModel$C4.\u003cinit\u003e()", + "io.github.dunwu.javatech.reflections.TestModel$C2.\u003cinit\u003e()", + "io.github.dunwu.javatech.reflections.TestModel$C4.m3()", + "io.github.dunwu.javatech.reflections.TestModel$C3.\u003cinit\u003e()", + "io.github.dunwu.javatech.reflections.TestModel$C6.\u003cinit\u003e()", + "io.github.dunwu.javatech.reflections.TestModel$C5.\u003cinit\u003e()", + "io.github.dunwu.javatech.reflections.TestModel$AM1.value()", + "io.github.dunwu.javatech.reflections.TestModel$AC2.value()", + "io.github.dunwu.javatech.reflections.TestModel$C1.\u003cinit\u003e()" + ], + "[int, java.lang.String[]]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[])" + ], + "[int, int]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.add(int, int)" + ], + "[java.lang.String]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String)", + "io.github.dunwu.javatech.reflections.TestModel$C4.\u003cinit\u003e(java.lang.String)" + ], + "[int[][], java.lang.String[][]]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][])" + ] + }, + "MethodsParameter": { + "io.github.dunwu.javatech.reflections.TestModel$C2": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.c2toC3(io.github.dunwu.javatech.reflections.TestModel$C2)" + ], + "int[][]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][])" + ], + "java.lang.String[][]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][])" + ], + "java.lang.String": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String)", + "io.github.dunwu.javatech.reflections.TestModel$C4.\u003cinit\u003e(java.lang.String)" + ], + "java.lang.String[]": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[])" + ], + "io.github.dunwu.javatech.reflections.TestModel$AM1": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String)", + "io.github.dunwu.javatech.reflections.TestModel$C4.\u003cinit\u003e(java.lang.String)" + ], + "int": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[])", + "io.github.dunwu.javatech.reflections.TestModel$C4.add(int, int)" + ] + }, + "SubTypes": { + "io.github.dunwu.javatech.reflections.TestModel$C3": [ + "io.github.dunwu.javatech.reflections.TestModel$C5" + ], + "io.github.dunwu.javatech.reflections.TestModel$C1": [ + "io.github.dunwu.javatech.reflections.TestModel$C3", + "io.github.dunwu.javatech.reflections.TestModel$C2" + ], + "java.lang.annotation.Annotation": [ + "io.github.dunwu.javatech.reflections.TestModel$AC3", + "io.github.dunwu.javatech.reflections.TestModel$MAI1", + "io.github.dunwu.javatech.reflections.TestModel$AC2", + "io.github.dunwu.javatech.reflections.TestModel$AC1n", + "io.github.dunwu.javatech.reflections.TestModel$AC1", + "io.github.dunwu.javatech.reflections.TestModel$AI2", + "io.github.dunwu.javatech.reflections.TestModel$AI1", + "io.github.dunwu.javatech.reflections.TestModel$AF1", + "io.github.dunwu.javatech.reflections.TestModel$AM1" + ], + "io.github.dunwu.javatech.reflections.TestModel$I1": [ + "io.github.dunwu.javatech.reflections.TestModel$I2" + ], + "io.github.dunwu.javatech.reflections.TestModel$I3": [ + "io.github.dunwu.javatech.reflections.TestModel$C6" + ], + "java.lang.Object": [ + "io.github.dunwu.javatech.reflections.TestModel$AI2", + "io.github.dunwu.javatech.reflections.TestModel$AI1", + "io.github.dunwu.javatech.reflections.TestModel$AF1", + "io.github.dunwu.javatech.reflections.TestModel$C7", + "io.github.dunwu.javatech.reflections.TestModel$AM1", + "io.github.dunwu.javatech.reflections.TestModel$C6", + "io.github.dunwu.javatech.reflections.TestModel$AC3", + "io.github.dunwu.javatech.reflections.TestModel$C4", + "io.github.dunwu.javatech.reflections.TestModel$MAI1", + "io.github.dunwu.javatech.reflections.TestModel$AC2", + "io.github.dunwu.javatech.reflections.TestModel$C1", + "io.github.dunwu.javatech.reflections.TestModel$AC1n", + "io.github.dunwu.javatech.reflections.TestModel$AC1", + "io.github.dunwu.javatech.reflections.TestModel$I1", + "io.github.dunwu.javatech.reflections.TestModel$I3", + "io.github.dunwu.javatech.reflections.TestModel$I2" + ], + "io.github.dunwu.javatech.reflections.TestModel$I2": [ + "io.github.dunwu.javatech.reflections.TestModel$C1" + ] + }, + "MethodsReturn": { + "io.github.dunwu.javatech.reflections.TestModel$C3": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.c2toC3(io.github.dunwu.javatech.reflections.TestModel$C2)" + ], + "void": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[])", + "io.github.dunwu.javatech.reflections.TestModel$C4.m1()", + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][])" + ], + "java.lang.String": [ + "io.github.dunwu.javatech.reflections.TestModel$AF1.value()", + "io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String)", + "io.github.dunwu.javatech.reflections.TestModel$C4.m3()", + "io.github.dunwu.javatech.reflections.TestModel$AM1.value()", + "io.github.dunwu.javatech.reflections.TestModel$AC2.value()" + ], + "int": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.add(int, int)" + ] + }, + "MethodsAnnotated": { + "io.github.dunwu.javatech.reflections.TestModel$AM1": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[])", + "io.github.dunwu.javatech.reflections.TestModel$C4.m1()", + "io.github.dunwu.javatech.reflections.TestModel$C4.m3()", + "io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][])", + "io.github.dunwu.javatech.reflections.TestModel$C4.\u003cinit\u003e(java.lang.String)" + ] + }, + "FieldsAnnotated": { + "io.github.dunwu.javatech.reflections.TestModel$AF1": [ + "io.github.dunwu.javatech.reflections.TestModel$C4.f1", + "io.github.dunwu.javatech.reflections.TestModel$C4.f2" + ] + }, + "Resources": { + "resource1-reflections.xml": [ + "META-INF/reflections/resource1-reflections.xml" + ], + "saved-testModel-reflections.xml": [ + "META-INF/reflections/saved-testModel-reflections.xml" + ], + "resource2-reflections.xml": [ + "META-INF/reflections/inner/resource2-reflections.xml" + ], + "testModel-reflections.xml": [ + "META-INF/reflections/testModel-reflections.xml" + ] + } + } +} diff --git a/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/testModel-reflections.xml b/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/testModel-reflections.xml new file mode 100644 index 00000000..4acbc004 --- /dev/null +++ b/codes/javatech/javatech-lib/src/test/resources/META-INF/reflections/testModel-reflections.xml @@ -0,0 +1,317 @@ + + + + + + io.github.dunwu.javatech.reflections.TestModel$AC3 + + io.github.dunwu.javatech.reflections.TestModel$C7 + + + + io.github.dunwu.javatech.reflections.TestModel$MAI1 + + io.github.dunwu.javatech.reflections.TestModel$AI1 + + + + io.github.dunwu.javatech.reflections.TestModel$AC2 + + io.github.dunwu.javatech.reflections.TestModel$C3 + io.github.dunwu.javatech.reflections.TestModel$C2 + io.github.dunwu.javatech.reflections.TestModel$AC3 + io.github.dunwu.javatech.reflections.TestModel$I3 + + + + io.github.dunwu.javatech.reflections.TestModel$AC1n + + io.github.dunwu.javatech.reflections.TestModel$C1 + + + + io.github.dunwu.javatech.reflections.TestModel$AC1 + + io.github.dunwu.javatech.reflections.TestModel$C1 + + + + io.github.dunwu.javatech.reflections.TestModel$AI2 + + io.github.dunwu.javatech.reflections.TestModel$I2 + + + + java.lang.annotation.Inherited + + io.github.dunwu.javatech.reflections.TestModel$MAI1 + io.github.dunwu.javatech.reflections.TestModel$AC1 + io.github.dunwu.javatech.reflections.TestModel$AI2 + + + + io.github.dunwu.javatech.reflections.TestModel$AI1 + + io.github.dunwu.javatech.reflections.TestModel$I1 + + + + java.lang.annotation.Retention + + io.github.dunwu.javatech.reflections.TestModel$AC3 + io.github.dunwu.javatech.reflections.TestModel$MAI1 + io.github.dunwu.javatech.reflections.TestModel$AC1n + io.github.dunwu.javatech.reflections.TestModel$AC2 + io.github.dunwu.javatech.reflections.TestModel$AC1 + io.github.dunwu.javatech.reflections.TestModel$AI2 + io.github.dunwu.javatech.reflections.TestModel$AI1 + io.github.dunwu.javatech.reflections.TestModel$AF1 + io.github.dunwu.javatech.reflections.TestModel$AM1 + + + + + + [io.github.dunwu.javatech.reflections.TestModel$C2] + + io.github.dunwu.javatech.reflections.TestModel$C4.c2toC3(io.github.dunwu.javatech.reflections.TestModel$C2) + + + + [] + + io.github.dunwu.javatech.reflections.TestModel$C7.<init>() + io.github.dunwu.javatech.reflections.TestModel$AF1.value() + io.github.dunwu.javatech.reflections.TestModel$C4.m1() + io.github.dunwu.javatech.reflections.TestModel$C4.<init>() + io.github.dunwu.javatech.reflections.TestModel$C2.<init>() + io.github.dunwu.javatech.reflections.TestModel$C4.m3() + io.github.dunwu.javatech.reflections.TestModel$C3.<init>() + io.github.dunwu.javatech.reflections.TestModel$C6.<init>() + io.github.dunwu.javatech.reflections.TestModel$C5.<init>() + io.github.dunwu.javatech.reflections.TestModel$AM1.value() + io.github.dunwu.javatech.reflections.TestModel$AC2.value() + io.github.dunwu.javatech.reflections.TestModel$C1.<init>() + + + + [int, java.lang.String[]] + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[]) + + + + [int, int] + + io.github.dunwu.javatech.reflections.TestModel$C4.add(int, int) + + + + [java.lang.String] + + io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String) + io.github.dunwu.javatech.reflections.TestModel$C4.<init>(java.lang.String) + + + + [int[][], java.lang.String[][]] + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][]) + + + + + + io.github.dunwu.javatech.reflections.TestModel$C2 + + io.github.dunwu.javatech.reflections.TestModel$C4.c2toC3(io.github.dunwu.javatech.reflections.TestModel$C2) + + + + int[][] + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][]) + + + + java.lang.String[][] + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][]) + + + + java.lang.String + + io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String) + io.github.dunwu.javatech.reflections.TestModel$C4.<init>(java.lang.String) + + + + java.lang.String[] + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[]) + + + + io.github.dunwu.javatech.reflections.TestModel$AM1 + + io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String) + io.github.dunwu.javatech.reflections.TestModel$C4.<init>(java.lang.String) + + + + int + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[]) + io.github.dunwu.javatech.reflections.TestModel$C4.add(int, int) + + + + + + io.github.dunwu.javatech.reflections.TestModel$C3 + + io.github.dunwu.javatech.reflections.TestModel$C5 + + + + io.github.dunwu.javatech.reflections.TestModel$C1 + + io.github.dunwu.javatech.reflections.TestModel$C3 + io.github.dunwu.javatech.reflections.TestModel$C2 + + + + java.lang.annotation.Annotation + + io.github.dunwu.javatech.reflections.TestModel$AC3 + io.github.dunwu.javatech.reflections.TestModel$MAI1 + io.github.dunwu.javatech.reflections.TestModel$AC2 + io.github.dunwu.javatech.reflections.TestModel$AC1n + io.github.dunwu.javatech.reflections.TestModel$AC1 + io.github.dunwu.javatech.reflections.TestModel$AI2 + io.github.dunwu.javatech.reflections.TestModel$AI1 + io.github.dunwu.javatech.reflections.TestModel$AF1 + io.github.dunwu.javatech.reflections.TestModel$AM1 + + + + io.github.dunwu.javatech.reflections.TestModel$I1 + + io.github.dunwu.javatech.reflections.TestModel$I2 + + + + io.github.dunwu.javatech.reflections.TestModel$I3 + + io.github.dunwu.javatech.reflections.TestModel$C6 + + + + java.lang.Object + + io.github.dunwu.javatech.reflections.TestModel$AI2 + io.github.dunwu.javatech.reflections.TestModel$AI1 + io.github.dunwu.javatech.reflections.TestModel$AF1 + io.github.dunwu.javatech.reflections.TestModel$C7 + io.github.dunwu.javatech.reflections.TestModel$AM1 + io.github.dunwu.javatech.reflections.TestModel$C6 + io.github.dunwu.javatech.reflections.TestModel$AC3 + io.github.dunwu.javatech.reflections.TestModel$C4 + io.github.dunwu.javatech.reflections.TestModel$MAI1 + io.github.dunwu.javatech.reflections.TestModel$AC2 + io.github.dunwu.javatech.reflections.TestModel$C1 + io.github.dunwu.javatech.reflections.TestModel$AC1n + io.github.dunwu.javatech.reflections.TestModel$AC1 + io.github.dunwu.javatech.reflections.TestModel$I1 + io.github.dunwu.javatech.reflections.TestModel$I3 + io.github.dunwu.javatech.reflections.TestModel$I2 + + + + io.github.dunwu.javatech.reflections.TestModel$I2 + + io.github.dunwu.javatech.reflections.TestModel$C1 + + + + + + io.github.dunwu.javatech.reflections.TestModel$C3 + + io.github.dunwu.javatech.reflections.TestModel$C4.c2toC3(io.github.dunwu.javatech.reflections.TestModel$C2) + + + + void + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[]) + io.github.dunwu.javatech.reflections.TestModel$C4.m1() + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][]) + + + + java.lang.String + + io.github.dunwu.javatech.reflections.TestModel$AF1.value() + io.github.dunwu.javatech.reflections.TestModel$C4.m4(java.lang.String) + io.github.dunwu.javatech.reflections.TestModel$C4.m3() + io.github.dunwu.javatech.reflections.TestModel$AM1.value() + io.github.dunwu.javatech.reflections.TestModel$AC2.value() + + + + int + + io.github.dunwu.javatech.reflections.TestModel$C4.add(int, int) + + + + + + io.github.dunwu.javatech.reflections.TestModel$AM1 + + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int, java.lang.String[]) + io.github.dunwu.javatech.reflections.TestModel$C4.m1() + io.github.dunwu.javatech.reflections.TestModel$C4.m3() + io.github.dunwu.javatech.reflections.TestModel$C4.m1(int[][], java.lang.String[][]) + io.github.dunwu.javatech.reflections.TestModel$C4.<init>(java.lang.String) + + + + + + io.github.dunwu.javatech.reflections.TestModel$AF1 + + io.github.dunwu.javatech.reflections.TestModel$C4.f1 + io.github.dunwu.javatech.reflections.TestModel$C4.f2 + + + + + + resource1-reflections.xml + + META-INF/reflections/resource1-reflections.xml + + + + saved-testModel-reflections.xml + + META-INF/reflections/saved-testModel-reflections.xml + + + + resource2-reflections.xml + + META-INF/reflections/inner/resource2-reflections.xml + + + + testModel-reflections.xml + + META-INF/reflections/testModel-reflections.xml + + + + diff --git a/codes/javatech/javatech-log/javatech-log4j/pom.xml b/codes/javatech/javatech-log/javatech-log4j/pom.xml new file mode 100644 index 00000000..528a5b66 --- /dev/null +++ b/codes/javatech/javatech-log/javatech-log4j/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 1.0.8 + + + io.github.dunwu.javatech + javatech-log4j + 1.0.0 + jar + JAVATECH-日志示例-Log4j + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + + org.slf4j + slf4j-api + + + org.slf4j + slf4j-log4j12 + + + log4j + log4j + 1.2.17 + + + + + junit + junit + test + + + + diff --git a/codes/javatech/javatech-log/javatech-log4j/src/main/java/io/github/dunwu/javatech/log/Log4jDemo.java b/codes/javatech/javatech-log/javatech-log4j/src/main/java/io/github/dunwu/javatech/log/Log4jDemo.java new file mode 100644 index 00000000..575a9d0e --- /dev/null +++ b/codes/javatech/javatech-log/javatech-log4j/src/main/java/io/github/dunwu/javatech/log/Log4jDemo.java @@ -0,0 +1,27 @@ +package io.github.dunwu.javatech.log; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * log4j 示例 + * + * @author Zhang Peng + * @see log4j 官网 + * @since 2018/3/29 + */ +public class Log4jDemo { + + private static final Logger logger = LoggerFactory.getLogger(Log4jDemo.class); + + public static void main(String[] args) { + for (int i = 0; i < 10; i++) { + logger.trace("NO.{} 这是一条 {} 日志记录", i, "trace"); + logger.debug("NO.{} 这是一条 {} 日志记录", i, "debug"); + logger.info("NO.{} 这是一条 {} 日志记录", i, "info"); + logger.warn("NO.{} 这是一条 {} 日志记录", i, "warn"); + logger.error("NO.{} 这是一条 {} 日志记录", i, "error"); + } + } + +} diff --git a/codes/javatech/javatech-log/javatech-log4j/src/main/resources/log4j.xml b/codes/javatech/javatech-log/javatech-log4j/src/main/resources/log4j.xml new file mode 100644 index 00000000..e86941cd --- /dev/null +++ b/codes/javatech/javatech-log/javatech-log4j/src/main/resources/log4j.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/codes/javatech/javatech-log/javatech-log4j2/pom.xml b/codes/javatech/javatech-log/javatech-log4j2/pom.xml new file mode 100644 index 00000000..85642321 --- /dev/null +++ b/codes/javatech/javatech-log/javatech-log4j2/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 1.0.8 + + + io.github.dunwu.javatech + javatech-log4j2 + 1.0.0 + jar + JAVATECH-日志示例-Log4j2 + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + + org.slf4j + slf4j-api + + + org.apache.logging.log4j + log4j-slf4j-impl + + + org.apache.logging.log4j + log4j-core + + + + + junit + junit + test + + + + diff --git a/codes/javatech/javatech-log/javatech-log4j2/src/main/java/io/github/dunwu/javatech/log/Log4j2Demo.java b/codes/javatech/javatech-log/javatech-log4j2/src/main/java/io/github/dunwu/javatech/log/Log4j2Demo.java new file mode 100644 index 00000000..71e47b86 --- /dev/null +++ b/codes/javatech/javatech-log/javatech-log4j2/src/main/java/io/github/dunwu/javatech/log/Log4j2Demo.java @@ -0,0 +1,27 @@ +package io.github.dunwu.javatech.log; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Log4j2 示例 + * + * @author Zhang Peng + * @see Log4j2 官网 + * @since 2018/3/29 + */ +public class Log4j2Demo { + + private static final Logger logger = LoggerFactory.getLogger(Log4j2Demo.class); + + public static void main(String[] args) { + for (int i = 0; i < 10; i++) { + logger.trace("NO.{} 这是一条 {} 日志记录", i, "trace"); + logger.debug("NO.{} 这是一条 {} 日志记录", i, "debug"); + logger.info("NO.{} 这是一条 {} 日志记录", i, "info"); + logger.warn("NO.{} 这是一条 {} 日志记录", i, "warn"); + logger.error("NO.{} 这是一条 {} 日志记录", i, "error"); + } + } + +} diff --git a/codes/javatech/javatech-log/javatech-log4j2/src/main/resources/log4j2.xml b/codes/javatech/javatech-log/javatech-log4j2/src/main/resources/log4j2.xml new file mode 100644 index 00000000..12650afa --- /dev/null +++ b/codes/javatech/javatech-log/javatech-log4j2/src/main/resources/log4j2.xml @@ -0,0 +1,45 @@ + + + + ???? + + + + + + + + + + + + + + + ${PATTERN} + + + + + + + + + + + + + + + + + + + + + + diff --git a/codes/javatech/javatech-log/javatech-logback/pom.xml b/codes/javatech/javatech-log/javatech-logback/pom.xml new file mode 100644 index 00000000..a368fa06 --- /dev/null +++ b/codes/javatech/javatech-log/javatech-logback/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 1.0.8 + + + io.github.dunwu.javatech + javatech-logback + 1.0.0 + jar + JAVATECH-日志示例-Logback + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + + ch.qos.logback + logback-classic + + + + + + + + + junit + junit + test + + + + diff --git a/codes/javatech/javatech-log/javatech-logback/src/main/java/io/github/dunwu/javatech/log/LogbackDemo.java b/codes/javatech/javatech-log/javatech-logback/src/main/java/io/github/dunwu/javatech/log/LogbackDemo.java new file mode 100644 index 00000000..5db50a71 --- /dev/null +++ b/codes/javatech/javatech-log/javatech-logback/src/main/java/io/github/dunwu/javatech/log/LogbackDemo.java @@ -0,0 +1,27 @@ +package io.github.dunwu.javatech.log; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Logback 示例 + * + * @author Zhang Peng + * @see logback 官网 + * @since 2018/3/29 + */ +public class LogbackDemo { + + private static final Logger logger = LoggerFactory.getLogger(LogbackDemo.class); + + public static void main(String[] args) { + for (int i = 0; i < 10; i++) { + logger.trace("NO.{} 这是一条 {} 日志记录", i, "trace"); + logger.debug("NO.{} 这是一条 {} 日志记录", i, "debug"); + logger.info("NO.{} 这是一条 {} 日志记录", i, "info"); + logger.warn("NO.{} 这是一条 {} 日志记录", i, "warn"); + logger.error("NO.{} 这是一条 {} 日志记录", i, "error"); + } + } + +} diff --git a/codes/javatech/javatech-log/javatech-logback/src/main/resources/logback.xml b/codes/javatech/javatech-log/javatech-logback/src/main/resources/logback.xml new file mode 100644 index 00000000..8d3ec5a8 --- /dev/null +++ b/codes/javatech/javatech-log/javatech-logback/src/main/resources/logback.xml @@ -0,0 +1,95 @@ + + + + + + dunwu-admin + + + + + + + + + + + + + + + ${LOG_CHARSET} + ${LOG_COLOR_PATTERN} + + + + + + + + ${LOG_DIR}/%d{yyyy-MM,aux}/${APP_NAME}_debug.%d{yyyy-MM-dd}.%i.log + + ${LOG_MAX_HISTORY} + + ${LOG_MAX_FILE_SIZE} + + + + ${LOG_PATTERN} + + + + DEBUG + + + + + + + 0 + + ${LOG_MAX_QUEUE_SIZE} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/codes/javatech/javatech-log/javatech-logstash/pom.xml b/codes/javatech/javatech-log/javatech-logstash/pom.xml new file mode 100644 index 00000000..feb0336a --- /dev/null +++ b/codes/javatech/javatech-log/javatech-logstash/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 1.0.8 + + + io.github.dunwu.javatech + javatech-logstash + 1.0.0 + jar + JAVATECH-日志示例-Logstash + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + ch.qos.logback + logback-classic + + + + diff --git a/codes/javatech/javatech-log/javatech-logstash/src/main/java/io/github/dunwu/javatech/ElasticDemo.java b/codes/javatech/javatech-log/javatech-logstash/src/main/java/io/github/dunwu/javatech/ElasticDemo.java new file mode 100644 index 00000000..6dbeafe2 --- /dev/null +++ b/codes/javatech/javatech-log/javatech-logstash/src/main/java/io/github/dunwu/javatech/ElasticDemo.java @@ -0,0 +1,39 @@ +package io.github.dunwu.javatech; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * 向 Elastic 日志中心传输日志 Logback logstash-logback-encoder jar 包会根据 logback 中的配置,将日志数据定向传输到 logstash 详见 + * src/main/resources/logback.xml appender 配置 使用 udp 方式传输时,有丢失日志的情况(ELK-UDP) 使用 tcp 方式传输时,不会丢失日志(ELK-TCP) Log4j 通过 + * org.apache.log4j.net.SocketAppender 发送 TCP 数据,Logstash 服务器使用 log4j input 插件 接收 + * + * @author Zhang Peng + */ +public class ElasticDemo { + + private static final Logger logger = LoggerFactory.getLogger(ElasticDemo.class); + + private static volatile int index = 0; + + public static void main(String[] args) { + // sendLog4jLog(); + sendLogbackLog(); + } + + private static void sendLogbackLog() { + ExecutorService executorService = Executors.newFixedThreadPool(100); + for (int i = 0; i < 10000; i++) { + executorService.submit(new Runnable() { + @Override + public void run() { + logger.info("这是第 {} 条日志", ++index); + } + }); + } + } + +} diff --git a/codes/javatech/javatech-log/javatech-logstash/src/main/resources/logback.xml b/codes/javatech/javatech-log/javatech-logstash/src/main/resources/logback.xml new file mode 100644 index 00000000..c917bd86 --- /dev/null +++ b/codes/javatech/javatech-log/javatech-logstash/src/main/resources/logback.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n + + + + + + + + ${user.dir}/logs/${FILE_NAME}-all.%d{yyyy-MM-dd}.log + 30 + + + + + 30MB + + + + %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n + + + + 192.168.28.32 + 9250 + + + + 192.168.28.32:9251 + 5 minutes + 6 second + 16384 + + {"appname":"metis"} + {"subappname":"metis-vdisk"} + + + + + + + + + + + + + + + + + + + diff --git a/codes/javatech/javatech-log/pom.xml b/codes/javatech/javatech-log/pom.xml new file mode 100644 index 00000000..94448ef8 --- /dev/null +++ b/codes/javatech/javatech-log/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + io.github.dunwu.javatech + javatech-log + 1.0.0 + pom + JAVATECH-日志示例 + + + javatech-log4j + javatech-log4j2 + javatech-logback + javatech-logstash + + diff --git a/codes/javatech/javatech-mq/javatech-kafka-springboot/pom.xml b/codes/javatech/javatech-mq/javatech-kafka-springboot/pom.xml new file mode 100644 index 00000000..f185b997 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka-springboot/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + io.github.dunwu.boot + dunwu-boot + 1.0.8 + + + io.github.dunwu.javatech + javatech-kafka-springboot + 1.0.0 + jar + JAVATECH-MQ示例-Kafka+SpringBoot + + + UTF-8 + UTF-8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.kafka + spring-kafka + + + org.springframework.kafka + spring-kafka-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/KafkaConsumer.java b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/KafkaConsumer.java new file mode 100644 index 00000000..071b4247 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/KafkaConsumer.java @@ -0,0 +1,24 @@ +package io.github.dunwu.javatech; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +/** + * Kafka 消费者 + * + * @author Zhang Peng + * @since 2018-11-28 + */ +@Component +public class KafkaConsumer { + + private final Logger log = LoggerFactory.getLogger(KafkaConsumer.class); + + @KafkaListener(topics = "test") + public void processMessage(String data) { + log.info("收到kafka消息:{}", data); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/KafkaProducer.java b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/KafkaProducer.java new file mode 100644 index 00000000..246104f2 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/KafkaProducer.java @@ -0,0 +1,37 @@ +package io.github.dunwu.javatech; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** + * Kafka生产者 + * + * @author Zhang Peng + * @since 2018-11-29 + */ +@Component +public class KafkaProducer { + + private final Logger log = LoggerFactory.getLogger(KafkaProducer.class); + + @Autowired + private KafkaTemplate template; + + @Transactional(rollbackFor = RuntimeException.class) + public void sendTransactionMsg(String topic, String data) { + log.info("向kafka发送数据:[{}]", data); + template.executeInTransaction(t -> { + t.send(topic, "prepare"); + if ("error".equals(data)) { + throw new RuntimeException("failed"); + } + t.send(topic, "finish"); + return true; + }); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/KafkaProducerController.java b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/KafkaProducerController.java new file mode 100644 index 00000000..70aebb9f --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/KafkaProducerController.java @@ -0,0 +1,28 @@ +package io.github.dunwu.javatech; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * spring-boot kafka 示例 + *

+ * 此 Controller 作为生产者,接受REST接口传入的消息,并写入到指定 Kafka Topic + *

+ * 访问方式:http://localhost:8080/kafka/send?topic=xxx&data=xxx + * + * @author Zhang Peng + */ +@RestController +@RequestMapping("kafka") +public class KafkaProducerController { + + @Autowired + private KafkaProducer kafkaProducer; + + @RequestMapping("sendTx") + public void send(String topic, String data) { + kafkaProducer.sendTransactionMsg(topic, data); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/MsgKafkaApplication.java b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/MsgKafkaApplication.java new file mode 100644 index 00000000..48144045 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/MsgKafkaApplication.java @@ -0,0 +1,13 @@ +package io.github.dunwu.javatech; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MsgKafkaApplication { + + public static void main(String[] args) { + SpringApplication.run(MsgKafkaApplication.class, args); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/config/KafkaConsumerConfig.java b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/config/KafkaConsumerConfig.java new file mode 100644 index 00000000..4048934a --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/config/KafkaConsumerConfig.java @@ -0,0 +1,62 @@ +package io.github.dunwu.javatech.config; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.common.serialization.StringDeserializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory; +import org.springframework.kafka.config.KafkaListenerContainerFactory; +import org.springframework.kafka.core.ConsumerFactory; +import org.springframework.kafka.core.DefaultKafkaConsumerFactory; +import org.springframework.kafka.listener.ConcurrentMessageListenerContainer; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@EnableKafka +public class KafkaConsumerConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Bean(name = "kafkaListenerContainerFactory") + public KafkaListenerContainerFactory> kafkaListenerContainerFactory() { + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory("groupA")); + factory.setConcurrency(3); + factory.getContainerProperties().setPollTimeout(3000); + return factory; + } + + public ConsumerFactory consumerFactory(String consumerGroupId) { + return new DefaultKafkaConsumerFactory<>(consumerConfig(consumerGroupId)); + } + + public Map consumerConfig(String consumerGroupId) { + Map props = new HashMap<>(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, false); + props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "100"); + props.put(ConsumerConfig.SESSION_TIMEOUT_MS_CONFIG, "15000"); + props.put(ConsumerConfig.GROUP_ID_CONFIG, consumerGroupId); + props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); + return props; + } + + @Bean(name = "kafkaListenerContainerFactory1") + public KafkaListenerContainerFactory> kafkaListenerContainerFactory1() { + ConcurrentKafkaListenerContainerFactory factory = + new ConcurrentKafkaListenerContainerFactory<>(); + factory.setConsumerFactory(consumerFactory("groupB")); + factory.setConcurrency(3); + factory.getContainerProperties().setPollTimeout(3000); + return factory; + } + +} diff --git a/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/config/KafkaProducerConfig.java b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/config/KafkaProducerConfig.java new file mode 100644 index 00000000..9cf3d03c --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/config/KafkaProducerConfig.java @@ -0,0 +1,61 @@ +package io.github.dunwu.javatech.config; + +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.annotation.EnableKafka; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; +import org.springframework.kafka.transaction.KafkaTransactionManager; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@EnableKafka +public class KafkaProducerConfig { + + @Value("${spring.kafka.bootstrap-servers}") + private String bootstrapServers; + + @Value("${spring.kafka.producer.retries}") + private Integer retries; + + @Value("${spring.kafka.producer.batch-size}") + private Integer batchSize; + + @Bean + public KafkaTransactionManager transactionManager() { + KafkaTransactionManager manager = new KafkaTransactionManager(producerFactory()); + return manager; + } + + @Bean + public ProducerFactory producerFactory() { + DefaultKafkaProducerFactory producerFactory = new DefaultKafkaProducerFactory<>( + producerConfigs()); + producerFactory.transactionCapable(); + producerFactory.setTransactionIdPrefix("hous-"); + return producerFactory; + } + + @Bean + public Map producerConfigs() { + Map props = new HashMap<>(7); + props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + props.put(ProducerConfig.RETRIES_CONFIG, retries); + props.put(ProducerConfig.BATCH_SIZE_CONFIG, batchSize); + props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + return props; + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/resources/application.properties b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/resources/application.properties new file mode 100644 index 00000000..e25cabf2 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/resources/application.properties @@ -0,0 +1,13 @@ +spring.kafka.bootstrap-servers=tdh60dev01:9092,tdh60dev02:9092,tdh60dev03:9092 +spring.kafka.producer.retries=3 +spring.kafka.producer.transaction-id-prefix=javaweb-kafka +# producer +spring.kafka.producer.batch-size=1000 +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer +# consumer +spring.kafka.consumer.group-id=javaweb +spring.kafka.consumer.enable-auto-commit=true +spring.kafka.consumer.auto-commit-interval=1000 +spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer diff --git a/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/resources/logback.xml b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/resources/logback.xml new file mode 100644 index 00000000..1946a29c --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + %d{HH:mm:ss.SSS} [%boldYellow(%thread)] [%highlight(%-5level)] %boldGreen(%c{36}.%M) - %boldBlue(%m%n) + + + + + + + + + + diff --git a/codes/javatech/javatech-mq/javatech-kafka-springboot/src/test/java/io/github/dunwu/javatech/KafkaProducerTest.java b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/test/java/io/github/dunwu/javatech/KafkaProducerTest.java new file mode 100644 index 00000000..e73fe65f --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka-springboot/src/test/java/io/github/dunwu/javatech/KafkaProducerTest.java @@ -0,0 +1,26 @@ +package io.github.dunwu.javatech; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +/** + * @author Zhang Peng + * @since 2018-11-29 + */ +@RunWith(SpringRunner.class) +@SpringBootTest(classes = MsgKafkaApplication.class) +public class KafkaProducerTest { + + @Autowired + private KafkaProducer kafkaProducer; + + @Test + public void test() { + kafkaProducer.sendTransactionMsg("test", "上联:天王盖地虎"); + kafkaProducer.sendTransactionMsg("test", "下联:宝塔镇河妖"); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-kafka/pom.xml b/codes/javatech/javatech-mq/javatech-kafka/pom.xml new file mode 100644 index 00000000..19ab59b3 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 1.0.8 + + + io.github.dunwu.javatech + javatech-kafka + 1.0.0 + jar + JAVATECH-MQ示例-Kafka + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + org.apache.kafka + kafka-clients + + + org.apache.kafka + kafka-streams + + + ch.qos.logback + logback-classic + + + diff --git a/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerAOC.java b/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerAOC.java new file mode 100644 index 00000000..041a69e4 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerAOC.java @@ -0,0 +1,44 @@ +package io.github.dunwu.javatech.kafka; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; + +import java.util.Arrays; +import java.util.Properties; + +/** + * Kafka 消费者消费消息示例 消费者配置参考:https://kafka.apache.org/documentation/#consumerconfigs + */ +public class ConsumerAOC { + + private static final String HOST = "localhost:9092"; + + public static void main(String[] args) { + // 1. 指定消费者的配置 + final Properties props = new Properties(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, HOST); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "test"); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true"); + props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringDeserializer"); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringDeserializer"); + + // 2. 使用配置初始化 Kafka 消费者 + KafkaConsumer consumer = new KafkaConsumer<>(props); + + // 3. 消费者订阅 Topic + consumer.subscribe(Arrays.asList("t1")); + while (true) { + // 4. 消费消息 + ConsumerRecords records = consumer.poll(100); + for (ConsumerRecord record : records) { + System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value()); + } + } + } + +} diff --git a/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerManual.java b/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerManual.java new file mode 100644 index 00000000..2b6b2674 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerManual.java @@ -0,0 +1,48 @@ +package io.github.dunwu.javatech.kafka; + +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; + +/** + * @author Zhang Peng + * @since 2018/7/12 + */ +public class ConsumerManual { + + private static final String HOST = "localhost:9092"; + + public static void main(String[] args) { + Properties props = new Properties(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, HOST); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "test"); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringDeserializer"); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringDeserializer"); + + KafkaConsumer consumer = new KafkaConsumer<>(props); + consumer.subscribe(Arrays.asList("t1", "t2")); + final int minBatchSize = 200; + List> buffer = new ArrayList<>(); + while (true) { + ConsumerRecords records = consumer.poll(100); + for (ConsumerRecord record : records) { + buffer.add(record); + } + if (buffer.size() >= minBatchSize) { + // 逻辑处理,例如保存到数据库 + consumer.commitSync(); + buffer.clear(); + } + } + } + +} diff --git a/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerManualPartition.java b/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerManualPartition.java new file mode 100644 index 00000000..7cc6c315 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerManualPartition.java @@ -0,0 +1,49 @@ +package io.github.dunwu.javatech.kafka; + +import org.apache.kafka.clients.consumer.*; +import org.apache.kafka.common.TopicPartition; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +/** + * @author Zhang Peng + * @since 2018/7/12 + */ +public class ConsumerManualPartition { + + private static final String HOST = "localhost:9092"; + + public static void main(String[] args) { + Properties props = new Properties(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, HOST); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "test2"); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringDeserializer"); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringDeserializer"); + + KafkaConsumer consumer = new KafkaConsumer<>(props); + consumer.subscribe(Arrays.asList("t1")); + + try { + while (true) { + ConsumerRecords records = consumer.poll(Long.MAX_VALUE); + for (TopicPartition partition : records.partitions()) { + List> partitionRecords = records.records(partition); + for (ConsumerRecord record : partitionRecords) { + System.out.println(partition.partition() + ": " + record.offset() + ": " + record.value()); + } + long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset(); + consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1))); + } + } + } finally { + consumer.close(); + } + } + +} diff --git a/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ProducerDemo.java b/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ProducerDemo.java new file mode 100644 index 00000000..3cc377ee --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ProducerDemo.java @@ -0,0 +1,49 @@ +package io.github.dunwu.javatech.kafka; + +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; + +import java.util.Properties; + +/** + * Kafka 生产者生产消息示例 生产者配置参考:https://kafka.apache.org/documentation/#producerconfigs + */ +public class ProducerDemo { + + private static final String HOST = "localhost:9092"; + + public static void main(String[] args) { + // 1. 指定生产者的配置 + Properties properties = new Properties(); + properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, HOST); + properties.put(ProducerConfig.ACKS_CONFIG, "all"); + properties.put(ProducerConfig.RETRIES_CONFIG, 0); + properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); + properties.put(ProducerConfig.LINGER_MS_CONFIG, 1); + properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432); + properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringSerializer"); + properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringSerializer"); + + // 2. 使用配置初始化 Kafka 生产者 + Producer producer = new KafkaProducer<>(properties); + + try { + // 3. 使用 send 方法发送异步消息 + for (int i = 0; i < 100; i++) { + String msg = "Message " + i; + producer.send(new ProducerRecord<>("HelloWorld", msg)); + System.out.println("Sent:" + msg); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + // 4. 关闭生产者 + producer.close(); + } + } + +} diff --git a/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ProducerInTransaction.java b/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ProducerInTransaction.java new file mode 100644 index 00000000..8105fc9e --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ProducerInTransaction.java @@ -0,0 +1,143 @@ +package io.github.dunwu.javatech.kafka; + +import org.apache.kafka.clients.consumer.*; +import org.apache.kafka.clients.producer.KafkaProducer; +import org.apache.kafka.clients.producer.Producer; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.clients.producer.ProducerRecord; +import org.apache.kafka.common.TopicPartition; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * @author Zhang Peng + */ +public class ProducerInTransaction { + + private static final String HOST = "192.168.28.32:9092"; + + public static void main(String[] args) { + onlyProduceInTransaction(); + consumeTransferProduce(); + } + + /** + * 在一个事务只有生产消息操作 + */ + public static void onlyProduceInTransaction() { + Producer producer = buildProducer(); + + // 1.初始化事务 + producer.initTransactions(); + + // 2.开启事务 + producer.beginTransaction(); + + try { + // 3.kafka写操作集合 + // 3.1 do业务逻辑 + + // 3.2 发送消息 + producer.send(new ProducerRecord("test", "transaction-data-1")); + producer.send(new ProducerRecord("test", "transaction-data-2")); + + // 3.3 do其他业务逻辑,还可以发送其他topic的消息。 + + // 4.事务提交 + producer.commitTransaction(); + } catch (Exception e) { + // 5.放弃事务 + producer.abortTransaction(); + } + } + + /** + * 在一个事务内,即有生产消息又有消费消息 + */ + public static void consumeTransferProduce() { + // 1.构建上产者 + Producer producer = buildProducer(); + // 2.初始化事务(生成productId),对于一个生产者,只能执行一次初始化事务操作 + producer.initTransactions(); + + // 3.构建消费者和订阅主题 + Consumer consumer = buildConsumer(); + consumer.subscribe(Arrays.asList("test")); + while (true) { + // 4.开启事务 + producer.beginTransaction(); + + // 5.1 接受消息 + ConsumerRecords records = consumer.poll(500); + + try { + // 5.2 do业务逻辑; + System.out.println("customer Message---"); + Map commits = new HashMap<>(); + for (ConsumerRecord record : records) { + // 5.2.1 读取消息,并处理消息。print the offset,key and value for the consumer + // records. + System.out.printf("offset = %d, key = %s, value = %s\n", record.offset(), record.key(), + record.value()); + + // 5.2.2 记录提交的偏移量 + commits.put(new TopicPartition(record.topic(), record.partition()), + new OffsetAndMetadata(record.offset())); + + // 6.生产新的消息。比如外卖订单状态的消息,如果订单成功,则需要发送跟商家结转消息或者派送员的提成消息 + producer.send(new ProducerRecord("test", "data2")); + } + + // 7.提交偏移量 + producer.sendOffsetsToTransaction(commits, "group0323"); + + // 8.事务提交 + producer.commitTransaction(); + } catch (Exception e) { + // 7.放弃事务 + producer.abortTransaction(); + } + } + } + + public static Producer buildProducer() { + // 1. 指定生产者的配置 + Properties properties = new Properties(); + properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, HOST); + properties.put(ProducerConfig.ACKS_CONFIG, "all"); + properties.put(ProducerConfig.RETRIES_CONFIG, 1); + properties.put(ProducerConfig.BATCH_SIZE_CONFIG, 16384); + properties.put(ProducerConfig.LINGER_MS_CONFIG, 1); + properties.put(ProducerConfig.BUFFER_MEMORY_CONFIG, 33554432); + properties.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "first-transactional"); + properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringSerializer"); + properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringSerializer"); + + // 2. 使用配置初始化 Kafka 生产者 + Producer producer = new KafkaProducer<>(properties); + return producer; + } + + public static Consumer buildConsumer() { + // 1. 指定消费者的配置 + final Properties props = new Properties(); + props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, HOST); + props.put(ConsumerConfig.GROUP_ID_CONFIG, "test"); + props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true"); + props.put(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, "1000"); + props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringDeserializer"); + props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, + "org.apache.kafka.common.serialization.StringDeserializer"); + + // 2. 使用配置初始化 Kafka 消费者 + Consumer consumer = new KafkaConsumer<>(props); + return consumer; + } + +} diff --git a/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/StreamDemo.java b/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/StreamDemo.java new file mode 100644 index 00000000..34dc10bb --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/StreamDemo.java @@ -0,0 +1,46 @@ +package io.github.dunwu.javatech.kafka; + +import org.apache.kafka.common.serialization.Serdes; +import org.apache.kafka.common.utils.Bytes; +import org.apache.kafka.streams.KafkaStreams; +import org.apache.kafka.streams.StreamsBuilder; +import org.apache.kafka.streams.StreamsConfig; +import org.apache.kafka.streams.kstream.KStream; +import org.apache.kafka.streams.kstream.KTable; +import org.apache.kafka.streams.kstream.Materialized; +import org.apache.kafka.streams.kstream.Produced; +import org.apache.kafka.streams.state.KeyValueStore; + +import java.util.Arrays; +import java.util.Properties; + +/** + * Kafka 流示例 消费者配置参考:https://kafka.apache.org/documentation/#streamsconfigs + */ +public class StreamDemo { + + private static final String HOST = "localhost:9092"; + + public static void main(String[] args) { + // 1. 指定流的配置 + Properties config = new Properties(); + config.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-application"); + config.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, HOST); + config.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass()); + config.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass()); + + // 设置流构造器 + StreamsBuilder builder = new StreamsBuilder(); + KStream textLines = builder.stream("TextLinesTopic"); + KTable wordCounts = textLines + .flatMapValues(textLine -> Arrays.asList(textLine.toLowerCase().split("\\W+"))) + .groupBy((key, word) -> word) + .count(Materialized.>as("counts-store")); + wordCounts.toStream().to("WordsWithCountsTopic", Produced.with(Serdes.String(), Serdes.Long())); + + // 根据流构造器和流配置初始化 Kafka 流 + KafkaStreams streams = new KafkaStreams(builder.build(), config); + streams.start(); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-kafka/src/main/resources/logback.xml b/codes/javatech/javatech-mq/javatech-kafka/src/main/resources/logback.xml new file mode 100644 index 00000000..72b2ddb5 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-kafka/src/main/resources/logback.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + 10 + 100 + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [javalib] [%thread] [%p] %c{36}#%M - %m%n + + + + + + ${LOG_PATH}/logs/${FILE_NAME}-error.%d{yyyy-MM-dd}.log + 30 + + + + + 10MB + + + + ERROR + ACCEPT + DENY + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [javalib] [%thread] [%p] %c{36}#%M - %m%n + + + + + + + + + + + diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/pom.xml b/codes/javatech/javatech-mq/javatech-rocketmq/pom.xml new file mode 100644 index 00000000..9bbc6d7b --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 1.0.8 + + + io.github.dunwu.javatech + javatech-rocketmq + 1.0.0 + jar + JAVATECH-MQ示例-Rocketmq + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + org.apache.rocketmq + rocketmq-client + 4.9.4 + + + org.apache.rocketmq + rocketmq-logging + 4.9.4 + + + ch.qos.logback + logback-classic + + + + diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/RocketConstant.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/RocketConstant.java new file mode 100644 index 00000000..d932c10b --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/RocketConstant.java @@ -0,0 +1,10 @@ +package io.github.dunwu.javatech.rocketmq; + +/** + * @author Zhang Peng + */ +public class RocketConstant { + + public static final String HOST = "localhost:9876"; + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/BatchConsumer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/BatchConsumer.java new file mode 100644 index 00000000..b05bbcd1 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/BatchConsumer.java @@ -0,0 +1,41 @@ +package io.github.dunwu.javatech.rocketmq.batch; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.List; + +public class BatchConsumer { + + public static void main(String[] args) throws InterruptedException, MQClientException { + + // Instantiate with specified consumer group name. + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleProducerGroup"); + + // Specify name server addresses. + consumer.setNamesrvAddr(RocketConstant.HOST); + + // Subscribe one more more topics to consume. + consumer.subscribe("BatchTest", "*"); + // Register callback to execute on arrival of messages fetched from brokers. + consumer.registerMessageListener(new MessageListenerConcurrently() { + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + + //Launch the consumer instance. + consumer.start(); + + System.out.printf("Consumer Started.%n"); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/BatchProducer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/BatchProducer.java new file mode 100644 index 00000000..c567d532 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/BatchProducer.java @@ -0,0 +1,40 @@ +package io.github.dunwu.javatech.rocketmq.batch; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.message.Message; + +import java.util.ArrayList; +import java.util.List; + +/** + * 批量发送消息(一次发送消息大小不超过 1MB) + * + * @author Zhang Peng + */ +public class BatchProducer { + + public static void main(String[] args) throws Exception { + // Instantiate with a producer group name. + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); + producer.setNamesrvAddr(RocketConstant.HOST); + producer.start(); + producer.setRetryTimesWhenSendAsyncFailed(0); + + String topic = "BatchTest"; + List messages = new ArrayList<>(); + messages.add(new Message(topic, "TagA", "OrderID001", "Hello world 0".getBytes())); + messages.add(new Message(topic, "TagA", "OrderID002", "Hello world 1".getBytes())); + messages.add(new Message(topic, "TagA", "OrderID003", "Hello world 2".getBytes())); + try { + producer.send(messages); + } catch (Exception e) { + e.printStackTrace(); + //handle the error + } + + // Shut down once the producer instance is not longer in use. + producer.shutdown(); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/BatchProducer2.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/BatchProducer2.java new file mode 100644 index 00000000..a1ae8715 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/BatchProducer2.java @@ -0,0 +1,41 @@ +package io.github.dunwu.javatech.rocketmq.batch; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.message.Message; + +import java.util.ArrayList; +import java.util.List; + +/** + * 批量发送消息(一次发送消息大小超过 1MB) + * + * @author Zhang Peng + */ +public class BatchProducer2 { + + public static void main(String[] args) throws Exception { + // Instantiate with a producer group name. + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); + producer.setNamesrvAddr(RocketConstant.HOST); + producer.start(); + producer.setRetryTimesWhenSendAsyncFailed(0); + + String topic = "BatchTest"; + List messages = new ArrayList<>(); + messages.add(new Message(topic, "TagA", "OrderID001", "Hello world 0".getBytes())); + messages.add(new Message(topic, "TagA", "OrderID002", "Hello world 1".getBytes())); + messages.add(new Message(topic, "TagA", "OrderID003", "Hello world 2".getBytes())); + // then you could split the large list into small ones: + ListSplitter splitter = new ListSplitter(messages); + + while (splitter.hasNext()) { + List listItem = splitter.next(); + producer.send(listItem); + } + + // Shut down once the producer instance is not longer in use. + producer.shutdown(); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/ListSplitter.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/ListSplitter.java new file mode 100644 index 00000000..837fc117 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/ListSplitter.java @@ -0,0 +1,57 @@ +package io.github.dunwu.javatech.rocketmq.batch; + +import org.apache.rocketmq.common.message.Message; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +public class ListSplitter implements Iterator> { + + private final int SIZE_LIMIT = 1000 * 1000; + private final List messages; + private int currIndex; + + public ListSplitter(List messages) { + this.messages = messages; + } + + @Override + public boolean hasNext() { + return currIndex < messages.size(); + } + + @Override + public List next() { + int nextIndex = currIndex; + int totalSize = 0; + for (; nextIndex < messages.size(); nextIndex++) { + Message message = messages.get(nextIndex); + int tmpSize = message.getTopic().length() + message.getBody().length; + Map properties = message.getProperties(); + for (Map.Entry entry : properties.entrySet()) { + tmpSize += entry.getKey().length() + entry.getValue().length(); + } + tmpSize = tmpSize + 20; //for log overhead + if (tmpSize > SIZE_LIMIT) { + //it is unexpected that single message exceeds the SIZE_LIMIT + //here just let it go, otherwise it will block the splitting process + if (nextIndex - currIndex == 0) { + //if the next sublist has no element, add this one and then break, otherwise just break + nextIndex++; + } + break; + } + if (tmpSize + totalSize > SIZE_LIMIT) { + break; + } else { + totalSize += tmpSize; + } + + } + List subList = messages.subList(currIndex, nextIndex); + currIndex = nextIndex; + return subList; + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/README.md b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/README.md new file mode 100644 index 00000000..5d9c24c2 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/batch/README.md @@ -0,0 +1,11 @@ +# Rocket 官方示例之 Batch Example + +批量发送消息提高了传递小消息的性能。 + +同一批次的消息应该具有:相同的主题、相同的 waitStoreMsgOK 并且不支持调度。 + +此外,一批消息的总大小不应超过 1MiB。 + +## 参考资料 + +- [Batch Example](https://rocketmq.apache.org/docs/batch-example/) diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/broadcast/BroadcastConsumer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/broadcast/BroadcastConsumer.java new file mode 100644 index 00000000..5c1c97d5 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/broadcast/BroadcastConsumer.java @@ -0,0 +1,45 @@ +package io.github.dunwu.javatech.rocketmq.broadcast; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageExt; +import org.apache.rocketmq.common.protocol.heartbeat.MessageModel; + +import java.util.List; + +/** + * 接收广播消息示例 + * + * @author Zhang Peng + */ +public class BroadcastConsumer { + + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); + consumer.setNamesrvAddr(RocketConstant.HOST); + + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + + //set to broadcast mode + consumer.setMessageModel(MessageModel.BROADCASTING); + + consumer.subscribe("TopicTest", "TagA || TagC || TagD"); + + consumer.registerMessageListener(new MessageListenerConcurrently() { + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n"); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + + consumer.start(); + System.out.printf("Broadcast Consumer Started.%n"); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/broadcast/BroadcastProducer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/broadcast/BroadcastProducer.java new file mode 100644 index 00000000..10628dd7 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/broadcast/BroadcastProducer.java @@ -0,0 +1,30 @@ +package io.github.dunwu.javatech.rocketmq.broadcast; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +/** + * 发送广播消息示例 + * + * @author Zhang Peng + */ +public class BroadcastProducer { + + public static void main(String[] args) throws Exception { + DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName"); + producer.setNamesrvAddr(RocketConstant.HOST); + producer.start(); + + for (int i = 0; i < 100; i++) { + Message msg = new Message("TopicTest", "TagA", "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.send(msg); + System.out.printf("%s%n", sendResult); + } + producer.shutdown(); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/broadcast/README.md b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/broadcast/README.md new file mode 100644 index 00000000..104b6eb0 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/broadcast/README.md @@ -0,0 +1,7 @@ +# Rocket 官方示例之 Broadcasting Example + +- 广播是向主题的所有订阅者发送消息。如果您希望所有订阅者都收到有关某个主题的消息,广播是一个不错的选择。 + +## 参考资料 + +- [Broadcasting Example](https://rocketmq.apache.org/docs/broadcast-example/) diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/filter/FilterConsumer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/filter/FilterConsumer.java new file mode 100644 index 00000000..635f125e --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/filter/FilterConsumer.java @@ -0,0 +1,33 @@ +package io.github.dunwu.javatech.rocketmq.filter; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.MessageSelector; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.List; + +public class FilterConsumer { + + public static void main(String[] args) throws InterruptedException, MQClientException { + + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); + // Specify name server addresses. + consumer.setNamesrvAddr(RocketConstant.HOST); + // only subsribe messages have property a, also a >=0 and a <= 3 + consumer.subscribe("TopicTest", MessageSelector.bySql("a between 0 and 3")); + + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + consumer.start(); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/filter/FilterProducer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/filter/FilterProducer.java new file mode 100644 index 00000000..1a0c2f94 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/filter/FilterProducer.java @@ -0,0 +1,37 @@ +package io.github.dunwu.javatech.rocketmq.filter; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +/** + * 批量发送消息(一次发送消息大小不超过 1MB) + * + * @author Zhang Peng + */ +public class FilterProducer { + + public static void main(String[] args) throws Exception { + // Instantiate with a producer group name. + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + producer.setNamesrvAddr(RocketConstant.HOST); + producer.start(); + + String[] tags = new String[] { "TagA", "TagB", "TagC", "TagD", "TagE" }; + for (int i = 0; i < 100; i++) { + //Create a message instance, specifying topic, tag and message body. + Message msg = new Message("TopicTest", tags[i % tags.length], "KEY" + i, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + // Set some properties. + msg.putUserProperty("a", String.valueOf(i)); + SendResult sendResult = producer.send(msg); + + System.out.printf("%s%n", sendResult); + } + + producer.shutdown(); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/order/OrderedConsumer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/order/OrderedConsumer.java new file mode 100644 index 00000000..da3361b7 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/order/OrderedConsumer.java @@ -0,0 +1,58 @@ +package io.github.dunwu.javatech.rocketmq.order; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeOrderlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerOrderly; +import org.apache.rocketmq.common.consumer.ConsumeFromWhere; +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 接收全局和分区排序的消息。 + * + * @author Zhang Peng + */ +public class OrderedConsumer { + + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); + consumer.setNamesrvAddr(RocketConstant.HOST); + + consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); + + consumer.subscribe("TopicTest", "TagA || TagC || TagD"); + + consumer.registerMessageListener(new MessageListenerOrderly() { + + AtomicLong consumeTimes = new AtomicLong(0); + + @Override + public ConsumeOrderlyStatus consumeMessage(List msgs, ConsumeOrderlyContext context) { + context.setAutoCommit(false); + System.out.printf(Thread.currentThread().getName() + " Receive New Messages: " + msgs + "%n"); + this.consumeTimes.incrementAndGet(); + if ((this.consumeTimes.get() % 2) == 0) { + return ConsumeOrderlyStatus.SUCCESS; + } else if ((this.consumeTimes.get() % 3) == 0) { + return ConsumeOrderlyStatus.ROLLBACK; + } else if ((this.consumeTimes.get() % 4) == 0) { + return ConsumeOrderlyStatus.COMMIT; + } else if ((this.consumeTimes.get() % 5) == 0) { + context.setSuspendCurrentQueueTimeMillis(3000); + return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT; + } + return ConsumeOrderlyStatus.SUCCESS; + + } + }); + + consumer.start(); + + System.out.printf("Consumer Started.%n"); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/order/OrderedProducer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/order/OrderedProducer.java new file mode 100644 index 00000000..6362ad8c --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/order/OrderedProducer.java @@ -0,0 +1,48 @@ +package io.github.dunwu.javatech.rocketmq.order; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.MessageQueueSelector; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageQueue; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +import java.util.List; + +/** + * 发送全局和分区排序的消息。 + * + * @author Zhang Peng + */ +public class OrderedProducer { + + public static void main(String[] args) throws Exception { + //Instantiate with a producer group name. + DefaultMQProducer producer = new DefaultMQProducer("example_group_name"); + // Specify name server addresses. + producer.setNamesrvAddr(RocketConstant.HOST); + //Launch the instance. + producer.start(); + String[] tags = new String[] { "TagA", "TagB", "TagC", "TagD", "TagE" }; + for (int i = 0; i < 100; i++) { + int orderId = i % 10; + //Create a message instance, specifying topic, tag and message body. + Message msg = new Message("TopicTest", tags[i % tags.length], "KEY" + i, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.send(msg, new MessageQueueSelector() { + @Override + public MessageQueue select(List mqs, Message msg, Object arg) { + Integer id = (Integer) arg; + int index = id % mqs.size(); + return mqs.get(index); + } + }, orderId); + + System.out.printf("%s%n", sendResult); + } + //server shutdown + producer.shutdown(); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/order/README.md b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/order/README.md new file mode 100644 index 00000000..1ebbff2f --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/order/README.md @@ -0,0 +1,7 @@ +# Rocket 官方示例之 Order Message Example + +- RocketMQ 使用 FIFO 顺序提供有序消息。 + +## 参考资料 + +- [Order Example](https://rocketmq.apache.org/docs/order-example/) diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/scheduled/README.md b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/scheduled/README.md new file mode 100644 index 00000000..c233a4ca --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/scheduled/README.md @@ -0,0 +1,7 @@ +# Rocket 官方示例之 Schedule Example + +- 定时消息与普通消息的不同之处在于,它们要等到指定的时间后才会发送。 + +## 参考资料 + +- [Schedule Example](https://rocketmq.apache.org/docs/schedule-example/) diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/scheduled/ScheduledMessageConsumer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/scheduled/ScheduledMessageConsumer.java new file mode 100644 index 00000000..2724afa4 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/scheduled/ScheduledMessageConsumer.java @@ -0,0 +1,44 @@ +package io.github.dunwu.javatech.rocketmq.scheduled; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.List; + +/** + * 接收定时消息 + * + * @author Zhang Peng + */ +public class ScheduledMessageConsumer { + + public static void main(String[] args) throws Exception { + // Instantiate message consumer + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer"); + // Specify name server addresses. + consumer.setNamesrvAddr(RocketConstant.HOST); + // Subscribe topics + consumer.subscribe("TestTopic", "*"); + // Register message listener + consumer.registerMessageListener(new MessageListenerConcurrently() { + @Override + public ConsumeConcurrentlyStatus consumeMessage(List messages, + ConsumeConcurrentlyContext context) { + for (MessageExt message : messages) { + // Print approximate delay time period + System.out.println( + "Receive message[msgId=" + message.getMsgId() + "] " + (System.currentTimeMillis() + - message.getStoreTimestamp()) + "ms later"); + } + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + // Launch consumer + consumer.start(); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/scheduled/ScheduledMessageProducer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/scheduled/ScheduledMessageProducer.java new file mode 100644 index 00000000..855fd5e1 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/scheduled/ScheduledMessageProducer.java @@ -0,0 +1,34 @@ +package io.github.dunwu.javatech.rocketmq.scheduled; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.message.Message; + +/** + * 发送定时消息 + * + * @author Zhang Peng + */ +public class ScheduledMessageProducer { + + public static void main(String[] args) throws Exception { + // Instantiate a producer to send scheduled messages + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); + // Specify name server addresses. + producer.setNamesrvAddr(RocketConstant.HOST); + // Launch producer + producer.start(); + int totalMessagesToSend = 100; + for (int i = 0; i < totalMessagesToSend; i++) { + Message message = new Message("TestTopic", ("Hello scheduled message " + i).getBytes()); + // This message will be delivered to consumer 10 seconds later. + message.setDelayTimeLevel(3); + // Send the message + producer.send(message); + } + + // Shutdown producer after use. + producer.shutdown(); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/AsyncProducer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/AsyncProducer.java new file mode 100644 index 00000000..4640fd94 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/AsyncProducer.java @@ -0,0 +1,60 @@ +package io.github.dunwu.javatech.rocketmq.simple; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendCallback; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * 异步发送 RocketMQ 消息示例 + * + * 异步传输通常用于响应时间敏感的业务场景。 + * + * @author Zhang Peng + */ +public class AsyncProducer { + + public static void main(String[] args) throws Exception { + //Instantiate with a producer group name. + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // Specify name server addresses. + producer.setNamesrvAddr(RocketConstant.HOST); + //Launch the instance. + producer.start(); + producer.setRetryTimesWhenSendAsyncFailed(0); + + int messageCount = 100; + final CountDownLatch countDownLatch = new CountDownLatch(messageCount); + for (int i = 0; i < messageCount; i++) { + try { + final int index = i; + Message msg = new Message("Jodie_topic_1023", "TagA", "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + producer.send(msg, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + countDownLatch.countDown(); + System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId()); + } + + @Override + public void onException(Throwable e) { + countDownLatch.countDown(); + System.out.printf("%-10d Exception %s %n", index, e); + e.printStackTrace(); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + countDownLatch.await(5, TimeUnit.SECONDS); + producer.shutdown(); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/Consumer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/Consumer.java new file mode 100644 index 00000000..247dd27a --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/Consumer.java @@ -0,0 +1,41 @@ +package io.github.dunwu.javatech.rocketmq.simple; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.List; + +public class Consumer { + + public static void main(String[] args) throws InterruptedException, MQClientException { + + // Instantiate with specified consumer group name. + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); + + // Specify name server addresses. + consumer.setNamesrvAddr(RocketConstant.HOST); + + // Subscribe one more more topics to consume. + consumer.subscribe("TopicTest", "*"); + // Register callback to execute on arrival of messages fetched from brokers. + consumer.registerMessageListener(new MessageListenerConcurrently() { + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + + //Launch the consumer instance. + consumer.start(); + + System.out.printf("Consumer Started.%n"); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/OnewayProducer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/OnewayProducer.java new file mode 100644 index 00000000..638fa690 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/OnewayProducer.java @@ -0,0 +1,36 @@ +package io.github.dunwu.javatech.rocketmq.simple; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +/** + * 单向发送 RocketMQ 消息示例 + *

+ * 单向传输用于需要中等可靠性的情况,例如日志收集。 + * + * @author Zhang Peng + */ +public class OnewayProducer { + + public static void main(String[] args) throws Exception { + //Instantiate with a producer group name. + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // Specify name server addresses. + producer.setNamesrvAddr(RocketConstant.HOST); + //Launch the instance. + producer.start(); + for (int i = 0; i < 100; i++) { + //Create a message instance, specifying topic, tag and message body. + Message msg = new Message("TopicTest" /* Topic */, "TagA" /* Tag */, ("Hello RocketMQ " + i).getBytes( + RemotingHelper.DEFAULT_CHARSET) /* Message body */); + //Call send message to deliver message to one of brokers. + producer.sendOneway(msg); + } + //Wait for sending to complete + Thread.sleep(5000); + producer.shutdown(); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/README.md b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/README.md new file mode 100644 index 00000000..4ebebf39 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/README.md @@ -0,0 +1,8 @@ +# Rocket 官方示例之 Simple Message Example + +- 使用 RocketMQ 通过三种方式发送消息:可靠同步、可靠异步、单向传输。 +- 使用 RocketMQ 消费消息。 + +## 参考资料 + +- [Simple Message Example](https://rocketmq.apache.org/docs/simple-example/) diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/SyncProducer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/SyncProducer.java new file mode 100644 index 00000000..55bff356 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/simple/SyncProducer.java @@ -0,0 +1,37 @@ +package io.github.dunwu.javatech.rocketmq.simple; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +/** + * 同步发送 RocketMQ 消息示例 + *

+ * 可靠同步传输应用场景广泛,如重要通知消息、短信通知、短信营销系统等。 + * + * @author Zhang Peng + */ +public class SyncProducer { + + public static void main(String[] args) throws Exception { + //Instantiate with a producer group name. + DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); + // Specify name server addresses. + producer.setNamesrvAddr(RocketConstant.HOST); + //Launch the instance. + producer.start(); + for (int i = 0; i < 100; i++) { + //Create a message instance, specifying topic, tag and message body. + Message msg = new Message("TopicTest" /* Topic */, "TagA" /* Tag */, ("Hello RocketMQ " + i).getBytes( + RemotingHelper.DEFAULT_CHARSET) /* Message body */); + //Call send message to deliver message to one of brokers. + SendResult sendResult = producer.send(msg); + System.out.printf("%s%n", sendResult); + } + //Shut down once the producer instance is not longer in use. + producer.shutdown(); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/transaction/TransactionConsumer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/transaction/TransactionConsumer.java new file mode 100644 index 00000000..d8a28443 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/transaction/TransactionConsumer.java @@ -0,0 +1,41 @@ +package io.github.dunwu.javatech.rocketmq.transaction; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext; +import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus; +import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.List; + +public class TransactionConsumer { + + public static void main(String[] args) throws InterruptedException, MQClientException { + + // Instantiate with specified consumer group name. + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); + + // Specify name server addresses. + consumer.setNamesrvAddr(RocketConstant.HOST); + + // Subscribe one more more topics to consume. + consumer.subscribe("TopicTestTx", "*"); + // Register callback to execute on arrival of messages fetched from brokers. + consumer.registerMessageListener(new MessageListenerConcurrently() { + + @Override + public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context) { + System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); + return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; + } + }); + + //Launch the consumer instance. + consumer.start(); + + System.out.printf("Consumer Started.%n"); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/transaction/TransactionListenerImpl.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/transaction/TransactionListenerImpl.java new file mode 100644 index 00000000..fe2f2d83 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/transaction/TransactionListenerImpl.java @@ -0,0 +1,41 @@ +package io.github.dunwu.javatech.rocketmq.transaction; + +import org.apache.rocketmq.client.producer.LocalTransactionState; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.common.message.MessageExt; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +public class TransactionListenerImpl implements TransactionListener { + + private AtomicInteger transactionIndex = new AtomicInteger(0); + + private ConcurrentHashMap localTrans = new ConcurrentHashMap<>(); + + @Override + public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { + int value = transactionIndex.getAndIncrement(); + int status = value % 3; + localTrans.put(msg.getTransactionId(), status); + return LocalTransactionState.UNKNOW; + } + + @Override + public LocalTransactionState checkLocalTransaction(MessageExt msg) { + Integer status = localTrans.get(msg.getTransactionId()); + if (null != status) { + switch (status) { + case 0: + return LocalTransactionState.UNKNOW; + case 1: + return LocalTransactionState.COMMIT_MESSAGE; + case 2: + return LocalTransactionState.ROLLBACK_MESSAGE; + } + } + return LocalTransactionState.COMMIT_MESSAGE; + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/transaction/TransactionProducer.java b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/transaction/TransactionProducer.java new file mode 100644 index 00000000..b0aabfa5 --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/transaction/TransactionProducer.java @@ -0,0 +1,56 @@ +package io.github.dunwu.javatech.rocketmq.transaction; + +import io.github.dunwu.javatech.rocketmq.RocketConstant; +import org.apache.rocketmq.client.exception.MQClientException; +import org.apache.rocketmq.client.producer.SendResult; +import org.apache.rocketmq.client.producer.TransactionListener; +import org.apache.rocketmq.client.producer.TransactionMQProducer; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +import java.io.UnsupportedEncodingException; +import java.util.concurrent.*; + +public class TransactionProducer { + + public static void main(String[] args) throws MQClientException, InterruptedException { + TransactionListener transactionListener = new TransactionListenerImpl(); + TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); + producer.setNamesrvAddr(RocketConstant.HOST); + ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, + new ArrayBlockingQueue(2000), + new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r); + thread.setName( + "client-transaction-msg-check-thread"); + return thread; + } + }); + + producer.setExecutorService(executorService); + producer.setTransactionListener(transactionListener); + producer.start(); + + String[] tags = new String[] { "TagA", "TagB", "TagC", "TagD", "TagE" }; + for (int i = 0; i < 10; i++) { + try { + Message msg = new Message("TopicTestTx", tags[i % tags.length], "KEY" + i, + ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); + SendResult sendResult = producer.sendMessageInTransaction(msg, null); + System.out.printf("%s%n", sendResult); + + Thread.sleep(10); + } catch (MQClientException | UnsupportedEncodingException e) { + e.printStackTrace(); + } + } + + for (int i = 0; i < 100000; i++) { + Thread.sleep(1000); + } + producer.shutdown(); + } + +} diff --git a/codes/javatech/javatech-mq/javatech-rocketmq/src/main/resources/logback.xml b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/resources/logback.xml new file mode 100644 index 00000000..0645b04e --- /dev/null +++ b/codes/javatech/javatech-mq/javatech-rocketmq/src/main/resources/logback.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + 10 + 100 + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%p] %c{36}#%M - %m%n + + + + + + ${LOG_PATH}/logs/${FILE_NAME}-error.%d{yyyy-MM-dd}.log + 30 + + + + + 10MB + + + + ERROR + ACCEPT + DENY + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%p] %c{36}#%M - %m%n + + + + + 1024 + 80 + 2000 + true + + + + + + + + + + + + diff --git a/codes/javatech/javatech-mq/pom.xml b/codes/javatech/javatech-mq/pom.xml new file mode 100644 index 00000000..2f13cdbb --- /dev/null +++ b/codes/javatech/javatech-mq/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + io.github.dunwu.javatech + javatech-mq + 1.0.0 + pom + JAVATECH-MQ示例 + + + javatech-kafka + javatech-kafka-springboot + javatech-rocketmq + + diff --git a/codes/javatech/javatech-others/javatech-cli/pom.xml b/codes/javatech/javatech-others/javatech-cli/pom.xml new file mode 100644 index 00000000..8b946f64 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-cli/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 1.0.8 + + + io.github.dunwu.javatech + javatech-cli + 1.0.0 + jar + JAVATECH-其他示例-命令行 + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + 5.4.0 + + + + + org.apache.commons + commons-lang3 + + + commons-cli + commons-cli + 1.4 + + + com.github.oshi + oshi-core + 4.1.0 + + + net.java.dev.jna + jna + + + net.java.dev.jna + jna-platform + + + + + net.java.dev.jna + jna + ${jna.version} + + + net.java.dev.jna + jna-platform + ${jna.version} + + + net.java.dev.jna + jna + + + + + junit + junit + test + + + + diff --git a/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/AnsiSystem.java b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/AnsiSystem.java new file mode 100644 index 00000000..dd2df754 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/AnsiSystem.java @@ -0,0 +1,244 @@ +package io.github.dunwu.javatech; + +import io.github.dunwu.javatech.constant.AnsiBgColor; +import io.github.dunwu.javatech.constant.AnsiColor; +import io.github.dunwu.javatech.constant.AnsiSgr; +import io.github.dunwu.javatech.constant.Color; +import org.apache.commons.lang3.StringUtils; + +/** + * 以 Ansi 方式在控制台输出(输出彩色字体、粗体、斜体、下划线等) + * + * @author Zhang Peng + * @see ANSI escape code + * @since 2019/10/30 + */ +public class AnsiSystem { + + public static final AnsiSystem RED = new AnsiSystem("\033[;31m"); + + public static final AnsiSystem GREEN = new AnsiSystem("\033[;32m"); + + public static final AnsiSystem YELLOW = new AnsiSystem("\033[;33m"); + + public static final AnsiSystem BLUE = new AnsiSystem("\033[;34m"); + + public static final AnsiSystem MAGENTA = new AnsiSystem("\033[;35m"); + + public static final AnsiSystem CYAN = new AnsiSystem("\033[;36m"); + + public static final AnsiSystem WHITE = new AnsiSystem("\033[;37m"); + + private static final String ENCODE_JOIN = ";"; + + private static final String ENCODE_START = "\033["; + + private static final String ENCODE_END = "m"; + + private static final String RESET = "\033[0;m"; + + private String code; + + public AnsiSystem(String code) { + this.code = code; + } + + public AnsiSystem(AnsiConfig config) { + this.code = encode(config); + } + + private String encode(AnsiConfig config) { + StringBuilder sb = new StringBuilder(); + sb.append(ENCODE_START); + if (config.isBold()) { + sb.append(ENCODE_JOIN).append(AnsiSgr.BOLD.getCode()); + } + if (config.isItalic()) { + sb.append(ENCODE_JOIN).append(AnsiSgr.ITALIC.getCode()); + } + if (config.isUnderline()) { + sb.append(ENCODE_JOIN).append(AnsiSgr.UNDERLINE.getCode()); + } + if (config.isSlowBlink()) { + sb.append(ENCODE_JOIN).append(AnsiSgr.SLOW_BLINK.getCode()); + } else { + if (config.isRapidBlink()) { + sb.append(ENCODE_JOIN).append(AnsiSgr.RAPID_BLINK.getCode()); + } + } + if (config.isReverseVideo()) { + sb.append(ENCODE_JOIN).append(AnsiSgr.REVERSE_VIDEO.getCode()); + } + if (config.isCanceal()) { + sb.append(ENCODE_JOIN).append(AnsiSgr.CONCEAL.getCode()); + } + if (config.getColor() != null) { + AnsiColor color = AnsiColor.valueOf(config.getColor().name()); + if (StringUtils.isNotBlank(color.getCode())) { + sb.append(ENCODE_JOIN).append(color.getCode()); + } + } + if (config.getBgColor() != null) { + AnsiBgColor color = AnsiBgColor.valueOf(config.getBgColor().name()); + if (StringUtils.isNotBlank(color.getCode())) { + sb.append(ENCODE_JOIN).append(color.getCode()); + } + } + sb.append(ENCODE_END); + return sb.toString(); + } + + public void print(String message) { + System.out.print(code + message + RESET); + } + + public void println(String message) { + System.out.println(code + message + RESET); + } + + public String getCode() { + return code; + } + + public void setCode(String code) { + this.code = code; + } + + /** + * Ansi 配置 + */ + public static class AnsiConfig { + + private boolean bold; + + private boolean italic; + + private boolean underline; + + private boolean slowBlink; + + private boolean rapidBlink; + + private boolean reverseVideo; + + private boolean canceal; + + private Color color; + + private Color bgColor; + + public AnsiConfig() { + this.bold = false; + this.italic = false; + this.underline = false; + this.slowBlink = false; + this.rapidBlink = false; + this.reverseVideo = false; + this.canceal = false; + this.color = Color.DEFAULT; + this.bgColor = Color.DEFAULT; + } + + public AnsiConfig(boolean bold, boolean italic, boolean underline, boolean slowBlink, boolean rapidBlink, + boolean reverseVideo, boolean canceal, Color color, Color bgColor) { + this.bold = bold; + this.italic = italic; + this.underline = underline; + this.slowBlink = slowBlink; + this.rapidBlink = rapidBlink; + this.reverseVideo = reverseVideo; + this.canceal = canceal; + this.color = color; + this.bgColor = bgColor; + } + + @Override + public String toString() { + return "AnsiParam{" + + "bold=" + bold + + ", italic=" + italic + + ", underline=" + underline + + ", slowBlink=" + slowBlink + + ", rapidBlink=" + rapidBlink + + ", reverseVideo=" + reverseVideo + + ", canceal=" + canceal + + ", color=" + color + + ", bgColor=" + bgColor + + '}'; + } + + public boolean isBold() { + return bold; + } + + public void setBold(boolean bold) { + this.bold = bold; + } + + public boolean isItalic() { + return italic; + } + + public void setItalic(boolean italic) { + this.italic = italic; + } + + public boolean isUnderline() { + return underline; + } + + public void setUnderline(boolean underline) { + this.underline = underline; + } + + public boolean isSlowBlink() { + return slowBlink; + } + + public void setSlowBlink(boolean slowBlink) { + this.slowBlink = slowBlink; + } + + public boolean isRapidBlink() { + return rapidBlink; + } + + public void setRapidBlink(boolean rapidBlink) { + this.rapidBlink = rapidBlink; + } + + public boolean isReverseVideo() { + return reverseVideo; + } + + public void setReverseVideo(boolean reverseVideo) { + this.reverseVideo = reverseVideo; + } + + public boolean isCanceal() { + return canceal; + } + + public void setCanceal(boolean canceal) { + this.canceal = canceal; + } + + public Color getColor() { + return color; + } + + public void setColor(Color color) { + this.color = color; + } + + public Color getBgColor() { + return bgColor; + } + + public void setBgColor(Color bgColor) { + this.bgColor = bgColor; + } + + } + +} diff --git a/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/CliDemo.java b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/CliDemo.java new file mode 100644 index 00000000..a6037c7d --- /dev/null +++ b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/CliDemo.java @@ -0,0 +1,56 @@ +package io.github.dunwu.javatech; + +import org.apache.commons.cli.ParseException; + +import java.util.Date; +import java.util.Properties; +import java.util.Scanner; + +/** + * @author Zhang Peng + * @since 2019/10/29 + */ +public class CliDemo { + + public static void main(String[] args) throws ParseException { + + while (true) { + Scanner scanner = new Scanner(System.in); + String param = ""; + if (scanner.hasNext()) { + param = scanner.next(); + } + + switch (param) { + case "date": + AnsiSystem.BLUE.println("date = " + new Date()); + break; + case "area": + AnsiSystem.BLUE.println("area = " + "China"); + break; + case "system": + Properties props = System.getProperties(); + System.out.println("Java的运行环境版本:" + props.getProperty("java.version")); + System.out.println("默认的临时文件路径:" + props.getProperty("java.io.tmpdir")); + System.out.println("操作系统的名称:" + props.getProperty("os.name")); + System.out.println("操作系统的构架:" + props.getProperty("os.arch")); + System.out.println("操作系统的版本:" + props.getProperty("os.version")); + System.out.println("用户的账户名称:" + props.getProperty("user.name")); + System.out.println("用户的主目录:" + props.getProperty("user.home")); + System.out.println("用户的当前工作目录:" + props.getProperty("user.dir")); + System.out.println("操作系统:" + props.getProperty("sun.desktop")); + System.out.println("CPU个数:" + Runtime.getRuntime().availableProcessors()); + System.out.println("虚拟机内存总量:" + Runtime.getRuntime().totalMemory()); + System.out.println("虚拟机空闲内存量:" + Runtime.getRuntime().freeMemory()); + System.out.println("虚拟机使用最大内存量:" + Runtime.getRuntime().maxMemory()); + break; + case "exit": + return; + default: + System.err.println("invalid param"); + break; + } + } + } + +} diff --git a/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/CliUtil.java b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/CliUtil.java new file mode 100644 index 00000000..0878bdd6 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/CliUtil.java @@ -0,0 +1,28 @@ +package io.github.dunwu.javatech; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; + +public class CliUtil { + + public static void prepare(String[] args) throws Exception { + // commons-cli命令行参数,需要带参数值 + Options options = new Options(); + // sql文件路径 + options.addOption("sql", true, "sql config"); + // 任务名称 + options.addOption("name", true, "job name"); + + // 解析命令行参数 + CommandLineParser parser = new DefaultParser(); + CommandLine cl = parser.parse(options, args); + String sql = cl.getOptionValue("sql"); + String name = cl.getOptionValue("name"); + + System.out.println("sql : " + sql); + System.out.println("name : " + name); + } + +} diff --git a/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/SystemInfoUtil.java b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/SystemInfoUtil.java new file mode 100644 index 00000000..58363700 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/SystemInfoUtil.java @@ -0,0 +1,265 @@ +package io.github.dunwu.javatech; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import oshi.SystemInfo; +import oshi.hardware.*; +import oshi.software.os.*; +import oshi.util.FormatUtil; +import oshi.util.Util; + +import java.util.Arrays; +import java.util.List; + +/** + * @author Zhang Peng + * @since 2019/10/30 + */ +public class SystemInfoUtil { + + private static Logger logger = LoggerFactory.getLogger(SystemInfoUtil.class); + + public static void main(String[] args) { + + logger.info("Initializing System..."); + SystemInfo systemInfo = new SystemInfo(); + HardwareAbstractionLayer hal = systemInfo.getHardware(); + logger.info("Checking computer system..."); + printComputerSystem(hal.getComputerSystem()); + logger.info("Checking Processor..."); + printProcessor(hal.getProcessor()); + logger.info("Checking Memory..."); + printMemory(hal.getMemory()); + logger.info("Checking CPU..."); + printCpu(hal.getProcessor()); + logger.info("Checking Sensors..."); + printSensors(hal.getSensors()); + logger.info("Checking Power sources..."); + printPowerSources(hal.getPowerSources()); + logger.info("Checking Disks..."); + printDisks(hal.getDiskStores()); + logger.info("Checking Network interfaces..."); + printNetworkInterfaces(hal.getNetworkIFs()); + // hardware: displays + logger.info("Checking Displays..."); + printDisplays(hal.getDisplays()); + // hardware: USB devices + logger.info("Checking USB Devices..."); + printUsbDevices(hal.getUsbDevices(true)); + OperatingSystem os = systemInfo.getOperatingSystem(); + System.out.println(os); + logger.info("Checking Processes..."); + printProcesses(os, hal.getMemory()); + logger.info("Checking File System..."); + printFileSystem(os.getFileSystem()); + logger.info("Checking Network parameterss..."); + printNetworkParameters(os.getNetworkParams()); + } + + public static void printComputerSystem(final ComputerSystem computerSystem) { + System.out.println("manufacturer: " + computerSystem.getManufacturer()); + System.out.println("model: " + computerSystem.getModel()); + System.out.println("serialnumber: " + computerSystem.getSerialNumber()); + final Firmware firmware = computerSystem.getFirmware(); + System.out.println("firmware:"); + System.out.println(" manufacturer: " + firmware.getManufacturer()); + System.out.println(" name: " + firmware.getName()); + System.out.println(" description: " + firmware.getDescription()); + System.out.println(" version: " + firmware.getVersion()); + System.out.println(" release date: " + (firmware.getReleaseDate() == null ? "unknown" + : firmware.getReleaseDate() == null ? "unknown" : firmware.getReleaseDate())); + final Baseboard baseboard = computerSystem.getBaseboard(); + System.out.println("baseboard:"); + System.out.println(" manufacturer: " + baseboard.getManufacturer()); + System.out.println(" model: " + baseboard.getModel()); + System.out.println(" version: " + baseboard.getVersion()); + System.out.println(" serialnumber: " + baseboard.getSerialNumber()); + } + + public static void printProcessor(CentralProcessor processor) { + System.out.println(processor); + System.out.println(" " + processor.getPhysicalProcessorCount() + " physical CPU(s)"); + System.out.println(" " + processor.getLogicalProcessorCount() + " logical CPU(s)"); + System.out.println("Identifier: " + processor.getIdentifier()); + System.out.println("ProcessorID: " + processor.getProcessorID()); + } + + public static void printMemory(GlobalMemory memory) { + System.out.println("以使用内存: " + FormatUtil.formatBytes(memory.getAvailable()) + "总共内存" + + FormatUtil.formatBytes(memory.getTotal())); + } + + public static void printCpu(CentralProcessor processor) { + long[] prevTicks = processor.getSystemCpuLoadTicks(); + System.out.println("CPU, IOWait, and IRQ ticks @ 0 sec:" + Arrays.toString(prevTicks)); + // Wait a second... + Util.sleep(1000); + long[] ticks = processor.getSystemCpuLoadTicks(); + System.out.println("CPU, IOWait, and IRQ ticks @ 1 sec:" + Arrays.toString(ticks)); + long user = + ticks[CentralProcessor.TickType.USER.getIndex()] - prevTicks[CentralProcessor.TickType.USER.getIndex()]; + long nice = + ticks[CentralProcessor.TickType.NICE.getIndex()] - prevTicks[CentralProcessor.TickType.NICE.getIndex()]; + long sys = + ticks[CentralProcessor.TickType.SYSTEM.getIndex()] - prevTicks[CentralProcessor.TickType.SYSTEM.getIndex()]; + long idle = + ticks[CentralProcessor.TickType.IDLE.getIndex()] - prevTicks[CentralProcessor.TickType.IDLE.getIndex()]; + long iowait = + ticks[CentralProcessor.TickType.IOWAIT.getIndex()] - prevTicks[CentralProcessor.TickType.IOWAIT.getIndex()]; + long irq = + ticks[CentralProcessor.TickType.IRQ.getIndex()] - prevTicks[CentralProcessor.TickType.IRQ.getIndex()]; + long softirq = ticks[CentralProcessor.TickType.SOFTIRQ.getIndex()] + - prevTicks[CentralProcessor.TickType.SOFTIRQ.getIndex()]; + long steal = + ticks[CentralProcessor.TickType.STEAL.getIndex()] - prevTicks[CentralProcessor.TickType.STEAL.getIndex()]; + long totalCpu = user + nice + sys + idle + iowait + irq + softirq + steal; + System.out.format( + "User: %.1f%% Nice: %.1f%% System: %.1f%% Idle: %.1f%% IOwait: %.1f%% IRQ: %.1f%% SoftIRQ: %.1f%% Steal: %.1f%%%n", + 100d * user / totalCpu, 100d * nice / totalCpu, 100d * sys / totalCpu, 100d * idle / totalCpu, + 100d * iowait / totalCpu, 100d * irq / totalCpu, 100d * softirq / totalCpu, 100d * steal / totalCpu); + // System.out.format("CPU load: %.1f%% (counting ticks)%n", processor.getSystemCpuLoadTicks() * 100); + // System.out.format("CPU load: %.1f%% (OS MXBean)%n", processor.getProcessorCpuLoadBetweenTicks() * 100); + double[] loadAverage = processor.getSystemLoadAverage(3); + System.out.println("CPU load averages:" + (loadAverage[0] < 0 ? " N/A" : String.format(" %.2f", loadAverage[0])) + + (loadAverage[1] < 0 ? " N/A" : String.format(" %.2f", loadAverage[1])) + + (loadAverage[2] < 0 ? " N/A" : String.format(" %.2f", loadAverage[2]))); + // per core CPU + StringBuilder procCpu = new StringBuilder("CPU load per processor:"); + double[] load = processor.getProcessorCpuLoadBetweenTicks(processor.getProcessorCpuLoadTicks()); + for (double avg : load) { + procCpu.append(String.format(" %.1f%%", avg * 100)); + } + System.out.println(procCpu.toString()); + } + + public static void printSensors(Sensors sensors) { + System.out.println("Sensors:"); + System.out.format(" CPU Temperature: %.1f°C%n", sensors.getCpuTemperature()); + System.out.println(" Fan Speeds: " + Arrays.toString(sensors.getFanSpeeds())); + System.out.format(" CPU Voltage: %.1fV%n", sensors.getCpuVoltage()); + } + + public static void printPowerSources(PowerSource[] powerSources) { + StringBuilder sb = new StringBuilder("Power: "); + if (powerSources.length == 0) { + sb.append("Unknown"); + } else { + double timeRemaining = powerSources[0].getTimeRemaining(); + if (timeRemaining < -1d) { + sb.append("Charging"); + } else if (timeRemaining < 0d) { + sb.append("Calculating time remaining"); + } else { + sb.append(String.format("%d:%02d remaining", (int) (timeRemaining / 3600), + (int) (timeRemaining / 60) % 60)); + } + } + for (PowerSource pSource : powerSources) { + sb.append(String.format("%n %s @ %.1f%%", pSource.getName(), pSource.getRemainingCapacity() * 100d)); + } + System.out.println(sb.toString()); + } + + public static void printDisks(HWDiskStore[] diskStores) { + System.out.println("Disks:"); + for (HWDiskStore disk : diskStores) { + boolean readwrite = disk.getReads() > 0 || disk.getWrites() > 0; + System.out.format(" %s: (model: %s - S/N: %s) size: %s, reads: %s (%s), writes: %s (%s), xfer: %s ms%n", + disk.getName(), disk.getModel(), disk.getSerial(), + disk.getSize() > 0 ? FormatUtil.formatBytesDecimal(disk.getSize()) : "?", + readwrite ? disk.getReads() : "?", readwrite ? FormatUtil.formatBytes(disk.getReadBytes()) : "?", + readwrite ? disk.getWrites() : "?", readwrite ? FormatUtil.formatBytes(disk.getWriteBytes()) : "?", + readwrite ? disk.getTransferTime() : "?"); + HWPartition[] partitions = disk.getPartitions(); + if (partitions == null) { + // TODO Remove when all OS's implemented + continue; + } + for (HWPartition part : partitions) { + System.out.format(" |-- %s: %s (%s) Maj:Min=%d:%d, size: %s%s%n", part.getIdentification(), + part.getName(), part.getType(), part.getMajor(), part.getMinor(), + FormatUtil.formatBytesDecimal(part.getSize()), + part.getMountPoint().isEmpty() ? "" : " @ " + part.getMountPoint()); + } + } + } + + public static void printNetworkInterfaces(NetworkIF[] networkIFs) { + System.out.println("Network interfaces:"); + for (NetworkIF net : networkIFs) { + System.out.format(" Name: %s (%s)%n", net.getName(), net.getDisplayName()); + System.out.format(" MAC Address: %s %n", net.getMacaddr()); + System.out.format(" MTU: %s, Speed: %s %n", net.getMTU(), FormatUtil.formatValue(net.getSpeed(), "bps")); + System.out.format(" IPv4: %s %n", Arrays.toString(net.getIPv4addr())); + System.out.format(" IPv6: %s %n", Arrays.toString(net.getIPv6addr())); + boolean hasData = net.getBytesRecv() > 0 || net.getBytesSent() > 0 || net.getPacketsRecv() > 0 + || net.getPacketsSent() > 0; + System.out.format(" Traffic: received %s/%s%s; transmitted %s/%s%s %n", + hasData ? net.getPacketsRecv() + " packets" : "?", + hasData ? FormatUtil.formatBytes(net.getBytesRecv()) : "?", + hasData ? " (" + net.getInErrors() + " err)" : "", + hasData ? net.getPacketsSent() + " packets" : "?", + hasData ? FormatUtil.formatBytes(net.getBytesSent()) : "?", + hasData ? " (" + net.getOutErrors() + " err)" : ""); + } + } + + public static void printDisplays(Display[] displays) { + System.out.println("Displays:"); + int i = 0; + for (Display display : displays) { + System.out.println(" Display " + i + ":"); + System.out.println(display.toString()); + i++; + } + } + + public static void printUsbDevices(UsbDevice[] usbDevices) { + System.out.println("USB Devices:"); + for (UsbDevice usbDevice : usbDevices) { + System.out.println(usbDevice.toString()); + } + } + + public static void printProcesses(OperatingSystem os, GlobalMemory memory) { + System.out.println("Processes: " + os.getProcessCount() + ", Threads: " + os.getThreadCount()); + // Sort by highest CPU + List procs = Arrays.asList(os.getProcesses(5, OperatingSystem.ProcessSort.CPU)); + System.out.println(" PID %CPU %MEM VSZ RSS Name"); + for (int i = 0; i < procs.size() && i < 5; i++) { + OSProcess p = procs.get(i); + System.out.format(" %5d %5.1f %4.1f %9s %9s %s%n", p.getProcessID(), + 100d * (p.getKernelTime() + p.getUserTime()) / p.getUpTime(), + 100d * p.getResidentSetSize() / memory.getTotal(), FormatUtil.formatBytes(p.getVirtualSize()), + FormatUtil.formatBytes(p.getResidentSetSize()), p.getName()); + } + } + + public static void printFileSystem(FileSystem fileSystem) { + System.out.println("File System:"); + System.out.format(" File Descriptors: %d/%d%n", fileSystem.getOpenFileDescriptors(), + fileSystem.getMaxFileDescriptors()); + OSFileStore[] fsArray = fileSystem.getFileStores(); + for (OSFileStore fs : fsArray) { + long usable = fs.getUsableSpace(); + long total = fs.getTotalSpace(); + System.out.format( + " %s (%s) [%s] %s of %s free (%.1f%%) is %s " + + (fs.getLogicalVolume() != null && fs.getLogicalVolume().length() > 0 ? "[%s]" : "%s") + + " and is mounted at %s%n", + fs.getName(), fs.getDescription().isEmpty() ? "file system" : fs.getDescription(), fs.getType(), + FormatUtil.formatBytes(usable), FormatUtil.formatBytes(fs.getTotalSpace()), 100d * usable / total, + fs.getVolume(), fs.getLogicalVolume(), fs.getMount()); + } + } + + public static void printNetworkParameters(NetworkParams networkParams) { + System.out.println("Network parameters:"); + System.out.format(" Host name: %s%n", networkParams.getHostName()); + System.out.format(" Domain name: %s%n", networkParams.getDomainName()); + System.out.format(" DNS servers: %s%n", Arrays.toString(networkParams.getDnsServers())); + System.out.format(" IPv4 Gateway: %s%n", networkParams.getIpv4DefaultGateway()); + System.out.format(" IPv6 Gateway: %s%n", networkParams.getIpv6DefaultGateway()); + } + +} diff --git a/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiBgColor.java b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiBgColor.java new file mode 100644 index 00000000..86f99a0c --- /dev/null +++ b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiBgColor.java @@ -0,0 +1,44 @@ +package io.github.dunwu.javatech.constant; + +/** + * ANSI 背景显示颜色枚举 + * + * @author Zhang Peng + * @see ANSI Colors + * @since 2019/10/30 + */ +public enum AnsiBgColor implements AnsiElement { + + DEFAULT(""), + BLACK("40"), + RED("41"), + GREEN("42"), + YELLOW("43"), + BLUE("44"), + MAGENTA("45"), + CYAN("46"), + WHITE("47"), + BRIGHT_BLACK("100"), + BRIGHT_RED("101"), + BRIGHT_GREEN("102"), + BRIGHT_YELLOW("109"), + BRIGHT_BLUE("104"), + BRIGHT_MAGENTA("105"), + BRIGHT_CYAN("106"), + BRIGHT_WHITE("107"); + + private final String code; + + AnsiBgColor(String code) { + this.code = code; + } + + @Override + public String toString() { + return code; + } + + public String getCode() { + return code; + } +} diff --git a/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiColor.java b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiColor.java new file mode 100644 index 00000000..38689728 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiColor.java @@ -0,0 +1,44 @@ +package io.github.dunwu.javatech.constant; + +/** + * ANSI 字体显示颜色枚举 + * + * @author Zhang Peng + * @see ANSI Colors + * @since 2019/10/30 + */ +public enum AnsiColor implements AnsiElement { + + DEFAULT(""), + BLACK("30"), + RED("31"), + GREEN("32"), + YELLOW("33"), + BLUE("34"), + MAGENTA("35"), + CYAN("36"), + WHITE("37"), + BRIGHT_BLACK("90"), + BRIGHT_RED("91"), + BRIGHT_GREEN("92"), + BRIGHT_YELLOW("99"), + BRIGHT_BLUE("94"), + BRIGHT_MAGENTA("95"), + BRIGHT_CYAN("96"), + BRIGHT_WHITE("97"); + + private final String code; + + AnsiColor(String code) { + this.code = code; + } + + @Override + public String toString() { + return code; + } + + public String getCode() { + return code; + } +} diff --git a/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiElement.java b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiElement.java new file mode 100644 index 00000000..0b24d5e4 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiElement.java @@ -0,0 +1,11 @@ +package io.github.dunwu.javatech.constant; + +public interface AnsiElement { + + /** + * @return the ANSI escape code + */ + @Override + String toString(); + +} diff --git a/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiSgr.java b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiSgr.java new file mode 100644 index 00000000..f4db6744 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiSgr.java @@ -0,0 +1,42 @@ +package io.github.dunwu.javatech.constant; + +/** + * SGR (Select Graphic Rendition) 设置显示属性。 + *

+ * 可以按相同的顺序设置多个属性,并用分号隔开。 + *

+ * 每个显示属性一直有效,直到随后发生SGR重置它为止。 + *

+ * 如果未给出代码,则将CSI m视为CSI 0 m(重置/正常)。 + * + * @author Zhang Peng + * @see SGR + * @since 2019/10/30 + */ +public enum AnsiSgr implements AnsiElement { + + NORMAL("0"), + BOLD("1"), + FAINT("2"), + ITALIC("3"), + UNDERLINE("4"), + SLOW_BLINK("5"), + RAPID_BLINK("6"), + REVERSE_VIDEO("7"), + CONCEAL("8"); + + private final String code; + + AnsiSgr(String code) { + this.code = code; + } + + @Override + public String toString() { + return code; + } + + public String getCode() { + return code; + } +} diff --git a/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/Color.java b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/Color.java new file mode 100644 index 00000000..31b58103 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/Color.java @@ -0,0 +1,36 @@ +package io.github.dunwu.javatech.constant; + +/** + * @author Zhang Peng + * @since 2019/10/30 + */ +public enum Color { + + DEFAULT("默认"), + BLACK("黑色"), + RED("红色"), + GREEN("绿色"), + YELLOW("黄色"), + BLUE("蓝色"), + MAGENTA("紫色"), + CYAN("青色"), + WHITE("白色"), + BRIGHT_BLACK("亮黑色"), + BRIGHT_RED("亮红色"), + BRIGHT_GREEN("亮绿色"), + BRIGHT_YELLOW("亮黄色"), + BRIGHT_BLUE("亮蓝色"), + BRIGHT_MAGENTA("亮紫色"), + BRIGHT_CYAN("亮青色"), + BRIGHT_WHITE("亮白色"); + + private String desc; + + Color(String desc) { + this.desc = desc; + } + + public String getDesc() { + return desc; + } +} diff --git a/codes/javatech/javatech-others/javatech-cli/src/test/java/io/github/dunwu/javatech/CliUtilTests.java b/codes/javatech/javatech-others/javatech-cli/src/test/java/io/github/dunwu/javatech/CliUtilTests.java new file mode 100644 index 00000000..13286c15 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-cli/src/test/java/io/github/dunwu/javatech/CliUtilTests.java @@ -0,0 +1,17 @@ +package io.github.dunwu.javatech; + +import org.junit.Test; + +/** + * @author Zhang Peng + * @since 2019/10/29 + */ +public class CliUtilTests { + + @Test + public void prepare() throws Exception { + String[] args = { "-sql", "select * from aa", "-name", "测试" }; + CliUtil.prepare(args); + } + +} diff --git a/codes/javatech/javatech-others/javatech-cli/src/test/java/io/github/dunwu/javatech/SystemInfoTest.java b/codes/javatech/javatech-others/javatech-cli/src/test/java/io/github/dunwu/javatech/SystemInfoTest.java new file mode 100644 index 00000000..05e945a5 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-cli/src/test/java/io/github/dunwu/javatech/SystemInfoTest.java @@ -0,0 +1,320 @@ +package io.github.dunwu.javatech; + +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import oshi.PlatformEnum; +import oshi.SystemInfo; +import oshi.hardware.*; +import oshi.hardware.CentralProcessor.TickType; +import oshi.software.os.*; +import oshi.software.os.OperatingSystem.ProcessSort; +import oshi.util.FormatUtil; +import oshi.util.Util; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.Assert.assertFalse; + +/** + * A demonstration of access to many of OSHI's capabilities + */ +public class SystemInfoTest { + + private static final Logger logger = LoggerFactory.getLogger(SystemInfoTest.class); + + static List oshi = new ArrayList<>(); + + /** + * Test that this platform is implemented.. + */ + @Test + public void testPlatformEnum() { + assertFalse(PlatformEnum.UNKNOWN.equals(SystemInfo.getCurrentPlatformEnum())); + // Exercise the main method + main(null); + } + + /** + * The main method, demonstrating use of classes. + * + * @param args the arguments (unused) + */ + public static void main(String[] args) { + logger.info("Initializing System..."); + SystemInfo si = new SystemInfo(); + + HardwareAbstractionLayer hal = si.getHardware(); + OperatingSystem os = si.getOperatingSystem(); + + printOperatingSystem(os); + + logger.info("Checking computer system..."); + printComputerSystem(hal.getComputerSystem()); + + logger.info("Checking Processor..."); + printProcessor(hal.getProcessor()); + + logger.info("Checking Memory..."); + printMemory(hal.getMemory()); + + logger.info("Checking CPU..."); + printCpu(hal.getProcessor()); + + logger.info("Checking Processes..."); + printProcesses(os, hal.getMemory()); + + logger.info("Checking Services..."); + printServices(os); + + logger.info("Checking Sensors..."); + printSensors(hal.getSensors()); + + logger.info("Checking Power sources..."); + printPowerSources(hal.getPowerSources()); + + logger.info("Checking Disks..."); + printDisks(hal.getDiskStores()); + + logger.info("Checking File System..."); + printFileSystem(os.getFileSystem()); + + logger.info("Checking Network interfaces..."); + printNetworkInterfaces(hal.getNetworkIFs()); + + logger.info("Checking Network parameters..."); + printNetworkParameters(os.getNetworkParams()); + + // hardware: displays + logger.info("Checking Displays..."); + printDisplays(hal.getDisplays()); + + // hardware: USB devices + logger.info("Checking USB Devices..."); + printUsbDevices(hal.getUsbDevices(true)); + + logger.info("Checking Sound Cards..."); + printSoundCards(hal.getSoundCards()); + + StringBuilder output = new StringBuilder(); + for (int i = 0; i < oshi.size(); i++) { + output.append(oshi.get(i)); + if (oshi.get(i) != null && !oshi.get(i).endsWith("\n")) { + output.append('\n'); + } + } + logger.info("Printing Operating System and Hardware Info:{}{}", '\n', output); + } + + private static void printOperatingSystem(final OperatingSystem os) { + oshi.add(String.valueOf(os)); + oshi.add("Booted: " + Instant.ofEpochSecond(os.getSystemBootTime())); + oshi.add("Uptime: " + FormatUtil.formatElapsedSecs(os.getSystemUptime())); + oshi.add("Running with" + (os.isElevated() ? "" : "out") + " elevated permissions."); + } + + private static void printComputerSystem(final ComputerSystem computerSystem) { + oshi.add("system: " + computerSystem.toString()); + oshi.add(" firmware: " + computerSystem.getFirmware().toString()); + oshi.add(" baseboard: " + computerSystem.getBaseboard().toString()); + } + + private static void printProcessor(CentralProcessor processor) { + oshi.add(processor.toString()); + } + + private static void printMemory(GlobalMemory memory) { + oshi.add("Memory: \n " + memory.toString()); + VirtualMemory vm = memory.getVirtualMemory(); + oshi.add("Swap: \n " + vm.toString()); + PhysicalMemory[] pmArray = memory.getPhysicalMemory(); + if (pmArray.length > 0) { + oshi.add("Physical Memory: "); + for (PhysicalMemory pm : pmArray) { + oshi.add(" " + pm.toString()); + } + } + } + + private static void printCpu(CentralProcessor processor) { + oshi.add("Context Switches/Interrupts: " + processor.getContextSwitches() + " / " + processor.getInterrupts()); + + long[] prevTicks = processor.getSystemCpuLoadTicks(); + long[][] prevProcTicks = processor.getProcessorCpuLoadTicks(); + oshi.add("CPU, IOWait, and IRQ ticks @ 0 sec:" + Arrays.toString(prevTicks)); + // Wait a second... + Util.sleep(1000); + long[] ticks = processor.getSystemCpuLoadTicks(); + oshi.add("CPU, IOWait, and IRQ ticks @ 1 sec:" + Arrays.toString(ticks)); + long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()]; + long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()]; + long sys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()]; + long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()]; + long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()]; + long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()]; + long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()]; + long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()]; + long totalCpu = user + nice + sys + idle + iowait + irq + softirq + steal; + + oshi.add(String.format( + "User: %.1f%% Nice: %.1f%% System: %.1f%% Idle: %.1f%% IOwait: %.1f%% IRQ: %.1f%% SoftIRQ: %.1f%% Steal: %.1f%%", + 100d * user / totalCpu, 100d * nice / totalCpu, 100d * sys / totalCpu, 100d * idle / totalCpu, + 100d * iowait / totalCpu, 100d * irq / totalCpu, 100d * softirq / totalCpu, 100d * steal / totalCpu)); + oshi.add(String.format("CPU load: %.1f%%", processor.getSystemCpuLoadBetweenTicks(prevTicks) * 100)); + double[] loadAverage = processor.getSystemLoadAverage(3); + oshi.add("CPU load averages:" + (loadAverage[0] < 0 ? " N/A" : String.format(" %.2f", loadAverage[0])) + + (loadAverage[1] < 0 ? " N/A" : String.format(" %.2f", loadAverage[1])) + + (loadAverage[2] < 0 ? " N/A" : String.format(" %.2f", loadAverage[2]))); + // per core CPU + StringBuilder procCpu = new StringBuilder("CPU load per processor:"); + double[] load = processor.getProcessorCpuLoadBetweenTicks(prevProcTicks); + for (double avg : load) { + procCpu.append(String.format(" %.1f%%", avg * 100)); + } + oshi.add(procCpu.toString()); + long freq = processor.getProcessorIdentifier().getVendorFreq(); + if (freq > 0) { + oshi.add("Vendor Frequency: " + FormatUtil.formatHertz(freq)); + } + freq = processor.getMaxFreq(); + if (freq > 0) { + oshi.add("Max Frequency: " + FormatUtil.formatHertz(freq)); + } + long[] freqs = processor.getCurrentFreq(); + if (freqs[0] > 0) { + StringBuilder sb = new StringBuilder("Current Frequencies: "); + for (int i = 0; i < freqs.length; i++) { + if (i > 0) { + sb.append(", "); + } + sb.append(FormatUtil.formatHertz(freqs[i])); + } + oshi.add(sb.toString()); + } + } + + private static void printProcesses(OperatingSystem os, GlobalMemory memory) { + oshi.add("Processes: " + os.getProcessCount() + ", Threads: " + os.getThreadCount()); + // Sort by highest CPU + List procs = Arrays.asList(os.getProcesses(5, ProcessSort.CPU)); + + oshi.add(" PID %CPU %MEM VSZ RSS Name"); + for (int i = 0; i < procs.size() && i < 5; i++) { + OSProcess p = procs.get(i); + oshi.add(String.format(" %5d %5.1f %4.1f %9s %9s %s", p.getProcessID(), + 100d * (p.getKernelTime() + p.getUserTime()) / p.getUpTime(), + 100d * p.getResidentSetSize() / memory.getTotal(), FormatUtil.formatBytes(p.getVirtualSize()), + FormatUtil.formatBytes(p.getResidentSetSize()), p.getName())); + } + } + + private static void printServices(OperatingSystem os) { + oshi.add("Services: "); + oshi.add(" PID State Name"); + // DO 5 each of running and stopped + int i = 0; + for (OSService s : os.getServices()) { + if (s.getState().equals(OSService.State.RUNNING) && i++ < 5) { + oshi.add(String.format(" %5d %7s %s", s.getProcessID(), s.getState(), s.getName())); + } + } + i = 0; + for (OSService s : os.getServices()) { + if (s.getState().equals(OSService.State.STOPPED) && i++ < 5) { + oshi.add(String.format(" %5d %7s %s", s.getProcessID(), s.getState(), s.getName())); + } + } + } + + private static void printSensors(Sensors sensors) { + oshi.add("Sensors: " + sensors.toString()); + } + + private static void printPowerSources(PowerSource[] powerSources) { + StringBuilder sb = new StringBuilder("Power Sources: "); + if (powerSources.length == 0) { + sb.append("Unknown"); + } + for (PowerSource powerSource : powerSources) { + sb.append("\n ").append(powerSource.toString()); + } + oshi.add(sb.toString()); + } + + private static void printDisks(HWDiskStore[] diskStores) { + oshi.add("Disks:"); + for (HWDiskStore disk : diskStores) { + oshi.add(" " + disk.toString()); + + HWPartition[] partitions = disk.getPartitions(); + for (HWPartition part : partitions) { + oshi.add(" |-- " + part.toString()); + } + } + } + + private static void printFileSystem(FileSystem fileSystem) { + oshi.add("File System:"); + + oshi.add(String.format(" File Descriptors: %d/%d", fileSystem.getOpenFileDescriptors(), + fileSystem.getMaxFileDescriptors())); + + OSFileStore[] fsArray = fileSystem.getFileStores(); + for (OSFileStore fs : fsArray) { + long usable = fs.getUsableSpace(); + long total = fs.getTotalSpace(); + oshi.add(String.format( + " %s (%s) [%s] %s of %s free (%.1f%%), %s of %s files free (%.1f%%) is %s " + + (fs.getLogicalVolume() != null && fs.getLogicalVolume().length() > 0 ? "[%s]" : "%s") + + " and is mounted at %s", + fs.getName(), fs.getDescription().isEmpty() ? "file system" : fs.getDescription(), fs.getType(), + FormatUtil.formatBytes(usable), FormatUtil.formatBytes(fs.getTotalSpace()), 100d * usable / total, + FormatUtil.formatValue(fs.getFreeInodes(), ""), FormatUtil.formatValue(fs.getTotalInodes(), ""), + 100d * fs.getFreeInodes() / fs.getTotalInodes(), fs.getVolume(), fs.getLogicalVolume(), + fs.getMount())); + } + } + + private static void printNetworkInterfaces(NetworkIF[] networkIFs) { + StringBuilder sb = new StringBuilder("Network Interfaces:"); + if (networkIFs.length == 0) { + sb.append(" Unknown"); + } + for (NetworkIF net : networkIFs) { + sb.append("\n ").append(net.toString()); + } + oshi.add(sb.toString()); + } + + private static void printNetworkParameters(NetworkParams networkParams) { + oshi.add("Network parameters:\n " + networkParams.toString()); + } + + private static void printDisplays(Display[] displays) { + oshi.add("Displays:"); + int i = 0; + for (Display display : displays) { + oshi.add(" Display " + i + ":"); + oshi.add(String.valueOf(display)); + i++; + } + } + + private static void printUsbDevices(UsbDevice[] usbDevices) { + oshi.add("USB Devices:"); + for (UsbDevice usbDevice : usbDevices) { + oshi.add(String.valueOf(usbDevice)); + } + } + + private static void printSoundCards(SoundCard[] cards) { + oshi.add("Sound Cards:"); + for (SoundCard card : cards) { + oshi.add(" " + String.valueOf(card)); + } + } + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/README.md b/codes/javatech/javatech-others/javatech-ruleengine/README.md new file mode 100644 index 00000000..92bab0d2 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/README.md @@ -0,0 +1,3 @@ +# 规则引擎示例 + +- [easy-rules](https://github.com/j-easy/easy-rules) - 使用便捷简单:支持注解、Java API、MVEL 表达式 方式定义规则 \ No newline at end of file diff --git a/codes/javatech/javatech-others/javatech-ruleengine/pom.xml b/codes/javatech/javatech-others/javatech-ruleengine/pom.xml new file mode 100644 index 00000000..bc10ab25 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/pom.xml @@ -0,0 +1,71 @@ + + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 1.0.8 + + + io.github.dunwu.javatech + javatech-ruleengine + 1.0.0 + jar + JAVATECH-其他示例-规则引擎 + + + 3.4.0 + 2.4.3.Final + + + + + + org.jeasy + easy-rules-core + ${easy-rules.version} + + + org.jeasy + easy-rules-mvel + ${easy-rules.version} + + + org.jeasy + easy-rules-support + ${easy-rules.version} + + + cn.hutool + hutool-all + + + + com.alibaba + fastjson + + + ch.qos.logback + logback-classic + + + org.junit.jupiter + junit-jupiter + test + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/eazyrules/Launcher.java b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/eazyrules/Launcher.java new file mode 100644 index 00000000..342a1039 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/eazyrules/Launcher.java @@ -0,0 +1,25 @@ +package io.github.dunwu.javatech.rule.eazyrules; + +import org.jeasy.rules.api.Facts; +import org.jeasy.rules.api.Rules; +import org.jeasy.rules.api.RulesEngine; +import org.jeasy.rules.core.DefaultRulesEngine; + +public class Launcher { + + public static void main(String[] args) { + // define facts + Facts facts = new Facts(); + facts.put("rain", true); + + // define rules + WeatherRule weatherRule = new WeatherRule(); + Rules rules = new Rules(); + rules.register(weatherRule); + + // fire rules on known facts + RulesEngine rulesEngine = new DefaultRulesEngine(); + rulesEngine.fire(rules, facts); + } + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/eazyrules/WeatherRule.java b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/eazyrules/WeatherRule.java new file mode 100644 index 00000000..af802028 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/eazyrules/WeatherRule.java @@ -0,0 +1,24 @@ +package io.github.dunwu.javatech.rule.eazyrules; + +import org.jeasy.rules.annotation.Action; +import org.jeasy.rules.annotation.Condition; +import org.jeasy.rules.annotation.Fact; +import org.jeasy.rules.annotation.Rule; + +/** + * @author Zhang Peng + * @since 2020-05-15 + */ +@Rule(name = "weather rule", description = "if it rains then take an umbrella" ) +public class WeatherRule { + + @Condition + public boolean itRains(@Fact("rain") boolean rain) { + return rain; + } + + @Action + public void takeAnUmbrella() { + System.out.println("It rains, take an umbrella!"); + } +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/BasicRule.java b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/BasicRule.java new file mode 100644 index 00000000..d3045483 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/BasicRule.java @@ -0,0 +1,128 @@ +package io.github.dunwu.javatech.rule.mvel; + +public class BasicRule implements Rule, Comparable { + + protected String name; + + private String description; + + private int priority; + + private String condition; + + private String action; + + public BasicRule() { + } + + @Override + public int hashCode() { + int result = name.hashCode(); + result = 31 * result + (description != null ? description.hashCode() : 0); + result = 31 * result + priority; + return result; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + BasicRule basicRule = (BasicRule) o; + + if (priority != basicRule.priority) { + return false; + } + if (!name.equals(basicRule.name)) { + return false; + } + return !(description != null ? !description.equals(basicRule.description) : basicRule.description != null); + } + + @Override + public String toString() { + return name; + } + + @Override + public int compareTo(Rule rule) { + if (priority < rule.getPriority()) { + return -1; + } else if (priority > rule.getPriority()) { + return 1; + } else { + return getName().compareTo(rule.getName()); + } + } + + @Override + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Override + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + @Override + public String getCondition() { + return condition; + } + + public void setCondition(String condition) { + this.condition = condition; + } + + @Override + public String getAction() { + return action; + } + + @Override + public boolean validate() { + if (condition == null || condition.length() == 0) { + throw new IllegalArgumentException("The rule condition must not be null or empty"); + } + if (action == null || action.length() == 0) { + throw new IllegalArgumentException("The rule action must not be null or empty"); + } + return true; + } + + @Override + public boolean evaluate(RuleContext ruleContext) { + return false; + } + + @Override + public void execute(RuleContext ruleContext) { + // do nothing + } + + public void setAction(String action) { + this.action = action; + } + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/DefaultRuleEngine.java b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/DefaultRuleEngine.java new file mode 100644 index 00000000..27e24fdf --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/DefaultRuleEngine.java @@ -0,0 +1,163 @@ +package io.github.dunwu.javatech.rule.mvel; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; + +public class DefaultRuleEngine implements RuleEngine { + + protected Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * The rules set. + */ + protected Set rules; + + /** + * The engine parameters + */ + protected RuleEngineParams params; + + /** + * The rule fact + */ + protected RuleContext fact; + + /** + * ruleSet Map + */ + private Map> ruleSetMap = new ConcurrentHashMap<>(); + + public DefaultRuleEngine(RuleEngineParams params) { + this.params = params; + this.rules = new TreeSet<>(); + if (params.isSilentMode()) { + // cancle log + } + } + + @Override + public RuleEngineParams getParams() { + return params; + } + + @Override + public void registerRule(Rule rule) { + // 检查规则 + if (rule.validate()) { + rules.add(rule); + } + } + + @Override + public void registerRule(MvelRuleSet ruleSet) { + ruleSet.getRules().forEach(rule -> registerRule(rule)); + logRegisteredRules(); + } + + private void logRegisteredRules() { + logger.info("Registered rules:"); + for (Rule rule : rules) { + logger.info("Rule { name = {}, description = {}, priority = {}}", rule.getName(), rule.getDescription(), + rule.getPriority()); + } + } + + @Override + public void unregisterRule(Rule rule) { + rules.remove(rule); + } + + @Override + public void clearRules() { + ruleSetMap.clear(); + } + + @Override + public Set getRules() { + return rules; + } + + @Override + public Map checkRules() { + logger.info("Checking rules"); + sortRules(); + Map result = new HashMap<>(); + for (Rule rule : rules) { + result.put(rule, rule.evaluate(fact)); + } + return result; + } + + @Override + public void launch(RuleContext fact) { + if (rules.isEmpty()) { + logger.warn("No rules registered! Nothing to apply"); + return; + } + + logEngineParams(); + sortRules(); + applyRules(fact); + } + + private void logEngineParams() { + logger.info("----- Params -----"); + logger.info("Engine name: {}", params.getName()); + logger.info("Rule priority threshold: {}", params.getPriorityThreshold()); + logger.info("Skip on first applied rule: {}", params.isSkipOnFirstAppliedRule()); + logger.info("Skip on first unapplied rule: {}", params.isSkipOnFirstUnAppliedRule()); + logger.info("Skip on first failed rule: {}", params.isSkipOnFirstFailedRule()); + } + + private void applyRules(RuleContext fact) { + logger.info("Rules evaluation started"); + + for (Rule rule : rules) { + final String name = rule.getName(); + final int priority = rule.getPriority(); + + if (priority > params.getPriorityThreshold()) { + logger.info( + "Rule priority threshold ({}) exceeded at rule {} with priority={}, next rules will be skipped", + new Object[] { params.getPriorityThreshold(), name, priority }); + break; + } + + if (rule.evaluate(fact)) { + logger.info("Rule [{}] triggered", name); + try { + rule.execute(fact); + logger.info("Rule {} performed successfully", name); + if (params.isSkipOnFirstAppliedRule()) { + logger.info("Next rules will be skipped since parameter skipOnFirstAppliedRule is set"); + break; + } + if (params.isSkipOnFirstUnAppliedRule()) { + logger.info("Next rules will be skipped since parameter skipOnFirstUnAppliedRule is set"); + break; + } + } catch (Exception exception) { + logger.error("Rule [{}] performed with error {}", name, exception); + + if (params.isSkipOnFirstFailedRule()) { + logger.info("Next rules will be skipped since parameter skipOnFirstFailedRule is set"); + break; + } + } + } else { + logger.info("Rule [{}] has been evaluated to false, it has not been executed", name); + } + } + } + + private void sortRules() { + rules = new TreeSet<>(rules); + } + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/MvelRule.java b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/MvelRule.java new file mode 100644 index 00000000..491be301 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/MvelRule.java @@ -0,0 +1,34 @@ +package io.github.dunwu.javatech.rule.mvel; + +import org.mvel2.MVEL; + +import java.io.Serializable; + +public class MvelRule extends BasicRule { + + /** + * 判断条件是否匹配 + */ + @Override + public boolean evaluate(RuleContext ruleContext) { + try { + return (Boolean) MVEL.eval(getCondition(), ruleContext); + } catch (Exception e) { + throw new RuntimeException(String.format("条件[%s]匹配发生异常:", getCondition()), e); + } + } + + /** + * 执行条件匹配后的操作 + */ + @Override + public void execute(RuleContext ruleContext) { + try { + Serializable exp = MVEL.compileExpression(getAction(), ruleContext); + MVEL.executeExpression(exp, ruleContext); + } catch (Exception e) { + throw new RuntimeException(String.format("后续操作[%s]执行发生异常:", getAction()), e); + } + } + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/MvelRuleSet.java b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/MvelRuleSet.java new file mode 100644 index 00000000..463746b0 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/MvelRuleSet.java @@ -0,0 +1,34 @@ +package io.github.dunwu.javatech.rule.mvel; + +import java.util.Set; +import java.util.TreeSet; + +public class MvelRuleSet { + + private String name; + + private TreeSet rules; + + public String getName() { + return name; + } + + public void setName(String name) { + if (name == null || name.length() == 0) { + name = RuleConstant.DEFAULT_RULE_NAME; + } + this.name = name; + } + + public Set getRules() { + if (rules == null) { + rules = new TreeSet<>(); + } + return rules; + } + + public void setRules(TreeSet rules) { + this.rules = rules; + } + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/Rule.java b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/Rule.java new file mode 100644 index 00000000..4b19a291 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/Rule.java @@ -0,0 +1,61 @@ +package io.github.dunwu.javatech.rule.mvel; + +public interface Rule { + + /** + * Getter for rule name. + * + * @return the rule name + */ + String getName(); + + /** + * Getter for rule description. + * + * @return rule description + */ + String getDescription(); + + /** + * Getter for rule priority. + * + * @return rule priority + */ + int getPriority(); + + /** + * Getter for the rule condition + * + * @return rule condition + */ + String getCondition(); + + /** + * Getter for the rule action + * + * @return rule action + */ + String getAction(); + + /** + * validate + * + * @return boolean + */ + boolean validate(); + + /** + * Rule conditions abstraction : this method encapsulates the rule's conditions. + * + * @return true if the rule should be applied, false else + */ + boolean evaluate(RuleContext ruleContext); + + /** + * Rule actions abstraction : this method encapsulates the rule's actions. + * + * @throws Exception thrown if an exception occurs during actions performing + */ + void execute(RuleContext ruleContext) throws Exception; + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleConstant.java b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleConstant.java new file mode 100644 index 00000000..03ba63d3 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleConstant.java @@ -0,0 +1,33 @@ +package io.github.dunwu.javatech.rule.mvel; + +/** + * 规则常量 + */ +public final class RuleConstant { + + /** + * Default rule name. + */ + public static final String DEFAULT_RULE_NAME = "rule"; + + /** + * Default engine name. + */ + public static final String DEFAULT_ENGINE_NAME = "engine"; + + /** + * Default rule description. + */ + public static final String DEFAULT_RULE_DESCRIPTION = "description"; + + /** + * Default rule priority. + */ + public static final int DEFAULT_RULE_PRIORITY = Integer.MAX_VALUE - 1; + + /** + * Default rule priority threshold. + */ + public static final int DEFAULT_RULE_PRIORITY_THRESHOLD = Integer.MAX_VALUE; + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleContext.java b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleContext.java new file mode 100644 index 00000000..acf04576 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleContext.java @@ -0,0 +1,8 @@ +package io.github.dunwu.javatech.rule.mvel; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class RuleContext extends ConcurrentHashMap implements Map { + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleEngine.java b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleEngine.java new file mode 100644 index 00000000..9d6321dc --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleEngine.java @@ -0,0 +1,54 @@ +package io.github.dunwu.javatech.rule.mvel; + +import java.util.Map; +import java.util.Set; + +public interface RuleEngine { + + /** + * 规则引擎 设置参数 + * + * @return The rules engine parameters + */ + RuleEngineParams getParams(); + + /** + * 注册rule + */ + void registerRule(Rule rule); + + /** + * 注册ruleSet + */ + void registerRule(MvelRuleSet ruleSet); + + /** + * 取消注册rule + */ + void unregisterRule(Rule rule); + + /** + * 清空规则列表 + */ + void clearRules(); + + /** + * Return the set of registered rules. + * + * @return the set of registered rules + */ + Set getRules(); + + /** + * Check rules without firing them. + * + * @return a map with the result of evaluation of each rule + */ + Map checkRules(); + + /** + * Launch all registered rules. + */ + void launch(RuleContext ruleContext); + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleEngineBuilder.java b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleEngineBuilder.java new file mode 100644 index 00000000..870a76a5 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleEngineBuilder.java @@ -0,0 +1,45 @@ +package io.github.dunwu.javatech.rule.mvel; + +public class RuleEngineBuilder { + + private RuleEngineParams params; + + private RuleEngineBuilder() { + params = new RuleEngineParams(RuleConstant.DEFAULT_ENGINE_NAME, false, false, false, + RuleConstant.DEFAULT_RULE_PRIORITY_THRESHOLD, false); + } + + public static RuleEngineBuilder newRuleEngine() { + return new RuleEngineBuilder(); + } + + public RuleEngine build() { + return new DefaultRuleEngine(params); + } + + public RuleEngineBuilder named(final String name) { + params.setName(name); + return this; + } + + public RuleEngineBuilder withSkipOnFirstAppliedRule(final boolean skipOnFirstAppliedRule) { + params.setSkipOnFirstAppliedRule(skipOnFirstAppliedRule); + return this; + } + + public RuleEngineBuilder withSkipOnFirstFailedRule(final boolean skipOnFirstFailedRule) { + params.setSkipOnFirstFailedRule(skipOnFirstFailedRule); + return this; + } + + public RuleEngineBuilder withRulePriorityThreshold(final int priorityThreshold) { + params.setPriorityThreshold(priorityThreshold); + return this; + } + + public RuleEngineBuilder withSilentMode(final boolean silentMode) { + params.setSilentMode(silentMode); + return this; + } + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleEngineParams.java b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleEngineParams.java new file mode 100644 index 00000000..36757859 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleEngineParams.java @@ -0,0 +1,93 @@ +package io.github.dunwu.javatech.rule.mvel; + +public class RuleEngineParams { + + /** + * The engine name. + */ + protected String name; + + /** + * 满足任意条件(即遇到第一个匹配规则时停止) Parameter to skip next applicable rules when a rule is applied. + */ + private boolean skipOnFirstAppliedRule; + + /** + * 满足所有条件(即遇到第第一个未匹配规则时停止) Parameter to skip next applicable rules when a rule has failed. + */ + private boolean skipOnFirstUnAppliedRule; + + /** + * Parameter to skip next applicable rules when a rule has failed. + */ + private boolean skipOnFirstFailedRule; + + /** + * Parameter to skip next rules if priority exceeds a user defined threshold. + */ + private int priorityThreshold; + + /** + * Parameter to mute loggers. + */ + private boolean silentMode; + + public RuleEngineParams(String name, boolean skipOnFirstAppliedRule, boolean skipOnFirstUnAppliedRule, + boolean skipOnFirstFailedRule, int priorityThreshold, boolean silentMode) { + this.name = name; + this.skipOnFirstAppliedRule = skipOnFirstAppliedRule; + this.skipOnFirstUnAppliedRule = skipOnFirstUnAppliedRule; + this.skipOnFirstFailedRule = skipOnFirstFailedRule; + this.priorityThreshold = priorityThreshold; + this.silentMode = silentMode; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getPriorityThreshold() { + return priorityThreshold; + } + + public void setPriorityThreshold(int priorityThreshold) { + this.priorityThreshold = priorityThreshold; + } + + public boolean isSilentMode() { + return silentMode; + } + + public void setSilentMode(boolean silentMode) { + this.silentMode = silentMode; + } + + public boolean isSkipOnFirstAppliedRule() { + return skipOnFirstAppliedRule; + } + + public void setSkipOnFirstAppliedRule(boolean skipOnFirstAppliedRule) { + this.skipOnFirstAppliedRule = skipOnFirstAppliedRule; + } + + public boolean isSkipOnFirstFailedRule() { + return skipOnFirstFailedRule; + } + + public void setSkipOnFirstFailedRule(boolean skipOnFirstFailedRule) { + this.skipOnFirstFailedRule = skipOnFirstFailedRule; + } + + public boolean isSkipOnFirstUnAppliedRule() { + return skipOnFirstUnAppliedRule; + } + + public void setSkipOnFirstUnAppliedRule(boolean skipOnFirstUnAppliedRule) { + this.skipOnFirstUnAppliedRule = skipOnFirstUnAppliedRule; + } + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/main/resources/logback.xml b/codes/javatech/javatech-others/javatech-ruleengine/src/main/resources/logback.xml new file mode 100644 index 00000000..8d3ec5a8 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/main/resources/logback.xml @@ -0,0 +1,95 @@ + + + + + + dunwu-admin + + + + + + + + + + + + + + + ${LOG_CHARSET} + ${LOG_COLOR_PATTERN} + + + + + + + + ${LOG_DIR}/%d{yyyy-MM,aux}/${APP_NAME}_debug.%d{yyyy-MM-dd}.%i.log + + ${LOG_MAX_HISTORY} + + ${LOG_MAX_FILE_SIZE} + + + + ${LOG_PATTERN} + + + + DEBUG + + + + + + + 0 + + ${LOG_MAX_QUEUE_SIZE} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/main/resources/weather-rule.yml b/codes/javatech/javatech-others/javatech-ruleengine/src/main/resources/weather-rule.yml new file mode 100644 index 00000000..77dddbb5 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/main/resources/weather-rule.yml @@ -0,0 +1,5 @@ +name: "weather rule" +description: "if it rains then take an umbrella" +condition: "rain == true" +actions: + - "System.out.println(\"It rains, take an umbrella!\");" diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/eazyrules/EazyRulesTest.java b/codes/javatech/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/eazyrules/EazyRulesTest.java new file mode 100644 index 00000000..1343c628 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/eazyrules/EazyRulesTest.java @@ -0,0 +1,85 @@ +package io.github.dunwu.javatech.rule.eazyrules; + +import cn.hutool.core.io.resource.ResourceUtil; +import org.jeasy.rules.api.Facts; +import org.jeasy.rules.api.Rule; +import org.jeasy.rules.api.Rules; +import org.jeasy.rules.api.RulesEngine; +import org.jeasy.rules.core.DefaultRulesEngine; +import org.jeasy.rules.core.RuleBuilder; +import org.jeasy.rules.mvel.MVELRule; +import org.jeasy.rules.mvel.MVELRuleFactory; +import org.jeasy.rules.support.YamlRuleDefinitionReader; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.FileReader; +import java.net.URL; + +/** + * Easy Rules 使用测试 + * + * @author Zhang Peng + * @since 2020-05-15 + */ +public class EazyRulesTest { + + @Test + @DisplayName("测试 easy-rules 注解方式") + public void testWeatherRule() { + WeatherRule rule = new WeatherRule(); + Rules rules = new Rules(); + rules.register(rule); + testRule(rules); + } + + @Test + @DisplayName("测试 Fluent API") + public void testFluentApi() { + Rule rule = new RuleBuilder() + .name("weather rule") + .description("if it rains then take an umbrella") + .when(facts -> facts.get("rain").equals(true)) + .then(facts -> System.out.println("It rains, take an umbrella!")) + .build(); + Rules rules = new Rules(); + rules.register(rule); + testRule(rules); + } + + @Test + @DisplayName("测试 MVEL Expression") + public void testExpression() { + Rule rule = new MVELRule() + .name("weather rule") + .description("if it rains then take an umbrella") + .when("rain == true") + .then("System.out.println(\"It rains, take an umbrella!\");"); + Rules rules = new Rules(); + rules.register(rule); + testRule(rules); + } + + @Test + @DisplayName("测试 Rule File") + public void testRuleFile() throws Exception { + MVELRuleFactory ruleFactory = new MVELRuleFactory(new YamlRuleDefinitionReader()); + URL url = ResourceUtil.getResource("weather-rule.yml"); + Rule rule = ruleFactory.createRule(new FileReader(url.getFile())); + Rules rules = new Rules(); + rules.register(rule); + testRule(rules); + } + + public void testRule(Rules rules) { + + // define facts + Facts facts = new Facts(); + facts.put("rain", true); + + // fire rules on known facts + RulesEngine rulesEngine = new DefaultRulesEngine(); + rulesEngine.fire(rules, facts); + } + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/mvel/ClassTests.java b/codes/javatech/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/mvel/ClassTests.java new file mode 100644 index 00000000..f20ba568 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/mvel/ClassTests.java @@ -0,0 +1,44 @@ +package io.github.dunwu.javatech.rule.mvel; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mvel2.MVEL; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Mike Brock + */ +public class ClassTests { + + private final String dir = "src/test/java/" + getClass().getPackage().getName().replaceAll("\\.", "/"); + + @Test + public void testScript() throws IOException { + final Object o = MVEL.evalFile(new File(dir + "/demo.mvel"), new HashMap()); + } + + @Test + public void testEval() { + String expression = "foobar > 99"; + Map vars = new HashMap(); + vars.put("foobar", new Integer(100)); + Boolean result = (Boolean) MVEL.eval(expression, vars); + Assertions.assertEquals(true, result); + } + + @Test + public void testCompileExpression() { + String expression = "foobar > 99"; + Serializable compiled = MVEL.compileExpression(expression); + Map vars = new HashMap(); + vars.put("foobar", new Integer(100)); + Boolean result = (Boolean) MVEL.executeExpression(compiled, vars); + Assertions.assertEquals(true, result); + } + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/mvel/SalaryRuleTest.java b/codes/javatech/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/mvel/SalaryRuleTest.java new file mode 100644 index 00000000..ae62d6ac --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/mvel/SalaryRuleTest.java @@ -0,0 +1,77 @@ +package io.github.dunwu.javatech.rule.mvel; + +import cn.hutool.core.io.FileUtil; +import com.alibaba.fastjson.JSON; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; + +public class SalaryRuleTest { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private final String SALARY_RULE_PATH = System.getProperty("user.dir") + "\\src\\test\\resources\\SalaryRule.json"; + + private RuleEngine ruleEngine; + + @BeforeEach + public void before() { + logger.info("Begin"); + RuleEngineParams params = new RuleEngineParams("SalaryEngine", true, false, true, + RuleConstant.DEFAULT_RULE_PRIORITY_THRESHOLD, false); + ruleEngine = new DefaultRuleEngine(params); + + String json = FileUtil.readString(new File(SALARY_RULE_PATH), "utf-8"); + MvelRuleSet ruleSet = JSON.parseObject(json, MvelRuleSet.class); + ruleEngine.registerRule(ruleSet); + } + + @Test + public void test_salaryRule() { + RuleContext ruleContext = new RuleContext(); + ruleContext.put("fee", 0.0); + + ruleContext.put("salary", 1000); + ruleEngine.launch(ruleContext); + Assertions.assertEquals(0, ruleContext.get("fee")); + + ruleContext.put("salary", 4000); + ruleEngine.launch(ruleContext); + Assertions.assertEquals(15.0, ruleContext.get("fee")); + + ruleContext.put("salary", 7000); + ruleEngine.launch(ruleContext); + Assertions.assertEquals(245.0, ruleContext.get("fee")); + + ruleContext.put("salary", 10000); + ruleEngine.launch(ruleContext); + Assertions.assertEquals(745.0, ruleContext.get("fee")); + + ruleContext.put("salary", 18000); + ruleEngine.launch(ruleContext); + Assertions.assertEquals(2620.0, ruleContext.get("fee")); + + ruleContext.put("salary", 40005); + ruleEngine.launch(ruleContext); + Assertions.assertEquals(8196.50, ruleContext.get("fee")); + + ruleContext.put("salary", 70005); + ruleEngine.launch(ruleContext); + Assertions.assertEquals(17771.75, ruleContext.get("fee")); + + ruleContext.put("salary", 100000); + ruleEngine.launch(ruleContext); + Assertions.assertEquals(29920.00, ruleContext.get("fee")); + } + + @AfterEach + public void after() { + logger.info("End"); + } + +} diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/mvel/demo.mvel b/codes/javatech/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/mvel/demo.mvel new file mode 100644 index 00000000..ec1733ee --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/mvel/demo.mvel @@ -0,0 +1,56 @@ +/** + * This is an MVEL script. + */ + +def Person { + String name; + int age; + String color; + + def speak() { + System.out.println("My name is " + name + " and I am " + age + " years old. I like the color " + color + "."); + } + + def makeUpperCase() { + name = name.toUpperCase(); + } + + def sayName(amount) { + for (int i = 0; i < amount; i++) { + System.out.println((i + 1) + ". " + name); + } + } +} + +tm = System.currentTimeMillis; + +def print(str) { + System.out.println(str); +} + +var p = new Person(); + +p.{ + name = "Bob", + age = 5, + color = "blue" +}; + +p.speak(); +p.makeUpperCase(); +p.speak(); + +print("\n---------\n"); + +p.sayName(10); + +for (a : "gorkem") { + print("->" + a); +} + +var list = ["cow", "pig", "lion"]; +var blah = ($.toUpperCase() in list if $.length() == 3); + +print(blah); +print(tm()); + diff --git a/codes/javatech/javatech-others/javatech-ruleengine/src/test/resources/SalaryRule.json b/codes/javatech/javatech-others/javatech-ruleengine/src/test/resources/SalaryRule.json new file mode 100644 index 00000000..6a43150e --- /dev/null +++ b/codes/javatech/javatech-others/javatech-ruleengine/src/test/resources/SalaryRule.json @@ -0,0 +1,51 @@ +{ + "name": "salaryRule", + "rules": [ + { + "name": "step1", + "action": "fee=0", + "condition": "salary<=3500" + }, + { + "name": "step2", + "action": "fee=(salary-3500)*0.03", + "condition": "salary>3500 && salary<=5000" + }, + { + "name": "step3", + "action": "fee=(salary-3500)*0.1-105", + "condition": "salary>5000 && salary<=8000", + "priority": 3 + }, + { + "name": "step4", + "action": "fee=(salary-3500)*0.2-555", + "condition": "salary>8000 && salary<=12500", + "priority": 4 + }, + { + "name": "step5", + "action": "fee=(salary-3500)*0.25-1005", + "condition": "salary>12500 && salary<=38500", + "priority": 5 + }, + { + "name": "step6", + "action": "fee=(salary-3500)*0.3-2755", + "condition": "salary>38500 && salary<=58500", + "priority": 6 + }, + { + "name": "step7", + "action": "fee=(salary-3500)*0.35-5505", + "condition": "salary>58500 && salary<=83500", + "priority": 7 + }, + { + "name": "step8", + "action": "fee=(salary-3500)*0.45-13505", + "condition": "salary>83500", + "priority": 8 + } + ] +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/pom.xml b/codes/javatech/javatech-others/javatech-zookeeper/pom.xml new file mode 100644 index 00000000..37774d2b --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 1.0.8 + + + io.github.dunwu.javatech + javatech-zookeeper + 1.0.0 + jar + JAVATECH-其他示例-Zookeeper + + + + org.apache.zookeeper + zookeeper + 3.5.6 + + + org.apache.curator + curator-recipes + 4.3.0 + + + cn.hutool + hutool-all + + + junit + junit + test + + + diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/Callback.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/Callback.java new file mode 100644 index 00000000..4e4f1c79 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/Callback.java @@ -0,0 +1,12 @@ +package io.github.dunwu.javatech.zk.dlock; + +/** + * Created by sunyujia@aliyun.com on 2016/2/23. + */ +public interface Callback { + + V onGetLock() throws InterruptedException; + + V onTimeout() throws InterruptedException; + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/DLockTemplate.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/DLockTemplate.java new file mode 100644 index 00000000..d15f51ad --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/DLockTemplate.java @@ -0,0 +1,16 @@ +package io.github.dunwu.javatech.zk.dlock; + +/** + * 分布式锁模板类 Created by sunyujia@aliyun.com on 2016/2/23. + */ +public interface DLockTemplate { + + /** + * @param lockId 锁id(对应业务唯一ID) + * @param timeout 单位毫秒 + * @param callback 回调函数 + * @return + */ + V execute(String lockId, long timeout, Callback callback); + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/DistributedLock.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/DistributedLock.java new file mode 100644 index 00000000..2ac111e1 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/DistributedLock.java @@ -0,0 +1,19 @@ +package io.github.dunwu.javatech.zk.dlock; + +import java.util.concurrent.TimeUnit; + +/** + * 分布式锁接口 + * + * @author Zhang Peng + * @since 2020-01-14 + */ +public interface DistributedLock { + + void lock(); + + boolean tryLock(long timeout, TimeUnit unit); + + void unlock(); + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/TimeoutHandler.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/TimeoutHandler.java new file mode 100644 index 00000000..89205564 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/TimeoutHandler.java @@ -0,0 +1,11 @@ +package io.github.dunwu.javatech.zk.dlock; + +/** + * @author Zhang Peng + * @since 2020-01-14 + */ +public interface TimeoutHandler { + + V onTimeout() throws InterruptedException; + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/ZkDLockTemplate.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/ZkDLockTemplate.java new file mode 100644 index 00000000..ae18db85 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/ZkDLockTemplate.java @@ -0,0 +1,51 @@ +package io.github.dunwu.javatech.zk.dlock; + +import org.apache.curator.framework.CuratorFramework; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * Created by sunyujia@aliyun.com on 2016/2/26. + */ +public class ZkDLockTemplate implements DLockTemplate { + + private static final Logger log = LoggerFactory.getLogger(ZkDLockTemplate.class); + + private CuratorFramework client; + + public ZkDLockTemplate(CuratorFramework client) { + this.client = client; + } + + @Override + public V execute(String lockId, long timeout, Callback callback) { + ZookeeperReentrantDistributedLock distributedReentrantLock = null; + boolean getLock = false; + try { + distributedReentrantLock = new ZookeeperReentrantDistributedLock(client, lockId); + if (tryLock(distributedReentrantLock, timeout)) { + getLock = true; + return callback.onGetLock(); + } else { + return callback.onTimeout(); + } + } catch (InterruptedException ex) { + log.error(ex.getMessage(), ex); + Thread.currentThread().interrupt(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } finally { + if (getLock) { + distributedReentrantLock.unlock(); + } + } + return null; + } + + private boolean tryLock(ZookeeperReentrantDistributedLock distributedReentrantLock, long timeout) { + return distributedReentrantLock.tryLock(timeout, TimeUnit.MILLISECONDS); + } + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/ZkReentrantLockCleanerTask.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/ZkReentrantLockCleanerTask.java new file mode 100644 index 00000000..17260096 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/ZkReentrantLockCleanerTask.java @@ -0,0 +1,83 @@ +package io.github.dunwu.javatech.zk.dlock; + +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.TimerTask; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +/** + * Created by sunyujia@aliyun.com on 2016/2/25. + */ +public class ZkReentrantLockCleanerTask extends TimerTask { + + private static final org.slf4j.Logger log = LoggerFactory.getLogger(ZkReentrantLockCleanerTask.class); + + private CuratorFramework client; + + private ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3); + + /** + * 检查周期 + */ + private long period = 5000; + + /** + * Curator RetryPolicy maxRetries + */ + private int maxRetries = 3; + + /** + * Curator RetryPolicy baseSleepTimeMs + */ + private final int baseSleepTimeMs = 1000; + + public ZkReentrantLockCleanerTask(String zookeeperAddress) { + try { + RetryPolicy retryPolicy = new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries); + client = CuratorFrameworkFactory.newClient(zookeeperAddress, retryPolicy); + client.start(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } catch (Throwable ex) { + ex.printStackTrace(); + log.error(ex.getMessage(), ex); + } + } + + public void start() { + executorService.execute(this); + } + + private boolean isEmpty(List list) { + return list == null || list.isEmpty(); + } + + @Override + public void run() { + try { + List childrenPaths = this.client.getChildren().forPath(ZookeeperReentrantDistributedLock.ROOT_PATH); + for (String path : childrenPaths) { + cleanNode(path); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void cleanNode(String path) { + try { + if (isEmpty(this.client.getChildren().forPath(path))) { + this.client.delete().forPath(path);//利用存在子节点无法删除和zk的原子性这两个特性. + } + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/ZookeeperReentrantDistributedLock.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/ZookeeperReentrantDistributedLock.java new file mode 100644 index 00000000..e568e4c2 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/ZookeeperReentrantDistributedLock.java @@ -0,0 +1,112 @@ +package io.github.dunwu.javatech.zk.dlock; + +import cn.hutool.core.collection.CollectionUtil; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.recipes.locks.InterProcessMutex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * 基于Zookeeper的可重入互斥锁(关于重入:仅限于持有zk锁的jvm内重入) Created by sunyujia@aliyun.com on 2016/2/24. + */ +public class ZookeeperReentrantDistributedLock implements DistributedLock { + + private static final Logger log = LoggerFactory.getLogger(ZookeeperReentrantDistributedLock.class); + + /** + * 线程池 + */ + private static final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3); + + /** + * 所有PERSISTENT锁节点的根位置 + */ + public static final String ROOT_PATH = "/distributed_lock/"; + + /** + * 每次延迟清理PERSISTENT节点的时间 Unit:MILLISECONDS + */ + private static final long DELAY_TIME_FOR_CLEAN = 1000; + + /** + * zk 共享锁实现 + */ + private InterProcessMutex interProcessMutex; + + /** + * 锁的ID,对应zk一个PERSISTENT节点,下挂EPHEMERAL节点. + */ + private String path; + + /** + * zk的客户端 + */ + private CuratorFramework client; + + public ZookeeperReentrantDistributedLock(CuratorFramework client, String lockId) { + this.client = client; + this.path = ROOT_PATH + lockId; + interProcessMutex = new InterProcessMutex(this.client, this.path); + } + + @Override + public void lock() { + try { + interProcessMutex.acquire(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + @Override + public boolean tryLock(long timeout, TimeUnit unit) { + try { + return interProcessMutex.acquire(timeout, unit); + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + @Override + public void unlock() { + try { + interProcessMutex.release(); + } catch (Throwable e) { + log.error(e.getMessage(), e); + } finally { + executorService.schedule(new Cleaner(client, path), DELAY_TIME_FOR_CLEAN, TimeUnit.MILLISECONDS); + } + } + + static class Cleaner implements Runnable { + + private String path; + + private CuratorFramework client; + + public Cleaner(CuratorFramework client, String path) { + this.path = path; + this.client = client; + } + + @Override + public void run() { + try { + List list = client.getChildren().forPath(path); + if (CollectionUtil.isEmpty(list)) { + client.delete().forPath(path); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + } + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperConnection.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperConnection.java new file mode 100644 index 00000000..a82cb0ae --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperConnection.java @@ -0,0 +1,56 @@ +package io.github.dunwu.javatech.zk.example; + +import org.apache.zookeeper.WatchedEvent; +import org.apache.zookeeper.Watcher; +import org.apache.zookeeper.Watcher.Event.KeeperState; +import org.apache.zookeeper.ZooKeeper; +import org.apache.zookeeper.ZooKeeper.States; + +import java.io.IOException; +import java.util.concurrent.CountDownLatch; + +/** + * ZooKeeper 连接示例 + * + * @author Zhang Peng + * @since 2018-07-12 + */ +public class ZooKeeperConnection { + + private static final String HOST = "localhost"; + + final CountDownLatch connectedSignal = new CountDownLatch(1); + + // declare zookeeper instance to access ZooKeeper ensemble + private ZooKeeper zoo; + + public static void main(String[] args) throws IOException, InterruptedException { + ZooKeeperConnection zooKeeperConnection = new ZooKeeperConnection(); + ZooKeeper zk = zooKeeperConnection.connect(HOST); + States state = zk.getState(); + System.out.println("ZooKeeper isAlive:" + state.isAlive()); + zk.close(); + } + + // Method to connect zookeeper ensemble. + public ZooKeeper connect(String host) throws IOException, InterruptedException { + + zoo = new ZooKeeper(host, 5000, new Watcher() { + @Override + public void process(WatchedEvent we) { + if (we.getState() == KeeperState.SyncConnected) { + connectedSignal.countDown(); + } + } + }); + + connectedSignal.await(); + return zoo; + } + + // Method to disconnect from zookeeper server + public void close() throws InterruptedException { + zoo.close(); + } + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperCreate.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperCreate.java new file mode 100644 index 00000000..819c6ed8 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperCreate.java @@ -0,0 +1,50 @@ +package io.github.dunwu.javatech.zk.example; + +import org.apache.zookeeper.CreateMode; +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.ZooDefs; +import org.apache.zookeeper.ZooKeeper; + +/** + * ZooKeeper 添加 Znode 示例 + * + * @author Zhang Peng + * @since 2018-07-12 + */ +public class ZooKeeperCreate { + + private static final String HOST = "localhost"; + + // create static instance for zookeeper class. + private static ZooKeeper zk; + + // create static instance for ZooKeeperConnection class. + private static ZooKeeperConnection conn; + + public static void main(String[] args) throws InterruptedException { + + // znode path + String path = "/MyFirstZnode"; // Assign path to znode + + // data in byte array + byte[] data = "My first zookeeper app".getBytes(); // Declare data + + try { + conn = new ZooKeeperConnection(); + zk = conn.connect(HOST); + create(path, data); // Create the data to the specified path + } catch (Exception e) { + System.out.println(e.getMessage()); // Catch error message + } finally { + if (conn != null) { + conn.close(); + } + } + } + + // Method to create znode in zookeeper ensemble + public static void create(String path, byte[] data) throws KeeperException, InterruptedException { + zk.create(path, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); + } + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperDelete.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperDelete.java new file mode 100644 index 00000000..72630cb1 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperDelete.java @@ -0,0 +1,41 @@ +package io.github.dunwu.javatech.zk.example; + +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.ZooKeeper; + +/** + * ZooKeeper 删除 Znode 示例 + * + * @author Zhang Peng + * @since 2018-07-12 + */ +public class ZooKeeperDelete { + + private static final String HOST = "localhost"; + + private static ZooKeeper zk; + + private static ZooKeeperConnection conn; + + public static void main(String[] args) throws InterruptedException, KeeperException { + String path = "/MyFirstZnode"; // Assign path to the znode + + try { + conn = new ZooKeeperConnection(); + zk = conn.connect(HOST); + delete(path); // delete the node with the specified path + } catch (Exception e) { + System.out.println(e.getMessage()); // catches error messages + } finally { + if (conn != null) { + conn.close(); + } + } + } + + // Method to check existence of znode and its status, if znode is available. + public static void delete(String path) throws KeeperException, InterruptedException { + zk.delete(path, zk.exists(path, true).getVersion()); + } + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperExists.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperExists.java new file mode 100644 index 00000000..846266ab --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperExists.java @@ -0,0 +1,44 @@ +package io.github.dunwu.javatech.zk.example; + +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.ZooKeeper; +import org.apache.zookeeper.data.Stat; + +/** + * ZooKeeper 判断 Znode 是否存在示例 + * + * @author Zhang Peng + * @since 2018-07-12 + */ +public class ZooKeeperExists { + + private static final String HOST = "localhost"; + + private static ZooKeeper zk; + + private static ZooKeeperConnection conn; + + public static void main(String[] args) throws InterruptedException, KeeperException { + String path = "/MyFirstZnode"; // Assign znode to the specified path + + try { + conn = new ZooKeeperConnection(); + zk = conn.connect(HOST); + Stat stat = znodeExists(path); // Stat checks the path of the znode + + if (stat != null) { + System.out.println("Node exists and the node version is " + stat.getVersion()); + } else { + System.out.println("Node does not exists"); + } + } catch (Exception e) { + System.out.println(e.getMessage()); // Catches error messages + } + } + + // Method to check existence of znode and its status, if znode is available. + public static Stat znodeExists(String path) throws KeeperException, InterruptedException { + return zk.exists(path, true); + } + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperGetChildren.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperGetChildren.java new file mode 100644 index 00000000..34372a6e --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperGetChildren.java @@ -0,0 +1,55 @@ +package io.github.dunwu.javatech.zk.example; + +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.ZooKeeper; +import org.apache.zookeeper.data.Stat; + +import java.util.List; + +/** + * ZooKeeper 获取 znode 的所有子节点示例 + * + * @author Zhang Peng + * @since 2018-07-12 + */ +public class ZooKeeperGetChildren { + + private static final String HOST = "localhost"; + + private static ZooKeeper zk; + + private static ZooKeeperConnection conn; + + public static void main(String[] args) throws InterruptedException, KeeperException { + String path = "/MyFirstZnode"; // Assign path to the znode + + try { + conn = new ZooKeeperConnection(); + zk = conn.connect(HOST); + Stat stat = znode_exists(path); // Stat checks the path + + if (stat != null) { + // getChildren method - get all the children of znode.It has two args, + // path and watch + List children = zk.getChildren(path, false); + for (int i = 0; i < children.size(); i++) { + System.out.println(children.get(i)); // Print children's + } + } else { + System.out.println("Node does not exists"); + } + } catch (Exception e) { + System.out.println(e.getMessage()); + } finally { + if (conn != null) { + conn.close(); + } + } + } + + // Method to check existence of znode and its status, if znode is available. + public static Stat znode_exists(String path) throws KeeperException, InterruptedException { + return zk.exists(path, true); + } + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperGetData.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperGetData.java new file mode 100644 index 00000000..d151b3b5 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperGetData.java @@ -0,0 +1,78 @@ +package io.github.dunwu.javatech.zk.example; + +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.WatchedEvent; +import org.apache.zookeeper.Watcher; +import org.apache.zookeeper.ZooKeeper; +import org.apache.zookeeper.data.Stat; + +import java.util.concurrent.CountDownLatch; + +/** + * ZooKeeper 获取数据示例 + * + * @author Zhang Peng + * @since 2018-07-12 + */ +public class ZooKeeperGetData { + + private static final String HOST = "localhost"; + + private static ZooKeeper zk; + + private static ZooKeeperConnection conn; + + public static void main(String[] args) throws InterruptedException { + String path = "/MyFirstZnode"; + final CountDownLatch connectedSignal = new CountDownLatch(1); + + try { + conn = new ZooKeeperConnection(); + zk = conn.connect(HOST); + Stat stat = existsZnode(path); + + if (stat != null) { + byte[] b = zk.getData(path, new Watcher() { + public void process(WatchedEvent we) { + + if (we.getType() == Event.EventType.None) { + switch (we.getState()) { + case Expired: + connectedSignal.countDown(); + break; + } + } else { + String path = "/MyFirstZnode"; + + try { + byte[] bn = zk.getData(path, false, null); + String data = new String(bn, "UTF-8"); + System.out.println(data); + connectedSignal.countDown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + } + }, null); + + String data = new String(b, "UTF-8"); + System.out.println(data); + connectedSignal.await(); + } else { + System.out.println("Node does not exists"); + } + } catch (Exception e) { + System.out.println(e.getMessage()); + } finally { + if (conn != null) { + conn.close(); + } + } + } + + public static Stat existsZnode(String path) throws KeeperException, InterruptedException { + return zk.exists(path, true); + } + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperSetData.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperSetData.java new file mode 100644 index 00000000..e4c3ac62 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperSetData.java @@ -0,0 +1,42 @@ +package io.github.dunwu.javatech.zk.example; + +import org.apache.zookeeper.KeeperException; +import org.apache.zookeeper.ZooKeeper; + +/** + * ZooKeeper 设置数据示例 + * + * @author Zhang Peng + * @since 2018-07-12 + */ +public class ZooKeeperSetData { + + private static final String HOST = "localhost"; + + private static ZooKeeper zk; + + private static ZooKeeperConnection conn; + + public static void main(String[] args) throws InterruptedException { + String path = "/MyFirstZnode"; + byte[] data = "Success".getBytes(); // Assign data which is to be updated. + + try { + conn = new ZooKeeperConnection(); + zk = conn.connect(HOST); + update(path, data); // Update znode data to the specified path + } catch (Exception e) { + System.out.println(e.getMessage()); + } finally { + if (conn != null) { + conn.close(); + } + } + } + + // Method to update the data in a znode. Similar to getData but without watcher. + public static void update(String path, byte[] data) throws KeeperException, InterruptedException { + zk.setData(path, data, zk.exists(path, true).getVersion()); + } + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/package-info.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/package-info.java new file mode 100644 index 00000000..21f2ef05 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/package-info.java @@ -0,0 +1,7 @@ +/** + * ZooKeeper 最基本操作示例 + * + * @author Zhang Peng + * @since 2020-01-13 + */ +package io.github.dunwu.javatech.zk.example; diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/sequence/DistributedSequence.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/sequence/DistributedSequence.java new file mode 100644 index 00000000..b7597338 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/sequence/DistributedSequence.java @@ -0,0 +1,9 @@ +package io.github.dunwu.javatech.zk.sequence; + +/** + * Created by sunyujia@aliyun.com on 2016/2/25. + */ +public interface DistributedSequence { + + public Long sequence(String sequenceName); +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/sequence/ZkDistributedSequence.java b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/sequence/ZkDistributedSequence.java new file mode 100644 index 00000000..59cb174d --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/sequence/ZkDistributedSequence.java @@ -0,0 +1,64 @@ +package io.github.dunwu.javatech.zk.sequence; + +import io.github.dunwu.javatech.zk.dlock.ZkReentrantLockCleanerTask; +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.slf4j.LoggerFactory; + +/** + * Created by sunyujia@aliyun.com on 2016/2/25. + */ +public class ZkDistributedSequence implements DistributedSequence { + + private static final org.slf4j.Logger log = LoggerFactory.getLogger(ZkReentrantLockCleanerTask.class); + + private CuratorFramework client; + + /** + * Curator RetryPolicy maxRetries + */ + private int maxRetries = 3; + + /** + * Curator RetryPolicy baseSleepTimeMs + */ + private final int baseSleepTimeMs = 1000; + + public ZkDistributedSequence(String zookeeperAddress) { + try { + RetryPolicy retryPolicy = new ExponentialBackoffRetry(baseSleepTimeMs, maxRetries); + client = CuratorFrameworkFactory.newClient(zookeeperAddress, retryPolicy); + client.start(); + } catch (Exception e) { + log.error(e.getMessage(), e); + } catch (Throwable ex) { + ex.printStackTrace(); + log.error(ex.getMessage(), ex); + } + } + + public int getMaxRetries() { + return maxRetries; + } + + public void setMaxRetries(int maxRetries) { + this.maxRetries = maxRetries; + } + + public int getBaseSleepTimeMs() { + return baseSleepTimeMs; + } + + public Long sequence(String sequenceName) { + try { + int value = client.setData().withVersion(-1).forPath("/" + sequenceName, "".getBytes()).getVersion(); + return new Long(value); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/test/java/io/github/dunwu/javatech/zk/dlock/ZkReentrantLockTemplateTest.java b/codes/javatech/javatech-others/javatech-zookeeper/src/test/java/io/github/dunwu/javatech/zk/dlock/ZkReentrantLockTemplateTest.java new file mode 100644 index 00000000..ba2bfb65 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/test/java/io/github/dunwu/javatech/zk/dlock/ZkReentrantLockTemplateTest.java @@ -0,0 +1,83 @@ +package io.github.dunwu.javatech.zk.dlock; + +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ThreadLocalRandom; + +/** + * Created by sunyujia@aliyun.com on 2016/2/24. + */ + +public class ZkReentrantLockTemplateTest { + + @Test + public void testTry() throws InterruptedException { + RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); + CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy); + client.start(); + + final ZkDLockTemplate template = new ZkDLockTemplate(client); + int size = 100; + final CountDownLatch startCountDownLatch = new CountDownLatch(1); + final CountDownLatch endDownLatch = new CountDownLatch(size); + for (int i = 0; i < size; i++) { + new Thread(() -> { + try { + startCountDownLatch.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + final int sleepTime = ThreadLocalRandom.current().nextInt(3) * 1000; + + template.execute("test", 3000, new Callback() { + @Override + public Object onGetLock() throws InterruptedException { + System.out.println(Thread.currentThread().getName() + " 获取锁"); + Thread.currentThread().sleep(sleepTime); + System.out.println(Thread.currentThread().getName() + ":sleeped:" + sleepTime); + endDownLatch.countDown(); + return null; + } + + @Override + public Object onTimeout() throws InterruptedException { + System.out.println(Thread.currentThread().getName() + " 获取锁"); + Thread.currentThread().sleep(sleepTime); + System.out.println(Thread.currentThread().getName() + ":sleeped:" + sleepTime); + endDownLatch.countDown(); + return null; + } + }); + }).start(); + } + startCountDownLatch.countDown(); + endDownLatch.await(); + } + + public static void main(String[] args) { + RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); + CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy); + client.start(); + + final ZkDLockTemplate template = new ZkDLockTemplate(client);//本类多线程安全,可通过spring注入 + template.execute("订单流水号", 5000, new Callback() { + @Override + public Object onGetLock() throws InterruptedException { + //TODO 获得锁后要做的事 + return null; + } + + @Override + public Object onTimeout() throws InterruptedException { + //TODO 获得锁超时后要做的事 + return null; + } + }); + } + +} diff --git a/codes/javatech/javatech-others/javatech-zookeeper/src/test/resources/logback.xml b/codes/javatech/javatech-others/javatech-zookeeper/src/test/resources/logback.xml new file mode 100644 index 00000000..e94627b4 --- /dev/null +++ b/codes/javatech/javatech-others/javatech-zookeeper/src/test/resources/logback.xml @@ -0,0 +1,62 @@ + + + + + + + + ${LOG_MSG} + + + + + DEBUG + + ${USER_HOME}/debug.log + + ${LOG_DIR}/debug%i.log + + 20MB + + + + ${LOG_MSG} + + + + ${USER_HOME}/info.log + + INFO + + + ${LOG_DIR}/info%i.log + + 20MB + + + + ${LOG_MSG} + + + + ${USER_HOME}/error.log + + ERROR + + + ${LOG_DIR}/error%i.log + + 20MB + + + + ${LOG_MSG} + + + + + + + + + diff --git a/codes/javatech/javatech-others/pom.xml b/codes/javatech/javatech-others/pom.xml new file mode 100644 index 00000000..221f2bcf --- /dev/null +++ b/codes/javatech/javatech-others/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + io.github.dunwu.javatech + javatech-others + 1.0.0 + pom + JAVATECH-其他示例 + + + javatech-cli + javatech-ruleengine + javatech-zookeeper + + diff --git a/codes/javatech/javatech-server/README.md b/codes/javatech/javatech-server/README.md new file mode 100644 index 00000000..3f574661 --- /dev/null +++ b/codes/javatech/javatech-server/README.md @@ -0,0 +1,50 @@ +# javatool-server + +> 本示例代码主要展示嵌入式服务器和 web 项目的集成。 +> +> 你可以在本项目中体验嵌入式 Tomcat 和嵌入式 Jetty 的启动方式。 +> + +**版本** + +* JDK:1.8 +* Tomcat:8.5.24 + +## Tomcat + +### Windows 启动 + +执行 `io.github.dunwu.javatech.server.SimpleTomcatServer#main` 方法。 + +或执行 `io.github.dunwu.javatech.server.TomcatServer.main` 方法。 + +启动后,访问 http://localhost:8080/javatool-server/ + +### 插件启动嵌入式 Tomcat + +由于插件很久没有更新(最新版本发布时间:2013-11-11),目前只能找到 Tomcat6 、Tomcat7 插件,所以弃用。 + +### 脚本启动 + +> 本项目添加了脚本启动范例。 +> +> 脚本代码全在 [`scripts`](https://github.com/dunwu/JavaStack/tree/master/scripts) 目录下。 + +* 初始化 + +```bash +wget https://raw.githubusercontent.com/dunwu/JavaStack/master/scripts/init.sh +chmod 777 init.sh +./init.sh +``` + +* 发布 + +``` +cd /home/zp/source/JavaStack/scripts +./javatool-server-release.sh master develop +``` + +## Jetty + +待添加。。。 diff --git a/codes/javatech/javatech-server/pom.xml b/codes/javatech/javatech-server/pom.xml new file mode 100644 index 00000000..b4879ebd --- /dev/null +++ b/codes/javatech/javatech-server/pom.xml @@ -0,0 +1,95 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 1.0.8 + + + io.github.dunwu.javatech + javatech-server + 1.0.0 + war + JAVATECH-服务器示例 + + + [8.5.40,) + + + + + + ch.qos.logback + logback-classic + + + org.logback-extensions + logback-ext-spring + 0.1.5 + + + org.slf4j + jcl-over-slf4j + + + + + + javax.servlet + javax.servlet-api + provided + + + javax.servlet.jsp + jsp-api + 2.2 + provided + + + + + + org.apache.tomcat.embed + tomcat-embed-core + + + org.apache.tomcat.embed + tomcat-embed-el + + + org.apache.tomcat.embed + tomcat-embed-jasper + + + + + + org.springframework + spring-context-support + + + org.springframework + spring-webmvc + + + org.springframework + spring-test + + + junit + junit + test + + + org.assertj + assertj-core + test + + + + + diff --git a/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/controller/HelloController.java b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/controller/HelloController.java new file mode 100644 index 00000000..07e678e0 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/controller/HelloController.java @@ -0,0 +1,61 @@ +package io.github.dunwu.javatech.controller; + +import io.github.dunwu.javatech.service.HelloService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.servlet.ModelAndView; + +/** + * spring mvc 的第一个程序 + * + * @author Zhang Peng + * @since 2016.07.29 + */ +@Controller +@RequestMapping(value = "/hello") +public class HelloController { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private HelloService helloService; + + /** + *

+ * 在本例中,Spring将会将数据传给 hello.jsp + *

+ * 访问形式:http://localhost:8080/hello?name=张三 + */ + @RequestMapping(value = "/name", method = RequestMethod.GET) + public ModelAndView hello(@RequestParam("name") String name) { + ModelAndView mav = new ModelAndView(); + mav.addObject("message", "你好," + name); + mav.setViewName(helloService.hello()); + return mav; + } + + /** + *

+ * 测试 logback 分级日志。配置项见src/main/resouces/logback.xml + *

+ * 访问形式:http://localhost:8080/log + */ + @ResponseBody + @RequestMapping(value = "/log", method = RequestMethod.GET) + public String log() { + String msg = "print log, current level: {}"; + log.trace(msg, "trace"); + log.debug(msg, "debug"); + log.info(msg, "info"); + log.warn(msg, "warn"); + log.error(msg, "error"); + return msg; + } + +} diff --git a/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/controller/IndexController.java b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/controller/IndexController.java new file mode 100644 index 00000000..65549bb7 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/controller/IndexController.java @@ -0,0 +1,31 @@ +/** + * The Apache License 2.0 Copyright (c) 2016 Zhang Peng + */ +package io.github.dunwu.javatech.controller; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.servlet.ModelAndView; + +/** + * @author Zhang Peng + * @since 2017/4/12. + */ +@Controller +public class IndexController { + + /** + *

+ * 返回 ModelAndView 对象到视图层。在本例中,视图解析器解析视图名为 index,会自动关联到 index.jsp。 + *

+ * 访问形式:http://localhost:8080/ + */ + @RequestMapping(value = "/", method = RequestMethod.GET) + public ModelAndView index() { + ModelAndView mav = new ModelAndView(); + mav.setViewName("index"); + return mav; + } + +} diff --git a/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/server/SimpleTomcatServer.java b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/server/SimpleTomcatServer.java new file mode 100644 index 00000000..5741fc81 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/server/SimpleTomcatServer.java @@ -0,0 +1,41 @@ +package io.github.dunwu.javatech.server; + +import org.apache.catalina.startup.Tomcat; + +import java.util.Optional; + +/** + * 简单的嵌入式 Tomcat 启动类 启动后可访问 http://localhost:8080/javatool-server/ + * + * @author Zhang Peng + */ +public class SimpleTomcatServer { + + private static final int PORT = 8080; + + private static final String CONTEXT_PATH = "/javatool-server"; + + public static void main(String[] args) throws Exception { + // 设定 profile + Optional profile = Optional.ofNullable(System.getProperty("spring.profiles.active")); + System.setProperty("spring.profiles.active", profile.orElse("develop")); + + Tomcat tomcat = new Tomcat(); + tomcat.setPort(PORT); + tomcat.getHost().setAppBase("."); + tomcat.addWebapp(CONTEXT_PATH, getAbsolutePath() + "src/main/webapp"); + tomcat.start(); + tomcat.getServer().await(); + } + + private static String getAbsolutePath() { + String path = null; + String folderPath = SimpleTomcatServer.class.getProtectionDomain().getCodeSource().getLocation().getPath() + .substring(1); + if (folderPath.indexOf("target") > 0) { + path = folderPath.substring(0, folderPath.indexOf("target")); + } + return path; + } + +} diff --git a/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/server/TomcatServer.java b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/server/TomcatServer.java new file mode 100644 index 00000000..a18bb192 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/server/TomcatServer.java @@ -0,0 +1,135 @@ +package io.github.dunwu.javatech.server; + +import org.apache.catalina.Server; +import org.apache.catalina.startup.Catalina; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.util.digester.Digester; +import org.apache.tomcat.util.scan.Constants; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.util.StringUtils; + +import java.io.File; + +public class TomcatServer { + + private static final Logger log = LoggerFactory.getLogger(TomcatServer.class); + + private static final String CONNECTOR_PORT = "8080"; + + private static final String RELATIVE_DEV_DUBBO_RESOVE_FILE = + "src/main/resources/properties/dubbo-resolve.properties"; + + private static final String RELATIVE_DUBBO_RESOVE_FILE = "WEB-INF/classes/properties/dubbo-resolve.properties"; + + // 以下设置轻易不要改动 + private static final String RELATIVE_DEV_BASE_DIR = "src/main/resources/tomcat/"; + + private static final String RELATIVE_BASE_DIR = "WEB-INF/classes/tomcat/"; + + private static final String RELATIVE_DEV_DOCBASE_DIR = "src/main/webapp"; + + private static final String RELATIVE_DOCBASE_DIR = "./"; + + public static void main(String[] args) throws Exception { + // 设定Spring的profile + if (StringUtils.isEmpty(System.getProperty("spring.profiles.active"))) { + System.setProperty("spring.profiles.active", "develop"); + } + + System.setProperty("tomcat.host.appBase", getAbsolutePath()); + File checkFile = new File(System.getProperty("tomcat.host.appBase") + "/WEB-INF"); + if (!checkFile.exists()) { + System.setProperty("catalina.base", getAbsolutePath() + RELATIVE_DEV_BASE_DIR); + System.setProperty("tomcat.context.docBase", RELATIVE_DEV_DOCBASE_DIR); + System.setProperty("dubbo.resolve.file", getAbsolutePath() + RELATIVE_DEV_DUBBO_RESOVE_FILE); + } else { + System.setProperty("catalina.base", getAbsolutePath() + RELATIVE_BASE_DIR); + System.setProperty("tomcat.context.docBase", RELATIVE_DOCBASE_DIR); + if ("develop".equalsIgnoreCase(System.getProperty("spring.profiles.active")) + || "test".equalsIgnoreCase("spring.profiles.active")) { + System.setProperty("dubbo.resolve.file", getAbsolutePath() + RELATIVE_DUBBO_RESOVE_FILE); + } + } + + if (StringUtils.isEmpty(System.getProperty("tomcat.connector.port"))) { + System.setProperty("tomcat.connector.port", CONNECTOR_PORT); + } + if (StringUtils.isEmpty(System.getProperty("tomcat.server.shutdownPort"))) { + System.setProperty("tomcat.server.shutdownPort", + String.valueOf(Integer.valueOf(System.getProperty("tomcat.connector.port")) + 10000)); + } + + log.info("====================ENV setting===================="); + log.info("spring.profiles.active:" + System.getProperty("spring.profiles.active")); + log.info("dubbo.resolve.file:" + System.getProperty("dubbo.resolve.file")); + log.info("catalina.base:" + System.getProperty("catalina.base")); + log.info("tomcat.host.appBase:" + System.getProperty("tomcat.host.appBase")); + log.info("tomcat.context.docBase:" + System.getProperty("tomcat.context.docBase")); + log.info("tomcat.connector.port:" + System.getProperty("tomcat.connector.port")); + log.info("tomcat.server.shutdownPort:" + System.getProperty("tomcat.server.shutdownPort")); + + ExtendedTomcat tomcat = new ExtendedTomcat(); + tomcat.start(); + tomcat.getServer().await(); + } + + private static String getAbsolutePath() { + String path = null; + String folderPath = TomcatServer.class.getProtectionDomain().getCodeSource().getLocation().getPath(); + if (folderPath.indexOf("WEB-INF") > 0) { + path = folderPath.substring(0, folderPath.indexOf("WEB-INF")); + } else if (folderPath.indexOf("target") > 0) { + path = folderPath.substring(0, folderPath.indexOf("target")); + } + return path; + } + + static class ExtendedTomcat extends Tomcat { + + private static final String RELATIVE_SERVERXML_PATH = "/conf/server.xml"; + + private Logger log = LoggerFactory.getLogger(this.getClass()); + + @Override + public Server getServer() { + if (server != null) { + return server; + } + // 默认不开启JNDI. 开启时, 注意maven必须添加tomcat-dbcp依赖 + System.setProperty("catalina.useNaming", "false"); + ExtendedCatalina extendedCatalina = new ExtendedCatalina(); + + // 覆盖默认的skip和scan jar包配置 + System.setProperty(Constants.SKIP_JARS_PROPERTY, ""); + System.setProperty(Constants.SCAN_JARS_PROPERTY, ""); + + Digester digester = extendedCatalina.createStartDigester(); + digester.push(extendedCatalina); + try { + server = ((ExtendedCatalina) digester + .parse(new File(System.getProperty("catalina.base") + RELATIVE_SERVERXML_PATH))).getServer(); + // 设置catalina.base和catalna.home + this.initBaseDir(); + return server; + } catch (Exception e) { + log.error("Error while parsing server.xml", e); + throw new RuntimeException("server未创建,请检查server.xml(路径:" + System.getProperty("catalina.base") + + RELATIVE_SERVERXML_PATH + ")配置是否正确"); + } + } + + private static class ExtendedCatalina extends Catalina { + + @Override + public Digester createStartDigester() { + return super.createStartDigester(); + } + + } + + } + +} + + diff --git a/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/service/HelloService.java b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/service/HelloService.java new file mode 100644 index 00000000..75d9b577 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/service/HelloService.java @@ -0,0 +1,15 @@ +package io.github.dunwu.javatech.service; + +import org.springframework.stereotype.Service; + +/** + * @author Zhang Peng + * @date 2022-07-28 + */ +@Service +public class HelloService { + + public String hello() { + return "hello"; + } +} diff --git a/codes/javatech/javatech-server/src/main/resources/logback.xml b/codes/javatech/javatech-server/src/main/resources/logback.xml new file mode 100644 index 00000000..9884e7be --- /dev/null +++ b/codes/javatech/javatech-server/src/main/resources/logback.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n + + + + + + + + ${user.dir}/logs/${FILE_NAME}-all.%d{yyyy-MM-dd}.log + 30 + + + + + 30MB + + + + %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n + + + + + + + + + + + + + + + + + + diff --git a/codes/javatech/javatech-server/src/main/resources/properties/application-develop.properties b/codes/javatech/javatech-server/src/main/resources/properties/application-develop.properties new file mode 100644 index 00000000..25f09a35 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/resources/properties/application-develop.properties @@ -0,0 +1,11 @@ +# jdbc +jdbc.driver = +jdbc.url = +jdbc.username = +jdbc.password = +# redis +redis.name = +redis.host = +redis.port = +redis.password = +redis.database = diff --git a/codes/javatech/javatech-server/src/main/resources/properties/application-test.properties b/codes/javatech/javatech-server/src/main/resources/properties/application-test.properties new file mode 100644 index 00000000..25f09a35 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/resources/properties/application-test.properties @@ -0,0 +1,11 @@ +# jdbc +jdbc.driver = +jdbc.url = +jdbc.username = +jdbc.password = +# redis +redis.name = +redis.host = +redis.port = +redis.password = +redis.database = diff --git a/codes/javatech/javatech-server/src/main/resources/spring/spring-servlet.xml b/codes/javatech/javatech-server/src/main/resources/spring/spring-servlet.xml new file mode 100644 index 00000000..6c05fe85 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/resources/spring/spring-servlet.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/codes/javatech/javatech-server/src/main/resources/tomcat/conf/server.xml b/codes/javatech/javatech-server/src/main/resources/tomcat/conf/server.xml new file mode 100644 index 00000000..77316698 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/resources/tomcat/conf/server.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/codes/javatech/javatech-server/src/main/resources/tomcat/conf/web.xml b/codes/javatech/javatech-server/src/main/resources/tomcat/conf/web.xml new file mode 100644 index 00000000..7dc916a5 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/resources/tomcat/conf/web.xml @@ -0,0 +1,4703 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + default + org.apache.catalina.servlets.DefaultServlet + + debug + 0 + + + listings + false + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + jsp + org.apache.jasper.servlet.JspServlet + + fork + false + + + xpoweredBy + false + + + keepgenerated + false + + 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + default + / + + + + + jsp + *.jsp + *.jspx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 30 + + + + + + + + + + + + + 123 + application/vnd.lotus-1-2-3 + + + 3dml + text/vnd.in3d.3dml + + + 3ds + image/x-3ds + + + 3g2 + video/3gpp2 + + + 3gp + video/3gpp + + + 7z + application/x-7z-compressed + + + aab + application/x-authorware-bin + + + aac + audio/x-aac + + + aam + application/x-authorware-map + + + aas + application/x-authorware-seg + + + abs + audio/x-mpeg + + + abw + application/x-abiword + + + ac + application/pkix-attr-cert + + + acc + application/vnd.americandynamics.acc + + + ace + application/x-ace-compressed + + + acu + application/vnd.acucobol + + + acutc + application/vnd.acucorp + + + adp + audio/adpcm + + + aep + application/vnd.audiograph + + + afm + application/x-font-type1 + + + afp + application/vnd.ibm.modcap + + + ahead + application/vnd.ahead.space + + + ai + application/postscript + + + aif + audio/x-aiff + + + aifc + audio/x-aiff + + + aiff + audio/x-aiff + + + aim + application/x-aim + + + air + application/vnd.adobe.air-application-installer-package+zip + + + ait + application/vnd.dvb.ait + + + ami + application/vnd.amiga.ami + + + anx + application/annodex + + + apk + application/vnd.android.package-archive + + + appcache + text/cache-manifest + + + application + application/x-ms-application + + + apr + application/vnd.lotus-approach + + + arc + application/x-freearc + + + art + image/x-jg + + + asc + application/pgp-signature + + + asf + video/x-ms-asf + + + asm + text/x-asm + + + aso + application/vnd.accpac.simply.aso + + + asx + video/x-ms-asf + + + atc + application/vnd.acucorp + + + atom + application/atom+xml + + + atomcat + application/atomcat+xml + + + atomsvc + application/atomsvc+xml + + + atx + application/vnd.antix.game-component + + + au + audio/basic + + + avi + video/x-msvideo + + + avx + video/x-rad-screenplay + + + aw + application/applixware + + + axa + audio/annodex + + + axv + video/annodex + + + azf + application/vnd.airzip.filesecure.azf + + + azs + application/vnd.airzip.filesecure.azs + + + azw + application/vnd.amazon.ebook + + + bat + application/x-msdownload + + + bcpio + application/x-bcpio + + + bdf + application/x-font-bdf + + + bdm + application/vnd.syncml.dm+wbxml + + + bed + application/vnd.realvnc.bed + + + bh2 + application/vnd.fujitsu.oasysprs + + + bin + application/octet-stream + + + blb + application/x-blorb + + + blorb + application/x-blorb + + + bmi + application/vnd.bmi + + + bmp + image/bmp + + + body + text/html + + + book + application/vnd.framemaker + + + box + application/vnd.previewsystems.box + + + boz + application/x-bzip2 + + + bpk + application/octet-stream + + + btif + image/prs.btif + + + bz + application/x-bzip + + + bz2 + application/x-bzip2 + + + c + text/x-c + + + c11amc + application/vnd.cluetrust.cartomobile-config + + + c11amz + application/vnd.cluetrust.cartomobile-config-pkg + + + c4d + application/vnd.clonk.c4group + + + c4f + application/vnd.clonk.c4group + + + c4g + application/vnd.clonk.c4group + + + c4p + application/vnd.clonk.c4group + + + c4u + application/vnd.clonk.c4group + + + cab + application/vnd.ms-cab-compressed + + + caf + audio/x-caf + + + cap + application/vnd.tcpdump.pcap + + + car + application/vnd.curl.car + + + cat + application/vnd.ms-pki.seccat + + + cb7 + application/x-cbr + + + cba + application/x-cbr + + + cbr + application/x-cbr + + + cbt + application/x-cbr + + + cbz + application/x-cbr + + + cc + text/x-c + + + cct + application/x-director + + + ccxml + application/ccxml+xml + + + cdbcmsg + application/vnd.contact.cmsg + + + cdf + application/x-cdf + + + cdkey + application/vnd.mediastation.cdkey + + + cdmia + application/cdmi-capability + + + cdmic + application/cdmi-container + + + cdmid + application/cdmi-domain + + + cdmio + application/cdmi-object + + + cdmiq + application/cdmi-queue + + + cdx + chemical/x-cdx + + + cdxml + application/vnd.chemdraw+xml + + + cdy + application/vnd.cinderella + + + cer + application/pkix-cert + + + cfs + application/x-cfs-compressed + + + cgm + image/cgm + + + chat + application/x-chat + + + chm + application/vnd.ms-htmlhelp + + + chrt + application/vnd.kde.kchart + + + cif + chemical/x-cif + + + cii + application/vnd.anser-web-certificate-issue-initiation + + + cil + application/vnd.ms-artgalry + + + cla + application/vnd.claymore + + + class + application/java + + + clkk + application/vnd.crick.clicker.keyboard + + + clkp + application/vnd.crick.clicker.palette + + + clkt + application/vnd.crick.clicker.template + + + clkw + application/vnd.crick.clicker.wordbank + + + clkx + application/vnd.crick.clicker + + + clp + application/x-msclip + + + cmc + application/vnd.cosmocaller + + + cmdf + chemical/x-cmdf + + + cml + chemical/x-cml + + + cmp + application/vnd.yellowriver-custom-menu + + + cmx + image/x-cmx + + + cod + application/vnd.rim.cod + + + com + application/x-msdownload + + + conf + text/plain + + + cpio + application/x-cpio + + + cpp + text/x-c + + + cpt + application/mac-compactpro + + + crd + application/x-mscardfile + + + crl + application/pkix-crl + + + crt + application/x-x509-ca-cert + + + cryptonote + application/vnd.rig.cryptonote + + + csh + application/x-csh + + + csml + chemical/x-csml + + + csp + application/vnd.commonspace + + + css + text/css + + + cst + application/x-director + + + csv + text/csv + + + cu + application/cu-seeme + + + curl + text/vnd.curl + + + cww + application/prs.cww + + + cxt + application/x-director + + + cxx + text/x-c + + + dae + model/vnd.collada+xml + + + daf + application/vnd.mobius.daf + + + dart + application/vnd.dart + + + dataless + application/vnd.fdsn.seed + + + davmount + application/davmount+xml + + + dbk + application/docbook+xml + + + dcr + application/x-director + + + dcurl + text/vnd.curl.dcurl + + + dd2 + application/vnd.oma.dd2+xml + + + ddd + application/vnd.fujixerox.ddd + + + deb + application/x-debian-package + + + def + text/plain + + + deploy + application/octet-stream + + + der + application/x-x509-ca-cert + + + dfac + application/vnd.dreamfactory + + + dgc + application/x-dgc-compressed + + + dib + image/bmp + + + dic + text/x-c + + + dir + application/x-director + + + dis + application/vnd.mobius.dis + + + dist + application/octet-stream + + + distz + application/octet-stream + + + djv + image/vnd.djvu + + + djvu + image/vnd.djvu + + + dll + application/x-msdownload + + + dmg + application/x-apple-diskimage + + + dmp + application/vnd.tcpdump.pcap + + + dms + application/octet-stream + + + dna + application/vnd.dna + + + doc + application/msword + + + docm + application/vnd.ms-word.document.macroenabled.12 + + + docx + application/vnd.openxmlformats-officedocument.wordprocessingml.document + + + dot + application/msword + + + dotm + application/vnd.ms-word.template.macroenabled.12 + + + dotx + application/vnd.openxmlformats-officedocument.wordprocessingml.template + + + dp + application/vnd.osgi.dp + + + dpg + application/vnd.dpgraph + + + dra + audio/vnd.dra + + + dsc + text/prs.lines.tag + + + dssc + application/dssc+der + + + dtb + application/x-dtbook+xml + + + dtd + application/xml-dtd + + + dts + audio/vnd.dts + + + dtshd + audio/vnd.dts.hd + + + dump + application/octet-stream + + + dv + video/x-dv + + + dvb + video/vnd.dvb.file + + + dvi + application/x-dvi + + + dwf + model/vnd.dwf + + + dwg + image/vnd.dwg + + + dxf + image/vnd.dxf + + + dxp + application/vnd.spotfire.dxp + + + dxr + application/x-director + + + ecelp4800 + audio/vnd.nuera.ecelp4800 + + + ecelp7470 + audio/vnd.nuera.ecelp7470 + + + ecelp9600 + audio/vnd.nuera.ecelp9600 + + + ecma + application/ecmascript + + + edm + application/vnd.novadigm.edm + + + edx + application/vnd.novadigm.edx + + + efif + application/vnd.picsel + + + ei6 + application/vnd.pg.osasli + + + elc + application/octet-stream + + + emf + application/x-msmetafile + + + eml + message/rfc822 + + + emma + application/emma+xml + + + emz + application/x-msmetafile + + + eol + audio/vnd.digital-winds + + + eot + application/vnd.ms-fontobject + + + eps + application/postscript + + + epub + application/epub+zip + + + es3 + application/vnd.eszigno3+xml + + + esa + application/vnd.osgi.subsystem + + + esf + application/vnd.epson.esf + + + et3 + application/vnd.eszigno3+xml + + + etx + text/x-setext + + + eva + application/x-eva + + + evy + application/x-envoy + + + exe + application/octet-stream + + + exi + application/exi + + + ext + application/vnd.novadigm.ext + + + ez + application/andrew-inset + + + ez2 + application/vnd.ezpix-album + + + ez3 + application/vnd.ezpix-package + + + f + text/x-fortran + + + f4v + video/x-f4v + + + f77 + text/x-fortran + + + f90 + text/x-fortran + + + fbs + image/vnd.fastbidsheet + + + fcdt + application/vnd.adobe.formscentral.fcdt + + + fcs + application/vnd.isac.fcs + + + fdf + application/vnd.fdf + + + fe_launch + application/vnd.denovo.fcselayout-link + + + fg5 + application/vnd.fujitsu.oasysgp + + + fgd + application/x-director + + + fh + image/x-freehand + + + fh4 + image/x-freehand + + + fh5 + image/x-freehand + + + fh7 + image/x-freehand + + + fhc + image/x-freehand + + + fig + application/x-xfig + + + flac + audio/flac + + + fli + video/x-fli + + + flo + application/vnd.micrografx.flo + + + flv + video/x-flv + + + flw + application/vnd.kde.kivio + + + flx + text/vnd.fmi.flexstor + + + fly + text/vnd.fly + + + fm + application/vnd.framemaker + + + fnc + application/vnd.frogans.fnc + + + for + text/x-fortran + + + fpx + image/vnd.fpx + + + frame + application/vnd.framemaker + + + fsc + application/vnd.fsc.weblaunch + + + fst + image/vnd.fst + + + ftc + application/vnd.fluxtime.clip + + + fti + application/vnd.anser-web-funds-transfer-initiation + + + fvt + video/vnd.fvt + + + fxp + application/vnd.adobe.fxp + + + fxpl + application/vnd.adobe.fxp + + + fzs + application/vnd.fuzzysheet + + + g2w + application/vnd.geoplan + + + g3 + image/g3fax + + + g3w + application/vnd.geospace + + + gac + application/vnd.groove-account + + + gam + application/x-tads + + + gbr + application/rpki-ghostbusters + + + gca + application/x-gca-compressed + + + gdl + model/vnd.gdl + + + geo + application/vnd.dynageo + + + gex + application/vnd.geometry-explorer + + + ggb + application/vnd.geogebra.file + + + ggt + application/vnd.geogebra.tool + + + ghf + application/vnd.groove-help + + + gif + image/gif + + + gim + application/vnd.groove-identity-message + + + gml + application/gml+xml + + + gmx + application/vnd.gmx + + + gnumeric + application/x-gnumeric + + + gph + application/vnd.flographit + + + gpx + application/gpx+xml + + + gqf + application/vnd.grafeq + + + gqs + application/vnd.grafeq + + + gram + application/srgs + + + gramps + application/x-gramps-xml + + + gre + application/vnd.geometry-explorer + + + grv + application/vnd.groove-injector + + + grxml + application/srgs+xml + + + gsf + application/x-font-ghostscript + + + gtar + application/x-gtar + + + gtm + application/vnd.groove-tool-message + + + gtw + model/vnd.gtw + + + gv + text/vnd.graphviz + + + gxf + application/gxf + + + gxt + application/vnd.geonext + + + gz + application/x-gzip + + + h + text/x-c + + + h261 + video/h261 + + + h263 + video/h263 + + + h264 + video/h264 + + + hal + application/vnd.hal+xml + + + hbci + application/vnd.hbci + + + hdf + application/x-hdf + + + hh + text/x-c + + + hlp + application/winhlp + + + hpgl + application/vnd.hp-hpgl + + + hpid + application/vnd.hp-hpid + + + hps + application/vnd.hp-hps + + + hqx + application/mac-binhex40 + + + htc + text/x-component + + + htke + application/vnd.kenameaapp + + + htm + text/html + + + html + text/html + + + hvd + application/vnd.yamaha.hv-dic + + + hvp + application/vnd.yamaha.hv-voice + + + hvs + application/vnd.yamaha.hv-script + + + i2g + application/vnd.intergeo + + + icc + application/vnd.iccprofile + + + ice + x-conference/x-cooltalk + + + icm + application/vnd.iccprofile + + + ico + image/x-icon + + + ics + text/calendar + + + ief + image/ief + + + ifb + text/calendar + + + ifm + application/vnd.shana.informed.formdata + + + iges + model/iges + + + igl + application/vnd.igloader + + + igm + application/vnd.insors.igm + + + igs + model/iges + + + igx + application/vnd.micrografx.igx + + + iif + application/vnd.shana.informed.interchange + + + imp + application/vnd.accpac.simply.imp + + + ims + application/vnd.ms-ims + + + in + text/plain + + + ink + application/inkml+xml + + + inkml + application/inkml+xml + + + install + application/x-install-instructions + + + iota + application/vnd.astraea-software.iota + + + ipfix + application/ipfix + + + ipk + application/vnd.shana.informed.package + + + irm + application/vnd.ibm.rights-management + + + irp + application/vnd.irepository.package+xml + + + iso + application/x-iso9660-image + + + itp + application/vnd.shana.informed.formtemplate + + + ivp + application/vnd.immervision-ivp + + + ivu + application/vnd.immervision-ivu + + + jad + text/vnd.sun.j2me.app-descriptor + + + jam + application/vnd.jam + + + jar + application/java-archive + + + java + text/x-java-source + + + jisp + application/vnd.jisp + + + jlt + application/vnd.hp-jlyt + + + jnlp + application/x-java-jnlp-file + + + joda + application/vnd.joost.joda-archive + + + jpe + image/jpeg + + + jpeg + image/jpeg + + + jpg + image/jpeg + + + jpgm + video/jpm + + + jpgv + video/jpeg + + + jpm + video/jpm + + + js + application/javascript + + + jsf + text/plain + + + json + application/json + + + jsonml + application/jsonml+json + + + jspf + text/plain + + + kar + audio/midi + + + karbon + application/vnd.kde.karbon + + + kfo + application/vnd.kde.kformula + + + kia + application/vnd.kidspiration + + + kml + application/vnd.google-earth.kml+xml + + + kmz + application/vnd.google-earth.kmz + + + kne + application/vnd.kinar + + + knp + application/vnd.kinar + + + kon + application/vnd.kde.kontour + + + kpr + application/vnd.kde.kpresenter + + + kpt + application/vnd.kde.kpresenter + + + kpxx + application/vnd.ds-keypoint + + + ksp + application/vnd.kde.kspread + + + ktr + application/vnd.kahootz + + + ktx + image/ktx + + + ktz + application/vnd.kahootz + + + kwd + application/vnd.kde.kword + + + kwt + application/vnd.kde.kword + + + lasxml + application/vnd.las.las+xml + + + latex + application/x-latex + + + lbd + application/vnd.llamagraphics.life-balance.desktop + + + lbe + application/vnd.llamagraphics.life-balance.exchange+xml + + + les + application/vnd.hhe.lesson-player + + + lha + application/x-lzh-compressed + + + link66 + application/vnd.route66.link66+xml + + + list + text/plain + + + list3820 + application/vnd.ibm.modcap + + + listafp + application/vnd.ibm.modcap + + + lnk + application/x-ms-shortcut + + + log + text/plain + + + lostxml + application/lost+xml + + + lrf + application/octet-stream + + + lrm + application/vnd.ms-lrm + + + ltf + application/vnd.frogans.ltf + + + lvp + audio/vnd.lucent.voice + + + lwp + application/vnd.lotus-wordpro + + + lzh + application/x-lzh-compressed + + + m13 + application/x-msmediaview + + + m14 + application/x-msmediaview + + + m1v + video/mpeg + + + m21 + application/mp21 + + + m2a + audio/mpeg + + + m2v + video/mpeg + + + m3a + audio/mpeg + + + m3u + audio/x-mpegurl + + + m3u8 + application/vnd.apple.mpegurl + + + m4a + audio/mp4 + + + m4b + audio/mp4 + + + m4r + audio/mp4 + + + m4u + video/vnd.mpegurl + + + m4v + video/mp4 + + + ma + application/mathematica + + + mac + image/x-macpaint + + + mads + application/mads+xml + + + mag + application/vnd.ecowin.chart + + + maker + application/vnd.framemaker + + + man + text/troff + + + mar + application/octet-stream + + + mathml + application/mathml+xml + + + mb + application/mathematica + + + mbk + application/vnd.mobius.mbk + + + mbox + application/mbox + + + mc1 + application/vnd.medcalcdata + + + mcd + application/vnd.mcd + + + mcurl + text/vnd.curl.mcurl + + + mdb + application/x-msaccess + + + mdi + image/vnd.ms-modi + + + me + text/troff + + + mesh + model/mesh + + + meta4 + application/metalink4+xml + + + metalink + application/metalink+xml + + + mets + application/mets+xml + + + mfm + application/vnd.mfmp + + + mft + application/rpki-manifest + + + mgp + application/vnd.osgeo.mapguide.package + + + mgz + application/vnd.proteus.magazine + + + mid + audio/midi + + + midi + audio/midi + + + mie + application/x-mie + + + mif + application/x-mif + + + mime + message/rfc822 + + + mj2 + video/mj2 + + + mjp2 + video/mj2 + + + mk3d + video/x-matroska + + + mka + audio/x-matroska + + + mks + video/x-matroska + + + mkv + video/x-matroska + + + mlp + application/vnd.dolby.mlp + + + mmd + application/vnd.chipnuts.karaoke-mmd + + + mmf + application/vnd.smaf + + + mmr + image/vnd.fujixerox.edmics-mmr + + + mng + video/x-mng + + + mny + application/x-msmoney + + + mobi + application/x-mobipocket-ebook + + + mods + application/mods+xml + + + mov + video/quicktime + + + movie + video/x-sgi-movie + + + mp1 + audio/mpeg + + + mp2 + audio/mpeg + + + mp21 + application/mp21 + + + mp2a + audio/mpeg + + + mp3 + audio/mpeg + + + mp4 + video/mp4 + + + mp4a + audio/mp4 + + + mp4s + application/mp4 + + + mp4v + video/mp4 + + + mpa + audio/mpeg + + + mpc + application/vnd.mophun.certificate + + + mpe + video/mpeg + + + mpeg + video/mpeg + + + mpega + audio/x-mpeg + + + mpg + video/mpeg + + + mpg4 + video/mp4 + + + mpga + audio/mpeg + + + mpkg + application/vnd.apple.installer+xml + + + mpm + application/vnd.blueice.multipass + + + mpn + application/vnd.mophun.application + + + mpp + application/vnd.ms-project + + + mpt + application/vnd.ms-project + + + mpv2 + video/mpeg2 + + + mpy + application/vnd.ibm.minipay + + + mqy + application/vnd.mobius.mqy + + + mrc + application/marc + + + mrcx + application/marcxml+xml + + + ms + text/troff + + + mscml + application/mediaservercontrol+xml + + + mseed + application/vnd.fdsn.mseed + + + mseq + application/vnd.mseq + + + msf + application/vnd.epson.msf + + + msh + model/mesh + + + msi + application/x-msdownload + + + msl + application/vnd.mobius.msl + + + msty + application/vnd.muvee.style + + + mts + model/vnd.mts + + + mus + application/vnd.musician + + + musicxml + application/vnd.recordare.musicxml+xml + + + mvb + application/x-msmediaview + + + mwf + application/vnd.mfer + + + mxf + application/mxf + + + mxl + application/vnd.recordare.musicxml + + + mxml + application/xv+xml + + + mxs + application/vnd.triscape.mxs + + + mxu + video/vnd.mpegurl + + + n-gage + application/vnd.nokia.n-gage.symbian.install + + + n3 + text/n3 + + + nb + application/mathematica + + + nbp + application/vnd.wolfram.player + + + nc + application/x-netcdf + + + ncx + application/x-dtbncx+xml + + + nfo + text/x-nfo + + + ngdat + application/vnd.nokia.n-gage.data + + + nitf + application/vnd.nitf + + + nlu + application/vnd.neurolanguage.nlu + + + nml + application/vnd.enliven + + + nnd + application/vnd.noblenet-directory + + + nns + application/vnd.noblenet-sealer + + + nnw + application/vnd.noblenet-web + + + npx + image/vnd.net-fpx + + + nsc + application/x-conference + + + nsf + application/vnd.lotus-notes + + + ntf + application/vnd.nitf + + + nzb + application/x-nzb + + + oa2 + application/vnd.fujitsu.oasys2 + + + oa3 + application/vnd.fujitsu.oasys3 + + + oas + application/vnd.fujitsu.oasys + + + obd + application/x-msbinder + + + obj + application/x-tgif + + + oda + application/oda + + + + odb + application/vnd.oasis.opendocument.database + + + + odc + application/vnd.oasis.opendocument.chart + + + + odf + application/vnd.oasis.opendocument.formula + + + odft + application/vnd.oasis.opendocument.formula-template + + + + odg + application/vnd.oasis.opendocument.graphics + + + + odi + application/vnd.oasis.opendocument.image + + + + odm + application/vnd.oasis.opendocument.text-master + + + + odp + application/vnd.oasis.opendocument.presentation + + + + ods + application/vnd.oasis.opendocument.spreadsheet + + + + odt + application/vnd.oasis.opendocument.text + + + oga + audio/ogg + + + ogg + audio/ogg + + + ogv + video/ogg + + + + ogx + application/ogg + + + omdoc + application/omdoc+xml + + + onepkg + application/onenote + + + onetmp + application/onenote + + + onetoc + application/onenote + + + onetoc2 + application/onenote + + + opf + application/oebps-package+xml + + + opml + text/x-opml + + + oprc + application/vnd.palm + + + org + application/vnd.lotus-organizer + + + osf + application/vnd.yamaha.openscoreformat + + + osfpvg + application/vnd.yamaha.openscoreformat.osfpvg+xml + + + otc + application/vnd.oasis.opendocument.chart-template + + + otf + font/otf + + + + otg + application/vnd.oasis.opendocument.graphics-template + + + + oth + application/vnd.oasis.opendocument.text-web + + + oti + application/vnd.oasis.opendocument.image-template + + + + otp + application/vnd.oasis.opendocument.presentation-template + + + + ots + application/vnd.oasis.opendocument.spreadsheet-template + + + + ott + application/vnd.oasis.opendocument.text-template + + + oxps + application/oxps + + + oxt + application/vnd.openofficeorg.extension + + + p + text/x-pascal + + + p10 + application/pkcs10 + + + p12 + application/x-pkcs12 + + + p7b + application/x-pkcs7-certificates + + + p7c + application/pkcs7-mime + + + p7m + application/pkcs7-mime + + + p7r + application/x-pkcs7-certreqresp + + + p7s + application/pkcs7-signature + + + p8 + application/pkcs8 + + + pas + text/x-pascal + + + paw + application/vnd.pawaafile + + + pbd + application/vnd.powerbuilder6 + + + pbm + image/x-portable-bitmap + + + pcap + application/vnd.tcpdump.pcap + + + pcf + application/x-font-pcf + + + pcl + application/vnd.hp-pcl + + + pclxl + application/vnd.hp-pclxl + + + pct + image/pict + + + pcurl + application/vnd.curl.pcurl + + + pcx + image/x-pcx + + + pdb + application/vnd.palm + + + pdf + application/pdf + + + pfa + application/x-font-type1 + + + pfb + application/x-font-type1 + + + pfm + application/x-font-type1 + + + pfr + application/font-tdpfr + + + pfx + application/x-pkcs12 + + + pgm + image/x-portable-graymap + + + pgn + application/x-chess-pgn + + + pgp + application/pgp-encrypted + + + pic + image/pict + + + pict + image/pict + + + pkg + application/octet-stream + + + pki + application/pkixcmp + + + pkipath + application/pkix-pkipath + + + plb + application/vnd.3gpp.pic-bw-large + + + plc + application/vnd.mobius.plc + + + plf + application/vnd.pocketlearn + + + pls + audio/x-scpls + + + pml + application/vnd.ctc-posml + + + png + image/png + + + pnm + image/x-portable-anymap + + + pnt + image/x-macpaint + + + portpkg + application/vnd.macports.portpkg + + + pot + application/vnd.ms-powerpoint + + + potm + application/vnd.ms-powerpoint.template.macroenabled.12 + + + potx + application/vnd.openxmlformats-officedocument.presentationml.template + + + ppam + application/vnd.ms-powerpoint.addin.macroenabled.12 + + + ppd + application/vnd.cups-ppd + + + ppm + image/x-portable-pixmap + + + pps + application/vnd.ms-powerpoint + + + ppsm + application/vnd.ms-powerpoint.slideshow.macroenabled.12 + + + ppsx + application/vnd.openxmlformats-officedocument.presentationml.slideshow + + + ppt + application/vnd.ms-powerpoint + + + pptm + application/vnd.ms-powerpoint.presentation.macroenabled.12 + + + pptx + application/vnd.openxmlformats-officedocument.presentationml.presentation + + + pqa + application/vnd.palm + + + prc + application/x-mobipocket-ebook + + + pre + application/vnd.lotus-freelance + + + prf + application/pics-rules + + + ps + application/postscript + + + psb + application/vnd.3gpp.pic-bw-small + + + psd + image/vnd.adobe.photoshop + + + psf + application/x-font-linux-psf + + + pskcxml + application/pskc+xml + + + ptid + application/vnd.pvi.ptid1 + + + pub + application/x-mspublisher + + + pvb + application/vnd.3gpp.pic-bw-var + + + pwn + application/vnd.3m.post-it-notes + + + pya + audio/vnd.ms-playready.media.pya + + + pyv + video/vnd.ms-playready.media.pyv + + + qam + application/vnd.epson.quickanime + + + qbo + application/vnd.intu.qbo + + + qfx + application/vnd.intu.qfx + + + qps + application/vnd.publishare-delta-tree + + + qt + video/quicktime + + + qti + image/x-quicktime + + + qtif + image/x-quicktime + + + qwd + application/vnd.quark.quarkxpress + + + qwt + application/vnd.quark.quarkxpress + + + qxb + application/vnd.quark.quarkxpress + + + qxd + application/vnd.quark.quarkxpress + + + qxl + application/vnd.quark.quarkxpress + + + qxt + application/vnd.quark.quarkxpress + + + ra + audio/x-pn-realaudio + + + ram + audio/x-pn-realaudio + + + rar + application/x-rar-compressed + + + ras + image/x-cmu-raster + + + rcprofile + application/vnd.ipunplugged.rcprofile + + + rdf + application/rdf+xml + + + rdz + application/vnd.data-vision.rdz + + + rep + application/vnd.businessobjects + + + res + application/x-dtbresource+xml + + + rgb + image/x-rgb + + + rif + application/reginfo+xml + + + rip + audio/vnd.rip + + + ris + application/x-research-info-systems + + + rl + application/resource-lists+xml + + + rlc + image/vnd.fujixerox.edmics-rlc + + + rld + application/resource-lists-diff+xml + + + rm + application/vnd.rn-realmedia + + + rmi + audio/midi + + + rmp + audio/x-pn-realaudio-plugin + + + rms + application/vnd.jcp.javame.midlet-rms + + + rmvb + application/vnd.rn-realmedia-vbr + + + rnc + application/relax-ng-compact-syntax + + + roa + application/rpki-roa + + + roff + text/troff + + + rp9 + application/vnd.cloanto.rp9 + + + rpss + application/vnd.nokia.radio-presets + + + rpst + application/vnd.nokia.radio-preset + + + rq + application/sparql-query + + + rs + application/rls-services+xml + + + rsd + application/rsd+xml + + + rss + application/rss+xml + + + rtf + application/rtf + + + rtx + text/richtext + + + s + text/x-asm + + + s3m + audio/s3m + + + saf + application/vnd.yamaha.smaf-audio + + + sbml + application/sbml+xml + + + sc + application/vnd.ibm.secure-container + + + scd + application/x-msschedule + + + scm + application/vnd.lotus-screencam + + + scq + application/scvp-cv-request + + + scs + application/scvp-cv-response + + + scurl + text/vnd.curl.scurl + + + sda + application/vnd.stardivision.draw + + + sdc + application/vnd.stardivision.calc + + + sdd + application/vnd.stardivision.impress + + + sdkd + application/vnd.solent.sdkm+xml + + + sdkm + application/vnd.solent.sdkm+xml + + + sdp + application/sdp + + + sdw + application/vnd.stardivision.writer + + + see + application/vnd.seemail + + + seed + application/vnd.fdsn.seed + + + sema + application/vnd.sema + + + semd + application/vnd.semd + + + semf + application/vnd.semf + + + ser + application/java-serialized-object + + + setpay + application/set-payment-initiation + + + setreg + application/set-registration-initiation + + + sfd-hdstx + application/vnd.hydrostatix.sof-data + + + sfs + application/vnd.spotfire.sfs + + + sfv + text/x-sfv + + + sgi + image/sgi + + + sgl + application/vnd.stardivision.writer-global + + + sgm + text/sgml + + + sgml + text/sgml + + + sh + application/x-sh + + + shar + application/x-shar + + + shf + application/shf+xml + + + + sid + image/x-mrsid-image + + + sig + application/pgp-signature + + + sil + audio/silk + + + silo + model/mesh + + + sis + application/vnd.symbian.install + + + sisx + application/vnd.symbian.install + + + sit + application/x-stuffit + + + sitx + application/x-stuffitx + + + skd + application/vnd.koan + + + skm + application/vnd.koan + + + skp + application/vnd.koan + + + skt + application/vnd.koan + + + sldm + application/vnd.ms-powerpoint.slide.macroenabled.12 + + + sldx + application/vnd.openxmlformats-officedocument.presentationml.slide + + + slt + application/vnd.epson.salt + + + sm + application/vnd.stepmania.stepchart + + + smf + application/vnd.stardivision.math + + + smi + application/smil+xml + + + smil + application/smil+xml + + + smv + video/x-smv + + + smzip + application/vnd.stepmania.package + + + snd + audio/basic + + + snf + application/x-font-snf + + + so + application/octet-stream + + + spc + application/x-pkcs7-certificates + + + spf + application/vnd.yamaha.smaf-phrase + + + spl + application/x-futuresplash + + + spot + text/vnd.in3d.spot + + + spp + application/scvp-vp-response + + + spq + application/scvp-vp-request + + + spx + audio/ogg + + + sql + application/x-sql + + + src + application/x-wais-source + + + srt + application/x-subrip + + + sru + application/sru+xml + + + srx + application/sparql-results+xml + + + ssdl + application/ssdl+xml + + + sse + application/vnd.kodak-descriptor + + + ssf + application/vnd.epson.ssf + + + ssml + application/ssml+xml + + + st + application/vnd.sailingtracker.track + + + stc + application/vnd.sun.xml.calc.template + + + std + application/vnd.sun.xml.draw.template + + + stf + application/vnd.wt.stf + + + sti + application/vnd.sun.xml.impress.template + + + stk + application/hyperstudio + + + stl + application/vnd.ms-pki.stl + + + str + application/vnd.pg.format + + + stw + application/vnd.sun.xml.writer.template + + + sub + text/vnd.dvb.subtitle + + + sus + application/vnd.sus-calendar + + + susp + application/vnd.sus-calendar + + + sv4cpio + application/x-sv4cpio + + + sv4crc + application/x-sv4crc + + + svc + application/vnd.dvb.service + + + svd + application/vnd.svd + + + svg + image/svg+xml + + + svgz + image/svg+xml + + + swa + application/x-director + + + swf + application/x-shockwave-flash + + + swi + application/vnd.aristanetworks.swi + + + sxc + application/vnd.sun.xml.calc + + + sxd + application/vnd.sun.xml.draw + + + sxg + application/vnd.sun.xml.writer.global + + + sxi + application/vnd.sun.xml.impress + + + sxm + application/vnd.sun.xml.math + + + sxw + application/vnd.sun.xml.writer + + + t + text/troff + + + t3 + application/x-t3vm-image + + + taglet + application/vnd.mynfc + + + tao + application/vnd.tao.intent-module-archive + + + tar + application/x-tar + + + tcap + application/vnd.3gpp2.tcap + + + tcl + application/x-tcl + + + teacher + application/vnd.smart.teacher + + + tei + application/tei+xml + + + teicorpus + application/tei+xml + + + tex + application/x-tex + + + texi + application/x-texinfo + + + texinfo + application/x-texinfo + + + text + text/plain + + + tfi + application/thraud+xml + + + tfm + application/x-tex-tfm + + + tga + image/x-tga + + + thmx + application/vnd.ms-officetheme + + + tif + image/tiff + + + tiff + image/tiff + + + tmo + application/vnd.tmobile-livetv + + + torrent + application/x-bittorrent + + + tpl + application/vnd.groove-tool-template + + + tpt + application/vnd.trid.tpt + + + tr + text/troff + + + tra + application/vnd.trueapp + + + trm + application/x-msterminal + + + tsd + application/timestamped-data + + + tsv + text/tab-separated-values + + + ttc + font/collection + + + ttf + font/ttf + + + ttl + text/turtle + + + twd + application/vnd.simtech-mindmapper + + + twds + application/vnd.simtech-mindmapper + + + txd + application/vnd.genomatix.tuxedo + + + txf + application/vnd.mobius.txf + + + txt + text/plain + + + u32 + application/x-authorware-bin + + + udeb + application/x-debian-package + + + ufd + application/vnd.ufdl + + + ufdl + application/vnd.ufdl + + + ulw + audio/basic + + + ulx + application/x-glulx + + + umj + application/vnd.umajin + + + unityweb + application/vnd.unity + + + uoml + application/vnd.uoml+xml + + + uri + text/uri-list + + + uris + text/uri-list + + + urls + text/uri-list + + + ustar + application/x-ustar + + + utz + application/vnd.uiq.theme + + + uu + text/x-uuencode + + + uva + audio/vnd.dece.audio + + + uvd + application/vnd.dece.data + + + uvf + application/vnd.dece.data + + + uvg + image/vnd.dece.graphic + + + uvh + video/vnd.dece.hd + + + uvi + image/vnd.dece.graphic + + + uvm + video/vnd.dece.mobile + + + uvp + video/vnd.dece.pd + + + uvs + video/vnd.dece.sd + + + uvt + application/vnd.dece.ttml+xml + + + uvu + video/vnd.uvvu.mp4 + + + uvv + video/vnd.dece.video + + + uvva + audio/vnd.dece.audio + + + uvvd + application/vnd.dece.data + + + uvvf + application/vnd.dece.data + + + uvvg + image/vnd.dece.graphic + + + uvvh + video/vnd.dece.hd + + + uvvi + image/vnd.dece.graphic + + + uvvm + video/vnd.dece.mobile + + + uvvp + video/vnd.dece.pd + + + uvvs + video/vnd.dece.sd + + + uvvt + application/vnd.dece.ttml+xml + + + uvvu + video/vnd.uvvu.mp4 + + + uvvv + video/vnd.dece.video + + + uvvx + application/vnd.dece.unspecified + + + uvvz + application/vnd.dece.zip + + + uvx + application/vnd.dece.unspecified + + + uvz + application/vnd.dece.zip + + + vcard + text/vcard + + + vcd + application/x-cdlink + + + vcf + text/x-vcard + + + vcg + application/vnd.groove-vcard + + + vcs + text/x-vcalendar + + + vcx + application/vnd.vcx + + + vis + application/vnd.visionary + + + viv + video/vnd.vivo + + + vob + video/x-ms-vob + + + vor + application/vnd.stardivision.writer + + + vox + application/x-authorware-bin + + + vrml + model/vrml + + + vsd + application/vnd.visio + + + vsf + application/vnd.vsf + + + vss + application/vnd.visio + + + vst + application/vnd.visio + + + vsw + application/vnd.visio + + + vtu + model/vnd.vtu + + + vxml + application/voicexml+xml + + + w3d + application/x-director + + + wad + application/x-doom + + + wav + audio/x-wav + + + wax + audio/x-ms-wax + + + + wbmp + image/vnd.wap.wbmp + + + wbs + application/vnd.criticaltools.wbs+xml + + + wbxml + application/vnd.wap.wbxml + + + wcm + application/vnd.ms-works + + + wdb + application/vnd.ms-works + + + wdp + image/vnd.ms-photo + + + weba + audio/webm + + + webm + video/webm + + + webp + image/webp + + + wg + application/vnd.pmi.widget + + + wgt + application/widget + + + wks + application/vnd.ms-works + + + wm + video/x-ms-wm + + + wma + audio/x-ms-wma + + + wmd + application/x-ms-wmd + + + wmf + application/x-msmetafile + + + + wml + text/vnd.wap.wml + + + + wmlc + application/vnd.wap.wmlc + + + + wmls + text/vnd.wap.wmlscript + + + + wmlsc + application/vnd.wap.wmlscriptc + + + wmv + video/x-ms-wmv + + + wmx + video/x-ms-wmx + + + wmz + application/x-msmetafile + + + woff + font/woff + + + woff2 + font/woff2 + + + wpd + application/vnd.wordperfect + + + wpl + application/vnd.ms-wpl + + + wps + application/vnd.ms-works + + + wqd + application/vnd.wqd + + + wri + application/x-mswrite + + + wrl + model/vrml + + + wsdl + application/wsdl+xml + + + wspolicy + application/wspolicy+xml + + + wtb + application/vnd.webturbo + + + wvx + video/x-ms-wvx + + + x32 + application/x-authorware-bin + + + x3d + model/x3d+xml + + + x3db + model/x3d+binary + + + x3dbz + model/x3d+binary + + + x3dv + model/x3d+vrml + + + x3dvz + model/x3d+vrml + + + x3dz + model/x3d+xml + + + xaml + application/xaml+xml + + + xap + application/x-silverlight-app + + + xar + application/vnd.xara + + + xbap + application/x-ms-xbap + + + xbd + application/vnd.fujixerox.docuworks.binder + + + xbm + image/x-xbitmap + + + xdf + application/xcap-diff+xml + + + xdm + application/vnd.syncml.dm+xml + + + xdp + application/vnd.adobe.xdp+xml + + + xdssc + application/dssc+xml + + + xdw + application/vnd.fujixerox.docuworks + + + xenc + application/xenc+xml + + + xer + application/patch-ops-error+xml + + + xfdf + application/vnd.adobe.xfdf + + + xfdl + application/vnd.xfdl + + + xht + application/xhtml+xml + + + xhtml + application/xhtml+xml + + + xhvml + application/xv+xml + + + xif + image/vnd.xiff + + + xla + application/vnd.ms-excel + + + xlam + application/vnd.ms-excel.addin.macroenabled.12 + + + xlc + application/vnd.ms-excel + + + xlf + application/x-xliff+xml + + + xlm + application/vnd.ms-excel + + + xls + application/vnd.ms-excel + + + xlsb + application/vnd.ms-excel.sheet.binary.macroenabled.12 + + + xlsm + application/vnd.ms-excel.sheet.macroenabled.12 + + + xlsx + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet + + + xlt + application/vnd.ms-excel + + + xltm + application/vnd.ms-excel.template.macroenabled.12 + + + xltx + application/vnd.openxmlformats-officedocument.spreadsheetml.template + + + xlw + application/vnd.ms-excel + + + xm + audio/xm + + + xml + application/xml + + + xo + application/vnd.olpc-sugar + + + xop + application/xop+xml + + + xpi + application/x-xpinstall + + + xpl + application/xproc+xml + + + xpm + image/x-xpixmap + + + xpr + application/vnd.is-xpr + + + xps + application/vnd.ms-xpsdocument + + + xpw + application/vnd.intercon.formnet + + + xpx + application/vnd.intercon.formnet + + + xsl + application/xml + + + xslt + application/xslt+xml + + + xsm + application/vnd.syncml+xml + + + xspf + application/xspf+xml + + + xul + application/vnd.mozilla.xul+xml + + + xvm + application/xv+xml + + + xvml + application/xv+xml + + + xwd + image/x-xwindowdump + + + xyz + chemical/x-xyz + + + xz + application/x-xz + + + yang + application/yang + + + yin + application/yin+xml + + + z + application/x-compress + + + Z + application/x-compress + + + z1 + application/x-zmachine + + + z2 + application/x-zmachine + + + z3 + application/x-zmachine + + + z4 + application/x-zmachine + + + z5 + application/x-zmachine + + + z6 + application/x-zmachine + + + z7 + application/x-zmachine + + + z8 + application/x-zmachine + + + zaz + application/vnd.zzazz.deck+xml + + + zip + application/zip + + + zir + application/vnd.zul + + + zirz + application/vnd.zul + + + zmm + application/vnd.handheld-entertainment+xml + + + + + + + + + + + + + + + + + + index.html + index.htm + index.jsp + + + diff --git a/codes/javatech/javatech-server/src/main/webapp/META-INF/MANIFEST.MF b/codes/javatech/javatech-server/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 00000000..254272e1 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/codes/javatech/javatech-server/src/main/webapp/WEB-INF/web.xml b/codes/javatech/javatech-server/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..974a088b --- /dev/null +++ b/codes/javatech/javatech-server/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,47 @@ + + + javatool + + + + spring-servlet + org.springframework.web.servlet.DispatcherServlet + 1 + + contextConfigLocation + classpath:spring/spring-servlet.xml + + + + spring-servlet + / + + + + + + + encodingFilter + org.springframework.web.filter.CharacterEncodingFilter + + encoding + UTF-8 + + + forceEncoding + true + + + + encodingFilter + /* + REQUEST + FORWARD + + + + + /views/jsp/index.jsp + + diff --git a/codes/javatech/javatech-server/src/main/webapp/views/jsp/hello.jsp b/codes/javatech/javatech-server/src/main/webapp/views/jsp/hello.jsp new file mode 100644 index 00000000..774fc949 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/webapp/views/jsp/hello.jsp @@ -0,0 +1,16 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> +<% + String path = request.getContextPath(); + String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; +%> + + + + + HelloController + + +

${message}

+回到首页
+ + diff --git a/codes/javatech/javatech-server/src/main/webapp/views/jsp/index.jsp b/codes/javatech/javatech-server/src/main/webapp/views/jsp/index.jsp new file mode 100644 index 00000000..763fee86 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/webapp/views/jsp/index.jsp @@ -0,0 +1,28 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> +<% + String path = request.getContextPath(); + String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; +%> + + + + + javatool + + + +

javatool

+

<%out.print("Server Ip:" + basePath);%>

+
+

示例列表

+ +
+ + diff --git a/codes/javatech/javatech-server/src/test/java/io/github/dunwu/javatech/HelloControllerTests.java b/codes/javatech/javatech-server/src/test/java/io/github/dunwu/javatech/HelloControllerTests.java new file mode 100644 index 00000000..946e65de --- /dev/null +++ b/codes/javatech/javatech-server/src/test/java/io/github/dunwu/javatech/HelloControllerTests.java @@ -0,0 +1,28 @@ +package io.github.dunwu.javatech; + +import io.github.dunwu.javatech.service.HelloService; +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * 单元测试 + * @author Zhang Peng + * @date 2022-07-28 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = {"classpath:spring/spring-servlet.xml"}) +public class HelloControllerTests { + + @Autowired + private HelloService service; + + @Test + public void test() { + String msg = service.hello(); + Assertions.assertThat("hello").isEqualTo(msg); + } +} diff --git a/codes/javatech/pom.xml b/codes/javatech/pom.xml new file mode 100644 index 00000000..8bfcf17b --- /dev/null +++ b/codes/javatech/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + io.github.dunwu.javatech + javatech + 1.0.0 + pom + JAVATECH + Java 技术示例源码 + + + javatech-cache + javatech-lib + javatech-log + javatech-mq + javatech-server + javatech-others + + diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-spring/pom.xml b/codes/javatool/javatool-monitor/javatool-zipkin-spring/pom.xml index 4225461a..8ada12b7 100644 --- a/codes/javatool/javatool-monitor/javatool-zipkin-spring/pom.xml +++ b/codes/javatool/javatool-monitor/javatool-zipkin-spring/pom.xml @@ -1,205 +1,204 @@ - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - io.github.dunwu.javatool - javatool-zipkin-spring - 1.0.0 - war - ${project.artifactId} - Java distributed tracing implementation compatible with Zipkin backend services. + io.github.dunwu.javatool + javatool-zipkin-spring + 1.0.0 + war + JAVATOOL-监控-Zipkin(Spring) - - UTF-8 - 1.7 - 1.7 + + UTF-8 + 1.7 + 1.7 - 4.3.26.RELEASE - 2.13.0 - 5.10.1 - + 4.3.26.RELEASE + 2.13.0 + 5.10.1 + - - - - io.zipkin.brave - brave-bom - ${brave.version} - pom - import - - - - - - - javax.servlet - javax.servlet-api - 3.1.0 - provided - - - org.springframework - spring-webmvc - ${spring.version} - - - org.springframework - spring-web - ${spring.version} - - - org.apache.httpcomponents - httpclient - 4.5.11 - + + + + io.zipkin.brave + brave-bom + ${brave.version} + pom + import + + + - - - io.zipkin.brave - brave-instrumentation-spring-webmvc - - - - io.zipkin.brave - brave-instrumentation-httpclient - + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + + org.springframework + spring-webmvc + ${spring.version} + + + org.springframework + spring-web + ${spring.version} + + + org.apache.httpcomponents + httpclient + 4.5.11 + - - - org.apache.logging.log4j - log4j-core - ${log4j.version} - - - org.apache.logging.log4j - log4j-jul - ${log4j.version} - - - org.apache.logging.log4j - log4j-jcl - ${log4j.version} - - - org.apache.logging.log4j - log4j-slf4j-impl - ${log4j.version} - - - io.zipkin.brave - brave-context-log4j2 - + + + io.zipkin.brave + brave-instrumentation-spring-webmvc + + + + io.zipkin.brave + brave-instrumentation-httpclient + - - - io.zipkin.brave - brave-spring-beans - - - io.zipkin.brave - brave - - - io.zipkin.reporter2 - zipkin-sender-okhttp3 - - + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.apache.logging.log4j + log4j-jul + ${log4j.version} + + + org.apache.logging.log4j + log4j-jcl + ${log4j.version} + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + io.zipkin.brave + brave-context-log4j2 + - - - - maven-compiler-plugin - 3.8.1 - + + + io.zipkin.brave + brave-spring-beans + + + io.zipkin.brave + brave + + + io.zipkin.reporter2 + zipkin-sender-okhttp3 + + - - maven-enforcer-plugin - 3.0.0-M3 - - - enforce-java - - enforce - - - - - - [1.7.0_80,) - - - - - - + + + + maven-compiler-plugin + 3.8.1 + - - maven-war-plugin - 3.2.3 - - false - WEB-INF/lib/servlet-api-*.jar - - - - - - - org.eclipse.jetty - jetty-maven-plugin - 9.4.26.v20200117 - - - - + + maven-enforcer-plugin + 3.0.0-M3 + + + enforce-java + + enforce + + + + + + [1.7.0_80,) + + + + + + - - - frontend - - - - org.eclipse.jetty - jetty-maven-plugin - - - 8081 - - - - zipkin.service - frontend - - - - + + maven-war-plugin + 3.2.3 + + false + WEB-INF/lib/servlet-api-*.jar + + - - - - backend - - - - org.eclipse.jetty - jetty-maven-plugin - - - 9000 - - - - zipkin.service - backend - - - - - - - - + + + + org.eclipse.jetty + jetty-maven-plugin + 9.4.26.v20200117 + + + + + + + + frontend + + + + org.eclipse.jetty + jetty-maven-plugin + + + 8081 + + + + zipkin.service + frontend + + + + + + + + + backend + + + + org.eclipse.jetty + jetty-maven-plugin + + + 9000 + + + + zipkin.service + backend + + + + + + + + diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-springboot/pom.xml b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/pom.xml index b314e9e5..7d99565f 100644 --- a/codes/javatool/javatool-monitor/javatool-zipkin-springboot/pom.xml +++ b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/pom.xml @@ -1,108 +1,106 @@ - 4.0.0 + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - io.github.dunwu.javatool - javatool-zipkin-springboot - 1.0.0 - jar - ${project.artifactId} - Example using Brave to trace RPCs from Spring Web MVC + io.github.dunwu.javatool + javatool-zipkin-springboot + 1.0.0 + jar + JAVATOOL-监控-Zipkin(SpringBoot) - - UTF-8 - 1.7 - 1.7 + + UTF-8 + 1.7 + 1.7 + 2.7.2 + 5.10.1 + - 1.5.22.RELEASE - 5.10.1 - + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + io.zipkin.brave + brave-bom + ${brave.version} + pom + import + + + - - - org.springframework.boot - spring-boot-dependencies - ${spring-boot.version} - pom - import - - - io.zipkin.brave - brave-bom - ${brave.version} - pom - import - - - - - - - org.springframework.boot - spring-boot-starter-web - - - org.apache.httpcomponents - httpclient - + + org.springframework.boot + spring-boot-starter-web + + + org.apache.httpcomponents + httpclient + - - - io.zipkin.brave - brave-instrumentation-spring-webmvc - - - - io.zipkin.brave - brave-instrumentation-httpclient - + + + io.zipkin.brave + brave-instrumentation-spring-webmvc + + + + io.zipkin.brave + brave-instrumentation-httpclient + - - - io.zipkin.brave - brave-context-slf4j - + + + io.zipkin.brave + brave-context-slf4j + - - - io.zipkin.brave - brave - - - io.zipkin.reporter2 - zipkin-sender-okhttp3 - - + + + io.zipkin.brave + brave + + + io.zipkin.reporter2 + zipkin-sender-okhttp3 + + - - - - maven-compiler-plugin - 3.8.1 - + + + + maven-compiler-plugin + 3.8.1 + - - maven-enforcer-plugin - 3.0.0-M3 - - - enforce-java - - enforce - - - - - - [1.7.0_80,) - - - - - - - - + + maven-enforcer-plugin + 3.0.0-M3 + + + enforce-java + + enforce + + + + + + [1.7.0_80,) + + + + + + + + diff --git a/codes/javatool/javatool-monitor/pom.xml b/codes/javatool/javatool-monitor/pom.xml index b3497277..027eca63 100644 --- a/codes/javatool/javatool-monitor/pom.xml +++ b/codes/javatool/javatool-monitor/pom.xml @@ -1,17 +1,17 @@ - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - io.github.dunwu.javatool - javatool-monitor - 1.0.0 - pom - ${project.artifactId} + io.github.dunwu.javatool + javatool-monitor + 1.0.0 + pom + JAVATOOL-监控 - - javatool-zipkin-spring - javatool-zipkin-springboot - + + javatool-zipkin-spring + javatool-zipkin-springboot + diff --git a/codes/javatool/pom.xml b/codes/javatool/pom.xml index 96d1160e..86b22be9 100644 --- a/codes/javatool/pom.xml +++ b/codes/javatool/pom.xml @@ -1,16 +1,16 @@ - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - io.github.dunwu.javatool - javatool - 1.0.0 - pom - JAVATOOL + io.github.dunwu.javatool + javatool + 1.0.0 + pom + JAVATOOL - - javatool-monitor - + + javatool-monitor + diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 5d3766dc..6bba8f05 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -1,63 +1,209 @@ -/** - * @see https://vuepress.vuejs.org/zh/ - */ +const baiduCode = require('./config/baiduCode.js') // 百度统计hm码 +const htmlModules = require('./config/htmlModules.js') + module.exports = { - port: "4000", - dest: "dist", - base: "/java-tutorial/", - title: "JAVA-TUTORIAL", - description: "Java 教程", - head: [["link", {rel: "icon", href: `/favicon.ico`}]], + port: '4000', + dest: 'docs/.temp', + base: '/java-tutorial/', // 默认'/'。如果你想将你的网站部署到如 https://foo.github.io/bar/,那么 base 应该被设置成 "/bar/",(否则页面将失去样式等文件) + title: 'JAVA-TUTORIAL', + description: '☕ java-tutorial 是一个 Java 教程,汇集一个老司机在 Java 领域的十年积累。', + theme: 'vdoing', // 使用依赖包主题 + // theme: require.resolve('../../vdoing'), // 使用本地主题 + head: [ + // 注入到页面 中的标签,格式[tagName, { attrName: attrValue }, innerHTML?] + ['link', { rel: 'icon', href: '/img/favicon.ico' }], //favicons,资源放在public文件夹 + ['meta', { name: 'keywords', content: 'vuepress,theme,blog,vdoing' }], + ['meta', { name: 'theme-color', content: '#11a8cd' }], // 移动浏览器主题颜色 + + ['meta', { name: 'wwads-cn-verify', content: 'mxqWx62nfQQ9ocT4e5DzISHzOWyF4s' }], // 广告相关,你可以去掉 + ['script', { src: 'https://cdn.wwads.cn/js/makemoney.js', type: 'text/javascript' }] // 广告相关,你可以去掉 + ], markdown: { + // lineNumbers: true, + extractHeaders: ['h2', 'h3', 'h4', 'h5', 'h6'], // 提取标题到侧边栏的级别,默认['h2', 'h3'] externalLinks: { - target: "_blank", rel: "noopener noreferrer" + target: '_blank', + rel: 'noopener noreferrer' } }, + // 主题配置 themeConfig: { - logo: "images/dunwu-logo-100.png", - repo: "dunwu/java-tutorial", - repoLabel: "Github", - docsDir: "docs", - docsBranch: "master", - editLinks: true, - smoothScroll: true, - locales: { - "/": { - label: "简体中文", selectText: "Languages", editLinkText: "帮助我们改善此页面!", lastUpdated: "上次更新", nav: [{ - text: "工具", link: "/javatool/", items: [{ - text: "构建", link: "/javatool/build/" - }, { - text: "IDE", link: "/javatool/ide/" - }, { - text: "监控", link: "/javatool/monitor/" - }] - }, { - text: "JavaEE", link: "/javaee/" - }, { - text: "✨ Java系列", ariaLabel: "Java", items: [{ - text: "Java 教程 📚", link: "https://dunwu.github.io/java-tutorial/", target: "_blank", rel: "" - }, { - text: "JavaCore 教程 📚", link: "https://dunwu.github.io/javacore/", target: "_blank", rel: "" - }, { - text: "JavaTech 教程 📚", link: "https://dunwu.github.io/javatech/", target: "_blank", rel: "" - }, { - text: "Spring 教程 📚", link: "https://dunwu.github.io/spring-tutorial/", target: "_blank", rel: "" - }, { - text: "Spring Boot 教程 📚", link: "https://dunwu.github.io/spring-boot-tutorial/", target: "_blank", rel: "" - }] - }, { - text: "🎯 博客", link: "https://github.com/dunwu/blog", target: "_blank", rel: "" - }], sidebar: "auto", sidebarDepth: 2 + nav: [ + { text: '首页', link: '/' }, + { text: 'JavaEE', link: '/01.Java/02.JavaEE/' }, + { + text: 'Java软件', + link: '/01.Java/11.软件/', + items: [ + { text: 'Java 构建', link: '/01.Java/11.软件/01.构建/' }, + { text: 'Java IDE', link: '/01.Java/11.软件/02.IDE/' }, + { text: 'Java 监控诊断', link: '/01.Java/11.软件/03.监控诊断/' } + ] + }, + { + text: 'Java工具', + link: '/01.Java/12.工具/', + items: [ + { text: 'Java IO 工具', link: '/01.Java/12.工具/01.IO/' }, + { text: 'JavaBean 工具', link: '/01.Java/12.工具/02.JavaBean/' }, + { text: 'Java 模板引擎', link: '/01.Java/12.工具/03.模板引擎/' }, + { text: 'Java 测试工具', link: '/01.Java/12.工具/04.测试/' } + ] + }, + { text: 'Java框架', link: '/01.Java/13.框架/' }, + { text: 'Java中间件', link: '/01.Java/14.中间件/' }, + { + text: '✨ Java系列', + ariaLabel: 'Java', + items: [ + { text: 'Java 教程 📚', link: 'https://dunwu.github.io/java-tutorial/', target: '_blank' }, + { text: 'JavaCore 教程 📚', link: 'https://dunwu.github.io/javacore/', target: '_blank' } + ] } - } + ], + sidebarDepth: 2, // 侧边栏显示深度,默认1,最大2(显示到h3标题) + logo: 'https://raw.githubusercontent.com/dunwu/images/master/common/dunwu-logo.png', // 导航栏logo + repo: 'dunwu/java-tutorial', // 导航栏右侧生成Github链接 + searchMaxSuggestions: 10, // 搜索结果显示最大数 + lastUpdated: '上次更新', // 更新的时间,及前缀文字 string | boolean (取值为git提交时间) + + docsDir: 'docs', // 编辑的文件夹 + editLinks: true, // 编辑链接 + editLinkText: '📝 帮助改善此页面!', + + // 以下配置是Vdoing主题改动的和新增的配置 + sidebar: { mode: 'structuring', collapsable: true }, // 侧边栏 'structuring' | { mode: 'structuring', collapsable: + // Boolean} | 'auto' | 自定义 温馨提示:目录页数据依赖于结构化的侧边栏数据,如果你不设置为'structuring',将无法使用目录页 + + sidebarOpen: true, // 初始状态是否打开侧边栏,默认true + updateBar: { + // 最近更新栏 + showToArticle: true // 显示到文章页底部,默认true + // moreArticle: '/archives' // “更多文章”跳转的页面,默认'/archives' + }, + // titleBadge: false, // 文章标题前的图标是否显示,默认true + // titleBadgeIcons: [ // 文章标题前图标的地址,默认主题内置图标 + // '图标地址1', + // '图标地址2' + // ], + // bodyBgImg: [ + // 'https://cdn.jsdelivr.net/gh/xugaoyi/image_store/blog/20200507175828.jpeg', + // 'https://cdn.jsdelivr.net/gh/xugaoyi/image_store/blog/20200507175845.jpeg', + // 'https://cdn.jsdelivr.net/gh/xugaoyi/image_store/blog/20200507175846.jpeg' + // ], // body背景大图,默认无。 单张图片 String || 多张图片 Array, 多张图片时每隔15秒换一张。 + + // categoryText: '随笔', // 碎片化文章(_posts文件夹的文章)预设生成的分类值,默认'随笔' + + // contentBgStyle: 1, + + category: true, // 是否打开分类功能,默认true。 如打开,会做的事情有:1. 自动生成的frontmatter包含分类字段 2.页面中显示与分类相关的信息和模块 3.自动生成分类页面(在@pages文件夹)。如关闭,则反之。 + tag: true, // 是否打开标签功能,默认true。 如打开,会做的事情有:1. 自动生成的frontmatter包含标签字段 2.页面中显示与标签相关的信息和模块 3.自动生成标签页面(在@pages文件夹)。如关闭,则反之。 + archive: true, // 是否打开归档功能,默认true。 如打开,会做的事情有:1.自动生成归档页面(在@pages文件夹)。如关闭,则反之。 + + author: { + // 文章默认的作者信息,可在md文件中单独配置此信息 String | {name: String, href: String} + name: 'dunwu', // 必需 + href: 'https://github.com/dunwu' // 可选的 + }, + social: { + // 社交图标,显示于博主信息栏和页脚栏 + // iconfontCssFile: '//at.alicdn.com/t/font_1678482_u4nrnp8xp6g.css', // 可选,阿里图标库在线css文件地址,对于主题没有的图标可自由添加 + icons: [ + { + iconClass: 'icon-youjian', + title: '发邮件', + link: 'mailto:forbreak@163.com' + }, + { + iconClass: 'icon-github', + title: 'GitHub', + link: 'https://github.com/dunwu' + } + ] + }, + footer: { + // 页脚信息 + createYear: 2019, // 博客创建年份 + copyrightInfo: '钝悟(dunwu) | CC-BY-SA-4.0' // 博客版权信息,支持a标签 + }, + htmlModules }, - plugins: [["@vuepress/active-header-links", { - sidebarLinkSelector: ".sidebar-link", headerAnchorSelector: ".header-anchor" - }], ["@vuepress/back-to-top", true], ["@vuepress/pwa", { - serviceWorker: true, updatePopup: true - }], ["@vuepress/medium-zoom", true], ["container", { - type: "vue", before: '
', after: "
" - }], ["container", { - type: "upgrade", before: info => ``, after: "" - }], ["flowchart"]] -}; + + // 插件 + plugins: [ + [ + require('./plugins/love-me'), + { + // 鼠标点击爱心特效 + color: '#11a8cd', // 爱心颜色,默认随机色 + excludeClassName: 'theme-vdoing-content' // 要排除元素的class, 默认空'' + } + ], + + ['fulltext-search'], // 全文搜索 + + // ['thirdparty-search', { // 可以添加第三方搜索链接的搜索框(原官方搜索框的参数仍可用) + // thirdparty: [ // 可选,默认 [] + // { + // title: '在GitHub中搜索', + // frontUrl: 'https://github.com/search?q=', // 搜索链接的前面部分 + // behindUrl: '' // 搜索链接的后面部分,可选,默认 '' + // }, + // { + // title: '在npm中搜索', + // frontUrl: 'https://www.npmjs.com/search?q=', + // }, + // { + // title: '在Bing中搜索', + // frontUrl: 'https://cn.bing.com/search?q=' + // } + // ] + // }], + + [ + 'one-click-copy', + { + // 代码块复制按钮 + copySelector: ['div[class*="language-"] pre', 'div[class*="aside-code"] aside'], // String or Array + copyMessage: '复制成功', // default is 'Copy successfully and then paste it for use.' + duration: 1000, // prompt message display time. + showInMobile: false // whether to display on the mobile side, default: false. + } + ], + [ + 'demo-block', + { + // demo演示模块 https://github.com/xiguaxigua/vuepress-plugin-demo-block + settings: { + // jsLib: ['http://xxx'], // 在线示例(jsfiddle, codepen)中的js依赖 + // cssLib: ['http://xxx'], // 在线示例中的css依赖 + // vue: 'https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js', // 在线示例中的vue依赖 + jsfiddle: false, // 是否显示 jsfiddle 链接 + codepen: true, // 是否显示 codepen 链接 + horizontal: false // 是否展示为横向样式 + } + } + ], + [ + 'vuepress-plugin-zooming', // 放大图片 + { + selector: '.theme-vdoing-content img:not(.no-zoom)', + options: { + bgColor: 'rgba(0,0,0,0.6)' + } + } + ], + [ + '@vuepress/last-updated', // "上次更新"时间格式 + { + transformer: (timestamp, lang) => { + const dayjs = require('dayjs') // https://day.js.org/ + return dayjs(timestamp).format('YYYY/MM/DD, HH:mm:ss') + } + } + ] + ], + + // 监听文件变化并重新构建 + extraWatchFiles: ['.vuepress/config.js', '.vuepress/config/htmlModules.js'] +} diff --git a/docs/.vuepress/config/baiduCode.js b/docs/.vuepress/config/baiduCode.js new file mode 100644 index 00000000..b0c50903 --- /dev/null +++ b/docs/.vuepress/config/baiduCode.js @@ -0,0 +1 @@ +module.exports = '' diff --git a/docs/.vuepress/config/htmlModules.js b/docs/.vuepress/config/htmlModules.js new file mode 100644 index 00000000..fc0a47eb --- /dev/null +++ b/docs/.vuepress/config/htmlModules.js @@ -0,0 +1,69 @@ +/** 插入自定义html模块 (可用于插入广告模块等) + * { + * homeSidebarB: htmlString, 首页侧边栏底部 + * + * sidebarT: htmlString, 全局左侧边栏顶部 + * sidebarB: htmlString, 全局左侧边栏底部 + * + * pageT: htmlString, 全局页面顶部 + * pageB: htmlString, 全局页面底部 + * pageTshowMode: string, 页面顶部-显示方式:未配置默认全局;'article' => 仅文章页①; 'custom' => 仅自定义页① + * pageBshowMode: string, 页面底部-显示方式:未配置默认全局;'article' => 仅文章页①; 'custom' => 仅自定义页① + * + * windowLB: htmlString, 全局左下角② + * windowRB: htmlString, 全局右下角② + * } + * + * ①注:在.md文件front matter配置`article: false`的页面是自定义页,未配置的默认是文章页(首页除外)。 + * ②注:windowLB 和 windowRB:1.展示区块最大宽高200px*400px。2.请给自定义元素定一个不超过200px*400px的宽高。3.在屏幕宽度小于960px时无论如何都不会显示。 + */ + +module.exports = { + // 万维广告 + // pageT: ` + //
+ // + // `, + windowRB: ` +
+ + `, +} + +// module.exports = { +// homeSidebarB: `
自定义模块测试
`, +// sidebarT: `
自定义模块测试
`, +// sidebarB: `
自定义模块测试
`, +// pageT: `
自定义模块测试
`, +// pageB: `
自定义模块测试
`, +// windowLB: `
自定义模块测试
`, +// windowRB: `
自定义模块测试
`, +// } diff --git a/docs/.vuepress/enhanceApp.js b/docs/.vuepress/enhanceApp.js index 7b3605fc..5bfa34f4 100644 --- a/docs/.vuepress/enhanceApp.js +++ b/docs/.vuepress/enhanceApp.js @@ -1,7 +1,59 @@ -export default ({ Vue, isServer }) => { +/** + * to主题使用者:你可以去掉本文件的所有代码 + */ +export default ({ + Vue, // VuePress 正在使用的 Vue 构造函数 + options, // 附加到根实例的一些选项 + router, // 当前应用的路由实例 + siteData, // 站点元数据 + isServer // 当前应用配置是处于 服务端渲染 还是 客户端 +}) => { + + // 用于监控在路由变化时检查广告拦截器 (to主题使用者:你可以去掉本文件的所有代码) if (!isServer) { - import('vue-toasted' /* webpackChunkName: "notification" */).then(module => { - Vue.use(module.default) + router.afterEach(() => { + //check if wwads' fire function was blocked after document is ready with 3s timeout (waiting the ad loading) + docReady(function () { + setTimeout(function () { + if (window._AdBlockInit === undefined) { + ABDetected(); + } + }, 3000); + }); + + // 删除事件改为隐藏事件 + setTimeout(() => { + const pageAD = document.querySelector('.page-wwads'); + if (!pageAD) return; + const btnEl = pageAD.querySelector('.wwads-hide'); + if (btnEl) { + btnEl.onclick = () => { + pageAD.style.display = 'none'; + } + } + // 显示广告模块 + if (pageAD.style.display === 'none') { + pageAD.style.display = 'flex'; + } + }, 900); }) } } + + +function ABDetected() { + const h = ""; + const wwadsEl = document.getElementsByClassName("wwads-cn"); + const wwadsContentEl = document.querySelector('.wwads-content'); + if (wwadsEl[0] && !wwadsContentEl) { + wwadsEl[0].innerHTML = h; + } +}; + +//check document ready +function docReady(t) { + "complete" === document.readyState || + "interactive" === document.readyState + ? setTimeout(t, 1) + : document.addEventListener("DOMContentLoaded", t); +} diff --git a/docs/.vuepress/plugins/love-me/index.js b/docs/.vuepress/plugins/love-me/index.js new file mode 100644 index 00000000..2851beb0 --- /dev/null +++ b/docs/.vuepress/plugins/love-me/index.js @@ -0,0 +1,12 @@ +const path = require('path') +const LoveMyPlugin = (options = {}) => ({ + define() { + const COLOR = + options.color || + 'rgb(' + ~~(255 * Math.random()) + ',' + ~~(255 * Math.random()) + ',' + ~~(255 * Math.random()) + ')' + const EXCLUDECLASS = options.excludeClassName || '' + return { COLOR, EXCLUDECLASS } + }, + enhanceAppFiles: [path.resolve(__dirname, 'love-me.js')] +}) +module.exports = LoveMyPlugin diff --git a/docs/.vuepress/plugins/love-me/love-me.js b/docs/.vuepress/plugins/love-me/love-me.js new file mode 100644 index 00000000..5c0369ac --- /dev/null +++ b/docs/.vuepress/plugins/love-me/love-me.js @@ -0,0 +1,89 @@ +export default () => { + if (typeof window !== 'undefined') { + ;(function (e, t, a) { + function r() { + for (var e = 0; e < s.length; e++) + s[e].alpha <= 0 + ? (t.body.removeChild(s[e].el), s.splice(e, 1)) + : (s[e].y--, + (s[e].scale += 0.004), + (s[e].alpha -= 0.013), + (s[e].el.style.cssText = + 'left:' + + s[e].x + + 'px;top:' + + s[e].y + + 'px;opacity:' + + s[e].alpha + + ';transform:scale(' + + s[e].scale + + ',' + + s[e].scale + + ') rotate(45deg);background:' + + s[e].color + + ';z-index:99999')) + requestAnimationFrame(r) + } + function n() { + var t = 'function' == typeof e.onclick && e.onclick + + e.onclick = function (e) { + // 过滤指定元素 + let mark = true + EXCLUDECLASS && + e.path && + e.path.forEach((item) => { + if (item.nodeType === 1) { + typeof item.className === 'string' && item.className.indexOf(EXCLUDECLASS) > -1 ? (mark = false) : '' + } + }) + + if (mark) { + t && t(), o(e) + } + } + } + function o(e) { + var a = t.createElement('div') + ;(a.className = 'heart'), + s.push({ + el: a, + x: e.clientX - 5, + y: e.clientY - 5, + scale: 1, + alpha: 1, + color: COLOR + }), + t.body.appendChild(a) + } + function i(e) { + var a = t.createElement('style') + a.type = 'text/css' + try { + a.appendChild(t.createTextNode(e)) + } catch (t) { + a.styleSheet.cssText = e + } + t.getElementsByTagName('head')[0].appendChild(a) + } + // function c() { + // return "rgb(" + ~~ (255 * Math.random()) + "," + ~~ (255 * Math.random()) + "," + ~~ (255 * Math.random()) + ")" + // } + var s = [] + ;(e.requestAnimationFrame = + e.requestAnimationFrame || + e.webkitRequestAnimationFrame || + e.mozRequestAnimationFrame || + e.oRequestAnimationFrame || + e.msRequestAnimationFrame || + function (e) { + setTimeout(e, 1e3 / 60) + }), + i( + ".heart{width: 10px;height: 10px;position: fixed;background: #f00;transform: rotate(45deg);-webkit-transform: rotate(45deg);-moz-transform: rotate(45deg);}.heart:after,.heart:before{content: '';width: inherit;height: inherit;background: inherit;border-radius: 50%;-webkit-border-radius: 50%;-moz-border-radius: 50%;position: fixed;}.heart:after{top: -5px;}.heart:before{left: -5px;}" + ), + n(), + r() + })(window, document) + } +} diff --git a/docs/.vuepress/public/images/dunwu-logo-100.png b/docs/.vuepress/public/images/dunwu-logo-100.png deleted file mode 100644 index 12d81778..00000000 Binary files a/docs/.vuepress/public/images/dunwu-logo-100.png and /dev/null differ diff --git a/docs/.vuepress/public/images/dunwu-logo-200.png b/docs/.vuepress/public/images/dunwu-logo-200.png deleted file mode 100644 index ea0a019c..00000000 Binary files a/docs/.vuepress/public/images/dunwu-logo-200.png and /dev/null differ diff --git a/docs/.vuepress/public/images/dunwu-logo-50.png b/docs/.vuepress/public/images/dunwu-logo-50.png deleted file mode 100644 index 90a19762..00000000 Binary files a/docs/.vuepress/public/images/dunwu-logo-50.png and /dev/null differ diff --git a/docs/.vuepress/public/img/bg.gif b/docs/.vuepress/public/img/bg.gif new file mode 100644 index 00000000..d4bf3c41 Binary files /dev/null and b/docs/.vuepress/public/img/bg.gif differ diff --git a/docs/.vuepress/public/images/dunwu-logo.png b/docs/.vuepress/public/img/dunwu-logo.png similarity index 100% rename from docs/.vuepress/public/images/dunwu-logo.png rename to docs/.vuepress/public/img/dunwu-logo.png diff --git a/docs/.vuepress/public/img/favicon.ico b/docs/.vuepress/public/img/favicon.ico new file mode 100644 index 00000000..51e9bfa0 Binary files /dev/null and b/docs/.vuepress/public/img/favicon.ico differ diff --git a/docs/.vuepress/public/img/more.png b/docs/.vuepress/public/img/more.png new file mode 100644 index 00000000..830613ba Binary files /dev/null and b/docs/.vuepress/public/img/more.png differ diff --git a/docs/.vuepress/public/img/other.png b/docs/.vuepress/public/img/other.png new file mode 100644 index 00000000..87f80989 Binary files /dev/null and b/docs/.vuepress/public/img/other.png differ diff --git a/docs/.vuepress/public/markmap/01.html b/docs/.vuepress/public/markmap/01.html new file mode 100644 index 00000000..c4e0bdbc --- /dev/null +++ b/docs/.vuepress/public/markmap/01.html @@ -0,0 +1,113 @@ + + + + + + + Markmap + + + + + + + + + diff --git a/docs/.vuepress/styles/index.styl b/docs/.vuepress/styles/index.styl new file mode 100644 index 00000000..3113dd61 --- /dev/null +++ b/docs/.vuepress/styles/index.styl @@ -0,0 +1,93 @@ +.home-wrapper .banner .banner-conent .hero h1{ + font-size 2.8rem!important +} +// 文档中适配 +table + width auto +.page >*:not(.footer),.card-box + box-shadow: none!important + +.page + @media (min-width $contentWidth + 80) + padding-top $navbarHeight!important +.home-wrapper .banner .banner-conent + padding 0 2.9rem + box-sizing border-box +.home-wrapper .banner .slide-banner .slide-banner-wrapper .slide-item a + h2 + margin-top 2rem + font-size 1.2rem!important + p + padding 0 1rem + +// 评论区颜色重置 +.gt-container + .gt-ico-tip + &::after + content: '。( Win + . ) or ( ⌃ + ⌘ + ␣ ) open Emoji' + color: #999 + .gt-meta + border-color var(--borderColor)!important + .gt-comments-null + color var(--textColor) + opacity .5 + .gt-header-textarea + color var(--textColor) + background rgba(180,180,180,0.1)!important + .gt-btn + border-color $accentColor!important + background-color $accentColor!important + .gt-btn-preview + background-color rgba(255,255,255,0)!important + color $accentColor!important + a + color $accentColor!important + .gt-svg svg + fill $accentColor!important + .gt-comment-content,.gt-comment-admin .gt-comment-content + background-color rgba(150,150,150,0.1)!important + &:hover + box-shadow 0 0 25px rgba(150,150,150,.5)!important + .gt-comment-body + color var(--textColor)!important + + +// qq徽章 +.qq + position: relative; +.qq::after + content: "可撩"; + background: $accentColor; + color:#fff; + padding: 0 5px; + border-radius: 10px; + font-size:12px; + position: absolute; + top: -4px; + right: -35px; + transform:scale(0.85); + +// demo模块图标颜色 +body .vuepress-plugin-demo-block__wrapper + &,.vuepress-plugin-demo-block__display + border-color rgba(160,160,160,.3) + .vuepress-plugin-demo-block__footer:hover + .vuepress-plugin-demo-block__expand::before + border-top-color: $accentColor !important; + border-bottom-color: $accentColor !important; + svg + fill: $accentColor !important; + + +// 全文搜索框 +.suggestions + overflow: auto + max-height: calc(100vh - 6rem) + @media (max-width: 719px) { + width: 90vw; + min-width: 90vw!important; + margin-right: -20px; + } + .highlight + color: $accentColor + font-weight: bold diff --git a/docs/.vuepress/styles/palette.styl b/docs/.vuepress/styles/palette.styl new file mode 100644 index 00000000..d98e697a --- /dev/null +++ b/docs/.vuepress/styles/palette.styl @@ -0,0 +1,62 @@ + +// 原主题变量已弃用,以下是vdoing使用的变量,你可以在这个文件内修改它们。 + +//***vdoing主题-变量***// + +// // 颜色 + +// $bannerTextColor = #fff // 首页banner区(博客标题)文本颜色 +// $accentColor = #11A8CD +// $arrowBgColor = #ccc +// $badgeTipColor = #42b983 +// $badgeWarningColor = darken(#ffe564, 35%) +// $badgeErrorColor = #DA5961 + +// // 布局 +// $navbarHeight = 3.6rem +// $sidebarWidth = 18rem +// $contentWidth = 860px +// $homePageWidth = 1100px +// $rightMenuWidth = 230px // 右侧菜单 + +// // 代码块 +// $lineNumbersWrapperWidth = 2.5rem + +// 浅色模式 +.theme-mode-light + --bodyBg: rgba(255,255,255,1) + --mainBg: rgba(255,255,255,1) + --sidebarBg: rgba(255,255,255,.8) + --blurBg: rgba(255,255,255,.9) + --textColor: #004050 + --textLightenColor: #0085AD + --borderColor: rgba(0,0,0,.15) + --codeBg: #f6f6f6 + --codeColor: #525252 + codeThemeLight() + +// 深色模式 +.theme-mode-dark + --bodyBg: rgba(30,30,34,1) + --mainBg: rgba(30,30,34,1) + --sidebarBg: rgba(30,30,34,.8) + --blurBg: rgba(30,30,34,.8) + --textColor: rgb(140,140,150) + --textLightenColor: #0085AD + --borderColor: #2C2C3A + --codeBg: #252526 + --codeColor: #fff + codeThemeDark() + +// 阅读模式 +.theme-mode-read + --bodyBg: rgba(245,245,213,1) + --mainBg: rgba(245,245,213,1) + --sidebarBg: rgba(245,245,213,.8) + --blurBg: rgba(245,245,213,.9) + --textColor: #004050 + --textLightenColor: #0085AD + --borderColor: rgba(0,0,0,.15) + --codeBg: #282c34 + --codeColor: #fff + codeThemeDark() diff --git a/docs/javaee/javaee-servlet.md "b/docs/01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" similarity index 90% rename from docs/javaee/javaee-servlet.md rename to "docs/01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" index 9de27a64..2133814e 100644 --- a/docs/javaee/javaee-servlet.md +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" @@ -1,29 +1,23 @@ -# Servlet 指南 - - - -- [1. JavaWeb 简介](#1-javaweb-简介) - - [1.1. Web 应用程序](#11-web-应用程序) - - [1.2. 常见 Web 服务器](#12-常见-web-服务器) -- [2. Servlet 简介](#2-servlet-简介) - - [2.1. 什么是 Servlet](#21-什么是-servlet) - - [2.2. Servlet 和 CGI 的区别](#22-servlet-和-cgi-的区别) - - [2.3. Servlet 版本以及主要特性](#23-servlet-版本以及主要特性) - - [2.4. Servlet 任务](#24-servlet-任务) - - [2.5. Servlet 生命周期](#25-servlet-生命周期) -- [3. Servlet API](#3-servlet-api) - - [3.1. Servlet 包](#31-servlet-包) - - [3.2. Servlet 接口](#32-servlet-接口) -- [4. Servlet 和 HTTP 状态码](#4-servlet-和-http-状态码) - - [4.1. HTTP 状态码](#41-http-状态码) - - [4.2. 设置 HTTP 状态码的方法](#42-设置-http-状态码的方法) - - [4.3. HTTP 状态码实例](#43-http-状态码实例) +--- +title: JavaWeb 之 Servlet 指南 +date: 2020-08-24 19:41:46 +order: 01 +categories: + - Java + - JavaEE + - JavaWeb +tags: + - Java + - JavaWeb + - Servlet +permalink: /pages/e98894/ +--- - +# JavaWeb 之 Servlet 指南 -## 1. JavaWeb 简介 +## JavaWeb 简介 -### 1.1. Web 应用程序 +### Web 应用程序 Web,在英语中 web 即表示网页的意思,它用于表示 Internet 主机上供外界访问的资源。 @@ -34,7 +28,7 @@ Internet 上供外界访问的 Web 资源分为: - 静态 web 资源:指 web 页面中供人们浏览的数据始终是不变。常见静态资源文件:html、css、各种图片类型(jpg、png) - 动态 web 资源:指 web 页面中供人们浏览的数据是由程序产生的,不同时间点访问 web 页面看到的内容各不相同。常见动态资源技术:JSP/Servlet、ASP、PHP -### 1.2. 常见 Web 服务器 +### 常见 Web 服务器 - [Tomcat](http://tomcat.apache.org/) - [Jetty](http://www.eclipse.org/jetty/) @@ -45,9 +39,9 @@ Internet 上供外界访问的 Web 资源分为: - [WebLogic](https://www.oracle.com/middleware/technologies/weblogic.html) - JBoss -## 2. Servlet 简介 +## Servlet 简介 -### 2.1. 什么是 Servlet +### 什么是 Servlet Servlet(Server Applet),即小服务程序或服务连接器。Servlet 是 Java 编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态 Web 内容。 @@ -56,14 +50,14 @@ Servlet(Server Applet),即小服务程序或服务连接器。Servlet 是 Servlet 运行于支持 Java 的应用服务器中。从原理上讲,Servlet 可以响应任何类型的请求,但绝大多数情况下 Servlet 只用来扩展基于 HTTP 协议的 Web 服务器。 -### 2.2. Servlet 和 CGI 的区别 +### Servlet 和 CGI 的区别 Servlet 技术出现之前,Web 主要使用 CGI 技术。它们的区别如下: - Servlet 是基于 Java 编写的,处于服务器进程中,他能够通过多线程方式运行 service() 方法,一个实例可以服务于多个请求,而且一般不会销毁; - CGI(Common Gateway Interface),即通用网关接口。它会为每个请求产生新的进程,服务完成后销毁,所以效率上低于 Servlet。 -### 2.3. Servlet 版本以及主要特性 +### Servlet 版本以及主要特性 | 版本 | 日期 | JAVA EE/JDK 版本 | 特性 | | ----------- | ------------- | ------------------ | --------------------------------------------------------------------- | @@ -78,7 +72,7 @@ Servlet 技术出现之前,Web 主要使用 CGI 技术。它们的区别如下 | Servlet 2.0 | | JDK 1.1 | Part of Java Servlet Development Kit 2.0 | | Servlet 1.0 | 1997 年 6 月 | | | -### 2.4. Servlet 任务 +### Servlet 任务 Servlet 执行以下主要任务: @@ -88,7 +82,7 @@ Servlet 执行以下主要任务: - 发送显式的数据(即文档)到客户端(浏览器)。该文档的格式可以是多种多样的,包括文本文件(HTML 或 XML)、二进制文件(GIF 图像)、Excel 等。 - 发送隐式的 HTTP 响应到客户端(浏览器)。这包括告诉浏览器或其他客户端被返回的文档类型(例如 HTML),设置 cookies 和缓存参数,以及其他类似的任务。 -### 2.5. Servlet 生命周期 +### Servlet 生命周期 ![img](http://www.runoob.com/wp-content/uploads/2014/07/Servlet-LifeCycle.jpg) @@ -100,9 +94,9 @@ Servlet 生命周期如下: 4. **销毁** - Servlet 通过调用 **destroy()** 方法终止(结束)。 5. **卸载** - Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。 -## 3. Servlet API +## Servlet API -### 3.1. Servlet 包 +### Servlet 包 Java Servlet 是运行在带有支持 Java Servlet 规范的解释器的 web 服务器上的 Java 类。 @@ -110,7 +104,7 @@ Servlet 可以使用 **javax.servlet** 和 **javax.servlet.http** 包创建, Java Servlet 就像任何其他的 Java 类一样已经被创建和编译。在您安装 Servlet 包并把它们添加到您的计算机上的 Classpath 类路径中之后,您就可以通过 JDK 的 Java 编译器或任何其他编译器来编译 Servlet。 -### 3.2. Servlet 接口 +### Servlet 接口 Servlet 接口定义了下面五个方法: @@ -201,7 +195,7 @@ destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调 } ``` -## 4. Servlet 和 HTTP 状态码 +## Servlet 和 HTTP 状态码 title: JavaEE Servlet HTTP 状态码 date: 2017-11-08 @@ -213,7 +207,7 @@ categories: - servlet - http -### 4.1. HTTP 状态码 +### HTTP 状态码 HTTP 请求和 HTTP 响应消息的格式是类似的,结构如下: @@ -262,7 +256,7 @@ HeaderN: ... - 500:服务器端在执行请求时发生了错误 - 503:服务器暂时处于超负载或正在进行停机维护,现在无法处理请求 -### 4.2. 设置 HTTP 状态码的方法 +### 设置 HTTP 状态码的方法 下面的方法可用于在 Servlet 程序中设置 HTTP 状态码。这些方法通过 `HttpServletResponse` 对象可用。 @@ -272,7 +266,7 @@ HeaderN: ... | 2 | **public void sendRedirect(String url)**该方法生成一个 302 响应,连同一个带有新文档 URL 的 _Location_ 头。 | | 3 | **public void sendError(int code, String message)**该方法发送一个状态码(通常为 404),连同一个在 HTML 文档内部自动格式化并发送到客户端的短消息。 | -### 4.3. HTTP 状态码实例 +### HTTP 状态码实例 下面的例子把 407 错误代码发送到客户端浏览器,浏览器会显示 "Need authentication!!!" 消息。 @@ -316,4 +310,4 @@ Apache Tomcat/5.5.29 ## 参考资料 - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) -- [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) +- [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) \ No newline at end of file diff --git a/docs/javaee/javaee-jsp.md "b/docs/01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" similarity index 93% rename from docs/javaee/javaee-jsp.md rename to "docs/01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" index 37e6fa66..32b8ab7c 100644 --- a/docs/javaee/javaee-jsp.md +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" @@ -1,12 +1,27 @@ -# JSP 指南 +--- +title: JavaWeb 之 Jsp 指南 +date: 2020-02-07 23:04:47 +order: 02 +categories: + - Java + - JavaEE + - JavaWeb +tags: + - Java + - JavaWeb + - JSP +permalink: /pages/8cc787/ +--- -## 1. 简介 +# JavaWeb 之 Jsp 指南 -### 1.1. 什么是 Java Server Pages +## 简介 + +### 什么是 Java Server Pages `JSP`全称`Java Server Pages`,是一种动态网页开发技术。 -它使用 JSP 标签在 HTML 网页中插入 Java 代码。标签通常以`<%`开头以`%>`结束。 +它使用 JSP 标签在 HTML 网页中插入 Java 代码。标签通常以 `<%` 开头以 `%>` 结束。 JSP 是一种 Java servlet,主要用于实现 Java web 应用程序的用户界面部分。网页开发者们通过结合 HTML 代码、XHTML 代码、XML 元素以及嵌入 JSP 操作和命令来编写 JSP。 @@ -14,7 +29,7 @@ JSP 通过网页表单获取用户输入数据、访问数据库及其他数据 JSP 标签有多种功能,比如访问数据库、记录用户选择信息、访问 JavaBeans 组件等,还可以在不同的网页中传递控制信息和共享信息。 -### 1.2. 为什么使用 JSP +### 为什么使用 JSP JSP 也是一种 Servlet,因此 JSP 能够完成 Servlet 能完成的任何工作。 @@ -27,7 +42,7 @@ JSP 程序与 CGI 程序有着相似的功能,但和 CGI 程序相比,JSP 最后,JSP 是 Java EE 不可或缺的一部分,是一个完整的企业级应用平台。这意味着 JSP 可以用最简单的方式来实现最复杂的应用。 -### 1.3. JSP 的优势 +### JSP 的优势 以下列出了使用 JSP 带来的其他好处: @@ -37,7 +52,7 @@ JSP 程序与 CGI 程序有着相似的功能,但和 CGI 程序相比,JSP - 与 JavaScript 相比:虽然 JavaScript 可以在客户端动态生成 HTML,但是很难与服务器交互,因此不能提供复杂的服务,比如访问数据库和图像处理等等。 - 与静态 HTML 相比:静态 HTML 不包含动态信息。 -## 2. JSP 工作原理 +## JSP 工作原理 **JSP 是一种 Servlet**,但工作方式和 Servlet 有所差别。 @@ -47,7 +62,7 @@ Jsp 是先将源代码部署到服务器再编译,**先部署后编译**。 Jsp 会在客户端第一次请求 Jsp 文件时被编译为 HttpJspPage 类(Servlet 的一个子类)。该类会被服务器临时存放在服务器工作目录里。所以,第一次请求 Jsp 后,访问速度会变快就是这个道理。 -### 2.1. JSP 工作流程 +### JSP 工作流程 网络服务器需要一个 JSP 引擎,也就是一个容器来处理 JSP 页面。容器负责截获对 JSP 页面的请求。本教程使用内嵌 JSP 容器的 Apache 来支持 JSP 开发。 @@ -57,7 +72,7 @@ JSP 容器与 Web 服务器协同合作,为 JSP 的正常运行提供必要的 ![img](http://www.runoob.com/wp-content/uploads/2014/01/jsp-arch.jpg) -#### 2.1.1. 工作步骤 +#### 工作步骤 以下步骤表明了 Web 服务器是如何使用 JSP 来创建网页的: @@ -73,7 +88,7 @@ JSP 容器与 Web 服务器协同合作,为 JSP 的正常运行提供必要的 一般情况下,JSP 引擎会检查 JSP 文件对应的 servlet 是否已经存在,并且检查 JSP 文件的修改日期是否早于 servlet。如果 JSP 文件的修改日期早于对应的 servlet,那么容器就可以确定 JSP 文件没有被修改过并且 servlet 有效。这使得整个流程与其他脚本语言(比如 PHP)相比要高效快捷一些。 -### 2.2. JSP 生命周期 +### JSP 生命周期 理解 JSP 底层功能的关键就是去理解它们所遵守的生命周期。 @@ -90,7 +105,7 @@ JSP 生命周期就是从创建到销毁的整个过程,类似于 servlet 生 ![img](http://www.runoob.com/wp-content/uploads/2014/01/jsp_life_cycle.jpg) -#### 2.2.1. JSP 编译 +#### JSP 编译 当浏览器请求 JSP 页面时,JSP 引擎会首先去检查是否需要编译这个文件。如果这个文件没有被编译过,或者在上次编译后被更改过,则编译这个 JSP 文件。 @@ -100,7 +115,7 @@ JSP 生命周期就是从创建到销毁的整个过程,类似于 servlet 生 - 将 JSP 文件转为 servlet。 - 编译 servlet。 -#### 2.2.2. JSP 初始化 +#### JSP 初始化 容器载入 JSP 文件后,它会在为请求提供任何服务前调用 jspInit()方法。如果您需要执行自定义的 JSP 初始化任务,复写 jspInit()方法就行了,就像下面这样: @@ -112,7 +127,7 @@ public void jspInit(){ 一般来讲程序只初始化一次,servlet 也是如此。通常情况下您可以在 jspInit()方法中初始化数据库连接、打开文件和创建查询表。 -#### 2.2.3. JSP 执行 +#### JSP 执行 这一阶段描述了 JSP 生命周期中一切与请求相关的交互行为,直到被销毁。 @@ -129,7 +144,7 @@ void _jspService(HttpServletRequest request, `_jspService()` 方法在每个 request 中被调用一次并且负责产生与之相对应的 response,并且它还负责产生所有 7 个 HTTP 方法的回应,比如 GET、POST、DELETE 等等。 -#### 2.2.4. JSP 清理 +#### JSP 清理 JSP 生命周期的销毁阶段描述了当一个 JSP 网页从容器中被移除时所发生的一切。 @@ -143,9 +158,9 @@ public void jspDestroy() { } ``` -## 3. 语法 +## 语法 -### 3.1. 脚本 +### 脚本 脚本程序可以包含任意量的 Java 语句、变量、方法或表达式,只要它们在脚本语言中是有效的。 @@ -185,7 +200,7 @@ public void jspDestroy() { ![img](http://www.runoob.com/wp-content/uploads/2014/01/jsp_hello_world.jpg) -#### 3.1.1. 中文编码问题 +#### 中文编码问题 如果我们要在页面正常显示中文,我们需要在 JSP 文件头部添加以下代码:`<>` @@ -214,7 +229,7 @@ pageEncoding="UTF-8"%> 这样中文就可以正常显示了。 -### 3.2. JSP 声明 +### JSP 声明 一个声明语句可以声明一个或多个变量、方法,供后面的 Java 代码使用。在 JSP 文件中,您必须先声明这些变量和方法然后才能使用它们。 @@ -238,7 +253,7 @@ JSP 声明的语法格式: <%! int i = 0; %> <%! int a, b, c; %> <%! Circle a = new Circle(2.0); %> ``` -### 3.3. JSP 表达式 +### JSP 表达式 一个 JSP 表达式中包含的脚本语言表达式,先被转化成 String,然后插入到表达式出现的地方。 @@ -287,7 +302,7 @@ pageEncoding="UTF-8"%> --- -### 3.4. JSP 注释 +### JSP 注释 JSP 注释主要有两个作用:为代码作注释以及将某段代码注释掉。 @@ -319,20 +334,20 @@ pageEncoding="UTF-8"%> 不同情况下使用注释的语法规则: -| **语法** | 描述 | -| ---------------- | ----------------------------------------------------- | -| `<%-- 注释 --%>` | JSP 注释,注释内容不会被发送至浏览器甚至不会被编译 | +| **语法** | 描述 | +| ---------------- |-------------------------------| +| `<%-- 注释 --%>` | JSP 注释,注释内容不会被发送至浏览器甚至不会被编译 | | `` | HTML 注释,通过浏览器查看网页源代码时可以看见注释内容 | -| `<%` | 代表静态 <%常量 | -| `%>` | 代表静态 %> 常量 | -| `'` | 在属性中使用的单引号 | -| `"` | 在属性中使用的双引号 | +| `<%` | 代表静态 `<%` 常量 | +| `%>` | 代表静态 `%>` 常量 | +| `'` | 在属性中使用的单引号 | +| `"` | 在属性中使用的双引号 | -### 3.5. 控制语句 +### 控制语句 JSP 提供对 Java 语言的全面支持。您可以在 JSP 程序中使用 Java API 甚至建立 Java 代码块,包括判断语句和循环语句等等。 -#### 3.5.1. if…else 语句 +#### if…else 语句 `If…else`块,请看下面这个例子: @@ -363,7 +378,7 @@ IF...ELSE 实例 今天不是周末 ``` -#### 3.5.2. switch…case 语句 +#### switch…case 语句 现在来看看 switch…case 块,与 if…else 块有很大的不同,它使用 out.println(),并且整个都装在脚本程序的标签中,就像下面这样: @@ -394,7 +409,7 @@ SWITCH...CASE 实例 星期三 ``` -#### 3.5.3. 循环语句 +#### 循环语句 在 JSP 程序中可以使用 Java 的三个基本循环类型:for,while,和 do…while。 @@ -456,25 +471,25 @@ JSP 支持所有 Java 逻辑和算术运算符。 下表罗列出了 JSP 常见运算符,优先级从高到底: -| **类别** | **操作符** | **结合性** | -| --------- | ----------------------------------- | ---------- | -| 后缀 | `() [] .` (点运算符) | 左到右 | -| 一元 | `++ - - ! ~` | 右到左 | -| 可乘性 | `* / %` | 左到右 | -| 可加性 | `+ -` | 左到右 | -| 移位 | `>> >>> <<` | 左到右 | -| 关系 | `> >= < <=` | 左到右 | -| 相等/不等 | `== !=` | 左到右 | -| 位与 | `&` | 左到右 | -| 位异或 | `^` | 左到右 | -| 位或 | `|` | 左到右 | -| 逻辑与 | `&&` | 左到右 | -| 逻辑或 | `||` | 左到右 | -| 条件判断 | `?:` | 右到左 | -| 赋值 | `= += -= *= /= %= >>= <<= &= ^= |=` | 右到左 | -| 逗号 | `,` | 左到右 | - -### 3.6. JSP 字面量 +| **类别** | **操作符** | **结合性** | +| --------- | ------------------------------------- | ---------- | +| 后缀 | `() [] .` (点运算符) | 左到右 | +| 一元 | `++ - - ! ~` | 右到左 | +| 可乘性 | `* / %` | 左到右 | +| 可加性 | `+ -` | 左到右 | +| 移位 | `>> >>> <<` | 左到右 | +| 关系 | `> >= < <=` | 左到右 | +| 相等/不等 | `== !=` | 左到右 | +| 位与 | `&` | 左到右 | +| 位异或 | `^` | 左到右 | +| 位或 | `|` | 左到右 | +| 逻辑与 | `&&` | 左到右 | +| 逻辑或 | `| |` | 左到右 | +| 条件判断 | `?:` | 右到左 | +| 赋值 | `= += -= \*= /= %= >>= <<= &= ^= | =` | 右到左 | +| 逗号 | `,` | 左到右 | + +### JSP 字面量 JSP 语言定义了以下几个字面量: @@ -484,7 +499,7 @@ JSP 语言定义了以下几个字面量: - 字符串(string):以单引号或双引号开始和结束; - Null:null。 -## 4. 指令 +## 指令 JSP 指令用来设置整个 JSP 页面相关的属性,如网页的编码方式和脚本语言。 @@ -506,7 +521,7 @@ JSP 中的三种指令标签: | `<%@ include ... %>` | 包含其他文件 | | `<%@ taglib ... %>` | 引入标签库的定义,可以是自定义标签 | -### 4.1. Page 指令 +### Page 指令 Page 指令为容器提供当前页面的使用说明。一个 JSP 页面可以包含多个`page`指令。 @@ -529,7 +544,7 @@ Page 指令的语法格式: pageEncoding="UTF-8" %> ``` -#### 4.1.1. 属性 +#### 属性 下表列出与 Page 指令相关的属性: @@ -549,7 +564,7 @@ pageEncoding="UTF-8" %> | isELIgnored | 指定是否执行 EL 表达式 | | isScriptingEnabled | 确定脚本元素能否被使用 | -### 4.2. Include 指令 +### Include 指令 JSP 可以通过`include`指令来包含其他文件。 @@ -571,7 +586,7 @@ Include 指令的语法格式如下: ``` -### 4.3. Taglib 指令 +### Taglib 指令 JSP 允许用户自定义标签,一个自定义标签库就是自定义标签的集合。 @@ -591,7 +606,7 @@ uri 属性确定标签库的位置,prefix 属性指定标签库的前缀。 ``` -## 5. JSP 动作元素 +## JSP 动作元素 JSP 动作元素是一组 JSP 内置的标签,只需要书写很少的标记代码就能使用 JSP 提供的丰富功能。JSP 动作元素是对常用的 JSP 功能的抽象与封装,包括两种,自定义 JSP 动作元素与标准 JSP 动作元素。 @@ -620,14 +635,14 @@ JSP 动作元素是一组 JSP 内置的标签,只需要书写很少的标记 | jsp:body | 设置动态定义的 XML 元素内容。 | | jsp:text | 在 JSP 页面和文档中使用写入文本的模板 | -### 5.1. 常见的属性 +### 常见的属性 所有的动作要素都有两个属性:id 属性和 scope 属性。 - **id 属性:**id 属性是动作元素的唯一标识,可以在 JSP 页面中引用。动作元素创建的 id 值可以通过 PageContext 来调用。 - **scope 属性:**该属性用于识别动作元素的生命周期。 id 属性和 scope 属性有直接关系,scope 属性定义了相关联 id 对象的寿命。 scope 属性有四个可能的值: (a) page, (b)request, (c)session, 和 (d) application。 -### 5.2. `` +### `` `` 用来包含静态和动态的文件。该动作把指定文件插入正在生成的页面。 @@ -688,7 +703,7 @@ include 动作实例 今天的日期是: 2016-6-25 14:08:17 ``` -### 5.3. `` +### `` **jsp:useBean** 动作用来加载一个将在 JSP 页面中使用的 JavaBean。 @@ -712,7 +727,7 @@ jsp:useBean 动作最简单的语法为: 在给出具体实例前,让我们先来看下 jsp:setProperty 和 jsp:getProperty 动作元素: -### 5.4. `` +### `` jsp:setProperty 用来设置已经实例化的 Bean 对象的属性,有两种用法。首先,你可以在 jsp:useBean 元素的外面(后面)使用 jsp:setProperty,如下所示: @@ -742,7 +757,7 @@ jsp:setProperty 动作有下面四个属性,如下表: | value | value 属性是可选的。该属性用来指定 Bean 属性的值。字符串数据会在目标类中通过标准的 valueOf 方法自动转换成数字、boolean、Boolean、 byte、Byte、char、Character。例如,boolean 和 Boolean 类型的属性值(比如"true")通过 Boolean.valueOf 转换,int 和 Integer 类型的属性值(比如"42")通过 Integer.valueOf 转换。    value 和 param 不能同时使用,但可以使用其中任意一个。 | | param | param 是可选的。它指定用哪个请求参数作为 Bean 属性的值。如果当前请求没有参数,则什么事情也不做,系统不会把 null 传递给 Bean 属性的 set 方法。因此,你可以让 Bean 自己提供默认属性值,只有当请求参数明确指定了新值时才修改默认属性值。 | -### 5.5. `` +### `` jsp:getProperty 动作提取指定 Bean 属性的值,转换成字符串,然后输出。语法格式如下: @@ -820,7 +835,7 @@ pageEncoding="UTF-8"%> ![img](http://www.runoob.com/wp-content/uploads/2014/01/D7AD87A8-3392-4D4E-8731-18806B0644CD.jpg) -### 5.6. `` +### `` jsp:forward 动作把请求转到另外的页面。jsp:forward 标记只有一个属性 page。语法格式如下所示: @@ -872,7 +887,7 @@ pageEncoding="UTF-8"%> 今天的日期是: 2016-6-25 14:37:25 ``` -### 5.7. `` +### `` jsp:plugin 动作用来根据浏览器的类型,插入通过 Java 插件 运行 Java Applet 所必需的 OBJECT 或 EMBED 元素。 @@ -897,7 +912,7 @@ plugin 动作有多个对应 HTML 元素的属性用于格式化 Java 组件。p 如果你有兴趣可以尝试使用 applet 来测试 `jsp:plugin` 动作元素,`` 元素是一个新元素,在组件出现故障的错误是发送给用户错误信息。 -### 5.8. `` 、 ``、`` +### `` 、 ``、`` `` 、 ``、`` 动作元素动态定义 XML 元素。动态是非常重要的,这就意味着 XML 元素在编译时是动态生成的而非静态。 @@ -929,7 +944,7 @@ pageEncoding="UTF-8"%> ![img](http://www.runoob.com/wp-content/uploads/2014/01/7D8C47F0-0DDE-4F1D-8BE1-B2C9C955683E.jpg) -### 5.9. `` +### `` 动作元素允许在 JSP 页面和文档中使用写入文本的模板,语法格式如下: @@ -963,7 +978,7 @@ PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 你可以对以上实例尝试使用及不使用该动作元素执行结果的区别。 -## 6. JSP 隐式对象 +## JSP 隐式对象 JSP 隐式对象是 JSP 容器为每个页面提供的 Java 对象,开发者可以直接使用它们而不用显式声明。JSP 隐式对象也被称为预定义变量。 @@ -981,7 +996,7 @@ JSP 所支持的九大隐式对象: | page | 类似于 Java 类中的 this 关键字 | | Exception | **Exception**类的对象,代表发生错误的 JSP 页面中对应的异常对象 | -### 6.1. request 对象 +### request 对象 `request`对象是`javax.servlet.http.HttpServletRequest` 类的实例。 @@ -989,7 +1004,7 @@ JSP 所支持的九大隐式对象: `request`对象提供了一系列方法来获取 HTTP 头信息,cookies,HTTP 方法等等。 -### 6.2. response 对象 +### response 对象 `response`对象是`javax.servlet.http.HttpServletResponse`类的实例。 @@ -997,7 +1012,7 @@ JSP 所支持的九大隐式对象: `response`对象也定义了处理 HTTP 头模块的接口。通过这个对象,开发者们可以添加新的 cookies,时间戳,HTTP 状态码等等。 -### 6.3. out 对象 +### out 对象 `out`对象是`javax.servlet.jsp.JspWriter`类的实例,用来在`response`对象中写入内容。 @@ -1013,13 +1028,13 @@ JSP 所支持的九大隐式对象: | **out.println(dataType dt)** | 输出 Type 类型的值然后换行 | | **out.flush()** | 刷新输出流 | -### 6.4. session 对象 +### session 对象 `session`对象是`javax.servlet.http.HttpSession`类的实例。和 Java Servlets 中的`session`对象有一样的行为。 `session`对象用来跟踪在各个客户端请求间的会话。 -### 6.5. application 对象 +### application 对象 `application`对象直接包装了 servlet 的`ServletContext`类的对象,是`javax.servlet.ServletContext`类的实例。 @@ -1027,7 +1042,7 @@ JSP 所支持的九大隐式对象: 通过向`application`中添加属性,则所有组成您 web 应用的 JSP 文件都能访问到这些属性。 -### 6.6. config 对象 +### config 对象 `config`对象是`javax.servlet.ServletConfig`类的实例,直接包装了 servlet 的`ServletConfig`类的对象。 @@ -1041,7 +1056,7 @@ config.getServletName(); 它返回包含在``元素中的 servlet 名字,注意,``元素在`WEB-INF\web.xml`文件中定义。 -### 6.7. pageContext 对象 +### pageContext 对象 `pageContext`对象是`javax.servlet.jsp.PageContext`类的实例,用来代表整个 JSP 页面。 @@ -1059,23 +1074,23 @@ config.getServletName(); pageContext.removeAttribute("attrName", PAGE_SCOPE); ``` -### 6.8. page 对象 +### page 对象 这个对象就是页面实例的引用。它可以被看做是整个 JSP 页面的代表。 `page`对象就是`this`对象的同义词。 -### 6.9. exception 对象 +### exception 对象 `exception`对象包装了从先前页面中抛出的异常信息。它通常被用来产生对出错条件的适当响应。 -## 7. EL 表达式 +## EL 表达式 EL 表达式是用`${}`括起来的脚本,用来更方便地读取对象。EL 表达式写在 JSP 的 HTML 代码中,而不能写在`<%`与`%>`引起的 JSP 脚本中。 JSP 表达式语言(EL)使得访问存储在 JavaBean 中的数据变得非常简单。JSP EL 既可以用来创建算术表达式也可以用来创建逻辑表达式。在 JSP EL 表达式内可以使用整型数,浮点数,字符串,常量 true、false,还有 null。 -### 7.1. 一个简单的语法 +### 一个简单的语法 典型的,当您需要在 JSP 标签中指定一个属性值时,只需要简单地使用字符串即可: @@ -1129,7 +1144,7 @@ ${expr} 这样,EL 表达式就会被忽略。若设为 false,则容器将会计算 EL 表达式。 -### 7.2. EL 中的基础操作符 +### EL 中的基础操作符 EL 表达式支持大部分 Java 所提供的算术和逻辑操作符: @@ -1154,7 +1169,7 @@ EL 表达式支持大部分 Java 所提供的算术和逻辑操作符: | ! or not | 测试取反 | | empty | 测试是否空值 | -### 7.3. JSP EL 中的函数 +### JSP EL 中的函数 JSP EL 允许您在表达式中使用函数。这些函数必须被定义在自定义标签库中。函数的使用语法如下: @@ -1170,7 +1185,7 @@ ${fn:length("Get my length")} 要使用任何标签库中的函数,您需要将这些库安装在服务器中,然后使用 `` 标签在 JSP 文件中包含这些库。 -### 7.4. JSP EL 隐含对象 +### JSP EL 隐含对象 JSP EL 支持下表列出的隐含对象: @@ -1190,7 +1205,7 @@ JSP EL 支持下表列出的隐含对象: 您可以在表达式中使用这些对象,就像使用变量一样。接下来会给出几个例子来更好的理解这个概念。 -### 7.5. pageContext 对象 +### pageContext 对象 pageContext 对象是 JSP 中 pageContext 对象的引用。通过 pageContext 对象,您可以访问 request 对象。比如,访问 request 对象传入的查询字符串,就像这样: @@ -1198,13 +1213,13 @@ pageContext 对象是 JSP 中 pageContext 对象的引用。通过 pageContext ${pageContext.request.queryString} ``` -### 7.6. Scope 对象 +### Scope 对象 pageScope,requestScope,sessionScope,applicationScope 变量用来访问存储在各个作用域层次的变量。 举例来说,如果您需要显式访问在 applicationScope 层的 box 变量,可以这样来访问:applicationScope.box。 -### 7.7. param 和 paramValues 对象 +### param 和 paramValues 对象 param 和 paramValues 对象用来访问参数值,通过使用 request.getParameter 方法和 request.getParameterValues 方法。 @@ -1232,7 +1247,7 @@ Param"; %> param 对象返回单一的字符串,而 paramValues 对象则返回一个字符串数组。 -### 7.8. header 和 headerValues 对象 +### header 和 headerValues 对象 header 和 headerValues 对象用来访问信息头,通过使用 request.getHeader 方法和 request.getHeaders 方法。 @@ -1264,7 +1279,7 @@ Example"; %> header 对象返回单一值,而 headerValues 则返回一个字符串数组。 -## 8. JSTL +## JSTL JSP 标准标签库(JSTL)是一个 JSP 标签集合,它封装了 JSP 应用的通用核心功能。 @@ -1278,7 +1293,7 @@ JSTL 支持通用的、结构化的任务,比如迭代,条件判断,XML - **XML 标签** - **JSTL 函数** -### 8.1. JSTL 库安装 +### JSTL 库安装 Apache Tomcat 安装 JSTL 库步骤如下: @@ -1339,9 +1354,9 @@ Apache Tomcat 安装 JSTL 库步骤如下: ``` -使用任何库,你必须在每个 JSP 文件中的头部包含 **** 标签。 +使用任何库,你必须在每个 JSP 文件中的头部包含 **``** 标签。 -### 8.2. 核心标签 +### 核心标签 核心标签是最常用的 JSTL 标签。引用核心标签库的语法如下: @@ -1349,24 +1364,24 @@ Apache Tomcat 安装 JSTL 库步骤如下: <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> ``` -| 标签 | 描述 | -| :---------------------------------------------------------------------- | :-------------------------------------------------------------------------- | -| [``](http://www.runoob.com/jsp/jstl-core-out-tag.html) | 用于在 JSP 中显示数据,就像<%= ... > | -| [``](http://www.runoob.com/jsp/jstl-core-set-tag.html) | 用于保存数据 | -| [``](http://www.runoob.com/jsp/jstl-core-remove-tag.html) | 用于删除数据 | -| [``](http://www.runoob.com/jsp/jstl-core-catch-tag.html) | 用来处理产生错误的异常状况,并且将错误信息储存起来 | -| [``](http://www.runoob.com/jsp/jstl-core-if-tag.html) | 与我们在一般程序中用的 if 一样 | -| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | 本身只当做的父标签 | -| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | 的子标签,用来判断条件是否成立 | -| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | 的子标签,接在标签后,当标签判断为 false 时被执行 | -| [``](http://www.runoob.com/jsp/jstl-core-import-tag.html) | 检索一个绝对或相对 URL,然后将其内容暴露给页面 | -| [``](http://www.runoob.com/jsp/jstl-core-foreach-tag.html) | 基础迭代标签,接受多种集合类型 | -| [``](http://www.runoob.com/jsp/jstl-core-foreach-tag.html) | 根据指定的分隔符来分隔内容并迭代输出 | -| [``](http://www.runoob.com/jsp/jstl-core-param-tag.html) | 用来给包含或重定向的页面传递参数 | -| [``](http://www.runoob.com/jsp/jstl-core-redirect-tag.html) | 重定向至一个新的 URL. | -| [``](http://www.runoob.com/jsp/jstl-core-url-tag.html) | 使用可选的查询参数来创造一个 URL | - -### 8.3. 格式化标签 +| 标签 | 描述 | +| :---------------------------------------------------------------------- |:------------------------------------------------------------------| +| [``](http://www.runoob.com/jsp/jstl-core-out-tag.html) | 用于在 JSP 中显示数据,就像<%= ... > | +| [``](http://www.runoob.com/jsp/jstl-core-set-tag.html) | 用于保存数据 | +| [``](http://www.runoob.com/jsp/jstl-core-remove-tag.html) | 用于删除数据 | +| [``](http://www.runoob.com/jsp/jstl-core-catch-tag.html) | 用来处理产生错误的异常状况,并且将错误信息储存起来 | +| [``](http://www.runoob.com/jsp/jstl-core-if-tag.html) | 与我们在一般程序中用的 if 一样 | +| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | 本身只当做 `` 和 `` 的父标签 | +| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | `` 的子标签,用来判断条件是否成立 | +| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | `` 的子标签,接在 `` 标签后,当 `` 标签判断为 false 时被执行 | +| [``](http://www.runoob.com/jsp/jstl-core-import-tag.html) | 检索一个绝对或相对 URL,然后将其内容暴露给页面 | +| [``](http://www.runoob.com/jsp/jstl-core-foreach-tag.html) | 基础迭代标签,接受多种集合类型 | +| [``](http://www.runoob.com/jsp/jstl-core-foreach-tag.html) | 根据指定的分隔符来分隔内容并迭代输出 | +| [``](http://www.runoob.com/jsp/jstl-core-param-tag.html) | 用来给包含或重定向的页面传递参数 | +| [``](http://www.runoob.com/jsp/jstl-core-redirect-tag.html) | 重定向至一个新的 URL. | +| [``](http://www.runoob.com/jsp/jstl-core-url-tag.html) | 使用可选的查询参数来创造一个 URL | + +### 格式化标签 JSTL 格式化标签用来格式化并输出文本、日期、时间、数字。引用格式化标签库的语法如下: @@ -1388,7 +1403,7 @@ JSTL 格式化标签用来格式化并输出文本、日期、时间、数字。 | [``](http://www.runoob.com/jsp/jstl-format-message-tag.html) | 显示资源配置文件信息 | | [``](http://www.runoob.com/jsp/jstl-format-requestencoding-tag.html) | 设置 request 的字符编码 | -### 8.4. SQL 标签 +### SQL 标签 JSTL SQL 标签库提供了与关系型数据库(Oracle,MySQL,SQL Server 等等)进行交互的标签。引用 SQL 标签库的语法如下: @@ -1405,7 +1420,7 @@ JSTL SQL 标签库提供了与关系型数据库(Oracle,MySQL,SQL Server | [``](http://www.runoob.com/jsp/jstl-sql-dateparam-tag.html) | 将 SQL 语句中的日期参数设为指定的 java.util.Date 对象值 | | [``](http://www.runoob.com/jsp/jstl-sql-transaction-tag.html) | 在共享数据库连接中提供嵌套的数据库行为元素,将所有语句以一个事务的形式来运行 | -### 8.5. XML 标签 +### XML 标签 JSTL XML 标签库提供了创建和操作 XML 文档的标签。引用 XML 标签库的语法如下: @@ -1423,20 +1438,20 @@ JSTL XML 标签库提供了创建和操作 XML 文档的标签。引用 XML 标 下载地址: -| 标签 | 描述 | -| :----------------------------------------------------------------------- | :---------------------------------------------------------- | -| [``](http://www.runoob.com/jsp/jstl-xml-out-tag.html) | 与<%= ... >,类似,不过只用于 XPath 表达式 | -| [``](http://www.runoob.com/jsp/jstl-xml-parse-tag.html) | 解析 XML 数据 | -| [``](http://www.runoob.com/jsp/jstl-xml-set-tag.html) | 设置 XPath 表达式 | -| [``](http://www.runoob.com/jsp/jstl-xml-if-tag.html) | 判断 XPath 表达式,若为真,则执行本体中的内容,否则跳过本体 | -| [``](http://www.runoob.com/jsp/jstl-xml-foreach-tag.html) | 迭代 XML 文档中的节点 | -| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | 的父标签 | -| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | 的子标签,用来进行条件判断 | -| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | 的子标签,当判断为 false 时被执行 | -| [``](http://www.runoob.com/jsp/jstl-xml-transform-tag.html) | 将 XSL 转换应用在 XML 文档中 | -| [``](http://www.runoob.com/jsp/jstl-xml-param-tag.html) | 与共同使用,用于设置 XSL 样式表 | - -### 8.6. JSTL 函数 +| 标签 | 描述 | +| :----------------------------------------------------------------------- |:----------------------------------------------| +| [``](http://www.runoob.com/jsp/jstl-xml-out-tag.html) | 与 `<%= ... >`,类似,不过只用于 XPath 表达式 | +| [``](http://www.runoob.com/jsp/jstl-xml-parse-tag.html) | 解析 XML 数据 | +| [``](http://www.runoob.com/jsp/jstl-xml-set-tag.html) | 设置 XPath 表达式 | +| [``](http://www.runoob.com/jsp/jstl-xml-if-tag.html) | 判断 XPath 表达式,若为真,则执行本体中的内容,否则跳过本体 | +| [``](http://www.runoob.com/jsp/jstl-xml-foreach-tag.html) | 迭代 XML 文档中的节点 | +| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | `` 和 `` 的父标签 | +| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | `` 的子标签,用来进行条件判断 | +| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | `` 的子标签,当 `` 判断为 false 时被执行 | +| [``](http://www.runoob.com/jsp/jstl-xml-transform-tag.html) | 将 XSL 转换应用在 XML 文档中 | +| [``](http://www.runoob.com/jsp/jstl-xml-param-tag.html) | 与 `` 共同使用,用于设置 XSL 样式表 | + +### JSTL 函数 JSTL 包含一系列标准函数,大部分是通用的字符串处理函数。引用 JSTL 函数库的语法如下: @@ -1463,9 +1478,9 @@ JSTL 包含一系列标准函数,大部分是通用的字符串处理函数。 | [fn:toUpperCase()](http://www.runoob.com/jsp/jstl-function-touppercase.html) | 将字符串中的字符转为大写 | | [fn:trim()](http://www.runoob.com/jsp/jstl-function-trim.html) | 移除首尾的空白符 | -## 9. Taglib +## Taglib -### 9.1. JSP 自定义标签 +### JSP 自定义标签 自定义标签是用户定义的 JSP 语言元素。当 JSP 页面包含一个自定义标签时将被转化为 servlet,标签转化为对被 称为 tag handler 的对象的操作,即当 servlet 执行时 Web container 调用那些操作。 @@ -1473,7 +1488,7 @@ JSP 标签扩展可以让你创建新的标签并且可以直接插入到一个 你可以继承 SimpleTagSupport 类并重写的 doTag()方法来开发一个最简单的自定义标签。 -### 9.2. 创建"Hello"标签 +### 创建"Hello"标签 接下来,我们想创建一个自定义标签叫作,标签格式为: @@ -1527,7 +1542,7 @@ JspWriter out = getJspContext().getOut(); out.println("Hello Custom Tag!"); } } Hello Custom Tag! ``` -### 9.3. 访问标签体 +### 访问标签体 你可以像标准标签库一样在标签中包含消息内容。如我们要在我们自定义的 Hello 中包含内容,格式如下: @@ -1596,7 +1611,7 @@ public class HelloTag extends SimpleTagSupport { This is message body ``` -### 9.4. 自定义标签属性 +### 自定义标签属性 你可以在自定义标准中设置各种属性,要接收属性,值自定义标签类必须实现 setter 方法, JavaBean 中的 setter 方法如下所示: @@ -1635,7 +1650,7 @@ public class HelloTag extends SimpleTagSupport { } ``` -属性的名称是"message",所以 setter 方法是的 setMessage()。现在让我们在 TLD 文件中使用的元素添加此属性: +属性的名称是"message",所以 setter 方法是的 setMessage()。现在让我们在 TLD 文件中使用的 `` 元素添加此属性: ``` @@ -1713,4 +1728,4 @@ This is custom tag java.util.Date ..... -``` +``` \ No newline at end of file diff --git a/docs/javaee/javaee-filter-listener.md "b/docs/01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" similarity index 88% rename from docs/javaee/javaee-filter-listener.md rename to "docs/01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" index 250fd9a0..aea60247 100644 --- a/docs/javaee/javaee-filter-listener.md +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" @@ -1,32 +1,32 @@ -# JavaEE 之 Filter 和 Listener +--- +title: JavaWeb 之 Filter 和 Listener +date: 2020-08-24 19:41:46 +order: 03 +categories: + - Java + - JavaEE + - JavaWeb +tags: + - Java + - JavaWeb + - Filter + - Listener +permalink: /pages/82df5f/ +--- + +# JavaWeb 之 Filter 和 Listener 引入了 Servlet 规范后,你不需要关心 Socket 网络通信、不需要关心 HTTP 协议,也不需要关心你的业务类是如何被实例化和调用的,因为这些都被 Servlet 规范标准化了,你只要关心怎么实现的你的业务逻辑。这对于程序员来说是件好事,但也有不方便的一面。所谓规范就是说大家都要遵守,就会千篇一律,但是如果这个规范不能满足你的业务的个性化需求,就有问题了,因此设计一个规范或者一个中间件,要充分考虑到可扩展性。Servlet 规范提供了两种扩展机制:**Filter**和**Listener**。 - - -- [1. Filter](#1-filter) - - [1.1. 过滤器方法](#11-过滤器方法) - - [1.2. 过滤器配置](#12-过滤器配置) -- [2. Listener](#2-listener) - - [2.1. 监听器的分类](#21-监听器的分类) - - [2.2. 监听对象的创建和销毁](#22-监听对象的创建和销毁) - - [2.3. 监听对象的属性变化](#23-监听对象的属性变化) - - [2.4. 监听 Session 内的对象](#24-监听-session-内的对象) -- [3. Filter 和 Listener](#3-filter-和-listener) -- [4. 示例代码](#4-示例代码) -- [5. 参考资料](#5-参考资料) - - - -## 1. Filter +## Filter **Filter 是过滤器,这个接口允许你对请求和响应做一些统一的定制化处理**。 Filter 提供了过滤链(Filter Chain)的概念,一个过滤链包括多个 Filter。客户端请求 request 在抵达 Servlet 之前会经过过滤链的所有 Filter,服务器响应 response 从 Servlet 抵达客户端浏览器之前也会经过过滤链的所有 FIlter。 -![img](http://dunwu.test.upcdn.net/snap/1559054413341.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/1559054413341.png) -### 1.1. 过滤器方法 +### 过滤器方法 Filter 接口有三个方法: @@ -71,7 +71,7 @@ public interface Filter { } ``` -### 1.2. 过滤器配置 +### 过滤器配置 `Filter` 需要配置在 `web.xml` 中才能生效。一个 `Filter` 需要配置 `` 与 `` 标签。 @@ -85,13 +85,13 @@ public interface Filter { - INCLUDE - JSP 中可以通过 `` 请求某 Servlet。仅在这种情况表有效。 - ERROR - JSP 中可以通过 `<%@ page errorPage="error.jsp" %>` 指定错误处理页面。仅在这种情况表有效。 -## 2. Listener +## Listener 监听器(`Listener`)用于监听 web 应用程序中的`ServletContext`, `HttpSession`和 `ServletRequest`等域对象的创建与销毁事件,以及监听这些域对象中的属性发生修改的事件。 使用 `Listener` 不需要关注该类事件时怎样触发或者怎么调用相应的 `Listener`,只要记住该类事件触发时一定会调用相应的 `Listener`,遵循 Servlet 规范的服务器会自动完成相应工作。 -### 2.1. 监听器的分类 +### 监听器的分类 在 Servlet 规范中定义了多种类型的监听器,它们用于监听的事件源分别为`ServletContext`,`HttpSession`和`ServletRequest`这三个域对象 Servlet 规范针对这三个对象上的操作,又把多种类型的监听器划分为三种类型: @@ -100,16 +100,16 @@ Servlet 规范针对这三个对象上的操作,又把多种类型的监听器 2. 监听域对象中的属性的增加和删除的事件监听器。 3. 监听绑定到 HttpSession 域中的某个对象的状态的事件监听器。 -### 2.2. 监听对象的创建和销毁 +### 监听对象的创建和销毁 -#### 2.2.1. HttpSessionListener +#### HttpSessionListener **`HttpSessionListener` 接口用于监听 `HttpSession` 对象的创建和销毁。** - 创建一个 `Session` 时,激发 `sessionCreated (HttpSessionEvent se)` 方法 - 销毁一个 `Session` 时,激发 `sessionDestroyed (HttpSessionEvent se)` 方法。 -#### 2.2.2. ServletContextListener +#### ServletContextListener **`ServletContextListener` 接口用于监听 `ServletContext` 对象的创建和销毁事件。** @@ -123,7 +123,7 @@ Servlet 规范针对这三个对象上的操作,又把多种类型的监听器 - 创建:服务器启动针对每一个 Web 应用创建 `ServletContext` - 销毁:服务器关闭前先关闭代表每一个 web 应用的 `ServletContext` -#### 2.2.3. ServletRequestListener +#### ServletRequestListener **`ServletRequestListener` 接口用于监听 `ServletRequest` 对象的创建和销毁。** @@ -135,12 +135,12 @@ Servlet 规范针对这三个对象上的操作,又把多种类型的监听器 - 创建:用户每一次访问都会创建 request 对象 - 销毁:当前访问结束,request 对象就会销毁 -### 2.3. 监听对象的属性变化 +### 监听对象的属性变化 域对象中属性的变更的事件监听器就是用来监听 `ServletContext`、`HttpSession`、`HttpServletRequest` 这三个对象中的属性变更信息事件的监听器。 这三个监听器接口分别是 `ServletContextAttributeListener`、`HttpSessionAttributeListener` `和 ServletRequestAttributeListener`,这三个接口中都定义了三个方法来处理被监听对象中的属性的增加,删除和替换的事件,同一个事件在这三个接口中对应的方法名称完全相同,只是接受的参数类型不同。 -#### 2.3.1. attributeAdded 方法 +#### attributeAdded 方法 当向被监听对象中增加一个属性时,web 容器就调用事件监听器的 `attributeAdded` 方法进行响应,这个方法接收一个事件类型的参数,监听器可以通过这个参数来获得正在增加属性的域对象和被保存到域中的属性对象 各个域属性监听器中的完整语法定义为: @@ -151,7 +151,7 @@ public void attributeReplaced(HttpSessionBindingEvent hsbe) public void attributeRmoved(ServletRequestAttributeEvent srae) ``` -#### 2.3.2. attributeRemoved 方法 +#### attributeRemoved 方法 当删除被监听对象中的一个属性时,web 容器调用事件监听器的 `attributeRemoved` 方法进行响应 各个域属性监听器中的完整语法定义为: @@ -162,7 +162,7 @@ public void attributeRemoved(HttpSessionBindingEvent hsbe) public void attributeRemoved(ServletRequestAttributeEvent srae) ``` -#### 2.3.3. attributeReplaced 方法 +#### attributeReplaced 方法 当监听器的域对象中的某个属性被替换时,web 容器调用事件监听器的 `attributeReplaced` 方法进行响应 各个域属性监听器中的完整语法定义为: @@ -173,7 +173,7 @@ public void attributeReplaced(HttpSessionBindingEvent hsbe) public void attributeReplaced(ServletRequestAttributeEvent srae) ``` -### 2.4. 监听 Session 内的对象 +### 监听 Session 内的对象 保存在 Session 域中的对象可以有多种状态: @@ -186,33 +186,33 @@ Servlet 规范中定义了两个特殊的监听器接口 `HttpSessionBindingList 实现这两个接口的类不需要 `web.xml` 文件中进行注册。 -#### 2.4.1. HttpSessionBindingListener +#### HttpSessionBindingListener `HttpSessionBindingListener` 接口的 JavaBean 对象可以感知自己被绑定或解绑定到 `Session` 中的事件。 - 当对象被绑定到 `HttpSession` 对象中时,web 服务器调用该对象的 `valueBound(HttpSessionBindingEvent event)` 方法。 - 当对象从 `HttpSession` 对象中解除绑定时,web 服务器调用该对象的 `valueUnbound(HttpSessionBindingEvent event)` 方法。 -#### 2.4.2. HttpSessionActivationListener +#### HttpSessionActivationListener 实现了 `HttpSessionActivationListener` 接口的 JavaBean 对象可以感知自己被活化(反序列化)和钝化(序列化)的事件。 - 当绑定到 `HttpSession` 对象中的 JavaBean 对象将要随 `HttpSession` 对象被序列化之前,web 服务器调用该 JavaBean 对象的 `sessionWillPassivate(HttpSessionEvent event)` 方法。这样 JavaBean 对象就可以知道自己将要和 `HttpSession` 对象一起被序列化到硬盘中. - 当绑定到 `HttpSession` 对象中的 JavaBean 对象将要随 `HttpSession` 对象被反序列化之后,web 服务器调用该 JavaBean 对象的 `sessionDidActive(HttpSessionEvent event)` 方法。这样 JavaBean 对象就可以知道自己将要和 `HttpSession` 对象一起被反序列化回到内存中 -## 3. Filter 和 Listener +## Filter 和 Listener Filter 和 Listener 的本质区别: - **Filter 是干预过程的**,它是过程的一部分,是基于过程行为的。 - **Listener 是基于状态的**,任何行为改变同一个状态,触发的事件是一致的。 -## 4. 示例代码 +## 示例代码 - `Filter` 的示例源码:[源码](https://github.com/dunwu/javatech/tree/master/codes/javaee-tutorial/javaee-tutorial-filter) - `Listener` 的示例源码:[源码](https://github.com/dunwu/javatech/tree/master/codes/javaee-tutorial/javaee-tutorial-listener) -## 5. 参考资料 +## 参考资料 - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) -- [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) +- [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) \ No newline at end of file diff --git a/docs/javaee/javaee-cookie-sesion.md "b/docs/01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" similarity index 96% rename from docs/javaee/javaee-cookie-sesion.md rename to "docs/01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" index 50953642..3729f4a6 100644 --- a/docs/javaee/javaee-cookie-sesion.md +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" @@ -1,12 +1,28 @@ -# JavaEE 之 Cookie 和 Session - -## 1. Cookie +--- +title: JavaWeb 之 Cookie 和 Session +date: 2020-08-24 19:41:46 +order: 04 +categories: + - Java + - JavaEE + - JavaWeb +tags: + - Java + - JavaWeb + - Cookie + - Session +permalink: /pages/c46bff/ +--- + +# JavaWeb 之 Cookie 和 Session + +## Cookie 由于 Http 是一种无状态的协议,服务器单从网络连接上无从知道客户身份。 会话跟踪是 Web 程序中常用的技术,用来跟踪用户的整个会话。常用会话跟踪技术是 Cookie 与 Session。 -### 1.1. Cookie 是什么 +### Cookie 是什么 Cookie 实际上是存储在客户端上的文本信息,并保留了各种跟踪的信息。 @@ -16,11 +32,11 @@ Cookie 实际上是存储在客户端上的文本信息,并保留了各种跟 2. 客户端浏览器会把 Cookie 保存下来。 3. 当浏览器再请求该网站时,浏览器把该请求的网址连同 Cookie 一同提交给服务器。服务器检查该 Cookie,以此来辨认用户状态。 -***注:Cookie 功能需要浏览器的支持,如果浏览器不支持 Cookie 或者 Cookie 禁用了,Cookie 功能就会失效。*** +**_注:Cookie 功能需要浏览器的支持,如果浏览器不支持 Cookie 或者 Cookie 禁用了,Cookie 功能就会失效。_** Java 中把 Cookie 封装成了`javax.servlet.http.Cookie`类。 -### 1.2. Cookie 剖析 +### Cookie 剖析 Cookies 通常设置在 HTTP 头信息中(虽然 JavaScript 也可以直接在浏览器上设置一个 Cookie)。 @@ -52,7 +68,7 @@ Accept-Charset: iso-8859-1,*,utf-8 Cookie: name=xyz ``` -### 1.3. Cookie 类中的方法 +### Cookie 类中的方法 | 方法 | 功能 | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | @@ -69,7 +85,7 @@ Cookie: name=xyz | public void setComment(String purpose) | 该方法规定了描述 cookie 目的的注释。该注释在浏览器向用户呈现 cookie 时非常有用。 | | public String getComment() | 该方法返回了描述 cookie 目的的注释,如果 cookie 没有注释则返回 null。 | -### 1.4. Cookie 的有效期 +### Cookie 的有效期 `Cookie`的`maxAge`决定着 Cookie 的有效期,单位为秒。 @@ -79,7 +95,7 @@ Cookie: name=xyz Cookie 中提供`getMaxAge()`**和**`setMaxAge(int expiry)`方法来读写`maxAge`属性。 -### 1.5. Cookie 的域名 +### Cookie 的域名 Cookie 是不可以跨域名的。域名 www.google.com 颁发的 Cookie 不会被提交到域名 www.baidu.com 去。这是由 Cookie 的隐私安全机制决定的。隐私安全机制能够禁止网站非法获取其他网站的 Cookie。 @@ -87,13 +103,13 @@ Cookie 是不可以跨域名的。域名 www.google.com 颁发的 Cookie 不会 Java 中使用`setDomain(Stringdomain)`和`getDomain()`方法来设置、获取 domain。 -### 1.6. Cookie 的路径 +### Cookie 的路径 Path 属性决定允许访问 Cookie 的路径。 Java 中使用`setPath(Stringuri)`和`getPath()`方法来设置、获取 path。 -### 1.7. Cookie 的安全属性 +### Cookie 的安全属性 HTTP 协议不仅是无状态的,而且是不安全的。 @@ -101,9 +117,9 @@ HTTP 协议不仅是无状态的,而且是不安全的。 Java 中使用`setSecure(booleanflag)`和`getSecure ()`方法来设置、获取 Secure。 -### 1.8. Cookie 实例 +### Cookie 实例 -#### 1.8.1. 添加 Cookie +#### 添加 Cookie 通过 Servlet 添加 Cookies 包括三个步骤: @@ -200,7 +216,7 @@ addCookies.jsp ``` -#### 1.8.2. 显示 Cookie +#### 显示 Cookie 要读取 Cookies,您需要通过调用 `HttpServletRequest` 的 `getCookies()` 方法创建一个 `javax.servlet.http.Cookie` 对象的数组。然后循环遍历数组,并使用 `getName()` 和 `getValue()` 方法来访问每个 cookie 和关联的值。 @@ -277,7 +293,7 @@ public class ReadCookies extends HttpServlet { } ``` -#### 1.8.3. 删除 Cookie +#### 删除 Cookie Java 中并没有提供直接删除 Cookie 的方法,如果想要删除一个 Cookie,直接将这个 Cookie 的有效期设为 0 就可以了。步骤如下: @@ -359,9 +375,9 @@ public class DeleteCookies extends HttpServlet { } ``` -## 2. Session +## Session -### 2.1. Session 是什么 +### Session 是什么 不同于 Cookie 保存在客户端浏览器中,Session 保存在服务器上。 @@ -369,7 +385,7 @@ public class DeleteCookies extends HttpServlet { Session 对应的类为 `javax.servlet.http.HttpSession` 类。Session 对象是在客户第一次请求服务器时创建的。 -### 2.2. Session 类中的方法 +### Session 类中的方法 `javax.servlet.http.HttpSession` 类中的方法: @@ -387,7 +403,7 @@ Session 对应的类为 `javax.servlet.http.HttpSession` 类。Session 对象是 | public void setAttribute(String name, Object value) | 该方法使用指定的名称绑定一个对象到该 session 会话。 | | public void setMaxInactiveInterval(int interval) | 该方法在 Servlet 容器指示该 session 会话无效之前,指定客户端请求之间的时间,以秒为单位。 | -### 2.3. Session 的有效期 +### Session 的有效期 由于会有越来越多的用户访问服务器,因此 Session 也会越来越多。为防止内存溢出,服务器会把长时间没有活跃的 Session 从内存中删除。 @@ -403,19 +419,19 @@ Tomcat 中 Session 的默认超时时间为 20 分钟。可以修改 web.xml 改 ``` -### 2.4. Session 对浏览器的要求 +### Session 对浏览器的要求 HTTP 协议是无状态的,Session 不能依据 HTTP 连接来判断是否为同一客户。因此服务器向客户端浏览器发送一个名为 JESSIONID 的 Cookie,他的值为该 Session 的 id(也就是 HttpSession.getId()的返回值)。Session 依据该 Cookie 来识别是否为同一用户。 该 Cookie 为服务器自动生成的,它的`maxAge`属性一般为-1,表示仅当前浏览器内有效,并且各浏览器窗口间不共享,关闭浏览器就会失效。 -### 2.5. URL 地址重写 +### URL 地址重写 URL 地址重写的原理是将该用户 Session 的 id 信息重写到 URL 地址中。服务器能够解析重写后的 URL 获取 Session 的 id。这样即使客户端不支持 Cookie,也可以使用 Session 来记录用户状态。 `HttpServletResponse`类提供了`encodeURL(Stringurl)`实现 URL 地址重写。 -### 2.6. Session 中禁用 Cookie +### Session 中禁用 Cookie 在`META-INF/context.xml`中编辑如下: @@ -426,9 +442,9 @@ URL 地址重写的原理是将该用户 Session 的 id 信息重写到 URL 地 部署后,TOMCAT 便不会自动生成名 JESSIONID 的 Cookie,Session 也不会以 Cookie 为识别标志,而仅仅以重写后的 URL 地址为识别标志了。 -### 2.7. Session 实例 +### Session 实例 -#### 2.7.1. Session 跟踪 +#### Session 跟踪 SessionTrackServlet.java @@ -510,7 +526,7 @@ web.xml ``` -#### 2.7.2. 删除 Session 会话数据 +#### 删除 Session 会话数据 当您完成了一个用户的 session 会话数据,您有以下几种选择: @@ -534,33 +550,33 @@ web.xml 在一个 Servlet 中的 `getMaxInactiveInterval()` 方法会返回 session 会话的超时时间,以秒为单位。所以,如果在 web.xml 中配置 session 会话超时时间为 15 分钟,那么`getMaxInactiveInterval()` 会返回 900。 -## 3. Cookie vs Session +## Cookie vs Session -### 3.1. 存取方式 +### 存取方式 Cookie 只能保存`ASCII`字符串,如果需要存取 Unicode 字符或二进制数据,需要进行`UTF-8`、`GBK`或`BASE64`等方式的编码。 Session 可以存取任何类型的数据,甚至是任何 Java 类。可以将 Session 看成是一个 Java 容器类。 -### 3.2. 隐私安全 +### 隐私安全 Cookie 存于客户端浏览器,一些客户端的程序可能会窥探、复制或修改 Cookie 内容。 Session 存于服务器,对客户端是透明的,不存在敏感信息泄露的危险。 -### 3.3. 有效期 +### 有效期 使用 Cookie 可以保证长时间登录有效,只要设置 Cookie 的`maxAge`属性为一个很大的数字。 而 Session 虽然理论上也可以通过设置很大的数值来保持长时间登录有效,但是,由于 Session 依赖于名为`JESSIONID`的 Cookie,而 Cookie `JESSIONID`的`maxAge`默认为-1,只要关闭了浏览器该 Session 就会失效,因此,Session 不能实现信息永久有效的效果。使用 URL 地址重写也不能实现。 -### 3.4. 服务器的开销 +### 服务器的开销 由于 Session 是保存在服务器的,每个用户都会产生一个 Session,如果并发访问的用户非常多,会产生很多的 Session,消耗大量的内存。 而 Cookie 由于保存在客户端浏览器上,所以不占用服务器资源。 -### 3.5. 浏览器的支持 +### 浏览器的支持 Cookie 需要浏览器支持才能使用。 @@ -568,7 +584,7 @@ Cookie 需要浏览器支持才能使用。 需要注意的事所有的用到 Session 程序的 URL 都要使用`response.encodeURL(StringURL)` 或`response.encodeRediretURL(String URL)`进行 URL 地址重写,否则导致 Session 会话跟踪失效。 -### 3.6. 跨域名 +### 跨域名 -* Cookie 支持跨域名。 -* Session 不支持跨域名。 +- Cookie 支持跨域名。 +- Session 不支持跨域名。 \ No newline at end of file diff --git a/docs/javaee/javaee-interview.md "b/docs/01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" similarity index 92% rename from docs/javaee/javaee-interview.md rename to "docs/01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" index 0f1be619..8e803b40 100644 --- a/docs/javaee/javaee-interview.md +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" @@ -1,8 +1,23 @@ -# JavaEE 面经 - -## 1. Servlet - -### 1.1. 什么是 Servlet +--- +title: JavaWeb 面经 +date: 2020-02-07 23:04:47 +order: 99 +categories: + - Java + - JavaEE + - JavaWeb +tags: + - Java + - JavaWeb + - Servlet +permalink: /pages/e175ce/ +--- + +# JavaWeb 面经 + +## Servlet + +### 什么是 Servlet Servlet(Server Applet),即小服务程序或服务连接器。Servlet 是 Java 编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态 Web 内容。 @@ -11,14 +26,14 @@ Servlet(Server Applet),即小服务程序或服务连接器。Servlet 是 Servlet 运行于支持 Java 的应用服务器中。从原理上讲,Servlet 可以响应任何类型的请求,但绝大多数情况下 Servlet 只用来扩展基于 HTTP 协议的 Web 服务器。 -### 1.2. Servlet 和 CGI 的区别 +### Servlet 和 CGI 的区别 Servlet 技术出现之前,Web 主要使用 CGI 技术。它们的区别如下: - Servlet 是基于 Java 编写的,处于服务器进程中,他能够通过多线程方式运行 service() 方法,一个实例可以服务于多个请求,而且一般不会销毁; - CGI(Common Gateway Interface),即通用网关接口。它会为每个请求产生新的进程,服务完成后销毁,所以效率上低于 Servlet。 -### 1.3. Servlet 版本以及主要特性 +### Servlet 版本以及主要特性 | 版本 | 日期 | JAVA EE/JDK 版本 | 特性 | | ----------- | ------------- | ------------------ | --------------------------------------------------------------------- | @@ -33,14 +48,14 @@ Servlet 技术出现之前,Web 主要使用 CGI 技术。它们的区别如下 | Servlet 2.0 | | JDK 1.1 | Part of Java Servlet Development Kit 2.0 | | Servlet 1.0 | 1997 年 6 月 | | | -### 1.4. Servlet 和 JSP 的区别 +### Servlet 和 JSP 的区别 1. Servlet 是一个运行在服务器上的 Java 类,依靠服务器支持向浏览器传输数据。 2. **JSP 本质上就是 Servlet**,每次运行的时候 JSP 都会被编译成 .java 文件,然后再被编译成 .class 文件。 3. 有了 JSP,Servlet 不再负责动态生成页面,转而去负责控制程序逻辑的作用,控制 JSP 与 JavaBean 之间的流转。 4. JSP 侧重于视图,而 Servlet 侧重于控制逻辑,在 MVC 架构模式中,JSP 适合充当视图 View,Servlet 适合充当控制器 Controller。 -### 1.5. 简述 Servlet 生命周期 +### 简述 Servlet 生命周期 ![img](http://www.runoob.com/wp-content/uploads/2014/07/Servlet-LifeCycle.jpg) @@ -52,19 +67,19 @@ Servlet 生命周期如下: 4. **销毁** - Servlet 通过调用 **destroy()** 方法终止(结束)。 5. **卸载** - Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。 -### 1.6. 如何现实 servlet 的单线程模式 +### 如何现实 servlet 的单线程模式 ```java <%@ page isThreadSafe="false" %> ``` -### 1.7. Servlet 中如何获取用户提交的查询参数或者表单数据 +### Servlet 中如何获取用户提交的查询参数或者表单数据 - HttpServletRequest 的 getParameter() 方法。 - HttpServletRequest 的 getParameterValues() 方法。 - HttpServletRequest 的 getParameterMap() 方法。 -### 1.8. request 的主要方法 +### request 的主要方法 - setAttribute(String name,Object):设置名字为 name 的 request 的参数值 - getAttribute(String name):返回由 name 指定的属性值 @@ -90,9 +105,9 @@ Servlet 生命周期如下: - getServerPort():获取服务器的端口号 - removeAttribute(String name):删除请求中的一个属性 -## 2. JSP +## JSP -### 2.1. JSP 的内置对象 +### JSP 的内置对象 1. **request**:包含**客户端请求的信息**; 2. **response**:包含**服务器传回客户端的响应信息**; @@ -104,14 +119,14 @@ Servlet 生命周期如下: 8. **page**:指**网页本身**; 9. **exception**:处理 JSP 文件执行时发生的错误和异常,只要在**错误页面**里才能使用。 -### 2.2. JSP 的作用域 +### JSP 的作用域 1. **page**:一个页面; 2. **request**:一次请求; 3. **session**:一次会话; 4. **application**:服务器从启动到停止。 -### 2.3. JSP 中 7 个动作指令和作用 +### JSP 中 7 个动作指令和作用 1. **jsp:forward** - 执行页面转向,把请求转发到下一个页面; 2. **jsp:param** - 用于传递参数,必须与其他支持参数的标签一起使用; @@ -121,14 +136,14 @@ Servlet 生命周期如下: 6. **jsp:setProperty** - 设置 JavaBean 的属性值; 7. **jsp:getProperty** - 获取 JavaBean 的属性值。 -### 2.4. JSP 中动态 INCLUDE 和静态 INCLUDE 有什么区别 +### JSP 中动态 INCLUDE 和静态 INCLUDE 有什么区别 - **静态 INCLUDE**:用 include 伪码实现,**不会检查所含文件的变化**,适用于包含**静态页面<%@ include file="页面名称.html" %>**。**先合并再编译**。 - **动态 INCLUDE**:用 jsp:include 动作实现 **** 它总是**会检查文件中的变化**,适用于包含**动态页面**,并且可以**带参数**。**先编译再合并**。 -## 3. 原理 +## 原理 -### 3.1. 请求转发(forward)和重定向(redirect)的区别 +### 请求转发(forward)和重定向(redirect)的区别 - 效率上 - 转发(forward) > 重定向(redirect) @@ -142,7 +157,7 @@ Servlet 生命周期如下: - 重定向(redirect)是两次 - 转发(forward)是一次 -### 3.2. get 请求和 post 请求的区别 +### get 请求和 post 请求的区别 ![img](https://upload-images.jianshu.io/upload_images/7779232-5be5ae990207f9d2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/814/format/webp) @@ -157,7 +172,7 @@ Servlet 生命周期如下: - 安全性高 - 请的求的数据内容放置在 HTML HEADER 中 -### 3.3. 用户在浏览器中输入 URL 之后,发什么了什么?写出请求和响应的流程 +### 用户在浏览器中输入 URL 之后,发什么了什么?写出请求和响应的流程 1. 域名解析 2. TCP 三次握手 @@ -169,12 +184,12 @@ Servlet 生命周期如下: 8. 服务器发送数据 9. TCP 连接关闭 -### 3.4. 什么是 Web Service? +### 什么是 Web Service? 1. WebService 就是一个应用程序,它向外界暴露出一个能够通过 Web 进行调用的 API。 2. 它是基于 HTTP 协议传输数据,这使得运行在不同机上的不同应用程序,无须借助附加的、专门的第三方 软件或硬件,就可以相互交换数据或集成。 -### 3.5. 会话跟踪技术有哪些? +### 会话跟踪技术有哪些? 由于 HTTP 协议本身是无状态的,服务器为了区分不同的用户,就需要对用户会话进行跟踪,简单的说就是为用户进行登记,为用户分配唯一的 ID,下一次用户在请求中包含此 ID,服务器根据此判断到底是哪一个用户。 @@ -191,7 +206,7 @@ Servlet 生命周期如下: - 与上面三种方式不同的是,HttpSession 放在服务器的内存中,因此不要将过大的对象放在里面,即使目前的 Servlet 容器可以在内存将满时将 HttpSession 中的对象移到其他存储设备中,但是这样势必影响性能。 - 添加到 HttpSession 中 的值可以是任意 Java 对象,这个对象最好实现了 Serializable 接口,这样 Servlet 容器在必要的时候可以将其序列 化到文件中,否则在序列化时就会出现异常。 -### 3.6. 响应结果状态码有哪些,并给出中文含义? +### 响应结果状态码有哪些,并给出中文含义? - `1**`:信息性状态码 - `2**`:成功状态码 @@ -211,7 +226,7 @@ Servlet 生命周期如下: - 500:服务器端在执行请求时发生了错误 - 503:服务器暂时处于超负载或正在进行停机维护,现在无法处理请求 -### 3.7. XML 文档定义有几种形式?它们之间有何本质区别?解析 XML 文档有哪几种方式? +### XML 文档定义有几种形式?它们之间有何本质区别?解析 XML 文档有哪几种方式? (1)XML 文档有两种约束方式: @@ -248,7 +263,7 @@ Servlet 生命周期如下: - 4)XMLWriter.write("..")-- 写出 - 5)XMLWriter.close()-- 关闭输出流 -## 4. 参考资料 +## 参考资料 - https://blog.csdn.net/YM_IlY/article/details/81266959 -- https://www.jianshu.com/p/f073dde56262 +- https://www.jianshu.com/p/f073dde56262 \ No newline at end of file diff --git a/docs/01.Java/02.JavaEE/01.JavaWeb/README.md b/docs/01.Java/02.JavaEE/01.JavaWeb/README.md new file mode 100644 index 00000000..56a9b42a --- /dev/null +++ b/docs/01.Java/02.JavaEE/01.JavaWeb/README.md @@ -0,0 +1,34 @@ +--- +title: JavaWeb +date: 2020-02-07 23:04:47 +categories: + - Java + - JavaEE + - JavaWeb +tags: + - JavaWeb +permalink: /pages/50f49f/ +hidden: true +index: false +--- + +# ☕ JavaWeb + +## 知识点 + +- [JavaWeb 之 Servlet 指南](01.JavaWeb之Servlet指南.md) +- [JavaWeb 之 Jsp 指南](01.JavaWeb之Servlet指南.md) +- [JavaWeb 之 Filter 和 Listener](03.JavaWeb之Filter和Listener.md) +- [JavaWeb 之 Cookie 和 Session](04.JavaWeb之Cookie和Session.md) +- [JavaWeb 面经](99.JavaWeb面经.md) + +## 学习资料 + +- **书籍** + - [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) + - [Head First Servlets & JSP](https://book.douban.com/subject/1942934/) +- **教程** + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) + - [Servlet 教程](https://www.runoob.com/servlet/servlet-tutorial.html) + - [博客园孤傲苍狼 JavaWeb 学习总结](https://www.cnblogs.com/xdp-gacl/tag/JavaWeb%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/) + - [JSP 教程](https://www.runoob.com/jsp/jsp-tutorial.html) \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" new file mode 100644 index 00000000..05822b5e --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -0,0 +1,886 @@ +--- +title: Tomcat 快速入门 +date: 2022-02-17 22:34:30 +order: 01 +categories: + - Java + - JavaEE + - 服务器 + - Tomcat +tags: + - Java + - JavaWeb + - 服务器 + - Tomcat +permalink: /pages/4a4c02/ +--- + +# Tomcat 快速入门 + +> 🎁 版本说明 +> +> 当前最新版本:Tomcat 8.5.24 +> +> 环境要求:JDK7+ + +## 1. Tomcat 简介 + +### 1.1. Tomcat 是什么 + +Tomcat 是由 Apache 开发的一个 Servlet 容器,实现了对 Servlet 和 JSP 的支持,并提供了作为 Web 服务器的一些特有功能,如 Tomcat 管理和控制平台、安全域管理和 Tomcat 阀等。 + +由于 Tomcat 本身也内含了一个 HTTP 服务器,它也可以被视作一个单独的 Web 服务器。但是,不能将 Tomcat 和 Apache HTTP 服务器混淆,Apache HTTP 服务器是一个用 C 语言实现的 HTTP Web 服务器;这两个 HTTP web server 不是捆绑在一起的。Tomcat 包含了一个配置管理工具,也可以通过编辑 XML 格式的配置文件来进行配置。 + +### 1.2. Tomcat 重要目录 + +- **/bin** - Tomcat 脚本存放目录(如启动、关闭脚本)。 `*.sh` 文件用于 Unix 系统; `*.bat` 文件用于 Windows 系统。 +- **/conf** - Tomcat 配置文件目录。 +- **/logs** - Tomcat 默认日志目录。 +- **/webapps** - webapp 运行的目录。 + +### 1.3. web 工程发布目录结构 + +一般 web 项目路径结构 + +``` +|-- webapp # 站点根目录 + |-- META-INF # META-INF 目录 + | `-- MANIFEST.MF # 配置清单文件 + |-- WEB-INF # WEB-INF 目录 + | |-- classes # class文件目录 + | | |-- *.class # 程序需要的 class 文件 + | | `-- *.xml # 程序需要的 xml 文件 + | |-- lib # 库文件夹 + | | `-- *.jar # 程序需要的 jar 包 + | `-- web.xml # Web应用程序的部署描述文件 + |-- # 自定义的目录 + |-- # 自定义的资源文件 +``` + +- `webapp`:工程发布文件夹。其实每个 war 包都可以视为 webapp 的压缩包。 + +- `META-INF`:META-INF 目录用于存放工程自身相关的一些信息,元文件信息,通常由开发工具,环境自动生成。 + +- `WEB-INF`:Java web 应用的安全目录。所谓安全就是客户端无法访问,只有服务端可以访问的目录。 +- `/WEB-INF/classes`:存放程序所需要的所有 Java class 文件。 + +- `/WEB-INF/lib`:存放程序所需要的所有 jar 文件。 + +- `/WEB-INF/web.xml`:web 应用的部署配置文件。它是工程中最重要的配置文件,它描述了 servlet 和组成应用的其它组件,以及应用初始化参数、安全管理约束等。 + +### 1.4. Tomcat 功能 + +Tomcat 支持的 I/O 模型有: + +- NIO:非阻塞 I/O,采用 Java NIO 类库实现。 +- NIO2:异步 I/O,采用 JDK 7 最新的 NIO2 类库实现。 +- APR:采用 Apache 可移植运行库实现,是 C/C++ 编写的本地库。 + +Tomcat 支持的应用层协议有: + +- HTTP/1.1:这是大部分 Web 应用采用的访问协议。 +- AJP:用于和 Web 服务器集成(如 Apache)。 +- HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。 + +## 2. Tomcat 入门 + +### 2.1. 安装 + +**前提条件** + +Tomcat 8.5 要求 JDK 版本为 1.7 以上。 + +进入 [Tomcat 官方下载地址](https://tomcat.apache.org/download-80.cgi) 选择合适版本下载,并解压到本地。 + +**Windows** + +添加环境变量 `CATALINA_HOME` ,值为 Tomcat 的安装路径。 + +进入安装目录下的 bin 目录,运行 startup.bat 文件,启动 Tomcat + +**Linux / Unix** + +下面的示例以 8.5.24 版本为例,包含了下载、解压、启动操作。 + +```bash +# 下载解压到本地 +wget http://mirrors.hust.edu.cn/apache/tomcat/tomcat-8/v8.5.24/bin/apache-tomcat-8.5.24.tar.gz +tar -zxf apache-tomcat-8.5.24.tar.gz +# 启动 Tomcat +./apache-tomcat-8.5.24/bin/startup.sh +``` + +启动后,访问 `http://localhost:8080` ,可以看到 Tomcat 安装成功的测试页面。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/tools/tomcat/tomcat.png) + +### 2.2. 配置 + +本节将列举一些重要、常见的配置项。详细的 Tomcat8 配置可以参考 [Tomcat 8 配置官方参考文档](http://tomcat.apache.org/tomcat-8.5-doc/config/index.html) 。 + +#### 2.2.1. Server + +> Server 元素表示整个 Catalina servlet 容器。 +> +> 因此,它必须是 `conf/server.xml` 配置文件中的根元素。它的属性代表了整个 servlet 容器的特性。 + +**属性表** + +| 属性 | 描述 | 备注 | +| --------- | ------------------------------------------------------------------------ | -------------------------------------------- | +| className | 这个类必须实现 org.apache.catalina.Server 接口。 | 默认 org.apache.catalina.core.StandardServer | +| address | 服务器等待关机命令的 TCP / IP 地址。如果没有指定地址,则使用 localhost。 | | +| port | 服务器等待关机命令的 TCP / IP 端口号。设置为-1 以禁用关闭端口。 | | +| shutdown | 必须通过 TCP / IP 连接接收到指定端口号的命令字符串,以关闭 Tomcat。 | | + +#### 2.2.2. Service + +> Service 元素表示一个或多个连接器组件的组合,这些组件共享一个用于处理传入请求的引擎组件。Server 中可以有多个 Service。 + +**属性表** + +| 属性 | 描述 | 备注 | +| --------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------- | +| className | 这个类必须实现`org.apache.catalina.Service`接口。 | 默认 `org.apache.catalina.core.StandardService` | +| name | 此服务的显示名称,如果您使用标准 Catalina 组件,将包含在日志消息中。与特定服务器关联的每个服务的名称必须是唯一的。 | | + +**实例 - `conf/server.xml` 配置文件示例** + +```xml + + + + ... + + +``` + +#### 2.2.3. Executor + +> Executor 表示可以在 Tomcat 中的组件之间共享的线程池。 + +**属性表** + +| 属性 | 描述 | 备注 | +| --------------- | ---------------------------------------------------------------- | ------------------------------------------------------ | +| className | 这个类必须实现`org.apache.catalina.Executor`接口。 | 默认 `org.apache.catalina.core.StandardThreadExecutor` | +| name | 线程池名称。 | 要求唯一, 供 Connector 元素的 executor 属性使用 | +| namePrefix | 线程名称前缀。 | | +| maxThreads | 最大活跃线程数。 | 默认 200 | +| minSpareThreads | 最小活跃线程数。 | 默认 25 | +| maxIdleTime | 当前活跃线程大于 minSpareThreads 时,空闲线程关闭的等待最大时间。 | 默认 60000ms | +| maxQueueSize | 线程池满情况下的请求排队大小。 | 默认 Integer.MAX_VALUE | + +```xml + + + +``` + +#### 2.2.4. Connector + +> Connector 代表连接组件。Tomcat 支持三种协议:HTTP/1.1、HTTP/2.0、AJP。 + +**属性表** + +| 属性 | 说明 | 备注 | +| --------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| asyncTimeout | Servlet3.0 规范中的异步请求超时 | 默认 30s | +| port | 请求连接的 TCP Port | 设置为 0,则会随机选取一个未占用的端口号 | +| protocol | 协议. 一般情况下设置为 HTTP/1.1,这种情况下连接模型会在 NIO 和 APR/native 中自动根据配置选择 | | +| URIEncoding | 对 URI 的编码方式. | 如果设置系统变量 org.apache.catalina.STRICT_SERVLET_COMPLIANCE 为 true,使用 ISO-8859-1 编码;如果未设置此系统变量且未设置此属性, 使用 UTF-8 编码 | +| useBodyEncodingForURI | 是否采用指定的 contentType 而不是 URIEncoding 来编码 URI 中的请求参数 | | + +以下属性在标准的 Connector(NIO, NIO2 和 APR/native)中有效: + +| 属性 | 说明 | 备注 | +| ----------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| acceptCount | 当最大请求连接 maxConnections 满时的最大排队大小 | 默认 100,注意此属性和 Executor 中属性 maxQueueSize 的区别.这个指的是请求连接满时的堆栈大小,Executor 的 maxQueueSize 指的是处理线程满时的堆栈大小 | +| connectionTimeout | 请求连接超时 | 默认 60000ms | +| executor | 指定配置的线程池名称 | | +| keepAliveTimeout | keeAlive 超时时间 | 默认值为 connectionTimeout 配置值.-1 表示不超时 | +| maxConnections | 最大连接数 | 连接满时后续连接放入最大为 acceptCount 的队列中. 对 NIO 和 NIO2 连接,默认值为 10000;对 APR/native,默认值为 8192 | +| maxThreads | 如果指定了 Executor, 此属性忽略;否则为 Connector 创建的内部线程池最大值 | 默认 200 | +| minSpareThreads | 如果指定了 Executor, 此属性忽略;否则为 Connector 创建线程池的最小活跃线程数 | 默认 10 | +| processorCache | 协议处理器缓存 Processor 对象的大小 | -1 表示不限制.当不使用 servlet3.0 的异步处理情况下: 如果配置 Executor,配置为 Executor 的 maxThreads;否则配置为 Connnector 的 maxThreads. 如果使用 Serlvet3.0 异步处理, 取 maxThreads 和 maxConnections 的最大值 | + +#### 2.2.5. Context + +> Context 元素表示一个 Web 应用程序,它在特定的虚拟主机中运行。每个 Web 应用程序都基于 Web 应用程序存档(WAR)文件,或者包含相应的解包内容的相应目录,如 Servlet 规范中所述。 + +**属性表** + +| 属性 | 说明 | 备注 | +| -------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------- | +| altDDName | web.xml 部署描述符路径 | 默认 /WEB-INF/web.xml | +| docBase | Context 的 Root 路径 | 和 Host 的 appBase 相结合, 可确定 web 应用的实际目录 | +| failCtxIfServletStartFails | 同 Host 中的 failCtxIfServletStartFails, 只对当前 Context 有效 | 默认为 false | +| logEffectiveWebXml | 是否日志打印 web.xml 内容(web.xml 由默认的 web.xml 和应用中的 web.xml 组成) | 默认为 false | +| path | web 应用的 context path | 如果为根路径,则配置为空字符串(""), 不能不配置 | +| privileged | 是否使用 Tomcat 提供的 manager servlet | | +| reloadable | /WEB-INF/classes/ 和/WEB-INF/lib/ 目录中 class 文件发生变化是否自动重新加载 | 默认为 false | +| swallowOutput | true 情况下, System.out 和 System.err 输出将被定向到 web 应用日志中 | 默认为 false | + +#### 2.2.6. Engine + +> Engine 元素表示与特定的 Catalina 服务相关联的整个请求处理机器。它接收并处理来自一个或多个连接器的所有请求,并将完成的响应返回给连接器,以便最终传输回客户端。 + +**属性表** + +| 属性 | 描述 | 备注 | +| ----------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | +| defaultHost | 默认主机名,用于标识将处理指向此服务器上主机名称但未在此配置文件中配置的请求的主机。 | 这个名字必须匹配其中一个嵌套的主机元素的名字属性。 | +| name | 此引擎的逻辑名称,用于日志和错误消息。 | 在同一服务器中使用多个服务元素时,每个引擎必须分配一个唯一的名称。 | + +#### 2.2.7. Host + +> Host 元素表示一个虚拟主机,它是一个服务器的网络名称(如“www.mycompany.com”)与运行 Tomcat 的特定服务器的关联。 + +**属性表** + +| 属性 | 说明 | 备注 | +| -------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------- | +| name | 名称 | 用于日志输出 | +| appBase | 虚拟主机对应的应用基础路径 | 可以是个绝对路径, 或\${CATALINA_BASE}相对路径 | +| xmlBase | 虚拟主机 XML 基础路径,里面应该有 Context xml 配置文件 | 可以是个绝对路径, 或\${CATALINA_BASE}相对路径 | +| createDirs | 当 appBase 和 xmlBase 不存在时,是否创建目录 | 默认为 true | +| autoDeploy | 是否周期性的检查 appBase 和 xmlBase 并 deploy web 应用和 context 描述符 | 默认为 true | +| deployIgnore | 忽略 deploy 的正则 | | +| deployOnStartup | Tomcat 启动时是否自动 deploy | 默认为 true | +| failCtxIfServletStartFails | 配置为 true 情况下,任何 load-on-startup >=0 的 servlet 启动失败,则其对应的 Contxt 也启动失败 | 默认为 false | + +#### 2.2.8. Cluster + +由于在实际开发中,我从未用过 Tomcat 集群配置,所以没研究。 + +### 2.3. 启动 + +#### 2.3.1. 部署方式 + +这种方式要求本地必须安装 Tomcat 。 + +将打包好的 war 包放在 Tomcat 安装目录下的 `webapps` 目录下,然后在 bin 目录下执行 `startup.bat` 或 `startup.sh` ,Tomcat 会自动解压 `webapps` 目录下的 war 包。 + +成功后,可以访问 `http://localhost:8080/xxx` (xxx 是 war 包文件名)。 + +> **注意** +> +> 以上步骤是最简单的示例。步骤中的 war 包解压路径、启动端口以及一些更多的功能都可以修改配置文件来定制 (主要是 `server.xml` 或 `context.xml` 文件)。 + +#### 2.3.2. 嵌入式 + +##### 2.3.2.1. API 方式 + +在 pom.xml 中添加依赖 + +```xml + + org.apache.tomcat.embed + tomcat-embed-core + 8.5.24 + +``` + +添加 SimpleEmbedTomcatServer.java 文件,内容如下: + +```java +import java.util.Optional; +import org.apache.catalina.startup.Tomcat; + +public class SimpleTomcatServer { + private static final int PORT = 8080; + private static final String CONTEXT_PATH = "/javatool-server"; + + public static void main(String[] args) throws Exception { + // 设定 profile + Optional profile = Optional.ofNullable(System.getProperty("spring.profiles.active")); + System.setProperty("spring.profiles.active", profile.orElse("develop")); + + Tomcat tomcat = new Tomcat(); + tomcat.setPort(PORT); + tomcat.getHost().setAppBase("."); + tomcat.addWebapp(CONTEXT_PATH, getAbsolutePath() + "src/main/webapp"); + tomcat.start(); + tomcat.getServer().await(); + } + + private static String getAbsolutePath() { + String path = null; + String folderPath = SimpleEmbedTomcatServer.class.getProtectionDomain().getCodeSource().getLocation().getPath() + .substring(1); + if (folderPath.indexOf("target") > 0) { + path = folderPath.substring(0, folderPath.indexOf("target")); + } + return path; + } +} +``` + +成功后,可以访问 `http://localhost:8080/javatool-server` 。 + +> **说明** +> +> 本示例是使用 `org.apache.tomcat.embed` 启动嵌入式 Tomcat 的最简示例。 +> +> 这个示例中使用的是 Tomcat 默认的配置,但通常,我们需要对 Tomcat 配置进行一些定制和调优。为了加载配置文件,启动类就要稍微再复杂一些。这里不想再贴代码,有兴趣的同学可以参考: +> +> [**示例项目**](https://github.com/dunwu/JavaStack/tree/master/codes/javatool/server) + +##### 2.3.2.2. 使用 maven 插件启动(不推荐) + +不推荐理由:这种方式启动 maven 虽然最简单,但是有一个很大的问题是,真的很久很久没发布新版本了(最新版本发布时间:2013-11-11)。且貌似只能找到 Tomcat6 、Tomcat7 插件。 + +**使用方法** + +在 pom.xml 中引入插件 + +```xml + + org.apache.tomcat.maven + tomcat7-maven-plugin + 2.2 + + 8080 + /${project.artifactId} + UTF-8 + + +``` + +运行 `mvn tomcat7:run` 命令,启动 Tomcat。 + +成功后,可以访问 `http://localhost:8080/xxx` (xxx 是 \${project.artifactId} 指定的项目名)。 + +#### 2.3.3. IDE 插件 + +常见 Java IDE 一般都有对 Tomcat 的支持。 + +以 Intellij IDEA 为例,提供了 **Tomcat and TomEE Integration** 插件(一般默认会安装)。 + +**使用步骤** + +- 点击 Run/Debug Configurations > New Tomcat Server > local ,打开 Tomcat 配置页面。 +- 点击 Confiure... 按钮,设置 Tomcat 安装路径。 +- 点击 Deployment 标签页,设置要启动的应用。 +- 设置启动应用的端口、JVM 参数、启动浏览器等。 +- 成功后,可以访问 `http://localhost:8080/`(当然,你也可以在 url 中设置上下文名称)。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/tools/tomcat/tomcat-intellij-run-config.png) + +> **说明** +> +> 个人认为这个插件不如 Eclipse 的 Tomcat 插件好用,Eclipse 的 Tomcat 插件支持对 Tomcat xml 配置文件进行配置。而这里,你只能自己去 Tomcat 安装路径下修改配置文件。 + +文中的嵌入式启动示例可以参考[**我的示例项目**](https://github.com/dunwu/JavaStack/tree/master/codes/javatool/server) + +## 3. Tomcat 架构 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201113193431.png) + +Tomcat 要实现 2 个核心功能: + +- **处理 Socket 连接**,负责网络字节流与 Request 和 Response 对象的转化。 +- **加载和管理 Servlet**,以及**处理具体的 Request 请求**。 + +为此,Tomcat 设计了两个核心组件: + +- **连接器(Connector)**:负责和外部通信 +- **容器(Container)**:负责内部处理 + +### 3.1. Service + +Tomcat 支持的 I/O 模型有: + +- NIO:非阻塞 I/O,采用 Java NIO 类库实现。 +- NIO2:异步 I/O,采用 JDK 7 最新的 NIO2 类库实现。 +- APR:采用 Apache 可移植运行库实现,是 C/C++ 编写的本地库。 + +Tomcat 支持的应用层协议有: + +- HTTP/1.1:这是大部分 Web 应用采用的访问协议。 +- AJP:用于和 Web 服务器集成(如 Apache)。 +- HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。 + +Tomcat 支持多种 I/O 模型和应用层协议。为了实现这点,一个容器可能对接多个连接器。但是,单独的连接器或容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作 Service 组件。Tomcat 内可能有多个 Service,通过在 Tomcat 中配置多个 Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201111093124.png) + +**一个 Tomcat 实例有一个或多个 Service;一个 Service 有多个 Connector 和 Container**。Connector 和 Container 之间通过标准的 ServletRequest 和 ServletResponse 通信。 + +### 3.2. 连接器 + +连接器对 Servlet 容器屏蔽了协议及 I/O 模型等的区别,无论是 HTTP 还是 AJP,在容器中获取到的都是一个标准的 ServletRequest 对象。 + +连接器的主要功能是: + +- 网络通信 +- 应用层协议解析 +- Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化 + +Tomcat 设计了 3 个组件来实现这 3 个功能,分别是 **`EndPoint`**、**`Processor`** 和 **`Adapter`**。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201111101440.png) + +组件间通过抽象接口交互。这样做还有一个好处是**封装变化。**这是面向对象设计的精髓,将系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。网络通信的 I/O 模型是变化的,可能是非阻塞 I/O、异步 I/O 或者 APR。应用层协议也是变化的,可能是 HTTP、HTTPS、AJP。浏览器端发送的请求信息也是变化的。但是整体的处理逻辑是不变的,EndPoint 负责提供字节流给 Processor,Processor 负责提供 Tomcat Request 对象给 Adapter,Adapter 负责提供 ServletRequest 对象给容器。 + +如果要支持新的 I/O 方案、新的应用层协议,只需要实现相关的具体子类,上层通用的处理逻辑是不变的。由于 I/O 模型和应用层协议可以自由组合,比如 NIO + HTTP 或者 NIO2 + AJP。Tomcat 的设计者将网络通信和应用层协议解析放在一起考虑,设计了一个叫 ProtocolHandler 的接口来封装这两种变化点。各种协议和通信模型的组合有相应的具体实现类。比如:Http11NioProtocol 和 AjpNioProtocol。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201027091819.png) + +#### 3.2.1. ProtocolHandler 组件 + +**连接器用 ProtocolHandler 接口来封装通信协议和 I/O 模型的差异**。ProtocolHandler 内部又分为 EndPoint 和 Processor 模块,EndPoint 负责底层 Socket 通信,Proccesor 负责应用层协议解析。 + +##### 3.2.1.1. EndPoint + +EndPoint 是通信端点,即通信监听的接口,是具体的 Socket 接收和发送处理器,是对传输层的抽象,因此 EndPoint 是用来实现 TCP/IP 协议的。 + +EndPoint 是一个接口,对应的抽象实现类是 AbstractEndpoint,而 AbstractEndpoint 的具体子类,比如在 NioEndpoint 和 Nio2Endpoint 中,有两个重要的子组件:Acceptor 和 SocketProcessor。 + +其中 Acceptor 用于监听 Socket 连接请求。SocketProcessor 用于处理接收到的 Socket 请求,它实现 Runnable 接口,在 Run 方法里调用协议处理组件 Processor 进行处理。为了提高处理能力,SocketProcessor 被提交到线程池来执行。而这个线程池叫作执行器(Executor)。 + +##### 3.2.1.2. Processor + +如果说 EndPoint 是用来实现 TCP/IP 协议的,那么 Processor 用来实现 HTTP 协议,Processor 接收来自 EndPoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象。 + +Processor 是一个接口,定义了请求的处理等方法。它的抽象实现类 AbstractProcessor 对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有 AJPProcessor、HTTP11Processor 等,这些具体实现类实现了特定协议的解析方法和请求处理方式。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201113185929.png) + +从图中我们看到,EndPoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池去处理,SocketProcessor 的 Run 方法会调用 Processor 组件去解析应用层协议,Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 方法。 + +#### 3.2.2. Adapter + +**连接器通过适配器 Adapter 调用容器**。 + +由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat 定义了自己的 Request 类来适配这些请求信息。 + +ProtocolHandler 接口负责解析请求并生成 Tomcat Request 类。但是这个 Request 对象不是标准的 ServletRequest,也就意味着,不能用 Tomcat Request 作为参数来调用容器。Tomcat 的解决方案是引入 CoyoteAdapter,这是适配器模式的经典运用,连接器调用 CoyoteAdapter 的 Sevice 方法,传入的是 Tomcat Request 对象,CoyoteAdapter 负责将 Tomcat Request 转成 ServletRequest,再调用容器的 Service 方法。 + +### 3.3. 容器 + +Tomcat 设计了 4 种容器,分别是 Engine、Host、Context 和 Wrapper。 + +- **Engine** - Servlet 的顶层容器,包含一 个或多个 Host 子容器; +- **Host** - 虚拟主机,负责 web 应用的部署和 Context 的创建; +- **Context** - Web 应用上下文,包含多个 Wrapper,负责 web 配置的解析、管理所有的 Web 资源; +- **Wrapper** - 最底层的容器,是对 Servlet 的封装,负责 Servlet 实例的创 建、执行和销毁。 + +#### 3.3.1. 请求分发 Servlet 过程 + +Tomcat 是怎么确定请求是由哪个 Wrapper 容器里的 Servlet 来处理的呢?答案是,Tomcat 是用 Mapper 组件来完成这个任务的。 + +举例来说,假如有一个网购系统,有面向网站管理人员的后台管理系统,还有面向终端客户的在线购物系统。这两个系统跑在同一个 Tomcat 上,为了隔离它们的访问域名,配置了两个虚拟域名:`manage.shopping.com`和`user.shopping.com`,网站管理人员通过`manage.shopping.com`域名访问 Tomcat 去管理用户和商品,而用户管理和商品管理是两个单独的 Web 应用。终端客户通过`user.shopping.com`域名去搜索商品和下订单,搜索功能和订单管理也是两个独立的 Web 应用。如下所示,演示了 url 应声 Servlet 的处理流程。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201113192022.jpg) + +假如有用户访问一个 URL,比如图中的`http://user.shopping.com:8080/order/buy`,Tomcat 如何将这个 URL 定位到一个 Servlet 呢? + +1. **首先,根据协议和端口号选定 Service 和 Engine。** +2. **然后,根据域名选定 Host。** +3. **之后,根据 URL 路径找到 Context 组件。** +4. **最后,根据 URL 路径找到 Wrapper(Servlet)。** + +这个路由分发过程具体是怎么实现的呢?答案是使用 Pipeline-Valve 管道。 + +#### 3.3.2. Pipeline-Value + +Pipeline 可以理解为现实中的管道,Valve 为管道中的阀门,Request 和 Response 对象在管道中经过各个阀门的处理和控制。 + +Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。Valve 表示一个处理点,比如权限认证和记录日志。 + +先来了解一下 Valve 和 Pipeline 接口的设计: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/tools/tomcat/Pipeline与Valve.png) + +- 每一个容器都有一个 Pipeline 对象,只要触发这个 Pipeline 的第一个 Valve,这个容器里 Pipeline 中的 Valve 就都会被调用到。但是,不同容器的 Pipeline 是怎么链式触发的呢,比如 Engine 中 Pipeline 需要调用下层容器 Host 中的 Pipeline。 +- 这是因为 Pipeline 中还有个 getBasic 方法。这个 BasicValve 处于 Valve 链表的末端,它是 Pipeline 中必不可少的一个 Valve,负责调用下层容器的 Pipeline 里的第一个 Valve。 +- Pipeline 中有 addValve 方法。Pipeline 中维护了 Valve 链表,Valve 可以插入到 Pipeline 中,对请求做某些处理。我们还发现 Pipeline 中没有 invoke 方法,因为整个调用链的触发是 Valve 来完成的,Valve 完成自己的处理后,调用 `getNext.invoke()` 来触发下一个 Valve 调用。 +- Valve 中主要的三个方法:`setNext`、`getNext`、`invoke`。Valve 之间的关系是单向链式结构,本身 `invoke` 方法中会调用下一个 Valve 的 `invoke` 方法。 +- 各层容器对应的 basic valve 分别是 `StandardEngineValve`、`StandardHostValve`、 `StandardContextValve`、`StandardWrapperValve`。 +- 由于 Valve 是一个处理点,因此 invoke 方法就是来处理请求的。注意到 Valve 中有 getNext 和 setNext 方法,因此我们大概可以猜到有一个链表将 Valve 链起来了。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/tools/tomcat/请求处理过程.png) + +整个调用过程由连接器中的 Adapter 触发的,它会调用 Engine 的第一个 Valve: + +```java +connector.getService().getContainer().getPipeline().getFirst().invoke(request, response); +``` + +## 4. Tomcat 生命周期 + +### 4.1. Tomcat 的启动过程 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201118145455.png) + +1. Tomcat 是一个 Java 程序,它的运行从执行 `startup.sh` 脚本开始。`startup.sh` 会启动一个 JVM 来运行 Tomcat 的启动类 `Bootstrap`。 +2. `Bootstrap` 会初始化 Tomcat 的类加载器并实例化 `Catalina`。 +3. `Catalina` 会通过 Digester 解析 `server.xml`,根据其中的配置信息来创建相应组件,并调用 `Server` 的 `start` 方法。 +4. `Server` 负责管理 `Service` 组件,它会调用 `Service` 的 `start` 方法。 +5. `Service` 负责管理 `Connector` 和顶层容器 `Engine`,它会调用 `Connector` 和 `Engine` 的 `start` 方法。 + +#### 4.1.1. Catalina 组件 + +Catalina 的职责就是解析 server.xml 配置,并据此实例化 Server。接下来,调用 Server 组件的 init 方法和 start 方法,将 Tomcat 启动起来。 + +Catalina 还需要处理各种“异常”情况,比如当我们通过“Ctrl + C”关闭 Tomcat 时,Tomcat 将如何优雅的停止并且清理资源呢?因此 Catalina 在 JVM 中注册一个“关闭钩子”。 + +```java +public void start() { + //1. 如果持有的 Server 实例为空,就解析 server.xml 创建出来 + if (getServer() == null) { + load(); + } + + //2. 如果创建失败,报错退出 + if (getServer() == null) { + log.fatal(sm.getString("catalina.noServer")); + return; + } + + //3. 启动 Server + try { + getServer().start(); + } catch (LifecycleException e) { + return; + } + + // 创建并注册关闭钩子 + if (useShutdownHook) { + if (shutdownHook == null) { + shutdownHook = new CatalinaShutdownHook(); + } + Runtime.getRuntime().addShutdownHook(shutdownHook); + } + + // 用 await 方法监听停止请求 + if (await) { + await(); + stop(); + } +} +``` + +为什么需要关闭钩子? + +如果我们需要在 JVM 关闭时做一些清理工作,比如将缓存数据刷到磁盘上,或者清理一些临时文件,可以向 JVM 注册一个“关闭钩子”。“关闭钩子”其实就是一个线程,JVM 在停止之前会尝试执行这个线程的 `run` 方法。 + +Tomcat 的“关闭钩子”—— `CatalinaShutdownHook` 做了些什么呢? + +```java +protected class CatalinaShutdownHook extends Thread { + + @Override + public void run() { + try { + if (getServer() != null) { + Catalina.this.stop(); + } + } catch (Throwable ex) { + ... + } + } +} +``` + +Tomcat 的“关闭钩子”实际上就执行了 `Server` 的 `stop` 方法,`Server` 的 `stop` 方法会释放和清理所有的资源。 + +#### 4.1.2. Server 组件 + +Server 组件的具体实现类是 StandardServer,Server 继承了 LifeCycleBase,它的生命周期被统一管理,并且它的子组件是 Service,因此它还需要管理 Service 的生命周期,也就是说在启动时调用 Service 组件的启动方法,在停止时调用它们的停止方法。Server 在内部维护了若干 Service 组件,它是以数组来保存的。 + +```java +@Override +public void addService(Service service) { + + service.setServer(this); + + synchronized (servicesLock) { + // 创建一个长度 +1 的新数组 + Service results[] = new Service[services.length + 1]; + + // 将老的数据复制过去 + System.arraycopy(services, 0, results, 0, services.length); + results[services.length] = service; + services = results; + + // 启动 Service 组件 + if (getState().isAvailable()) { + try { + service.start(); + } catch (LifecycleException e) { + // Ignore + } + } + + // 触发监听事件 + support.firePropertyChange("service", null, service); + } + +} +``` + +Server 并没有一开始就分配一个很长的数组,而是在添加的过程中动态地扩展数组长度,当添加一个新的 Service 实例时,会创建一个新数组并把原来数组内容复制到新数组,这样做的目的其实是为了节省内存空间。 + +除此之外,Server 组件还有一个重要的任务是启动一个 Socket 来监听停止端口,这就是为什么你能通过 shutdown 命令来关闭 Tomcat。不知道你留意到没有,上面 Caralina 的启动方法的最后一行代码就是调用了 Server 的 await 方法。 + +在 await 方法里会创建一个 Socket 监听 8005 端口,并在一个死循环里接收 Socket 上的连接请求,如果有新的连接到来就建立连接,然后从 Socket 中读取数据;如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入 stop 流程。 + +#### 4.1.3. Service 组件 + +Service 组件的具体实现类是 StandardService。 + +【源码】StandardService 源码定义 + +```java +public class StandardService extends LifecycleBase implements Service { + // 名字 + private String name = null; + + //Server 实例 + private Server server = null; + + // 连接器数组 + protected Connector connectors[] = new Connector[0]; + private final Object connectorsLock = new Object(); + + // 对应的 Engine 容器 + private Engine engine = null; + + // 映射器及其监听器 + protected final Mapper mapper = new Mapper(); + protected final MapperListener mapperListener = new MapperListener(this); + + // ... +} +``` + +StandardService 继承了 LifecycleBase 抽象类。 + +StandardService 维护了一个 MapperListener 用于支持 Tomcat 热部署。当 Web 应用的部署发生变化时,Mapper 中的映射信息也要跟着变化,MapperListener 就是一个监听器,它监听容器的变化,并把信息更新到 Mapper 中,这是典型的观察者模式。 + +作为“管理”角色的组件,最重要的是维护其他组件的生命周期。此外在启动各种组件时,要注意它们的依赖关系,也就是说,要注意启动的顺序。 + +```java +protected void startInternal() throws LifecycleException { + + //1. 触发启动监听器 + setState(LifecycleState.STARTING); + + //2. 先启动 Engine,Engine 会启动它子容器 + if (engine != null) { + synchronized (engine) { + engine.start(); + } + } + + //3. 再启动 Mapper 监听器 + mapperListener.start(); + + //4. 最后启动连接器,连接器会启动它子组件,比如 Endpoint + synchronized (connectorsLock) { + for (Connector connector: connectors) { + if (connector.getState() != LifecycleState.FAILED) { + connector.start(); + } + } + } +} +``` + +从启动方法可以看到,Service 先启动了 Engine 组件,再启动 Mapper 监听器,最后才是启动连接器。这很好理解,因为内层组件启动好了才能对外提供服务,才能启动外层的连接器组件。而 Mapper 也依赖容器组件,容器组件启动好了才能监听它们的变化,因此 Mapper 和 MapperListener 在容器组件之后启动。组件停止的顺序跟启动顺序正好相反的,也是基于它们的依赖关系。 + +#### 4.1.4. Engine 组件 + +Engine 本质是一个容器,因此它继承了 ContainerBase 基类,并且实现了 Engine 接口。 + +### 4.2. Web 应用的部署方式 + +注:catalina.home:安装目录;catalina.base:工作目录;默认值 user.dir + +- Server.xml 配置 Host 元素,指定 appBase 属性,默认\$catalina.base/webapps/ +- Server.xml 配置 Context 元素,指定 docBase,元素,指定 web 应用的路径 +- 自定义配置:在\$catalina.base/EngineName/HostName/XXX.xml 配置 Context 元素 + +HostConfig 监听了 StandardHost 容器的事件,在 start 方法中解析上述配置文件: + +- 扫描 appbase 路径下的所有文件夹和 war 包,解析各个应用的 META-INF/context.xml,并 创建 StandardContext,并将 Context 加入到 Host 的子容器中。 +- 解析\$catalina.base/EngineName/HostName/下的所有 Context 配置,找到相应 web 应 用的位置,解析各个应用的 META-INF/context.xml,并创建 StandardContext,并将 Context 加入到 Host 的子容器中。 + +注: + +- HostConfig 并没有实际解析 Context.xml,而是在 ContextConfig 中进行的。 +- HostConfig 中会定期检查 watched 资源文件(context.xml 配置文件) + +ContextConfig 解析 context.xml 顺序: + +- 先解析全局的配置 config/context.xml +- 然后解析 Host 的默认配置 EngineName/HostName/context.xml.default +- 最后解析应用的 META-INF/context.xml + +ContextConfig 解析 web.xml 顺序: + +- 先解析全局的配置 config/web.xml +- 然后解析 Host 的默认配置 EngineName/HostName/web.xml.default 接着解析应用的 MEB-INF/web.xml +- 扫描应用 WEB-INF/lib/下的 jar 文件,解析其中的 META-INF/web-fragment.xml 最后合并 xml 封装成 WebXml,并设置 Context + +注: + +- 扫描 web 应用和 jar 中的注解(Filter、Listener、Servlet)就是上述步骤中进行的。 +- 容器的定期执行:backgroundProcess,由 ContainerBase 来实现的,并且只有在顶层容器 中才会开启线程。(backgroundProcessorDelay=10 标志位来控制) + +### 4.3. LifeCycle + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201118105012.png) + +#### 4.3.1. 请求处理过程 + +
+ +
+ +1. 根据 server.xml 配置的指定的 connector 以及端口监听 http、或者 ajp 请求 +2. 请求到来时建立连接,解析请求参数,创建 Request 和 Response 对象,调用顶层容器 pipeline 的 invoke 方法 +3. 容器之间层层调用,最终调用业务 servlet 的 service 方法 +4. Connector 将 response 流中的数据写到 socket 中 + +### 4.4. Connector 流程 + +
+ +
+ +#### 4.4.1. 阻塞 IO + +
+ +
+ +#### 4.4.2. 非阻塞 IO + +
+ +
+ +#### 4.4.3. IO 多路复用 + +
+ +
+ +阻塞与非阻塞的区别在于进行读操作和写操作的系统调用时,如果此时内核态没有数据可读或者没有缓冲空间可写时,是否阻塞。 + +IO 多路复用的好处在于可同时监听多个 socket 的可读和可写事件,这样就能使得应用可以同时监听多个 socket,释放了应用线程资源。 + +#### 4.4.4. Tomcat 各类 Connector 对比 + +
+ +
+ +- JIO:用 java.io 编写的 TCP 模块,阻塞 IO +- NIO:用 java.nio 编写的 TCP 模块,非阻塞 IO,(IO 多路复用) +- APR:全称 Apache Portable Runtime,使用 JNI 的方式来进行读取文件以及进行网络传输 + +Apache Portable Runtime 是一个高度可移植的库,它是 Apache HTTP Server 2.x 的核心。 APR 具有许多用途,包括访问高级 IO 功能(如 sendfile,epoll 和 OpenSSL),操作系统级功能(随机数生成,系统状态等)和本地进程处理(共享内存,NT 管道和 Unix 套接字)。 + +表格中字段含义说明: + +- Support Polling - 是否支持基于 IO 多路复用的 socket 事件轮询 +- Polling Size - 轮询的最大连接数 +- Wait for next Request - 在等待下一个请求时,处理线程是否释放,BIO 是没有释放的,所以在 keep-alive=true 的情况下处理的并发连接数有限 +- Read Request Headers - 由于 request header 数据较少,可以由容器提前解析完毕,不需要阻塞 +- Read Request Body - 读取 request body 的数据是应用业务逻辑的事情,同时 Servlet 的限制,是需要阻塞读取的 +- Write Response - 跟读取 request body 的逻辑类似,同样需要阻塞写 + +**NIO 处理相关类** + +
+ +
+ +Poller 线程从 EventQueue 获取 PollerEvent,并执行 PollerEvent 的 run 方法,调用 Selector 的 select 方法,如果有可读的 Socket 则创建 Http11NioProcessor,放入到线程池中执行; + +CoyoteAdapter 是 Connector 到 Container 的适配器,Http11NioProcessor 调用其提供的 service 方法,内部创建 Request 和 Response 对象,并调用最顶层容器的 Pipeline 中的第一个 Valve 的 invoke 方法 + +Mapper 主要处理 http url 到 servlet 的映射规则的解析,对外提供 map 方法 + +### 4.5. Comet + +Comet 是一种用于 web 的推送技术,能使服务器实时地将更新的信息传送到客户端,而无须客户端发出请求 +在 WebSocket 出来之前,如果不适用 comet,只能通过浏览器端轮询 Server 来模拟实现服务器端推送。 +Comet 支持 servlet 异步处理 IO,当连接上数据可读时触发事件,并异步写数据(阻塞) + +Tomcat 要实现 Comet,只需继承 HttpServlet 同时,实现 CometProcessor 接口 + +- Begin:新的请求连接接入调用,可进行与 Request 和 Response 相关的对象初始化操作,并保存 response 对象,用于后续写入数据 +- Read:请求连接有数据可读时调用 +- End:当数据可用时,如果读取到文件结束或者 response 被关闭时则被调用 +- Error:在连接上发生异常时调用,数据读取异常、连接断开、处理异常、socket 超时 + +Note: + +- Read:在 post 请求有数据,但在 begin 事件中没有处理,则会调用 read,如果 read 没有读取数据,在会触发 Error 回调,关闭 socket +- End:当 socket 超时,并且 response 被关闭时也会调用;server 被关闭时调用 +- Error:除了 socket 超时不会关闭 socket,其他都会关闭 socket +- End 和 Error 时间触发时应关闭当前 comet 会话,即调用 CometEvent 的 close 方法 + Note:在事件触发时要做好线程安全的操作 + +### 4.6. 异步 Servlet + +
+ +
+ +传统流程: + +- 首先,Servlet 接收到请求之后,request 数据解析; +- 接着,调用业务接口的某些方法,以完成业务处理; +- 最后,根据处理的结果提交响应,Servlet 线程结束 + +
+ +
+ +异步处理流程: + +- 客户端发送一个请求 +- Servlet 容器分配一个线程来处理容器中的一个 servlet +- servlet 调用 request.startAsync(),保存 AsyncContext, 然后返回 +- 任何方式存在的容器线程都将退出,但是 response 仍然保持开放 +- 业务线程使用保存的 AsyncContext 来完成响应(线程池) +- 客户端收到响应 + +Servlet 线程将请求转交给一个异步线程来执行业务处理,线程本身返回至容器,此时 Servlet 还没有生成响应数据,异步线程处理完业务以后,可以直接生成响应数据(异步线程拥有 ServletRequest 和 ServletResponse 对象的引用) + +**为什么 web 应用中支持异步?** + +推出异步,主要是针对那些比较耗时的请求:比如一次缓慢的数据库查询,一次外部 REST API 调用, 或者是其他一些 I/O 密集型操作。这种耗时的请求会很快的耗光 Servlet 容器的线程池,继而影响可扩展性。 + +Note:从客户端的角度来看,request 仍然像任何其他的 HTTP 的 request-response 交互一样,只是耗费了更长的时间而已 + +**异步事件监听** + +- onStartAsync:Request 调用 startAsync 方法时触发 +- onComplete:syncContext 调用 complete 方法时触发 +- onError:处理请求的过程出现异常时触发 +- onTimeout:socket 超时触发 + +Note : +onError/ onTimeout 触发后,会紧接着回调 onComplete +onComplete 执行后,就不可再操作 request 和 response + +## 5. 参考资料 + +- **官方** + + - [Tomcat 官方网站](http://tomcat.apache.org/) + - [Tomcat Wiki](http://wiki.apache.org/tomcat/FrontPage) + - [Tomee 官方网站](http://tomee.apache.org/) + +- **文章** + - [Creating a Web App with Bootstrap and Tomcat Embedded](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/basic_app_embedded_tomcat/basic_app-tomcat-embedded.html) + - [Tomcat 组成与工作原理](https://juejin.im/post/58eb5fdda0bb9f00692a78fc) + - [Tomcat 工作原理](https://www.ibm.com/developerworks/cn/java/j-lo-tomcat1/index.html) + - [Tomcat 设计模式分析](https://www.ibm.com/developerworks/cn/java/j-lo-tomcat2/index.html?ca=drs-) \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" new file mode 100644 index 00000000..fbf9fe8e --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" @@ -0,0 +1,523 @@ +--- +title: Tomcat连接器 +date: 2022-02-17 22:34:30 +order: 02 +categories: + - Java + - JavaEE + - 服务器 + - Tomcat +tags: + - Java + - JavaWeb + - 服务器 + - Tomcat +permalink: /pages/13f070/ +--- + +# Tomcat 连接器 + +## 1. NioEndpoint 组件 + +Tomcat 的 NioEndPoint 组件利用 Java NIO 实现了 I/O 多路复用模型。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127094302.jpg) + +NioEndPoint 子组件功能简介: + +- `LimitLatch` 是连接控制器,负责控制最大连接数。NIO 模式下默认是 10000,达到这个阈值后,连接请求被拒绝。 +- `Acceptor` 负责监听连接请求。`Acceptor` 运行在一个单独的线程里,它在一个死循环里调用 accept 方法来接收新连接,一旦有新的连接请求到来,accept 方法返回一个 `Channel` 对象,接着把 `Channel` 对象交给 `Poller` 去处理。 +- `Poller` 的本质是一个 `Selector`,也运行在单独线程里。`Poller` 内部维护一个 `Channel` 数组,它在一个死循环里不断检测 `Channel` 的数据就绪状态,一旦有 `Channel` 可读,就生成一个 `SocketProcessor` 任务对象扔给 `Executor` 去处理。 +- `Executor` 就是线程池,负责运行 `SocketProcessor` 任务类,`SocketProcessor` 的 run 方法会调用 `Http11Processor` 来读取和解析请求数据。我们知道,`Http11Processor` 是应用层协议的封装,它会调用容器获得响应,再把响应通过 `Channel` 写出。 + +NioEndpoint 如何实现高并发的呢? + +要实现高并发需要合理设计线程模型充分利用 CPU 资源,尽量不要让线程阻塞;另外,就是有多少任务,就用相应规模的线程数去处理。 + +NioEndpoint 要完成三件事情:接收连接、检测 I/O 事件以及处理请求,那么最核心的就是把这三件事情分开,用不同规模的线程去处理,比如用专门的线程组去跑 Acceptor,并且 Acceptor 的个数可以配置;用专门的线程组去跑 Poller,Poller 的个数也可以配置;最后具体任务的执行也由专门的线程池来处理,也可以配置线程池的大小。 + +### 1.1. LimitLatch + +`LimitLatch` 用来控制连接个数,当连接数到达最大时阻塞线程,直到后续组件处理完一个连接后将连接数减 1。请你注意到达最大连接数后操作系统底层还是会接收客户端连接,但用户层已经不再接收。 + +```java +public class LimitLatch { + private class Sync extends AbstractQueuedSynchronizer { + + @Override + protected int tryAcquireShared() { + long newCount = count.incrementAndGet(); + if (newCount > limit) { + count.decrementAndGet(); + return -1; + } else { + return 1; + } + } + + @Override + protected boolean tryReleaseShared(int arg) { + count.decrementAndGet(); + return true; + } + } + + private final Sync sync; + private final AtomicLong count; + private volatile long limit; + + // 线程调用这个方法来获得接收新连接的许可,线程可能被阻塞 + public void countUpOrAwait() throws InterruptedException { + sync.acquireSharedInterruptibly(1); + } + + // 调用这个方法来释放一个连接许可,那么前面阻塞的线程可能被唤醒 + public long countDown() { + sync.releaseShared(0); + long result = getCount(); + return result; + } +} +``` + +LimitLatch 内步定义了内部类 Sync,而 Sync 扩展了 AQS,AQS 是 Java 并发包中的一个核心类,它在内部维护一个状态和一个线程队列,可以用来**控制线程什么时候挂起,什么时候唤醒**。我们可以扩展它来实现自己的同步器,实际上 Java 并发包里的锁和条件变量等等都是通过 AQS 来实现的,而这里的 LimitLatch 也不例外。 + +理解源码要点: + +- 用户线程通过调用 LimitLatch 的 countUpOrAwait 方法来拿到锁,如果暂时无法获取,这个线程会被阻塞到 AQS 的队列中。那 AQS 怎么知道是阻塞还是不阻塞用户线程呢?其实这是由 AQS 的使用者来决定的,也就是内部类 Sync 来决定的,因为 Sync 类重写了 AQS 的**tryAcquireShared() 方法**。它的实现逻辑是如果当前连接数 count 小于 limit,线程能获取锁,返回 1,否则返回 -1。 +- 如何用户线程被阻塞到了 AQS 的队列,那什么时候唤醒呢?同样是由 Sync 内部类决定,Sync 重写了 AQS 的**releaseShared() 方法**,其实就是当一个连接请求处理完了,这时又可以接收一个新连接了,这样前面阻塞的线程将会被唤醒。 + +### 1.2. Acceptor + +Acceptor 实现了 Runnable 接口,因此可以跑在单独线程里。一个端口号只能对应一个 ServerSocketChannel,因此这个 ServerSocketChannel 是在多个 Acceptor 线程之间共享的,它是 Endpoint 的属性,由 Endpoint 完成初始化和端口绑定。 + +``` +serverSock = ServerSocketChannel.open(); +serverSock.socket().bind(addr,getAcceptCount()); +serverSock.configureBlocking(true); +``` + +- bind 方法的第二个参数表示操作系统的等待队列长度,我在上面提到,当应用层面的连接数到达最大值时,操作系统可以继续接收连接,那么操作系统能继续接收的最大连接数就是这个队列长度,可以通过 acceptCount 参数配置,默认是 100。 +- ServerSocketChannel 被设置成阻塞模式,也就是说它是以阻塞的方式接收连接的。ServerSocketChannel 通过 accept() 接受新的连接,accept() 方法返回获得 SocketChannel 对象,然后将 SocketChannel 对象封装在一个 PollerEvent 对象中,并将 PollerEvent 对象压入 Poller 的 Queue 里,这是个典型的生产者 - 消费者模式,Acceptor 与 Poller 线程之间通过 Queue 通信。 + +### 1.3. Poller + +`Poller` 本质是一个 `Selector`,它内部维护一个 `Queue`。 + +``` +private final SynchronizedQueue events = new SynchronizedQueue<>(); +``` + +`SynchronizedQueue` 的核心方法都使用了 `Synchronized` 关键字进行修饰,用来保证同一时刻只有一个线程进行读写。 + +使用 `SynchronizedQueue`,意味着同一时刻只有一个 `Acceptor` 线程对队列进行读写;同时有多个 `Poller` 线程在运行,每个 `Poller` 线程都有自己的队列。每个 `Poller` 线程可能同时被多个 `Acceptor` 线程调用来注册 `PollerEvent`。同样 `Poller` 的个数可以通过 pollers 参数配置。 + +`Poller` 不断的通过内部的 `Selector` 对象向内核查询 `Channel` 的状态,一旦可读就生成任务类 `SocketProcessor` 交给 `Executor` 去处理。`Poller` 的另一个重要任务是循环遍历检查自己所管理的 `SocketChannel` 是否已经超时,如果有超时就关闭这个 `SocketChannel`。 + +### 1.4. SocketProcessor + +我们知道,`Poller` 会创建 `SocketProcessor` 任务类交给线程池处理,而 `SocketProcessor` 实现了 `Runnable` 接口,用来定义 `Executor` 中线程所执行的任务,主要就是调用 `Http11Processor` 组件来处理请求。`Http11Processor` 读取 `Channel` 的数据来生成 `ServletRequest` 对象,这里请你注意: + +`Http11Processor` 并不是直接读取 `Channel` 的。这是因为 Tomcat 支持同步非阻塞 I/O 模型和异步 I/O 模型,在 Java API 中,相应的 Channel 类也是不一样的,比如有 `AsynchronousSocketChannel` 和 `SocketChannel`,为了对 `Http11Processor` 屏蔽这些差异,Tomcat 设计了一个包装类叫作 `SocketWrapper`,`Http11Processor` 只调用 `SocketWrapper` 的方法去读写数据。 + +## 2. Nio2Endpoint 组件 + +Nio2Endpoint 工作流程跟 NioEndpoint 较为相似。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127143839.jpg) + +Nio2Endpoint 子组件功能说明: + +- `LimitLatch` 是连接控制器,它负责控制最大连接数。 +- `Nio2Acceptor` 扩展了 `Acceptor`,用异步 I/O 的方式来接收连接,跑在一个单独的线程里,也是一个线程组。`Nio2Acceptor` 接收新的连接后,得到一个 `AsynchronousSocketChannel`,`Nio2Acceptor` 把 `AsynchronousSocketChannel` 封装成一个 `Nio2SocketWrapper`,并创建一个 `SocketProcessor` 任务类交给线程池处理,并且 `SocketProcessor` 持有 `Nio2SocketWrapper` 对象。 +- `Executor` 在执行 `SocketProcessor` 时,`SocketProcessor` 的 run 方法会调用 `Http11Processor` 来处理请求,`Http11Processor` 会通过 `Nio2SocketWrapper` 读取和解析请求数据,请求经过容器处理后,再把响应通过 `Nio2SocketWrapper` 写出。 + +Nio2Endpoint 跟 NioEndpoint 的一个明显不同点是,**Nio2Endpoint 中没有 Poller 组件,也就是没有 Selector。这是为什么呢?因为在异步 I/O 模式下,Selector 的工作交给内核来做了。** + +### 2.1. Nio2Acceptor + +和 `NioEndpint` 一样,`Nio2Endpoint` 的基本思路是用 `LimitLatch` 组件来控制连接数。 + +但是 `Nio2Acceptor` 的监听连接的过程不是在一个死循环里不断的调 accept 方法,而是通过回调函数来完成的。我们来看看它的连接监听方法: + +``` +serverSock.accept(null, this); +``` + +其实就是调用了 accept 方法,注意它的第二个参数是 this,表明 `Nio2Acceptor` 自己就是处理连接的回调类,因此 `Nio2Acceptor` 实现了 `CompletionHandler` 接口。那么它是如何实现 `CompletionHandler` 接口的呢? + +```java +protected class Nio2Acceptor extends Acceptor + implements CompletionHandler { + + @Override + public void completed(AsynchronousSocketChannel socket, + Void attachment) { + + if (isRunning() && !isPaused()) { + if (getMaxConnections() == -1) { + // 如果没有连接限制,继续接收新的连接 + serverSock.accept(null, this); + } else { + // 如果有连接限制,就在线程池里跑 Run 方法,Run 方法会检查连接数 + getExecutor().execute(this); + } + // 处理请求 + if (!setSocketOptions(socket)) { + closeSocket(socket); + } + } + } +} +``` + +可以看到 `CompletionHandler` 的两个模板参数分别是 `AsynchronousServerSocketChannel` 和 Void,我在前面说过第一个参数就是 `accept` 方法的返回值,第二个参数是附件类,由用户自己决定,这里为 Void。`completed` 方法的处理逻辑比较简单: + +- 如果没有连接限制,继续在本线程中调用 `accept` 方法接收新的连接。 +- 如果有连接限制,就在线程池里跑 `run` 方法去接收新的连接。那为什么要跑 `run` 方法呢,因为在 `run` 方法里会检查连接数,当连接达到最大数时,线程可能会被 `LimitLatch` 阻塞。为什么要放在线程池里跑呢?这是因为如果放在当前线程里执行,`completed` 方法可能被阻塞,会导致这个回调方法一直不返回。 + +接着 `completed` 方法会调用 `setSocketOptions` 方法,在这个方法里,会创建 `Nio2SocketWrapper` 和 `SocketProcessor`,并交给线程池处理。 + +### 2.2. Nio2SocketWrapper + +`Nio2SocketWrapper` 的主要作用是封装 Channel,并提供接口给 `Http11Processor` 读写数据。讲到这里你是不是有个疑问:`Http11Processor` 是不能阻塞等待数据的,按照异步 I/O 的套路,`Http11Processor` 在调用 `Nio2SocketWrapper` 的 read 方法时需要注册回调类,read 调用会立即返回,问题是立即返回后 `Http11Processor` 还没有读到数据, 怎么办呢?这个请求的处理不就失败了吗? + +为了解决这个问题,`Http11Processor` 是通过 2 次 read 调用来完成数据读取操作的。 + +- 第一次 read 调用:连接刚刚建立好后,`Acceptor` 创建 `SocketProcessor` 任务类交给线程池去处理,`Http11Processor` 在处理请求的过程中,会调用 `Nio2SocketWrapper` 的 read 方法发出第一次读请求,同时注册了回调类 `readCompletionHandler`,因为数据没读到,`Http11Processor` 把当前的 `Nio2SocketWrapper` 标记为数据不完整。**接着 `SocketProcessor` 线程被回收,`Http11Processor` 并没有阻塞等待数据**。这里请注意,`Http11Processor` 维护了一个 `Nio2SocketWrapper` 列表,也就是维护了连接的状态。 +- 第二次 read 调用:当数据到达后,内核已经把数据拷贝到 `Http11Processor` 指定的 Buffer 里,同时回调类 `readCompletionHandler` 被调用,在这个回调处理方法里会**重新创建一个新的 `SocketProcessor` 任务来继续处理这个连接**,而这个新的 `SocketProcessor` 任务类持有原来那个 `Nio2SocketWrapper`,这一次 `Http11Processor` 可以通过 `Nio2SocketWrapper` 读取数据了,因为数据已经到了应用层的 Buffer。 + +这个回调类 `readCompletionHandler` 的源码如下,最关键的一点是,**`Nio2SocketWrapper` 是作为附件类来传递的**,这样在回调函数里能拿到所有的上下文。 + +``` +this.readCompletionHandler = new CompletionHandler>() { + public void completed(Integer nBytes, SocketWrapperBase attachment) { + ... + // 通过附件类 SocketWrapper 拿到所有的上下文 + Nio2SocketWrapper.this.getEndpoint().processSocket(attachment, SocketEvent.OPEN_READ, false); + } + + public void failed(Throwable exc, SocketWrapperBase attachment) { + ... + } +} +``` + +## 3. AprEndpoint 组件 + +我们在使用 Tomcat 时,可能会在启动日志里看到这样的提示信息: + +> The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: \*\*\* + +这句话的意思就是推荐你去安装 APR 库,可以提高系统性能。 + +APR(Apache Portable Runtime Libraries)是 Apache 可移植运行时库,它是用 C 语言实现的,其目的是向上层应用程序提供一个跨平台的操作系统接口库。Tomcat 可以用它来处理包括文件和网络 I/O,从而提升性能。Tomcat 支持的连接器有 NIO、NIO.2 和 APR。跟 NioEndpoint 一样,AprEndpoint 也实现了非阻塞 I/O,它们的区别是:NioEndpoint 通过调用 Java 的 NIO API 来实现非阻塞 I/O,而 AprEndpoint 是通过 JNI 调用 APR 本地库而实现非阻塞 I/O 的。 + +同样是非阻塞 I/O,为什么 Tomcat 会提示使用 APR 本地库的性能会更好呢?这是因为在某些场景下,比如需要频繁与操作系统进行交互,Socket 网络通信就是这样一个场景,特别是如果你的 Web 应用使用了 TLS 来加密传输,我们知道 TLS 协议在握手过程中有多次网络交互,在这种情况下 Java 跟 C 语言程序相比还是有一定的差距,而这正是 APR 的强项。 + +Tomcat 本身是 Java 编写的,为了调用 C 语言编写的 APR,需要通过 JNI 方式来调用。JNI(Java Native Interface) 是 JDK 提供的一个编程接口,它允许 Java 程序调用其他语言编写的程序或者代码库,其实 JDK 本身的实现也大量用到 JNI 技术来调用本地 C 程序库。 + +### 3.1. AprEndpoint 工作流程 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127145740.jpg) + +#### 3.1.1. Acceptor + +Accpetor 的功能就是监听连接,接收并建立连接。它的本质就是调用了四个操作系统 API:socket、bind、listen 和 accept。那 Java 语言如何直接调用 C 语言 API 呢?答案就是通过 JNI。具体来说就是两步:先封装一个 Java 类,在里面定义一堆用**native 关键字**修饰的方法,像下面这样。 + +```java +public class Socket { + ... + // 用 native 修饰这个方法,表明这个函数是 C 语言实现 + public static native long create(int family, int type, + int protocol, long cont) + + public static native int bind(long sock, long sa); + + public static native int listen(long sock, int backlog); + + public static native long accept(long sock) +} +``` + +接着用 C 代码实现这些方法,比如 bind 函数就是这样实现的: + +```java +// 注意函数的名字要符合 JNI 规范的要求 +JNIEXPORT jint JNICALL +Java_org_apache_tomcat_jni_Socket_bind(JNIEnv *e, jlong sock,jlong sa) + { + jint rv = APR_SUCCESS; + tcn_socket_t *s = (tcn_socket_t *)sock; + apr_sockaddr_t *a = (apr_sockaddr_t *) sa; + + // 调用 APR 库自己实现的 bind 函数 + rv = (jint)apr_socket_bind(s->sock, a); + return rv; + } +``` + +专栏里我就不展开 JNI 的细节了,你可以[扩展阅读](http://jnicookbook.owsiak.org/contents/)获得更多信息和例子。我们要注意的是函数名字要符合 JNI 的规范,以及 Java 和 C 语言如何互相传递参数,比如在 C 语言有指针,Java 没有指针的概念,所以在 Java 中用 long 类型来表示指针。AprEndpoint 的 Acceptor 组件就是调用了 APR 实现的四个 API。 + +#### 3.1.2. Poller + +Acceptor 接收到一个新的 Socket 连接后,按照 NioEndpoint 的实现,它会把这个 Socket 交给 Poller 去查询 I/O 事件。AprEndpoint 也是这样做的,不过 AprEndpoint 的 Poller 并不是调用 Java NIO 里的 Selector 来查询 Socket 的状态,而是通过 JNI 调用 APR 中的 poll 方法,而 APR 又是调用了操作系统的 epoll API 来实现的。 + +这里有个特别的地方是在 AprEndpoint 中,我们可以配置一个叫`deferAccept`的参数,它对应的是 TCP 协议中的`TCP_DEFER_ACCEPT`,设置这个参数后,当 TCP 客户端有新的连接请求到达时,TCP 服务端先不建立连接,而是再等等,直到客户端有请求数据发过来时再建立连接。这样的好处是服务端不需要用 Selector 去反复查询请求数据是否就绪。 + +这是一种 TCP 协议层的优化,不是每个操作系统内核都支持,因为 Java 作为一种跨平台语言,需要屏蔽各种操作系统的差异,因此并没有把这个参数提供给用户;但是对于 APR 来说,它的目的就是尽可能提升性能,因此它向用户暴露了这个参数。 + +### 3.2. APR 提升性能的秘密 + +APR 连接器之所以能提高 Tomcat 的性能,除了 APR 本身是 C 程序库之外,还有哪些提速的秘密呢? + +**JVM 堆 VS 本地内存** + +我们知道 Java 的类实例一般在 JVM 堆上分配,而 Java 是通过 JNI 调用 C 代码来实现 Socket 通信的,那么 C 代码在运行过程中需要的内存又是从哪里分配的呢?C 代码能否直接操作 Java 堆? + +为了回答这些问题,我先来说说 JVM 和用户进程的关系。如果你想运行一个 Java 类文件,可以用下面的 Java 命令来执行。 + +``` +java my.class +``` + +这个命令行中的`java`其实是**一个可执行程序,这个程序会创建 JVM 来加载和运行你的 Java 类**。操作系统会创建一个进程来执行这个`java`可执行程序,而每个进程都有自己的虚拟地址空间,JVM 用到的内存(包括堆、栈和方法区)就是从进程的虚拟地址空间上分配的。请你注意的是,JVM 内存只是进程空间的一部分,除此之外进程空间内还有代码段、数据段、内存映射区、内核空间等。从 JVM 的角度看,JVM 内存之外的部分叫作本地内存,C 程序代码在运行过程中用到的内存就是本地内存中分配的。下面我们通过一张图来理解一下。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127150729.jpg) + +Tomcat 的 Endpoint 组件在接收网络数据时需要预先分配好一块 Buffer,所谓的 Buffer 就是字节数组`byte[]`,Java 通过 JNI 调用把这块 Buffer 的地址传给 C 代码,C 代码通过操作系统 API 读取 Socket 并把数据填充到这块 Buffer。Java NIO API 提供了两种 Buffer 来接收数据:HeapByteBuffer 和 DirectByteBuffer,下面的代码演示了如何创建两种 Buffer。 + +``` +// 分配 HeapByteBuffer +ByteBuffer buf = ByteBuffer.allocate(1024); + +// 分配 DirectByteBuffer +ByteBuffer buf = ByteBuffer.allocateDirect(1024); +``` + +创建好 Buffer 后直接传给 Channel 的 read 或者 write 函数,最终这块 Buffer 会通过 JNI 调用传递给 C 程序。 + +``` +// 将 buf 作为 read 函数的参数 +int bytesRead = socketChannel.read(buf); +``` + +那 HeapByteBuffer 和 DirectByteBuffer 有什么区别呢?HeapByteBuffer 对象本身在 JVM 堆上分配,并且它持有的字节数组`byte[]`也是在 JVM 堆上分配。但是如果用**HeapByteBuffer**来接收网络数据,**需要把数据从内核先拷贝到一个临时的本地内存,再从临时本地内存拷贝到 JVM 堆**,而不是直接从内核拷贝到 JVM 堆上。这是为什么呢?这是因为数据从内核拷贝到 JVM 堆的过程中,JVM 可能会发生 GC,GC 过程中对象可能会被移动,也就是说 JVM 堆上的字节数组可能会被移动,这样的话 Buffer 地址就失效了。如果这中间经过本地内存中转,从本地内存到 JVM 堆的拷贝过程中 JVM 可以保证不做 GC。 + +如果使用 HeapByteBuffer,你会发现 JVM 堆和内核之间多了一层中转,而 DirectByteBuffer 用来解决这个问题,DirectByteBuffer 对象本身在 JVM 堆上,但是它持有的字节数组不是从 JVM 堆上分配的,而是从本地内存分配的。DirectByteBuffer 对象中有个 long 类型字段 address,记录着本地内存的地址,这样在接收数据的时候,直接把这个本地内存地址传递给 C 程序,C 程序会将网络数据从内核拷贝到这个本地内存,JVM 可以直接读取这个本地内存,这种方式比 HeapByteBuffer 少了一次拷贝,因此一般来说它的速度会比 HeapByteBuffer 快好几倍。你可以通过上面的图加深理解。 + +Tomcat 中的 AprEndpoint 就是通过 DirectByteBuffer 来接收数据的,而 NioEndpoint 和 Nio2Endpoint 是通过 HeapByteBuffer 来接收数据的。你可能会问,NioEndpoint 和 Nio2Endpoint 为什么不用 DirectByteBuffer 呢?这是因为本地内存不好管理,发生内存泄漏难以定位,从稳定性考虑,NioEndpoint 和 Nio2Endpoint 没有去冒这个险。 + +#### 3.2.1. sendfile + +我们再来考虑另一个网络通信的场景,也就是静态文件的处理。浏览器通过 Tomcat 来获取一个 HTML 文件,而 Tomcat 的处理逻辑无非是两步: + +1. 从磁盘读取 HTML 到内存。 +2. 将这段内存的内容通过 Socket 发送出去。 + +但是在传统方式下,有很多次的内存拷贝: + +- 读取文件时,首先是内核把文件内容读取到内核缓冲区。 +- 如果使用 HeapByteBuffer,文件数据从内核到 JVM 堆内存需要经过本地内存中转。 +- 同样在将文件内容推入网络时,从 JVM 堆到内核缓冲区需要经过本地内存中转。 +- 最后还需要把文件从内核缓冲区拷贝到网卡缓冲区。 + +从下面的图你会发现这个过程有 6 次内存拷贝,并且 read 和 write 等系统调用将导致进程从用户态到内核态的切换,会耗费大量的 CPU 和内存资源。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127151041.jpg) + +而 Tomcat 的 AprEndpoint 通过操作系统层面的 sendfile 特性解决了这个问题,sendfile 系统调用方式非常简洁。 + +``` +sendfile(socket, file, len); +``` + +它带有两个关键参数:Socket 和文件句柄。将文件从磁盘写入 Socket 的过程只有两步: + +第一步:将文件内容读取到内核缓冲区。 + +第二步:数据并没有从内核缓冲区复制到 Socket 关联的缓冲区,只有记录数据位置和长度的描述符被添加到 Socket 缓冲区中;接着把数据直接从内核缓冲区传递给网卡。这个过程你可以看下面的图。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127151155.jpg) + +## 4. Executor 组件 + +为了提高处理能力和并发度,Web 容器一般会把处理请求的工作放到线程池里来执行,Tomcat 扩展了原生的 Java 线程池,来满足 Web 容器高并发的需求。 + +### 4.1. Tomcat 定制线程池 + +Tomcat 的线程池也是一个定制版的 ThreadPoolExecutor。Tomcat 传入的参数是这样的: + +``` +// 定制版的任务队列 +taskqueue = new TaskQueue(maxQueueSize); + +// 定制版的线程工厂 +TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority()); + +// 定制版的线程池 +executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf); +``` + +其中的两个关键点: + +- Tomcat 有自己的定制版任务队列和线程工厂,并且可以限制任务队列的长度,它的最大长度是 maxQueueSize。 +- Tomcat 对线程数也有限制,设置了核心线程数(minSpareThreads)和最大线程池数(maxThreads)。 + +除了资源限制以外,Tomcat 线程池还定制自己的任务处理流程。我们知道 Java 原生线程池的任务处理逻辑比较简单: + +1. 前 corePoolSize 个任务时,来一个任务就创建一个新线程。 +2. 后面再来任务,就把任务添加到任务队列里让所有的线程去抢,如果队列满了就创建临时线程。 +3. 如果总线程数达到 maximumPoolSize,**执行拒绝策略。** + +Tomcat 线程池扩展了原生的 ThreadPoolExecutor,通过重写 execute 方法实现了自己的任务处理逻辑: + +1. 前 corePoolSize 个任务时,来一个任务就创建一个新线程。 +2. 再来任务的话,就把任务添加到任务队列里让所有的线程去抢,如果队列满了就创建临时线程。 +3. 如果总线程数达到 maximumPoolSize,**则继续尝试把任务添加到任务队列中去。** +4. **如果缓冲队列也满了,插入失败,执行拒绝策略。** + +观察 Tomcat 线程池和 Java 原生线程池的区别,其实就是在第 3 步,Tomcat 在线程总数达到最大数时,不是立即执行拒绝策略,而是再尝试向任务队列添加任务,添加失败后再执行拒绝策略。那具体如何实现呢,其实很简单,我们来看一下 Tomcat 线程池的 execute 方法的核心代码。 + +``` +public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor { + + ... + + public void execute(Runnable command, long timeout, TimeUnit unit) { + submittedCount.incrementAndGet(); + try { + // 调用 Java 原生线程池的 execute 去执行任务 + super.execute(command); + } catch (RejectedExecutionException rx) { + // 如果总线程数达到 maximumPoolSize,Java 原生线程池执行拒绝策略 + if (super.getQueue() instanceof TaskQueue) { + final TaskQueue queue = (TaskQueue)super.getQueue(); + try { + // 继续尝试把任务放到任务队列中去 + if (!queue.force(command, timeout, unit)) { + submittedCount.decrementAndGet(); + // 如果缓冲队列也满了,插入失败,执行拒绝策略。 + throw new RejectedExecutionException("..."); + } + } + } + } +} +``` + +从这个方法你可以看到,Tomcat 线程池的 execute 方法会调用 Java 原生线程池的 execute 去执行任务,如果总线程数达到 maximumPoolSize,Java 原生线程池的 execute 方法会抛出 RejectedExecutionException 异常,但是这个异常会被 Tomcat 线程池的 execute 方法捕获到,并继续尝试把这个任务放到任务队列中去;如果任务队列也满了,再执行拒绝策略。 + +### 4.2. Tomcat 定制任务队列 + +细心的你有没有发现,在 Tomcat 线程池的 execute 方法最开始有这么一行: + +``` +submittedCount.incrementAndGet(); +``` + +这行代码的意思把 submittedCount 这个原子变量加一,并且在任务执行失败,抛出拒绝异常时,将这个原子变量减一: + +``` +submittedCount.decrementAndGet(); +``` + +其实 Tomcat 线程池是用这个变量 submittedCount 来维护已经提交到了线程池,但是还没有执行完的任务个数。Tomcat 为什么要维护这个变量呢?这跟 Tomcat 的定制版的任务队列有关。Tomcat 的任务队列 TaskQueue 扩展了 Java 中的 LinkedBlockingQueue,我们知道 LinkedBlockingQueue 默认情况下长度是没有限制的,除非给它一个 capacity。因此 Tomcat 给了它一个 capacity,TaskQueue 的构造函数中有个整型的参数 capacity,TaskQueue 将 capacity 传给父类 LinkedBlockingQueue 的构造函数。 + +```java +public class TaskQueue extends LinkedBlockingQueue { + + public TaskQueue(int capacity) { + super(capacity); + } + ... +} +``` + +这个 capacity 参数是通过 Tomcat 的 maxQueueSize 参数来设置的,但问题是默认情况下 maxQueueSize 的值是`Integer.MAX_VALUE`,等于没有限制,这样就带来一个问题:当前线程数达到核心线程数之后,再来任务的话线程池会把任务添加到任务队列,并且总是会成功,这样永远不会有机会创建新线程了。 + +为了解决这个问题,TaskQueue 重写了 LinkedBlockingQueue 的 offer 方法,在合适的时机返回 false,返回 false 表示任务添加失败,这时线程池会创建新的线程。那什么是合适的时机呢?请看下面 offer 方法的核心源码: + +```java +public class TaskQueue extends LinkedBlockingQueue { + + ... + @Override + // 线程池调用任务队列的方法时,当前线程数肯定已经大于核心线程数了 + public boolean offer(Runnable o) { + + // 如果线程数已经到了最大值,不能创建新线程了,只能把任务添加到任务队列。 + if (parent.getPoolSize() == parent.getMaximumPoolSize()) + return super.offer(o); + + // 执行到这里,表明当前线程数大于核心线程数,并且小于最大线程数。 + // 表明是可以创建新线程的,那到底要不要创建呢?分两种情况: + + //1. 如果已提交的任务数小于当前线程数,表示还有空闲线程,无需创建新线程 + if (parent.getSubmittedCount()<=(parent.getPoolSize())) + return super.offer(o); + + //2. 如果已提交的任务数大于当前线程数,线程不够用了,返回 false 去创建新线程 + if (parent.getPoolSize()> clazzes, ServletContext ctx) throws ServletException { + ... + } +} +``` + +一旦定义好了 SCI,Tomcat 在启动阶段扫描类时,会将 HandlesTypes 注解中指定的类都扫描出来,作为 SCI 的 onStartup 方法的参数,并调用 SCI 的 onStartup 方法。注意到 WsSci 的 HandlesTypes 注解中定义了`ServerEndpoint.class`、`ServerApplicationConfig.class`和`Endpoint.class`,因此在 Tomcat 的启动阶段会将这些类的类实例(注意不是对象实例)传递给 WsSci 的 onStartup 方法。那么 WsSci 的 onStartup 方法又做了什么事呢? + +它会构造一个 WebSocketContainer 实例,你可以把 WebSocketContainer 理解成一个专门处理 WebSocket 请求的**Endpoint 容器**。也就是说 Tomcat 会把扫描到的 Endpoint 子类和添加了注解`@ServerEndpoint`的类注册到这个容器中,并且这个容器还维护了 URL 到 Endpoint 的映射关系,这样通过请求 URL 就能找到具体的 Endpoint 来处理 WebSocket 请求。 + +### 5.2. WebSocket 请求处理 + +Tomcat 用 ProtocolHandler 组件屏蔽应用层协议的差异,其中 ProtocolHandler 中有两个关键组件:Endpoint 和 Processor。需要注意,这里的 Endpoint 跟上文提到的 WebSocket 中的 Endpoint 完全是两回事,连接器中的 Endpoint 组件用来处理 I/O 通信。WebSocket 本质就是一个应用层协议,因此不能用 HttpProcessor 来处理 WebSocket 请求,而要用专门 Processor 来处理,而在 Tomcat 中这样的 Processor 叫作 UpgradeProcessor。 + +为什么叫 Upgrade Processor 呢?这是因为 Tomcat 是将 HTTP 协议升级成 WebSocket 协议的。 + +WebSocket 是通过 HTTP 协议来进行握手的,因此当 WebSocket 的握手请求到来时,HttpProtocolHandler 首先接收到这个请求,在处理这个 HTTP 请求时,Tomcat 通过一个特殊的 Filter 判断该当前 HTTP 请求是否是一个 WebSocket Upgrade 请求(即包含`Upgrade: websocket`的 HTTP 头信息),如果是,则在 HTTP 响应里添加 WebSocket 相关的响应头信息,并进行协议升级。具体来说就是用 UpgradeProtocolHandler 替换当前的 HttpProtocolHandler,相应的,把当前 Socket 的 Processor 替换成 UpgradeProcessor,同时 Tomcat 会创建 WebSocket Session 实例和 Endpoint 实例,并跟当前的 WebSocket 连接一一对应起来。这个 WebSocket 连接不会立即关闭,并且在请求处理中,不再使用原有的 HttpProcessor,而是用专门的 UpgradeProcessor,UpgradeProcessor 最终会调用相应的 Endpoint 实例来处理请求。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127153521.jpg) + +你可以看到,Tomcat 对 WebSocket 请求的处理没有经过 Servlet 容器,而是通过 UpgradeProcessor 组件直接把请求发到 ServerEndpoint 实例,并且 Tomcat 的 WebSocket 实现不需要关注具体 I/O 模型的细节,从而实现了与具体 I/O 方式的解耦。 + +## 6. 参考资料 + +- **官方** + - [Tomcat 官方网站](http://tomcat.apache.org/) + - [Tomcat Wiki](http://wiki.apache.org/tomcat/FrontPage) + - [Tomee 官方网站](http://tomee.apache.org/) +- **教程** + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" new file mode 100644 index 00000000..0f71d9ce --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" @@ -0,0 +1,728 @@ +--- +title: Tomcat容器 +date: 2022-02-17 22:34:30 +order: 03 +categories: + - Java + - JavaEE + - 服务器 + - Tomcat +tags: + - Java + - JavaWeb + - 服务器 + - Tomcat +permalink: /pages/d5076a/ +--- + +# Tomcat 容器 + +## Tomcat 实现热部署和热加载 + +- 热加载的实现方式是 Web 容器启动一个后台线程,定期检测类文件的变化,如果有变化,就重新加载类,在这个过程中不会清空 Session ,一般用在开发环境。 +- 热部署原理类似,也是由后台线程定时检测 Web 应用的变化,但它会重新加载整个 Web 应用。这种方式会清空 Session,比热加载更加干净、彻底,一般用在生产环境。 + +Tomcat 通过开启后台线程,使得各个层次的容器组件都有机会完成一些周期性任务。Tomcat 是基于 ScheduledThreadPoolExecutor 实现周期性任务的: + +```java +bgFuture = exec.scheduleWithFixedDelay( + new ContainerBackgroundProcessor(),// 要执行的 Runnable + backgroundProcessorDelay, // 第一次执行延迟多久 + backgroundProcessorDelay, // 之后每次执行间隔多久 + TimeUnit.SECONDS); // 时间单位 +``` + +第一个参数就是要周期性执行的任务类 ContainerBackgroundProcessor,它是一个 Runnable,同时也是 ContainerBase 的内部类,ContainerBase 是所有容器组件的基类,我们来回忆一下容器组件有哪些,有 Engine、Host、Context 和 Wrapper 等,它们具有父子关系。 + +### ContainerBackgroundProcessor 实现 + +我们接来看 ContainerBackgroundProcessor 具体是如何实现的。 + +```java +protected class ContainerBackgroundProcessor implements Runnable { + + @Override + public void run() { + // 请注意这里传入的参数是 " 宿主类 " 的实例 + processChildren(ContainerBase.this); + } + + protected void processChildren(Container container) { + try { + //1. 调用当前容器的 backgroundProcess 方法。 + container.backgroundProcess(); + + //2. 遍历所有的子容器,递归调用 processChildren, + // 这样当前容器的子孙都会被处理 + Container[] children = container.findChildren(); + for (int i = 0; i < children.length; i++) { + // 这里请你注意,容器基类有个变量叫做 backgroundProcessorDelay,如果大于 0,表明子容器有自己的后台线程,无需父容器来调用它的 processChildren 方法。 + if (children[i].getBackgroundProcessorDelay() <= 0) { + processChildren(children[i]); + } + } + } catch (Throwable t) { ... } +``` + +上面的代码逻辑也是比较清晰的,首先 ContainerBackgroundProcessor 是一个 Runnable,它需要实现 run 方法,它的 run 很简单,就是调用了 processChildren 方法。这里有个小技巧,它把“宿主类”,也就是**ContainerBase 的类实例当成参数传给了 run 方法**。 + +而在 processChildren 方法里,就做了两步:调用当前容器的 backgroundProcess 方法,以及递归调用子孙的 backgroundProcess 方法。请你注意 backgroundProcess 是 Container 接口中的方法,也就是说所有类型的容器都可以实现这个方法,在这个方法里完成需要周期性执行的任务。 + +这样的设计意味着什么呢?我们只需要在顶层容器,也就是 Engine 容器中启动一个后台线程,那么这个线程**不但会执行 Engine 容器的周期性任务,它还会执行所有子容器的周期性任务**。 + +### backgroundProcess 方法 + +上述代码都是在基类 ContainerBase 中实现的,那具体容器类需要做什么呢?其实很简单,如果有周期性任务要执行,就实现 backgroundProcess 方法;如果没有,就重用基类 ContainerBase 的方法。ContainerBase 的 backgroundProcess 方法实现如下: + +```java +public void backgroundProcess() { + + //1. 执行容器中 Cluster 组件的周期性任务 + Cluster cluster = getClusterInternal(); + if (cluster != null) { + cluster.backgroundProcess(); + } + + //2. 执行容器中 Realm 组件的周期性任务 + Realm realm = getRealmInternal(); + if (realm != null) { + realm.backgroundProcess(); + } + + //3. 执行容器中 Valve 组件的周期性任务 + Valve current = pipeline.getFirst(); + while (current != null) { + current.backgroundProcess(); + current = current.getNext(); + } + + //4. 触发容器的 " 周期事件 ",Host 容器的监听器 HostConfig 就靠它来调用 + fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null); +} +``` + +从上面的代码可以看到,不仅每个容器可以有周期性任务,每个容器中的其他通用组件,比如跟集群管理有关的 Cluster 组件、跟安全管理有关的 Realm 组件都可以有自己的周期性任务。 + +我在前面的专栏里提到过,容器之间的链式调用是通过 Pipeline-Valve 机制来实现的,从上面的代码你可以看到容器中的 Valve 也可以有周期性任务,并且被 ContainerBase 统一处理。 + +请你特别注意的是,在 backgroundProcess 方法的最后,还触发了容器的“周期事件”。我们知道容器的生命周期事件有初始化、启动和停止等,那“周期事件”又是什么呢?它跟生命周期事件一样,是一种扩展机制,你可以这样理解: + +又一段时间过去了,容器还活着,你想做点什么吗?如果你想做点什么,就创建一个监听器来监听这个“周期事件”,事件到了我负责调用你的方法。 + +总之,有了 ContainerBase 中的后台线程和 backgroundProcess 方法,各种子容器和通用组件不需要各自弄一个后台线程来处理周期性任务,这样的设计显得优雅和整洁。 + +### Tomcat 热加载 + +有了 ContainerBase 的周期性任务处理“框架”,作为具体容器子类,只需要实现自己的周期性任务就行。而 Tomcat 的热加载,就是在 Context 容器中实现的。Context 容器的 backgroundProcess 方法是这样实现的: + +```java +public void backgroundProcess() { + + //WebappLoader 周期性的检查 WEB-INF/classes 和 WEB-INF/lib 目录下的类文件 + Loader loader = getLoader(); + if (loader != null) { + loader.backgroundProcess(); + } + + //Session 管理器周期性的检查是否有过期的 Session + Manager manager = getManager(); + if (manager != null) { + manager.backgroundProcess(); + } + + // 周期性的检查静态资源是否有变化 + WebResourceRoot resources = getResources(); + if (resources != null) { + resources.backgroundProcess(); + } + + // 调用父类 ContainerBase 的 backgroundProcess 方法 + super.backgroundProcess(); +} +``` + +从上面的代码我们看到 Context 容器通过 WebappLoader 来检查类文件是否有更新,通过 Session 管理器来检查是否有 Session 过期,并且通过资源管理器来检查静态资源是否有更新,最后还调用了父类 ContainerBase 的 backgroundProcess 方法。 + +这里我们要重点关注,WebappLoader 是如何实现热加载的,它主要是调用了 Context 容器的 reload 方法,而 Context 的 reload 方法比较复杂,总结起来,主要完成了下面这些任务: + +1. 停止和销毁 Context 容器及其所有子容器,子容器其实就是 Wrapper,也就是说 Wrapper 里面 Servlet 实例也被销毁了。 +2. 停止和销毁 Context 容器关联的 Listener 和 Filter。 +3. 停止和销毁 Context 下的 Pipeline 和各种 Valve。 +4. 停止和销毁 Context 的类加载器,以及类加载器加载的类文件资源。 +5. 启动 Context 容器,在这个过程中会重新创建前面四步被销毁的资源。 + +在这个过程中,类加载器发挥着关键作用。一个 Context 容器对应一个类加载器,类加载器在销毁的过程中会把它加载的所有类也全部销毁。Context 容器在启动过程中,会创建一个新的类加载器来加载新的类文件。 + +在 Context 的 reload 方法里,并没有调用 Session 管理器的 distroy 方法,也就是说这个 Context 关联的 Session 是没有销毁的。你还需要注意的是,Tomcat 的热加载默认是关闭的,你需要在 conf 目录下的 Context.xml 文件中设置 reloadable 参数来开启这个功能,像下面这样: + +``` + +``` + +### Tomcat 热部署 + +我们再来看看热部署,热部署跟热加载的本质区别是,热部署会重新部署 Web 应用,原来的 Context 对象会整个被销毁掉,因此这个 Context 所关联的一切资源都会被销毁,包括 Session。 + +那么 Tomcat 热部署又是由哪个容器来实现的呢?应该不是由 Context,因为热部署过程中 Context 容器被销毁了,那么这个重担就落在 Host 身上了,因为它是 Context 的父容器。 + +跟 Context 不一样,Host 容器并没有在 backgroundProcess 方法中实现周期性检测的任务,而是通过监听器 HostConfig 来实现的,HostConfig 就是前面提到的“周期事件”的监听器,那“周期事件”达到时,HostConfig 会做什么事呢? + +```java +public void lifecycleEvent(LifecycleEvent event) { + // 执行 check 方法。 + if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) { + check(); + } +} +``` + +它执行了 check 方法,我们接着来看 check 方法里做了什么。 + +```java +protected void check() { + + if (host.getAutoDeploy()) { + // 检查这个 Host 下所有已经部署的 Web 应用 + DeployedApplication[] apps = + deployed.values().toArray(new DeployedApplication[0]); + + for (int i = 0; i < apps.length; i++) { + // 检查 Web 应用目录是否有变化 + checkResources(apps[i], false); + } + + // 执行部署 + deployApps(); + } +} +``` + +其实 HostConfig 会检查 webapps 目录下的所有 Web 应用: + +- 如果原来 Web 应用目录被删掉了,就把相应 Context 容器整个销毁掉。 +- 是否有新的 Web 应用目录放进来了,或者有新的 WAR 包放进来了,就部署相应的 Web 应用。 + +因此 HostConfig 做的事情都是比较“宏观”的,它不会去检查具体类文件或者资源文件是否有变化,而是检查 Web 应用目录级别的变化。 + +## Tomcat 的类加载机制 + +Tomcat 的自定义类加载器 `WebAppClassLoader` 打破了双亲委派机制,它**首先自己尝试去加载某个类,如果找不到再代理给父类加载器**,其目的是优先加载 Web 应用自己定义的类。具体实现就是重写 ClassLoader 的两个方法:findClass 和 loadClass。 + +### findClass 方法 + +我们先来看看 findClass 方法的实现,为了方便理解和阅读,我去掉了一些细节: + +```java +public Class findClass(String name) throws ClassNotFoundException { + ... + + Class clazz = null; + try { + //1. 先在 Web 应用目录下查找类 + clazz = findClassInternal(name); + } catch (RuntimeException e) { + throw e; + } + + if (clazz == null) { + try { + //2. 如果在本地目录没有找到,交给父加载器去查找 + clazz = super.findClass(name); + } catch (RuntimeException e) { + throw e; + } + + //3. 如果父类也没找到,抛出 ClassNotFoundException + if (clazz == null) { + throw new ClassNotFoundException(name); + } + + return clazz; +} +``` + +在 findClass 方法里,主要有三个步骤: + +1. 先在 Web 应用本地目录下查找要加载的类。 +2. 如果没有找到,交给父加载器去查找,它的父加载器就是上面提到的系统类加载器 AppClassLoader。 +3. 如何父加载器也没找到这个类,抛出 ClassNotFound 异常。 + +### loadClass 方法 + +接着我们再来看 Tomcat 类加载器的 loadClass 方法的实现,同样我也去掉了一些细节: + +```java +public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + + synchronized (getClassLoadingLock(name)) { + + Class clazz = null; + + //1. 先在本地 cache 查找该类是否已经加载过 + clazz = findLoadedClass0(name); + if (clazz != null) { + if (resolve) + resolveClass(clazz); + return clazz; + } + + //2. 从系统类加载器的 cache 中查找是否加载过 + clazz = findLoadedClass(name); + if (clazz != null) { + if (resolve) + resolveClass(clazz); + return clazz; + } + + // 3. 尝试用 ExtClassLoader 类加载器类加载,为什么? + ClassLoader javaseLoader = getJavaseClassLoader(); + try { + clazz = javaseLoader.loadClass(name); + if (clazz != null) { + if (resolve) + resolveClass(clazz); + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + + // 4. 尝试在本地目录搜索 class 并加载 + try { + clazz = findClass(name); + if (clazz != null) { + if (resolve) + resolveClass(clazz); + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + + // 5. 尝试用系统类加载器 (也就是 AppClassLoader) 来加载 + try { + clazz = Class.forName(name, false, parent); + if (clazz != null) { + if (resolve) + resolveClass(clazz); + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + } + + //6. 上述过程都加载失败,抛出异常 + throw new ClassNotFoundException(name); +} +``` + +loadClass 方法稍微复杂一点,主要有六个步骤: + +1. 先在本地 Cache 查找该类是否已经加载过,也就是说 Tomcat 的类加载器是否已经加载过这个类。 +2. 如果 Tomcat 类加载器没有加载过这个类,再看看系统类加载器是否加载过。 +3. 如果都没有,就让**ExtClassLoader**去加载,这一步比较关键,目的**防止 Web 应用自己的类覆盖 JRE 的核心类**。因为 Tomcat 需要打破双亲委派机制,假如 Web 应用里自定义了一个叫 Object 的类,如果先加载这个 Object 类,就会覆盖 JRE 里面的那个 Object 类,这就是为什么 Tomcat 的类加载器会优先尝试用 ExtClassLoader 去加载,因为 ExtClassLoader 会委托给 BootstrapClassLoader 去加载,BootstrapClassLoader 发现自己已经加载了 Object 类,直接返回给 Tomcat 的类加载器,这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。 +4. 如果 ExtClassLoader 加载器加载失败,也就是说 JRE 核心类中没有这类,那么就在本地 Web 应用目录下查找并加载。 +5. 如果本地目录下没有这个类,说明不是 Web 应用自己定义的类,那么由系统类加载器去加载。这里请你注意,Web 应用是通过`Class.forName`调用交给系统类加载器的,因为`Class.forName`的默认加载器就是系统类加载器。 +6. 如果上述加载过程全部失败,抛出 ClassNotFound 异常。 + +从上面的过程我们可以看到,Tomcat 的类加载器打破了双亲委派机制,没有一上来就直接委托给父加载器,而是先在本地目录下加载,为了避免本地目录下的类覆盖 JRE 的核心类,先尝试用 JVM 扩展类加载器 ExtClassLoader 去加载。那为什么不先用系统类加载器 AppClassLoader 去加载?很显然,如果是这样的话,那就变成双亲委派机制了,这就是 Tomcat 类加载器的巧妙之处。 + +### Tomcat 实现应用隔离 + +Tomcat 作为 Web 容器,需要解决以下问题: + +1. 如果在 Tomcat 中运行了两个 Web 应用程序,两个 Web 应用中有同名的 Servlet,但是功能不同,Tomcat 需要同时加载和管理这两个同名的 Servlet 类,保证它们不会冲突,因此 Web 应用之间的类需要隔离。 +2. 两个 Web 应用都依赖同一个第三方的 JAR 包,比如 Spring,那 Spring 的 JAR 包被加载到内存后,Tomcat 要保证这两个 Web 应用能够共享,也就是说 Spring 的 JAR 包只被加载一次,否则随着依赖的第三方 JAR 包增多,JVM 的内存会膨胀。 +3. 需要隔离 Tomcat 本身的类和 Web 应用的类。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201130141536.png) + +#### WebAppClassLoader + +针对第一个问题: + +如果使用 JVM 默认 AppClassLoader 来加载 Web 应用,AppClassLoader 只能加载一个 Servlet 类,在加载第二个同名 Servlet 类时,AppClassLoader 会返回第一个 Servlet 类的 Class 实例,这是因为在 AppClassLoader 看来,同名的 Servlet 类只被加载一次。 + +Tomcat 的解决方案是自定义一个类加载器 WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例。我们知道,Context 容器组件对应一个 Web 应用,因此,每个 Context 容器负责创建和维护一个 WebAppClassLoader 加载器实例。这背后的原理是,**不同的加载器实例加载的类被认为是不同的类**,即使它们的类名相同。这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间,每一个 Web 应用都有自己的类空间,Web 应用之间通过各自的类加载器互相隔离。 + +#### SharedClassLoader + +针对第二个问题: + +本质需求是两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类。我们知道,在双亲委派机制里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗,应用程序也正是通过这种方式共享 JRE 的核心类。因此 Tomcat 的设计者又加了一个类加载器 SharedClassLoader,作为 WebAppClassLoader 的父加载器,专门来加载 Web 应用之间共享的类。如果 WebAppClassLoader 自己没有加载到某个类,就会委托父加载器 SharedClassLoader 去加载这个类,SharedClassLoader 会在指定目录下加载共享类,之后返回给 WebAppClassLoader,这样共享的问题就解决了。 + +#### CatalinaClassloader + +如何隔离 Tomcat 本身的类和 Web 应用的类? + +要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。基于此 Tomcat 又设计一个类加载器 CatalinaClassloader,专门来加载 Tomcat 自身的类。这样设计有个问题,那 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办呢? + +#### CommonClassLoader + +老办法,还是再增加一个 CommonClassLoader,作为 CatalinaClassloader 和 SharedClassLoader 的父加载器。CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。 + +## Tomcat 实现 Servlet 规范 + +Servlet 容器最重要的任务就是创建 Servlet 的实例并且调用 Servlet。 + +一个 Web 应用里往往有多个 Servlet,而在 Tomcat 中一个 Web 应用对应一个 Context 容器,也就是说一个 Context 容器需要管理多个 Servlet 实例。但 Context 容器并不直接持有 Servlet 实例,而是通过子容器 Wrapper 来管理 Servlet,你可以把 Wrapper 容器看作是 Servlet 的包装。 + +为什么需要 Wrapper 呢?Context 容器直接维护一个 Servlet 数组不就行了吗?这是因为 Servlet 不仅仅是一个类实例,它还有相关的配置信息,比如它的 URL 映射、它的初始化参数,因此设计出了一个包装器,把 Servlet 本身和它相关的数据包起来,没错,这就是面向对象的思想。 + +除此以外,Servlet 规范中还有两个重要特性:Listener 和 Filter,Tomcat 也需要创建它们的实例,并在合适的时机去调用它们的方法。 + +### Servlet 管理 + +Tomcat 是用 Wrapper 容器来管理 Servlet 的,那 Wrapper 容器具体长什么样子呢?我们先来看看它里面有哪些关键的成员变量: + +```java +protected volatile Servlet instance = null; +``` + +它拥有一个 Servlet 实例,并且 Wrapper 通过 loadServlet 方法来实例化 Servlet。为了方便你阅读,我简化了代码: + +```java +public synchronized Servlet loadServlet() throws ServletException { + Servlet servlet; + + //1. 创建一个 Servlet 实例 + servlet = (Servlet) instanceManager.newInstance(servletClass); + + //2. 调用了 Servlet 的 init 方法,这是 Servlet 规范要求的 + initServlet(servlet); + + return servlet; +} +``` + +其实 loadServlet 主要做了两件事:创建 Servlet 的实例,并且调用 Servlet 的 init 方法,因为这是 Servlet 规范要求的。 + +那接下来的问题是,什么时候会调到这个 loadServlet 方法呢?为了加快系统的启动速度,我们往往会采取资源延迟加载的策略,Tomcat 也不例外,默认情况下 Tomcat 在启动时不会加载你的 Servlet,除非你把 Servlet 的`loadOnStartup`参数设置为`true`。 + +这里还需要你注意的是,虽然 Tomcat 在启动时不会创建 Servlet 实例,但是会创建 Wrapper 容器,就好比尽管枪里面还没有子弹,先把枪造出来。那子弹什么时候造呢?是真正需要开枪的时候,也就是说有请求来访问某个 Servlet 时,这个 Servlet 的实例才会被创建。 + +那 Servlet 是被谁调用的呢?我们回忆一下专栏前面提到过 Tomcat 的 Pipeline-Valve 机制,每个容器组件都有自己的 Pipeline,每个 Pipeline 中有一个 Valve 链,并且每个容器组件有一个 BasicValve(基础阀)。Wrapper 作为一个容器组件,它也有自己的 Pipeline 和 BasicValve,Wrapper 的 BasicValve 叫 **StandardWrapperValve**。 + +你可以想到,当请求到来时,Context 容器的 BasicValve 会调用 Wrapper 容器中 Pipeline 中的第一个 Valve,然后会调用到 StandardWrapperValve。我们先来看看它的 invoke 方法是如何实现的,同样为了方便你阅读,我简化了代码: + +```java +public final void invoke(Request request, Response response) { + + //1. 实例化 Servlet + servlet = wrapper.allocate(); + + //2. 给当前请求创建一个 Filter 链 + ApplicationFilterChain filterChain = + ApplicationFilterFactory.createFilterChain(request, wrapper, servlet); + + //3. 调用这个 Filter 链,Filter 链中的最后一个 Filter 会调用 Servlet + filterChain.doFilter(request.getRequest(), response.getResponse()); + +} +``` + +StandardWrapperValve 的 invoke 方法比较复杂,去掉其他异常处理的一些细节,本质上就是三步: + +- 第一步,创建 Servlet 实例; +- 第二步,给当前请求创建一个 Filter 链; +- 第三步,调用这个 Filter 链。 + +你可能会问,为什么需要给每个请求创建一个 Filter 链?这是因为每个请求的请求路径都不一样,而 Filter 都有相应的路径映射,因此不是所有的 Filter 都需要来处理当前的请求,我们需要根据请求的路径来选择特定的一些 Filter 来处理。 + +第二个问题是,为什么没有看到调到 Servlet 的 service 方法?这是因为 Filter 链的 doFilter 方法会负责调用 Servlet,具体来说就是 Filter 链中的最后一个 Filter 会负责调用 Servlet。 + +接下来我们来看 Filter 的实现原理。 + +### Filter 管理 + +我们知道,跟 Servlet 一样,Filter 也可以在`web.xml`文件里进行配置,不同的是,Filter 的作用域是整个 Web 应用,因此 Filter 的实例是在 Context 容器中进行管理的,Context 容器用 Map 集合来保存 Filter。 + +```java +private Map filterDefs = new HashMap<>(); +``` + +那上面提到的 Filter 链又是什么呢?Filter 链的存活期很短,它是跟每个请求对应的。一个新的请求来了,就动态创建一个 FIlter 链,请求处理完了,Filter 链也就被回收了。理解它的原理也非常关键,我们还是来看看源码: + +```java +public final class ApplicationFilterChain implements FilterChain { + + //Filter 链中有 Filter 数组,这个好理解 + private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0]; + + //Filter 链中的当前的调用位置 + private int pos = 0; + + // 总共有多少了 Filter + private int n = 0; + + // 每个 Filter 链对应一个 Servlet,也就是它要调用的 Servlet + private Servlet servlet = null; + + public void doFilter(ServletRequest req, ServletResponse res) { + internalDoFilter(request,response); + } + + private void internalDoFilter(ServletRequest req, + ServletResponse res){ + + // 每个 Filter 链在内部维护了一个 Filter 数组 + if (pos < n) { + ApplicationFilterConfig filterConfig = filters[pos++]; + Filter filter = filterConfig.getFilter(); + + filter.doFilter(request, response, this); + return; + } + + servlet.service(request, response); + +} +``` + +从 ApplicationFilterChain 的源码我们可以看到几个关键信息: + +- Filter 链中除了有 Filter 对象的数组,还有一个整数变量 pos,这个变量用来记录当前被调用的 Filter 在数组中的位置。 +- Filter 链中有个 Servlet 实例,这个好理解,因为上面提到了,每个 Filter 链最后都会调到一个 Servlet。 +- Filter 链本身也实现了 doFilter 方法,直接调用了一个内部方法 internalDoFilter。 +- internalDoFilter 方法的实现比较有意思,它做了一个判断,如果当前 Filter 的位置小于 Filter 数组的长度,也就是说 Filter 还没调完,就从 Filter 数组拿下一个 Filter,调用它的 doFilter 方法。否则,意味着所有 Filter 都调到了,就调用 Servlet 的 service 方法。 + +但问题是,方法体里没看到循环,谁在不停地调用 Filter 链的 doFIlter 方法呢?Filter 是怎么依次调到的呢? + +答案是**Filter 本身的 doFilter 方法会调用 Filter 链的 doFilter 方法**,我们还是来看看代码就明白了: + +```java +public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain){ + + ... + + // 调用 Filter 的方法 + chain.doFilter(request, response); + + } +``` + +注意 Filter 的 doFilter 方法有个关键参数 FilterChain,就是 Filter 链。并且每个 Filter 在实现 doFilter 时,必须要调用 Filter 链的 doFilter 方法,而 Filter 链中保存当前 FIlter 的位置,会调用下一个 FIlter 的 doFilter 方法,这样链式调用就完成了。 + +Filter 链跟 Tomcat 的 Pipeline-Valve 本质都是责任链模式,但是在具体实现上稍有不同,你可以细细体会一下。 + +### Listener 管理 + +我们接着聊 Servlet 规范里 Listener。跟 Filter 一样,Listener 也是一种扩展机制,你可以监听容器内部发生的事件,主要有两类事件: + +- 第一类是生命状态的变化,比如 Context 容器启动和停止、Session 的创建和销毁。 +- 第二类是属性的变化,比如 Context 容器某个属性值变了、Session 的某个属性值变了以及新的请求来了等。 + +我们可以在`web.xml`配置或者通过注解的方式来添加监听器,在监听器里实现我们的业务逻辑。对于 Tomcat 来说,它需要读取配置文件,拿到监听器类的名字,实例化这些类,并且在合适的时机调用这些监听器的方法。 + +Tomcat 是通过 Context 容器来管理这些监听器的。Context 容器将两类事件分开来管理,分别用不同的集合来存放不同类型事件的监听器: + +```java +// 监听属性值变化的监听器 +private List applicationEventListenersList = new CopyOnWriteArrayList<>(); + +// 监听生命事件的监听器 +private Object applicationLifecycleListenersObjects[] = new Object[0]; +``` + +剩下的事情就是触发监听器了,比如在 Context 容器的启动方法里,就触发了所有的 ServletContextListener: + +```java +//1. 拿到所有的生命周期监听器 +Object instances[] = getApplicationLifecycleListeners(); + +for (int i = 0; i < instances.length; i++) { + //2. 判断 Listener 的类型是不是 ServletContextListener + if (!(instances[i] instanceof ServletContextListener)) + continue; + + //3. 触发 Listener 的方法 + ServletContextListener lr = (ServletContextListener) instances[i]; + lr.contextInitialized(event); +} +``` + +需要注意的是,这里的 ServletContextListener 接口是一种留给用户的扩展机制,用户可以实现这个接口来定义自己的监听器,监听 Context 容器的启停事件。Spring 就是这么做的。ServletContextListener 跟 Tomcat 自己的生命周期事件 LifecycleListener 是不同的。LifecycleListener 定义在生命周期管理组件中,由基类 LifeCycleBase 统一管理。 + +## Tomcat 支持异步 Servlet + +### 异步示例 + +```java +@WebServlet(urlPatterns = {"/async"}, asyncSupported = true) +public class AsyncServlet extends HttpServlet { + + //Web 应用线程池,用来处理异步 Servlet + ExecutorService executor = Executors.newSingleThreadExecutor(); + + public void service(HttpServletRequest req, HttpServletResponse resp) { + //1. 调用 startAsync 或者异步上下文 + final AsyncContext ctx = req.startAsync(); + + // 用线程池来执行耗时操作 + executor.execute(new Runnable() { + + @Override + public void run() { + + // 在这里做耗时的操作 + try { + ctx.getResponse().getWriter().println("Handling Async Servlet"); + } catch (IOException e) {} + + //3. 异步 Servlet 处理完了调用异步上下文的 complete 方法 + ctx.complete(); + } + + }); + } +} +``` + +有三个要点: + +1. 通过注解的方式来注册 Servlet,除了 @WebServlet 注解,还需要加上 asyncSupported=true 的属性,表明当前的 Servlet 是一个异步 Servlet。 +2. Web 应用程序需要调用 Request 对象的 startAsync 方法来拿到一个异步上下文 AsyncContext。这个上下文保存了请求和响应对象。 +3. Web 应用需要开启一个新线程来处理耗时的操作,处理完成后需要调用 AsyncContext 的 complete 方法。目的是告诉 Tomcat,请求已经处理完成。 + +这里请你注意,虽然异步 Servlet 允许用更长的时间来处理请求,但是也有超时限制的,默认是 30 秒,如果 30 秒内请求还没处理完,Tomcat 会触发超时机制,向浏览器返回超时错误,如果这个时候你的 Web 应用再调用`ctx.complete`方法,会得到一个 IllegalStateException 异常。 + +### 异步 Servlet 原理 + +通过上面的例子,相信你对 Servlet 的异步实现有了基本的理解。要理解 Tomcat 在这个过程都做了什么事情,关键就是要弄清楚`req.startAsync`方法和`ctx.complete`方法都做了什么。 + +#### startAsync 方法 + +startAsync 方法其实就是创建了一个异步上下文 AsyncContext 对象,AsyncContext 对象的作用是保存请求的中间信息,比如 Request 和 Response 对象等上下文信息。你来思考一下为什么需要保存这些信息呢? + +这是因为 Tomcat 的工作线程在`Request.startAsync`调用之后,就直接结束回到线程池中了,线程本身不会保存任何信息。也就是说一个请求到服务端,执行到一半,你的 Web 应用正在处理,这个时候 Tomcat 的工作线程没了,这就需要有个缓存能够保存原始的 Request 和 Response 对象,而这个缓存就是 AsyncContext。 + +有了 AsyncContext,你的 Web 应用通过它拿到 request 和 response 对象,拿到 Request 对象后就可以读取请求信息,请求处理完了还需要通过 Response 对象将 HTTP 响应发送给浏览器。 + +除了创建 AsyncContext 对象,startAsync 还需要完成一个关键任务,那就是告诉 Tomcat 当前的 Servlet 处理方法返回时,不要把响应发到浏览器,因为这个时候,响应还没生成呢;并且不能把 Request 对象和 Response 对象销毁,因为后面 Web 应用还要用呢。 + +在 Tomcat 中,负责 flush 响应数据的是 CoyoteAdaptor,它还会销毁 Request 对象和 Response 对象,因此需要通过某种机制通知 CoyoteAdaptor,具体来说是通过下面这行代码: + +```java +this.request.getCoyoteRequest().action(ActionCode.ASYNC_START, this); +``` + +你可以把它理解为一个 Callback,在这个 action 方法里设置了 Request 对象的状态,设置它为一个异步 Servlet 请求。 + +我们知道连接器是调用 CoyoteAdapter 的 service 方法来处理请求的,而 CoyoteAdapter 会调用容器的 service 方法,当容器的 service 方法返回时,CoyoteAdapter 判断当前的请求是不是异步 Servlet 请求,如果是,就不会销毁 Request 和 Response 对象,也不会把响应信息发到浏览器。你可以通过下面的代码理解一下,这是 CoyoteAdapter 的 service 方法,我对它进行了简化: + +```java +public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) { + + // 调用容器的 service 方法处理请求 + connector.getService().getContainer().getPipeline(). + getFirst().invoke(request, response); + + // 如果是异步 Servlet 请求,仅仅设置一个标志, + // 否则说明是同步 Servlet 请求,就将响应数据刷到浏览器 + if (request.isAsync()) { + async = true; + } else { + request.finishRequest(); + response.finishResponse(); + } + + // 如果不是异步 Servlet 请求,就销毁 Request 对象和 Response 对象 + if (!async) { + request.recycle(); + response.recycle(); + } +} +``` + +接下来,当 CoyoteAdaptor 的 service 方法返回到 ProtocolHandler 组件时,ProtocolHandler 判断返回值,如果当前请求是一个异步 Servlet 请求,它会把当前 Socket 的协议处理者 Processor 缓存起来,将 SocketWrapper 对象和相应的 Processor 存到一个 Map 数据结构里。 + +```java +private final Map connections = new ConcurrentHashMap<>(); +``` + +之所以要缓存是因为这个请求接下来还要接着处理,还是由原来的 Processor 来处理,通过 SocketWrapper 就能从 Map 里找到相应的 Processor。 + +#### complete 方法 + +接着我们再来看关键的`ctx.complete`方法,当请求处理完成时,Web 应用调用这个方法。那么这个方法做了些什么事情呢?最重要的就是把响应数据发送到浏览器。 + +这件事情不能由 Web 应用线程来做,也就是说`ctx.complete`方法不能直接把响应数据发送到浏览器,因为这件事情应该由 Tomcat 线程来做,但具体怎么做呢? + +我们知道,连接器中的 Endpoint 组件检测到有请求数据达到时,会创建一个 SocketProcessor 对象交给线程池去处理,因此 Endpoint 的通信处理和具体请求处理在两个线程里运行。 + +在异步 Servlet 的场景里,Web 应用通过调用`ctx.complete`方法时,也可以生成一个新的 SocketProcessor 任务类,交给线程池处理。对于异步 Servlet 请求来说,相应的 Socket 和协议处理组件 Processor 都被缓存起来了,并且这些对象都可以通过 Request 对象拿到。 + +讲到这里,你可能已经猜到`ctx.complete`是如何实现的了: + +```java +public void complete() { + // 检查状态合法性,我们先忽略这句 + check(); + + // 调用 Request 对象的 action 方法,其实就是通知连接器,这个异步请求处理完了 +request.getCoyoteRequest().action(ActionCode.ASYNC_COMPLETE, null); + +} +``` + +我们可以看到 complete 方法调用了 Request 对象的 action 方法。而在 action 方法里,则是调用了 Processor 的 processSocketEvent 方法,并且传入了操作码 OPEN_READ。 + +```java +case ASYNC_COMPLETE: { + clearDispatches(); + if (asyncStateMachine.asyncComplete()) { + processSocketEvent(SocketEvent.OPEN_READ, true); + } + break; +} +``` + +我们接着看 processSocketEvent 方法,它调用 SocketWrapper 的 processSocket 方法: + +```java +protected void processSocketEvent(SocketEvent event, boolean dispatch) { + SocketWrapperBase socketWrapper = getSocketWrapper(); + if (socketWrapper != null) { + socketWrapper.processSocket(event, dispatch); + } +} +``` + +而 SocketWrapper 的 processSocket 方法会创建 SocketProcessor 任务类,并通过 Tomcat 线程池来处理: + +```java +public boolean processSocket(SocketWrapperBase socketWrapper, + SocketEvent event, boolean dispatch) { + + if (socketWrapper == null) { + return false; + } + + SocketProcessorBase sc = processorCache.pop(); + if (sc == null) { + sc = createSocketProcessor(socketWrapper, event); + } else { + sc.reset(socketWrapper, event); + } + // 线程池运行 + Executor executor = getExecutor(); + if (dispatch && executor != null) { + executor.execute(sc); + } else { + sc.run(); + } +} +``` + +请你注意 createSocketProcessor 函数的第二个参数是 SocketEvent,这里我们传入的是 OPEN_READ。通过这个参数,我们就能控制 SocketProcessor 的行为,因为我们不需要再把请求发送到容器进行处理,只需要向浏览器端发送数据,并且重新在这个 Socket 上监听新的请求就行了。 + +## 参考资料 + +- **官方** + - [Tomcat 官方网站](http://tomcat.apache.org/) + - [Tomcat Wiki](http://wiki.apache.org/tomcat/FrontPage) + - [Tomee 官方网站](http://tomee.apache.org/) +- **教程** + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" new file mode 100644 index 00000000..3beb96e8 --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" @@ -0,0 +1,156 @@ +--- +title: Tomcat优化 +date: 2022-02-17 22:34:30 +order: 04 +categories: + - Java + - JavaEE + - 服务器 + - Tomcat +tags: + - Java + - JavaWeb + - 服务器 + - Tomcat +permalink: /pages/f9e1e6/ +--- + +# Tomcat 优化 + +## Tomcat 启动优化 + +如果 Tomcat 启动比较慢,可以考虑一些优化点 + +### 清理 Tomcat + +- **清理不必要的 Web 应用**:首先我们要做的是删除掉 webapps 文件夹下不需要的工程,一般是 host-manager、example、doc 等这些默认的工程,可能还有以前添加的但现在用不着的工程,最好把这些全都删除掉。 +- **清理 XML 配置文件**:Tomcat 在启动时会解析所有的 XML 配置文件,解析 XML 较为耗时,所以应该尽量保持配置文件的简洁。 +- **清理 JAR 文件**:JVM 的类加载器在加载类时,需要查找每一个 JAR 文件,去找到所需要的类。如果删除了不需要的 JAR 文件,查找的速度就会快一些。这里请注意:**Web 应用中的 lib 目录下不应该出现 Servlet API 或者 Tomcat 自身的 JAR**,这些 JAR 由 Tomcat 负责提供。 +- **清理其他文件**:及时清理日志,删除 logs 文件夹下不需要的日志文件。同样还有 work 文件夹下的 catalina 文件夹,它其实是 Tomcat 把 JSP 转换为 Class 文件的工作目录。有时候我们也许会遇到修改了代码,重启了 Tomcat,但是仍没效果,这时候便可以删除掉这个文件夹,Tomcat 下次启动的时候会重新生成。 + +### 禁止 Tomcat TLD 扫描 + +Tomcat 为了支持 JSP,在应用启动的时候会扫描 JAR 包里面的 TLD 文件,加载里面定义的标签库。所以在 Tomcat 的启动日志里,你可能会碰到这种提示: + +> At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time. + +Tomcat 的意思是,我扫描了你 Web 应用下的 JAR 包,发现 JAR 包里没有 TLD 文件。我建议配置一下 Tomcat 不要去扫描这些 JAR 包,这样可以提高 Tomcat 的启动速度,并节省 JSP 编译时间。 + +如何配置不去扫描这些 JAR 包呢,这里分两种情况: + +- 如果你的项目没有使用 JSP 作为 Web 页面模板,而是使用 Velocity 之类的模板引擎,你完全可以把 TLD 扫描禁止掉。方法是,找到 Tomcat 的`conf/`目录下的`context.xml`文件,在这个文件里 Context 标签下,加上**JarScanner**和**JarScanFilter**子标签,像下面这样。 + + ```xml + + + + + + ``` + +- 如果你的项目使用了 JSP 作为 Web 页面模块,意味着 TLD 扫描无法避免,但是我们可以通过配置来告诉 Tomcat,只扫描那些包含 TLD 文件的 JAR 包。方法是,找到 Tomcat 的`conf/`目录下的`catalina.properties`文件,在这个文件里的 jarsToSkip 配置项中,加上你的 JAR 包。 + + ``` + tomcat.util.scan.StandardJarScanFilter.jarsToSkip=xxx.jar + ``` + +### 关闭 WebSocket 支持 + +Tomcat 会扫描 WebSocket 注解的 API 实现,比如 `@ServerEndpoint` 注解的类。如果不需要使用 WebSockets 就可以关闭它。具体方法是,找到 Tomcat 的 `conf/` 目录下的 `context.xml` 文件,给 `Context` 标签加一个 **`containerSciFilter`** 的属性: + +```xml + +... + +``` + +更进一步,如果你不需要 WebSockets 这个功能,你可以把 Tomcat `lib` 目录下的 `websocket-api.jar` 和 `tomcat-websocket.jar` 这两个 JAR 文件删除掉,进一步提高性能。 + +### 关闭 JSP 支持 + +如果不需要使用 JSP,可以关闭 JSP 功能: + +```xml + +... + +``` + +如果要同时关闭 WebSocket 和 Jsp,可以这样配置: + +```xml + +... + +``` + +### 禁止扫描 Servlet 注解 + +Servlet 3.0 引入了注解 Servlet,Tomcat 为了支持这个特性,会在 Web 应用启动时扫描你的类文件,因此如果你没有使用 Servlet 注解这个功能,可以告诉 Tomcat 不要去扫描 Servlet 注解。具体配置方法是,在你的 Web 应用的`web.xml`文件中,设置``元素的属性`metadata-complete="true"`,像下面这样。 + +```xml + + +``` + +`metadata-complete` 的意思是,`web.xml` 里配置的 Servlet 是完整的,不需要再去库类中找 Servlet 的定义。 + +### 配置 Web-Fragment 扫描 + +Servlet 3.0 还引入了“Web 模块部署描述符片段”的 `web-fragment.xml`,这是一个部署描述文件,可以完成 `web.xml` 的配置功能。而这个 `web-fragment.xml` 文件必须存放在 JAR 文件的 `META-INF` 目录下,而 JAR 包通常放在 `WEB-INF/lib` 目录下,因此 Tomcat 需要对 JAR 文件进行扫描才能支持这个功能。 + +可以通过配置 `web.xml` 里面的 `` 元素直接指定了哪些 JAR 包需要扫描 `web fragment`,如果 `` 元素是空的, 则表示不需要扫描,像下面这样。 + +```xml + +... + +... + +``` + +### 随机数熵源优化 + +Tomcat 7 以上的版本依赖 Java 的 SecureRandom 类来生成随机数,比如 Session ID。而 JVM 默认使用阻塞式熵源(`/dev/random`), 在某些情况下就会导致 Tomcat 启动变慢。当阻塞时间较长时, 你会看到这样一条警告日志: + +``` + org.apache.catalina.util.SessionIdGenerator createSecureRandom +INFO: Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [8152] milliseconds. +``` + +解决方案是通过设置,让 JVM 使用非阻塞式的熵源。 + +我们可以设置 JVM 的参数: + +``` +-Djava.security.egd=file:/dev/./urandom +``` + +或者是设置 `java.security` 文件,位于 `$JAVA_HOME/jre/lib/security` 目录之下: `securerandom.source=file:/dev/./urandom` + +这里请你注意,`/dev/./urandom` 中间有个 `./` 的原因是 Oracle JRE 中的 Bug,Java 8 里面的 SecureRandom 类已经修正这个 Bug。 阻塞式的熵源(`/dev/random`)安全性较高, 非阻塞式的熵源(`/dev/./urandom`)安全性会低一些,因为如果你对随机数的要求比较高, 可以考虑使用硬件方式生成熵源。 + +### 并行启动多个 Web 应用 + +Tomcat 启动的时候,默认情况下 Web 应用都是一个一个启动的,等所有 Web 应用全部启动完成,Tomcat 才算启动完毕。如果在一个 Tomcat 下有多个 Web 应用,为了优化启动速度,你可以配置多个应用程序并行启动,可以通过修改 `server.xml` 中 Host 元素的 `startStopThreads` 属性来完成。`startStopThreads` 的值表示你想用多少个线程来启动你的 Web 应用,如果设成 0 表示你要并行启动 Web 应用,像下面这样的配置。 + +```xml + + ... + + ... + + ... + +``` + +需要注意的是,Engine 元素里也配置了这个参数,这意味着如果你的 Tomcat 配置了多个 Host(虚拟主机),Tomcat 会以并行的方式启动多个 Host。 + +## 参考资料 + +- **官方** + - [Tomcat 官方网站](http://tomcat.apache.org/) + - [Tomcat Wiki](http://wiki.apache.org/tomcat/FrontPage) + - [Tomee 官方网站](http://tomee.apache.org/) +- **教程** + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" new file mode 100644 index 00000000..a841530f --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" @@ -0,0 +1,35 @@ +--- +title: Tomcat 和 Jetty +date: 2022-02-17 22:34:30 +order: 05 +categories: + - Java + - JavaEE + - 服务器 + - Tomcat +tags: + - Java + - JavaWeb + - 服务器 + - Tomcat + - Jetty +permalink: /pages/f37326/ +--- + +## Tomcat 和 Jetty + +Web 容器 Tomcat 或 Jetty,作为重要的系统中间件,连接着浏览器和你的 Web 应用,并且支撑着 Web 程序的运行,可以说,**弄懂了 Tomcat 和 Jetty 的原理,Java Web 开发对你来说就毫无秘密可言**。 + +## Web 容器 + +早期的 Web 应用主要用于浏览新闻等静态页面,HTTP 服务器(比如 Apache、Nginx)向浏览器返回静态 HTML,浏览器负责解析 HTML,将结果呈现给用户。 + +随着互联网的发展,我们已经不满足于仅仅浏览静态页面,还希望通过一些交互操作,来获取动态结果,因此也就需要一些扩展机制能够让 HTTP 服务器调用服务端程序。 + +于是 Sun 公司推出了 Servlet 技术。你可以把 Servlet 简单理解为运行在服务端的 Java 小程序,但是 Servlet 没有 main 方法,不能独立运行,因此必须把它部署到 Servlet 容器中,由容器来实例化并调用 Servlet。 + +而 Tomcat 和 Jetty 就是一个 Servlet 容器。为了方便使用,它们也具有 HTTP 服务器的功能,因此**Tomcat 或者 Jetty 就是一个“HTTP 服务器 + Servlet 容器”,我们也叫它们 Web 容器。** + +其他应用服务器比如 JBoss 和 WebLogic,它们不仅仅有 Servlet 容器的功能,也包含 EJB 容器,是完整的 Java EE 应用服务器。从这个角度看,Tomcat 和 Jetty 算是一个轻量级的应用服务器。 + +在微服务架构日渐流行的今天,开发人员更喜欢稳定的、轻量级的应用服务器,并且应用程序用内嵌的方式来运行 Servlet 容器也逐渐流行起来。之所以选择轻量级,是因为在微服务架构下,我们把一个大而全的单体应用,拆分成一个个功能单一的微服务,在这个过程中,服务的数量必然要增加,但为了减少资源的消耗,并且降低部署的成本,我们希望运行服务的 Web 容器也是轻量级的,Web 容器本身应该消耗较少的内存和 CPU 资源,并且由应用本身来启动一个嵌入式的 Web 容器,而不是通过 Web 容器来部署和启动应用,这样可以降低应用部署的复杂度。 \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" new file mode 100644 index 00000000..6204b0c8 --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" @@ -0,0 +1,36 @@ +--- +title: Tomcat 教程 +date: 2022-02-18 08:53:11 +categories: + - Java + - JavaEE + - 服务器 + - Tomcat +tags: + - Java + - JavaWeb + - 服务器 + - Tomcat +permalink: /pages/33e817/ +hidden: true +index: false +--- + +# Tomcat 教程 + +## 📖 内容 + +- [Tomcat 快速入门](01.Tomcat快速入门.md) +- [Tomcat 连接器](02.Tomcat连接器.md) +- [Tomcat 容器](03.Tomcat容器.md) +- [Tomcat 优化](04.Tomcat优化.md) +- [Tomcat 和 Jetty](05.Tomcat和Jetty.md) + +## 📚 资料 + +- [Tomcat 官网](http://tomcat.apache.org/) +- [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" new file mode 100644 index 00000000..db02b55c --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" @@ -0,0 +1,677 @@ +--- +title: Jetty 快速入门 +date: 2022-02-17 22:34:30 +order: 02 +categories: + - Java + - JavaEE + - 服务器 +tags: + - Java + - JavaWeb + - 服务器 + - Jetty +permalink: /pages/ec364e/ +--- + +# Jetty 快速入门 + +## Jetty 简介 + +**jetty 是什么?** + +jetty 是轻量级的 web 服务器和 servlet 引擎。 + +它的最大特点是:可以很方便的作为**嵌入式服务器**。 + +它是 eclipse 的一个开源项目。不用怀疑,就是你常用的那个 eclipse。 + +它是使用 Java 开发的,所以天然对 Java 支持良好。 + +[官方网址](http://www.eclipse.org/jetty/index.html) + +[github 源码地址](https://github.com/eclipse/jetty.project) + +**什么是嵌入式服务器?** + +以 jetty 来说明,就是只要引入 jetty 的 jar 包,可以通过直接调用其 API 的方式来启动 web 服务。 + +用过 Tomcat、Resin 等服务器的朋友想必不会陌生那一套安装、配置、部署的流程吧,还是挺繁琐的。使用 jetty,就不需要这些过程了。 + +jetty 非常适用于项目的开发、测试,因为非常快捷。如果想用于生产环境,则需要谨慎考虑,它不一定能像成熟的 Tomcat、Resin 等服务器一样支持企业级 Java EE 的需要。 + +## Jetty 的使用 + +我觉得嵌入式启动方式的一个好处在于:可以直接运行项目,无需每次部署都得再配置服务器。 + +jetty 的嵌入式启动使用有两种方式: + +API 方式 + +maven 插件方式 + +### API 方式 + +添加 maven 依赖 + +```xml + + org.eclipse.jetty + jetty-webapp + 9.3.2.v20150730 + test + + + org.eclipse.jetty + jetty-annotations + 9.3.2.v20150730 + test + + + org.eclipse.jetty + apache-jsp + 9.3.2.v20150730 + test + + + org.eclipse.jetty + apache-jstl + 9.3.2.v20150730 + test + +``` + +官方的启动代码 + +```java +public class SplitFileServer +{ + public static void main( String[] args ) throws Exception + { + // 创建Server对象,并绑定端口 + Server server = new Server(); + ServerConnector connector = new ServerConnector(server); + connector.setPort(8090); + server.setConnectors(new Connector[] { connector }); + + // 创建上下文句柄,绑定上下文路径。这样启动后的url就会是:http://host:port/context + ResourceHandler rh0 = new ResourceHandler(); + ContextHandler context0 = new ContextHandler(); + context0.setContextPath("/"); + + // 绑定测试资源目录(在本例的配置目录dir0的路径是src/test/resources/dir0) + File dir0 = MavenTestingUtils.getTestResourceDir("dir0"); + context0.setBaseResource(Resource.newResource(dir0)); + context0.setHandler(rh0); + + // 和上面的例子一样 + ResourceHandler rh1 = new ResourceHandler(); + ContextHandler context1 = new ContextHandler(); + context1.setContextPath("/"); + File dir1 = MavenTestingUtils.getTestResourceDir("dir1"); + context1.setBaseResource(Resource.newResource(dir1)); + context1.setHandler(rh1); + + // 绑定两个资源句柄 + ContextHandlerCollection contexts = new ContextHandlerCollection(); + contexts.setHandlers(new Handler[] { context0, context1 }); + server.setHandler(contexts); + + // 启动 + server.start(); + + // 打印dump时的信息 + System.out.println(server.dump()); + + // join当前线程 + server.join(); + } +} +``` + +直接运行 Main 方法,就可以启动 web 服务。 + +**_注:以上代码在 eclipse 中运行没有问题,如果想在 Intellij 中运行还需要为它指定配置文件。_** + +如果想了解在 Eclipse 和 Intellij 都能运行的通用方法可以参考我的 github 代码示例。 + +我的实现也是参考 springside 的方式。 + +代码行数有点多,不在这里贴代码了。 + +[完整参考代码](https://github.com/dunwu/spring-notes) + +### Maven 插件方式 + +如果你熟悉 maven,那么实在太简单了 + +**_注: Maven 版本必须在 3.3 及以上版本。_** + +(1) 添加 maven 插件 + +```xml + + org.eclipse.jetty + jetty-maven-plugin + 9.3.12.v20160915 + +``` + +(2) 执行 maven 命令: + +``` +mvn jetty:run +``` + +讲真,就是这么简单。jetty 默认会为你创建一个 web 服务,地址为 127.0.0.1:8080。 + +当然,你也可以在插件中配置你的 webapp 环境 + +```xml + + org.eclipse.jetty + jetty-maven-plugin + 9.3.12.v20160915 + + + ${project.basedir}/src/staticfiles + + + + / + ${project.basedir}/src/over/here/web.xml + ${project.basedir}/src/over/here/jetty-env.xml + + + + ${project.basedir}/somewhere/else + + + **/Foo.class + + + + src/mydir + src/myfile.txt + + + + + + src/other-resources + + **/*.xml + **/*.properties + + + **/myspecial.xml + **/myspecial.properties + + + + + +``` + +官方给的 jetty-env.xml 范例 + +```xml + + + + + + + + gargle + 100 + true + + + + + wiggle + 55.0 + true + + + + + jdbc/mydatasource99 + + + org.apache.derby.jdbc.EmbeddedXADataSource + databaseName=testdb99;createDatabase=create + mydatasource99 + + + + + +``` + +## Jetty 的架构 + +### Jetty 架构简介 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127154145.jpg) + +Jetty Server 就是由多个 Connector(连接器)、多个 Handler(处理器),以及一个线程池组成。 + +跟 Tomcat 一样,Jetty 也有 HTTP 服务器和 Servlet 容器的功能,因此 Jetty 中的 Connector 组件和 Handler 组件分别来实现这两个功能,而这两个组件工作时所需要的线程资源都直接从一个全局线程池 ThreadPool 中获取。 + +Jetty Server 可以有多个 Connector 在不同的端口上监听客户请求,而对于请求处理的 Handler 组件,也可以根据具体场景使用不同的 Handler。这样的设计提高了 Jetty 的灵活性,需要支持 Servlet,则可以使用 ServletHandler;需要支持 Session,则再增加一个 SessionHandler。也就是说我们可以不使用 Servlet 或者 Session,只要不配置这个 Handler 就行了。 + +为了启动和协调上面的核心组件工作,Jetty 提供了一个 Server 类来做这个事情,它负责创建并初始化 Connector、Handler、ThreadPool 组件,然后调用 start 方法启动它们。 + +### Jetty 和 Tomcat 架构区别 + +对比一下 Tomcat 的整体架构图,你会发现 Tomcat 在整体上跟 Jetty 很相似,它们的第一个区别是 Jetty 中没有 Service 的概念,Tomcat 中的 Service 包装了多个连接器和一个容器组件,一个 Tomcat 实例可以配置多个 Service,不同的 Service 通过不同的连接器监听不同的端口;而 Jetty 中 Connector 是被所有 Handler 共享的。 + +第二个区别是,在 Tomcat 中每个连接器都有自己的线程池,而在 Jetty 中所有的 Connector 共享一个全局的线程池。 + +### Connector 组件 + +跟 Tomcat 一样,Connector 的主要功能是对 I/O 模型和应用层协议的封装。I/O 模型方面,最新的 Jetty 9 版本只支持 NIO,因此 Jetty 的 Connector 设计有明显的 Java NIO 通信模型的痕迹。至于应用层协议方面,跟 Tomcat 的 Processor 一样,Jetty 抽象出了 Connection 组件来封装应用层协议的差异。 + +服务端在 NIO 通信上主要完成了三件事情:**监听连接、I/O 事件查询以及数据读写**。因此 Jetty 设计了**Acceptor、SelectorManager 和 Connection 来分别做这三件事情** + +#### Acceptor + +**Acceptor 用于接受请求**。跟 Tomcat 一样,Jetty 也有独立的 Acceptor 线程组用于处理连接请求。在 `Connector` 的实现类 `ServerConnector` 中,有一个 `_acceptors` 的数组,在 Connector 启动的时候, 会根据 `_acceptors` 数组的长度创建对应数量的 Acceptor,而 Acceptor 的个数可以配置。 + +```java +for (int i = 0; i < _acceptors.length; i++) +{ + Acceptor a = new Acceptor(i); + getExecutor().execute(a); +} +``` + +`Acceptor` 是 `ServerConnector` 中的一个内部类,同时也是一个 `Runnable`,`Acceptor` 线程是通过 `getExecutor()` 得到的线程池来执行的,前面提到这是一个全局的线程池。 + +`Acceptor` 通过阻塞的方式来接受连接,这一点跟 Tomcat 也是一样的。 + +```java +public void accept(int acceptorID) throws IOException +{ + ServerSocketChannel serverChannel = _acceptChannel; + if (serverChannel != null && serverChannel.isOpen()) + { + // 这里是阻塞的 + SocketChannel channel = serverChannel.accept(); + // 执行到这里时说明有请求进来了 + accepted(channel); + } +} +``` + +接受连接成功后会调用 `accepted()` 函数,`accepted()` 函数中会将 `SocketChannel` 设置为非阻塞模式,然后交给 `Selector` 去处理,因此这也就到了 `Selector` 的地界了。 + +```java +private void accepted(SocketChannel channel) throws IOException +{ + channel.configureBlocking(false); + Socket socket = channel.socket(); + configure(socket); + // _manager 是 SelectorManager 实例,里面管理了所有的 Selector 实例 + _manager.accept(channel); +} +``` + +**SelectorManager** + +**Jetty 的 `Selector` 由 `SelectorManager` 类管理**,而被管理的 `Selector` 叫作 `ManagedSelector`。`SelectorManager` 内部有一个 `ManagedSelector` 数组,真正干活的是 `ManagedSelector`。咱们接着上面分析,看看在 `SelectorManager` 在 `accept` 方法里做了什么。 + +```java +public void accept(SelectableChannel channel, Object attachment) +{ + // 选择一个 ManagedSelector 来处理 Channel + final ManagedSelector selector = chooseSelector(); + // 提交一个任务 Accept 给 ManagedSelector + selector.submit(selector.new Accept(channel, attachment)); +} +``` + +SelectorManager 从本身的 Selector 数组中选择一个 Selector 来处理这个 Channel,并创建一个任务 Accept 交给 ManagedSelector,ManagedSelector 在处理这个任务主要做了两步: + +第一步,调用 Selector 的 register 方法把 Channel 注册到 Selector 上,拿到一个 SelectionKey。 + +``` + _key = _channel.register(selector, SelectionKey.OP_ACCEPT, this); +``` + +第二步,创建一个 EndPoint 和 Connection,并跟这个 SelectionKey(Channel)绑在一起: + +```java +private void createEndPoint(SelectableChannel channel, SelectionKey selectionKey) throws IOException +{ + //1. 创建 Endpoint + EndPoint endPoint = _selectorManager.newEndPoint(channel, this, selectionKey); + + //2. 创建 Connection + Connection connection = _selectorManager.newConnection(channel, endPoint, selectionKey.attachment()); + + //3. 把 Endpoint、Connection 和 SelectionKey 绑在一起 + endPoint.setConnection(connection); + selectionKey.attach(endPoint); + +} +``` + +这里需要你特别注意的是,ManagedSelector 并没有直接调用 EndPoint 的方法去处理数据,而是通过调用 EndPoint 的方法**返回一个 Runnable,然后把这个 Runnable 扔给线程池执行**,所以你能猜到,这个 Runnable 才会去真正读数据和处理请求。 + +**Connection** + +这个 Runnable 是 EndPoint 的一个内部类,它会调用 Connection 的回调方法来处理请求。Jetty 的 Connection 组件类比就是 Tomcat 的 Processor,负责具体协议的解析,得到 Request 对象,并调用 Handler 容器进行处理。下面我简单介绍一下它的具体实现类 HttpConnection 对请求和响应的处理过程。 + +**请求处理**:HttpConnection 并不会主动向 EndPoint 读取数据,而是向在 EndPoint 中注册一堆回调方法: + +``` +getEndPoint().fillInterested(_readCallback); +``` + +这段代码就是告诉 EndPoint,数据到了你就调我这些回调方法 \_readCallback 吧,有点异步 I/O 的感觉,也就是说 Jetty 在应用层面模拟了异步 I/O 模型。 + +而在回调方法 \_readCallback 里,会调用 EndPoint 的接口去读数据,读完后让 HTTP 解析器去解析字节流,HTTP 解析器会将解析后的数据,包括请求行、请求头相关信息存到 Request 对象里。 + +**响应处理**:Connection 调用 Handler 进行业务处理,Handler 会通过 Response 对象来操作响应流,向流里面写入数据,HttpConnection 再通过 EndPoint 把数据写到 Channel,这样一次响应就完成了。 + +到此你应该了解了 Connector 的工作原理,下面我画张图再来回顾一下 Connector 的工作流程。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201118175805.jpg) + +1. Acceptor 监听连接请求,当有连接请求到达时就接受连接,一个连接对应一个 Channel,Acceptor 将 Channel 交给 ManagedSelector 来处理。 + +2. ManagedSelector 把 Channel 注册到 Selector 上,并创建一个 EndPoint 和 Connection 跟这个 Channel 绑定,接着就不断地检测 I/O 事件。 + +3. I/O 事件到了就调用 EndPoint 的方法拿到一个 Runnable,并扔给线程池执行。 + +4. 线程池中调度某个线程执行 Runnable。 + +5. Runnable 执行时,调用回调函数,这个回调函数是 Connection 注册到 EndPoint 中的。 + +6. 回调函数内部实现,其实就是调用 EndPoint 的接口方法来读数据。 + +7. Connection 解析读到的数据,生成请求对象并交给 Handler 组件去处理。 + +### Handler 组件 + +Jetty 的 Handler 设计是它的一大特色,Jetty 本质就是一个 Handler 管理器,Jetty 本身就提供了一些默认 Handler 来实现 Servlet 容器的功能,你也可以定义自己的 Handler 来添加到 Jetty 中,这体现了“**微内核 + 插件**”的设计思想。 + +**Handler 就是一个接口,它有一堆实现类**,Jetty 的 Connector 组件调用这些接口来处理 Servlet 请求。 + +```java +public interface Handler extends LifeCycle, Destroyable +{ + // 处理请求的方法 + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException; + + // 每个 Handler 都关联一个 Server 组件,被 Server 管理 + public void setServer(Server server); + public Server getServer(); + + // 销毁方法相关的资源 + public void destroy(); +} +``` + +方法说明: + +- `Handler` 的 `handle` 方法跟 Tomcat 容器组件的 service 方法一样,它有 `ServletRequest` 和 `ServeletResponse` 两个参数。 +- 因为任何一个 `Handler` 都需要关联一个 `Server` 组件,`Handler` 需要被 `Server` 组件来管理。`Handler` 通过 `setServer` 和 `getServer` 方法绑定 `Server`。 +- `Handler` 会加载一些资源到内存,因此通过设置 `destroy` 方法来销毁。 + +#### Handler 继承关系 + +Handler 只是一个接口,完成具体功能的还是它的子类。那么 Handler 有哪些子类呢?它们的继承关系又是怎样的?这些子类是如何实现 Servlet 容器功能的呢? + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201118181025.png) + +在 AbstractHandler 之下有 AbstractHandlerContainer,为什么需要这个类呢?这其实是个过渡,为了实现链式调用,一个 Handler 内部必然要有其他 Handler 的引用,所以这个类的名字里才有 Container,意思就是这样的 Handler 里包含了其他 Handler 的引用。 + +HandlerWrapper 和 HandlerCollection 都是 Handler,但是这些 Handler 里还包括其他 Handler 的引用。不同的是,HandlerWrapper 只包含一个其他 Handler 的引用,而 HandlerCollection 中有一个 Handler 数组的引用。 + +HandlerWrapper 有两个子类:Server 和 ScopedHandler。 + +- Server 比较好理解,它本身是 Handler 模块的入口,必然要将请求传递给其他 Handler 来处理,为了触发其他 Handler 的调用,所以它是一个 HandlerWrapper。 +- ScopedHandler 也是一个比较重要的 Handler,实现了“具有上下文信息”的责任链调用。为什么我要强调“具有上下文信息”呢?那是因为 Servlet 规范规定 Servlet 在执行过程中是有上下文的。那么这些 Handler 在执行过程中如何访问这个上下文呢?这个上下文又存在什么地方呢?答案就是通过 ScopedHandler 来实现的。 + +HandlerCollection 其实维护了一个 Handler 数组。这是为了同时支持多个 Web 应用,如果每个 Web 应用有一个 Handler 入口,那么多个 Web 应用的 Handler 就成了一个数组,比如 Server 中就有一个 HandlerCollection,Server 会根据用户请求的 URL 从数组中选取相应的 Handler 来处理,就是选择特定的 Web 应用来处理请求。 + +Handler 可以分成三种类型: + +- 第一种是**协调 Handler**,这种 Handler 负责将请求路由到一组 Handler 中去,比如 HandlerCollection,它内部持有一个 Handler 数组,当请求到来时,它负责将请求转发到数组中的某一个 Handler。 +- 第二种是**过滤器 Handler**,这种 Handler 自己会处理请求,处理完了后再把请求转发到下一个 Handler,比如图上的 HandlerWrapper,它内部持有下一个 Handler 的引用。需要注意的是,所有继承了 HandlerWrapper 的 Handler 都具有了过滤器 Handler 的特征,比如 ContextHandler、SessionHandler 和 WebAppContext 等。 +- 第三种是**内容 Handler**,说白了就是这些 Handler 会真正调用 Servlet 来处理请求,生成响应的内容,比如 ServletHandler。如果浏览器请求的是一个静态资源,也有相应的 ResourceHandler 来处理这个请求,返回静态页面。 + +#### 实现 Servlet 规范 + +ServletHandler、ContextHandler 以及 WebAppContext 等,它们实现了 Servlet 规范。 + +Servlet 规范中有 Context、Servlet、Filter、Listener 和 Session 等,Jetty 要支持 Servlet 规范,就需要有相应的 Handler 来分别实现这些功能。因此,Jetty 设计了 3 个组件:ContextHandler、ServletHandler 和 SessionHandler 来实现 Servle 规范中规定的功能,而**WebAppContext 本身就是一个 ContextHandler**,另外它还负责管理 ServletHandler 和 SessionHandler。 + +ContextHandler 会创建并初始化 Servlet 规范里的 ServletContext 对象,同时 ContextHandler 还包含了一组能够让你的 Web 应用运行起来的 Handler,可以这样理解,Context 本身也是一种 Handler,它里面包含了其他的 Handler,这些 Handler 能处理某个特定 URL 下的请求。比如,ContextHandler 包含了一个或者多个 ServletHandler。 + +ServletHandler 实现了 Servlet 规范中的 Servlet、Filter 和 Listener 的功能。ServletHandler 依赖 FilterHolder、ServletHolder、ServletMapping、FilterMapping 这四大组件。FilterHolder 和 ServletHolder 分别是 Filter 和 Servlet 的包装类,每一个 Servlet 与路径的映射会被封装成 ServletMapping,而 Filter 与拦截 URL 的映射会被封装成 FilterMapping。 + +SessionHandler 用来管理 Session。除此之外 WebAppContext 还有一些通用功能的 Handler,比如 SecurityHandler 和 GzipHandler,同样从名字可以知道这些 Handler 的功能分别是安全控制和压缩 / 解压缩。 + +WebAppContext 会将这些 Handler 构建成一个执行链,通过这个链会最终调用到我们的业务 Servlet。 + +## Jetty 的线程策略 + +### 传统 Selector 编程模型 + +常规的 NIO 编程思路是,将 I/O 事件的侦测和请求的处理分别用不同的线程处理。具体过程是: + +启动一个线程,在一个死循环里不断地调用 select 方法,检测 Channel 的 I/O 状态,一旦 I/O 事件达到,比如数据就绪,就把该 I/O 事件以及一些数据包装成一个 Runnable,将 Runnable 放到新线程中去处理。 + +在这个过程中按照职责划分,有两个线程在干活,一个是 I/O 事件检测线程,另一个是 I/O 事件处理线程。这样的好处是它们互不干扰和阻塞对方。 + +### Jetty 的 Selector 编程模型 + +将 I/O 事件检测和业务处理这两种工作分开的思路也有缺点:当 Selector 检测读就绪事件时,数据已经被拷贝到内核中的缓存了,同时 CPU 的缓存中也有这些数据了,我们知道 CPU 本身的缓存比内存快多了,这时当应用程序去读取这些数据时,如果用另一个线程去读,很有可能这个读线程使用另一个 CPU 核,而不是之前那个检测数据就绪的 CPU 核,这样 CPU 缓存中的数据就用不上了,并且线程切换也需要开销。 + +因此 Jetty 的 Connector 做了一个大胆尝试,那就是**把 I/O 事件的生产和消费放到同一个线程来处理**,如果这两个任务由同一个线程来执行,如果执行过程中线程不阻塞,操作系统会用同一个 CPU 核来执行这两个任务,这样就能利用 CPU 缓存了。 + +#### ManagedSelector + +ManagedSelector 的本质就是一个 Selector,负责 I/O 事件的检测和分发。为了方便使用,Jetty 在 Java 原生的 Selector 上做了一些扩展,就变成了 ManagedSelector,我们先来看看它有哪些成员变量: + +```java +public class ManagedSelector extends ContainerLifeCycle implements Dumpable +{ + // 原子变量,表明当前的 ManagedSelector 是否已经启动 + private final AtomicBoolean _started = new AtomicBoolean(false); + + // 表明是否阻塞在 select 调用上 + private boolean _selecting = false; + + // 管理器的引用,SelectorManager 管理若干 ManagedSelector 的生命周期 + private final SelectorManager _selectorManager; + + //ManagedSelector 不止一个,为它们每人分配一个 id + private final int _id; + + // 关键的执行策略,生产者和消费者是否在同一个线程处理由它决定 + private final ExecutionStrategy _strategy; + + //Java 原生的 Selector + private Selector _selector; + + //"Selector 更新任务 " 队列 + private Deque _updates = new ArrayDeque<>(); + private Deque _updateable = new ArrayDeque<>(); + + ... +} +``` + +这些成员变量中其他的都好理解,就是“Selector 更新任务”队列`_updates`和执行策略`_strategy`可能不是很直观。 + +#### SelectorUpdate 接口 + +为什么需要一个“Selector 更新任务”队列呢,对于 Selector 的用户来说,我们对 Selector 的操作无非是将 Channel 注册到 Selector 或者告诉 Selector 我对什么 I/O 事件感兴趣,那么这些操作其实就是对 Selector 状态的更新,Jetty 把这些操作抽象成 SelectorUpdate 接口。 + +``` +/** + * A selector update to be done when the selector has been woken. + */ +public interface SelectorUpdate +{ + void update(Selector selector); +} +``` + +这意味着如果你不能直接操作 ManageSelector 中的 Selector,而是需要向 ManagedSelector 提交一个任务类,这个类需要实现 SelectorUpdate 接口 update 方法,在 update 方法里定义你想要对 ManagedSelector 做的操作。 + +比如 Connector 中 Endpoint 组件对读就绪事件感兴趣,它就向 ManagedSelector 提交了一个内部任务类 ManagedSelector.SelectorUpdate: + +``` +_selector.submit(_updateKeyAction); +``` + +这个`_updateKeyAction`就是一个 SelectorUpdate 实例,它的 update 方法实现如下: + +``` +private final ManagedSelector.SelectorUpdate _updateKeyAction = new ManagedSelector.SelectorUpdate() +{ + @Override + public void update(Selector selector) + { + // 这里的 updateKey 其实就是调用了 SelectionKey.interestOps(OP_READ); + updateKey(); + } +}; +``` + +我们看到在 update 方法里,调用了 SelectionKey 类的 interestOps 方法,传入的参数是`OP_READ`,意思是现在我对这个 Channel 上的读就绪事件感兴趣了。 + +那谁来负责执行这些 update 方法呢,答案是 ManagedSelector 自己,它在一个死循环里拉取这些 SelectorUpdate 任务类逐个执行。 + +#### Selectable 接口 + +那 I/O 事件到达时,ManagedSelector 怎么知道应该调哪个函数来处理呢?其实也是通过一个任务类接口,这个接口就是 Selectable,它返回一个 Runnable,这个 Runnable 其实就是 I/O 事件就绪时相应的处理逻辑。 + +``` +public interface Selectable +{ + // 当某一个 Channel 的 I/O 事件就绪后,ManagedSelector 会调用的回调函数 + Runnable onSelected(); + + // 当所有事件处理完了之后 ManagedSelector 会调的回调函数,我们先忽略。 + void updateKey(); +} +``` + +ManagedSelector 在检测到某个 Channel 上的 I/O 事件就绪时,也就是说这个 Channel 被选中了,ManagedSelector 调用这个 Channel 所绑定的附件类的 onSelected 方法来拿到一个 Runnable。 + +这句话有点绕,其实就是 ManagedSelector 的使用者,比如 Endpoint 组件在向 ManagedSelector 注册读就绪事件时,同时也要告诉 ManagedSelector 在事件就绪时执行什么任务,具体来说就是传入一个附件类,这个附件类需要实现 Selectable 接口。ManagedSelector 通过调用这个 onSelected 拿到一个 Runnable,然后把 Runnable 扔给线程池去执行。 + +那 Endpoint 的 onSelected 是如何实现的呢? + +``` +@Override +public Runnable onSelected() +{ + int readyOps = _key.readyOps(); + + boolean fillable = (readyOps & SelectionKey.OP_READ) != 0; + boolean flushable = (readyOps & SelectionKey.OP_WRITE) != 0; + + // return task to complete the job + Runnable task= fillable + ? (flushable + ? _runCompleteWriteFillable + : _runFillable) + : (flushable + ? _runCompleteWrite + : null); + + return task; +} +``` + +上面的代码逻辑很简单,就是读事件到了就读,写事件到了就写。 + +#### ExecutionStrategy + +铺垫了这么多,终于要上主菜了。前面我主要介绍了 ManagedSelector 的使用者如何跟 ManagedSelector 交互,也就是如何注册 Channel 以及 I/O 事件,提供什么样的处理类来处理 I/O 事件,接下来我们来看看 ManagedSelector 是如何统一管理和维护用户注册的 Channel 集合。再回到今天开始的讨论,ManagedSelector 将 I/O 事件的生产和消费看作是生产者消费者模式,为了充分利用 CPU 缓存,生产和消费尽量放到同一个线程处理,那这是如何实现的呢?Jetty 定义了 ExecutionStrategy 接口: + +``` +public interface ExecutionStrategy +{ + // 只在 HTTP2 中用到,简单起见,我们先忽略这个方法。 + public void dispatch(); + + // 实现具体执行策略,任务生产出来后可能由当前线程执行,也可能由新线程来执行 + public void produce(); + + // 任务的生产委托给 Producer 内部接口, + public interface Producer + { + // 生产一个 Runnable(任务) + Runnable produce(); + } +} +``` + +我们看到 ExecutionStrategy 接口比较简单,它将具体任务的生产委托内部接口 Producer,而在自己的 produce 方法里来实现具体执行逻辑,**也就是生产出来的任务要么由当前线程执行,要么放到新线程中执行**。Jetty 提供了一些具体策略实现类:ProduceConsume、ProduceExecuteConsume、ExecuteProduceConsume 和 EatWhatYouKill。它们的区别是: + +- ProduceConsume:任务生产者自己依次生产和执行任务,对应到 NIO 通信模型就是用一个线程来侦测和处理一个 ManagedSelector 上所有的 I/O 事件,后面的 I/O 事件要等待前面的 I/O 事件处理完,效率明显不高。通过图来理解,图中绿色表示生产一个任务,蓝色表示执行这个任务。 + +![img](data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAvUAAAA1CAYAAADBNRQQAAAUuUlEQVR42u2dCXgV5dXHyUIWQAJJQEF2P7e6VgG1qIUY9iqI8hFQaB/7tdbuaqmtC1pEUZa6VEVFK2KlSkEp2ycoi6IIQsUgAgkEEnYIYctCCLnz9py5Z8Kb4d4scO/NzJ3/eZ7/c+fOzL25mXPueX9z5sx7GzWCwWAwGAwGg8FgMFjUWgwpVlOcpngHKq4Oive4Ah2PuAA+dvJnj4dvoTOIb+QvKFz+ig2yjFhzrr8sxQD1YF4CeuvLnUBKJCWRkh2qJJsSteVkqJoStWOky+nHq0kNvodfoUC5INH2HPkLCnUuTQySSxFrzvQX80xjYRuAPcwzQG/BPH8RmpFSSC1JqaQ0UrqDlCZKFbXUlKptd9rnbohjZB0X9mdzES+3EDnZv2kBfAnfQsFyQAvbYyryFxQif+kx1kLLoym2eEOsOctf7J9zNMAH2MOi3qwKfYJURlOe29xfOV2TN5K+668mZvdTz37dRz39VR81YlolZNPwv1eQTqisN46rYa8XK/JvG7f4dtL6fmrCur5q/Jre8CVUS3yXqWGvHeP4bo/8BUUo1tog1twj8ldTG9jDYFEL9fFyJstntee6JVFN2uCHPk5SY1fcgoEnyECU9Wa5yppaqoZOOcyJ7SK3+HZidl/1zNre6skvMuFLqMb45hPWO18p4vi+DPkLilCsXYRYcxXUt5TCZWMpZMJgUQv1CdJy04rUyS2JiisPz6ztY0LfY5/0xMATbCDiKj1Vlu54aT8ntq6uGITW+6tK477srR5f2gu+hGqI7zL1v68eUXe8uIfjuwfyFxShWOuKWHMV1J8rbVNJAvUxaMOBRaPFySUprtK3JV3iikRlXk70Vx6e+DRD/WnBzRh4ahiIuEo/5IVdnNh+6J5BqLdZVXr4IwAPFCy+T5hXobhyevtzBRzf/ZC/oAjFWi/EmqugvoNU65OlOwFQD4taqE+SYOegv9ItiWrCN33VU6t6qzFLMtToORgUg0K92XpziAaifE5s/d0B9f5e+ieWZ6iH5sG3UHDQGja1RN358gE1eHIex/cQ5C8oQrHWH7HmKqjvIjfSWi04gHpYVJrVT893i3ciXeuGRGX1CHJ7Bl9OfHDWjRh4aqwuFdJAtI0T2yC3VJaeWp1ptt4AeKBgsnqcubVs0KQtHN8jkL+gCMXaYMSaq6D+QlJruWEWUA+LeqhPkzPZbu5KVJnq0cU91QMze2DgiZJKpr2y9IcPboIvoVpAa5+6bUIOx/dI5C8oQrF2B2LNVVB/sUB9M0A9zAtQz5elLiB1d1OienJlpnpkUU91//sYFGuD+kGTtnJiG+oqqOfK0mxUMaFaQOtvewm0NnN8j0L+giIUa0MRa66C+kvkZllAPQxQj0QFqAfUQ4B65C/EGqAeUA+DAeqRqAD1gHoIUI/8BahHrAHqYTBAPRIVoB6CAPUQoB6xBqiHwQD1gHpAPQSoR/6CAPWAekA9DFCPRAWoB9RDgHrkL0A9Yg1QD4MB6pGoAPWAeghQj/wFqEesAephMGdC/e6yTcpuJ3ylanfpJrV0/2sBXzNn51i1q3Sj+sf237kuUeUcME77fw3DUEWlSq3KN9SDc3ynveanMyrVd/sMNX2N4Tqor49/n9/8I/V54TuqsHy7Kq8sNX08d9fTgPowxda4xf64KjlhqJ2HDfXhep8aNR1QH674fiV3mPr2yCJ1rKJQHa8sVttK1qoZ2+/3HGjVN07fXGWo/EOGOn7SUNvp8bUvfZ6E+jMZK1n/3vWkmUuX7X/dkbF2JmMi6+fvVapv9xpq7U4DUB8Ji4mJKYiPj1c16Cva7be8TI99nPCZY2Nj/yKfp6ttUzKtX06Po8Hz4YH6bSVr1Iajn5jKOfY5QV2JuX7VwZnVoI8TVOnJw+a2fxX82bVQ/5+dSi3bapj6LM9QBYf96w9RImOIt/bnhLZ+jz/RLdrsXqivi3+/PbLYXFdQkq3WHZ6viiuKzOdzdz0FqA9xbI3/xGcOnPuOKbVwk6Fy5bUrthmA+jDEN+eu/ce3mus20/bc4pWq0qggKDuupuXd60mor0uc/vNr/7q8g4ZanGOow2X+XPjWV4Znob4uudTSq1tGqLKTR8ztXx+a62ior+uYaGllvn87xwSgPjKA/HJcXNxMFgH+BgH5NdY62v60C6CenXcFfd6PeD3/T+E4/+E/HURxDlSsBHcTUivS/5Cu++ums0tU9orVW3k/M6GDKxE8IC6lKgMPgrq5Geofnl+9+nAXaUuhf9ukZT71m1mVqrSiegXDzVBfm3+t5wz01j5/3/Z/tM5nVqIA9aGLLV63rchQJ32G+uVM/z53v31qn1/9C1Af6vjmK05sa4o+qNrnswPTzHWfF073JNTXFqc/fqdSlVN1fs9RVXUF6fcf+MztXLn3KtTXFmv6Nj55PFpxwBVQX5e8Zen5z3zqBMXGkePhgfrhb52kcbCC/XUp6TzSOaQEh3NZIIXtBOQxhmKC43ts6x0N9XQykqtfXQgD1MdoUG85p7GmBAeKP1eSnLnyGeyFFtSfCdgHS1Qsq1L7+taR6sOdf6mqTljrownqWUty/dte/sKnfvF+pVq6xTC1eb8RdVBv9+/CPZPM5SX7Xq22T8nJQ+aVGUB96GKLYYlt477q8bRgo3+fx//fB6gPcXyvLHzXrKj+M//Bqu2zd4wxt688OANQHyBO/zjXZ7aGzfnWVw30uNix8wigPlisWesW7J5oFsL46rYbod4eD9Y6HhuPlRvq3bWGWZwINdSbQH8K6r9HakNqId0JTuayxtJFEa+BftiuLNQI9bR+FGkOQXQRPS6k9bdqoP066X1azKTtObT/57Ipg5ZX0Lqj3MoT4L2H8HraXsL7yPteqm1Pp/edTtsO8t+l5ddIE22V+kdp3V/ptR+GAep1oI8XhyQKMCdLJbypQ9VMgrwt6WLSDZM3+qG+vmAfLFFN2ZKlfEalWYF4MWdwtW3bS7+Oykr9Qf9VVPXQ3Orbpqz0RR3U2/3LsLOCKpbT8u6r2mfq1lHma/eW5QLqQxhbfCmb2xomL6++n3Xy+OtZqNSHM3+xGL52lK0399FB3+uV+mA5kMUno++t879WB32vQ32gWOP44vs2+GSSr3i6tVIfKB7W7PDfW8FXF0MN9TrQs+/IX5eT2pPSpFrvZC5LFn5M1AA/bGBfI9QTVB+WdpwxtHyAQVvOPhoxxNPzPaRiWv6M9hvLlX167qP3W0TLv6B1MwTGH5D3vZ63kzbS8oO8npYL+aSgqimceuQF1N/hXnmuypPKg/TUDwgT1Mdq1Xl2hnKpbuAkYIH9mSQqvhQ9d9d4U4v3vkD9p3nm+q3Fq097TTRAPfcuz/3OZ4p7RfmmILaPc04H92iA+vr41+pB5t5RtsV7XwLUhym2LL0iMZa9R0X9cQoH1NcnvjceXVbVUsfVVK/eKFufOOX2i0qf/3V88+RdHr5Rti6xxrnzYPkOgvzbXAP1dYmHl1f46ATGqDoBCAvUE9DzGJj1xnH215Uu5jI72EcO6rntRavMjxWw/pEF9fJ8jHYT7jfcpy9AbBr3vdO6Q1JF7k/v86o4xHrfafI+fLZ1vfzdf+jdN3zy0ABQHy9nVk1dHDw3ni3UBzKu0HK1NhqhPpBxchoToPUhGqC+Pv6dsmW4yi9dZ+6z6dintG4goD5MsWXdjMhVvkKqilk99oD68MX3wj2TzVlwuK2Mb2KcteNhz89+U1uccivOPII9hjiGutnZ3m2/qS3WFu193vw+v5c/uureJDdAfW3xwPf6cDsWx4H12nBBPfsta2op++v7LuaypAaDeoZmDc6Hyr4/sUF9C9mlKcG3wVV4guy/WaJ9Vsl+18h+7fjv0bYJ9LiA9q+U7Sn0/KeyPNjWU/9mhKFer9LzyUZLUmvp4Wonl306OEjtNHWQm2T5xOmmSev7mWDFYH8miWo13bnPFVnW/N3Pqre333faTT/RBPVjF/mnqmRxsuLpKtm4V5AvM0cb1NfVvzPy7zf7Q3lQ+qpodtAYANSffWzd826lWrfbP3jya++b6Y3jFA6or0/+OtVe9mOz57mg5BtPQn19cqAlbrng2cA4P/CUhl6E+tpijU8WeQrL2TseM8WQz7aleJX53KlQX1s88D0/fHMstw2O+7jSFM/eVUzbeZl77UMD9f4qPY+BwpMXkToJjzmZy9oIP6YKT1r3AFj99Q1zo2wgqOfWG+01raRlZz1B9mS7ZIrFIQzxpGxa9yy9192k+RbUk+6T5b62z/N4A0E9H/zm0rfFN5+erwVOR4epvaij3CR7FUP9xOyzg/r6zNccjT31rFy50/+hedHfUx9Ib2/7pdkHyoMS30SIH58KX2yNeucUHM3K9ven4senwhff31HLTWF5gXkVSl/Pv8nAYF+Xk1cv3Cirx+mL1Gqxg6Y15Ef7lSW2ict8noT62mKNf+OjJnNarNU1Hj7JNWr8v5771BdaqCe/kb+ulWktO2v841Quayv8mCY8mSxt7I6E+qO2OfAPajfMWnYL6Xdc9aZtn3KfvrS1WO87T4P6DIH0cfob0D7LGrBS30wq9a1kCqXzbZVxp+h87aTjglBV6gH1p+705znEvQb1L+UOoZluikygfzPvHvyibJhjq2pWiRU+zx2nhoB66zcY5u+eULXujbyfmCdVxRUHcaNsgDh96mN/3ltVYAR8/SMLAPWBxG03Mwv+VCVrZjGe3pKfu3H2G46H33/oU098VF083SlX6nn53vfCXqlv53AuO0/4saUbKvVHba97RCB7PB903pf2KZUfiOL3+Jgr9Vyhp6eX8d+h5xXyt3pI//wGUhkt38sQz7PcWFNXNkBPfYLcVX2OtBmliXNaO1DpcjbYRs4Q+fj2MHvqAfVnlcDmbvCdNn2XV6CepyxlKyrfSVXNJdWUfXghoD6EscVV+TKaFvBEpaGWbz1dwX7BEVB/5vH9Lv36NQM8V1HXHppj/u7GgePbzdd+QTOUAOoDxylPXcnHjX81lG/mtn5wiHupR06vBNTXQW6d/aamMTEiPfVvlLG/riZ1EWg+1+Fclq4BfRMpase7BeobE2A/Q+tPWCDON8rKP8fWjbbtsrZJG85EWd4p+3Sh56u1fXK4VacBZ79JlDOrpuKU5nJVwYlqIQHUTqYJvcEC+lBNaelFqJ/6pT+B8cDlNaj/Dw04wYxbcgD1oYutMQt9NV7G5t5WQH3o89c86n+2fg2bjX9N1g/0AwH1QXIgV2etqVYt48q9W27oBtSHZ0wMN9Rrs99cIcXL1gLMTuayc6TjI1mb1jIu3D9CFWprLFDZuoZtbbR1neWMS7d0Wd8QpoO9VbHX56p3opLkxCNFji1fmrqegb6+VfpIK1oGRTdBfaQEqIcaAurPRNx2w78C+sLmW5G/6ii+eXL0v33mzd0ujzXH51Kvx5oO9tKJ0E5uPm3mcC6z5qhPsFXoYxvBGgTs9V+VjdfU2CHSP5M1BWdr6xdlnQ70gHpAPQQ5AeqRvzwda4B6F4G9Vhhurk0R6VQui9cYEkDvALB3k+Ll7DBdbpbtXt+2GyQqQD2gHgLUI38B6hFrTgV7mfnmXKnSN3Yhq8UAr2F1sdOgHokKUA+ohwD1yF8QoD5aVAPUw2CAeiQqQD2gHgLUI38B6hFrgHoYDFCPRAWoB9RDgHrkL0A9Yg1QD4MB6pGoAPUQQAtQDwHqEWuAehgMUA+oB9RDgHrkLwhQD6iHwQD1SFSAekA9BKhH/gLUI9YA9TAYoB6JClAPqIcA9RCgHrEGqIfBAPUYFAH1EEALUA8B6hFrgHoYoB5QD6gH1EOAeuQvCFAPqIfBXA/1aaQupG5uSlTjvsxUjy7uqR6YiUGxNqgfPDmPE9sQV0H9kgz1hw9ugi+hmkHrpX0EWjkc3yORv6AIxdodiDVXQf3FpNaAephXoD6V1Il0jbsSVW/1GKq5NUJ91tRSdecrhQT12zixDXID1E/MJqhfnakeX9pLjZ5zM3wJ1QJa++lK1BaO7+HIX1CEYm0QYs1VUH+hQH1TQD0smi2OlERqSepAutINicpezQX4BYP6ChPqh045pG5/Lp8TWz/H+3ajH+rHr+mtnlieoR6aB99C0XMlCvkramKtP2LNVVDfRdqMmwDqYdEO9YmkFFJb6Tu7jtSbNJiURbqbe1UdpJGku+SzDeWBXD7r7aQ7Zf1dst8oj4r/9+FyPG4j9SX1Iv2QdAsPSJp/nXasRkrM8ecaIf9HlmiEx/0KVc8Bw7gNQmKcY7ofaQBXUSX2hyN/QSHwV5b45lYZGzmX9iRlkgaKDzFWOstfQyQf/IB0Oam9FC+TpUMBUA+LSoslJUifWbq04FwuYM9Jq48MlgMdJGvw7itJNUNTpnxma3Af6FENkOPDAH8z6QZSV1F3Ug8ZmKxj5bTPPkDzs64BmgZCno7vfgJYGRLjP5C4vlFOXjNlH+Qv6Gz91Uf8c5OMjVYuvU7iLQNjpeP81UvGvSsb+ScB4Ztkm0sRMw5QD4tmqLf66lMk8DtKxZ6/DNdoIOgUdRNdK5/vatJV8niNrLf26e5RWcfn++LH75EuEvENQ5fZ/NvNof9DV+2xu+1zetm/XpcVE9b3/wopRlwmukpi/1rkLyhEufRqibFLbLn0cs13iDXn+Iv/z0tJnUltpEpvtd7EAf1g0Qz1cVKtbyJgny5fgvZSue/iUHUWdZITkY6y7OTPHOnj00n8eL60V50nj+00/3Z22Oe+wKZA2+BfyPr+8/e+g8SzFdfttXyA/AWFwlcdJb7ayvjYRsulHRBrjvt/O4h/WpFaNPLfIJsoRcxYoB8sWi1Gq9YnSMW+mVym4jNbnhUnzaFK1R5Tbesgv1pKQksRnzaX5RYipx+vdJvStEcISpUYt+K8pS22kb+gUMaZnktTNGGsdKa/UoRnkm1Aj7YbmCfAPk6De/4CJMmXwYlKsn0+J3/Whpbly0RNCQGOodPUBL6D6pELkrRYT0L+gsKUS/Ucmoix0vH+StBabgD0ME+BvQX3FuDrineQ4uqheI8q0LGI1R5jXeTfePgWqkcOQP6CwumvWFsuRay5w18AehgMBoPBYDAYDOYs+y+rVzUaolvqoQAAAABJRU5ErkJggg==) + +- ProduceExecuteConsume:任务生产者开启新线程来运行任务,这是典型的 I/O 事件侦测和处理用不同的线程来处理,缺点是不能利用 CPU 缓存,并且线程切换成本高。同样我们通过一张图来理解,图中的棕色表示线程切换。 + +![img](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAucAAAEHCAYAAAANq+jXAAAw4ElEQVR42u2dCZgU5bnve/aFGRhgANlhUBHEDUFRE5OggqCIAWVRNHn03KMm95rFx+O9yRVPuEZFwMQoKhCXaGLikqMHl4hLcIvBJUYMLiAiiAKyyirgdNd936634KOZHh2mu6e6+/d7nv/T1VXVVd90Vff86uu3qiIRAAAAAAAACD0FksIkKQphkrWRttP2r9v2IAV8/AEAACCMYh5ITLGkRFJqKQthgraVJAltp+2Ntb04QdoLkHQAAAAIm5gXm7yUS1pJqiTVktYhTbW1Udta6aSKttP2Rtqu7a4wYS9JEHQAAACAFpfzQMxVyqt+9f4ILxty03sjvBnvjPCmLzrdu/Gt4Z60vZa20/amxCS91D4D9J4DAABAi1NoYq69iNrr2S5bxGrGu74gTls43LvhjWEqWj1pO21vopy3NkEvidB7DgAAACGScy1LaCvpkm2SOPXNYd4vF5ymojWAttP2Jsp5h4hf6qIHp0XIOQAAAIRBzvVnfe017yipyyZJnLbw9Hjv7ZSXT1HROpG20/Ymynk3OyitsINU5BwAAABaFO0t1F7DNpIukn5ZI4nvjIiXVlz32jDvP18YqqJ1Gm2n7U2U87qI33uuJ4kGpS0AAAAALSrneiJojaSH5MhskkQ9IfGXr57qTX4uLomjaDttb6Kc95UcFPFLW5BzAAAACI2ct5P0lAzMNkm89u+neZOf/baK1ljaTtubKOf9I/4vRirnpcg5AAAAhEnOe0mOzUZJvNqXxHG0nbY3Uc4HSLpG/Ku2IOcAAAAQGjlvL+ktGZR9kniqd/Uz31HRGk/baTtyDgAAAMg5kkjbkXMAAAAA5BzBpe3IOQAAACDnSCJtR84BAAAAkHMEl7Yj5wAAAICcI4m0HTkHAAAAQM4RXNqOnAMAAABy/jXz6Y73vER2Rbd7n25/z/vrZ7MafM2jK6d4n2x/1/v9Rz9qUUlsStt//f6Z3svr7vPW7fzI21m/Pd7+uZ9clxVtv23JeO9fn8/ztuxe531Rv9Vbtu0N7/6PfpIVbXfz35/8v/j7Pv+z2cg5AADkHwUFBSuKi4u9RvKazHa5DsvjsDC0ubCw8BfWnkEJkypk/PPyeGU63qoszH5yftN7zROtZdte9xZtfjaexVteFoHdFh+/YP2D+wiuCtb2LzfFpz204v+EQs6/Ttv/9fnT8XErti30/rnpcW/r7g3x53M/+WWo267v+WdfLI2Pe1+mL9n6ilcf2y0y/IV3z4eXhP59D3LHB+d5O778PD79zY1zD2i9uo9rksh5ASGEEJLipFx0ZxYVFT2oEVFfZEL+ejBOpl+XBXKub8wR0t6ndLz+TWmU80JLUUKKQ5SgTSWSSkmtpC4Vcp7YE3v3h//Di8Vi8R5RFcS/Sm+nSqFLWOT8q9oePFcxD+a5a9m/ybhovLc3zG3X3n3l9Q3/tWeeF9feEx/38rp7Q912d5oeVGzevTZVcn6EpJukxg5Si0P4WSWEEJIdSfS+Qidp5WqVW5HcixLGh1rO5aBiidvbnwY5L3CEvNikt8z+4WsqQppK6zU8SHKI5LgZ7/rikirR0gS9y7OXXuA9svIXe3pJg/FhlfPEtj+5anp8+Lk1d+wzz7YvN8Z/BQhz219Z94d4j/Qfl1+xZ/qfP54cn/7K+vtD3fZg3BOfTosf2OmvLgcq57pv6z6uf4e0/ShJTzs4rQ7x55QQQkj2pNwcsMSR9rRKeqNyLuMvlDwqMrxBHp+U8aMcYZ4teUAGT5Xpi2X+l23SUBl+ScZt1hKZBpY9RsfL9G06jy23nzO9VpZ7r0xbr+uV4VmSaQk95/9Xxt0kr30kDXKeKOZltnGqTHzbWM9c2BK0q9Z6D/tKjldpORBBTyZat38wwYvG6uM9ob9ZfPY+0z7a/mao5Tyx7Sq2L0kv8z0fXrZnnjlLL4y/dvWOJaFue+LrVHo/3vF2fB5X2MPadm2v1snrQYb+WnEgcu6K+fRFp2vbj5H0kXS20q4aQgghpJlu1docsNKR9KIWk3OR401W5jJZhteqMFstZ0RlXJ6vkmyV4Rdlvina0y7Po7K8eTJ8qYy736T6p7bcITpd8q4MX6HjZXidyn2wYq0hN+G+T2vJtZdcsjNJzfnINMp5IOatJF6W5sTpb58el5cDlXMtlZj7yfXxPL36Zqlz/jA+funWV/d7TdjkvCltD+q4tV5aeXr1rVnT9nc3z99TUqS90dnwvuv7vH7nxyLrZzVbzlXMpy0crm0fnMWfVUIIIdmRQNALnXObMifnWk7i9JRPMUE+M5Bzez7ZOdn0La1jtyOKOFoXLuM22lHHCFnOHfJ4pLPce2w5+jP0EFvv792qFj0IyKCcFzr12xV2xJStO9A3VFpUXlRimnvljQDtVdYe5rDLeVPafvsHE73l2/8Zn+e9LS/IuDOypu1PrpoRv2qLluLoyZUPf/yzULd93upfx3vR/7T8yj11/gci53t6zeUA9MZ/xuX8eP5pEEIISXMqraO6qEXkXOXXkexzbd7vJ8h5jc3SSiQ6pr3iIsu3BJF5Fth8A22+bro+mXajPD4h89fb9Dby/GIbPjuh5vzOFpDzUus117+vk6S7nWSptdyHhTCHWtv0sb/9xH+yildz5PxVucKG9iJrHv90qve7jy7b76S+sMr51237/ct/Eq+JVmF8bcOfk/59YWy7mzlLvxev4V6x7a1Qt10PIvTSiX/++Op4VNaVD7YuiD9vqpxPW3i6N/Ufw7TtJ1jdeT8r6zo0pJ9VQggh4U/wf6TOHFDP52tnnc1lVmHRInI+rDE515IW5zUdrBTmbZHlGYmxWtAxKuOShTJuqixrkuTxQM4ll9nw8IT2XIOct4ycN+W62WGvOW8ov1v2g3jtswqjnlDZ0jfy+bptf0dKWdbtXBHv8XfH6/XaVdC/7gFGS7RdryffGF+77cg5IYSQzMh5j2yS8837FGtLTbpzYmjAKZIf6dmuMu0FrWO3PypY7mOOnA812b7WXYDMM78Fy1ra2EmWnU3Qe9mG6hOiaHt6O23ra6VDzS5ryWU5v3XJGLkyy4a4mN/54UWhuMvm1217cH32xz+9cc+43374/Xjv/9bd60Pddi1neXDF/96T4Ko5ellFfd6MspYhEf9a54c4n4mwfVYJIYRkR+rMq9T9umgntDlhK+vAzQ45F35usny9lrHovDLPdrtRkC7jGe051x5zeXq4rkee77Z1nWT15YskO2T4EpVxvSpLcMnEFjwhtI1d/aGjHTl1tg0VlnR22tXVpETf35OCE0KR8/2jl4FUNuxcKT3Rz+2ThZueDHXb/yB3YVUR117oNzY+Gr/e/NovPoq/9m9yBZSw/2LhJsUnhGpvR0/7HHQO4WeVEEJIdiRwq46RvVcA017z8si+V2wJvZyXiCjfION3BUKtJ4TaH6YMlmmfBNOsvGWaDa+0eerk+avOPIu1BKYF5DzZpRTdS+y0DUnc9tTaUZ72ng8Jes1TdSnFXJLzf4gMJkNLXcIuuI9JPXdwV1ZF7w7qi/kZeSXnjVxKsW0IP6uEEEKyI8H/D/dSiuUJJ4OmRc7TRYnVfXZsZFpnZ1xv6+lyqbXxLUFO3YToQG9ElMmkUhLzre1azqJ34bz5/VF59b5zEyJCCCGRzN2EKChlSftNiOCrBb3QEfWiSHhvBx7UyVdaXZTWSg0Obm2O4NL2XJRzV9Cl7UdE/Btw1diXKbefJoQQ0ly3cpN1veXQ8hSZlLS3Xx0GIYm0PdflPIidDNrVfj0qpVcDAAAAkHMEl7Yj5wAAAADIOW1HzpFzAAAAQM4RXNqOnAMAAAAg57QdOUfOAQAAADlHEmk7cg4AAACAnNN22o6cAwAAAHKOJNJ25BwAAADggOX82CyWxHG0nbYj5wAAAJALct5O0ksyMPsk8TTv6me/raJ1Dm2n7U2U88ORcwAAAAijnLeV9JQcnW2S+MsFp3mTfUn8Lm2n7U2U836SzpJq5BwAAADCIudlkjaSbpIB2SSJ0xYO9657bZh3zfx4ecUZtJ22N1HOD5V0klQh5wAAABAmOdef9Q8yWRkkOUUyWjJOcp7kghBlkmSi5FzJKMlwyVDJd6zdIyVjJONpO21PaPtYyQjJNyVHRfxSrlpJpaQEOQcAAIAwyLlKSauIf1JoD0l/yWDJyZLTJKdLzghRRpoYnmptPMHaqwcVx0u+YcI4jLbT9oS268HESZJjJIdIukhqJBWSYuQcAAAAWppCkxKtO9fSlo4Rv/a8r+QIyUATsONClkEmWEfawcRh1matIdYrcBxF22l7A20/OuKfBHpwxD8RVA9ItaSlzA5UC/hKAAAAgJaW86D3vMIEXX/m1x5F7UXXyyv2MZkJS7Q9ddY2bWM3a29XS3c7wKDttD2x7b2s3VrCpVcoqrYD06DXHDkHAACAFqUgsrf3vNQEvcokXa/g0t5kXdMhRKk1uWpn7axx0tbG03ba3lDbtZ16jkUrE/MSO0ClpAUAAABCQWFkbw+6SnqZSYumMsSpsMegrWU2roK20/ZG2l5hbS61/T0Qc3rNAQAAIDQUJEh6IOphj9vWxEfaTtsba3uRs88DAAAAhFLQCcnHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZAEFlsIkKWpmki0rlevIhSR739ke7BcNpYCvLgAAgNwWcxWBYkmJpdRS1syUJqSkgXFleR73vXC3QUkD7xnbIz/3jRLbN1xpR9ABAAByTMoL7B99IGjlkkpJtaV1ClLtpJWTVK4jFxK8P1UW971qnebtUcX2yIp9o8KEvQRBBwAAyE05L7R/9CrlrdbMO9zLRMbP2eZN+O0X3sS7dnkT7/7SO++e+ryOvgfx3LU7/r7o+yPbo5btQRqKSXog6Mg5AABADsl5sfXEac9pu0zJ4Lg7PvcmzNnuTbhzZ1xIkXNfzvX90PdF3x/ZHr3ZHiSJnFeboBfbATYAAADkgJwHveZaxtJO0iVTMjj21s+8cbM2W28tMrhPr/msLfH3R7bHUWwPkkTOa+2AutTpPacHHQAAIMvlvMj+uWsvXCdJXaZkcMzNK71zb98Y762llCKQ810ixzvkfdkk788nKmDfYHuQJHLeRVIT8cvRipFzAACA3JFz/efeRtJV0j9TMnj2jGXeOTPX+rXOWkqBnMelWN+Pc27b4H33V8tVwEawPUgSOe8paW+/eiHnAAAAOSbnbe2ffcbKKEZP/0BKKdZ442dvRQYT5VwkefT0pSpg32V7kCRy3kfSweS8BDkHAADIHTnXk8q03ry3ZGCmZPCsG9/3xt6yOl5fjQw6ci5yrPXfo6ctUQEbx/YgSeT8EEnHiH95ReQcAAAgh+Rce9705/E6yaBMyeCoqe95Y3+zyj8JERncK+fxk0HXiCwvVgGbyPYgSeS8b8Q/T6QKOQcAAEDOmy+DNyCDoZJztgdyDgAAAMg5MoicE+QcAAAAkHPknO2BnAMAAAByjgwi5wQ5BwAAAOQcOWd7IOcAAACAnCODyDlBzgEAAAA5R87ZHsg5AAAAIOdhksHFa2NeIrFYzNuw3fMWLI95Vzwa3e81F99f772zJubd+3oMOQ9hmrpN71wQ85ZvjHlffBnzPpLHWX+PIucAAGBWVFCwori42Gskr8lsl+uwPA4LQ5sLCwt/Ye0Z5IweXFRUNF/+ns8li2We6+wfRSoFkqQveSfn/1jpefOXxuJ58cOYt2KTP36jCJ3KeDC/it3bq3zhm/c+ch5mOf862/SPb/rjPlwf855eHPM27fC37d2vxXJNzgkhJN+SMtGdKVL7oEakdpEJ+evBOJPcsMt5nbR9l2S5TpN2P63TZfimNMh5oaUoIcWkSWno/WuVT3L+s8f37S09X/LBOn/a9PlR7389XO9t371vjyxyHm45/6pt+r376r2d0lu+arPnXXivP8+P/ysan6496Vku59WSUr4bCSF54i1FjhOm9RfDq1VqRW4vShgfajmX5zfb88HBLCLq70i2pOjNKnCEvNh6h8ok5ZYKcsApd97L1pIOkj75KOea55b402b+Lepd+kC999cPYvG8/1kMOc9COU/cpv8xN+pt2xXzHv1XdB+B14OwlZ9nrZwfJuksaWOfab4bCSH54i4lliJH0jMr5zL+QsmjIr0b5PFJGT/KEebZkgdk8FQtK5H5X7ZJQ2X4JRm3WUtkGlj2GB0v07fpPLbcfs70WlnuvTJtva5XhmdJprlyLsOvyLS1CQL/R5unJsViXmYbpspkso2tgzQ9bew9rLb3s631wB2sB1qrn+qfdz3n67f5PeRXzd132u2vRJHzLO05T7ZNNdqT/qd/+q91hT3L5Fy/r7tI2jmfZb4bCSG57i5VTodEWgW9UTkXAd5kZS6TVYZVmO1nTBXkl+X5KslWGX5R5puiPe3yPCrLmyfDl8q4+02Yf2rLHaLTJe/K8BU6XobXqdwHK5b5n7cSlfvk6ZUybYlkZ0JZy2mSU5z2lss8KyWrU9hrHoi5ll54JG3Rf/KHSo7LdTl/aVnMm/tONB6tPdaTB5VnFu8v4Mh5dsh5U7aplrnUR/3X6Umj52fvCaH9Jd0lHfn+IoTkacrMEzMv51pO4vRMTzFBPjOQc3s+2TnZ9C2tY7ejiTiy7Kdk3EY74hghy7lDHo90lnuPLUd7YIbYen/vdorrQUADJ4QGVMo6HrG/4+JUVNFY+0vsCKk1O2Fa091+Jj9+1V/6583VWgL05MDJf4ki5zlwtZav2qZa4vKYSPyyDTEvKld2+fPCrC1rOVzS00pb+A4jhORjKpxzbjIr5/I40pHsc23e7yfIeVBG0kokOqa94iLXtwSReRbYfANtvm66Ppl2ozw+IfPX2/Q2Ktc2fHZCycqdSeR8oK5Pl+H0zqdKzkut17zGSi+624mLh5hMkqbnUKsv13/sXXVfsOG8kPMp8/xLJGp++FB9/DKJypadsXi5A3KefXLelG0aZNLv6uNX49FLL/77n7JSzgdIettnuCvfjYSQHE5f+26rs++6TlaS28o8sbgl5HxYY3KuJS3OazpYKczbItMzEmNSNkZFWrJQxk2VZU2SPB7IueQyGx6e0J5rGpDz07XcRcteZPjEVJ5/ipwj55k6eXCJXdnjqseoOc+VE0Ldbfqbl6Lex3J5RX105wkurzhtfhQ5J4QQ5Dylcr55n2JtqUl3TgwN0NrwH2lduEx7QevYrVYnWO5jjpwPtbKWa90F6PXME+T8ZFnOF3qyasS/VnYkDXIelLVou2rtJ1zdML1sI/UhTUqd/UPvYf/QD3Le07yV8+DKHtc/i5zn2tVadJv+8hl/Oy5YEWvw9T9/IprNZS1dEj7HfDcSQnLRXXrZd1xn88HW5p4tVtbSJDkXfm5yfb2WnOi8Ms92PcnTlvGM9pxrj7l9wV8uz3fbuk6y+vJFkh0yfInKuF67PLhBUiDn8vqHbD0zdV1urLY91SeEqqC3txOggn9GXUiT0tneu062c+uVHjqYqMdPCM1HOZ+7KLrnsnvIeW7IubtNtYRFL5moJSxvrIx5t8l2fWW5/1qtPb/g3qw/IbQ9342EkDxwl+D7ro15YcudEHoAcl4ignyD3iAoEGo9IdT+KGWwTPskmGblLdNseKXNUyfPX3Xm0bt/TnXkvEhPME12h1NnXc2V82SXUnQvr9OWfO24lySqtrS1nV5/Msq7Sylq5vzdFzkVNuQ8N+Q8cZv++JHonuvWB2hP+g8erM+1Syny3UgIycVk/FKK6aLEvsA7NjKtszMuqF90qbXxLQE3Icr8TYhyVs7DHuQ8M9GTRq/876h30R+y+++IcBMiQgg3Ico6Mc8VAkEvdESd21On7la4JfbTUK3VdA1CBpFztkdWyHlf+8Wr2qm55LuREJLr3lLkOGEBYg65eOBTZCdUtEfOkXO2R9bJeSf7ibeEf1IAAADIOTKInBPkHAAAAJBz5JztgZwj5wAAAMg5MoicE+QcAAAAkHPknO1BkHMAAADkHBlEzglyDgAAAMg5cs72IMg5AAAAco4MIudIMHIOAAAAyDlyjpwT5BwAACB/5VxvidtO0ltybMZkcKrI4C2rkcFEOZ+9VeT8M2/0tCUqYOPZHiSJnB8q6Rjx7/CLnAMAAOSgnPeUHJMpGdSeYZVB7SlGBhuQ8+lLVcDOYXuQJHJ+MHIOAACQm3JeLqmRdJcMyJQMqnyqhKqMIoOOnM/Z5p1z2zrv7BnLVMBGsT1IEjnXX7q0HK0SOQcAAMgtOS+TtJZ0jvg/lQ+WnCI5SzJOcr7kgmZEXz9Be4EloyVnSE6XjLTn59j085q5nmzOJMlEe7/1PRkhOU0yVDLM3rMxkvFp2B5n2vY4w56fa205P4+3R9j2Dd3u37V94URJf0k3SduI/8tXMXIOAACQG3JeKCmN+CeV1Ub80hb9x3+c5GTJqSaKI03eDiQjTP50Wd+SfENykj1+y8YPdwQxHzPC3gN9L75t783xlhMk35R8x4Q9ldsjWNc3bB3ftvGn23xnkFDsG8Ns+w+RHBnxT97uZAfVZXaQDQAAADki59rrpr1vbSJ+DasKul4J4gjJwIjfk35cM6KvH2TLOtIywJZ/lI0flIL1ZHPc9+goe3/0IKmfPep7dbTk2BRvj6MStsfRzjbP5+0Rtn3jWNtW/UzM9Vcu7TWvtIPrQr7OAAAAckPOg9KWUvtHr4LeQdJF0iPi17X2sRx8gOljQqHL6hXxa9t72EFAT5tW18x1ZHuC96iXvSf6HmnZQteE9ysd26OHs/xebI9Q7hu9bft0tQNoFXM9EbTMDq6RcwAAgBwi6D1XQdce9CqTdL2CS3uT9eak1tLe0s7koq0Nt3Pm6ZDHqXXeD31vapy0dd6/dG+P9myP0O4bui9U2+fUFXNqzQEAAHJMzjXag15ikl5uAqCpTGEqEpabjnXkQsoTUpHB7cH7H85U2L5QZp/TYuezCwAAADlGQYKkBylOcYqSPJKvfp/YHiTYD4LPKj3mAAAAOS7ohJDsCQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5CQFlsIkKWpmki0rlesg2Z9k+18BH1EAAADIRzFXQSqWlFhKLWXNTGkDKUvh8kn2x903Smw/LELQAQAAIN+kvMAkKJDxckmlpNrSOgWpThhO1XJJbkX3iyrb/8ptfyxG0AEAACCf5LzQxFxlqNWaeYd7mcj4Odu8Cb/9wpt41y5v4t1feufdU0/IPtH9MeL3qhcj5wAAAJAvcl5sAqQ9lu0yJefj7vjcmzBnuzfhzp0i6LuRUdKQnLc1Qdce9KDEBQAAACBn5TzoNdcygnaSLpmS87G3fuaNm7XZes+Rc9KgnHeW1EgqInt7zwEAAAByVs61N1J7JbXWt5OkLlNyPubmld65t2+M955T2kKSyHlvSYeI33tegpwDAABAPsi51pq3kXSV9M+UnJ89Y5l3zsy1fu25lrYg52R/Oe8b8XvPW9tBJHIOAAAAeSHnWtvbU3JUpuR89PQPpLRljTd+9lbknCST8wGSbnbwWIacAwAAQD7Iudbzar25lhAMzJScn3Xj+97YW1Z742dtQc5JMjk/WtI94tedl9v+CgAAAJDTcq4ng7aX1EkGZUrOR019zxv7m1X+SaHIOWlYzo+J+L/otEXOAQAAADlPp5zfgJyTr5Tzgcg5AAAAIOfIOUHOAQAAAJBzQpBzAAAAQM6Rc4KcAwAAACDnyDlBzgEAAAA5R84Jcg4AAACAnCPnBDkHAAAAQM4znsVrY14isVjM27Dd8xYsj3lXPBrdZ/5rn6733lkT87btinkrN8W8R96Oehfei5wj5wAAuWIgBQUriouLvUbymsx2uQ7L47AwtLmwsPAX1p5BwbiioqIJ8re8JdkuWSTP/y0NskbyK8h5BuX8Hys9b/7SWDwvfhjzVmzyx28USb/4fn/e65+NxsV9zRbPe/K9mLfEXvvSslheibnuD0nknM8tIYS0XFImujNFZB/UqNSakL8ejJPp12WBnI/Q59L+N7W98rhMn0v7z0+DnBdaihJSTLI+idu0RNJKUoucp1/Of/b4vj3k50s+WOdPmz7fn7ZsQ8z7MhrzfvCgP8+k3+2d54cP5aWc95K0s4PIEr6TCCEko55Q6KQgXd57tUntRQnjQy3n0t6/ipBvsx4kpb/9HX9OoZgXOv/w9J9gmfVWaSpITqXc2b6tJR0kfSSDkfPMybnmuSX+tJl/i3rfu68+Pvzumn17yZ9415/nmr9E80bMJ961O/j+q7P9s7UJOp9fQghJvyMEnlDqSHvaBL1ROZfxF0oeFRHeII9PyvhRjjDPljwgg6fK9MUy/8s2aagMvyTjNmuJTAPLHqPjVa51HltuP2d6rSz3Xpm2Xtcrw7Mk01w5l+G/ybi7ndeUy7y7ZVkPp0HMy2zjVNk/xDaSGpJT0e1abdEDvoMkhyDnme85X7/Nr0G/am40Xtryxzdj3ozn953v/c/81//Ph/NAyk3MJ961S7//jrP9srOVXtXwfUQIIRn1hEoT9ZJ0Cnqjci7Cu8nKXCbL8FoVZjtqUEF+WZ6vkmyV4Rdlvina0y7Po7K8eTJ8qYy736T6p7bcITpd8q4MX6HjZXidyn2wYpn/eX2NvPY+eXqlTFsi2ZlYc25oCcK3tcdc55HhkSmU80DMdR0eyat0lfRVGULO0yfnWjc+951oPE8v9k8IVZ5ZnLye/LZXovF5Fq7y8qPH3ORc9wnZH4+XHMbnkxBCWjyuoGdWzrWcxOkpn2KCfGYg5/Z8snOyqZ6guSjiXEVAlv2UjNtoPc8jZDl3yOORznLvseXoEckQW+/v3aoWPQhIIuffCk5k1Xp5e7OaXUXj1B9X2NESO2J+pYdJ0PHIeWau1hKwaYfnTU5SrqK96Hpy6DrpXQ9q0PNDznd5E367Q/fLEySH8/kkhJAWT6VT4lKY6ZrzkY5kn2vzfj9BzmuCXmyR6Jj2iotc3xJE5llg8w20+brp+mTajfL4hMxfb9PbyPOLbfjshJrzO5PIub4xddom7cGXx2dTKOel1muuf18nSXer9zzExI3kRg61+nK9CkY32849rdQKOU+jnE+ZVx8vXdHoyZ33vu6P37IzFq83D+a/6A/13j8/9eVdX3vZg/lzEmiCnJ8oOcL22YMth9ivPHyWCSEk/Z5wkJ2UX2Udwi0i58Mak3MVYuc1HawU5m2R6RmJsT9sjMq4ZKGMmyrLmiR5PJBzyWU2PDyhPdc4cq61lv8hOSVB4GdryYxNR84Jcp6FJ4QusSuxXPWYP+1CkfS3V/nXQX94YSx+tZZ8ukILck4IIch5U+V88z7F2lKT7pwYGqAS/SP9I2TaC1rHbrXcwXIfc+R8qJW1XOsuQOaZ78h5vIfersfe0DwHpbisRdtVaydhqbj1MknvQ7I+uh172weuq+07XShrafmrtej1zfe5estL0by6rvlXlLX0se+hnnwfEUJI2j2hl3mB+kEH67StNJcNv5wLPze5vl7LWKzcZLue5GnLeEZ7zrXH3P7JXK5XWbF1nWT15YskO2T4EpVxWdZNQV15ZO+lFB9WQderusjT0fJ4s7VtfppOCG1jPfIdTeA620Yi2Z+D7JeRWmcbc0JoC8n53EXRPZdS1F7yHbtj3q76mPf80v2TeCfRPDohtKftu534PiKEkLSms+MJwVWyqiL7X7El1HJeIqJ8g4zf5Zyo+ZQJjzJYpn0STLPylmk2vNLmqZPnrzrzLNYSmISa8w56iUdnnpg8f8iOaFIl58kupeheWqctyfokXkqxXYRLKbaYnM/5uy/nryyPeZOfjHqNoTXrXEpxz6UU+SwTQkjq/SDZpRTdk0HTdjOiVFNiNbsdG5nW2RnX23orXWptfGNUWw98dYrbz02IuAkRNyEiYbwJUZ8INyEihJCWvAlR4jXOs0bOc4FA0AsdUec22bl9a94SK2OqtTqzQcg5CYugR/wrXvWyX3cqnX8QfCcRQkhmPKHI8ULEHCADB2NFJj3tkXMStpic97SfW8sjzv0kAAAAAJBz5Jwg5wAAAADIOUHOkXMAAABAzpFzgpwDAAAAIOcEOUfOAQAAADlHzglyDgAAAICcE4KcAwAAAHKOnBPkHAAAAAA5R85JEjk/BjkHAACAfJNzvVWv3oGxt+TYjMn5VJHzW1Yj56QxOT9a0sPkvAw5BwAAgHySc+2hPCZTcn7WjYvjcj5+1hbknCST8yMl3SU1yDkAAADki5yXm/yoBA3IlJyPnr7UG3vrZ9742VuRc5JMzvtLukham5wX8rEFAACAXJfzMpOfzpJDJYMlp0jOkoyTnC+5oBmZZMuYaMs7VzLWhsfbtEnNXAfJ/pxn+8YoyXckx0oOlnSSVEtKkXMAAADIdTkvNOmpktRG/NIW7a08TnKy5FTJCMlIyRkHmJGW0yWn2TI1wyTDneU3Zx0kuxPsH7pffFMySHJYxP81R0uu9KTlEuQcAAAA8kHOiyN+3XkbSUcT9L6SIyL+pewGm6w3N7qcY+1xkCVY9vEpWgfJ3ui+cIztd/oLjp4I2iGyt6SlGDkHAACAXJfzoLRFe88rTdBViLqYHOkVXPpYDm5G+jipsyQut7nrINmdOtvfdL/TEqtaE3M9cCyx/RQ5BwAAgJwn6D0vNRGqMknXcoL2JuvNSW2ScW46kLxOsB/o/tbWpFz3w3LbLwMxL+DjCgAAAPkg54UmQCUmQ+Um6prKFKdVwiMhbsodKS92xJxecwAAAMgbChIkPUhxGlLiDKdrHSS74wo5PeYAAACQt4JOSBgDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkAcUSAqTpKiZSba8VK8njEn2nhZYAAAAAAAaFPNAKIslJZJSS1kzEyynJGG4JMXrCWPcv7U4QdoRdAAAAABIKubFJpPlklaSKkm1pHUKU23LdZefjvWEKcHfWGnCHog6gg4AAAAA+8l5IOYq5VVr5h3uZSLj52zzJvz2C2/iXbu9iXd/6Z13T31exCS9NEHQAQAAAADicqiSqD262rvbLlNyPu6Oz70Jc7Z7E+7cmW9y3sYR9KDEBQAAAABgj5yrLLaVdMmUnI+99TNv/Kwt1nu+K5/kvFPEL3cpj+ztPQcAAAAAiIuh9uBqr3lHSV2m5HzMzZ94596+UeR8R77JeXdJezsgKkHOAQAAACBAyyq0pEVLLbpI+mVKzr/7q+XeObeti9ee55mc97EDoSrkHAAAAAAS5VzLK2okPSRHZkrOR09f6p0zc61/YqjUneeRnPeVdI74V3EJ6s4BAAAAAPZcpaWdpKdkYMbkfNoSqTtf49ed55ec94/4v1Jo3XkZcg4AAAAADcl5L8mxmZLzs25cnK9yfrika8QvJULOAQAAAGA/OdcTFHtLBiHnaZfzAcg5AAAAACDnyDkAAAAAIOfIOXIOAAAAAMg5cg4AAAAAyDlyjpwDAAAAAHKOnAMAAAAAco6cI+cAAAAAgJxbFq+NeYnEYjFvw3bPW7A85l3xaLTB1/37n+q9f62OeW+sjCHnAJB7FBQUrCguLvYayWsy2+U6LI/DwtDmwsLCX1h7BjUwuaNMe0nm+X2q3ypCCGnB5Kyc/2Ol581fGovnxQ9j3opN/viNIukX37//615Z7k/ftMNrCTlnXySEuEmL6M4sKip6UCOivsiE/PVgnEy/LpvkXNr8qE6Tv+WtNMl5oaUoIcWEEJLiuN8xpZJKSa2kLpfk/GeP79tDfr7kg3X+tOnz95326xej3q4vY97nX2RUzmvswKiY731C8vp7uDAhBZnw3qtVbEVwL0oYnxVyLu2+UKR8h2RNiuW8wBFy3VAl1otSbqkghJA0ptzEvFrSSdJHMjhX5Vzz3BJ/2sy/7Z126QP13padMe8Pb8S8ZRtiaZXziXd/qf9jjpB0l7STtHK+8/neJyT/voPLzf1KHXHPiKQ3Kucqv9ozLeK7QR6flPGjHGGeLXlABk+V6Ytl/pdt0lAtM5Fxm7VEpoFlj9HxMn2bzmPL7edMr5Xl3ivT1ut6ZXiWZFoDct5dpn8ujz+WxzdTKOeJYl5mG6pK0tp+7qwhhJA0prU9qiR2kRwqOT6Xe87Xb/Nr0K+au3fa6x/HvI82xrxJv6tPq5yrmE+8a7f+jzlS0kPSwb7rW/O9T0hefw9XOwfqpY6gt5yci/BusjKXyTK8VoXZGhdRGZfnqyRbZfhFmW+K9rTL86gsb54MXyrj7jep/qktd4hOl7wrw1foeBlep3IfrFjmf15fI6+9T55eKdOWSHYmyHmBrOMZOyAoTJOcB2KuG8UjhJAWTF/9/swVOX9pWcyb+040nqcX+yeEKs8s3nvC58yXol5UThYNRD5Dcn6UpJfkIPY5QkhCMibojcq5lpM4PeVTTJDPDOTcnk/eY7UiyFrHHnHOcJdlPyXjNlrP8whZzh3WOxEs9x5bjh6dDLH1uid3qnyvSpDzH2o5izweYutNpZwHveYl1mPemh2SENLCOVxyQi5erSVAxXvyX3wR/+FD9d62XTHvsXf29qKnVc5FzCfetUvf56OthKgr+xwhJCGtnHNRWk7O5XGkI9nn2rzfT5DzGpullQhyTHvFRa5vCSLzLLD5Btp83XR9Mu1GeXxC5q+36W3k+cU2fHZCzfmdjpz3ltdsl8efOAcF6ZDzUtsQNVbz2d1OytIDgsMIISRNOdQEUb9vett3jp6oeGKuyPmUefXxq7JoVMTvfd0fr/Xl37uv3nvi3Vj8JNAZz0e9a5+pj2fNFs/bKtN1WGvR0yznWtrSzR6DbdCXfZOQvPse7mmlhbXWiVxhnbdFLSnnwxqTcy1pcV7TwUph3haZnpEY+yPHqIxLFsq4qbKsSZLHAzmXXGbDwxPac40j52fZepZayUu87MWyxNaDnBNCkPMsOiF0iV2t5arHot6zS5L3sCu/eiGKnBNC0v09fHCCnLfOFjnfvE+xttSkOyeGBpwi+ZH+FCDTXtA6dqvlDpb7mCPnQ62s5Vp3ATLPfEfO+2q5jRtZ5mq9YouV4XRIcVlLG9sonU3Qe9k/zT6EEJLiBELewxJ0CuRUWUtjV2u5/tmo9+NHot5/PrVvVm32e851+JI/pb3nvJt953dN+N7nu5+Q3P8Odr+Hu1oHbVsrz3YvsZodci783OT6ei1j0Xm1BEVP8rRlPKM959pjbv9sLpfnu21dJ1l9+SKrJ79EZVyWdVNwg6RIwzchimTghFAVdL0RSEc7UaizHUkRQkiqc5D9MwjS03pyhuSynM9dFN3vUopuWuCE0FoL3/uE5O/3cK2JuZa0VIbihNADkPMSkekbZPyuQKj1hFD7clMGy7RPgmlW3jLNhlfaPHXy/FVnnsVaAtMCcp7sUoruZXbaEkJIipN3l1LUzPm7L+d6N9AWlPOvcylF9lFCcvv7N0ibyN5LKVY4Yl4UyeANiVKJloT0c6S8oWmdnXG97acDl1ob3xJwEyJCCDchSrGchzlJbkJUwfc+IXl/I6JS88CM3YQIvlrQCx1R5xbOhJBM3Tq61AS91mogByHn6U3EP/G2m/WalSdsD/ZNQvLve7gowQURcwCAPKbYBLG9/YqInGdGzrvaz9llEeeeHQAAAACAnCPnyDkAAAAAIOfIOXIOAAAAAMg5cg4AAAAAyDlyjpwDAAAAAHKOnAMAAAAAco6cI+cAAAAAgJwj5wAAAACQpXJ+LHKOnAMAAABAy8q53kq+l2RgpuR89LQlvpzP3ppvcn44cg4AAAAAjcl5W0lPydEZk/PpS0XOP4vL+cS7duWTnPeTdJG0lpQi5wAAAAAQoGKovbfai9tNMiBTcn72jGXeOTPXeuPnbMs3OT9E0klSjZwDAAAAQENyrr24B0kOlQySnCIZLRknOU9yQTNyvmS8ZIxklOR0yTDJcHs+1qY3dz1hyyTJBPu7R0i+KTky4pcP1UpaSUokheyGAAAAABDIeYmJop4U2kPSXzJYcrLkNJPpMw4wI01MVcSH2jJPlJxgjyfb+GHNXE8Y4/7d+rceHfF7zbWkpUZSgZwDAAAAgIuKYVB3rqUtHSN+7XlfyRGSgSbqxzUj+vpjTU6153iAHQAMsOdH2/TmrieMGWR/n54E2ifinwiqB0FVkb0lLcg5AAAAAOyR86D3vMIEXUsutHdXe9F7m1QefIDpY6mL+OUcuszuTnra+LpmrieMcf9urefXOnO9Kk61HQwVI+cAAAAA4FIQ2dt7XmqCXmWSrldwaW+yrunQjLS3tLPlBmlnSdV6wpZa+9u0jEXr+luZmJc4Yl7AbggAAAAAAYWRvT3oKullJpCayhSlwnkMll3hpDKHE/zNZXYAVIyYAwAAAEBjFCRIeiDqqYy73MTHXE+RI+SBlCPmAAAAANCooJPMBQCgyfx/I9Z+uHcWfZsAAAAASUVORK5CYII=) + +- ExecuteProduceConsume:任务生产者自己运行任务,但是该策略可能会新建一个新线程以继续生产和执行任务。这种策略也被称为“吃掉你杀的猎物”,它来自狩猎伦理,认为一个人不应该杀死他不吃掉的东西,对应线程来说,不应该生成自己不打算运行的任务。它的优点是能利用 CPU 缓存,但是潜在的问题是如果处理 I/O 事件的业务代码执行时间过长,会导致线程大量阻塞和线程饥饿。 + +![img](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAucAAAEHCAYAAAANq+jXAAAyNUlEQVR42u2dCZgU9Z33e+7hHI7hFORSETXEIKhrjEFuUBPXXeOFJo9uNnHzvskq67pvEjEhXoCYmMRN1DUiJiSiRhbRKKAoEAQxCoRrkFMuueUYLqf7//5+Vb+CmmYGR5ijavrzeZ7v09VV1dX/6a6p/tS/f/XvRAIAAAAAAAAiT5Yku5LkRDCVtTMu7a/L1ynb3u8sdnsAAACA6Ip5IHS5kjxJvqUgggnalmftDdocTlTbXlevU/Ba5SDoAAAAANEX81wTuUJJI0ljSRNJ04imibVT0zCUxjFoe22/To3ttSm09ziQdAQdAAAAIGJyHoi5ilvjn68Y6qKeR5b7Gbd0qHt48RA3ZuFgJ21veeP4MkdOHDuZKbD3PBs5BwAAAIgO2SZpKmvau9oiLnI+bpkv52MXDXYPvTdIpbMT8l0lOW9ugp6fONZ7DgAAAAARkvOGJm3t4yTn2ms++v1B7v55A1U6z0O+qyTnbSVFkgYJvw49m38DAAAAgOjIufagaq95a0nX2Mi512s+RHrNB7pRc/qrdF6CfFdJzjtLihPHes+RcwAAAICIoGUNWtKiPantJT3iJOdaa/7Au4PcT97up9I5EPmukpyflfB7z5vYe5/DvwEAAABAdORcLwRtJjld0jNucq4lLSPf8OT8KuS7SnJ+ruQ0OyFDzgEAAAAiKOctJJ0kveIg5169+RIZpeWDwe6+dwa6e2b0Vem8Bvmukpz3lHS0E7JC5BwAAAAgmnLeWXJBLOV8+uUqnd9Avqsk519M+N+SIOcAAAAAEZXzlpIukt7xk/MBgZxfh3xXSc7PNzlvjpwDAAAAIOfIOXIOAAAAAMg5Qc4BAAAAkHPkHDkHAAAAAOScIOcAAAAAyDlyjpwDAAAAAHJOkHMAAACADJbzTQeWu3QOJ0vdptLl7s2tj1f4mMkbRrmNpcvc79f+IHZyXrItddzfm0ql3M5S5+atS7kRk5PHPea2iWVu6ccpN2FBCjkHAACAkyMrK2t9bm6uO0HeldW+r9NyOygKbc7Ozv6ptad32qIGMv8tub2rJl6qGOY4OX9k+anJ+Zr9C9ySPTO8lOyd4w6V7ffmz9sx6ei6v1hxpfvfjT9zpZ/u9pY9v/7/xVbO/7bBuZmrUl5mrU659bv9+btE0lXGg/VV1hdv9iX+9RWRk/MsQgghhNRYql10H8vJyZmkEVFfYkK+IJgnyx+IgZzrC/MFae9rOl//phqU82xLTlpyI5SgTXmShpJiSdfqkPOJa+8oN//p1d/2epS1F12l/M2tT7iy1JFyPc5xlvMfTi3fQ36T5MPt/rKHZybd/32hzJUeKd/LXsdy/qWE/2uwLe29z4vg/kkIIYTEKenOlx1KVk177z0qtyK5t6bNj7Scy0nFynBvfw3IeVZIyHNNeAqsZ1LTIKJROWsqaSs5U3Khiu/JCHplcq7Zd2Snt+yJVTe7lzb89GjPejC/Psm55o2V/rLH/pp0332uzL35YcrLiq2pKMi5/k+cYe95M0mjCO+fhBBCSJwSeJ86YH5I3ANRr305l/m3SCaLDO+U21dl/lUhYX5C8pxMDpDlJbL+HFvUT6Zny7w9WiJTwbav0fmyfL+uY9vtEVpeLNudIMt26PPK9OOSsWk95z+WeY/IY1+qATlPF/MCe4Mam/gWmQRFLUG7tNe8g6S75KJxS335/byCXpmc/+bD610yVeb1nv+y5Opyy9aWvl8ve853+JU87u4p5Zf9Zm4yCnJ+keRsK21pLWkR0f2TEEIIiVuKzP0amwsWhL6hrhs5FznebWUuI2V6mwqznTkkVMbl/mbJPpmeJeuN0p52uZ+U7b0u09+VeRNNqu+07V6syyXLZHqEzpfp7Sr3wRNrDbkJ97NaS6695JJDldScD6tBOQ/EXHsiXUxzycOLhzgV9JOV81nbxrspGx/0Mm3Lo27rwdXe/FX75h/3mPog57PXpNyUpUkv00r8C0KV6SXHC3hE5JwQQgghtZcCc8QaK3E5oZxrOUmop3yUCfKVgZzb/ZGhi00Xah17IjRihNaFy7xddtYxVLbzW7ntGdrueNtOE5V3e97fh6ta9CSgFuU8O1S/3cDOmOK6A106dtFgT3xVgE91tJaALQdWuidX3VIv5bwidh9wbuRfksg5IYQQQsLXd9W+nKv8hiT7Wlv3W2ly3sxWaSQSndJecZHlXwWRdebZer1svQ76fLJsjNy+IuuX2fIiuX+bTV+dVnP+VB3Ieb71muvf10bS0S6yPNPKCKKWs6xtenuOXSh42ZiFpybn82VUlmlbfu1l6qbR7pm1t3sXglb0mPog56Ne94dI1Hzv+TJvmERl76GU++azkZTzy620RUdtOddKxM4mhBBCyCmlu3lVV3NAdcHm5ob5Ndl7XuULQiuScy1pCT2mlZXCLBZZHpceWd5N681VxiWLZN5o2dZwydRAziW32/TgtPbci5zXjZxXdEFoZamPNeealTZay90vJ5FzQgghBDkviLKc7ylXrC016aELQwP6S36gV7vKsre1jt3+qGC7L4fkvJ/J9n3hDcg6M+uwrKXILrJsZ29OZ3ujukUo2p4uobZ1t9KhUy5rQc6Pjdby4IxIyvlX7f/iPDsxOyNi+yYhhBASt3S1dDb3a2cuWGRlLfmhi0KjLefCj0yWH9QyFl1X1im1HwrSbUzXnnPtMbdevu/L/SP2XF+2+vIlkgMy/R2VDh2VJRgysQ4vCNU3Q8eR1tEw2tqb1D5CaRdq12km6vr6fjm4IBQ5P3k5n7IkeXQoxQjK+aV2IqZS3sne//aEEEIIqRa3am0OWBTqNc+rswtCT0LO80SUH5L5hwOh1gtC7Q9T+siyjcEyK28Za9MbbJ2ucn9+aJ0SLYGpAzmvbCjF8BA7zSOScHuK7SxPe88vDnrNq2soxUyU8yff8SV87rpUXIZSbE4IIYSQU/aq8FCK2mNemKiFoRRrijyrfW19gmXtQvO6WI9fmGKbXxfUqx8hOtkfIqqtREHO45oEP0JECCGE1KsfIYLPFvTskKjnJKL70+hBnbwKeiurleqjUh5lMUfOT1nOv2Q1cS0Tx4Z34qeXCSGEkFP3qnDCTpiFJkNVybEzvJb2rUPvKEs5cl4tcn6+lbQ0t/c+h38DAAAAAOQcOUfOAQAAAAA5R86RcwAAAADkHDlHzgEAAAAAOSfIOQAAAAByjpwj5wAAAACAnBPkHAAAAAA5R86RcwAAAAA4RTm/IMZy/g3kGzkHAAAAqA9y3iLh/2pkr1jK+Yy+Kp3/jHxXSc57Sjoi5wAAAADRlXMVtU6S8+Mm5/fPG+hG+nL+j8h3leT8PEkHSTPkHAAAACB6cl4gKTJhOy8ucj5u6VA3ZqHI+fwB7t6ZXlnLFch3leT8bEl7SVN775FzAAAAgIjJuYpaW8lZkt6S/pKvS74huVFyc4QyXHKD5FrJVZLBkn6Sy63dQyVXS66LYNtr+3XSv/+f9cRF8lXJlyTdJK0ljSX5yDkAAABAtOQ8T9Io4V8UqhcKniPpI7lMMlAyxOQuKhlmQt7fhPMSa6+eVFwoudREPWj7sIi1vy5eJ31NLkj4veb6DYleY9DQ3vts/g0AAAAAooGKWW7Crz3W0hbtUdXa8+6SL0h6mfheGLGoiGsvcE87mehu4tkj4ddUfzHCba+L10lfkzMT/oWgxZIm9p7nIucAAAAA0ZLzoPe8gQm6ypvWJGsvug6vqGUQZ0Qo2p6u1jZto/YEnxZKRzvBCNoetfbX5uuk6WyviZYt6bcjTe29zrP3HjkHAAAAiAhZiWO95/kmbY1N0pubzBVbWkUo2p4WFm1ns1Ca2/yWEWx3XbxOLe010fdUy5cKEXMAAACA6JKdONaDrpJeYAKnaRjhNLAUpqVBKA3J0deiwE7AckNijpwDAAAARJCsNEkPRD3qqaitcWp/bb9OSDkAAABAjASdZE4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMoAsSXYlyYlgKmtf1Nudqals38riXw8AAACgYjEPRCpXkifJtxREMPlpyatgXgGJ3PuVZ/tXWNoRdAAAAIAKxDzXBKpQ0kjSWNJE0jTCaWLtDNraJAZtztQ0sf2qgQl7HoIOAAAAcLycB2KuUt745yuGuqjnkeVD3bilQ93Di4e4MQsHu4feG+RuHF9GYhST9EDQkXMAAACAhN9rqWKuPZna+9wiDnI+bpmI+ZIhbuwiX8zve2cAwhs/OW9igp5r+yEAAAAAcm5y1FDSXNI+NnIuveaj31cxH+h+8lY/hDd+cl5sJ4RaShX0ntODDgAAABkv5/kmSa0lXeMi52MXDZFe84Fu1Oz+7oev9UV44yfn7SXNEn45VS5yDgAAAOD3WGpJS5HJUo9YyPnSoV5Jy/3zB7h7Z17u7p7yVYQ3fnLeSdIy4X9rg5wDAAAAmJxrz6X2YJ4u6RkXOdcLQbWkZeSMvu4//vwVhDd+ct5N0srkPA85BwAAADgm5y0Sfk9mrzjIuV4MOuaDwd6FoPdMv9zd+fylCG/85PzMhF9K1Qg5BwAAADhezjtLLoibnP94Wl93x3NfRnjjJ+fdJW0S/vUOyDkAAABASM619reLpHec5Pxncwe4H72OnCPnAAAAAMg5ck6QcwAAAADkHDlHzgEAAACQc+ScIOcAAAAAyDlyjpwDAAAAIOfIOUHOAQAAAJBz5Bw5BwAAAEDOP3c2HVju0jmcLHWbSpe7N7c+Xm7d/155nfv7J6+7vUe2u4Nl+9ya/e+5iWvvyDg5L9mWOu41S6VSbmepc/PWpdyIycly6z81L+XW7Uq5g5+m3Fq5ffydJHIOAADlycrKWp+bm+tOkHdlte/rtNwOikKbs7Ozf2rt6R2a3ScnJ2em/D2fSEpknQfsA6PaXipCIphql/M1+xe4JXtmeCnZO8cdKtvvzZ+3Y5K33i9WXOm2HlzlzVshy1fum+vKUkdE5A+68au/k5Fy/rcNzs1clfIya3XKrd/tz98lkn7bRH/dP77vz1u9I+WmlaTc7gO+zD/9bqq+yTkhhGRKakx0HxOpnaQRqV1iQr4gmGeSG3U57yptPyxZp8uk3dN0uUw/UgNynm3JSUsuIbWQiva7BtUp5+k94E+v/rbXG6y96CrmUzY+4K23YOefj64za9t4b96c7RMyUs5/OLV8D/hNkg+3+8senpl033y2zB2S3vLNe5y7ZYK/zr//Oekt1570mMr52ZK2kqaSAo6FhJB6/nmbE3LAWv2m8B6VWpHbW9PmR1rO5f6jdr9PsIqI+lLJ3mp68bJCQp5rvUQF1mNZaHJESG2mMLQPas9lKz1JrQk51+w7stNb9sSqm93c7X/wetP/uG7E0eUvfjTSWz53x0Tk3PLGSn/ZY39Nuv+cknT7D6fc5L8nywl86ZGU2/BJbOX8HMlpkha2D3IsJITU98/cPEtOSNLrVs5l/i2SySK9O+X2VZl/VUiYn5A8J5MDtKxE1p9ji/rJ9GyZt0dLZCrY9jU6X5bv13Vsuz1Cy4tluxNk2Q59Xpl+XDI2LOcyPVeWbUsT+D/aOs2qWcwL7I1qbD1GRfYchNRWimzfa2L7YZGVF3TTE9RHllevnP/mw+tdMlXm9Z7/suTq4x6nwv7RgcXeOmFhz/Se8x1+NZC7e8rx4q496X/6wH9sWNhjJufnSTrZvteCYyEhpJ5/5jY2/yuobUE/oZyLAO+2MpeRKsMqzDKdb4I8R+5vluyT6Vmy3ijtaZf7Sdne6zL9XZk30YT5TtvuxbpcskymR+h8md6uch88saz/lpWoPCt375JlKyWH0spaBkr6h9pbKOtskGypxl7zQMwbSRwhEUt7yZmSC09VzrVEZcrGB71M2/Ko1Jev9uav2jf/uMcs2zPz6IWQr2wam7EXhM5ek3JTlia9aD25XhCqTC85vldcy1zKkv7j9KLRm+J7Qej5/N8RQjI0BeaFdS/nWk4S6pkeZYJ8ZSDndn9k6GLThVrHbmcXHrLt12TeLjsDGSrb+a3c9gxtd7xtR3sFL7bn/X24U1xPAiq4IDSgoTzHS/Z33FYdVTTW/jw7Y2rKTkkimI52gd5F45ZV32gtAVsOrHRPrrrluMe8unmcN2pL6ae73YFPP3EvfPTDjB+tJUAv+Bz5l+N7xbXE5WWR+DU7U/JtQ8q9uCi2ZS29+L8jhGRoGljndE6dy7ncDgtJ9rW27rfS5DwoI2kkEp3SXnGR618FkXXm2Xq9bL0O+nyybIzcviLrl9nyIpVrm746rWTlqUrkvJc+n24j1DtfXXKeb73mzexr3I5W43umXRhFSG3lLCth6WQ1v+1tulrkfL6MyjJty6+9TN002j2z9nbvQtATPfbJVd/0RmxZv39hRsr5qNfLvFFZNN97vsxNWODP33so5ZWwVPTY4c+UucWb/aEX//VPsZRzPf6ea/vdWRwLCSH1MN3t2NbVvE/9r7n5YL71nte5nA86kZxrSUvoMa2sFGaxyPS49JhcXKMiLVkk80bLtoZLpgZyLrndpgentefeCuR8iJa7aNmLTF9SndefIuckk+T8s8YrXyqlLNsPrZda9BvKzd9+aK0n6J8l8plyQehKG63l7peT7pezk+4jGV5Rb8PrBMMrjp2ZRM4JIQQ5rxE531OuWFtq0kMXhgZobfgPtC5clr2tdexWuxNs9+WQnPezspb7whvQ8czT5Pwy2c5BvVhVy1qq+TVJL2vRdhVL2tkb1dnetG6E1EJ0X9MhE083MW8bOlmsFTn/+yfTvPWmbhpzdN7/rP6W1wO878gOLghNG63lwRlJd/90f9jEeetTFT7+R6/EUs4vsIv3u9k+ybGQEFIfP3M722dsO/O/puaakSlr+VxyLvzI5PpBLTnRdWWdUr3I07YxXXvOtcfcemC+L/eP2HN92erLl0gOyPR3VMZ17PLgB5ICOZfHP2/P85g+VzhW217dF4SqoOu40q1NjtpZ7yUhNZ12ISHXg0QL2xdPCy4IrWk5/8PaH3gifqis1L23a7L8eugTbtvBtd5j/yrDLCLnfqYsSR4dSlFLWHTIRH3d3tuQcv89N+nmrvMfq7XnN0+I7QWhZ9iJYnuOhYSQevyZ29o+a4vMA6NzQehJyHmeCPJD+gNBgVDrBaH2Ryp9ZNnGYJmVt4y16Q22Tle5Pz+0jv765+iQnOfoBaaV/cJp6LlOVc4rG0oxPNxOc0JqOOGhnZpYiuzgoaJU7UMpVpSXpRZdLwIN0F8H9cX8CuTc8uQ7vpyrhHs/OvRS0q3YWv4CUu1J/7dJZfVlKEWOhYSQ+pg6H0qxpsizrz9bn2BZu9C8LtYTGKbY5tcF/AgRicuPEJ20nJ9MtJxFf0H00RVXfa7H1Rc5P5noRaN3/W/S3fqHeP8dCX6EiBDCjxDFVszrC4GgZ4dEnZ+rJnX9k8LBtRDByWvv2hLzU0kmy3l9iV0s1dZ6lAo4FhJC6vnnbU7IAbMQcwCojBw7o2+JnJNalvPuVtLS2E4S+bACAAAA5Bw5J8g5AAAAAHKOnCPnyDkAAAAAck6QcwAAAADkHDknyDkAAAAAck6QcwAAAADkHDknyDkAAAAAck6QcwAAAID4yvkFyDlBzgEAAADqVs71J9Q7S3rFSc7ve2eA+/G0vu7OSch5DOX8LElrSSPkHAAAAKC8nDeXdJKcHy85H+jumdHXjXjxUoQ3fnJ+BnIOAAAAcLycF0iKJB0k58VBzsctHerGLBzs7p830N375uXursmXIbzxk3Mto9JyqobIOQAAAEB5OW8qaZvwSw16S/pLvi75huRGyc0RynBr03WSf5JcLfmatfefQm0eHrF2Z2qG23v1j5JBkksk59jJoH5j00CSi5wDAAAA+HKuvZZaWqC9mKebOPWRXCYZKBkiuSJCGSYZaqI3QHK5pK+lf6jNwyLW7kxN8F7p+3SxpKeka8K/GLSpnRzm8K8IAAAAkEhkJ/xeS60719IWrQHW2nMdSeMLkl4m6hdGLNqmC6x950u+aLe9bH6fiLY7ExO8V/oe9TAxb5fwe821pCXf9kMAAAAA5DxxrPe8gQl6saR9wu9F17rgbgn/4r2opJulq7Wvi51QdLZ5QaLW7kxNt9B7dJqdAKqY67c1BXZyiJwDAAAAJPwa36D3PN8EvbFJugpUS5N1TasIpTjUtpZpKY5omzM5+l7ocJ3NJE1sPwuLObXmAAAAAEZ24lgPeq5JU6GlYQzSICbtzPQ0sH1K96+8kJjTaw4AAACQRlaapAeiHvXEpZ3k2PuVE9rX6DEHAAAAOIGgE1KbAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAOifLkl1JciKS7M+ZHJLxqWzfyOLfHgAAAKIu5iozuZI8S76lICLJDyVoY7i94XYXkIxP+v6SmybsCDoAAABETsqzTFgCqS2UNJQ0sTSNYLRdjS0NLY3sfpTbTepuX2lo+3a+STqCDgAAAJGU82wTcxWXRj9fMdRFOY8s9zNu6VD38OIhbszCwU7aXXzj+DJHyGfFTuIKQoKOnAMAAECk5DzXZEV7F1vEQc7HLfPlfOyiwe6h9wapcHVGPEkV5by5Cbr2oAclLgAAAACRkPOg11y/8m8haR8XOdde89HvD3L3zxuowtUT8SRVlPN2kmaSBoljvecAAAAAkZBz7TnUHkSty20j6RoLOfd6zYdIr/lAN2pOfxWuSxFPUkU57yJplfB7z/OQcwAAAIianGuteZHkNMk5cZFzrTV/4N1B7idv91PhGox4kirKefeE33ve1E5MkXMAAACInJxrHW4nyRfjJOda0jLyDU/Ov454kirK+XmSDnZCWoCcAwAAQNTkXGtvtd5cv+7vFXU59+rNl8goLR8Mdve9M9DdM6OvCtc/I56kinJ+vqRjwq87L7T/AQAAAIDIyLleDNpS0lXSO3ZyPv1yFa7rEE9SRTn/UsL/lqg5cg4AAADIebXK+YBAzq9HPEkV5bwXcg4AAADIOXJOkHMAAAAA5JwQ5BwAAACQc+ScIOcAAAAAyDkhyDkAAAAg58g5Qc4BAAAAkHNCkHMAAABAziWbDix36RxOlrpNpcvdm1sfL7fuL1Zc6eZsf9ZtP7TWHSordRtLl7kpGx9Azj9HSraljnu9U6mU21nq3Lx1KTdicrLc+vdNK3NLP065/YdTbsPulHtpcdLdMgE5R84BAOqb3WRlrc/NzXUnyLuy2vd1Wm4HRaHN2dnZP7X29A7m5eTkXC9/y0JJqWSJ3P+XGhBBEr1Uu5yv2b/ALdkzw0vJ3jki3/u9+fN2TDq67t8/mebNW79/kftg91S378hO7/6Ujfcj559Tzv+2wbmZq1JeZq1OufW7/fm7RNJvm+iv++CMpCfuH+917tXlKbfSHjt7TQo59/8HOBYQQkjdp9pE9zER2UkalVoT8gXBPFn+QAzkfKjel/a/r+2V2zV6X9p/Uw3IebYlJy25pFZS0eveSFJcXXI+ce0d5eY/vfrbnhhqL7r2mAf3VcyDdX635l9kXtLrZUfOP5+c/3Bq+R7ymyQfbveXPTzTX7ZmZ8p9mky5f5vkrzP8mWPrfO/5jJbzBpX8X3BMIoSQmneQ7FCyasp77zGpvTVtfqTlXNr7pgj5fvuwUs6xv+PFahTz7NAHXp6kwHqtCu0DktRuwq99kaS15AxJn0eWD3Wa6pJzTdAz/sSqm92rmx/2pt/4+Lfl1tn/6S5X+ulu5PwU5Vzzxkp/2WN/TbpvPlvmTS/7uHwv+SvL/HXu/UsyE+W8s31b1MiORQUciwghpFb9Q4+7+SFprzFBP6Gcy/xbJJNFhHfK7asy/6qQMD8heU4mB8jyEll/ji3qJ9OzZd4eLZGpYNvX6HyVa13HttsjtLxYtjtBlu3Q55XpxyVjw3Iu03+VeU+HHlMo6x6Rbb1QA2JeYG9OY0lTE8NmpNZTZK+/poWkneQsyYUqv9Up57/58HqXTJV5veW/LLna/XHdCDd7+wQ3fvXtR9d5ctUt3mO3HFiJnFdDz/kOv5LI3T0l6ZW2/PH9lBv3Vvn1Vmz1H/9/XsiM1+uGpz/1IvvKBZIukla2/zfieEQIIbUaPd42sbLaQuu0rTFBP6Gci/DutjKXkTK9TYXZzhpUkOfI/c2SfTI9S9YbpT3tcj8p23tdpr8r8yaaVN9p271Yl0uWyfQInS/T21XugyeW9d/Sx8hjn5W7d8mylZJD6TXnhn5I9dUec11HpodVo5wHYq7P4Ugkc7bkonFLfTn/vIIeyPmsbeOldvxBL9O2POq2HlztzV+1b36Fj9NSF61TV6Zt+TVy/jnlXOvGpyxNeplW4l8Qqkwvqbye/L/nJr11Fm12GfN6pcm5lnC1sRNT/vcJIaRuExb02pVzLScJ9ZSPMkG+MpBzuz8ydLGpXqC5JBEaTUC2/ZrM22U9PUNlO7+V256h7Y637egZycX2vL8PV7XoSUAlcv7V4EJWrZe3F+uUq2is/XnWY96UnTCyOUf3GZXzk+k9r2i0lgDtEdfe8fTH/ObDG9y60g+8dZbvfVvmXYGcn8JoLQG7Dzg3spJyFe1F128xtkvvelCDnmFy3ttKuNpZ7zn/+4QQUrdpGCpxya7tmvNhIcm+1tb9VpqcNwt6sUWiU9orLnL9qyCyzjxbr5et10GfT5aNkdtXZP0yW14k92+z6avTas6fqkTO9YXpqm3SHny5nVGNcp5vvebNrMeqo/VenWk9tqT20t1e965We6tf8XeTnCf5BxXfU5Hz+TIqi/aAa6ZuGu2eWXu71zuevv7EdXd4tegqiu/ufLHCdZDzz5bzUa+XeaUrGr24c8ICf/7eQymv3jxY/9Y/lLkPNvnyro+9fVJmvV4VyPlpkrYm6eHjUXeOEYQQUmM5y5yjkx2DW1iHc2FdyfmgE8m5CnHoMa2sFGaxyPS49Ngfdo3KuGSRzBst2xoumRrIueR2mx6c1p57Q3KuF0X9p6R/msA/oSUzthw5r3+pMTmv6ILQ9Dyz5t/cwbJ93gWgL340kh8hquYLQlfaSCx3v+wvu0UkffFmfxz0FxalvNFaMu31Qs4JIQQ5Pxk531OuWFtq0kMXhgaoRP9A/whZ9rbWsVstd7Ddl0Ny3s/KWu4Lb0DWmRmSc6+H3sZjr2idttVc1qLtKg59IHa2D8VupFbSNSTlp+s3L3bbxcpaalzOf73yGhmZZacn5k+tvpVfCK3B0Vp0fPNyo7fMTmaclFehrKU1xyNCCKk1Bwn8o70dg5tZWUtBLORc+JHJ9YNaxmLlJqV6kadtY7r2nGuPudw9V59HR1mx5/qy1ZcvkRyQ6e/oh5Js65GgrjxxbCjFF1TQdVQXuft1uX3U2jazhi4ILbIe+dahnqv2pNbSzl731vaP0c7+UY5eEFqTcv7Shp966+08tMEt3fNGuSza/SpyXg1yPmVJ8uhQitpLfuCIjDNflnJvrTo+6b8kmoEXhDa341ErjkeEEFIr/tHGjrvNQr3m4RFbIi3neSLKD8n8w6ELNV8zqVL6yLKNwTIrbxlr0xtsna5yf35onRItgUmrOW+lQzyG1knJ/eftw6q65LyyoRTDQ+s0JzWe8FCKRaETpfb2VdNFgZjXlJz/bdeUSi9k1FIX5PzU5fzJd3w5n7su5Ua+mnQnQmvWGUrxuKEUOVYQQkjN+EdFQymGLwatsR8jqm70bKJHSMorWtYuNK+L1VOGKbb5J6KJ9cA3qeb28yNE0f8RojaJU/wRotoKck6q8UeIGif4ESJCCKnLHyFKH+M8NnJeHwgEPTsk6vxMdt3/fG6e9RwWWz1Y7yiLOXJOqknOO1lPToNExT8pzXGCEEJq3kFyQl6ImAPYP0GOfa3U0upwe0dZzJFzUs1yXpgI/Y4EAAAAAHKOnBPkHAAAAAA5J8g5cg4AAADIOXJOkHMAAAAA5Jwg58g5AAAAIOfIOUHOAQAAAJBzQpBzAAAAQM6Rc4KcAwAAAGScnF+HeJIqyvmXkHMAAACIspzrryS2kHSRXBA7OZ/RV4XrWsSTVFHOz5ecbnJegJwDAABAVOVcexO/FCc5v3/eQDfSl/NrEE9SRTnvKekoaYacAwAAQBTlvNBERYXlvDjI+bilQ92YhSLn8we4e2d6ZS1XIJ6kinJ+jqS9pKnJeTaHAgAAAIiSnBeYqLSTnCXpI+kv+ZrkG5KbJDdHJMMlN0iutfYNkfSzDJAMk1wtuU5yY4TaTeomN9q+cpXkcskFkjMkbSRNJPnIOQAAAERJzrNNUBpLihN+aYv2LF4oucyEd6hJ7xV1nGGWwXby8FXJJdZWPaG4SHKpSdhAE/cotJvU3f4yxPbhr0h6S85O+N8QaRmXXgidh5wDAABA1OQ8N+HXnRdJWpugd5d8IeEPO9fHBDgqUcnSETd62onE2ZYekvMkX4xou0ntp4/tK7ov67dCeiFoq8SxkpZc5BwAAACiJOdBaYv2njc0QVd5aW8ioyO4dLOcEYFoO7pau/QkooPlNMvpNj9q7SZ1k2Bf0f1Cy7aKTcz1ZDTP9n3kHAAAACJF0Hueb9LS2CRdv/pvabIelRRbtF3NLc1CaR5qd3HE2k7qdl9pavt2oe3rgZhncQgAAACAqMl5tslKnolLoYm6pmEE08DaWBhqa7jNUW03qZsUhqQ8NyTm9JoDAABAJMlKk/QguRFOejvj0m5Sd/tLdij0mAMAAEDkBZ2QTAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAhMmSZFeSnAimsrZFuc3V9TenJ8sCAAAAAPVIzAMJzJXkSfItBRFMftp0fiXL6lPy7X3Js/coLO0IOgAAAEA9E/NcE8AGkkaSxpKmEU1RhNtWk2li70tDE/ZA1BF0AAAAgHoi5zkmeYUqfj9fMdRFOY8sH+rGLR3qHl4yxI1ZONg99N4gd+P4soyMSXp+mqADAAAAQEzJNrHTXljtkW0ZdTkft8wX87GLfDG/752BmSznRSFBD0pcAAAAACDGcp5ngtdCclpc5Hz0+4Pc/fMGup+83S+T5bxNwi93KUwc6z0HAAAAgBjLufa6NjHROyMOcj520RCv13zUnP7ux9P6ZrKcd5S0tJOrPOQcAAAAIN5oKYSWtDSTdJCcG3k5XzrUK2l54N1BXq/5f029LJPlvJukdcIvSULOAQAAAOqBnGtJhJa0dJKcHwc51wtB758/wI18o5+7a3JGy3l3SbuE/81HUHcOAAAAADFF65R16EQtjegi6R0XOdcLQUfO6OtGvHhpJsv5OZL2Cb/uvAA5BwAAAKg/ct5VcmGc5PwelfMXMlrOz5WclvBHbUHOAQAAAOqJnBcn/PrlGMn5AHfP9Mvdnc9ntJyfh5wDAAAAIOfIOXIOAAAAAMg5co6cAwAAACDnyDlyDgAAAADIOXKOnAMAAAAg58g5cg4AAAAAyDlyjpwDAAAAIOcnJc2bDix36RxOlrpNpcvdm1sfr/Rx/7vxZ25j6TI3c+sTkZPzkm2p4/6mVCrldpY6N29dyo2YnKzwcf/6pzL39y0p996GFHIOAPWTrKys9bm5ue4EeVdW+75Oy+2gKLQ5Ozv7p9ae3hUsbi3LZss6v6/ul4oQQj5Hql3O1+xf4JbsmeGlZO8cd6hsvzd/3o5Jxz3mtx/e6A58+om3/P1dUyIr53/b4NzMVSkvs1an3Prd/vxdIum3TTz+cXPX+ct3H3A11rYbnv60MjlnvyaEBKlR0X0sJydnkkZEfYkJ+YJgnix/IE5yLm2erMvkb1lYQ3KebclJSy4hJKOTfkzIkzSsTjmfuPaOcvOfXv1tr7dZe9F/seLKcstW7pvr9hzZFnk5/+HU8j3kN0k+3O4ve3hm+WW/mJV0hz9NuU8O1pqcd5A0kxTa+8nxnpDMPKZnpyWrNr33HhVbEdxb0+bHQs6l3beIlB+QfFzNcp4VEvJcO0gX2AG70HrHCCEkSIHdNtFv8yRnqJw/srx65Vyz78hOb9kTq24+Ou+VTWNdWeqIV9YSNznXvLHSX/bYX48t++5zZW7voZT7w3spt2ZnqsbkXMX8ht8d0c+XnpLTJS0lje1Ei+M9IZmVwPP0mJ4fEvdalfQTyrnKr/ZMi/julNtXZf5VIWF+QvKcTA6Q5SWy/hxb1E/LTGTeHi2RqWDb1+h8Wb5f17Ht9ggtL5btTpBlO/R5ZfpxydgK5LyjLP9Ebv9dbt+vRjlPF/PgQ1cP1k3tK89mhBBiaWpS3sTut5WcKbmouuX8Nx9e75KpMq/3/JclV3vzVNIPlu1zc7f/wf1uzb/Esud8h1+t4+6ecmzZgo9Sbu2ulBv+TFmNyXkg5jf87rB+vnxR0kXSRtLC3kuO94Rk7jG9kYl6fkjQ617ORXh3W5nLSJnepsJsjUyojMv9zZJ9Mj1L1hulPe1yPynbe12mvyvzJppU32nbvViXS5bJ9AidL9PbVe6DJ5b139LHyGOflbt3ybKVkkNpcp4lzzHdTgiya0jOAzHXN8cRQkgVo6UR3fV4N27Zqcn5rG3j3ZSND3qZtuVRt/Xgam/+qn3zj66rdek7Dn0ksv61WMj57DUpN2Vp0su0Ev+CUGV6ybELPh+bnZSTkNRRka9pOb/+qUP6vp1v5Ujsw4SQcGpd0E8o51pOEuopH2WCfGUg53Z/5FGrFUHWOvZE6Cp32fZrMm+X9TwPle381r4+DLY73rajZykX2/OGL+5U+d6cJuff03IW651KVLOcZ4fqRxvYGRQ7JyGkqjndvg38h1OV84rYcmCle3LVLd56r2/5hdeL/qd1d3n34yDnFaHiPfIvvoh/7/kyt/9wyr289Fgveo3JufWaX/8/B/V9u0ByFvsvISQtQQ96NORcboeFJPtaW/dbaXLezFZpJIKc0l5xketfBZF15tl6vWy9Dvp8smyM3L4i65fZ8iK5f5tNX51Wc/5USM67yGNK5faO0ElBTch5vr0hzexrzo6SrnZCcDYhhJjMaX15ZztGdLDpcwI519KWz1veEsj5fBmVZdqWX3uZumm0e2bt7eUuBC39dLc3dOKLH93jRWVd+XDfPO9+FOV81Otl3qgsGhXxCQv8+Vpf/s1ny9wry1LeRaDj3kq6+6aXefl4r3P7ZLlOay169cv5geDzpYf1nne12zPtWxD2dUIy55iu//udJO0T/gX+TayzNrhIvM7lfNCJ5FxLWkKPaWWlMItFpselx/7Ya1TGJYtk3mjZ1nDJ1EDOJbfb9OC09twbkvOv2fOsspIXr+zFstKeBzknhMReziu6IDScQ2Wl7kSkj+gS1QtCV9poLXe/nHQzVqZO+Df9/O0kck4IqeljeljOm8ZNzveUK9aWmvTQhaEB/SU/0K8EZNnbWsdutdzBdl8OyXk/K2u5L7wBWWdmSM67a7lNOLLNLTpii5XhtKrmspYie3Pa2Ydv59CBmxCSuelqFxDqQfw0O5C3s/s9akPOtZxl0vr/OppXNz/sPU6HVdT7cRut5cEZSffvLyXdT14rn817/J5znf7On2q0rKW7HeM7WYLjPcd8Qur/8Tw4pp9ux3TtmG1uZdlBWUv85Fz4kcn1g1rGoutqCYpe5GnbmK4959pjLnfP1eeR+0fsub5s9eVLrJ78Oyrjsq1Hgh9ISlT8I0SJWrggVAVdh9fSIdLa2gdwe0JIRqedpY2dwBdb50AH64E55QtCP0vO0xPH0Vo0U5YkjxtKMZxaviC0gx3n23K8JyTj0jZ0TG9uJS0NI3VB6EnIeZ7I9EMy/3Ag1HpBqEmt0keWbQyWWXnLWJveYOt0lfvzQ+uUaAlMHch5ZUMphofbaU4IyegEw+01seNDc5M5/Wr0opoY57w+yvmT7/hyrr8GWhdyXsWhFNnfCanfx/Jmacf0RuZ/+aFe81r/QaLqJM++1m19gmXtQvO62FcIYYptfl3AjxARQk72R4jaWM3iSct5baU25TyqqeRHiILeMo73hGTuDxHlm//V+o8QwWcLenZI1PkpZ0JIZT/3nG9C18rKIy6Mspgj5+UFXd6vL1hJSzP7YM7jeE9IRh/Ts9OCmAMAxIxc63UpRs7jF3m/zrNvcIvsm5AcdmkAAAAA5Bw5R84BAAAAADlHzpFzAAAAAOQcOUfOAQAAAAA5R86RcwAAAADkHDlHzgEAAAAAOUfOkXMAAAAA5Bw5R84BAAAAADn/bDkf8QJyjpwDAAAA1C85159/7yrpEx85H+jumdHXjXgxo+X8XOQcAAAAoP7JeQtJZ0mvuMj5/fMGupEi5//x569kspz3kLSXNJXkI+cAAAAA8UZlrlDSXNJR0jMOcj520WD3wLuD3L0zL3f/OSWj5fxMSRtJE+QcAAAAoH7IuZZDaFmE9sB2l1woGSi5WnKd5CbJzRHMcGvbjZIbbHp4aPktEW33yf6t10uukQyVfEVPpBL+tx16vUAjSZ4km10aAAAAIN5yrj2ujU3yVPa0jvkiyVclg0wGr4hYhtntUJsOboeFll8ZwXafbPTvGyzpJ7lEcn7C7zXXE6pmCb80CTkHAAAAiDkqc0HdufaetzFBPzvh98xeIOmT8HvTo5SL7PbiCuZdZPMvimC7TyW9Tcr15ElH1tELQVvaiVVQ0oKcAwAAAMRczoPe84Ym6K1M/DpJupgInhHhnJmWM0K39SX6HnS1E6cOdhKlF/FqrXmhnWAh5wAAAAAxJytxrPdcBV170BubpKv8ac9ssQl7VBK0p3XotnXofvp69SXF9n5oGYuOztLIxDwvJOZZ7NIAAAAA8SY7cawHXUWvwKRPRb1hRNOogvuNKllWn9LA3psCO5nKRcwBAAAA6h9ZaZKeY+IXl+Sl3dbn5ISEPJByxBwAAACgHgo6iWcAAE6K/w9cZlqkCNlgBQAAAABJRU5ErkJggg==) + +- EatWhatYouKill:这是 Jetty 对 ExecuteProduceConsume 策略的改良,在线程池线程充足的情况下等同于 ExecuteProduceConsume;当系统比较忙线程不够时,切换成 ProduceExecuteConsume 策略。为什么要这么做呢,原因是 ExecuteProduceConsume 是在同一线程执行 I/O 事件的生产和消费,它使用的线程来自 Jetty 全局的线程池,这些线程有可能被业务代码阻塞,如果阻塞得多了,全局线程池中的线程自然就不够用了,最坏的情况是连 I/O 事件的侦测都没有线程可用了,会导致 Connector 拒绝浏览器请求。于是 Jetty 做了一个优化,在低线程情况下,就执行 ProduceExecuteConsume 策略,I/O 侦测用专门的线程处理,I/O 事件的处理扔给线程池处理,其实就是放到线程池的队列里慢慢处理。 + +分析了这几种线程策略,我们再来看看 Jetty 是如何实现 ExecutionStrategy 接口的。答案其实就是实现 produce 接口生产任务,一旦任务生产出来,ExecutionStrategy 会负责执行这个任务。 + +``` +private class SelectorProducer implements ExecutionStrategy.Producer +{ + private Set _keys = Collections.emptySet(); + private Iterator _cursor = Collections.emptyIterator(); + + @Override + public Runnable produce() + { + while (true) + { + // 如何 Channel 集合中有 I/O 事件就绪,调用前面提到的 Selectable 接口获取 Runnable, 直接返回给 ExecutionStrategy 去处理 + Runnable task = processSelected(); + if (task != null) + return task; + + // 如果没有 I/O 事件就绪,就干点杂活,看看有没有客户提交了更新 Selector 的任务,就是上面提到的 SelectorUpdate 任务类。 + processUpdates(); + updateKeys(); + + // 继续执行 select 方法,侦测 I/O 就绪事件 + if (!select()) + return null; + } + } + } +``` + +SelectorProducer 是 ManagedSelector 的内部类,SelectorProducer 实现了 ExecutionStrategy 中的 Producer 接口中的 produce 方法,需要向 ExecutionStrategy 返回一个 Runnable。在这个方法里 SelectorProducer 主要干了三件事情 + +1. 如果 Channel 集合中有 I/O 事件就绪,调用前面提到的 Selectable 接口获取 Runnable,直接返回给 ExecutionStrategy 去处理。 +2. 如果没有 I/O 事件就绪,就干点杂活,看看有没有客户提交了更新 Selector 上事件注册的任务,也就是上面提到的 SelectorUpdate 任务类。 +3. 干完杂活继续执行 select 方法,侦测 I/O 就绪事件。 + +## 参考资料 + +- [Jetty 官方网址](http://www.eclipse.org/jetty/index.html) +- [Jetty Github](https://github.com/eclipse/jetty.project) +- [Jetty wiki](http://wiki.eclipse.org/Jetty/Reference/jetty-env.xml) \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" new file mode 100644 index 00000000..9e9a4473 --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" @@ -0,0 +1,39 @@ +--- +title: Java 服务器 +date: 2022-02-17 22:34:30 +categories: + - Java + - JavaEE + - 服务器 +tags: + - Java + - JavaWeb + - 服务器 +permalink: /pages/e3f3f3/ +hidden: true +index: false +--- + +# Java 服务器 + +## 📖 内容 + +- [Tomcat 快速入门](01.Tomcat/01.Tomcat快速入门.md) +- [Tomcat 连接器](01.Tomcat/02.Tomcat连接器.md) +- [Tomcat 容器](01.Tomcat/03.Tomcat容器.md) +- [Tomcat 优化](01.Tomcat/04.Tomcat优化.md) +- [Tomcat 和 Jetty](01.Tomcat/05.Tomcat和Jetty.md) +- [Jetty](02.Jetty.md) + +## 📚 资料 + +- [Tomcat 官网](http://tomcat.apache.org/) +- [Jetty 官网](http://www.eclipse.org/jetty/index.html) +- [Jetty Github](https://github.com/eclipse/jetty.project) +- [Nginx 官网](https://www.nginx.com/) +- [Nginx 的中文维基](http://tool.oschina.net/apidocs/apidoc?api=nginx-zh) +- [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git a/docs/01.Java/02.JavaEE/README.md b/docs/01.Java/02.JavaEE/README.md new file mode 100644 index 00000000..846c3677 --- /dev/null +++ b/docs/01.Java/02.JavaEE/README.md @@ -0,0 +1,61 @@ +--- +title: JavaEE +date: 2022-02-18 08:53:11 +categories: + - Java + - JavaEE +tags: + - Java + - JavaEE +permalink: /pages/80a822/ +hidden: true +index: false +--- + +# JavaEE + +## 📖 内容 + +### JavaWeb + +- [JavaWeb 面经](01.JavaWeb/99.JavaWeb面经.md) +- [JavaWeb 之 Servlet 指南](01.JavaWeb/01.JavaWeb之Servlet指南.md) +- [JavaWeb 之 Jsp 指南](01.JavaWeb/02.JavaWeb之Jsp指南.md) +- [JavaWeb 之 Filter 和 Listener](01.JavaWeb/03.JavaWeb之Filter和Listener.md) +- [JavaWeb 之 Cookie 和 Session](01.JavaWeb/04.JavaWeb之Cookie和Session.md) + +### Java 服务器 + +> Tomcat 和 Jetty 都是 Java 比较流行的轻量级服务器。 +> +> Nginx 是目前最流行的反向代理服务器,也常用于负载均衡。 + +- [Tomcat 快速入门](02.服务器/01.Tomcat/01.Tomcat快速入门.md) +- [Tomcat 连接器](02.服务器/01.Tomcat/02.Tomcat连接器.md) +- [Tomcat 容器](02.服务器/01.Tomcat/03.Tomcat容器.md) +- [Tomcat 优化](02.服务器/01.Tomcat/04.Tomcat优化.md) +- [Tomcat 和 Jetty](02.服务器/01.Tomcat/05.Tomcat和Jetty.md) +- [Jetty](02.服务器/02.Jetty.md) + +## 📚 资料 + +- **JavaWeb** + - **书籍** + - [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) + - [Head First Servlets & JSP](https://book.douban.com/subject/1942934/) + - **教程** + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) + - [Servlet 教程](https://www.runoob.com/servlet/servlet-tutorial.html) + - [博客园孤傲苍狼 JavaWeb 学习总结](https://www.cnblogs.com/xdp-gacl/tag/JavaWeb%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/) + - [JSP 教程](https://www.runoob.com/jsp/jsp-tutorial.html) +- **服务器** + - [Tomcat 官网](http://tomcat.apache.org/) + - [Jetty 官网](http://www.eclipse.org/jetty/index.html) + - [Jetty Github](https://github.com/eclipse/jetty.project) + - [Nginx 官网](https://www.nginx.com/) + - [Nginx 的中文维基](http://tool.oschina.net/apidocs/apidoc?api=nginx-zh) + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git a/docs/javatool/build/maven/maven-quickstart.md "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" similarity index 93% rename from docs/javatool/build/maven/maven-quickstart.md rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" index 63e69fcf..b2efbdda 100644 --- a/docs/javatool/build/maven/maven-quickstart.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -1,37 +1,22 @@ -# Maven 教程之入门指南 - -> **📦 本文已归档在 [java-tutorial](https://dunwu.github.io/java-tutorial/#/)** - - - -- [一、Maven 简介](#一maven-简介) - - [Maven 是什么](#maven-是什么) - - [Maven 的生命周期](#maven-的生命周期) - - [Maven 的标准工程结构](#maven-的标准工程结构) - - [Maven 的"约定优于配置"](#maven-的约定优于配置) - - [Maven 的版本规范](#maven-的版本规范) -- [二、Maven 安装](#二maven-安装) - - [环境准备](#环境准备) - - [下载解压](#下载解压) - - [环境变量](#环境变量) - - [检测安装成功](#检测安装成功) - - [Maven 配置文件](#maven-配置文件) -- [三、快速入门](#三快速入门) - - [创建 Maven 工程](#创建-maven-工程) - - [在 Intellij 中创建 Maven 工程](#在-intellij-中创建-maven-工程) - - [在 Eclipse 中创建 Maven 工程](#在-eclipse-中创建-maven-工程) -- [四、使用说明](#四使用说明) - - [如何添加依赖](#如何添加依赖) - - [如何寻找 jar 包](#如何寻找-jar-包) - - [如何使用 Maven 插件(Plugin)](#如何使用-maven-插件plugin) - - [如何一次编译多个工程](#如何一次编译多个工程) - - [常用 Maven 插件](#常用-maven-插件) - - [Maven 命令](#maven-命令) -- [参考资料](#参考资料) - - - -## 一、Maven 简介 +--- +title: Maven 快速入门 +date: 2020-02-07 23:04:47 +order: 01 +categories: + - Java + - 软件 + - 构建 + - Maven +tags: + - Java + - 构建 + - Maven +permalink: /pages/e5b79f/ +--- + +# Maven 快速入门 + +## Maven 简介 ### Maven 是什么 @@ -103,7 +88,7 @@ maven 在版本管理时候可以使用几个特殊的字符串 SNAPSHOT,LATES - **LATEST** - 指某个特定构件的最新发布,这个发布可能是一个发布版,也可能是一个 snapshot 版,具体看哪个时间最后。 - **RELEASE** - 指最后一个发布版。 -## 二、Maven 安装 +## Maven 安装 > Linux 环境安装可以使用我写一键安装脚本:https://github.com/dunwu/linux-tutorial/tree/master/codes/linux/ops/service/maven @@ -150,9 +135,9 @@ export PATH=$MAVEN_HOME/bin:$PATH 右键 "计算机",选择 "属性",之后点击 "高级系统设置",点击"环境变量",来设置环境变量,有以下系统变量需要配置: -![img](http://dunwu.test.upcdn.net/snap/20200108143017.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200108143017.png) -![img](http://dunwu.test.upcdn.net/snap/20200108143038.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200108143038.png) ### 检测安装成功 @@ -183,7 +168,7 @@ OS name: "linux", version: "3.10.0-327.el7.x86_64", arch: "amd64", family: "unix ``` -## 三、快速入门 +## 快速入门 ### 创建 Maven 工程 @@ -280,15 +265,15 @@ java -cp target/my-app-1.0-SNAPSHOT.jar com.mycompany.app.App 依次点击 File -> New -> Project 打开创建工程对话框,选择 Maven 工程。 -![img](http://dunwu.test.upcdn.net/snap/1555414103572.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/1555414103572.png) (2)输入项目信息 -![img](http://dunwu.test.upcdn.net/snap/1555415549748.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/1555415549748.png) (3)点击 Intellij 侧边栏中的 Maven 工具界面,有几个可以直接使用的 maven 命令,可以帮助你进行构建。 -![img](http://dunwu.test.upcdn.net/snap/1555415806237.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/1555415806237.png) ### 在 Eclipse 中创建 Maven 工程 @@ -300,7 +285,7 @@ java -cp target/my-app-1.0-SNAPSHOT.jar com.mycompany.app.App 点击 Help -> Eclipse Marketplace,搜索 maven 关键字,选择安装红框对应的 Maven 插件。 -![img](http://dunwu.test.upcdn.net/snap/20181127195117.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195117.png) (2)Maven 环境配置 @@ -308,7 +293,7 @@ java -cp target/my-app-1.0-SNAPSHOT.jar com.mycompany.app.App 如下图所示,配置 settings.xml 文件的位置 -![img](http://dunwu.test.upcdn.net/snap/20181127195128.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195128.png) (3)创建 Maven 工程 @@ -316,7 +301,7 @@ File -> New -> Maven Project -> Next,在接下来的窗口中会看到一大 接下来设置项目的参数,如下: -![img](http://dunwu.test.upcdn.net/snap/20181127195151.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195151.png) **groupId**是项目组织唯一的标识符,实际对应 JAVA 的包的结构,是 main 目录里 java 的目录结构。 @@ -330,11 +315,11 @@ Eclipse 中构建方式: 在 Elipse 项目上右击 -> Run As 就能看到很多 Maven 操作。这些操作和 maven 命令是等效的。例如 Maven clean,等同于 mvn clean 命令。 -![img](http://dunwu.test.upcdn.net/snap/20181127195208.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195208.png) 你也可以点击 Maven build,输入组合命令,并保存下来。如下图: -![img](http://dunwu.test.upcdn.net/snap/20181127195219.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195219.png) Maven 命令构建方式: @@ -342,9 +327,9 @@ Maven 命令构建方式: 进入工程所在目录,输入 maven 命令就可以了。 -![img](http://dunwu.test.upcdn.net/snap/20181127195243.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195243.png) -## 四、使用说明 +## 使用说明 ### 如何添加依赖 @@ -569,4 +554,4 @@ mvn clean install -Dmaven.test.skip=true -B -U - [Maven in 5 Minutes](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html) - [Maven Getting Started Guide](https://maven.apache.org/guides/getting-started/index.html) - [maven 常见问题问答](http://www.oschina.net/question/158170_29368) -- [常用 Maven 插件介绍](https://www.cnblogs.com/crazy-fox/archive/2012/02/09/2343722.html) +- [常用 Maven 插件介绍](https://www.cnblogs.com/crazy-fox/archive/2012/02/09/2343722.html) \ No newline at end of file diff --git a/docs/javatool/build/maven/maven-pom.md "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" similarity index 95% rename from docs/javatool/build/maven/maven-pom.md rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" index 02d34f99..d19300f4 100644 --- a/docs/javatool/build/maven/maven-pom.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" @@ -1,39 +1,22 @@ +--- +title: Maven 教程之 pom.xml 详解 +date: 2019-05-14 14:57:33 +order: 02 +categories: + - Java + - 软件 + - 构建 + - Maven +tags: + - Java + - 构建 + - Maven +permalink: /pages/d893c2/ +--- + # Maven 教程之 pom.xml 详解 -> **📦 本文已归档在 [java-tutorial](https://dunwu.github.io/java-tutorial/#/)** - - - -- [一、pom.xml 简介](#一pomxml-简介) - - [什么是 pom](#什么是-pom) - - [pom 配置一览](#pom-配置一览) -- [二、基本配置](#二基本配置) - - [maven 坐标](#maven-坐标) -- [三、依赖配置](#三依赖配置) - - [dependencies](#dependencies) - - [parent](#parent) - - [dependencyManagement](#dependencymanagement) - - [modules](#modules) - - [properties](#properties) -- [四、构建配置](#四构建配置) - - [build](#build) - - [reporting](#reporting) -- [五、项目信息](#五项目信息) -- [六、环境配置](#六环境配置) - - [issueManagement](#issuemanagement) - - [ciManagement](#cimanagement) - - [mailingLists](#mailinglists) - - [scm](#scm) - - [prerequisites](#prerequisites) - - [repositories](#repositories) - - [pluginRepositories](#pluginrepositories) - - [distributionManagement](#distributionmanagement) - - [profiles](#profiles) -- [参考资料](#参考资料) - - - -## 一、pom.xml 简介 +## pom.xml 简介 ### 什么是 pom @@ -88,7 +71,7 @@ pom.xml 就是 maven 的配置文件,用以描述项目的各种信息。 ``` -## 二、基本配置 +## 基本配置 - **project** - `project` 是 pom.xml 中描述符的根。 - **modelVersion** - `modelVersion` 指定 pom.xml 符合哪个版本的描述符。maven 2 和 3 只能为 4.0.0。 @@ -123,7 +106,7 @@ pom.xml 就是 maven 的配置文件,用以描述项目的各种信息。 - **RELEASE** :指最后一个发布版。 - **packaging** - 项目的类型,描述了项目打包后的输出,默认是 jar。常见的输出类型为:pom, jar, maven-plugin, ejb, war, ear, rar, par。 -## 三、依赖配置 +## 依赖配置 ### dependencies @@ -239,7 +222,7 @@ maven 支持继承功能。子 POM 可以使用 `parent` 指定父 POM ,然后 ``` -## 四、构建配置 +## 构建配置 ### build @@ -493,7 +476,7 @@ build 可以分为 "project build" 和 "profile build"。 ``` -## 五、项目信息 +## 项目信息 项目信息相关的这部分标签**都不是必要的**,也就是说完全可以不填写。 @@ -587,7 +570,7 @@ build 可以分为 "project build" 和 "profile build"。 - **contributors** - 项目贡献者列表,`` 的子标签和 `` 的完全相同。 -## 六、环境配置 +## 环境配置 ### issueManagement @@ -816,4 +799,4 @@ POM 执行的预设条件。 ## 参考资料 -- [maven 官方文档之 pom](https://maven.apache.org/pom.html) +- [maven 官方文档之 pom](https://maven.apache.org/pom.html) \ No newline at end of file diff --git a/docs/javatool/build/maven/maven-settings.md "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" similarity index 95% rename from docs/javatool/build/maven/maven-settings.md rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" index 8df72ab9..9fce2931 100644 --- a/docs/javatool/build/maven/maven-settings.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" @@ -1,30 +1,22 @@ -# Maven 教程之 settings.xml 详解 - -> **📦 本文已归档在 [java-tutorial](https://dunwu.github.io/java-tutorial/#/)** - - +--- +title: Maven 教程之 settings.xml 详解 +date: 2019-05-14 14:57:33 +order: 03 +categories: + - Java + - 软件 + - 构建 + - Maven +tags: + - Java + - 构建 + - Maven +permalink: /pages/1d58f1/ +--- -- [一、settings.xml 简介](#一settingsxml-简介) - - [settings.xml 有什么用](#settingsxml-有什么用) - - [settings.xml 文件位置](#settingsxml-文件位置) - - [配置优先级](#配置优先级) -- [二、settings.xml 元素详解](#二settingsxml-元素详解) - - [顶级元素概览](#顶级元素概览) - - [LocalRepository](#localrepository) - - [InteractiveMode](#interactivemode) - - [UsePluginRegistry](#usepluginregistry) - - [Offline](#offline) - - [PluginGroups](#plugingroups) - - [Servers](#servers) - - [Mirrors](#mirrors) - - [Proxies](#proxies) - - [Profiles](#profiles) - - [ActiveProfiles](#activeprofiles) -- [参考资料](#参考资料) - - +# Maven 教程之 settings.xml 详解 -## 一、settings.xml 简介 +## settings.xml 简介 ### settings.xml 有什么用 @@ -50,7 +42,7 @@ settings.xml 文件一般存在于两个位置: 如果这些文件同时存在,在应用配置时,会合并它们的内容,如果有重复的配置,优先级高的配置会覆盖优先级低的。 -## 二、settings.xml 元素详解 +## settings.xml 元素详解 ### 顶级元素概览 @@ -414,4 +406,4 @@ maven 插件是一种特殊类型的构件。由于这个原因,插件仓库 ## 参考资料 -- [maven 官方文档之 settings](https://maven.apache.org/settings.html) +- [maven 官方文档之 settings](https://maven.apache.org/settings.html) \ No newline at end of file diff --git a/docs/javatool/build/maven/maven-action.md "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/04.Maven\345\256\236\346\210\230\351\227\256\351\242\230\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" similarity index 84% rename from docs/javatool/build/maven/maven-action.md rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/04.Maven\345\256\236\346\210\230\351\227\256\351\242\230\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" index c010a1cf..af20c6e4 100644 --- a/docs/javatool/build/maven/maven-action.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/04.Maven\345\256\236\346\210\230\351\227\256\351\242\230\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" @@ -1,27 +1,22 @@ -# Maven 实战问题和最佳实践 - -> **📦 本文已归档在 [java-tutorial](https://dunwu.github.io/java-tutorial/#/)** - - +--- +title: Maven 实战问题和最佳实践 +date: 2018-11-28 09:29:22 +order: 04 +categories: + - Java + - 软件 + - 构建 + - Maven +tags: + - Java + - 构建 + - Maven +permalink: /pages/198618/ +--- -- [常见问题](#常见问题) - - [dependencies 和 dependencyManagement,plugins 和 pluginManagement 有什么区别](#dependencies-和-dependencymanagementplugins-和-pluginmanagement-有什么区别) - - [IDEA 修改 JDK 版本后编译报错](#idea-修改-jdk-版本后编译报错) - - [重复引入依赖](#重复引入依赖) - - [如何打包一个可以直接运行的 Spring Boot jar 包](#如何打包一个可以直接运行的-spring-boot-jar-包) - - [去哪儿找 maven dependency](#去哪儿找-maven-dependency) - - [如何指定编码](#如何指定编码) - - [如何指定 JDK 版本](#如何指定-jdk-版本) - - [如何避免将 dependency 打包到构件中](#如何避免将-dependency-打包到构件中) - - [如何跳过单元测试](#如何跳过单元测试) - - [如何引入本地 jar](#如何引入本地-jar) - - [如何排除依赖](#如何排除依赖) -- [最佳实践](#最佳实践) - - [通过 bom 统一管理版本](#通过-bom-统一管理版本) - - +# Maven 实战问题和最佳实践 -## 一、Maven 常见问题 +## Maven 常见问题 ### dependencies 和 dependencyManagement,plugins 和 pluginManagement 有什么区别 @@ -53,21 +48,21 @@ maven 的 JDK 源与指定的 JDK 编译版本不符。 Project SDK 是否正确 -![img](http://dunwu.test.upcdn.net/snap/20181127203324.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203324.png) SDK 路径是否正确 -![img](http://dunwu.test.upcdn.net/snap/20181127203427.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203427.png) - **查看 Settings > Maven 的配置** JDK for importer 是否正确 -![img](http://dunwu.test.upcdn.net/snap/20181127203408.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203408.png) Runner 是否正确 -![img](http://dunwu.test.upcdn.net/snap/20181127203439.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203439.png) ### 重复引入依赖 @@ -246,7 +241,7 @@ Runner 是否正确 ``` -## 二、Maven 最佳实践 +## Maven 最佳实践 ### 通过 bom 统一管理版本 @@ -297,4 +292,4 @@ spring-boot-dependencies 的 pom.xml 形式: -``` +``` \ No newline at end of file diff --git a/docs/javatool/build/maven/maven-deploy.md "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" similarity index 94% rename from docs/javatool/build/maven/maven-deploy.md rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" index ba18d389..2f29b940 100644 --- a/docs/javatool/build/maven/maven-deploy.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" @@ -1,21 +1,20 @@ -# Maven 教程之发布 jar 到私服或中央仓库 - -> **📦 本文已归档在 [java-tutorial](https://dunwu.github.io/java-tutorial/#/)** - - +--- +title: Maven 教程之发布 jar 到私服或中央仓库 +date: 2019-05-14 14:57:33 +order: 05 +categories: + - Java + - 软件 + - 构建 + - Maven +tags: + - Java + - 构建 + - Maven +permalink: /pages/7bdaf9/ +--- -- [发布 jar 包到中央仓库](#发布-jar-包到中央仓库) - - [在 Sonatype 创建 Issue](#在-sonatype-创建-issue) - - [使用 GPG 生成公私钥对](#使用-gpg-生成公私钥对) - - [Maven 配置](#maven-配置) - - [部署和发布](#部署和发布) -- [部署 maven 私服](#部署-maven-私服) - - [下载安装 Nexus](#下载安装-nexus) - - [启动停止 Nexus](#启动停止-nexus) - - [使用 Nexus](#使用-nexus) -- [参考资料](#参考资料) - - +# Maven 教程之发布 jar 到私服或中央仓库 ## 发布 jar 包到中央仓库 @@ -33,7 +32,7 @@ 注册账号成功后,根据你 Java 包的功能分别写上`Summary`、`Description`、`Group Id`、`SCM url`以及`Project URL`等必要信息,可以参见我之前创建的 Issue:[OSSRH-36187](https://issues.sonatype.org/browse/OSSRH-36187)。 -![img](http://dunwu.test.upcdn.net/snap/20181106143734.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181106143734.png) 创建完之后需要等待 Sonatype 的工作人员审核处理,审核时间还是很快的,我的审核差不多等待了两小时。当 Issue 的 Status 变为`RESOLVED`后,就可以进行下一步操作了。 @@ -309,7 +308,7 @@ gpg: unchanged: 1 进入[官方下载地址](https://www.sonatype.com/download-oss-sonatype),选择合适版本下载。 -![img](http://dunwu.test.upcdn.net/snap/20181127203029.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203029.png) 本人希望将 Nexus 部署在 Linux 机器,所以选用的是 Unix 版本。 @@ -341,13 +340,13 @@ Usage: ./nexus {start|stop|run|run-redirect|status|restart|force-reload} 启动成功后,在浏览器中访问 `http://:8081`,欢迎页面如下图所示: -![img](http://dunwu.test.upcdn.net/snap/20181127203131.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203131.png) 点击右上角 Sign in 登录,默认用户名/密码为:admin/admin123。 有必要提一下的是,在 Nexus 的 Repositories 管理页面,展示了可用的 maven 仓库,如下图所示: -![img](http://dunwu.test.upcdn.net/snap/20181127203156.png!zp) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203156.png) > 说明: > @@ -477,4 +476,4 @@ $ mvn clean deploy -Dmaven.skip.test=true -P zp - http://www.ruanyifeng.com/blog/2013/07/gpg.html - https://www.cnblogs.com/hoobey/p/6102382.html - https://blog.csdn.net/wzygis/article/details/49276779 -- https://blog.csdn.net/clj198606061111/article/details/52200928 +- https://blog.csdn.net/clj198606061111/article/details/52200928 \ No newline at end of file diff --git a/docs/javatool/build/maven/maven-checkstyle-plugin.md "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" similarity index 97% rename from docs/javatool/build/maven/maven-checkstyle-plugin.md rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" index 9699ad59..561aa81a 100644 --- a/docs/javatool/build/maven/maven-checkstyle-plugin.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" @@ -1,15 +1,20 @@ -# Maven 插件之代码检查 - -> **📦 本文已归档在 [java-tutorial](https://dunwu.github.io/java-tutorial/#/)** -> - +--- +title: Maven 插件之代码检查 +date: 2019-12-16 17:09:26 +order: 06 +categories: + - Java + - 软件 + - 构建 + - Maven +tags: + - Java + - 构建 + - Maven +permalink: /pages/370f1d/ +--- -- [maven-checkstyle-plugin](#maven-checkstyle-plugin) - - [定义 checkstyle.xml](#定义-checkstylexml) -- [maven-pmd-plugin](#maven-pmd-plugin) -- [参考资料](#参考资料) - - +# Maven 插件之代码检查 ## maven-checkstyle-plugin @@ -84,7 +89,7 @@ scope: 可以检查的方法的范围,例如:public只能检查public修饰的方法,private可以检查所有的方法 allowMissingParamTags: 是否忽略对参数注释的检查 allowMissingThrowsTags: 是否忽略对throws注释的检查 - allowMissingReturnTag: 是否忽略对return注释的检查 --> + allowMissingReturntags: 是否忽略对return注释的检查 --> @@ -422,4 +427,4 @@ - https://www.jianshu.com/p/557b975ae40d - 阿里巴巴编程规范 - https://github.com/alibaba/p3c - - https://github.com/alibaba/p3c/blob/master/p3c-pmd/pom.xml + - https://github.com/alibaba/p3c/blob/master/p3c-pmd/pom.xml \ No newline at end of file diff --git a/docs/javatool/build/maven/README.md "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" similarity index 64% rename from docs/javatool/build/maven/README.md rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" index 0a2d0869..179cb604 100644 --- a/docs/javatool/build/maven/README.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" @@ -1,3 +1,20 @@ +--- +title: Maven 教程 +date: 2020-08-04 15:20:54 +categories: + - Java + - 软件 + - 构建 + - Maven +tags: + - Java + - 构建 + - Maven +permalink: /pages/85f27a/ +hidden: true +index: false +--- + # Maven 教程 > [Maven](https://github.com/apache/maven) 是一个项目管理工具。它负责管理项目开发过程中的几乎所有的东西。 @@ -12,12 +29,12 @@ ## 📖 内容 -- [Maven 入门指南](maven-quickstart.md) -- [Maven 教程之 pom.xml 详解](maven-pom.md) -- [Maven 教程之 settings.xml 详解](maven-settings.md) -- [Maven 实战问题和最佳实践](maven-action.md) -- [Maven 教程之发布 jar 到私服或中央仓库](maven-deploy.md) -- [Maven 插件之代码检查](maven-checkstyle-plugin.md) +- [Maven 快速入门](01.Maven快速入门.md) +- [Maven 教程之 pom.xml 详解](02.Maven教程之pom.xml详解.md) +- [Maven 教程之 settings.xml 详解](03.Maven教程之settings.xml详解.md) +- [Maven 实战问题和最佳实践](04.Maven实战问题和最佳实践.md) +- [Maven 教程之发布 jar 到私服或中央仓库](05.Maven教程之发布jar到私服或中央仓库.md) +- [Maven 插件之代码检查](06.Maven插件之代码检查.md) ## 📚 资料 @@ -29,4 +46,4 @@ ## 🚪 传送 -◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git a/docs/javatool/build/ant.md "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" similarity index 96% rename from docs/javatool/build/ant.md rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" index f808f4dc..d0b8d139 100644 --- a/docs/javatool/build/ant.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" @@ -1,22 +1,19 @@ -# Ant 简易教程 - - +--- +title: Ant 简易教程 +date: 2017-12-06 09:46:28 +order: 02 +categories: + - Java + - 软件 + - 构建 +tags: + - Java + - 构建 + - Ant +permalink: /pages/0bafae/ +--- -- [简介](#简介) -- [下载和安装](#下载和安装) - - [下载](#下载) - - [配置环境变量](#配置环境变量) - - [验证](#验证) -- [例子](#例子) -- [关键元素](#关键元素) - - [Project 元素](#project-元素) - - [Target 元素](#target-元素) - - [Task 元素](#task-元素) - - [Property 元素](#property-元素) - - [extension-point 元素](#extension-point-元素) -- [参考资料](#参考资料) - - +# Ant 简易教程 ## 简介 @@ -380,4 +377,4 @@ _——Ant1.8.0 新增特性。_ ## 参考资料 - [ant 官方手册](http://ant.apache.org/manual/index.html) -- [http://www.blogjava.net/amigoxie/archive/2007/11/09/159413.html](http://www.blogjava.net/amigoxie/archive/2007/11/09/159413.html) +- [http://www.blogjava.net/amigoxie/archive/2007/11/09/159413.html](http://www.blogjava.net/amigoxie/archive/2007/11/09/159413.html) \ No newline at end of file diff --git a/docs/javatool/build/README.md "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" similarity index 50% rename from docs/javatool/build/README.md rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" index a1be1ce3..08c9c99a 100644 --- a/docs/javatool/build/README.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" @@ -1,4 +1,19 @@ -# 构建工具 +--- +title: Java 构建 +date: 2020-08-04 15:20:54 +categories: + - Java + - 软件 + - 构建 +tags: + - Java + - 构建 +permalink: /pages/d1859b/ +hidden: true +index: false +--- + +# Java 构建 > Java 项目需要通过 **构建工具** 来管理项目依赖,完成编译、打包、发布、生成 JavaDoc 等任务。 > @@ -10,16 +25,16 @@ ### Maven -- [Maven 入门指南](maven/maven-quickstart.md) -- [Maven 教程之 pom.xml 详解](maven/maven-pom.md) -- [Maven 教程之 settings.xml 详解](maven/maven-settings.md) -- [Maven 实战问题和最佳实践](maven/maven-action.md) -- [Maven 教程之发布 jar 到私服或中央仓库](maven/maven-deploy.md) -- [Maven 插件之代码检查](maven/maven-checkstyle-plugin.md) +- [Maven 快速入门](01.Maven/01.Maven快速入门.md) +- [Maven 教程之 pom.xml 详解](01.Maven/02.Maven教程之pom.xml详解.md) +- [Maven 教程之 settings.xml 详解](01.Maven/03.Maven教程之settings.xml详解.md) +- [Maven 实战问题和最佳实践](01.Maven/04.Maven实战问题和最佳实践.md) +- [Maven 教程之发布 jar 到私服或中央仓库](01.Maven/05.Maven教程之发布jar到私服或中央仓库.md) +- [Maven 插件之代码检查](01.Maven/06.Maven插件之代码检查.md) ### Ant -- [Ant 简易教程](ant.md) +- [Ant 简易教程](02.Ant.md) ## 📚 资料 @@ -32,4 +47,4 @@ ## 🚪 传送 -◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git a/docs/javatool/ide/intellij-idea.md "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" similarity index 94% rename from docs/javatool/ide/intellij-idea.md rename to "docs/01.Java/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" index e1bac2d4..85b4f254 100644 --- a/docs/javatool/ide/intellij-idea.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" @@ -1,4 +1,19 @@ -# Intellij IDEA 应用指南 +--- +title: Intellij IDEA 快速入门 +date: 2019-11-29 18:10:14 +order: 01 +categories: + - Java + - 软件 + - IDE +tags: + - Java + - IDE + - Intellij +permalink: /pages/ac5c6a/ +--- + +# Intellij IDEA 快速入门 ## 快捷键 @@ -241,29 +256,23 @@ IntelliJ IDEA 作为一个以快捷键为中心的 IDE,为大多数操作建 [下载地址](https://github.com/altercation/solarized) -## 破解 +## FAQ -Intellij 是一个收费的 IDE,坦白说有点小贵,买不起。 +(1)运行时报错 -所以,很惭愧,只好用下破解方法了。网上有很多使用注册码的网文,但是注册码不稳定,随时可能被封。还是自行搭建一个注册服务器比较稳定。我使用了 [ilanyu](http://blog.lanyus.com/) 博文 [IntelliJ IDEA License Server 本地搭建教程](http://blog.lanyus.com/archives/174.html) 的方法,亲测十分有效。 +> Error running XXX. Command line is too long. Shorten the command line via JAR manifest or via a classpath file and rerun -我的备用地址:[百度云盘](https://yun.baidu.com/disk/home?#list/vmode=list&path=%2F%E8%BD%AF%E4%BB%B6%2F%E5%BC%80%E5%8F%91%E8%BD%AF%E4%BB%B6%2FIDE) +解决方案: -下载并解压文中的压缩包到本地,选择适合操作系统的版本运行。 +找到 `.idea/libraies/workspace.xml` 中的 `` -如果是在 Linux 上运行,推荐创建一个脚本,代码如下: +添加一行配置: -```bash -# 使用 nohup 创建守护进程,运行 IntelliJIDEALicenseServer_linux_amd64 -# 如果运行在其他 Linux 发行版本,替换执行的脚本即可 -nohup sh IntelliJIDEALicenseServer_linux_amd64 2>&1 +```xml + ``` -这样做是因为:大部分人使用 linux 是使用仿真器连接虚拟机,如果断开连接,进程也会被 kill,每次启动这个注册服务器很麻烦不是吗?而启动了守护进程,则不会出现这种情况,只有你主动 kill 进程才能将其干掉。 - -Windows 版本是 exe 程序,将其设为开机自动启动即可,别告诉我你不知道怎么设置开机自动启动。 - ## 参考资料 - [IntelliJ-IDEA-Tutorial](https://github.com/judasn/IntelliJ-IDEA-Tutorial) -- [极客学院 - Intellij IDEA 使用教程](http://wiki.jikexueyuan.com/project/intellij-idea-tutorial/) +- [极客学院 - Intellij IDEA 使用教程](http://wiki.jikexueyuan.com/project/intellij-idea-tutorial/) \ No newline at end of file diff --git a/docs/javatool/ide/eclipse.md "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" similarity index 99% rename from docs/javatool/ide/eclipse.md rename to "docs/01.Java/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" index 15a6ca98..d8f3730d 100644 --- a/docs/javatool/ide/eclipse.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" @@ -1,4 +1,18 @@ -# Eclipse 应用指南 +--- +title: Eclipse 快速入门 +date: 2018-07-01 11:27:47 +order: 02 +categories: + - Java + - 软件 + - IDE +tags: + - Java + - IDE +permalink: /pages/2257c7/ +--- + +# Eclipse 快速入门 ## 代码智能提示 @@ -230,4 +244,4 @@ http://tomcat.apache.org/download-70.cgi这里有Tomcat的安装包和源码包 | Ctrl+T | 快速显示当前类的继承结构 | | Ctrl+W | 关闭当前 Editer | | Ctrl+L | 文本编辑器 转至行 | -| F2 | 显示工具提示描述 | +| F2 | 显示工具提示描述 | \ No newline at end of file diff --git a/docs/javatool/ide/vscode.md "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" similarity index 78% rename from docs/javatool/ide/vscode.md rename to "docs/01.Java/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" index 3e0b7ac0..909e80c5 100644 --- a/docs/javatool/ide/vscode.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" @@ -1,4 +1,18 @@ -# vscode 应用指南 +--- +title: Vscode 快速入门 +date: 2019-05-14 14:57:33 +order: 03 +categories: + - Java + - 软件 + - IDE +tags: + - Java + - IDE +permalink: /pages/0f7153/ +--- + +# Vscode 快速入门 ## 快捷键 @@ -44,4 +58,4 @@ - https://github.com/Microsoft/vscode-docs - https://github.com/Microsoft/vscode-tips-and-tricks - 更多资源 - - https://github.com/viatsko/awesome-vscode + - https://github.com/viatsko/awesome-vscode \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/README.md" "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/README.md" new file mode 100644 index 00000000..e487ace7 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/README.md" @@ -0,0 +1,26 @@ +--- +title: Java IDE +date: 2022-02-18 08:53:11 +categories: + - Java + - 软件 + - IDE +tags: + - Java + - IDE +permalink: /pages/8695a7/ +hidden: true +index: false +--- + +# Java IDE + +> 自从有了 **IDE**,写代码从此就告别了刀耕火种的蛮荒时代。 +> +> - [Eclipse](02.Eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 +> - 曾经抗拒从转 [Intellij Idea](01.Intellij.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 +> - 你可以在 [vscode](03.VsCode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 + +- [Intellij IDEA 快速入门](01.Intellij.md) +- [Eclipse 快速入门](02.Eclipse.md) +- [Vscode 快速入门](03.VsCode.md) \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" new file mode 100644 index 00000000..7ccdb465 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" @@ -0,0 +1,41 @@ +--- +title: 监控工具对比 +date: 2020-02-11 17:48:32 +order: 01 +categories: + - Java + - 软件 + - 监控诊断 +tags: + - Java + - 监控 +permalink: /pages/16563a/ +--- + +# 监控工具对比 + +## 监控工具发展史 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211165813.png) + +## 监控工具比对 + +### 特性对比 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211171551.png) + +### 生态对比 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211172631.png) + +## 技术选型 + +- Zipkin 欠缺 APM 报表能力,不推荐。 +- 企业级,推荐 CAT +- 关注和试点 SkyWalking。 + +用好调用链监控,需要订制化、自研能力。 + +## 参考资料 + +[CAT、Zipkin 和 SkyWalking 该如何选型?](https://time.geekbang.org/dailylesson/detail/100028416) \ No newline at end of file diff --git a/docs/javatool/monitor/cat.md "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" similarity index 88% rename from docs/javatool/monitor/cat.md rename to "docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" index 3769221d..ee099d36 100644 --- a/docs/javatool/monitor/cat.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" @@ -1,4 +1,19 @@ -# Cat +--- +title: CAT 快速入门 +date: 2020-02-11 17:48:32 +order: 02 +categories: + - Java + - 软件 + - 监控诊断 +tags: + - Java + - 监控 + - CAT +permalink: /pages/821ca3/ +--- + +# CAT 快速入门 ## CAT 简介 @@ -15,14 +30,14 @@ CAT(Central Application Tracking),是基于 Java 开发的分布式实时 ### 支持的消息类型 -CAT监控系统将每次URL、Service的请求内部执行情况都封装为一个完整的消息树、消息树可能包括Transaction、Event、Heartbeat、Metric等信息。 +CAT 监控系统将每次 URL、Service 的请求内部执行情况都封装为一个完整的消息树、消息树可能包括 Transaction、Event、Heartbeat、Metric 等信息。 - **Transaction** 适合记录跨越系统边界的程序访问行为,比如远程调用,数据库调用,也适合执行时间较长的业务逻辑监控,Transaction 用来记录一段代码的执行时间和次数 - **Event** 用来记录一件事发生的次数,比如记录系统异常,它和 transaction 相比缺少了时间的统计,开销比 transaction 要小 - **Heartbeat** 表示程序内定期产生的统计信息, 如 CPU 利用率, 内存利用率, 连接池状态, 系统负载等 - **Metric** 用于记录业务指标、指标可能包含对一个指标记录次数、记录平均值、记录总和,业务指标最低统计粒度为 1 分钟 -![img](http://dunwu.test.upcdn.net/snap/20200211174235.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211174235.png) ## CAT 部署 @@ -57,7 +72,7 @@ CAT 主要分为三个模块: 在实际开发和部署中,cat-consumer 和 cat-home 是部署在一个 jvm 内部,每个 CAT 服务端都可以作为 consumer 也可以作为 home,这样既能减少整个 CAT 层级结构,也可以增加整个系统稳定性。 -![img](http://dunwu.test.upcdn.net/snap/20200211174001.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211174001.png) 上图是 CAT 目前多机房的整体结构图: @@ -68,4 +83,4 @@ CAT 主要分为三个模块: ## 参考资料 -- [CAT Github](https://github.com/dianping/cat) +- [CAT Github](https://github.com/dianping/cat) \ No newline at end of file diff --git a/docs/javatool/monitor/zipkin.md "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" similarity index 94% rename from docs/javatool/monitor/zipkin.md rename to "docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" index e7c76a94..e665495d 100644 --- a/docs/javatool/monitor/zipkin.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" @@ -1,4 +1,19 @@ -# Zipkin 应用指南 +--- +title: Zipkin 快速入门 +date: 2020-03-23 22:56:45 +order: 03 +categories: + - Java + - 软件 + - 监控诊断 +tags: + - Java + - 监控 + - Zipkin +permalink: /pages/0a8826/ +--- + +# Zipkin 快速入门 **Zipkin 是一个基于 Java 开发的、开源的、分布式实时数据跟踪系统(Distributed Tracking System)**。它采集有助于解决服务架构中延迟问题的实时数据。 @@ -14,7 +29,7 @@ Zipkin 基于 Google Dapper 的论文设计而来,由 Twitter 公司开发贡 Zipkin UI 还提供了一个依赖关系图,该关系图显示了每个应用程序中跟踪了多少个请求。这对于识别聚合行为(包括错误路径或对不赞成使用的服务的调用)很有帮助。 -![Zipkin UI](http://dunwu.test.upcdn.net/snap/20200211161706.png) +![Zipkin UI](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211161706.png) ### 多平台 @@ -32,7 +47,7 @@ Zipkin 服务器捆绑了用于采集和存储数据的扩展。 数据以 json 形式存储,可以参考:[Zipkin 官方的 Swagger API](https://zipkin.io/zipkin-api/#/default/post_spans) -![Zipkin Swagger API](http://dunwu.test.upcdn.net/snap/20200211162055.png) +![Zipkin Swagger API](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211162055.png) ## 二、Zipkin 安装 @@ -78,7 +93,7 @@ ZipKin 可以分为两部分, 架构如下: -![Zipkin 架构](http://dunwu.test.upcdn.net/snap/20200211155836.png) +![Zipkin 架构](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211155836.png) ### Zipkin Server @@ -154,4 +169,4 @@ Instrumented client 和 server 是分别使用了 ZipKin Client 的服务,Zipk - [Zipkin 官网](https://zipkin.io/) - [Zipkin Github](https://github.com/openzipkin/zipkin) -- [brave](https://github.com/openzipkin/brave) +- [brave](https://github.com/openzipkin/brave) \ No newline at end of file diff --git a/docs/javatool/monitor/skywalking.md "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" similarity index 77% rename from docs/javatool/monitor/skywalking.md rename to "docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" index 50aa54d1..e3573437 100644 --- a/docs/javatool/monitor/skywalking.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" @@ -1,4 +1,19 @@ -# SkyWalking +--- +title: SkyWalking 快速入门 +date: 2020-02-07 23:04:47 +order: 04 +categories: + - Java + - 软件 + - 监控诊断 +tags: + - Java + - 监控 + - SkyWalking +permalink: /pages/df7dec/ +--- + +# SkyWalking 快速入门 SkyWalking 是一个基于 Java 开发的分布式系统的应用程序性能监视工具,专为微服务、云原生架构和基于容器(Docker、K8s、Mesos)架构而设计。 @@ -8,7 +23,7 @@ SkyWalking 是观察性分析平台和应用性能管理系统。 提供分布式追踪、服务网格遥测分析、度量聚合和可视化一体化解决方案。 -![img](http://dunwu.test.upcdn.net/snap/20200211152235.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211152235.png) ### SkyWalking 特性 @@ -29,9 +44,9 @@ SkyWalking 是观察性分析平台和应用性能管理系统。 从逻辑上讲,SkyWalking 分为四个部分:探针(Probes),平台后端,存储和 UI。 -![SkyWalking 架构](http://dunwu.test.upcdn.net/snap/20200211153516.png) +![SkyWalking 架构](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211153516.png) -- **探针(Probes)** - 探针是指集成到目标系统中的代理或SDK库。它们负责收集数据(包括跟踪数据和统计数据)并将其按照 SkyWalking 的要求重新格式化为。 +- **探针(Probes)** - 探针是指集成到目标系统中的代理或 SDK 库。它们负责收集数据(包括跟踪数据和统计数据)并将其按照 SkyWalking 的要求重新格式化为。 - **平台后端** - 平台后端是一个提供后端服务的集群。它用于聚合、分析和驱动从探针到 UI 的流程。它还为传入格式(如 Zipkin 的格式),存储实现程序和集群管理提供可插入功能。 您甚至可以使用 Observability Analysis Language 自定义聚合和分析。 - **存储** - 您可以选择一个 SkyWalking 已实现的存储,如由 Sharding-Sphere 管理的 ElasticSearch,H2 或 MySQL 集群,也可以自行实现一个存储。 - **UI** - 用户界面很酷,对于 SkyWalking 最终用户而言非常强大。它也可以自定义以匹配您的自定义后端。 @@ -40,7 +55,7 @@ SkyWalking 是观察性分析平台和应用性能管理系统。 进入 [Apache SkyWalking 官方下载页面](http://skywalking.apache.org/downloads/),选择安装版本,下载解压到本地。 -![SkyWalking 组件](http://dunwu.test.upcdn.net/snap/20200211154612.png) +![SkyWalking 组件](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211154612.png) 安装分为三个部分: @@ -50,4 +65,4 @@ SkyWalking 是观察性分析平台和应用性能管理系统。 ## 参考资料 -- [SkyWalking Github](https://github.com/apache/skywalking) +- [SkyWalking Github](https://github.com/apache/skywalking) \ No newline at end of file diff --git a/docs/javatool/monitor/arthas.md "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" similarity index 89% rename from docs/javatool/monitor/arthas.md rename to "docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" index a205242b..7e5726c9 100644 --- a/docs/javatool/monitor/arthas.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" @@ -1,4 +1,19 @@ -# Arthas 应用指南 +--- +title: Arthas 快速入门 +date: 2020-02-07 23:04:47 +order: 05 +categories: + - Java + - 软件 + - 监控诊断 +tags: + - Java + - 诊断 + - Arthas +permalink: /pages/c689d1/ +--- + +# Arthas 快速入门 > `Arthas` 是 Alibaba 开源的 Java 诊断工具 。 @@ -315,8 +330,8 @@ ts=2018-11-28 19:22:35; [cost=29.969732ms] result=@ArrayList[ ### 基础命令 - help——查看命令帮助信息 -- [cat](https://alibaba.github.io/arthas/cat.html)——打印文件内容,和linux里的cat命令类似 -- [pwd](https://alibaba.github.io/arthas/pwd.html)——返回当前的工作目录,和linux命令类似 +- [cat](https://alibaba.github.io/arthas/cat.html)——打印文件内容,和 linux 里的 cat 命令类似 +- [pwd](https://alibaba.github.io/arthas/pwd.html)——返回当前的工作目录,和 linux 命令类似 - cls——清空当前屏幕区域 - session——查看当前会话的信息 - [reset](https://alibaba.github.io/arthas/reset.html)——重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类 @@ -324,33 +339,33 @@ ts=2018-11-28 19:22:35; [cost=29.969732ms] result=@ArrayList[ - history——打印命令历史 - quit——退出当前 Arthas 客户端,其他 Arthas 客户端不受影响 - shutdown——关闭 Arthas 服务端,所有 Arthas 客户端全部退出 -- [keymap](https://alibaba.github.io/arthas/keymap.html)——Arthas快捷键列表及自定义快捷键 +- [keymap](https://alibaba.github.io/arthas/keymap.html)——Arthas 快捷键列表及自定义快捷键 -### jvm相关 +### jvm 相关 - [dashboard](https://alibaba.github.io/arthas/dashboard.html)——当前系统的实时数据面板 - [thread](https://alibaba.github.io/arthas/thread.html)——查看当前 JVM 的线程堆栈信息 - [jvm](https://alibaba.github.io/arthas/jvm.html)——查看当前 JVM 的信息 -- [sysprop](https://alibaba.github.io/arthas/sysprop.html)——查看和修改JVM的系统属性 -- [sysenv](https://alibaba.github.io/arthas/sysenv.html)——查看JVM的环境变量 -- [vmoption](https://alibaba.github.io/arthas/vmoption.html)——查看和修改JVM里诊断相关的option -- [logger](https://alibaba.github.io/arthas/logger.html)——查看和修改logger +- [sysprop](https://alibaba.github.io/arthas/sysprop.html)——查看和修改 JVM 的系统属性 +- [sysenv](https://alibaba.github.io/arthas/sysenv.html)——查看 JVM 的环境变量 +- [vmoption](https://alibaba.github.io/arthas/vmoption.html)——查看和修改 JVM 里诊断相关的 option +- [logger](https://alibaba.github.io/arthas/logger.html)——查看和修改 logger - [getstatic](https://alibaba.github.io/arthas/getstatic.html)——查看类的静态属性 -- [ognl](https://alibaba.github.io/arthas/ognl.html)——执行ognl表达式 +- [ognl](https://alibaba.github.io/arthas/ognl.html)——执行 ognl 表达式 - [mbean](https://alibaba.github.io/arthas/mbean.html)——查看 Mbean 的信息 -- [heapdump](https://alibaba.github.io/arthas/heapdump.html)——dump java heap, 类似jmap命令的heap dump功能 +- [heapdump](https://alibaba.github.io/arthas/heapdump.html)——dump java heap, 类似 jmap 命令的 heap dump 功能 -### class/classloader相关 +### class/classloader 相关 -- [sc](https://alibaba.github.io/arthas/sc.html)——查看JVM已加载的类信息 +- [sc](https://alibaba.github.io/arthas/sc.html)——查看 JVM 已加载的类信息 - [sm](https://alibaba.github.io/arthas/sm.html)——查看已加载类的方法信息 - [jad](https://alibaba.github.io/arthas/jad.html)——反编译指定已加载类的源码 - [mc](https://alibaba.github.io/arthas/mc.html)——内存编绎器,内存编绎`.java`文件为`.class`文件 -- [redefine](https://alibaba.github.io/arthas/redefine.html)——加载外部的`.class`文件,redefine到JVM里 +- [redefine](https://alibaba.github.io/arthas/redefine.html)——加载外部的`.class`文件,redefine 到 JVM 里 - [dump](https://alibaba.github.io/arthas/dump.html)——dump 已加载类的 byte code 到特定目录 -- [classloader](https://alibaba.github.io/arthas/classloader.html)——查看classloader的继承树,urls,类加载信息,使用classloader去getResource +- [classloader](https://alibaba.github.io/arthas/classloader.html)——查看 classloader 的继承树,urls,类加载信息,使用 classloader 去 getResource -### monitor/watch/trace相关 +### monitor/watch/trace 相关 > 请注意,这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行 `shutdown` 或将增强过的类执行 `reset` 命令。 @@ -362,29 +377,29 @@ ts=2018-11-28 19:22:35; [cost=29.969732ms] result=@ArrayList[ ### options -- [options](https://alibaba.github.io/arthas/options.html)——查看或设置Arthas全局开关 +- [options](https://alibaba.github.io/arthas/options.html)——查看或设置 Arthas 全局开关 ### 管道 -Arthas支持使用管道对上述命令的结果进行进一步的处理,如`sm java.lang.String * | grep 'index'` +Arthas 支持使用管道对上述命令的结果进行进一步的处理,如`sm java.lang.String * | grep 'index'` -- grep——搜索满足条件的结果 -- plaintext——将命令的结果去除ANSI颜色 +- grep——搜索满足条件的  结果 +- plaintext——将  命令的结果去除 ANSI 颜色 - wc——按行统计输出结果 ### 后台异步任务 -当线上出现偶发的问题,比如需要watch某个条件,而这个条件一天可能才会出现一次时,异步后台任务就派上用场了,详情请参考[这里](https://alibaba.github.io/arthas/async.html) +当线上出现偶发的问题,比如需要 watch 某个条件,而这个条件一天可能才会出现一次时,异步后台任务就派上用场了,详情请参考[这里](https://alibaba.github.io/arthas/async.html) -- 使用 > 将结果重写向到日志文件,使用 & 指定命令是后台运行,session断开不影响任务执行(生命周期默认为1天) -- jobs——列出所有job +- 使用 > 将结果重写向到日志文件,使用 & 指定命令是后台运行,session 断开不影响任务执行(生命周期默认为 1 天) +- jobs——列出所有 job - kill——强制终止任务 - fg——将暂停的任务拉到前台执行 - bg——将暂停的任务放到后台执行 ### Web Console -通过websocket连接Arthas。 +通过 websocket 连接 Arthas。 - [Web Console](https://alibaba.github.io/arthas/web-console.html) @@ -394,7 +409,7 @@ Arthas支持使用管道对上述命令的结果进行进一步的处理,如`s 在启动时,指定`stat-url`,就会回报执行的每一行命令,比如: `./as.sh --stat-url 'http://192.168.10.11:8080/api/stat'` -在tunnel server里有一个示例的回报代码,用户可以自己在服务器上实现。 +在 tunnel server 里有一个示例的回报代码,用户可以自己在服务器上实现。 [StatController.java](https://github.com/alibaba/arthas/blob/master/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/web/StatController.java) @@ -403,10 +418,10 @@ Arthas支持使用管道对上述命令的结果进行进一步的处理,如`s - [异步命令支持](https://alibaba.github.io/arthas/async.html) - [执行结果存日志](https://alibaba.github.io/arthas/save-log.html) - [批处理的支持](https://alibaba.github.io/arthas/batch-support.html) -- [ognl表达式的用法说明](https://github.com/alibaba/arthas/issues/11) +- [ognl 表达式的用法说明](https://github.com/alibaba/arthas/issues/11) ## 参考资料 - [Arthas Github](https://github.com/alibaba/arthas) - [Arthas 用户文档](https://alibaba.github.io/arthas/index.html) -- [arthas源码分析](https://www.jianshu.com/p/4e34d0ab47d1) +- [arthas 源码分析](https://www.jianshu.com/p/4e34d0ab47d1) \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" new file mode 100644 index 00000000..a2caaea0 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" @@ -0,0 +1,33 @@ +--- +title: Java 监控诊断 +date: 2020-02-11 17:48:32 +categories: + - Java + - 软件 + - 监控诊断 +tags: + - Java + - 监控 + - 诊断 +permalink: /pages/3d16d3/ +hidden: true +index: false +--- + +# Java 监控诊断 + +## 内容 + +- [监控工具对比](01.监控工具对比.md) +- [CAT](02.CAT.md) +- [Zipkin](03.Zipkin.md) +- [SkyWalking](04.Skywalking.md) +- [Arthas](05.Arthas.md) + +## 资料 + +- [CAT Github](https://github.com/dianping/cat) +- [Zipkin Github](https://github.com/openzipkin/zipkin) +- [SkyWalking Github](https://github.com/apache/skywalking) +- [PinPoint Github](https://github.com/naver/pinpoint) +- [Arthas Github](https://github.com/alibaba/arthas) \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/README.md" "b/docs/01.Java/11.\350\275\257\344\273\266/README.md" new file mode 100644 index 00000000..d44fbe38 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/README.md" @@ -0,0 +1,70 @@ +--- +title: Java 软件 +date: 2022-02-18 08:53:11 +categories: + - Java + - 软件 +tags: + - Java +permalink: /pages/2cb045/ +hidden: true +index: false +--- + +# Java 软件 + +> 本部分内容主要是 Java 开发领域使用的一些 Java 软件,如构建工具、IDE、服务器、日志中心等等。 + +## 📖 内容 + +### 构建 + +> Java 项目需要通过 [**构建工具**](01.构建) 来管理项目依赖,完成编译、打包、发布、生成 JavaDoc 等任务。 +> +> - 目前最主流的构建工具是 Maven,它的功能非常强大。 +> - Gradle 号称是要替代 Maven 等构件工具,它的版本管理确实简洁,但是需要学习 Groovy,学习成本比 Maven 高。 +> - Ant 功能比 Maven 和 Gradle 要弱,现代 Java 项目基本不用了,但也有一些传统的 Java 项目还在使用。 + +- [Maven](01.构建/01.Maven) 📚 + - [Maven 快速入门](01.构建/01.Maven/01.Maven快速入门.md) + - [Maven 教程之 pom.xml 详解](01.构建/01.Maven/02.Maven教程之pom.xml详解.md) + - [Maven 教程之 settings.xml 详解](01.构建/01.Maven/03.Maven教程之settings.xml详解.md) + - [Maven 实战问题和最佳实践](01.构建/01.Maven/04.Maven实战问题和最佳实践.md) + - [Maven 教程之发布 jar 到私服或中央仓库](01.构建/01.Maven/05.Maven教程之发布jar到私服或中央仓库.md) + - [Maven 插件之代码检查](01.构建/01.Maven/06.Maven插件之代码检查.md) +- [Ant 简易教程](01.构建/02.Ant.md) + +### IDE + +> 自从有了 [**IDE**](02.IDE),写代码从此就告别了刀耕火种的蛮荒时代。 +> +> - [Eclipse](02.IDE/02.Eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 +> - 曾经抗拒从转 [Intellij Idea](02.IDE/01.Intellij.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 +> - 你可以在 [vscode](02.IDE/03.VsCode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 + +- [Intellij Idea](02.IDE/01.Intellij.md) +- [Eclipse](02.IDE/02.Eclipse.md) +- [vscode](02.IDE/03.VsCode.md) + +### 监控诊断 + +> [监控/诊断](03.监控诊断) 工具主要用于 Java 应用的运维。通过采集、分析、存储、可视化应用的有效数据,帮助开发者、使用者快速定位问题,找到性能瓶颈。 + +- [监控工具对比](03.监控诊断/01.监控工具对比.md) +- [CAT](03.监控诊断/02.CAT.md) +- [Zipkin](03.监控诊断/03.Zipkin.md) +- [SkyWalking](03.监控诊断/04.Skywalking.md) +- [Arthas](03.监控诊断/05.Arthas.md) + +## 📚 资料 + +- **官网** + - [Maven Github](https://github.com/apache/maven) + - [Maven 官方文档](https://maven.apache.org/ref/current) + - [Ant 官方手册](http://ant.apache.org/manual/index.html) +- **书籍** + - [《Maven 实战》](https://book.douban.com/subject/5345682/) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" new file mode 100644 index 00000000..2e3cfea8 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" @@ -0,0 +1,485 @@ +--- +title: Java 和 JSON 序列化 +date: 2022-02-17 22:34:30 +order: 01 +categories: + - Java + - 工具 + - IO +tags: + - Java + - IO + - 序列化 + - JSON +permalink: /pages/4622a6/ +--- + +# Java 和 JSON 序列化 + +> JSON(JavaScript Object Notation)是一种基于文本的数据交换格式。几乎所有的编程语言都有很好的库或第三方工具来提供基于 JSON 的 API 支持,因此你可以非常方便地使用任何自己喜欢的编程语言来处理 JSON 数据。 +> +> 本文主要从 Java 语言的角度来讲解 JSON 的应用。 + +## JSON 简介 + +### JSON 是什么 + +JSON 起源于 1999 年的 [JS 语言规范 ECMA262 的一个子集](http://javascript.crockford.com/)(即 15.12 章节描述了格式与解析),后来 2003 年作为一个数据格式[ECMA404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf)(很囧的序号有不有?)发布。 +2006 年,作为 [rfc4627](http://www.ietf.org/rfc/rfc4627.txt) 发布,这时规范增加到 18 页,去掉没用的部分,十页不到。 + +JSON 的应用很广泛,这里有超过 100 种语言下的 JSON 库:[json.org](http://www.json.org/)。 + +更多的可以参考这里,[关于 json 的一切](https://github.com/burningtree/awesome-json)。 + +### JSON 标准 + +这估计是最简单标准规范之一: + +- 只有两种结构:对象内的键值对集合结构和数组,对象用 `{}` 表示、内部是 `"key":"value"`,数组用 `[]` 表示,不同值用逗号分开 +- 基本数值有 7 个: `false` / `null` / `true` / `object` / `array` / `number` / `string` +- 再加上结构可以嵌套,进而可以用来表达复杂的数据 +- 一个简单实例: + +```json +{ + "Image": { + "Width": 800, + "Height": 600, + "Title": "View from 15th Floor", + "Thumbnail": { + "Url": "http://www.example.com/image/481989943", + "Height": 125, + "Width": "100" + }, + "IDs": [116, 943, 234, 38793] + } +} +``` + +> 扩展阅读: +> +> - - 图文并茂介绍 json 数据形式 +> +> - [json 的 RFC 文档](http://tools.ietf.org/html/rfc4627) + +### JSON 优缺点 + +优点: + +- 基于纯文本,所以对于人类阅读是很友好的。 +- 规范简单,所以容易处理,开箱即用,特别是 JS 类的 ECMA 脚本里是内建支持的,可以直接作为对象使用。 +- 平台无关性,因为类型和结构都是平台无关的,而且好处理,容易实现不同语言的处理类库,可以作为多个不同异构系统之间的数据传输格式协议,特别是在 HTTP/REST 下的数据格式。 + +缺点: + +- 性能一般,文本表示的数据一般来说比二进制大得多,在数据传输上和解析处理上都要更影响性能。 +- 缺乏 schema,跟同是文本数据格式的 XML 比,在类型的严格性和丰富性上要差很多。XML 可以借由 XSD 或 DTD 来定义复杂的格式,并由此来验证 XML 文档是否符合格式要求,甚至进一步的,可以基于 XSD 来生成具体语言的操作代码,例如 apache xmlbeans。并且这些工具组合到一起,形成一套庞大的生态,例如基于 XML 可以实现 SOAP 和 WSDL,一系列的 ws-\*规范。但是我们也可以看到 JSON 在缺乏规范的情况下,实际上有更大一些的灵活性,特别是近年来 REST 的快速发展,已经有一些 schema 相关的发展(例如[理解 JSON Schema](https://spacetelescope.github.io/understanding-json-schema/index.html),[使用 JSON Schema](http://usingjsonschema.com/downloads/), [在线 schema 测试](http://azimi.me/json-schema-view/demo/demo.html)),也有类似于 WSDL 的[WADL](https://www.w3.org/Submission/wadl/)出现。 + +### JSON 工具 + +- 使用 JSON 实现 RPC(类似 XML-RPC):[JSON-RPC](http://www.jsonrpc.org/) +- 使用 JSON 实现 path 查询操作(类似 XML-PATH):[JsonPATH](https://github.com/json-path/JsonPath) +- 在线查询工具:[JsonPATH](http://jsonpath.com/) + +- 格式化工具:[jsbeautifier](http://jsbeautifier.org/) +- chrome 插件:[5 个 Json View 插件](http://www.cnplugins.com/zhuanti/five-chrome-json-plugins.html) +- 在线 Mock: [在线 mock](https://www.easy-mock.com/) +- 其他 Mock:[SoapUI](https://www.soapui.org/rest-testing-mocking/rest-service-mocking.html)可以支持,SwaggerUI 也可以,[RestMock](https://github.com/andrzejchm/RESTMock)也可以。 + +### Java JSON 库 + +Java 中比较流行的 JSON 库有: + +- [Fastjson](https://github.com/alibaba/fastjson) - 阿里巴巴开发的 JSON 库,性能十分优秀。 +- [Jackson](http://wiki.fasterxml.com/JacksonHome) - 社区十分活跃且更新速度很快。Spring 框架默认 JSON 库。 +- [Gson](https://github.com/google/gson) - 谷歌开发的 JSON 库,目前功能最全的 JSON 库 。 + +从性能上来看,一般情况下:Fastjson > Jackson > Gson + +### JSON 编码指南 + +> 遵循好的设计与编码风格,能提前解决 80%的问题,个人推荐 Google JSON 风格指南。 +> +> - 英文版[Google JSON Style Guide](https://google.github.io/styleguide/jsoncstyleguide.xml): +> - 中文版[Google JSON 风格指南](https://github.com/darcyliu/google-styleguide/blob/master/JSONStyleGuide.md): + +简单摘录如下: + +- 属性名和值都是用双引号,不要把注释写到对象里面,对象数据要简洁 +- 不要随意结构化分组对象,推荐是用扁平化方式,层次不要太复杂 +- 命名方式要有意义,比如单复数表示 +- 驼峰式命名,遵循 Bean 规范 +- 使用版本来控制变更冲突 +- 对于一些关键字,不要拿来做 key +- 如果一个属性是可选的或者包含空值或 null 值,考虑从 JSON 中去掉该属性,除非它的存在有很强的语义原因 +- 序列化枚举类型时,使用 name 而不是 value +- 日期要用标准格式处理 +- 设计好通用的分页参数 +- 设计好异常处理 + +[JSON API](http://jsonapi.org.cn/format/)与 Google JSON 风格指南有很多可以相互参照之处。 + +[JSON API](http://jsonapi.org.cn/format/)是数据交互规范,用以定义客户端如何获取与修改资源,以及服务器如何响应对应请求。 + +JSON API 设计用来最小化请求的数量,以及客户端与服务器间传输的数据量。在高效实现的同时,无需牺牲可读性、灵活性和可发现性。 + +## Fastjson 应用 + +### 添加 maven 依赖 + +```xml + + com.alibaba + fastjson + x.x.x + +``` + +### Fastjson API + +#### 定义 Bean + +**Group.java** + +```java +public class Group { + + private Long id; + private String name; + private List users = new ArrayList(); +} +``` + +**User.java** + +```java +public class User { + + private Long id; + private String name; +} +``` + +**初始化 Bean** + +```java +Group group = new Group(); +group.setId(0L); +group.setName("admin"); + +User guestUser = new User(); +guestUser.setId(2L); +guestUser.setName("guest"); + +User rootUser = new User(); +rootUser.setId(3L); +rootUser.setName("root"); + +group.addUser(guestUser); +group.addUser(rootUser); +``` + +#### 序列化 + +```java +String jsonString = JSON.toJSONString(group); +System.out.println(jsonString); +``` + +#### 反序列化 + +```java +Group bean = JSON.parseObject(jsonString, Group.class); +``` + +### Fastjson 注解 + +#### `@JSONField` + +> 扩展阅读:更多 API 使用细节可以参考:[JSONField 用法](https://github.com/alibaba/fastjson/wiki/JSONField),这里介绍基本用法。 + +可以配置在属性(setter、getter)和字段(必须是 public field)上。 + +```java +@JSONField(name="ID") +public int getId() {return id;} + +// 配置date序列化和反序列使用yyyyMMdd日期格式 +@JSONField(format="yyyyMMdd") +public Date date1; + +// 不序列化 +@JSONField(serialize=false) +public Date date2; + +// 不反序列化 +@JSONField(deserialize=false) +public Date date3; + +// 按ordinal排序 +@JSONField(ordinal = 2) +private int f1; + +@JSONField(ordinal = 1) +private int f2; +``` + +#### `@JSONType` + +- 自定义序列化:[ObjectSerializer](https://github.com/alibaba/fastjson/wiki/JSONType_serializer) +- 子类型处理:[SeeAlso](https://github.com/alibaba/fastjson/wiki/JSONType_seeAlso_cn) + +JSONType.alphabetic 属性: fastjson 缺省时会使用字母序序列化,如果你是希望按照 java fields/getters 的自然顺序序列化,可以配置 JSONType.alphabetic,使用方法如下: + +```java +@JSONType(alphabetic = false) +public static class B { + public int f2; + public int f1; + public int f0; +} +``` + +## Jackson 应用 + +> 扩展阅读:更多 API 使用细节可以参考 [jackson-databind 官方说明](https://github.com/FasterXML/jackson-databind) + +### 添加 maven 依赖 + +```xml + + com.fasterxml.jackson.core + jackson-databind + 2.9.8 + +``` + +### Jackson API + +#### 序列化 + +```java +ObjectMapper mapper = new ObjectMapper(); + +mapper.writeValue(new File("result.json"), myResultObject); +// or: +byte[] jsonBytes = mapper.writeValueAsBytes(myResultObject); +// or: +String jsonString = mapper.writeValueAsString(myResultObject); +``` + +#### 反序列化 + +```java +ObjectMapper mapper = new ObjectMapper(); + +MyValue value = mapper.readValue(new File("data.json"), MyValue.class); +// or: +value = mapper.readValue(new URL("http://some.com/api/entry.json"), MyValue.class); +// or: +value = mapper.readValue("{\"name\":\"Bob\", \"age\":13}", MyValue.class); +``` + +#### 容器的序列化和反序列化 + +```java +Person p = new Person("Tom", 20); +Person p2 = new Person("Jack", 22); +Person p3 = new Person("Mary", 18); + +List persons = new LinkedList<>(); +persons.add(p); +persons.add(p2); +persons.add(p3); + +Map map = new HashMap<>(); +map.put("persons", persons); + +String json = null; +try { + json = mapper.writeValueAsString(map); +} catch (JsonProcessingException e) { + e.printStackTrace(); +} +``` + +### Jackson 注解 + +> 扩展阅读:更多注解使用细节可以参考 [jackson-annotations 官方说明](https://github.com/FasterXML/jackson-annotations/wiki/Jackson-Annotations) + +#### `@JsonProperty` + +```java +public class MyBean { + private String _name; + + // without annotation, we'd get "theName", but we want "name": + @JsonProperty("name") + public String getTheName() { return _name; } + + // note: it is enough to add annotation on just getter OR setter; + // so we can omit it here + public void setTheName(String n) { _name = n; } +} +``` + +#### `@JsonIgnoreProperties` 和 `@JsonIgnore` + +```java +// means that if we see "foo" or "bar" in JSON, they will be quietly skipped +// regardless of whether POJO has such properties +@JsonIgnoreProperties({ "foo", "bar" }) +public class MyBean { + // will not be written as JSON; nor assigned from JSON: + @JsonIgnore + public String internal; + + // no annotation, public field is read/written normally + public String external; + + @JsonIgnore + public void setCode(int c) { _code = c; } + + // note: will also be ignored because setter has annotation! + public int getCode() { return _code; } +} +``` + +#### `@JsonCreator` + +```java +public class CtorBean { + public final String name; + public final int age; + + @JsonCreator // constructor can be public, private, whatever + private CtorBean(@JsonProperty("name") String name, + @JsonProperty("age") int age) + { + this.name = name; + this.age = age; + } +} +``` + +#### `@JsonPropertyOrder` + +alphabetic 设为 true 表示,json 字段按自然顺序排列,默认为 false。 + +```java +@JsonPropertyOrder(alphabetic = true) +public class JacksonAnnotationBean {} +``` + +## Gson 应用 + +> 详细内容可以参考官方文档:[Gson 用户指南](https://github.com/google/gson/blob/master/UserGuide.md) + +### 添加 maven 依赖 + +```xml + + com.google.code.gson + gson + 2.8.6 + +``` + +### Gson API + +#### 序列化 + +```java +Gson gson = new Gson(); +gson.toJson(1); // ==> 1 +gson.toJson("abcd"); // ==> "abcd" +gson.toJson(10L); // ==> 10 +int[] values = { 1 }; +gson.toJson(values); // ==> [1] +``` + +#### 反序列化 + +```java +int i1 = gson.fromJson("1", int.class); +Integer i2 = gson.fromJson("1", Integer.class); +Long l1 = gson.fromJson("1", Long.class); +Boolean b1 = gson.fromJson("false", Boolean.class); +String str = gson.fromJson("\"abc\"", String.class); +String[] anotherStr = gson.fromJson("[\"abc\"]", String[].class); +``` + +#### GsonBuilder + +`Gson` 实例可以通过 `GsonBuilder` 来定制实例化,以控制其序列化、反序列化行为。 + +```java +Gson gson = new GsonBuilder() + .setPrettyPrinting() + .setDateFormat("yyyy-MM-dd HH:mm:ss") + .excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT, Modifier.VOLATILE) + .create(); +``` + +### Gson 注解 + +#### `@Since` + +[`@Since`](https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/annotations/Since.java) 用于控制对象的序列化版本。示例: + +```java +public class VersionedClass { + @Since(1.1) private final String newerField; + @Since(1.0) private final String newField; + private final String field; + + public VersionedClass() { + this.newerField = "newer"; + this.newField = "new"; + this.field = "old"; + } +} + +VersionedClass versionedObject = new VersionedClass(); +Gson gson = new GsonBuilder().setVersion(1.0).create(); +String jsonOutput = gson.toJson(versionedObject); +System.out.println(jsonOutput); +System.out.println(); + +gson = new Gson(); +jsonOutput = gson.toJson(versionedObject); +System.out.println(jsonOutput); +``` + +#### `@SerializedName` + +`@SerializedName` 用于将类成员按照指定名称序列化、反序列化。示例: + +```java +private class SomeObject { + @SerializedName("custom_naming") private final String someField; + private final String someOtherField; + + public SomeObject(String a, String b) { + this.someField = a; + this.someOtherField = b; + } +} +``` + +## 示例源码 + +> 示例源码:[javalib-io-json](https://github.com/dunwu/java-tutorial/tree/master/javalib-io-json) + +## 参考资料 + +- **官方** + - [Fastjson Github](https://github.com/alibaba/fastjson) + - [Gson Github](https://github.com/google/gson) + - [jackson 官方文档](https://github.com/FasterXML/jackson-docs) + - [jackson-databind](https://github.com/FasterXML/jackson-databind) +- **文章** + - + - [json 的 RFC 文档](http://tools.ietf.org/html/rfc4627) + - [JSON 最佳实践](https://kimmking.github.io/2017/06/06/json-best-practice/) + - [【简明教程】JSON](https://www.jianshu.com/p/8b428e1d1564) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" new file mode 100644 index 00000000..631a5fcb --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" @@ -0,0 +1,391 @@ +--- +title: Java 二进制序列化 +date: 2022-02-17 22:34:30 +order: 02 +categories: + - Java + - 工具 + - IO +tags: + - Java + - IO + - 序列化 + - 二进制 +permalink: /pages/08d872/ +--- + +# Java 二进制序列化 + +## 简介 + +### 为什么需要二进制序列化库 + +原因很简单,就是 Java 默认的序列化机制(`ObjectInputStream` 和 `ObjectOutputStream`)具有很多缺点。 + +> 不了解 Java 默认的序列化机制,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/2b2f0f/) + +Java 自身的序列化方式具有以下缺点: + +- **无法跨语言使用**。这点最为致命,对于很多需要跨语言通信的异构系统来说,不能跨语言序列化,即意味着完全无法通信(彼此数据不能识别,当然无法交互了)。 +- **序列化的性能不高**。序列化后的数据体积较大,这大大影响存储和传输的效率。 +- 序列化一定需要实现 `Serializable` 接口。 +- 需要关注 `serialVersionUID`。 + +引入二进制序列化库就是为了解决这些问题,这在 RPC 应用中尤为常见。 + +### 主流序列化库简介 + +#### Protobuf + +[Protobuf](https://developers.google.com/protocol-buffers/) 是 Google 公司内部的混合语言数据标准,是一种轻便、高效的结构化数据存储 +格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。Protobuf +使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL +编译器,生成序列化工具类。 + +优点: + +- 序列化后体积相比 JSON、Hessian 小很多 +- 序列化反序列化速度很快,不需要通过反射获取类型 +- 语言和平台无关(基于 IDL),IDL 能清晰地描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器 +- 消息格式升级和兼容性不错,可以做到后向兼容 +- 支持 Java, C++, Python 三种语言 + +缺点: + +- Protobuf 对于具有反射和动态能力的语言来说,用起来很费劲。 + +#### Thrift + +> [Thrift](https://github.com/apache/thrift) 是 apache 开源项目,是一个点对点的 RPC 实现。 + +它具有以下特性: + +- 支持多种语言(目前支持 28 种语言,如:C++、go、Java、Php、Python、Ruby 等等)。 +- 使用了组建大型数据交换及存储工具,对于大型系统中的内部数据传输,相对于 Json 和 xml 在性能上和传输大小上都有明显的优势。 +- 支持三种比较典型的编码方式(通用二进制编码,压缩二进制编码,优化的可选字段压缩编解码)。 + +#### Hessian + +[Hessian](http://hessian.caucho.com/) 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。Hessian 协 +议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节 +数也更小。 + +RPC 框架 Dubbo 就支持 Thrift 和 Hession。 + +它具有以下特性: + +- 支持多种语言。如:Java、Python、C++、C#、PHP、Ruby 等。 +- 相对其他二进制序列化库较慢。 + +Hessian 本身也有问题,官方版本对 Java 里面一些常见对象的类型不支持: + +- Linked 系列,LinkedHashMap、LinkedHashSet 等,但是可以通过扩展 CollectionDeserializer 类修复; +- Locale 类,可以通过扩展 ContextSerializerFactory 类修复; +- Byte/Short 反序列化的时候变成 Integer。 + +#### Kryo + +> [Kryo](https://github.com/EsotericSoftware/kryo) 是用于 Java 的快速高效的二进制对象图序列化框架。Kryo 还可以执行自动的深拷贝和浅拷贝。 这是从对象到对象的直接复制,而不是从对象到字节的复制。 + +它具有以下特性: + +- 速度快,序列化体积小 +- 官方不支持 Java 以外的其他语言 + +#### FST + +> [FST](https://github.com/RuedigerMoeller/fast-serialization) 是一个 Java 实现二进制序列化库。 + +它具有以下特性: + +- 近乎于 100% 兼容 JDK 序列化,且比 JDK 原序列化方式快 10 倍 +- 2.17 开始与 Android 兼容 +- (可选)2.29 开始支持将任何可序列化的对象图编码/解码为 JSON(包括共享引用) + +#### 小结 + +了解了以上这些常见的二进制序列化库的特性。在技术选型时,我们就可以做到有的放矢。 + +**(1)选型参考依据** + +对于二进制序列化库,我们的选型考量一般有以下几点: + +- **是否支持跨语言** + - 根据业务实际需求来决定。一般来说,支持跨语言,为了兼容,使用复杂度上一般会更高一些。 +- **序列化、反序列化的性能** +- **类库是否轻量化,API 是否简单易懂** + +**(2)选型建议** + +- 如果需要跨语言通信,那么可以考虑:Protobuf、Thrift、Hession。 + + - [thrift](https://github.com/apache/thrift)、[protobuf](https://github.com/protocolbuffers/protobuf) - 适用于对性能敏感,对开发体验要求不高的内部系统。 + - [hessian](http://hessian.caucho.com/doc/hessian-overview.xtp) - 适用于对开发体验敏感,性能有要求的内外部系统。 + +- 如果不需要跨语言通信,可以考虑:[Kryo](https://github.com/EsotericSoftware/kryo) 和 [FST](https://github.com/RuedigerMoeller/fast-serialization),性能不错,且 API 十分简单。 + +## FST 应用 + +### 引入依赖 + +```xml + + de.ruedigermoeller + fst + 2.56 + +``` + +### FST API + +示例: + +```java +import org.nustaq.serialization.FSTConfiguration; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class FstDemo { + + private static FSTConfiguration DEFAULT_CONFIG = FSTConfiguration.createDefaultConfiguration(); + + /** + * 将对象序列化为 byte 数组 + * + * @param obj 任意对象 + * @param 对象的类型 + * @return 序列化后的 byte 数组 + */ + public static byte[] writeToBytes(T obj) { + return DEFAULT_CONFIG.asByteArray(obj); + } + + /** + * 将对象序列化为 byte 数组后,再使用 Base64 编码 + * + * @param obj 任意对象 + * @param 对象的类型 + * @return 序列化后的字符串 + */ + public static String writeToString(T obj) { + byte[] bytes = writeToBytes(obj); + return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8); + } + + /** + * 将 byte 数组反序列化为原对象 + * + * @param bytes {@link #writeToBytes} 方法序列化后的 byte 数组 + * @param clazz 原对象的类型 + * @param 原对象的类型 + * @return 原对象 + */ + public static T readFromBytes(byte[] bytes, Class clazz) throws IOException { + Object obj = DEFAULT_CONFIG.asObject(bytes); + if (clazz.isInstance(obj)) { + return (T) obj; + } else { + throw new IOException("derialize failed"); + } + } + + /** + * 将字符串反序列化为原对象,先使用 Base64 解码 + * + * @param str {@link #writeToString} 方法序列化后的字符串 + * @param clazz 原对象的类型 + * @param 原对象的类型 + * @return 原对象 + */ + public static T readFromString(String str, Class clazz) throws IOException { + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + return readFromBytes(Base64.getDecoder().decode(bytes), clazz); + } + +} +``` + +测试: + +```java +long begin = System.currentTimeMillis(); +for (int i = 0; i < BATCH_SIZE; i++) { + TestBean oldBean = BeanUtils.initJdk8Bean(); + byte[] bytes = FstDemo.writeToBytes(oldBean); + TestBean newBean = FstDemo.readFromBytes(bytes, TestBean.class); +} +long end = System.currentTimeMillis(); +System.out.printf("FST 序列化/反序列化耗时:%s", (end - begin)); +``` + +## Kryo 应用 + +### 引入依赖 + +```xml + + com.esotericsoftware + kryo + 5.0.0-RC4 + +``` + +### Kryo API + +示例: + +```java +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; +import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy; +import org.objenesis.strategy.StdInstantiatorStrategy; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class KryoDemo { + + // 每个线程的 Kryo 实例 + private static final ThreadLocal kryoLocal = ThreadLocal.withInitial(() -> { + Kryo kryo = new Kryo(); + + /** + * 不要轻易改变这里的配置!更改之后,序列化的格式就会发生变化, + * 上线的同时就必须清除 Redis 里的所有缓存, + * 否则那些缓存再回来反序列化的时候,就会报错 + */ + //支持对象循环引用(否则会栈溢出) + kryo.setReferences(true); //默认值就是 true,添加此行的目的是为了提醒维护者,不要改变这个配置 + + //不强制要求注册类(注册行为无法保证多个 JVM 内同一个类的注册编号相同;而且业务系统中大量的 Class 也难以一一注册) + kryo.setRegistrationRequired(false); //默认值就是 false,添加此行的目的是为了提醒维护者,不要改变这个配置 + + //Fix the NPE bug when deserializing Collections. + ((DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy()) + .setFallbackInstantiatorStrategy(new StdInstantiatorStrategy()); + + return kryo; + }); + + /** + * 获得当前线程的 Kryo 实例 + * + * @return 当前线程的 Kryo 实例 + */ + public static Kryo getInstance() { + return kryoLocal.get(); + } + + /** + * 将对象序列化为 byte 数组 + * + * @param obj 任意对象 + * @param 对象的类型 + * @return 序列化后的 byte 数组 + */ + public static byte[] writeToBytes(T obj) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + Output output = new Output(byteArrayOutputStream); + + Kryo kryo = getInstance(); + kryo.writeObject(output, obj); + output.flush(); + + return byteArrayOutputStream.toByteArray(); + } + + /** + * 将对象序列化为 byte 数组后,再使用 Base64 编码 + * + * @param obj 任意对象 + * @param 对象的类型 + * @return 序列化后的字符串 + */ + public static String writeToString(T obj) { + byte[] bytes = writeToBytes(obj); + return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8); + } + + /** + * 将 byte 数组反序列化为原对象 + * + * @param bytes {@link #writeToBytes} 方法序列化后的 byte 数组 + * @param clazz 原对象的类型 + * @param 原对象的类型 + * @return 原对象 + */ + @SuppressWarnings("unchecked") + public static T readFromBytes(byte[] bytes, Class clazz) { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + Input input = new Input(byteArrayInputStream); + + Kryo kryo = getInstance(); + return (T) kryo.readObject(input, clazz); + } + + /** + * 将字符串反序列化为原对象,先使用 Base64 解码 + * + * @param str {@link #writeToString} 方法序列化后的字符串 + * @param clazz 原对象的类型 + * @param 原对象的类型 + * @return 原对象 + */ + public static T readFromString(String str, Class clazz) { + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + return readFromBytes(Base64.getDecoder().decode(bytes), clazz); + } + +} +``` + +测试: + +```java +long begin = System.currentTimeMillis(); +for (int i = 0; i < BATCH_SIZE; i++) { + TestBean oldBean = BeanUtils.initJdk8Bean(); + byte[] bytes = KryoDemo.writeToBytes(oldBean); + TestBean newBean = KryoDemo.readFromBytes(bytes, TestBean.class); +} +long end = System.currentTimeMillis(); +System.out.printf("Kryo 序列化/反序列化耗时:%s", (end - begin)); +``` + +Hessian 应用 + +```java +Student student = new Student(); +student.setNo(101); +student.setName("HESSIAN"); +//把student对象转化为byte数组 +ByteArrayOutputStream bos = new ByteArrayOutputStream(); +Hessian2Output output = new Hessian2Output(bos); +output.writeObject(student); +output.flushBuffer(); +byte[] data = bos.toByteArray(); +bos.close(); +//把刚才序列化出来的byte数组转化为student对象 +ByteArrayInputStream bis = new ByteArrayInputStream(data); +Hessian2Input input = new Hessian2Input(bis); +Student deStudent = (Student) input.readObject(); +input.close(); +System.out.println(deStudent); +``` + +## 参考资料 + +- **官方** + - [Protobuf 官网](https://developers.google.com/protocol-buffers/) + - [Protobuf Github](https://github.com/protocolbuffers/protobuf) + - [Thrift Github](https://github.com/apache/thrift) + - [Kryo Github](https://github.com/EsotericSoftware/kryo) + - [Hessian 官网](http://hessian.caucho.com/) + - [FST Github](https://github.com/RuedigerMoeller/fast-serialization) +- **文章** + - [java 序列化框架对比](https://www.jianshu.com/p/937883b6b2e5) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/01.IO/README.md" "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/README.md" new file mode 100644 index 00000000..1cd5ba95 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/README.md" @@ -0,0 +1,39 @@ +--- +title: Java 序列化工具 +date: 2022-02-17 22:34:30 +categories: + - Java + - 工具 + - IO +tags: + - Java + - IO + - 序列化 +permalink: /pages/08b504/ +hidden: true +index: false +--- + +# Java 序列化工具 + +Java 官方的序列化存在许多问题,因此,很多人更愿意使用优秀的第三方序列化工具来替代 Java 自身的序列化机制。 如果想详细了解 Java 自身序列化方式,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/2b2f0f/) + +序列化库技术选型: + +- [thrift](https://github.com/apache/thrift)、[protobuf](https://github.com/protocolbuffers/protobuf) - 适用于对性能敏感,对开发体验要求不高的内部系统。 +- [hessian](http://hessian.caucho.com/doc/hessian-overview.xtp) - 适用于对开发体验敏感,性能有要求的内外部系统。 +- [jackson](https://github.com/FasterXML/jackson)、[gson](https://github.com/google/gson)、[fastjson](https://github.com/alibaba/fastjson) - 适用于对序列化后的数据要求有良好的可读性(转为 json 、xml 形式)。 + +## 内容 + +- [JSON](01.JSON序列化.md) - Fastjson、Jackson、Gson +- [二进制](02.二进制序列化.md) - Protobuf、Thrift、Hessian、Kryo、FST + +## 资料 + +- [Thrift Github](https://github.com/apache/thrift) +- [Protobuf Github](https://github.com/protocolbuffers/protobuf) +- [Hessian 官网](http://hessian.caucho.com/doc/hessian-overview.xtp) +- [Fastjson Github](https://github.com/alibaba/fastjson) +- [Jackson Github](https://github.com/FasterXML/jackson) +- [Gson Github](https://github.com/google/gson) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" "b/docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" new file mode 100644 index 00000000..39b1df3a --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" @@ -0,0 +1,536 @@ +--- +title: Lombok 快速入门 +date: 2022-02-17 22:34:30 +order: 01 +categories: + - Java + - 工具 + - JavaBean +tags: + - Java + - JavaBean + - Lombok +permalink: /pages/eb1d46/ +--- + +# Lombok 快速入门 + +## Lombok 简介 + +Lombok 是一种 Java 实用工具,可用来帮助开发人员消除 Java 的冗长,尤其是对于简单的 Java 对象(POJO)。它通过注释实现这一目的。通过在开发环境中实现 Lombok,开发人员可以节省构建诸如 `hashCode()` 和 `equals()` 、`getter / setter` 这样的方法以及以往用来分类各种 accessor 和 mutator 的大量时间。 + +## Lombok 安装 + +由于 Lombok 仅在编译阶段生成代码,所以使用 Lombok 注解的源代码,在 IDE 中会被高亮显示错误,针对这个问题可以通过安装 IDE 对应的插件来解决。具体的安装方式可以参考:[Setting up Lombok with Eclipse and Intellij](https://www.baeldung.com/lombok-ide) + +使 IntelliJ IDEA 支持 Lombok 方式如下: + +- **Intellij 设置支持注解处理** + - 点击 File > Settings > Build > Annotation Processors + - 勾选 Enable annotation processing +- **安装插件** + - 点击 Settings > Plugins > Browse repositories + - 查找 Lombok Plugin 并进行安装 + - 重启 IntelliJ IDEA +- **将 lombok 添加到 pom 文件** + +```xml + + org.projectlombok + lombok + 1.16.8 + +``` + +## Lombok 使用 + +Lombok 提供注解 API 来修饰指定的类: + +### @Getter and @Setter + +[@Getter and @Setter](http://jnb.ociweb.com/jnb/jnbJan2010.html#gettersetter) Lombok 代码: + +```java +@Getter @Setter private boolean employed = true; +@Setter(AccessLevel.PROTECTED) private String name; +``` + +等价于 Java 源码: + +```java +private boolean employed = true; +private String name; + +public boolean isEmployed() { + return employed; +} + +public void setEmployed(final boolean employed) { + this.employed = employed; +} + +protected void setName(final String name) { + this.name = name; +} +``` + +### @NonNull + +[@NonNull](http://jnb.ociweb.com/jnb/jnbJan2010.html#nonnull) Lombok 代码: + +```java +@Getter @Setter @NonNull +private List members; +``` + +等价于 Java 源码: + +```java +@NonNull +private List members; + +public Family(@NonNull final List members) { + if (members == null) throw new java.lang.NullPointerException("members"); + this.members = members; +} + +@NonNull +public List getMembers() { + return members; +} + +public void setMembers(@NonNull final List members) { + if (members == null) throw new java.lang.NullPointerException("members"); + this.members = members; +} +``` + +### @ToString + +[@ToString](http://jnb.ociweb.com/jnb/jnbJan2010.html#tostring) Lombok 代码: + +```java +@ToString(callSuper=true,exclude="someExcludedField") +public class Foo extends Bar { + private boolean someBoolean = true; + private String someStringField; + private float someExcludedField; +} +``` + +等价于 Java 源码: + +```java +public class Foo extends Bar { + private boolean someBoolean = true; + private String someStringField; + private float someExcludedField; + + @java.lang.Override + public java.lang.String toString() { + return "Foo(super=" + super.toString() + + ", someBoolean=" + someBoolean + + ", someStringField=" + someStringField + ")"; + } +} +``` + +### @EqualsAndHashCode + +[@EqualsAndHashCode](http://jnb.ociweb.com/jnb/jnbJan2010.html#equals) Lombok 代码: + +```java +@EqualsAndHashCode(callSuper=true,exclude={"address","city","state","zip"}) +public class Person extends SentientBeing { + enum Gender { Male, Female } + + @NonNull private String name; + @NonNull private Gender gender; + + private String ssn; + private String address; + private String city; + private String state; + private String zip; +} +``` + +等价于 Java 源码: + +```java +public class Person extends SentientBeing { + + enum Gender { + /*public static final*/ Male /* = new Gender() */, + /*public static final*/ Female /* = new Gender() */; + } + @NonNull + private String name; + @NonNull + private Gender gender; + private String ssn; + private String address; + private String city; + private String state; + private String zip; + + @java.lang.Override + public boolean equals(final java.lang.Object o) { + if (o == this) return true; + if (o == null) return false; + if (o.getClass() != this.getClass()) return false; + if (!super.equals(o)) return false; + final Person other = (Person)o; + if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false; + if (this.gender == null ? other.gender != null : !this.gender.equals(other.gender)) return false; + if (this.ssn == null ? other.ssn != null : !this.ssn.equals(other.ssn)) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + final int PRIME = 31; + int result = 1; + result = result * PRIME + super.hashCode(); + result = result * PRIME + (this.name == null ? 0 : this.name.hashCode()); + result = result * PRIME + (this.gender == null ? 0 : this.gender.hashCode()); + result = result * PRIME + (this.ssn == null ? 0 : this.ssn.hashCode()); + return result; + } +} +``` + +### @Data + +[@Data](http://jnb.ociweb.com/jnb/jnbJan2010.html#data) Lombok 代码: + +```java +@Data(staticConstructor="of") +public class Company { + private final Person founder; + private String name; + private List employees; +} +``` + +等价于 Java 源码: + +```java +public class Company { + private final Person founder; + private String name; + private List employees; + + private Company(final Person founder) { + this.founder = founder; + } + + public static Company of(final Person founder) { + return new Company(founder); + } + + public Person getFounder() { + return founder; + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + + public List getEmployees() { + return employees; + } + + public void setEmployees(final List employees) { + this.employees = employees; + } + + @java.lang.Override + public boolean equals(final java.lang.Object o) { + if (o == this) return true; + if (o == null) return false; + if (o.getClass() != this.getClass()) return false; + final Company other = (Company)o; + if (this.founder == null ? other.founder != null : !this.founder.equals(other.founder)) return false; + if (this.name == null ? other.name != null : !this.name.equals(other.name)) return false; + if (this.employees == null ? other.employees != null : !this.employees.equals(other.employees)) return false; + return true; + } + + @java.lang.Override + public int hashCode() { + final int PRIME = 31; + int result = 1; + result = result * PRIME + (this.founder == null ? 0 : this.founder.hashCode()); + result = result * PRIME + (this.name == null ? 0 : this.name.hashCode()); + result = result * PRIME + (this.employees == null ? 0 : this.employees.hashCode()); + return result; + } + + @java.lang.Override + public java.lang.String toString() { + return "Company(founder=" + founder + ", name=" + name + ", employees=" + employees + ")"; + } +} +``` + +### @Cleanup + +[@Cleanup](http://jnb.ociweb.com/jnb/jnbJan2010.html#cleanup) Lombok 代码: + +```java +public void testCleanUp() { + try { + @Cleanup ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(new byte[] {'Y','e','s'}); + System.out.println(baos.toString()); + } catch (IOException e) { + e.printStackTrace(); + } +} +``` + +等价于 Java 源码: + +```java +public void testCleanUp() { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + baos.write(new byte[]{'Y', 'e', 's'}); + System.out.println(baos.toString()); + } finally { + baos.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } +} +``` + +### @Synchronized + +[@Synchronized](http://jnb.ociweb.com/jnb/jnbJan2010.html#synchronized) Lombok 代码: + +```java +private DateFormat format = new SimpleDateFormat("MM-dd-YYYY"); + +@Synchronized +public String synchronizedFormat(Date date) { + return format.format(date); +} +``` + +等价于 Java 源码: + +```java +private final java.lang.Object $lock = new java.lang.Object[0]; +private DateFormat format = new SimpleDateFormat("MM-dd-YYYY"); + +public String synchronizedFormat(Date date) { + synchronized ($lock) { + return format.format(date); + } +} +``` + +### @SneakyThrows + +[@SneakyThrows](http://jnb.ociweb.com/jnb/jnbJan2010.html#sneaky) Lombok 代码: + +```java +@SneakyThrows +public void testSneakyThrows() { + throw new IllegalAccessException(); +} +``` + +等价于 Java 源码: + +```java +public void testSneakyThrows() { + try { + throw new IllegalAccessException(); + } catch (java.lang.Throwable $ex) { + throw lombok.Lombok.sneakyThrow($ex); + } +} +``` + +### 示例源码 + +> 示例源码:[javalib-bean](https://github.com/dunwu/java-tutorial/tree/master/javalib-bean) + +## Lombok 使用注意点 + +### 谨慎使用 `@Builder` + +在类上标注了 `@Data` 和 `@Builder` 注解的时候,编译时,lombok 优化后的 Class 中会没有默认的构造方法。在反序列化的时候,没有默认构造方法就可能会报错。 + +【示例】使用 `@Builder` 不当导致 json 反序列化失败 + +```java +@Data +@Builder +public class BuilderDemo01 { + + private String name; + + public static void main(String[] args) throws JsonProcessingException { + BuilderDemo01 demo01 = BuilderDemo01.builder().name("demo01").build(); + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(demo01); + BuilderDemo01 expectDemo01 = mapper.readValue(json, BuilderDemo01.class); + System.out.println(expectDemo01.toString()); + } + +} +``` + +运行时会抛出异常: + +``` +Exception in thread "main" com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `io.github.dunwu.javatech.bean.lombok.BuilderDemo01` (although at least one Creator exists): cannot deserialize from Object value (no delegate- or property-based Creator) + at [Source: (String)"{"name":"demo01"}"; line: 1, column: 2] + at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:63) + at com.fasterxml.jackson.databind.DeserializationContext.reportInputMismatch(DeserializationContext.java:1432) + at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1062) + at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1297) + at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:326) + at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159) + at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4218) + at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3214) + at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3182) + at io.github.dunwu.javatech.bean.lombok.BuilderDemo01.main(BuilderDemo01.java:22) +``` + +【示例】使用 `@Builder` 正确方法 + +```java +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BuilderDemo02 { + + private String name; + + public static void main(String[] args) throws JsonProcessingException { + BuilderDemo02 demo02 = BuilderDemo02.builder().name("demo01").build(); + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(demo02); + BuilderDemo02 expectDemo02 = mapper.readValue(json, BuilderDemo02.class); + System.out.println(expectDemo02.toString()); + } + +} +``` + +### `@Data` 注解和继承 + +使用 `@Data` 注解时,则有了 `@EqualsAndHashCode` 注解,那么就会在此类中存在 `equals(Object other)` 和 `hashCode()` 方法,且不会使用父类的属性,这就导致了可能的问题。比如,有多个类有相同的部分属性,把它们定义到父类中,恰好 id(数据库主键)也在父类中,那么就会存在部分对象在比较时,它们并不相等,这是因为:lombok 自动生成的 `equals(Object other)` 和 `hashCode()` 方法判定为相等,从而导致和预期不符。 + +修复此问题的方法很简单: + +- 使用 `@Data` 时,加上 `@EqualsAndHashCode(callSuper=true)` 注解。 +- 使用 `@Getter @Setter @ToString` 代替 `@Data` 并且自定义 `equals(Object other)` 和 `hashCode()` 方法。 + +【示例】测试 `@Data` 和 `@EqualsAndHashCode` + +```java +@Data +@ToString(exclude = "age") +@EqualsAndHashCode(exclude = { "age", "sex" }) +public class Person { + + protected String name; + + protected Integer age; + + protected String sex; + +} + +@Data +@EqualsAndHashCode(callSuper = true, exclude = { "address", "city", "state", "zip" }) +public class EqualsAndHashCodeDemo extends Person { + + @NonNull + private String name; + + @NonNull + private Gender gender; + + private String ssn; + + private String address; + + private String city; + + private String state; + + private String zip; + + public EqualsAndHashCodeDemo(@NonNull String name, @NonNull Gender gender) { + this.name = name; + this.gender = gender; + } + + public EqualsAndHashCodeDemo(@NonNull String name, @NonNull Gender gender, + String ssn, String address, String city, String state, String zip) { + this.name = name; + this.gender = gender; + this.ssn = ssn; + this.address = address; + this.city = city; + this.state = state; + this.zip = zip; + } + + public enum Gender { + Male, + Female + } + +} + +@Test +@DisplayName("测试 @EqualsAndHashCode") +public void testEqualsAndHashCodeDemo() { + EqualsAndHashCodeDemo demo1 = + new EqualsAndHashCodeDemo("name1", EqualsAndHashCodeDemo.Gender.Female, "ssn", "xxx", "xxx", "xxx", "xxx"); + EqualsAndHashCodeDemo demo2 = + new EqualsAndHashCodeDemo("name1", EqualsAndHashCodeDemo.Gender.Female, "ssn", "ooo", "ooo", "ooo", "ooo"); + Assertions.assertEquals(demo1, demo2); + + Person person = new Person(); + person.setName("张三"); + person.setAge(20); + person.setSex("男"); + + Person person2 = new Person(); + person2.setName("张三"); + person2.setAge(18); + person2.setSex("男"); + + Person person3 = new Person(); + person3.setName("李四"); + person3.setAge(20); + person3.setSex("男"); + + Assertions.assertEquals(person2, person); + Assertions.assertNotEquals(person3, person); +} +``` + +上面的单元测试可以通过,但如果将 `@EqualsAndHashCode(callSuper = true, exclude = { "address", "city", "state", "zip" })` 注掉就会报错。 + +## 参考资料 + +- [Lombok 官网](https://projectlombok.org/) +- [Lombok Github](https://github.com/rzwitserloot/lombok) +- [IntelliJ IDEA - Lombok Plugin](http://plugins.jetbrains.com/plugin/6317-lombok-plugin) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" "b/docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" new file mode 100644 index 00000000..e9a7c230 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" @@ -0,0 +1,789 @@ +--- +title: Dozer 快速入门 +date: 2022-02-17 22:34:30 +order: 02 +categories: + - Java + - 工具 + - JavaBean +tags: + - Java + - JavaBean + - Dozer +permalink: /pages/45e21b/ +--- + +# Dozer 快速入门 + +这篇文章是本人在阅读 Dozer 官方文档(5.5.1 版本,官网已经一年多没更新了)的过程中,整理下来我认为比较基础的应用场景。 + +本文中提到的例子应该能覆盖 JavaBean 映射的大部分场景,希望对你有所帮助。 + +## 简介 + +**Dozer 是什么?** + +**Dozer 是一个 JavaBean 映射工具库。** + +它支持简单的属性映射,复杂类型映射,双向映射,隐式显式的映射,以及递归映射。 + +它支持三种映射方式:注解、API、XML。 + +它是开源的,遵从[Apache 2.0 协议](http://www.apache.org/licenses/LICENSE-2.0) + +## 安装 + +### 引入 jar 包 + +**maven 方式** + +如果你的项目使用 maven,添加以下依赖到你的 pom.xml 即可: + +```xml + + net.sf.dozer + dozer + 5.4.0 + +``` + +**非 maven 方式** + +如果你的项目不使用 maven,那就只能发扬不怕苦不怕累的精神了。 + +使用 Dozer 需要引入 Dozer 的 jar 包以及其依赖的第三方 jar 包。 + +- [Dozer](http://sourceforge.net/project/showfiles.php?group_id=133517) +- [Dozer 依赖的第三方 jar 包](http://dozer.sourceforge.net/dependencies.html) + +### Eclipse 插件 + +Dozer 有插件可以在 Eclipse 中使用(不知道是否好用,反正我没用过) + +插件地址: + +## 使用 + +将 Dozer 引入到工程中后,我们就可以来小试一番了。 + +实践出真知,先以一个最简单的例子来展示 Dozer 映射的处理过程。 + +### 准备 + +我们先准备两个要互相映射的类 + +NotSameAttributeA.java + +```java +public class NotSameAttributeA { + private long id; + private String name; + private Date date; + + // 省略getter/setter +} +``` + +NotSameAttributeB.java + +```java +public class NotSameAttributeB { + private long id; + private String value; + private Date date; + + // 省略getter/setter +} +``` + +这两个类存在属性名不完全相同的情况:name 和 value。 + +### Dozer 的配置 + +#### 为什么要有映射配置? + +如果要映射的两个对象有完全相同的属性名,那么一切都很简单。 + +只需要直接使用 Dozer 的 API 即可: + +```java +Mapper mapper = new DozerBeanMapper(); +DestinationObject destObject = + mapper.map(sourceObject, DestinationObject.class); +``` + +但实际映射时,往往存在属性名不同的情况。 + +所以,你需要一些配置来告诉 Dozer 应该转换什么,怎么转换。 + +**_注:官网着重建议:在现实应用中,最好不要每次映射对象时都创建一个`Mapper`实例来工作,这样会产生不必要的开销。如果你不使用 IoC 容器(如:spring)来管理你的项目,那么,最好将`Mapper`定义为单例模式。_** + +#### 映射配置文件 + +在`src/test/resources`目录下添加`dozer/dozer-mapping.xml`文件。 +``标签中允许你定义``和``,对应着相互映射的类。 +``标签里定义要映射的特殊属性。需要注意``和``对应,``和``对应,聪明的你,猜也猜出来了吧。 + +```xml + + + + org.zp.notes.spring.common.dozer.vo.NotSameAttributeA + org.zp.notes.spring.common.dozer.vo.NotSameAttributeB + + name + value + + + +``` + +### 与 Spring 整合 + +#### 配置 DozerBeanMapperFactoryBean + +在`src/test/resources`目录下添加`spring/spring-dozer.xml`文件。 + +Dozer 与 Spring 的整合很便利,你只需要声明一个`DozerBeanMapperFactoryBean`, +将所有的 dozer 映射配置文件作为属性注入到`mappingFiles`, +`DozerBeanMapperFactoryBean`会加载这些规则。 + +spring-dozer.xml 文件范例 + +```xml + + + + + + + classpath*:dozer/dozer-mapping.xml + + + + +``` + +#### 自动装配 + +至此,万事具备,你只需要自动装配`mapper`。 + +```java +RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = {"classpath:spring/spring-dozer.xml"}) +@TransactionConfiguration(defaultRollback = false) +public class DozerTest extends TestCase { + @Autowired + Mapper mapper; + + @Test + public void testNotSameAttributeMapping() { + NotSameAttributeA src = new NotSameAttributeA(); + src.setId(007); + src.setName("邦德"); + src.setDate(new Date()); + + NotSameAttributeB desc = mapper.map(src, NotSameAttributeB.class); + Assert.assertNotNull(desc); + } +} +``` + +运行一下单元测试,绿灯通过。 + +## Dozer 支持的数据类型转换 + +Dozer 可以自动做数据类型转换。当前,Dozer 支持以下数据类型转换(都是双向的) + +- **Primitive to Primitive Wrapper** + + 原型(int、long 等)和原型包装类(Integer、Long) + +- **Primitive to Custom Wrapper** + + 原型和定制的包装 + +- **Primitive Wrapper to Primitive Wrapper** + + 原型包装类和包装类 + +- **Primitive to Primitive** + + 原型和原型 + +- **Complex Type to Complex Type** + + 复杂类型和复杂类型 + +- **String to Primitive** + + 字符串和原型 + +- **String to Primitive Wrapper** + + 字符串和原型包装类 + +- **String to Complex Type if the Complex Type contains a String constructor** + + 字符串和有字符串构造器的复杂类型(类) + +- **String to Map** + + 字符串和 Map + +- **Collection to Collection** + + 集合和集合 + +- **Collection to Array** + + 集合和数组 + +- **Map to Complex Type** + + Map 和复杂类型 + +- **Map to Custom Map Type** + + Map 和定制 Map 类型 + +- **Enum to Enum** + + 枚举和枚举 + +- **Each of these can be mapped to one another: java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp, java.util.Calendar, java.util.GregorianCalendar** + + 这些时间相关的常见类可以互换:java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp, java.util.Calendar, java.util.GregorianCalendar + +- **String to any of the supported Date/Calendar Objects.** + + 字符串和支持 Date/Calendar 的对象 + +- **Objects containing a toString() method that produces a long representing time in (ms) to any supported Date/Calendar object.** + + 如果一个对象的 toString()方法返回的是一个代表 long 型的时间数值(单位:ms),就可以和任何支持 Date/Calendar 的对象转换。 + +## Dozer 的映射配置 + +在前面的简单例子中,我们体验了一把 Dozer 的映射流程。但是两个类进行映射,有很多复杂的情况,相应的,你也需要一些更复杂的配置。 + +Dozer 有三种映射配置方式: + +- **注解方式** +- **API 方式** +- **XML 方式** + +### 用注解来配置映射 + +**Dozer 5.3.2**版本开始支持注解方式配置映射(只有一个注解:`@Mapping`)。可以应对一些简单的映射处理,复杂的就玩不转了。 + +看一下`@Mapping`的声明就可以知道,这个注解只能用于元素和方法。 + +```java +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface Mapping { + String value() default ""; +} +``` + +让我们来试试吧: + +TargetBean.java + +```java +public class SourceBean { + + private Long id; + + private String name; + + @Mapping("binaryData") + private String data; + + @Mapping("pk") + public Long getId() { + return this.id; + } + + //其余getter/setter方法略 +} +``` + +TargetBean.java + +```java +public class TargetBean { + + private String pk; + + private String name; + + private String binaryData; + + //getter/setter方法略 +} +``` + +定义了两个相互映射的 Java 类,只需要在源类中用`@Mapping`标记和目标类中对应的属性就可以了。 + +```java +@Test +public void testAnnotationMapping() { + SourceBean src = new SourceBean(); + src.setId(7L); + src.setName("邦德"); + src.setData("00000111"); + + TargetBean desc = mapper.map(src, TargetBean.class); + Assert.assertNotNull(desc); +} +``` + +测试一下,绿灯通过。 + +官方文档说,虽然当前版本(文档的版本对应 Dozer 5.5.1)仅支持`@Mapping`,但是在未来的发布版本会提供其他的注解功能,那就敬请期待吧(再次吐槽一下:一年多没更新了)。 + +### 用 API 来配置映射 + +个人觉得这种方式比较麻烦,不推荐,也不想多做介绍,就是这么任性。 + +### 用 XML 来配置映射 + +需要**强调**的是:如果两个类的所有属性都能很好的互转,可以你中有我,我中有你,不分彼此,那么就不要画蛇添足的在 xml 中去声明映射规则了。 + +#### 属性名不同时的映射(Basic Property Mapping) + +**Dozer 会自动映射属性名相同的属性,所以不必添加在 xml 文件中。** + +```xml + + one + onePrime + +``` + +#### 字符串和日期映射(String to Date Mapping) + +字符串在和日期进行映射时,允许用户指定日期的格式。 + +格式的设置分为三个作用域级别: + +**属性级别** + +对当前属性有效(这个属性必须是日期字符串) + +```xml + + dateString + dateObject + +``` + +**类级别** + +对这个类中的所有日期相关的属性有效 + +```xml + + org.dozer.vo.TestObject + org.dozer.vo.TestObjectPrime + + dateString + dateObject + + +``` + +**全局级别** + +对整个文件中的所有日期相关的属性有效。 + +```xml + + + MM/dd/yyyy HH:mm + + + + org.dozer.vo.TestObject + org.dozer.vo.TestObjectPrime + + dateString + dateObject + + + +``` + +#### 集合和数组映射(Collection and Array Mapping) + +Dozer 可以自动处理以下类型的双向转换。 + +- List to List +- List to Array +- Array to Array +- Set to Set +- Set to Array +- Set to List + +**使用 hint** + +如果使用泛型或数组,没有必要使用 hint。 + +如果不使用泛型或数组。在处理集合或数组之间的转换时,你需要用`hint`指定目标列表的数据类型。 + +若你不指定`hint`,Dozer 将认为目标集合和源集合的类型是一致的。 + +使用 Hints 的范例: + +```xml + + hintList + hintList + org.dozer.vo.TheFirstSubClassPrime + +``` + +**累计映射和非累计映射(Cumulative vs. Non-Cumulative List Mapping)** + +如果你要转换的目标类已经初始化,你可以选择让 Dozer 添加或更新对象到你的集合中。 + +而这取决于`relationship-type`配置,默认是累计。 + +它的设置有作用域级别: + +- 全局级 + +``` + + + non-cumulative + + +``` + +- 类级别 + +```xml + + + + + +``` + +- 属性级别 + +```xml + + hintList + hintList + org.dozer.vo.TheFirstSubClass + org.dozer.vo.TheFirstSubClassPrime + +``` + +**移动孤儿(Removing Orphans)** + +这里的孤儿是指目标集合中存在,但是源集合中不存在的元素。 + +你可以使用`remove-orphans`开关来选择是否移除这样的元素。 + +```xml + + srcList + destList + +``` + +#### 深度映射(Deep Mapping) + +所谓深度映射,是指允许你指定属性的属性(比如一个类的属性本身也是一个类)。举例来说 + +Source.java + +```java +public class Source { + private long id; + private String info; +} +``` + +Dest.java + +```java +public class Dest { + private long id; + private Info info; +} +``` + +```java +public class Info { + private String content; +} +``` + +映射规则 + +```xml + + org.zp.notes.spring.common.dozer.vo.Source + org.zp.notes.spring.common.dozer.vo.Dest + + info + info.content + + +``` + +#### 排除属性(Excluding Fields) + +就像任何团体都有捣乱分子,类之间转换时也有想要排除的因子。 + +如何在做类型转换时,自动排除一些属性,Dozer 提供了几种方法,这里只介绍一种比较通用的方法。 + +更多详情参考[官网](http://dozer.sourceforge.net/documentation/exclude.html)。 + +field-exclude 可以排除不需要映射的属性。 + +```xml + + fieldToExclude + fieldToExclude + +``` + +#### 单向映射(One-Way Mapping) + +**_注:本文的映射方式,无特殊说明,都是双向映射的。_** + +有的场景可能希望转换过程不可逆,即单向转换。 + +单向转换可以通过使用`one-way`来开启 + +类级别 + +```xml + + org.dozer.vo.TestObjectFoo + org.dozer.vo.TestObjectFooPrime + + oneFoo + oneFooPrime + + +``` + +属性级别 + +```xml + + org.dozer.vo.TestObjectFoo2 + org.dozer.vo.TestObjectFooPrime2 + + oneFoo2 + oneFooPrime2 + + + + oneFoo3.prime + oneFooPrime3 + +``` + +#### 全局配置(Global Configuration) + +全局配置用来设置全局的配置信息。此外,任何定制转换都是在这里定义的。 + +全局配置都是可选的。 + +- ``表示日期格式 +- ``错误处理开关 +- ``通配符 +- ``裁剪字符串开关 + +```xml + + + MM/dd/yyyy HH:mm + true + true + false + + + + org.dozer.vo.TestCustomConverterObject + another.type.to.Associate + + + + +``` + +全局配置的作用是帮助你少配置一些参数,如果个别类的映射规则需要变更,你可以 mapping 中覆盖它。 + +覆盖的范例如下 + +```xml + + + + + + + + + + + + + + + +``` + +#### 定制转换(Custom Converters) + +如果 Dozer 默认的转换规则不能满足实际需要,你可以选择定制转换。 + +定制转换通过配置 XML 来告诉 Dozer 如何去转换两个指定的类。当 Dozer 转换这两个指定类的时候,会调用你的映射规则去替换标准映射规则。 + +为了让 Dozer 识别,你必须实现`org.dozer.CustomConverter`接口。否则,Dozer 会抛异常。 + +具体做法: + +(1) 创建一个类实现`org.dozer.CustomConverter`接口。 + +```java +public class TestCustomConverter implements CustomConverter { + + public Object convert(Object destination, Object source, + Class destClass, Class sourceClass) { + if (source == null) { + return null; + } + CustomDoubleObject dest = null; + if (source instanceof Double) { + // check to see if the object already exists + if (destination == null) { + dest = new CustomDoubleObject(); + } else { + dest = (CustomDoubleObject) destination; + } + dest.setTheDouble(((Double) source).doubleValue()); + return dest; + } else if (source instanceof CustomDoubleObject) { + double sourceObj = + ((CustomDoubleObject) source).getTheDouble(); + return new Double(sourceObj); + } else { + throw new MappingException("Converter TestCustomConverter " + + "used incorrectly. Arguments passed in were:" + + destination + " and " + source); + } + } +``` + +(2) 在 xml 中引用定制的映射规则 + +引用定制的映射规则也是分级的,你可以酌情使用。 + +- 全局级 + +```xml + + + + + + + org.dozer.vo.CustomDoubleObject + java.lang.Double + + + + + org.dozer.vo.TestCustomConverterHashMapObject + org.dozer.vo.TestCustomConverterHashMapPrimeObject + + + + +``` + +- 属性级 + +```xml + + org.dozer.vo.SimpleObj + org.dozer.vo.SimpleObjPrime2 + + field1 + field1Prime + + +``` + +#### 映射的继承(Inheritance Mapping) + +Dozer 支持映射规则的继承机制。 + +属性如果有着相同的名字则不需要在 xml 中配置,除非使用了`hint` + +我们来看一个例子 + +```xml + + org.dozer.vo.SuperClass + org.dozer.vo.SuperClassPrime + + + superAttribute + superAttr + + + + + org.dozer.vo.SubClass + org.dozer.vo.SubClassPrime + + + attribute + attributePrime + + + + + org.dozer.vo.SubClass2 + org.dozer.vo.SubClassPrime2 + + + attribute2 + attributePrime2 + + +``` + +在上面的例子中 SubClass、SubClass2 是 SuperClass 的子类; + +SubClassPrime 和 SubClassPrime2 是 SuperClassPrime 的子类。 + +superAttribute 和 superAttr 的映射规则会被子类所继承,所以不必再重复的在子类中去声明。 + +## 参考 + +[Dozer 官方文档](http://dozer.sourceforge.net/documentation/gettingstarted.html) | [Dozer 源码地址](https://github.com/DozerMapper/dozer) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" new file mode 100644 index 00000000..8de94dbc --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" @@ -0,0 +1,175 @@ +--- +title: Freemark 快速入门 +date: 2022-02-17 22:34:30 +order: 01 +categories: + - Java + - 工具 + - 模板引擎 +tags: + - Java + - 模板引擎 + - Freemark +permalink: /pages/a60ccf/ +--- + +# Freemark 快速入门 + +> FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML 网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个 Java 类库,是一款程序员可以嵌入他们所开发产品的组件。 + +## Freemark 简介 + +Freemark 模板编写为 FreeMarker Template Language (FTL)。它是简单的,专用的语言, _不是_ 像 PHP 那样成熟的编程语言。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。 + +![img](http://freemarker.foofun.cn/figures/overview.png) + +这种方式通常被称为 [MVC (模型 视图 控制器) 模式](http://freemarker.foofun.cn/gloss.html#gloss.MVC),对于动态网页来说,是一种特别流行的模式。 它帮助从开发人员(Java 程序员)中分离出网页设计师(HTML 设计师)。设计师无需面对模板中的复杂逻辑, 在没有程序员来修改或重新编译代码时,也可以修改页面的样式。 + +Freemark 模板一句话概括就是:**_`模板 + 数据模型 = 输出`_** + +## 总体结构 + +- **文本**:文本会照着原样来输出。 +- **插值**:这部分的输出会被计算的值来替换。插值由 `${` and `}` 所分隔(或者 `#{` and `}`,这种风格已经不建议再使用了;[点击查看更多](http://freemarker.foofun.cn/ref_depr_numerical_interpolation.html))。 +- **FTL 标签**:FTL 标签和 HTML 标签很相似,但是它们却是给 FreeMarker 的指示, 而且不会打印在输出内容中。 +- **注释**:注释和 HTML 的注释也很相似,但它们是由 `<#--` 和 `-->`来分隔的。注释会被 FreeMarker 直接忽略, 更不会在输出内容中显示。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/ftl-template.png) + +> 🔔 注意: +> +> - FTL 是区分大小写的。 +> - `插值` 仅仅可以在 `文本` 中使用。 +> - `FTL 标签` 不可以在其他 `FTL 标签` 和 `插值` 中使用。 +> - `注释` 可以放在 `FTL 标签` 和 `插值` 中。 + +### 指令 + +FTL 指令有两种类型: [预定义指令](http://freemarker.foofun.cn/gloss.html#gloss.predefinedDirective) 和 [用户自定义指令](http://freemarker.foofun.cn/gloss.html#gloss.userDefinedDirective)。 对于用户自定义的指令使用 `@` 来代替 `#`。 + +> 🔔 注意: +> +> - FreeMarker 仅仅关心 FTL 标签的嵌套而不关心 HTML 标签的嵌套。 它只会把 HTML 看做是文本,不会来解释 HTML。 +> - 如果你尝试使用一个不存在的指令(比如,输错了指令的名称), FreeMarker 就会拒绝执行模板,同时抛出错误信息。 +> - FreeMarker 会忽略 FTL 标签中多余的 [空白标记](http://freemarker.foofun.cn/gloss.html#gloss.whiteSpace)。 + +### 表达式 + +以下为快速浏览清单,如果需要了解更多细节,请参考[**这里**](http://freemarker.foofun.cn/dgui_template_exp.html)。 + +- [直接指定值](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_direct) + - [字符串](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_direct_string): `"Foo"` 或者 `'Foo'` 或者 `"It's \"quoted\""` 或者 `'It\'s "quoted"'` 或者 `r"C:\raw\string"` + - [数字](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_direct_number): `123.45` + - [布尔值](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_direct_boolean): `true`, `false` + - [序列](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_direct_seuqence): `["foo", "bar", 123.45]`; 值域: `0..9`, `0..<10` (或 `0..!10`), `0..` + - [哈希表](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_direct_hash): `{"name":"green mouse", "price":150}` +- [检索变量](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_var) + - [顶层变量](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_var_toplevel): `user` + - [从哈希表中检索数据](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_var_hash): `user.name`, `user["name"]` + - [从序列中检索数据](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_var_sequence): `products[5]` + - [特殊变量](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_var_special): `.main` +- [字符串操作](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_stringop) + - [插值(或连接)](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_stringop_interpolation): `"Hello ${user}!"` (或 `"Hello " + user + "!"`) + - [获取一个字符](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_get_character): `name[0]` + - [字符串切分:](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_stringop_slice) 包含结尾: `name[0..4]`,不包含结尾: `name[0..<5]`,基于长度(宽容处理): `name[0..*5]`,去除开头:`name[5..]` +- [序列操作](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_sequenceop) + - [连接](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_sequenceop_cat): `users + ["guest"]` + - [序列切分](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_seqenceop_slice):包含结尾: `products[20..29]`, 不包含结尾: `products[20..<30]`,基于长度(宽容处理):`products[20..*10]`,去除开头: `products[20..]` +- [哈希表操作](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_hashop) + - [连接](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_hashop_cat): `passwords + { "joe": "secret42" }` +- [算术运算](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_arit): `(x * 1.5 + 10) / 2 - y % 100` +- [比较运算](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_comparison): `x == y`, `x != y`, `x < y`, `x > y`, `x >= y`, `x <= y`, `x lt y`, `x lte y`, `x gt y`, `x gte y`, 等等。。。。。。 +- [逻辑操作](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_logicalop): `!registered && (firstVisit || fromEurope)` +- [内建函数](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_builtin): `name?upper_case`, `path?ensure_starts_with('/')` +- [方法调用](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_methodcall): `repeat("What", 3)` +- [处理不存在的值](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_missing) + - [默认值](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_missing_default): `name!"unknown"` 或者 `(user.name)!"unknown"` 或者 `name!` 或者 `(user.name)!` + - [检测不存在的值](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_missing_test): `name??` 或者 `(user.name)??` +- [赋值操作](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_assignment): `=`, `+=`, `-=`, `*=`, `/=`, `%=`, `++`, `--` + +### 变量 + +注意:变量 _仅仅_ 在 [文本区](http://freemarker.foofun.cn/dgui_template_overallstructure.html) (比如 `

Hello ${name}!

`) 和 [字符串](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_direct_string) 中起作用。 + +正确示例: + +``` +<#include "/footer/${company}.html"> +<#if big>... +``` + +错误示例: + +``` +<#if ${big}>... +<#if "${big}">... +``` + +## 数据类型 + +Freemark 支持的类型有: + +### 标量 + +字符串 + +``` +${"Hello ${user}"} +${"I can escape with \\ ${user}"} +${r"Now I can read dollar signs $"} +``` + +输出: + +``` +Hello deister +I can escape with \ deister +Now I can read dollar signs $ +``` + +数字 + +布尔值 + +日期/时间 (日期,时间或日期时间) + +### 容器 + +- 哈希表 +- 序列 +- 集合 + +### 子程序 + +- [方法和函数](http://freemarker.foofun.cn/dgui_datamodel_types.html#dgui_datamodel_method) +- [用户自定义指令](http://freemarker.foofun.cn/dgui_datamodel_types.html#dgui_datamodel_userdefdir) + +### 其它 + +- [结点](http://freemarker.foofun.cn/dgui_datamodel_types.html#dgui_datamodel_node) + +## 转义符 + +FTL 支持的所有转义字符: + +| 转义序列 | 含义 | +| :------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | +| `\"` | 引号 (u0022) | +| `\'` | 单引号(又称为撇号) (u0027) | +| `\{` | 起始花括号:`{` | +| `\\` | 反斜杠 (u005C) | +| `\n` | 换行符 (u000A) | +| `\r` | 回车 (u000D) | +| `\t` | 水平制表符(又称为 tab) (u0009) | +| `\b` | 退格 (u0008) | +| `\f` | 换页 (u000C) | +| `\l` | 小于号:`<` | +| `\g` | 大于号:`>` | +| `\a` | &符:`&` | +| `\xCode` | 字符的 16 进制 [Unicode](http://freemarker.foofun.cn/gloss.html#gloss.unicode) 码 ([UCS](http://freemarker.foofun.cn/gloss.html#gloss.UCS) 码) | + +## 参考资料 + +- [Freemark Github](https://github.com/apache/freemarker) +- [Freemark 中文教程](http://freemarker.foofun.cn/) +- [在线 Freemark 工具](https://try.freemarker.apache.org/) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/02.Thymeleaf.md" "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/02.Thymeleaf.md" new file mode 100644 index 00000000..df2e8fc4 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/02.Thymeleaf.md" @@ -0,0 +1,483 @@ +--- +title: Thymeleaf 快速入门 +date: 2022-02-17 22:34:30 +order: 02 +categories: + - Java + - 工具 + - 模板引擎 +tags: + - Java + - 模板引擎 + - Thymeleaf +permalink: /pages/e7d2ad/ +--- + +# Thymeleaf 快速入门 + +## 标准方言 + +标准方言是指 Thymeleaf 定义了一组功能,这些功能应该足以满足大多数情况。可以识别这些标准方言在模板中的使用,因为它将包含以`th`前缀开头的属性,如``。 + +### 表达式 + +`${...}` : 变量表达式。 + +`*{...}` : 选择表达式。 + +`#{...}` : 消息 (i18n) 表达式。 + +`@{...}` : 链接 (URL) 表达式。 + +`~{...}` : 片段表达式。 + +#### 变量表达式 + +变量表达式是 OGNL 表达式 - 如果将 Thymeleaf 与 Spring - 集成在上下文变量上(也称为 Spring 术语中的模型属性),则为 Spring EL。 它们看起来像这样: + +```html +${session.user.name} +``` + +它们作为属性值或作为它们的一部分,取决于属性: + +```html + +``` + +上面的表达式与下面是相同的(在 OGNL 和 SpringEL 中): + +```java +((Book)context.getVariable("book")).getAuthor().getName() +``` + +但是不仅在涉及输出的场景中找到变量表达式,而且还可以使用更复杂的处理方式,如:条件,迭代…等等。 + +```html +
  • +``` + +这里`${books}`从上下文中选择名为`books`的变量,并在`th:each`中使用循环将其评估为迭代器。 + +#### 选择表达式 + +选择表达式就像变量表达式一样,它们不是整个上下文变量映射上执行,而是在先前选择的对象。 它们看起来像这样: + +```html +*{customer.name} +``` + +它们所作用的对象由`th:object`属性指定: + +```html +
    + ... + ... + ... +
    +``` + +所以这相当于: + +```java +{ + // th:object="${book}" + final Book selection = (Book) context.getVariable("book"); + // th:text="*{title}" + output(selection.getTitle()); +} +``` + +#### 消息(i18n)表达式 + +消息表达式(通常称为文本外部化,国际化或 i18n)允许从外部源(如:`.properties`)文件中检索特定于语言环境的消息,通过键来引用这引用消息。 + +在 Spring 应用程序中,它将自动与 Spring 的 MessageSource 机制集成。如下 - + +``` +#{main.title} +#{message.entrycreated(${entryId})} +``` + +以下是在模板中使用它们的方式: + +```html + + ... + + + ... +
    ......
    +``` + +请注意,如果希望消息键由上下文变量的值确定,或者希望将变量指定为参数,则可以在消息表达式中使用变量表达式: + +```html +#{${config.adminWelcomeKey}(${session.user.name})} Jsp +``` + +#### 链接(URL)表达式 + +链接表达式在构建 URL 并向其添加有用的上下文和会话信息(通常称为 URL 重写的过程)。 +因此,对于部署在 Web 服务器的`/myapp`上下文中的 Web 应用程序,可以使用以下表达式: + +```html +... +``` + +可以转换成如下的东西: + +```html +... +``` + +甚至,如果需要保持会话,并且 cookie 未启用(或者服务器还不知道),那么生成的格式为: + +```html +... HTML +``` + +网址也可以带参数,如下所示: + +```html +... +``` + +这将产生类似以下的结果 - + +```html + +... +``` + +链接表达式可以是相对的,在这种情况下,应用程序上下文将不会被加到 URL 的前面: + +```html +... +``` + +也是服务器相对的(同样,没有应用程序上下文的前缀): + +```html +... +``` + +和协议相关(就像绝对 URL 一样,但浏览器将使用与正在显示的页面相同的 HTTP 或 HTTPS 协议): + +```html +... +``` + +当然,链接表达式也可以是绝对的: + +```html +... +``` + +但是绝对(或协议相对)URL ,在 Thymeleaf 链接表达式中应该添加什么值? 很简单:由响应过滤器定义 URL 重写:在基于 Servlet 的 Web 应用程序中,对于每个输出的 URL(上下文相对,相对,绝对…),在显示 URL 之前,Thymeleaf 总是调用`HttpServletResponse.encodeUrl(...)`机制。 这意味着一个过滤器可以通过包装 HttpServletResponse 对象来为应用程序执行自定义的 URL 重写。 + +#### 片段表达式 + +片段表达式是一种简单的方法用来表示标记的片段并将其移动到模板中。 由于这些表达式,片段可以被复制,传递给其他模板的参数等等。 + +最常见的是使用`th:insert`或`th:replace`来插入片段: + +```html +
    ...
    +``` + +但是它们可以在任何地方使用,就像任何其他变量一样: + +```html +
    +

    +
    +``` + +片段表达式可以有参数。 + +#### 表达式预处理 + +关于表达式的最后一件事是知道表达式预处理,在`__`之间指定,如下所示: + +``` +#{selection.__${sel.code}__} +``` + +上面代码中,第一个被执行的变量表达式是:`${sel.code}`,并且将使用它的结果作为表达式的一部分(假设`${sel.code}`的结果为:`ALL`),在此处执行国际化的情况下(这将查找与关键`selection.ALL`消息)。 + +### 文字和操作 + +有很多类型的文字和操作可用,它们分别如下: + +- 文字 + - 文本文字,例如:`'one text'`, `'Another one!'`,`…` + - 数字文字,例如:`0`,`10`, `314`, `31.01`, `112.83`,`…` + - 布尔文字,例如:`true`,`false` + - Null 文字,例如:`Null` + - 文字标记,例如:`one`, `sometext`, `main`,`…` +- 文本操作: + - 字符串连接:`+` + - 文字替换:`|The name is ${name}|` +- 算术运算: + - 二进制操作:`+`, `-`, `*`, `/`, `%` + - 减号(一元运算符):`-` +- 布尔运算: + - 二进制运算符,`and`,`or` + - 布尔否定(一元运算符):`!`,`not` +- 比较和相等: + - 比较运算符:`>`,`<`,`>=`,`<=`(`gt`,`lt`,`ge`,`le`) + - 相等运算符:`==`, `!=` (`eq`, `ne`) +- 条件操作符: + - If-then:`(if) ? (then)` + - If-then-else:`(if) ? (then) : (else)` + - Default: `(value) ?: (defaultvalue)` + +### 基本属性 + +下面来看看标准方言中的几个最基本的属性。 从`th:`文本开始,它代替了标签的主体: + +```html +

    Welcome everyone!

    +``` + +现在,`th:each`重复它所在元素的次数,由它的表达式返回的数组或列表所指定的次数,为迭代元素创建一个内部变量,其语法与 Java 的 foreach 表达式相同: + +```html +
  • + En las Orillas del Sar +
  • +``` + +最后,Thymeleaf 为特定的 XHTML 和 HTML5 属性提供了许多`th`属性,这些属性只评估它们的表达式,并将这些属性的值设置为结果。 + +```html +
    + + +
    +``` + +### 标准 URL + +Thymeleaf 标准方言(称为 Standard 和 SpringStandard)提供了一种在 Web 应用程序中轻松创建 URL 的方法,以便它们包含任何所需的 URL 工件。 这是通过连接表达方式来完成的,这是一种类似于 Thymeleaf 标准的表现:`@{...}` + +#### 绝对网址 + +绝对 URL 用于创建到其他服务器的链接。它们需要指定一个协议名称(`http://`或`https://`)开头。 + +```html + +``` + +上面链接不会被修改,除非在服务器上配置了 URL 重写过滤器,并在`HttpServletResponse.encodeUrl(...)`方法中执行修改。最后生成的 HTML 代码如下: + +```html + +``` + +#### 上下文相关 URL + +最常用的 URL 类型是上下文相关的。 这些 URL 是一旦安装在服务器上,就会与 Web 应用程序根相关联 URL。 例如,如果将一个名称为`myapp.war`的文件部署到一个 Tomcat 服务器中,那么应用程序一般是通过 URL:`http://localhost:8080/myapp`来访问,`myapp`就是上下文名称。 + +与上下文相关的 URL 以`/`字符开头: + +```html + +``` + +如果应用程序访问 URL 为:`http://localhost:8080/myapp`,则此 URL 将输出: + +```html + +``` + +#### 与服务器相关 URL + +服务器相关的 URL 与上下文相关的 URL 非常相似,只是它们不假定 URL 要链接到应用程序上下文中的资源,因此允许链接到同一服务器中的不同上下文: + +```html + +``` + +当前应用程序的上下文将被忽略,因此尽管应用程序部署在`http:// localhost:8080 / myapp`,但该 URL 将输出: + +```html + +``` + +#### 协议相关 URL + +与协议相关的 URL 实际上是绝对的 URL,它将保持用于显示当前页面的协议(HTTP,HTTPS)。 它们通常用于包括样式,脚本等外部资源: + +```html + +``` + +它将呈现与上面一致的 URL(URL 重写除外),如: + +```html + +``` + +#### 添加参数 + +如何向使用`@{...}`表达式创建的 URL 添加参数? 这也很简单: + +```html + +``` + +上面示例代码,最终将输出为: + +```html + +``` + +也可以添加几个参数,用逗号分隔它们: + +```html + +``` + +上面代码将输出结果为: + +```html + + +``` + +还可以使用正常参数的路径变量的形式包含参数,但在 URL 的路径中指定一个占位符: + +```html + +``` + +上面输出结果为: + +```html + +``` + +#### 网址片段标识符 + +片段标识符可以包含在 URL 中,包含参数和不包含参数。 它们将始终包含在网址的基础上,参考以下代码: + +```html + +``` + +执行输出结果如下 - + +```shell + +``` + +#### URL 重写 + +Thymeleaf 允许在应用程序中配置 URL 重写过滤器,它通过调用 Thymeleaf 模板生成的每个 URL 的 Servlet API 的`javax.servlet.http.HttpServletResponse`类中的`response.encodeURL()`方法来实现。 + +下面在 Java Web 应用程序中支持 URL 重写操作的标准方式,并允许 URL: + +- 自动检测用户是否启用了 Cookie,如果未启用或者如果它是第一个请求并且 cookie 配置仍未知。则将`;jsessionid=...`片段添加到 URL。 +- 在需要时自动将代理配置应用于 URL。 +- 使用不同的 CDN 设置,以便链接到分布在多个服务器中的内容。 + +#### URL 其它属性 + +不要以为在`@{...}`表达式中只有`th:href`属性来表示 URL 。 事实上,它们可以像变量表达式(`${...}`)或消息外部化/国际化(`#{...}`)一样用于任何地方。 + +例如,表单提交时,可使用以下写法 - + +```html +
    +``` + +或作为其他表达的一部分。 如下作为外部化/国际化字符串的参数: + +```html +

    +``` + +#### 在 URL 中使用表达式 + +下面来看看,如下所示的 URL 表达式: + +```html +
    +``` + +但`3`和`'show_all'`都不能是文字值,因为只有在运行时才能知道它们的值,怎么办? + +```html + +``` + +下面看看另一个 URL 表达式,如下所示: + +```html + +``` + +它其实是下面 URL 的一个快捷方式: + +```html + +``` + +这意味着 URL 基本身可以被指定为一个表达式,例如一个变量表达式: + +```html + +``` + +或外部化/国际化的文本: + +```html + +``` + +甚至可以使用复杂的表达式,包括条件表达式,例如: + +```html + +``` + +如果要更清洁,那么可以使用`th:with` : + +```html + +``` + +又或者 - + +```html +
    + ... + ... + ... +
    +``` + +## 扩展 + +TODO + +## 参考资料 + +- [Thymeleaf 官网](https://www.thymeleaf.org/) +- [Thymeleaf Github](https://github.com/thymeleaf/thymeleaf/) +- [Thymeleaf 教程](https://fanlychie.github.io/post/thymeleaf.html) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/03.Velocity.md" "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/03.Velocity.md" new file mode 100644 index 00000000..ccb6514f --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/03.Velocity.md" @@ -0,0 +1,331 @@ +--- +title: Velocity 快速入门 +date: 2022-02-17 22:34:30 +order: 03 +categories: + - Java + - 工具 + - 模板引擎 +tags: + - Java + - 模板引擎 + - Velocity +permalink: /pages/3ba0ff/ +--- + +# Velocity 快速入门 + +**Velocity (简称 VTL)是一个基于 Java 的模版引擎**。它允许 web 页面设计者引用 JAVA 代码预定义的方法。Web 设计者可以根据 MVC 模式和 JAVA 程序员并行工作,这意味着 Web 设计者可以单独专注于设计良好的站点,而程序员则可单独专注于编写底层代码。Velocity 将 Java 代码从 web 页面中分离出来,使站点在长时间运行后仍然具有很好的可维护性,并提供了一个除 JSP 和 PHP 之外的可行的被选方案。 + +## 注释 + +单行注释以##开始,并在本行结束。 + +```velocity +## This is a single line comment. +``` + +多行注释,以 `#` 开始并以 `#` 结束可以处理这种情况。 + +```velocity +#* + Thus begins a multi-line comment. Online visitors won't + see this text because the Velocity Templating Engine will + ignore it. +*# +``` + +注释块 ,可以用来存储诸如文档作者、版本信息等。 + +```velocity +#** +This is a VTL comment block and +may be used to store such information +as the document author and versioning +information: +@author +@version 5 +*# +``` + +## 引用 + +VTL 中有三种类型的引用:变量,属性和方法。 + +### 变量 + +变量(Variables)的简略标记是有一个前导 `$` 字符后跟一个 VTL 标识符(Identifier.)组成。一个 VTL 标识符必须以一个字母开始(a .. z 或 A .. Z)。 + +剩下的字符将由以下类型的字符组成: + +- 字母 (a .. z, A .. Z) +- 数字 (0 .. 9) +- 连字符("-") +- 下划线 ("\_") + +示例:有效变量 + +```velocity +## 有效变量变量名 +$foo +$mudSlinger +$mud-slinger +$mud_slinger +$mudSlinger1 + +## 给变量赋值 +#set( $foo = "bar" ) +``` + +### 属性 + +VTL 引用的第二种元素是属性,而属性具有独特的格式。属性的简略标记识前导符 `$` 后跟一个 VTL 标识符,在后跟一个点号(".")最后又是一个 VTL 标识符。 + +示例:有效属性 + +```velocity +$customer.Address +$purchase.Total +``` + +### 方法 + +方法在 JAVA 代码中定义,并作一些有用的事情,比如运行一个计算器或者作出一个决定。方法是实际上也是引用,由前导符 `$` 后跟一个 VTL 标识符,后跟一个 VTL 方法体(Method Body)。 VTL 方法体由一个 VTL 标识符后跟一个左括号,再跟可选的参数列表,最后是右括号。 + +示例:有效方法 + +```velocity +$customer.getAddress() +$purchase.getTotal() +$page.setTitle( "My Home Page" ) +$person.setAttributes( ["Strange", "Weird", "Excited"] ) +``` + +## 赋值 + +`#set` 指令用来为引用设置相应的值。值可以被值派给变量引用或者是属性引用,而且赋值要在括号里括起来。 + +```velocity +#set( $monkey = $bill ) ## variable reference +#set( $monkey.Friend = "monica" ) ## string literal +#set( $monkey.Blame = $whitehouse.Leak ) ## property reference +#set( $monkey.Plan = $spindoctor.weave($web) ) ## method reference +#set( $monkey.Number = 123 ) ##number literal +#set( $monkey.Say = ["Not", $my, "fault"] ) ## ArrayList +``` + +## 字符串 + +使用 `#set` 指令时,括在双引号中的字面字符串将解析和重新解释 。 然而,当字面字符串括在单引号中时,不被解析: + +示例: + +```velocity +#set( $foo = "bar" ) +$foo +#set( $blargh = '$foo' ) +$blargh +``` + +输出: + +```velocity +Bar + $foo +``` + +## 条件 + +VTL 使用 `#If`、`#elseif`、`#else` 指令做条件语句控制。 + +示例: + +```velocity +#if( $foo < 10 ) + Go North +#elseif( $foo == 10 ) + Go East +#elseif( $bar == 6 ) + Go South +#else + Go West +#end +``` + +## 逻辑 + +VTL 支持与(`&&`)、或(`||`)、非(`!`)逻辑判断。 + +示例: + +```velocity +#if( $foo && $bar ) + This AND that +#end + +#if( $foo || $bar ) + This or That +#end + +#if( !$foo ) + NOT that +#end +``` + +## 循环 + +VTL 通过 `#foreach` 支持循环 + +```velocity +
      +#foreach( $product in $allProducts ) +
    • $product
    • +#end +
    +``` + +## 包含 + +VTL 通过 `#include` 来导入其他文件。 + +示例: + +```velocity +#include( "one.txt" ) + +#include( "one.gif","two.txt","three.htm" ) + +#include( "greetings.txt", $seasonalstock ) +``` + +## 解析 + +VTL 通过 `#parse` 导入其他 vm 文件。 + +```velocity +$count +#set( $count = $count - 1 ) +#if( $count > 0 ) + #parse( "parsefoo.vm" ) +#else + All done with parsefoo.vm! +#end +``` + +## 停止 + +VTL 使用 `#stop` 停止模板引擎的执行,并返回。这通常用作调试。 + +```velocity +#stop ## +``` + +## 宏 + +VTL 使用 `#macro` 和 `#end` 配合来定义宏,以此实现自定义指令。 + +示例一: + +```velocity +## 定义宏 +#macro( d ) + +#end + +## 使用宏 +#d() +``` + +示例二: + +```velocity +## 定义宏 +#macro( tablerows $color $somelist ) + #foreach( $something in $somelist ) + $something + #end +#end + +## 使用宏 +#set( $greatlakes = ["Superior","Michigan","Huron","Erie","Ontario"] ) +#set( $color = "blue" ) + + #tablerows( $color $greatlakes ) +
    +``` + +输出: + +```html + + + + + + + + + + + + + + + + +
    Superior
    Michigan
    Huron
    Erie
    Ontario
    +``` + +## 转义 + +VTL 使用 `\` 符号来进行字符转义。 + +示例一 + +```velocity +## The following line defines $email in this template: +#set( $email = "foo" ) +$email +\$email +``` + +输出: + +```velocity +foo +$email +``` + +## 语义要点 + +Velocity 有一些语义要点,容易产生歧义,这里归纳一下。 + +(1)**Velocity 的行为并不受空格的影响**。 + +示例:以下三种写法效果一致 + +```velocity +## 写法一 +Send me #set($foo = ["$10 and ","a cake"])#foreach($a in $foo)$a #end please. + +## 写法二 +Send me +#set( $foo = ["$10 and ","a cake"] ) +#foreach( $a in $foo ) +$a +#end +please. + +## 写法三 +Send me +#set($foo = ["$10 and ","a cake"]) + #foreach ($a in $foo )$a + #end please. +``` + +## 参考资料 + +- [Velocity Github](https://github.com/apache/velocity-engine/) +- [Velocity 官网](https://velocity.apache.org/) +- [Velocity 中文文档](https://wizardforcel.gitbooks.io/velocity-doc/content/) +- [velocity-spring-boot-project](https://github.com/alibaba/velocity-spring-boot-project) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/README.md" "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/README.md" new file mode 100644 index 00000000..ffd80ba3 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/README.md" @@ -0,0 +1,58 @@ +--- +title: Java 模板引擎 +date: 2022-02-17 22:34:30 +categories: + - Java + - 工具 + - 模板引擎 +tags: + - Java + - 模板引擎 +permalink: /pages/9d37fa/ +hidden: true +index: false +--- + +# Java 模板引擎 + +模板引擎不属于特定技术领域,它是跨领域跨平台的概念。 模板引擎的作用就是分离业务数据和最终呈现内容,它可以生成特定格式的文档(模板) 。 + +模板引擎简单来说,就是:**_`模板 + 数据模型 = 输出`_** + +较早,也比较经典的模板引擎是 JavaEE 的标准技术 JSP。 + +但 JSP 存在以下缺点,导致逐渐被淘汰: + +- **性能差** + - JSP 本质上是 Servlet,第一次请求 JSP 页面,必须要在 web 服务器中编译成 servlet,所以第一次响应较慢。 + - 每次请求 JSP 都是访问 servlet 再用输出流输出的 html 页面。 + - JSP 中的内容很多,页面响应会很慢,因为是同步加载。 +- **无法前后端分离** + - 动态资源和静态资源全部耦合在一起,无法做到前后端分离。一旦服务器出现状况,前后台一起玩完。 + - 而且 Java 工程师既当爹又当妈,又要维护 Java 代码,又要维护 JSP 代码,痛苦。 + - 前端工程师如果不理解 JSP 语法,面对各种 JSP 标签、表达式、指令,会一脸懵逼,痛苦。 +- **不是所有服务器都支持** - JSP 必须要在支持 JSP 技术的 web 服务器里运行(如 Tomcat)。但有些服务器则不支持 JSP ,如 Nginx。 + +在 Java 领域,目前最常见的模板引擎就是: + +- Freemark +- Thymeleaf +- Velocity + +## 内容 + +- [Freemark](01.Freemark.md) +- [Thymeleaf](02.Thymeleaf.md) +- [Velocity](03.Velocity.md) + +## 资源 + +- **Freemark** + - [Freemark Github](https://github.com/apache/freemarker/) + - [Freemark 中文教程](http://freemarker.foofun.cn/) + - [在线 Freemark 工具](https://try.freemarker.apache.org/) +- **Velocity** + - [Velocity Github](https://github.com/apache/velocity-engine/) + - [Velocity 官网](https://velocity.apache.org/) + - [Velocity 中文文档](https://wizardforcel.gitbooks.io/velocity-doc/content/) + - [velocity-spring-boot-project](https://github.com/alibaba/velocity-spring-boot-project) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/01.Junit.md" "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/01.Junit.md" new file mode 100644 index 00000000..b1d02cb9 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/01.Junit.md" @@ -0,0 +1,594 @@ +--- +title: JUnit5 快速入门 +date: 2022-02-17 22:34:30 +order: 01 +categories: + - Java + - 工具 + - 测试 +tags: + - Java + - 测试 + - JUnit +permalink: /pages/b39f47/ +--- + +# JUnit5 快速入门 + +## JUnit5 简介 + +与以前的 JUnit 版本不同,JUnit 5 由来自三个不同子项目的几个不同模块组成。 + +JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage + +JUnit Platform 是在 JVM 上启动测试框架的基础。它还定义了用于开发在平台上运行的测试框架的 TestEngine API。此外,该平台还提供了一个控制台启动器,用于从命令行启动平台,并提供 JUnit 平台套件引擎,用于使用平台上的一个或多个测试引擎运行自定义测试套件。 + +JUnit Jupiter 是编程模型和扩展模型的组合,用于在 JUnit 5 中编写测试和扩展。Jupiter 子项目提供了一个 测试引擎(`TestEngine` )用于在平台上运行基于 Jupiter 的测试。 + +JUnit Vintage 提供了一个测试引擎(`TestEngine` ),用于在平台上运行基于 JUnit 3 和 JUnit 4 的测试。它要求 JUnit 4.12 或更高版本。 + +JUnit 5 在运行时需要 Java 8(或更高版本)。 + +## JUnit5 安装 + +在 pom 中添加依赖 + +```xml + + 5.3.2 + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-params + ${junit.jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.jupiter.version} + test + + +``` + +组件间依赖关系: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javalib/test/junit/junit5-components.png) + +## JUnit5 注解 + +| Annotation | Description | +| -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `@Test` | Denotes that a method is a test method. Unlike JUnit 4’s `@Test` annotation, this annotation does not declare any attributes, since test extensions in JUnit Jupiter operate based on their own dedicated annotations. Such methods are _inherited_ unless they are _overridden_. | +| `@ParameterizedTest` | Denotes that a method is a [parameterized test](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests). Such methods are _inherited_ unless they are _overridden_. | +| `@RepeatedTest` | Denotes that a method is a test template for a [repeated test](https://junit.org/junit5/docs/current/user-guide/#writing-tests-repeated-tests). Such methods are _inherited_ unless they are _overridden_. | +| `@TestFactory` | Denotes that a method is a test factory for [dynamic tests](https://junit.org/junit5/docs/current/user-guide/#writing-tests-dynamic-tests). Such methods are _inherited_ unless they are _overridden_. | +| `@TestInstance` | Used to configure the [test instance lifecycle](https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle) for the annotated test class. Such annotations are _inherited_. | +| `@TestTemplate` | Denotes that a method is a [template for test cases](https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-templates) designed to be invoked multiple times depending on the number of invocation contexts returned by the registered [providers](https://junit.org/junit5/docs/current/user-guide/#extensions-test-templates). Such methods are _inherited_ unless they are _overridden_. | +| `@DisplayName` | Declares a custom display name for the test class or test method. Such annotations are not _inherited_. | +| `@BeforeEach` | Denotes that the annotated method should be executed _before_ **each** `@Test`, `@RepeatedTest`, `@ParameterizedTest`, or `@TestFactory` method in the current class; analogous to JUnit 4’s `@Before`. Such methods are _inherited_ unless they are _overridden_. | +| `@AfterEach` | Denotes that the annotated method should be executed _after_ **each** `@Test`, `@RepeatedTest`, `@ParameterizedTest`, or `@TestFactory` method in the current class; analogous to JUnit 4’s `@After`. Such methods are _inherited_ unless they are _overridden_. | +| `@BeforeAll` | Denotes that the annotated method should be executed _before_ **all** `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and `@TestFactory` methods in the current class; analogous to JUnit 4’s `@BeforeClass`. Such methods are _inherited_ (unless they are _hidden_ or _overridden_) and must be `static` (unless the "per-class" [test instance lifecycle](https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle) is used). | +| `@AfterAll` | Denotes that the annotated method should be executed _after_ **all** `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and `@TestFactory` methods in the current class; analogous to JUnit 4’s `@AfterClass`. Such methods are _inherited_ (unless they are _hidden_ or _overridden_) and must be `static` (unless the "per-class" [test instance lifecycle](https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle) is used). | +| `@Nested` | Denotes that the annotated class is a nested, non-static test class. `@BeforeAll` and `@AfterAll`methods cannot be used directly in a `@Nested` test class unless the "per-class" [test instance lifecycle](https://junit.org/junit5/docs/current/user-guide/#writing-tests-test-instance-lifecycle) is used. Such annotations are not _inherited_. | +| `@Tag` | Used to declare _tags_ for filtering tests, either at the class or method level; analogous to test groups in TestNG or Categories in JUnit 4. Such annotations are _inherited_ at the class level but not at the method level. | +| `@Disabled` | Used to _disable_ a test class or test method; analogous to JUnit 4’s `@Ignore`. Such annotations are not _inherited_. | +| `@ExtendWith` | Used to register custom [extensions](https://junit.org/junit5/docs/current/user-guide/#extensions). Such annotations are _inherited_. | + +## JUnit5 示例 + +> 我将一部分官方示例放在了我的个人项目中,可以直接下载测试。 +> +> 示例源码路径:https://github.com/dunwu/java-tutorial/tree/master/codes/javatech/javatech-lib/src/test/java/io/github/dunwu/javatech/test/junit5 + +### 基本的单元测试类和方法 + +```java +import org.junit.jupiter.api.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +class Junit5StandardTests { + + private static final Logger LOGGER = LoggerFactory.getLogger(Junit5StandardTests.class); + + @BeforeAll + static void beforeAll() { + LOGGER.info("call beforeAll()"); + } + + @BeforeEach + void beforeEach() { + LOGGER.info("call beforeEach()"); + } + + @Test + void succeedingTest() { + LOGGER.info("call succeedingTest()"); + } + + @Test + void failingTest() { + LOGGER.info("call failingTest()"); + // fail("a failing test"); + } + + @Test + @Disabled("for demonstration purposes") + void skippedTest() { + LOGGER.info("call skippedTest()"); + // not executed + } + + @AfterEach + void afterEach() { + LOGGER.info("call afterEach()"); + } + + @AfterAll + static void afterAll() { + LOGGER.info("call afterAll()"); + } +} +``` + +### 定制测试类和方法的显示名称 + +支持普通字符、特殊符号、emoji + +```java +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("A special test case") +class JunitDisplayNameDemo { + + @Test + @DisplayName("Custom test name containing spaces") + void testWithDisplayNameContainingSpaces() { } + + @Test + @DisplayName("╯°□°)╯") + void testWithDisplayNameContainingSpecialCharacters() { } + + @Test + @DisplayName("😱") + void testWithDisplayNameContainingEmoji() { } +} +``` + +### 断言(Assertions) + +```java +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static java.time.Duration.ofMillis; +import static java.time.Duration.ofMinutes; +import static org.junit.jupiter.api.Assertions.*; + +class AssertionsDemo { + + private static Person person; + + @BeforeAll + public static void beforeAll() { + person = new Person("John", "Doe"); + } + + @Test + void standardAssertions() { + assertEquals(2, 2); + assertEquals(4, 4, "The optional assertion message is now the last parameter."); + assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- " + + "to avoid constructing complex messages unnecessarily."); + } + + @Test + void groupedAssertions() { + // In a grouped assertion all assertions are executed, and any + // failures will be reported together. + assertAll("person", () -> assertEquals("John", person.getFirstName()), + () -> assertEquals("Doe", person.getLastName())); + } + + @Test + void dependentAssertions() { + // Within a code block, if an assertion fails the + // subsequent code in the same block will be skipped. + assertAll("properties", () -> { + String firstName = person.getFirstName(); + assertNotNull(firstName); + + // Executed only if the previous assertion is valid. + assertAll("first name", () -> assertTrue(firstName.startsWith("J")), + () -> assertTrue(firstName.endsWith("n"))); + }, () -> { + // Grouped assertion, so processed independently + // of results of first name assertions. + String lastName = person.getLastName(); + assertNotNull(lastName); + + // Executed only if the previous assertion is valid. + assertAll("last name", () -> assertTrue(lastName.startsWith("D")), + () -> assertTrue(lastName.endsWith("e"))); + }); + } + + @Test + void exceptionTesting() { + Throwable exception = assertThrows(IllegalArgumentException.class, () -> { + throw new IllegalArgumentException("a message"); + }); + assertEquals("a message", exception.getMessage()); + } + + @Test + void timeoutNotExceeded() { + // The following assertion succeeds. + assertTimeout(ofMinutes(2), () -> { + // Perform task that takes less than 2 minutes. + }); + } + + @Test + void timeoutNotExceededWithResult() { + // The following assertion succeeds, and returns the supplied object. + String actualResult = assertTimeout(ofMinutes(2), () -> { + return "a result"; + }); + assertEquals("a result", actualResult); + } + + @Test + void timeoutNotExceededWithMethod() { + // The following assertion invokes a method reference and returns an object. + String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting); + assertEquals("Hello, World!", actualGreeting); + } + + @Test + void timeoutExceeded() { + // The following assertion fails with an error message similar to: + // execution exceeded timeout of 10 ms by 91 ms + assertTimeout(ofMillis(10), () -> { + // Simulate task that takes more than 10 ms. + Thread.sleep(100); + }); + } + + @Test + void timeoutExceededWithPreemptiveTermination() { + // The following assertion fails with an error message similar to: + // execution timed out after 10 ms + assertTimeoutPreemptively(ofMillis(10), () -> { + // Simulate task that takes more than 10 ms. + Thread.sleep(100); + }); + } + + private static String greeting() { + return "Hello, World!"; + } + +} +``` + +### 假想(Assumptions) + +```java +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.Assumptions.assumingThat; + +import org.junit.jupiter.api.Test; + +class AssumptionsDemo { + + @Test + void testOnlyOnCiServer() { + assumeTrue("CI".equals(System.getenv("ENV"))); + // remainder of test + } + + @Test + void testOnlyOnDeveloperWorkstation() { + assumeTrue("DEV".equals(System.getenv("ENV")), + () -> "Aborting test: not on developer workstation"); + // remainder of test + } + + @Test + void testInAllEnvironments() { + assumingThat("CI".equals(System.getenv("ENV")), + () -> { + // perform these assertions only on the CI server + assertEquals(2, 2); + }); + + // perform these assertions in all environments + assertEquals("a string", "a string"); + } + +} +``` + +### 禁用 + +禁用单元测试类示例: + +```java +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +@Disabled +class DisabledClassDemo { + @Test + void testWillBeSkipped() { + } +} +``` + +禁用单元测试方法示例: + +```java +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +class DisabledTestsDemo { + + @Disabled + @Test + void testWillBeSkipped() { + } + + @Test + void testWillBeExecuted() { + } +} +``` + +### 测试条件 + +#### 操作系统条件 + +```java +@Test +@EnabledOnOs(MAC) +void onlyOnMacOs() { + // ... +} + +@TestOnMac +void testOnMac() { + // ... +} + +@Test +@EnabledOnOs({ LINUX, MAC }) +void onLinuxOrMac() { + // ... +} + +@Test +@DisabledOnOs(WINDOWS) +void notOnWindows() { + // ... +} + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Test +@EnabledOnOs(MAC) +@interface TestOnMac { +} +``` + +#### Java 运行时版本条件 + +```java +@Test +@EnabledOnJre(JAVA_8) +void onlyOnJava8() { + // ... +} + +@Test +@EnabledOnJre({ JAVA_9, JAVA_10 }) +void onJava9Or10() { + // ... +} + +@Test +@DisabledOnJre(JAVA_9) +void notOnJava9() { + // ... +} +``` + +#### 系统属性条件 + +```java +@Test +@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*") +void onlyOn64BitArchitectures() { + // ... +} + +@Test +@DisabledIfSystemProperty(named = "ci-server", matches = "true") +void notOnCiServer() { + // ... +} +``` + +### 嵌套测试 + +```java +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.EmptyStackException; +import java.util.Stack; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@DisplayName("A stack") +class TestingAStackDemo { + + Stack stack; + + @Test + @DisplayName("is instantiated with new Stack()") + void isInstantiatedWithNew() { + new Stack<>(); + } + + @Nested + @DisplayName("when new") + class WhenNew { + + @BeforeEach + void createNewStack() { + stack = new Stack<>(); + } + + @Test + @DisplayName("is empty") + void isEmpty() { + assertTrue(stack.isEmpty()); + } + + @Test + @DisplayName("throws EmptyStackException when popped") + void throwsExceptionWhenPopped() { + assertThrows(EmptyStackException.class, () -> stack.pop()); + } + + @Test + @DisplayName("throws EmptyStackException when peeked") + void throwsExceptionWhenPeeked() { + assertThrows(EmptyStackException.class, () -> stack.peek()); + } + + @Nested + @DisplayName("after pushing an element") + class AfterPushing { + + String anElement = "an element"; + + @BeforeEach + void pushAnElement() { + stack.push(anElement); + } + + @Test + @DisplayName("it is no longer empty") + void isNotEmpty() { + assertFalse(stack.isEmpty()); + } + + @Test + @DisplayName("returns the element when popped and is empty") + void returnElementWhenPopped() { + assertEquals(anElement, stack.pop()); + assertTrue(stack.isEmpty()); + } + + @Test + @DisplayName("returns the element when peeked but remains not empty") + void returnElementWhenPeeked() { + assertEquals(anElement, stack.peek()); + assertFalse(stack.isEmpty()); + } + } + } +} +``` + +### 重复测试 + +```java +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.logging.Logger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.RepetitionInfo; +import org.junit.jupiter.api.TestInfo; + +class RepeatedTestsDemo { + + private Logger logger = // ... + + @BeforeEach + void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) { + int currentRepetition = repetitionInfo.getCurrentRepetition(); + int totalRepetitions = repetitionInfo.getTotalRepetitions(); + String methodName = testInfo.getTestMethod().get().getName(); + logger.info(String.format("About to execute repetition %d of %d for %s", // + currentRepetition, totalRepetitions, methodName)); + } + + @RepeatedTest(10) + void repeatedTest() { + // ... + } + + @RepeatedTest(5) + void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) { + assertEquals(5, repetitionInfo.getTotalRepetitions()); + } + + @RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}") + @DisplayName("Repeat!") + void customDisplayName(TestInfo testInfo) { + assertEquals(testInfo.getDisplayName(), "Repeat! 1/1"); + } + + @RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME) + @DisplayName("Details...") + void customDisplayNameWithLongPattern(TestInfo testInfo) { + assertEquals(testInfo.getDisplayName(), "Details... :: repetition 1 of 1"); + } + + @RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}") + void repeatedTestInGerman() { + // ... + } + +} +``` + +### 参数化测试 + +```java +@ParameterizedTest +@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) +void palindromes(String candidate) { + assertTrue(isPalindrome(candidate)); +} +``` + +## 参考资料 + +- [Junit5 Github](https://github.com/junit-team/junit5) +- [Junit5 官方用户手册](https://junit.org/junit5/docs/current/user-guide/) +- [Junit5 Javadoc](https://junit.org/junit5/docs/current/api/) +- [Junit5 官方示例](https://github.com/junit-team/junit5-samples) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/02.Mockito.md" "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/02.Mockito.md" new file mode 100644 index 00000000..e33ffaa4 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/02.Mockito.md" @@ -0,0 +1,577 @@ +--- +title: Mockito 快速入门 +date: 2022-02-17 22:34:30 +order: 02 +categories: + - Java + - 工具 + - 测试 +tags: + - Java + - 测试 + - Mockito +permalink: /pages/f2c6f5/ +--- + +# Mockito 快速入门 + +> Mockito 是一个针对 Java 的 mock 框架。 + +## 预备知识 + +如果需要往下学习,你需要先理解 Junit 框架中的单元测试。 + +如果你不熟悉 JUnit,请看 [Junit 教程](http://www.vogella.com/tutorials/JUnit/article.html) + +## 使用 mock 对象来进行测试 + +### 单元测试的目标和挑战 + +单元测试的思路是在不涉及依赖关系的情况下测试代码(隔离性),所以测试代码与其他类或者系统的关系应该尽量被消除。一个可行的消除方法是替换掉依赖类(测试替换),也就是说我们可以使用替身来替换掉真正的依赖对象。 + +### 测试类的分类 + +- **dummy object** 做为参数传递给方法但是绝对不会被使用。譬如说,这种测试类内部的方法不会被调用,或者是用来填充某个方法的参数。 +- **Fake** 是真正接口或抽象类的实现体,但给对象内部实现很简单。譬如说,它存在内存中而不是真正的数据库中。(译者注:**Fake** 实现了真正的逻辑,但它的存在只是为了测试,而不适合于用在产品中。) +- **stub** 类是依赖类的部分方法实现,而这些方法在你测试类和接口的时候会被用到,也就是说 **stub** 类在测试中会被实例化。**stub** 类会回应任何外部测试的调用。**stub** 类有时候还会记录调用的一些信息。 +- **mock object** 是指类或者接口的模拟实现,你可以自定义这个对象中某个方法的输出结果。 + +测试替代技术能够在测试中模拟测试类以外对象。因此你可以验证测试类是否响应正常。譬如说,你可以验证在 Mock 对象的某一个方法是否被调用。这可以确保隔离了外部依赖的干扰只测试测试类。 + +我们选择 Mock 对象的原因是因为 Mock 对象只需要少量代码的配置。 + +### Mock 对象的产生 + +你可以手动创建一个 Mock 对象或者使用 Mock 框架来模拟这些类,Mock 框架允许你在运行时创建 Mock 对象并且定义它的行为。 + +一个典型的例子是把 Mock 对象模拟成数据的提供者。在正式的生产环境中它会被实现用来连接数据源。但是我们在测试的时候 Mock 对象将会模拟成数据提供者来确保我们的测试环境始终是相同的。 + +Mock 对象可以被提供来进行测试。因此,我们测试的类应该避免任何外部数据的强依赖。 + +通过 Mock 对象或者 Mock 框架,我们可以测试代码中期望的行为。譬如说,验证只有某个存在 Mock 对象的方法是否被调用了。 + +### 使用 Mockito 生成 Mock 对象 + +_Mockito_ 是一个流行 mock 框架,可以和 JUnit 结合起来使用。Mockito 允许你创建和配置 mock 对象。使用 Mockito 可以明显的简化对外部依赖的测试类的开发。 + +一般使用 Mockito 需要执行下面三步 + +1. 模拟并替换测试代码中外部依赖 +2. 执行测试代码 +3. 验证测试代码是否被正确的执行 0 + +## 为自己的项目添加 Mockito 依赖 + +### 在 Gradle 添加 Mockito 依赖 + +如果你的项目使用 Gradle 构建,将下面代码加入 Gradle 的构建文件中为自己项目添加 Mockito 依赖 + +``` +repositories { jcenter() } +dependencies { testCompile "org.mockito:mockito-core:2.0.57-beta" } +``` + +### 在 Maven 添加 Mockito 依赖 + +需要在 Maven 声明依赖,您可以在 [http://search.maven.org](http://search.maven.org/) 网站中搜索 `g:"org.mockito", a:"mockito-core"` 来得到具体的声明方式。 + +### 在 Eclipse IDE 使用 Mockito + +Eclipse IDE 支持 Gradle 和 Maven 两种构建工具,所以在 Eclipse IDE 添加依赖取决你使用的是哪一个构建工具。 + +### 以 OSGi 或者 Eclipse 插件形式添加 Mockito 依赖 + +在 Eclipse RCP 应用依赖通常可以在 p2 update 上得到。Orbit 是一个很好的第三方仓库,我们可以在里面寻找能在 Eclipse 上使用的应用和插件。 + +Orbit 仓库地址:[http://download.eclipse.org/tools/orbit/downloads](http://download.eclipse.org/tools/orbit/downloads) + +## 使用 Mockito API + +### 静态引用 + +如果在代码中静态引用了`org.mockito.Mockito.*;`,那你你就可以直接调用静态方法和静态变量而不用创建对象,譬如直接调用 mock() 方法。 + +### 使用 Mockito 创建和配置 mock 对象 + +除了上面所说的使用 mock() 静态方法外,Mockito 还支持通过 `@Mock` 注解的方式来创建 mock 对象。 + +如果你使用注解,那么必须要实例化 mock 对象。Mockito 在遇到使用注解的字段的时候,会调用`MockitoAnnotations.initMocks(this)` 来初始化该 mock 对象。另外也可以通过使用`@RunWith(MockitoJUnitRunner.class)`来达到相同的效果。 + +通过下面的例子我们可以了解到使用`@Mock` 的方法和`MockitoRule`规则。 + +```java +import static org.mockito.Mockito.*; + +public class MockitoTest { + + @Mock + MyDatabase databaseMock; (1) + + @Rule public MockitoRule mockitoRule = MockitoJUnit.rule(); (2) + + @Test + public void testQuery() { + ClassToTest t = new ClassToTest(databaseMock); (3) + boolean check = t.query("* from t"); (4) + assertTrue(check); (5) + verify(databaseMock).query("* from t"); (6) + } +} +``` + +1. 告诉 Mockito 模拟 databaseMock 实例 +2. Mockito 通过 @mock 注解创建 mock 对象 +3. 使用已经创建的 mock 初始化这个类 +4. 在测试环境下,执行测试类中的代码 +5. 使用断言确保调用的方法返回值为 true +6. 验证 query 方法是否被 `MyDatabase` 的 mock 对象调用 + +### 配置 mock + +当我们需要配置某个方法的返回值的时候,Mockito 提供了链式的 API 供我们方便的调用 + +`when(….).thenReturn(….)`可以被用来定义当条件满足时函数的返回值,如果你需要定义多个返回值,可以多次定义。当你多次调用函数的时候,Mockito 会根据你定义的先后顺序来返回返回值。Mocks 还可以根据传入参数的不同来定义不同的返回值。譬如说你的函数可以将`anyString` 或者 `anyInt`作为输入参数,然后定义其特定的放回值。 + +```java +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; + +@Test +public void test1() { + // 创建 mock + MyClass test = Mockito.mock(MyClass.class); + + // 自定义 getUniqueId() 的返回值 + when(test.getUniqueId()).thenReturn(43); + + // 在测试中使用mock对象 + assertEquals(test.getUniqueId(), 43); +} + +// 返回多个值 +@Test +public void testMoreThanOneReturnValue() { + Iterator i= mock(Iterator.class); + when(i.next()).thenReturn("Mockito").thenReturn("rocks"); + String result=i.next()+" "+i.next(); + // 断言 + assertEquals("Mockito rocks", result); +} + +// 如何根据输入来返回值 +@Test +public void testReturnValueDependentOnMethodParameter() { + Comparable c= mock(Comparable.class); + when(c.compareTo("Mockito")).thenReturn(1); + when(c.compareTo("Eclipse")).thenReturn(2); + // 断言 + assertEquals(1,c.compareTo("Mockito")); +} + +// 如何让返回值不依赖于输入 +@Test +public void testReturnValueInDependentOnMethodParameter() { + Comparable c= mock(Comparable.class); + when(c.compareTo(anyInt())).thenReturn(-1); + // 断言 + assertEquals(-1 ,c.compareTo(9)); +} + +// 根据参数类型来返回值 +@Test +public void testReturnValueInDependentOnMethodParameter() { + Comparable c= mock(Comparable.class); + when(c.compareTo(isA(Todo.class))).thenReturn(0); + // 断言 + Todo todo = new Todo(5); + assertEquals(todo ,c.compareTo(new Todo(1))); +} +``` + +对于无返回值的函数,我们可以使用`doReturn(…).when(…).methodCall`来获得类似的效果。例如我们想在调用某些无返回值函数的时候抛出异常,那么可以使用`doThrow` 方法。如下面代码片段所示 + +```java +import static org.mockito.Mockito.*; +import static org.junit.Assert.*; + +// 下面测试用例描述了如何使用doThrow()方法 + +@Test(expected=IOException.class) +public void testForIOException() { + // 创建并配置 mock 对象 + OutputStream mockStream = mock(OutputStream.class); + doThrow(new IOException()).when(mockStream).close(); + + // 使用 mock + OutputStreamWriter streamWriter= new OutputStreamWriter(mockStream); + streamWriter.close(); +} +``` + +### 验证 mock 对象方法是否被调用 + +Mockito 会跟踪 mock 对象里面所有的方法和变量。所以我们可以用来验证函数在传入特定参数的时候是否被调用。这种方式的测试称行为测试,行为测试并不会检查函数的返回值,而是检查在传入正确参数时候函数是否被调用。 + +```java +import static org.mockito.Mockito.*; + +@Test +public void testVerify() { + // 创建并配置 mock 对象 + MyClass test = Mockito.mock(MyClass.class); + when(test.getUniqueId()).thenReturn(43); + + // 调用mock对象里面的方法并传入参数为12 + test.testing(12); + test.getUniqueId(); + test.getUniqueId(); + + // 查看在传入参数为12的时候方法是否被调用 + verify(test).testing(Matchers.eq(12)); + + // 方法是否被调用两次 + verify(test, times(2)).getUniqueId(); + + // 其他用来验证函数是否被调用的方法 + verify(mock, never()).someMethod("never called"); + verify(mock, atLeastOnce()).someMethod("called at least once"); + verify(mock, atLeast(2)).someMethod("called at least twice"); + verify(mock, times(5)).someMethod("called five times"); + verify(mock, atMost(3)).someMethod("called at most 3 times"); +} +``` + +### 使用 Spy 封装 java 对象 + +@Spy 或者`spy()`方法可以被用来封装 java 对象。被封装后,除非特殊声明(打桩 _stub_),否则都会真正的调用对象里面的每一个方法 + +```java +import static org.mockito.Mockito.*; + +// Lets mock a LinkedList +List list = new LinkedList(); +List spy = spy(list); + +// 可用 doReturn() 来打桩 +doReturn("foo").when(spy).get(0); + +// 下面代码不生效 +// 真正的方法会被调用 +// 将会抛出 IndexOutOfBoundsException 的异常,因为 List 为空 +when(spy.get(0)).thenReturn("foo"); +``` + +方法`verifyNoMoreInteractions()`允许你检查没有其他的方法被调用了。 + +### 使用 @InjectMocks 在 Mockito 中进行依赖注入 + +我们也可以使用`@InjectMocks` 注解来创建对象,它会根据类型来注入对象里面的成员方法和变量。假定我们有 ArticleManager 类 + +```java +public class ArticleManager { + private User user; + private ArticleDatabase database; + + ArticleManager(User user) { + this.user = user; + } + + void setDatabase(ArticleDatabase database) { } +} +``` + +这个类会被 Mockito 构造,而类的成员方法和变量都会被 mock 对象所代替,正如下面的代码片段所示: + +```java +@RunWith(MockitoJUnitRunner.class) +public class ArticleManagerTest { + + @Mock ArticleCalculator calculator; + @Mock ArticleDatabase database; + @Most User user; + + @Spy private UserProvider userProvider = new ConsumerUserProvider(); + + @InjectMocks private ArticleManager manager; (1) + + @Test public void shouldDoSomething() { + // 假定 ArticleManager 有一个叫 initialize() 的方法被调用了 + // 使用 ArticleListener 来调用 addListener 方法 + manager.initialize(); + + // 验证 addListener 方法被调用 + verify(database).addListener(any(ArticleListener.class)); + } +} +``` + +1. 创建 ArticleManager 实例并注入 Mock 对象 + +更多的详情可以查看 [http://docs.mockito.googlecode.com/hg/1.9.5/org/mockito/InjectMocks.html](http://docs.mockito.googlecode.com/hg/1.9.5/org/mockito/InjectMocks.html) + +### 捕捉参数 + +`ArgumentCaptor`类允许我们在 verification 期间访问方法的参数。得到方法的参数后我们可以使用它进行测试。 + +```java +import static org.hamcrest.Matchers.hasItem; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.Arrays; +import java.util.List; + +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +public class MockitoTests { + @Rule + public MockitoRule rule = MockitoJUnit.rule(); + + @Captor + private ArgumentCaptor> captor; + + @Test + public final void shouldContainCertainListItem() { + List asList = Arrays.asList("someElement_test", "someElement"); + final List mockedList = mock(List.class); + mockedList.addAll(asList); + + verify(mockedList).addAll(captor.capture()); + final List capturedArgument = captor.getValue(); + assertThat(capturedArgument, hasItem("someElement")); + } +} +``` + +### Mockito 的限制 + +Mockito 当然也有一定的限制。而下面三种数据类型则不能够被测试 + +- final classes +- anonymous classes +- primitive types + +## 在 Android 中使用 Mockito + +在 Android 中的 Gradle 构建文件中加入 Mockito 依赖后就可以直接使用 Mockito 了。若想使用 Android Instrumented tests 的话,还需要添加 dexmaker 和 dexmaker-mockito 依赖到 Gradle 的构建文件中。(需要 Mockito 1.9.5 版本以上) + +```java +dependencies { + testCompile 'junit:junit:4.12' + // Mockito unit test 的依赖 + testCompile 'org.mockito:mockito-core:1.+' + // Mockito Android instrumentation tests 的依赖 + androidTestCompile 'org.mockito:mockito-core:1.+' + androidTestCompile "com.google.dexmaker:dexmaker:1.2" + androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2" +} +``` + +## 实例:使用 Mockito 写一个 Instrumented Unit Test + +### 创建一个测试的 Android 应用 + +创建一个包名为`com.vogella.android.testing.mockito.contextmock`的 Android 应用,添加一个静态方法 ,方法里面创建一个包含参数的 Intent,如下代码所示: + +```java +public static Intent createQuery(Context context, String query, String value) { + // 简单起见,重用MainActivity + Intent i = new Intent(context, MainActivity.class); + i.putExtra("QUERY", query); + i.putExtra("VALUE", value); + return i; +} +``` + +### 在 app/build.gradle 文件中添加 Mockito 依赖 + +```java +dependencies { + // Mockito 和 JUnit 的依赖 + // instrumentation unit tests on the JVM + androidTestCompile 'junit:junit:4.12' + androidTestCompile 'org.mockito:mockito-core:2.0.57-beta' + androidTestCompile 'com.android.support.test:runner:0.3' + androidTestCompile "com.google.dexmaker:dexmaker:1.2" + androidTestCompile "com.google.dexmaker:dexmaker-mockito:1.2" + + // Mockito 和 JUnit 的依赖 + // tests on the JVM + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.+' + +} +``` + +### 创建测试 + +使用 Mockito 创建一个单元测试来验证在传递正确 extra data 的情况下,intent 是否被触发。 + +因此我们需要使用 Mockito 来 mock 一个`Context`对象,如下代码所示: + +```java +package com.vogella.android.testing.mockitocontextmock; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +public class TextIntentCreation { + + @Test + public void testIntentShouldBeCreated() { + Context context = Mockito.mock(Context.class); + Intent intent = MainActivity.createQuery(context, "query", "value"); + assertNotNull(intent); + Bundle extras = intent.getExtras(); + assertNotNull(extras); + assertEquals("query", extras.getString("QUERY")); + assertEquals("value", extras.getString("VALUE")); + } +} +``` + +## 实例:使用 Mockito 创建一个 mock 对象 + +### 目标 + +创建一个 Api,它可以被 Mockito 来模拟并做一些工作 + +### 创建一个 Twitter API 的例子 + +实现 `TwitterClient`类,它内部使用到了 `ITweet` 的实现。但是`ITweet`实例很难得到,譬如说他需要启动一个很复杂的服务来得到。 + +```java +public interface ITweet { + + String getMessage(); +} + + +public class TwitterClient { + + public void sendTweet(ITweet tweet) { + String message = tweet.getMessage(); + + // send the message to Twitter + } +} +``` + +### 模拟 ITweet 的实例 + +为了能够不启动复杂的服务来得到 `ITweet`,我们可以使用 Mockito 来模拟得到该实例。 + +```java +@Test +public void testSendingTweet() { + TwitterClient twitterClient = new TwitterClient(); + + ITweet iTweet = mock(ITweet.class); + + when(iTweet.getMessage()).thenReturn("Using mockito is great"); + + twitterClient.sendTweet(iTweet); +} +``` + +现在 `TwitterClient` 可以使用 `ITweet` 接口的实现,当调用 `getMessage()` 方法的时候将会打印 "Using Mockito is great" 信息。 + +### 验证方法调用 + +确保 getMessage() 方法至少调用一次。 + +```java +@Test +public void testSendingTweet() { + TwitterClient twitterClient = new TwitterClient(); + + ITweet iTweet = mock(ITweet.class); + + when(iTweet.getMessage()).thenReturn("Using mockito is great"); + + twitterClient.sendTweet(iTweet); + + verify(iTweet, atLeastOnce()).getMessage(); +} +``` + +### 验证 + +运行测试,查看代码是否测试通过。 + +## 模拟静态方法 + +### 使用 Powermock 来模拟静态方法 + +因为 Mockito 不能够 mock 静态方法,因此我们可以使用 `Powermock`。 + +```java +import java.net.InetAddress; +import java.net.UnknownHostException; + +public final class NetworkReader { + public static String getLocalHostname() { + String hostname = ""; + try { + InetAddress addr = InetAddress.getLocalHost(); + // Get hostname + hostname = addr.getHostName(); + } catch ( UnknownHostException e ) { + } + return hostname; + } +} +``` + +我们模拟了 NetworkReader 的依赖,如下代码所示: + +```java +import org.junit.runner.RunWith; +import org.powermock.core.classloader.annotations.PrepareForTest; + +@RunWith( PowerMockRunner.class ) +@PrepareForTest( NetworkReader.class ) +public class MyTest { + +// 测试代码 + + @Test +public void testSomething() { + mockStatic( NetworkUtil.class ); + when( NetworkReader.getLocalHostname() ).andReturn( "localhost" ); + + // 与 NetworkReader 协作的测试 +} +``` + +### 用封装的方法代替 Powermock + +有时候我们可以在静态方法周围包含非静态的方法来达到和 Powermock 同样的效果。 + +```java +class FooWraper { + void someMethod() { + Foo.someStaticMethod() + } +} +``` + +## 引用和引申 + +- [官网](https://site.mockito.org/) +- [Github](https://github.com/mockito/mockito) +- [使用强大的 Mockito 测试框架来测试你的代码](https://github.com/xitu/gold-miner/blob/master/TODO/Unit-tests-with-Mockito.md) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/03.Jmeter.md" "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/03.Jmeter.md" new file mode 100644 index 00000000..7378e09f --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/03.Jmeter.md" @@ -0,0 +1,219 @@ +--- +title: JMeter 快速入门 +date: 2022-02-17 22:34:30 +order: 03 +categories: + - Java + - 工具 + - 测试 +tags: + - Java + - 测试 + - JMeter +permalink: /pages/0e5ab1/ +--- + +# JMeter 快速入门 + +> [Jmeter](https://github.com/apache/jmeter) 是一款基于 Java 开发的功能和性能测试软件。 +> +> 🎁 本文编辑时的最新版本为:5.1.1 + +## 简介 + +[Jmeter](https://github.com/apache/jmeter) 是一款使用 Java 开发的功能和性能测试软件。 + +### 特性 + +Jmeter 能够加载和性能测试许多不同的应用程序/服务器/协议类型: + +- 网络 - HTTP,HTTPS(Java,NodeJS,PHP,ASP.NET 等) +- SOAP / REST Web 服务 +- FTP 文件 +- 通过 JDBC 的数据库 +- LDAP +- 通过 JMS 的面向消息的中间件(MOM) +- 邮件-SMTP(S),POP3(S)和 IMAP(S) +- 本机命令或 Shell 脚本 +- TCP 协议 +- Java 对象 + +### 工作流 + +Jmeter 的工作原理是仿真用户向服务器发送请求,并收集服务器应答信息并计算统计信息。 + +Jmeter 的工作流如下图所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/test/jmeter-workflow.png) + +### 主要元素 + +Jmeter 的主要元素如下: + +- **`测试计划(Test Plan)`** - 可以将测试计划视为 JMeter 的测试脚本 。测试计划由测试元素组成,例如线程组,逻辑控制器,样本生成控制器,监听器,定时器,断言和配置元素。 +- **`线程组(Thread Group)`** - 线程组的作用是:模拟大量用户负载的运行场景。 + - 设置线程数 + - 设置加速期 + - 设置执行测试的次数 +- **`控制器(Controllers)`** - 可以分为两大类: + - **`采样器(Sampler)`** - 采样器的作用是模拟用户对目标服务器发送请求。 采样器是必须将组件添加到测试计划中的,因为它只能让 JMeter 知道需要将哪种类型的请求发送到服务器。 请求可以是 HTTP,HTTP(s),FTP,TCP,SMTP,SOAP 等。 + - **`逻辑控制器`** - 逻辑控制器的作用是:控制多个请求发送的循环次数及顺序等。 +- **`监听器(Listeners)`** - 监听器的作用是:收集测试结果信息。如查看结果树、汇总报告等。 +- **`计时器(Timers)`** - 计时器的作用是:控制多个请求发送的时间频次。 +- **`配置元素(Configuration Elements)`** - 配置元素的工作与采样器的工作类似。但是,它不发送请求,而是提供预备的数据等,如 CSV、函数助手。 +- **`预处理器元素(Pre-Processor Elements)`** - 预处理器元素在采样器发出请求之前执行,如果预处理器附加到采样器元素,那么它将在该采样器元素运行之前执行。预处理器元素用于在运行之前准备环境及参数。 +- **`后处理器元素(Post-Processor Elements)`** - 后处理器元素是在发送采样器请求之后执行的元素,常用于处理响应数据。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/test/jmeter-elements.png) + +> 📌 提示: +> +> Jmeter 元素的数量关系大致如下: +> +> 1. 脚本中最多只能有一个测试计划。 +> 2. 测试计划中至少要有一个线程组。 +> 3. 线程组中至少要有一个取样器。 +> 4. 线程组中至少要有一个监听器。 + +## 安装 + +### 环境要求 + +- 必要的。Jmeter 基于 JDK8 开发,所以必须运行在 JDK8 环境。 + + - JDK8 + +- 可选的。有些 jar 包不是 Jmeter 提供的,如果需要相应的功能,需要自行下载并置于 `lib` 目录。 + - JDBC + - JMS + - [Bouncy Castle](http://www.bouncycastle.org/test_releases.html) + +### 下载 + +进入 [**Jmeter 官网下载地址**](https://jmeter.apache.org/download_jmeter.cgi) 选择需要版本进行下载。 + +### 启动 + +解压 Jmeter 压缩包,进入 bin 目录 + +Unix 类系统运行 `jmeter` ;Windows 系统运行 `jmeter.bat` + +![image-20191024104517721](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024104517721.png) + +## 使用 + +### 创建测试计划 + +> 🔔 注意: +> +> - 在运行整个测试计划之前,应保存测试计划。 +> +> - JMeter 的测试计划以 `.jmx` 扩展文件的形式保存。 + +#### 创建线程组 + +- 在“测试计划”上右键 【添加】=>【线程(用户)】=>【线程组】。 + +- 设置线程数和循环次数 + +![image-20191024105545736](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024105545736.png) + +#### 配置原件 + +- 在新建的线程组上右键 【添加】=>【配置元件】=>【HTTP 请求默认值】。 + +- 填写协议、服务器名称或 IP、端口号 + +![image-20191024110016264](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024110016264.png) + +#### 构造 HTTP 请求 + +- 在“线程组”上右键 【添加-】=>【取样器】=>【HTTP 请求】。 + +- 填写协议、服务器名称或 IP、端口号(如果配置了 HTTP 请求默认值可以忽略) +- 填写方法、路径 +- 填写参数、消息体数据、文件上传 + +![image-20191024110953063](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024110953063.png) + +#### 添加 HTTP 请求头 + +- 在“线程组”上右键 【添加】=>【配置元件】=>【HTTP 信息头管理器】 +- 由于我的测试例中传输的数据为 json 形式,所以设置键值对 `Content-Type`:`application/json` + +![image-20191024111825226](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024111825226.png) + +#### 添加断言 + +- 在“线程组”上右键 【添加】=>【断言】=>【 响应断言 】 +- 在我的案例中,以 HTTP 应答状态码为 200 来判断请求是否成功 + +![image-20191024112335130](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024112335130.png) + +#### 添加察看结果树 + +- 在“线程组”上右键 【添加】=>【监听器】=>【察看结果树】 +- 直接点击运行,就可以查看测试结果 + +![image-20191024113849270](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024113849270.png) + +#### 添加汇总报告 + +- 在“线程组”上右键 【添加】=>【监听器】=>【汇总报告】 +- 直接点击运行,就可以查看测试结果 + +![image-20191024114016424](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024114016424.png) + +#### 保存测试计划 + +执行测试计划前,GUI 会提示先保存配置为 `jmx` 文件。 + +### 执行测试计划 + +官方建议不要直接使用 GUI 来执行测试计划,这种模式指适用于创建测试计划和 debug。 + +执行测试计划应该使用命令行模式,语法形式如下: + +```bash +jmeter -n -t [jmx file] -l [results file] -e -o [Path to web report folder] +``` + +执行测试计划后,在 `-e -o` 参数后指定的 web 报告目录下,可以找到测试报告内容。在浏览器中打开 `index.html` 文件,可以看到如下报告: + +![image-20191024120233058](https://raw.githubusercontent.com/dunwu/images/master/snap/jmeter/image-20191024120233058.png) + +## 问题 + +### 如何读取本地 txt/csv 文件作为请求参数 + +参考:[Jmeter 读取本地 txt/csv 文件作为请求参数,实现接口自动化](https://www.jianshu.com/p/3b2d3b643415) + +(1)依次点击【添加】=>【配置元件】=>【CSV 数据文件设置】 + +配置如下所示: + +![image-20191127175820747](https://raw.githubusercontent.com/dunwu/images/master/snap/image-20191127175820747.png) + +重要配置说明(其他配置根据实际情况填): + +- 文件名:输入需要导入的数据文件位置。 +- 文件编码:设为 UTF-8,避免乱码。 +- 变量名称:使用 `,` 分隔输入变量列表。如截图中设置了两个变量 `a` 和 `b` + +(2)在 HTTP 请求的消息体数据中配置参数 + +``` +[{"a":"${a}","b":"${b}"}] +``` + +### 如何有序发送数据 + +依次点击【添加】=>【逻辑控制器】=>【事务控制器】 + +## 参考资料 + +- [Jmeter 官网](https://jmeter.apache.org/) +- [Jmeter Github](https://github.com/apache/jmeter) +- [Jmeter 性能测试入门](https://www.cnblogs.com/TankXiao/p/4045439.html) +- [易百教程 - Jmeter 教程](https://www.yiibai.com/jmeter) +- [Jmeter 读取本地 txt/csv 文件作为请求参数,实现接口自动化](https://www.jianshu.com/p/3b2d3b643415) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/04.JMH.md" "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/04.JMH.md" new file mode 100644 index 00000000..e5707faf --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/04.JMH.md" @@ -0,0 +1,355 @@ +--- +title: JMH 快速入门 +date: 2022-02-17 22:34:30 +order: 04 +categories: + - Java + - 工具 + - 测试 +tags: + - Java + - 测试 + - JUnit +permalink: /pages/9c6402/ +--- + +# JMH 快速入门 + +## 基准测试简介 + +### 什么是基准测试 + +基准测试是指通过设计科学的测试方法、测试工具和测试系统,实现对一类测试对象的某项性能指标进行定量的和可对比的测试。 + +现代软件常常都把高性能作为目标。那么,何为高性能,性能就是快,更快吗?显然,如果没有一个量化的标准,难以衡量性能的好坏。 + +不同的基准测试其具体内容和范围也存在很大的不同。如果是专业的性能工程师,更加熟悉的可能是类似 SPEC 提供的工业标准的系统级测试;而对于大多数 Java 开发者,更熟悉的则是范围相对较小、关注点更加细节的微基准测试(Micro-Benchmark)。何谓 Micro Benchmark 呢? 简单地说就是在 method 层面上的 benchmark,精度可以精确到 **微秒级**。 + +### 何时需要微基准测试 + +微基准测试大多是 API 级别的性能测试。 + +微基准测试的适用场景: + +- 如果开发公共类库、中间件,会被其他模块经常调用的 API。 +- 对于性能,如响应延迟、吞吐量有严格要求的核心 API。 + +## JMH 简介 + +[JMH(即 Java Microbenchmark Harness)](http://openjdk.java.net/projects/code-tools/jmh/),是目前主流的微基准测试框架。JMH 是由 Hotspot JVM 团队专家开发的,除了支持完整的基准测试过程,包括预热、运行、统计和报告等,还支持 Java 和其他 JVM 语言。更重要的是,它针对 Hotspot JVM 提供了各种特性,以保证基准测试的正确性,整体准确性大大优于其他框架,并且,JMH 还提供了用近乎白盒的方式进行 Profiling 等工作的能力。 + +### 为什么需要 JMH + +#### 死码消除 + +所谓死码,是指注释的代码,不可达的代码块,可达但不被使用的代码等等 。 + +#### 常量折叠与常量传播 + +[常量折叠](https://zh.wikipedia.org/wiki/常數折疊#常數傳播) (Constant folding) 是一个在编译时期简化常数的一个过程,常数在表示式中仅仅代表一个简单的数值,就像是整数 `2`,若是一个变数从未被修改也可作为常数,或者直接将一个变数被明确地被标注为常数,例如下面的描述: + +### JMH 的注意点 + +- 测试前需要预热。 +- 防止无用代码进入测试方法中。 +- 并发测试。 +- 测试结果呈现。 + +### 应用场景 + +1. 当你已经找出了热点函数,而需要对热点函数进行进一步的优化时,就可以使用 JMH 对优化的效果进行定量的分析。 +2. 想定量地知道某个函数需要执行多长时间,以及执行时间和输入 n 的相关性 +3. 一个函数有两种不同实现(例如 JSON 序列化/反序列化有 Jackson 和 Gson 实现),不知道哪种实现性能更好 + +### JMH 概念 + +- `Iteration` - iteration 是 JMH 进行测试的最小单位,包含一组 invocations。 +- `Invocation` - 一次 benchmark 方法调用。 +- `Operation` - benchmark 方法中,被测量操作的执行。如果被测试的操作在 benchmark 方法中循环执行,可以使用`@OperationsPerInvocation`表明循环次数,使测试结果为单次 operation 的性能。 +- `Warmup` - 在实际进行 benchmark 前先进行预热。因为某个函数被调用多次之后,JIT 会对其进行编译,通过预热可以使测量结果更加接近真实情况。 + +## JMH 快速入门 + +### 添加 maven 依赖 + +```xml + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + provided + +``` + +### 测试代码 + +```java +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.runner.*; + +import java.util.concurrent.TimeUnit; + +@BenchmarkMode(Mode.Throughput) +@Warmup(iterations = 3) +@Measurement(iterations = 10, time = 5, timeUnit = TimeUnit.SECONDS) +@Threads(8) +@Fork(2) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +public class StringBuilderBenchmark { + + @Benchmark + public void testStringAdd() { + String a = ""; + for (int i = 0; i < 10; i++) { + a += i; + } + // System.out.println(a); + } + + @Benchmark + public void testStringBuilderAdd() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; i++) { + sb.append(i); + } + // System.out.println(sb.toString()); + } + + public static void main(String[] args) throws RunnerException { + Options options = new OptionsBuilder() + .include(StringBuilderBenchmark.class.getSimpleName()) + .output("d:/Benchmark.log") + .build(); + new Runner(options).run(); + } + +} +``` + +### 执行 JMH + +#### 命令行 + +(1)初始化 **benchmarking** 工程 + +```shell +$ mvn archetype:generate \ + -DinteractiveMode=false \ + -DarchetypeGroupId=org.openjdk.jmh \ + -DarchetypeArtifactId=jmh-java-benchmark-archetype \ + -DgroupId=org.sample \ + -DartifactId=test \ + -Dversion=1.0 +``` + +(2)构建 benchmark + +```shell +cd test/ +mvn clean install +``` + +(3)运行 benchmark + +```shell +java -jar target/benchmarks.jar +``` + +#### 执行 main 方法 + +执行 main 方法,耐心等待测试结果,最终会生成一个测试报告,内容大致如下; + +```shell +# JMH version: 1.22 +# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13 +# VM invoker: C:\Program Files\Java\jdk1.8.0_181\jre\bin\java.exe +# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.2.3\lib\idea_rt.jar=58635:D:\Program Files\JetBrains\IntelliJ IDEA 2019.2.3\bin -Dfile.encoding=UTF-8 +# Warmup: 3 iterations, 10 s each +# Measurement: 10 iterations, 5 s each +# Timeout: 10 min per iteration +# Threads: 8 threads, will synchronize iterations +# Benchmark mode: Throughput, ops/time +# Benchmark: io.github.dunwu.javatech.jmh.StringBuilderBenchmark.testStringAdd + +# Run progress: 0.00% complete, ETA 00:05:20 +# Fork: 1 of 2 +# Warmup Iteration 1: 21803.050 ops/ms +# Warmup Iteration 2: 22501.860 ops/ms +# Warmup Iteration 3: 20953.944 ops/ms +Iteration 1: 21627.645 ops/ms +Iteration 2: 21215.269 ops/ms +Iteration 3: 20863.282 ops/ms +Iteration 4: 21617.715 ops/ms +Iteration 5: 21695.645 ops/ms +Iteration 6: 21886.784 ops/ms +Iteration 7: 21986.899 ops/ms +Iteration 8: 22389.540 ops/ms +Iteration 9: 22507.313 ops/ms +Iteration 10: 22124.133 ops/ms + +# Run progress: 25.00% complete, ETA 00:04:02 +# Fork: 2 of 2 +# Warmup Iteration 1: 22262.108 ops/ms +# Warmup Iteration 2: 21567.804 ops/ms +# Warmup Iteration 3: 21787.002 ops/ms +Iteration 1: 21598.970 ops/ms +Iteration 2: 22486.133 ops/ms +Iteration 3: 22157.834 ops/ms +Iteration 4: 22321.827 ops/ms +Iteration 5: 22477.063 ops/ms +Iteration 6: 22154.760 ops/ms +Iteration 7: 21561.095 ops/ms +Iteration 8: 22194.863 ops/ms +Iteration 9: 22493.844 ops/ms +Iteration 10: 22568.078 ops/ms + + +Result "io.github.dunwu.javatech.jmh.StringBuilderBenchmark.testStringAdd": + 21996.435 ±(99.9%) 412.955 ops/ms [Average] + (min, avg, max) = (20863.282, 21996.435, 22568.078), stdev = 475.560 + CI (99.9%): [21583.480, 22409.390] (assumes normal distribution) + + +# JMH version: 1.22 +# VM version: JDK 1.8.0_181, Java HotSpot(TM) 64-Bit Server VM, 25.181-b13 +# VM invoker: C:\Program Files\Java\jdk1.8.0_181\jre\bin\java.exe +# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2019.2.3\lib\idea_rt.jar=58635:D:\Program Files\JetBrains\IntelliJ IDEA 2019.2.3\bin -Dfile.encoding=UTF-8 +# Warmup: 3 iterations, 10 s each +# Measurement: 10 iterations, 5 s each +# Timeout: 10 min per iteration +# Threads: 8 threads, will synchronize iterations +# Benchmark mode: Throughput, ops/time +# Benchmark: io.github.dunwu.javatech.jmh.StringBuilderBenchmark.testStringBuilderAdd + +# Run progress: 50.00% complete, ETA 00:02:41 +# Fork: 1 of 2 +# Warmup Iteration 1: 241500.886 ops/ms +# Warmup Iteration 2: 134206.032 ops/ms +# Warmup Iteration 3: 86907.846 ops/ms +Iteration 1: 86143.339 ops/ms +Iteration 2: 74725.356 ops/ms +Iteration 3: 72316.121 ops/ms +Iteration 4: 77319.716 ops/ms +Iteration 5: 83469.256 ops/ms +Iteration 6: 87712.360 ops/ms +Iteration 7: 79421.899 ops/ms +Iteration 8: 80867.839 ops/ms +Iteration 9: 82619.163 ops/ms +Iteration 10: 87026.928 ops/ms + +# Run progress: 75.00% complete, ETA 00:01:20 +# Fork: 2 of 2 +# Warmup Iteration 1: 228342.337 ops/ms +# Warmup Iteration 2: 124737.248 ops/ms +# Warmup Iteration 3: 82598.851 ops/ms +Iteration 1: 86877.318 ops/ms +Iteration 2: 89388.624 ops/ms +Iteration 3: 88523.558 ops/ms +Iteration 4: 87547.332 ops/ms +Iteration 5: 88376.087 ops/ms +Iteration 6: 88848.837 ops/ms +Iteration 7: 85998.124 ops/ms +Iteration 8: 86796.998 ops/ms +Iteration 9: 87994.726 ops/ms +Iteration 10: 87784.453 ops/ms + + +Result "io.github.dunwu.javatech.jmh.StringBuilderBenchmark.testStringBuilderAdd": + 84487.902 ±(99.9%) 4355.525 ops/ms [Average] + (min, avg, max) = (72316.121, 84487.902, 89388.624), stdev = 5015.829 + CI (99.9%): [80132.377, 88843.427] (assumes normal distribution) + + +# Run complete. Total time: 00:05:23 + +REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on +why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial +experiments, perform baseline and negative tests that provide experimental control, make sure +the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts. +Do not assume the numbers tell you what you want them to tell. + +Benchmark Mode Cnt Score Error Units +StringBuilderBenchmark.testStringAdd thrpt 20 21996.435 ± 412.955 ops/ms +StringBuilderBenchmark.testStringBuilderAdd thrpt 20 84487.902 ± 4355.525 ops/ms +``` + +## JMH API + +下面来了解一下 jmh 常用 API + +### @BenchmarkMode + +基准测试类型。这里选择的是 `Throughput` 也就是吞吐量。根据源码点进去,每种类型后面都有对应的解释,比较好理解,吞吐量会得到单位时间内可以进行的操作数。 + +- `Throughput` - 整体吞吐量,例如“1 秒内可以执行多少次调用”。 +- `AverageTime` - 调用的平均时间,例如“每次调用平均耗时 xxx 毫秒”。 +- `SampleTime` - 随机取样,最后输出取样结果的分布,例如“99%的调用在 xxx 毫秒以内,99.99%的调用在 xxx 毫秒以内” +- `SingleShotTime` - 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为 0,用于测试冷启动时的性能。 +- `All` - 所有模式 + +### @Warmup + +上面我们提到了,进行基准测试前需要进行预热。一般我们前几次进行程序测试的时候都会比较慢, 所以要让程序进行几轮预热,保证测试的准确性。其中的参数 iterations 也就非常好理解了,就是预热轮数。 + +为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。 + +### @Measurement + +度量,其实就是一些基本的测试参数。 + +- `iterations` - 进行测试的轮次 +- `time` - 每轮进行的时长 +- `timeUnit` - 时长单位 + +都是一些基本的参数,可以根据具体情况调整。一般比较重的东西可以进行大量的测试,放到服务器上运行。 + +### @Threads + +每个进程中的测试线程,这个非常好理解,根据具体情况选择,一般为 cpu 乘以 2。 + +### @Fork + +进行 fork 的次数。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。 + +### @OutputTimeUnit + +这个比较简单了,基准测试结果的时间类型。一般选择秒、毫秒、微秒。 + +### @Benchmark + +方法级注解,表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。 + +### @Param + +属性级注解,@Param 可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。 + +### @Setup + +方法级注解,这个注解的作用就是我们需要在测试之前进行一些准备工作,比如对一些数据的初始化之类的。 + +### @TearDown + +方法级注解,这个注解的作用就是我们需要在测试之后进行一些结束工作,比如关闭线程池,数据库连接等的,主要用于资源的回收等。 + +### @State + +当使用 @Setup 参数的时候,必须在类上加这个参数,不然会提示无法运行。 + +State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。 因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为三种。 + +- `Thread` - 该状态为每个线程独享。 +- `Group` - 该状态为同一个组里面所有线程共享。 +- `Benchmark` - 该状态在所有线程间共享。 + +关于 State 的用法,官方的 code sample 里有比较好的[例子](http://hg.openjdk.java.net/code-tools/jmh/file/cb9aa824b55a/jmh-samples/src/main/java/org/openjdk/jmh/samples/JMHSample_03_States.java)。 + +## 参考资料 + +- [jmh 官方示例](http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/) +- [Java 微基准测试框架 JMH](https://www.xncoding.com/2018/01/07/java/jmh.html) +- [JAVA 拾遗 — JMH 与 8 个测试陷阱](https://www.cnkirito.moe/java-jmh/) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/README.md" "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/README.md" new file mode 100644 index 00000000..ab247b86 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/README.md" @@ -0,0 +1,23 @@ +--- +title: Java 测试 +date: 2022-02-17 22:34:30 +categories: + - Java + - 工具 + - 测试 +tags: + - Java + - 测试 +permalink: /pages/2cecc3/ +hidden: true +index: false +--- + +# Java 测试 + +## 内容 + +- [Junit](01.Junit.md) +- [Mockito](02.Mockito.md) +- [Jmeter](03.Jmeter.md) +- [JMH](04.JMH.md) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.Java\346\227\245\345\277\227.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.Java\346\227\245\345\277\227.md" new file mode 100644 index 00000000..efdc23d6 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.Java\346\227\245\345\277\227.md" @@ -0,0 +1,745 @@ +--- +title: javalib-log +date: 2022-02-17 22:34:30 +order: 01 +categories: + - Java + - 工具 + - 其他 +tags: + - Java + - 日志 +permalink: /pages/fcc1c4/ +--- + +# 细说 Java 主流日志工具库 + +> 在项目开发中,为了跟踪代码的运行情况,常常要使用日志来记录信息。 +> +> 在 Java 世界,有很多的日志工具库来实现日志功能,避免了我们重复造轮子。 +> +> 我们先来逐一了解一下主流日志工具。 + +## 日志框架 + +### java.util.logging (JUL) + +JDK1.4 开始,通过 `java.util.logging` 提供日志功能。 + +它能满足基本的日志需要,但是功能没有 Log4j 强大,而且使用范围也没有 Log4j 广泛。 + +### Log4j + +Log4j 是 apache 的一个开源项目,创始人 Ceki Gulcu。 + +Log4j 应该说是 Java 领域资格最老,应用最广的日志工具。从诞生之日到现在一直广受业界欢迎。 + +Log4j 是高度可配置的,并可通过在运行时的外部文件配置。它根据记录的优先级别,并提供机制,以指示记录信息到许多的目的地,诸如:数据库,文件,控制台,UNIX 系统日志等。 + +Log4j 中有三个主要组成部分: + +- **loggers** - 负责捕获记录信息。 +- **appenders** - 负责发布日志信息,以不同的首选目的地。 +- **layouts** - 负责格式化不同风格的日志信息。 + +[官网地址](http://logging.apache.org/log4j/2.x/) + +### Logback + +Logback 是由 log4j 创始人 Ceki Gulcu 设计的又一个开源日记组件,目标是替代 log4j。 + +logback 当前分成三个模块:`logback-core`、`logback-classic` 和 `logback-access`。 + +- `logback-core` - 是其它两个模块的基础模块。 +- `logback-classic` - 是 log4j 的一个 改良版本。此外 `logback-classic` 完整实现 SLF4J API 使你可以很方便地更换成其它日记系统如 log4j 或 JDK14 Logging。 +- `logback-access` - 访问模块与 Servlet 容器集成提供通过 Http 来访问日记的功能。 + +[官网地址](http://logback.qos.ch/) + +### Log4j2 + +[官网地址](http://logging.apache.org/log4j/2.x/) + +按照官方的说法,Log4j2 是 Log4j 和 Logback 的替代。 + +Log4j2 架构: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javalib/log/log4j2-architecture.jpg) + +### Log4j vs Logback vs Log4j2 + +按照官方的说法,**Log4j2 大大优于 Log4j 和 Logback。** + +那么,Log4j2 相比于先问世的 Log4j 和 Logback,它具有哪些优势呢? + +1. Log4j2 旨在用作审计日志记录框架。 Log4j 1.x 和 Logback 都会在重新配置时丢失事件。 Log4j 2 不会。在 Logback 中,Appender 中的异常永远不会对应用程序可见。在 Log4j 中,可以将 Appender 配置为允许异常渗透到应用程序。 +2. Log4j2 在多线程场景中,[异步 Loggers](https://logging.apache.org/log4j/2.x/manual/async.html) 的吞吐量比 Log4j 1.x 和 Logback 高 10 倍,延迟低几个数量级。 +3. Log4j2 对于独立应用程序是无垃圾的,对于稳定状态日志记录期间的 Web 应用程序来说是低垃圾。这减少了垃圾收集器的压力,并且可以提供更好的响应时间性能。 +4. Log4j2 使用插件系统,通过添加新的 Appender、Filter、Layout、Lookup 和 Pattern Converter,可以非常轻松地扩展框架,而无需对 Log4j 进行任何更改。 +5. 由于插件系统配置更简单。配置中的条目不需要指定类名。 +6. 支持[自定义日志等级](https://logging.apache.org/log4j/2.x/manual/customloglevels.html)。 +7. 支持 [lambda 表达式](https://logging.apache.org/log4j/2.x/manual/api.html#LambdaSupport)。 +8. 支持[消息对象](https://logging.apache.org/log4j/2.x/manual/messages.html)。 +9. Log4j 和 Logback 的 Layout 返回的是字符串,而 Log4j2 返回的是二进制数组,这使得它能被各种 Appender 使用。 +10. Syslog Appender 支持 TCP 和 UDP 并且支持 BSD 系统日志。 +11. Log4j2 利用 Java5 并发特性,尽量小粒度的使用锁,减少锁的开销。 + +## 日志门面 + +何谓日志门面? + +日志门面是对不同日志框架提供的一个 API 封装,可以在部署的时候不修改任何配置即可接入一种日志实现方案。 + +### common-logging + +common-logging 是 apache 的一个开源项目。也称**Jakarta Commons Logging,缩写 JCL**。 + +common-logging 的功能是提供日志功能的 API 接口,本身并不提供日志的具体实现(当然,common-logging 内部有一个 Simple logger 的简单实现,但是功能很弱,直接忽略),而是在**运行时**动态的绑定日志实现组件来工作(如 log4j、java.util.loggin)。 + +[官网地址](http://commons.apache.org/proper/commons-logging/) + +### slf4j + +全称为 Simple Logging Facade for Java,即 java 简单日志门面。 + +什么,作者又是 Ceki Gulcu!这位大神写了 Log4j、Logback 和 slf4j,专注日志组件开发五百年,一直只能超越自己。 + +类似于 Common-Logging,slf4j 是对不同日志框架提供的一个 API 封装,可以在部署的时候不修改任何配置即可接入一种日志实现方案。但是,slf4j 在**编译时**静态绑定真正的 Log 库。使用 SLF4J 时,如果你需要使用某一种日志实现,那么你必须选择正确的 SLF4J 的 jar 包的集合(各种桥接包)。 + +[官网地址](http://www.slf4j.org/) + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javalib/log/slf4j-to-other-log.png) + +### common-logging vs slf4j + +slf4j 库类似于 Apache Common-Logging。但是,他在编译时静态绑定真正的日志库。这点似乎很麻烦,其实也不过是导入桥接 jar 包而已。 + +slf4j 一大亮点是提供了更方便的日志记录方式: + +不需要使用`logger.isDebugEnabled()`来解决日志因为字符拼接产生的性能问题。slf4j 的方式是使用`{}`作为字符串替换符,形式如下: + +```java +logger.debug("id: {}, name: {} ", id, name); +``` + +### 总结 + +综上所述,使用 slf4j + Logback 可谓是目前最理想的日志解决方案了。 + +接下来,就是如何在项目中实施了。 + +## 实施日志解决方案 + +使用日志解决方案基本可分为三步: + +1. 引入 jar 包 +2. 配置 +3. 使用 API + +常见的各种日志解决方案的第 2 步和第 3 步基本一样,实施上的差别主要在第 1 步,也就是使用不同的库。 + +### 引入 jar 包 + +这里首选推荐使用 slf4j + logback 的组合。 + +如果你习惯了 common-logging,可以选择 common-logging+log4j。 + +强烈建议不要直接使用日志实现组件(logback、log4j、java.util.logging),理由前面也说过,就是无法灵活替换日志库。 + +还有一种情况:你的老项目使用了 common-logging,或是直接使用日志实现组件。如果修改老的代码,工作量太大,需要兼容处理。在下文,都将看到各种应对方法。 + +**_注:据我所知,当前仍没有方法可以将 slf4j 桥接到 common-logging。如果我孤陋寡闻了,请不吝赐教。_** + +#### slf4j 直接绑定日志组件 + +**slf4j + logback** + +添加依赖到 pom.xml 中即可。 + +_logback-classic-1.0.13.jar_ 会自动将 _slf4j-api-1.7.21.jar_ 和 _logback-core-1.0.13.jar_ 也添加到你的项目中。 + +```xml + + ch.qos.logback + logback-classic + 1.0.13 + +``` + +**slf4j + log4j** + +添加依赖到 pom.xml 中即可。 + +_slf4j-log4j12-1.7.21.jar_ 会自动将 _slf4j-api-1.7.21.jar_ 和 _log4j-1.2.17.jar_ 也添加到你的项目中。 + +```xml + + org.slf4j + slf4j-log4j12 + 1.7.21 + +``` + +**slf4j + java.util.logging** + +添加依赖到 pom.xml 中即可。 + +_slf4j-jdk14-1.7.21.jar_ 会自动将 _slf4j-api-1.7.21.jar_ 也添加到你的项目中。 + +```xml + + org.slf4j + slf4j-jdk14 + 1.7.21 + +``` + +#### slf4j 兼容非 slf4j 日志组件 + +在介绍解决方案前,先提一个概念——桥接 + +**什么是桥接呢** + +假如你正在开发应用程序所调用的组件当中已经使用了 common-logging,这时你需要 jcl-over-slf4j.jar 把日志信息输出重定向到 slf4j-api,slf4j-api 再去调用 slf4j 实际依赖的日志组件。这个过程称为桥接。下图是官方的 slf4j 桥接策略图: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javalib/log/slf4j-bind-strategy.png) + +从图中应该可以看出,无论你的老项目中使用的是 common-logging 或是直接使用 log4j、java.util.logging,都可以使用对应的桥接 jar 包来解决兼容问题。 + +**slf4j 兼容 common-logging** + +```xml + + org.slf4j + jcl-over-slf4j + 1.7.12 + +``` + +**slf4j 兼容 log4j** + +```xml + + org.slf4j + log4j-over-slf4j + 1.7.12 + +``` + +**slf4j 兼容 java.util.logging** + +```xml + + org.slf4j + jul-to-slf4j + 1.7.12 + +``` + +#### spring 集成 slf4j + +做 java web 开发,基本离不开 spring 框架。很遗憾,spring 使用的日志解决方案是 common-logging + log4j。 + +所以,你需要一个桥接 jar 包:_logback-ext-spring_。 + +```xml + + ch.qos.logback + logback-classic + 1.1.3 + + + org.logback-extensions + logback-ext-spring + 0.1.2 + + + org.slf4j + jcl-over-slf4j + 1.7.12 + +``` + +#### common-logging 绑定日志组件 + +**common-logging + log4j** + +添加依赖到 pom.xml 中即可。 + +```xml + + commons-logging + commons-logging + 1.2 + + + log4j + log4j + 1.2.17 + +``` + +### 使用 API + +#### slf4j 用法 + +使用 slf4j 的 API 很简单。使用`LoggerFactory`初始化一个`Logger`实例,然后调用 Logger 对应的打印等级函数就行了。 + +```java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class App { + private static final Logger log = LoggerFactory.getLogger(App.class); + public static void main(String[] args) { + String msg = "print log, current level: {}"; + log.trace(msg, "trace"); + log.debug(msg, "debug"); + log.info(msg, "info"); + log.warn(msg, "warn"); + log.error(msg, "error"); + } +} +``` + +#### common-logging 用法 + +common-logging 用法和 slf4j 几乎一样,但是支持的打印等级多了一个更高级别的:**fatal**。 + +此外,common-logging 不支持`{}`替换参数,你只能选择拼接字符串这种方式了。 + +```java +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +public class JclTest { + private static final Log log = LogFactory.getLog(JclTest.class); + + public static void main(String[] args) { + String msg = "print log, current level: "; + log.trace(msg + "trace"); + log.debug(msg + "debug"); + log.info(msg + "info"); + log.warn(msg + "warn"); + log.error(msg + "error"); + log.fatal(msg + "fatal"); + } +} +``` + +## log4j2 配置 + +log4j2 基本配置形式如下: + +```xml +; + + + value + + + + + + + + ... + + + + + + ... + + + + + +``` + +配置示例: + +```xml + + + + target/test.log + + + + + + + + + + + + + + + + + + + + + %d %p %C{1.} [%t] %m%n + + + + + + + + + + + + + + + + + + + + + + +``` + +## logback 配置 + +### `` + +- 作用:`` 是 logback 配置文件的根元素。 +- 要点 + - 它有 ``、``、`` 三个子元素。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javalib/log/logback-configuration.png) + +### `` + +- 作用:将记录日志的任务委托给名为 appender 的组件。 +- 要点 + - 可以配置零个或多个。 + - 它有 ``、``、``、`` 四个子元素。 +- 属性 + - name:设置 appender 名称。 + - class:设置具体的实例化类。 + +#### `` + +- 作用:设置日志文件路径。 + +#### `` + +- 作用:设置过滤器。 +- 要点 + - 可以配置零个或多个。 + +#### `` + +- 作用:设置 appender。 +- 要点 + - 可以配置零个或一个。 +- 属性 + - class:设置具体的实例化类。 + +#### `` + +- 作用:设置编码。 +- 要点 + - 可以配置零个或多个。 +- 属性 + - class:设置具体的实例化类。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javalib/log/logback-appender.png) + +### `` + +- 作用:设置 logger。 +- 要点 + - 可以配置零个或多个。 +- 属性 + - name + - level:设置日志级别。不区分大小写。可选值:TRACE、DEBUG、INFO、WARN、ERROR、ALL、OFF。 + - additivity:可选值:true 或 false。 + +#### `` + +- 作用:appender 引用。 +- 要点 + - 可以配置零个或多个。 + +### `` + +- 作用:设置根 logger。 +- 要点 + - 只能配置一个。 + - 除了 level,不支持任何属性。level 属性和 `` 中的相同。 + - 有一个子元素 ``,与 `` 中的相同。 + +### 完整的 logback.xml 参考示例 + +在下面的配置文件中,我为自己的项目代码(根目录:org.zp.notes.spring)设置了五种等级: + +TRACE、DEBUG、INFO、WARN、ERROR,优先级依次从低到高。 + +因为关注 spring 框架本身的一些信息,我增加了专门打印 spring WARN 及以上等级的日志。 + +```xml + + + + + + + + + + + %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n + + + + + + + + ${user.dir}/logs/${DIR_NAME}/all.%d{yyyy-MM-dd}.log + 30 + + + + + 30MB + + + + %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n + + + + + + + ${user.dir}/logs/${DIR_NAME}/error.%d{yyyy-MM-dd}.log + 30 + + + + + 10MB + + + + ERROR + ACCEPT + DENY + + + + %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n + + + + + + + ${user.dir}/logs/${DIR_NAME}/warn.%d{yyyy-MM-dd}.log + 30 + + + + + 10MB + + + + WARN + ACCEPT + DENY + + + + %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n + + + + + + + ${user.dir}/logs/${DIR_NAME}/info.%d{yyyy-MM-dd}.log + 30 + + + + + 10MB + + + + INFO + ACCEPT + DENY + + + + %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n + + + + + + + ${user.dir}/logs/${DIR_NAME}/debug.%d{yyyy-MM-dd}.log + 30 + + + + + 10MB + + + + DEBUG + ACCEPT + DENY + + + + %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n + + + + + + + ${user.dir}/logs/${DIR_NAME}/trace.%d{yyyy-MM-dd}.log + 30 + + + + + 10MB + + + + TRACE + ACCEPT + DENY + + + + %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n + + + + + + + ${user.dir}/logs/${DIR_NAME}/springframework.%d{yyyy-MM-dd}.log + + 30 + + + + + 10MB + + + + %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +## log4j 配置 + +### 完整的 log4j.xml 参考示例 + +log4j 的配置文件一般有 xml 格式或 properties 格式。这里为了和 logback.xml 做个对比,就不介绍 properties 了,其实也没太大差别。 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +## 参考 + +- [slf4 官方文档](http://www.slf4j.org/manual.html) +- [logback 官方文档](http://logback.qos.ch/) +- [log4j 官方文档](http://logging.apache.org/log4j/1.2/) +- [commons-logging 官方文档](http://commons.apache.org/proper/commons-logging/) +- \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/02.Java\345\267\245\345\205\267\345\214\205.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/02.Java\345\267\245\345\205\267\345\214\205.md" new file mode 100644 index 00000000..192f3c42 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/02.Java\345\267\245\345\205\267\345\214\205.md" @@ -0,0 +1,21 @@ +--- +title: javalib-util +date: 2022-02-17 22:34:30 +order: 02 +categories: + - Java + - 工具 + - 其他 +tags: + - Java + - 工具包 +permalink: /pages/27ad42/ +--- + +# 细说 Java 主流工具包 + +- apache.commons + - [commons-lang](https://github.com/apache/commons-lang) + - [commons-collections](https://github.com/apache/commons-collections) + - [common-io](https://github.com/apache/commons-io) +- [guava](https://github.com/google/guava) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/03.Reflections.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/03.Reflections.md" new file mode 100644 index 00000000..6200ebea --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/03.Reflections.md" @@ -0,0 +1,108 @@ +--- +title: Reflections 快速入门 +date: 2022-02-17 22:34:30 +order: 03 +categories: + - Java + - 工具 + - 其他 +tags: + - Java + - 反射 + - Reflections +permalink: /pages/ce6195/ +--- + +# Reflections 快速入门 + +引入 pom + +```xml + + org.reflections + reflections + 0.9.11 + +``` + +典型应用 + +```java +Reflections reflections = new Reflections("my.project"); +Set> subTypes = reflections.getSubTypesOf(SomeType.class); +Set> annotated = reflections.getTypesAnnotatedWith(SomeAnnotation.class); +``` + +## 使用 + +基本上,使用 Reflections 首先使用 urls 和 scanners 对其进行实例化 + +```java +//scan urls that contain 'my.package', include inputs starting with 'my.package', use the default scanners +Reflections reflections = new Reflections("my.package"); + +//or using ConfigurationBuilder +new Reflections(new ConfigurationBuilder() + .setUrls(ClasspathHelper.forPackage("my.project.prefix")) + .setScanners(new SubTypesScanner(), + new TypeAnnotationsScanner().filterResultsBy(optionalFilter), ...), + .filterInputsBy(new FilterBuilder().includePackage("my.project.prefix")) + ...); +``` + +然后,使用方便的查询方法 + +```java +// 子类型扫描 +Set> modules = + reflections.getSubTypesOf(com.google.inject.Module.class); +// 类型注解扫描 +Set> singletons = + reflections.getTypesAnnotatedWith(javax.inject.Singleton.class); +// 资源扫描 +Set properties = + reflections.getResources(Pattern.compile(".*\\.properties")); +// 方法注解扫描 +Set resources = + reflections.getMethodsAnnotatedWith(javax.ws.rs.Path.class); +Set injectables = + reflections.getConstructorsAnnotatedWith(javax.inject.Inject.class); +// 字段注解扫描 +Set ids = + reflections.getFieldsAnnotatedWith(javax.persistence.Id.class); +// 方法参数扫描 +Set someMethods = + reflections.getMethodsMatchParams(long.class, int.class); +Set voidMethods = + reflections.getMethodsReturn(void.class); +Set pathParamMethods = + reflections.getMethodsWithAnyParamAnnotated(PathParam.class); +// 方法参数名扫描 +List parameterNames = + reflections.getMethodParamNames(Method.class) +// 方法使用扫描 +Set usages = + reflections.getMethodUsages(Method.class) +``` + +说明: + +- 如果未配置扫描程序,则将使用默认值 - SubTypesScanner 和 TypeAnnotationsScanner。 +- 还可以配置类加载器,它将用于从名称中解析运行时类。 +- Reflection 默认情况下会扩展超类型。 这解决了传输 URL 不被扫描的一些问题。 + +## ReflectionUtils + +```java +import static org.reflections.ReflectionUtils.*; + +Set getters = getAllMethods(someClass, + withModifier(Modifier.PUBLIC), withPrefix("get"), withParametersCount(0)); + +//or +Set listMethodsFromCollectionToBoolean = + getAllMethods(List.class, + withParametersAssignableTo(Collection.class), withReturnType(boolean.class)); + +Set fields = getAllFields(SomeClass.class, withAnnotation(annotation), withTypeAssignableTo(type)); +``` \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/04.JavaMail.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/04.JavaMail.md" new file mode 100644 index 00000000..dc623bbf --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/04.JavaMail.md" @@ -0,0 +1,456 @@ +--- +title: JavaMail 快速入门 +date: 2022-02-17 22:34:30 +order: 04 +categories: + - Java + - 工具 + - 其他 +tags: + - Java + - 邮件 +permalink: /pages/cd38ec/ +--- + +# JavaMail 快速入门 + +## 简介 + +### 邮件相关的标准 + +厂商所提供的 JavaMail 服务程序可以有选择地实现某些邮件协议,常见的邮件协议包括: + +- `SMTP(Simple Mail Transfer Protocol)` :即简单邮件传输协议,它是一组用于由源地址到目的地址传送邮件的规则,由它来控制信件的中转方式。 +- `POP3(Post Office Protocol - Version 3)` :即邮局协议版本 3 ,用于接收电子邮件的标准协议。 +- `IMAP(Internet Mail Access Protocol)` :即 Internet 邮件访问协议。是 POP3 的替代协议。 + +这三种协议都有对应 SSL 加密传输的协议,分别是 **SMTPS**, **POP3S**和 **IMAPS**。 + +`MIME(Multipurpose Internet Mail Extensions)` :即多用途因特网邮件扩展标准。它不是邮件传输协议。但对传输内容的消息、附件及其它的内容定义了格式。 + +### JavaMail 简介 + +JavaMail 是由 Sun 发布的用来处理 email 的 API 。它并没有包含在 Java SE 中,而是作为 Java EE 的一部分。 + +- `mail.jar` :此 JAR 文件包含 JavaMail API 和 Sun 提供的 SMTP 、 IMAP 和 POP3 服务提供程序; +- `activation.jar` :此 JAR 文件包含 JAF API 和 Sun 的实现。 + +JavaMail 包中用于处理电子邮件的核心类是: `Properties` 、 `Session` 、 `Message` 、 `Address` 、 `Authenticator` 、 `Transport` 、 `Store` 等。 + +### 邮件传输过程 + +如上图,电子邮件的处理步骤如下: + +1. 创建一个 Session 对象。 +2. Session 对象创建一个 Transport 对象 /Store 对象,用来发送 / 保存邮件。 +3. Transport 对象 /Store 对象连接邮件服务器。 +4. Transport 对象 /Store 对象创建一个 Message 对象 ( 也就是邮件内容 ) 。 +5. Transport 对象发送邮件; Store 对象获取邮箱的邮件。 + +### Message 结构 + +- `MimeMessage` 类:代表整封邮件。 +- `MimeBodyPart` 类:代表邮件的一个 MIME 信息。 +- `MimeMultipart` 类:代表一个由多个 MIME 信息组合成的组合 MIME 信息。 + +![img](http://upload-images.jianshu.io/upload_images/3101171-948230d2f5c7a620.png) + +## JavaMail 的核心类 + +JavaMail 对收发邮件进行了高级的抽象,形成了一些关键的的接口和类,它们构成了程序的基础,下面我们分别来了解一下这些最常见的对象。 + +### java.util.Properties 类(属性对象) + +java.util.Properties 类代表一组属性集合。 + +它的每一个键和值都是 String **类型。** + +由于 JavaMail 需要和邮件服务器进行通信,这就要求程序提供许多诸如服务器地址、端口、用户名、密码等信息, JavaMail 通过 Properties 对象封装这些属性信息。 + +例: 如下面的代码封装了几个属性信息: + +```java +Properties prop = new Properties(); +prop.setProperty("mail.debug", "true"); +prop.setProperty("mail.host", "[email protected]"); +prop.setProperty("mail.transport.protocol", "smtp"); +prop.setProperty("mail.smtp.auth", "true"); +``` + +针对不同的的邮件协议, JavaMail 规定了服务提供者必须支持一系列属性, + +下表是一些常见属性(属性值都以 String 类型进行设置,属性类型栏仅表示属性是如何被解析的): + +| 关键词 | 类型 | 描述 | +| ----------------------- | ------- | --------------------------------------------- | +| mail.debug | boolean | debug 开关。 | +| mail.host | String | 指定发送、接收邮件的默认邮箱服务器。 | +| mail.store.protocol | String | 指定接收邮件的协议。 | +| mail.transport.protocol | String | 指定发送邮件的协议。 | +| mail.debug.auth | boolean | debug 输出中是否包含认证命令。默认是 false 。 | + +详情请参考官方 API 文档: + + 。 + +### javax.mail.Session 类(会话对象) + +`Session` 表示一个邮件会话。 + +Session 的主要作用包括两个方面: + +- 接收各种配置属性信息:通过 Properties 对象设置的属性信息; +- 初始化 JavaMail 环境:根据 JavaMail 的配置文件,初始化 JavaMail 环境,以便通过 Session 对象创建其他重要类的实例。 + +JavaMail 在 Jar 包的 META-INF 目录下,通过以下文件提供了基本配置信息,以便 session 能够根据这个配置文件加载提供者的实现类: + +- javamail.default.providers +- javamail.default.address.map + +![img](http://upload-images.jianshu.io/upload_images/3101171-b59382c69385df45.png) + +**例:** + +```java +Properties props = new Properties(); +props.setProperty("mail.transport.protocol", "smtp"); +Session session = Session.getInstance(props); +``` + +### javax.mail.Transport 类(邮件传输) + +邮件操作只有发送或接收两种处理方式。 + +JavaMail 将这两种不同操作描述为传输( javax.mail.Transport )和存储( javax.mail.Store ),传输对应邮件的发送,而存储对应邮件的接收。 + +- `getTransport` - Session 类中的 **getTransport()**有多个重载方法,可以用来创建 Transport 对象。 +- `connect` - 如果设置了认证命令—— mail.smtp.auth ,那么使用 Transport 类的 connect 方法连接服务器时,则必须加上用户名和密码。 +- `sendMessage` - Transport 类的 sendMessage 方法用来发送邮件消息。 +- `close` - Transport 类的 close 方法用来关闭和邮件服务器的连接。 + +### javax.mail.Store 类(邮件存储 ) + +- `getStore` - Session 类中的 getStore () 有多个重载方法,可以用来创建 Store 对象。 +- `connect` - 如果设置了认证命令—— mail.smtp.auth ,那么使用 Store 类的 connect 方法连接服务器时,则必须加上用户名和密码。 +- `getFolder` - Store 类的 getFolder 方法可以 获取邮箱内的邮件夹 Folder 对象 +- `close` - Store 类的 close 方法用来关闭和邮件服务器的连接。 + +### javax.mail.Message 类(消息对象) + +- `javax.mail.Message` - 是个抽象类,只能用子类去实例化,多数情况下为 `javax.mail.internet.MimeMessage`。 +- `MimeMessage` - 代表 MIME 类型的电子邮件消息。 + +要创建一个 Message ,需要将 Session 对象传递给 `MimeMessage` 构造器: + +```java +MimeMessage message = new MimeMessage(session); +``` + +注意:还存在其它构造器,如用按 RFC822 格式的输入流来创建消息。 + +- setFrom - 设置邮件的发件人 +- setRecipient - 设置邮件的发送人、抄送人、密送人 + +三种预定义的地址类型是: + +- `Message.RecipientType.TO` - 收件人 +- `Message.RecipientType.CC` - 抄送人 +- `Message.RecipientType.BCC` - 密送人 +- `setSubject` - 设置邮件的主题 +- `setContent` - 设置邮件内容 +- `setText` - 如果邮件内容是纯文本,可以使用此接口设置文本内容。 + +### javax.mail.Address 类(地址) + +一旦您创建了 Session 和 Message ,并将内容填入消息后,就可以用 Address 确定信件地址了。和 Message 一样, Address 也是个抽象类。您用的是 javax.mail.internet.InternetAddress 类。 + +若创建的地址只包含电子邮件地址,只要传递电子邮件地址到构造器就行了。 + +**例:** + +```java +Address address = new InternetAddress("[email protected]"); +``` + +### Authenticator 类(认证者) + +与 java.net 类一样, JavaMail API 也可以利用 `Authenticator` 通过用户名和密码访问受保护的资源。对于 JavaMail API 来说,这些资源就是邮件服务器。`Authenticator` 在 javax.mail 包中,而且它和 java.net 中同名的类 Authenticator 不同。两者并不共享同一个 Authenticator ,因为 JavaMail API 用于 Java 1.1 ,它没有 java.net 类别。 + +要使用 Authenticator ,先创建一个抽象类的子类,并从 `getPasswordAuthentication()` 方法中返回 `PasswordAuthentication` 实例。创建完成后,您必需向 session 注册 `Authenticator` 。然后,在需要认证的时候,就会通知 `Authenticator` 。您可以弹出窗口,也可以从配置文件中(虽然没有加密是不安全的)读取用户名和密码,将它们作为 `PasswordAuthentication` 对象返回给调用程序。 + +**例:** + +```java +Properties props = new Properties(); +Authenticator auth = new MyAuthenticator(); +Session session = Session.getDefaultInstance(props, auth); +``` + +## 实例 + +### 发送文本邮件 + +```java +public static void main(String[] args) throws Exception { + Properties prop = new Properties(); + prop.setProperty("mail.debug", "true"); + prop.setProperty("mail.host", MAIL_SERVER_HOST); + prop.setProperty("mail.transport.protocol", "smtp"); + prop.setProperty("mail.smtp.auth", "true"); + + // 1、创建session + Session session = Session.getInstance(prop); + Transport ts = null; + + // 2、通过session得到transport对象 + ts = session.getTransport(); + + // 3、连上邮件服务器 + ts.connect(MAIL_SERVER_HOST, USER, PASSWORD); + + // 4、创建邮件 + MimeMessage message = new MimeMessage(session); + + // 邮件消息头 + message.setFrom(new InternetAddress(MAIL_FROM)); // 邮件的发件人 + message.setRecipient(Message.RecipientType.TO, new InternetAddress(MAIL_TO)); // 邮件的收件人 + message.setRecipient(Message.RecipientType.CC, new InternetAddress(MAIL_CC)); // 邮件的抄送人 + message.setRecipient(Message.RecipientType.BCC, new InternetAddress(MAIL_BCC)); // 邮件的密送人 + message.setSubject("测试文本邮件"); // 邮件的标题 + + // 邮件消息体 + message.setText("天下无双。"); + + // 5、发送邮件 + ts.sendMessage(message, message.getAllRecipients()); + ts.close(); +} +``` + +### 发送 HTML 格式的邮件 + +```java +public static void main(String[] args) throws Exception { + Properties prop = new Properties(); + prop.setProperty("mail.debug", "true"); + prop.setProperty("mail.host", MAIL_SERVER_HOST); + prop.setProperty("mail.transport.protocol", "smtp"); + prop.setProperty("mail.smtp.auth", "true"); + + // 1、创建session + Session session = Session.getInstance(prop); + Transport ts = null; + + // 2、通过session得到transport对象 + ts = session.getTransport(); + + // 3、连上邮件服务器 + ts.connect(MAIL_SERVER_HOST, USER, PASSWORD); + + // 4、创建邮件 + MimeMessage message = new MimeMessage(session); + + // 邮件消息头 + message.setFrom(new InternetAddress(MAIL_FROM)); // 邮件的发件人 + message.setRecipient(Message.RecipientType.TO, new InternetAddress(MAIL_TO)); // 邮件的收件人 + message.setRecipient(Message.RecipientType.CC, new InternetAddress(MAIL_CC)); // 邮件的抄送人 + message.setRecipient(Message.RecipientType.BCC, new InternetAddress(MAIL_BCC)); // 邮件的密送人 + message.setSubject("测试HTML邮件"); // 邮件的标题 + + String htmlContent = "

    Hello

    " + "

    显示图片1.jpg

    "; + MimeBodyPart text = new MimeBodyPart(); + text.setContent(htmlContent, "text/html;charset=UTF-8"); + MimeBodyPart image = new MimeBodyPart(); + DataHandler dh = new DataHandler(new FileDataSource("D:\\05_Datas\\图库\\吉他少年背影.png")); + image.setDataHandler(dh); + image.setContentID("abc.jpg"); + + // 描述数据关系 + MimeMultipart mm = new MimeMultipart(); + mm.addBodyPart(text); + mm.addBodyPart(image); + mm.setSubType("related"); + message.setContent(mm); + message.saveChanges(); + + // 5、发送邮件 + ts.sendMessage(message, message.getAllRecipients()); + ts.close(); +} +``` + +### 发送带附件的邮件 + +```java +public static void main(String[] args) throws Exception { + Properties prop = new Properties(); + prop.setProperty("mail.debug", "true"); + prop.setProperty("mail.host", MAIL_SERVER_HOST); + prop.setProperty("mail.transport.protocol", "smtp"); + prop.setProperty("mail.smtp.auth", "true"); + + // 1、创建session + Session session = Session.getInstance(prop); + + // 2、通过session得到transport对象 + Transport ts = session.getTransport(); + + // 3、连上邮件服务器 + ts.connect(MAIL_SERVER_HOST, USER, PASSWORD); + + // 4、创建邮件 + MimeMessage message = new MimeMessage(session); + + // 邮件消息头 + message.setFrom(new InternetAddress(MAIL_FROM)); // 邮件的发件人 + message.setRecipient(Message.RecipientType.TO, new InternetAddress(MAIL_TO)); // 邮件的收件人 + message.setRecipient(Message.RecipientType.CC, new InternetAddress(MAIL_CC)); // 邮件的抄送人 + message.setRecipient(Message.RecipientType.BCC, new InternetAddress(MAIL_BCC)); // 邮件的密送人 + message.setSubject("测试带附件邮件"); // 邮件的标题 + + MimeBodyPart text = new MimeBodyPart(); + text.setContent("邮件中有两个附件。", "text/html;charset=UTF-8"); + + // 描述数据关系 + MimeMultipart mm = new MimeMultipart(); + mm.setSubType("related"); + mm.addBodyPart(text); + String[] files = { + "D:\\00_Temp\\temp\\1.jpg", "D:\\00_Temp\\temp\\2.png" + }; + + // 添加邮件附件 + for (String filename : files) { + MimeBodyPart attachPart = new MimeBodyPart(); + attachPart.attachFile(filename); + mm.addBodyPart(attachPart); + } + + message.setContent(mm); + message.saveChanges(); + + // 5、发送邮件 + ts.sendMessage(message, message.getAllRecipients()); + ts.close(); +} +``` + +### 获取邮箱中的邮件 + +```java + public static void main(String[] args) throws Exception { + + // 创建一个有具体连接信息的Properties对象 + Properties prop = new Properties(); + prop.setProperty("mail.debug", "true"); + prop.setProperty("mail.store.protocol", "pop3"); + prop.setProperty("mail.pop3.host", MAIL_SERVER_HOST); + + // 1、创建session + Session session = Session.getInstance(prop); + + // 2、通过session得到Store对象 + Store store = session.getStore(); + + // 3、连上邮件服务器 + store.connect(MAIL_SERVER_HOST, USER, PASSWORD); + + // 4、获得邮箱内的邮件夹 + Folder folder = store.getFolder("inbox"); + folder.open(Folder.READ_ONLY); + + // 获得邮件夹Folder内的所有邮件Message对象 + Message[] messages = folder.getMessages(); + for (int i = 0; i < messages.length; i++) { + String subject = messages[i].getSubject(); + String from = (messages[i].getFrom()[0]).toString(); + System.out.println("第 " + (i + 1) + "封邮件的主题:" + subject); + System.out.println("第 " + (i + 1) + "封邮件的发件人地址:" + from); + } + + // 5、关闭 + folder.close(false); + store.close(); +} +``` + +### 转发邮件 + +例:获取指定邮件夹下的第一封邮件并转发 + +```java + public static void main(String[] args) throws Exception { + Properties prop = new Properties(); + prop.put("mail.store.protocol", "pop3"); + prop.put("mail.pop3.host", MAIL_SERVER_POP3); + prop.put("mail.pop3.starttls.enable", "true"); + prop.put("mail.smtp.auth", "true"); + prop.put("mail.smtp.host", MAIL_SERVER_SMTP); + + // 1、创建session + Session session = Session.getDefaultInstance(prop); + + // 2、读取邮件夹 + Store store = session.getStore("pop3"); + store.connect(MAIL_SERVER_POP3, USER, PASSWORD); + Folder folder = store.getFolder("inbox"); + folder.open(Folder.READ_ONLY); + + // 获取邮件夹中第1封邮件信息 + Message[] messages = folder.getMessages(); + if (messages.length <= 0) { + return; + } + Message message = messages[0]; + + // 打印邮件关键信息 + String from = InternetAddress.toString(message.getFrom()); + if (from != null) { + System.out.println("From: " + from); + } + + String replyTo = InternetAddress.toString(message.getReplyTo()); + if (replyTo != null) { + System.out.println("Reply-to: " + replyTo); + } + + String to = InternetAddress.toString(message.getRecipients(Message.RecipientType.TO)); + if (to != null) { + System.out.println("To: " + to); + } + + String subject = message.getSubject(); + if (subject != null) { + System.out.println("Subject: " + subject); + } + + Date sent = message.getSentDate(); + if (sent != null) { + System.out.println("Sent: " + sent); + } + + // 设置转发邮件信息头 + Message forward = new MimeMessage(session); + forward.setFrom(new InternetAddress(MAIL_FROM)); + forward.setRecipient(Message.RecipientType.TO, new InternetAddress(MAIL_TO)); + forward.setSubject("Fwd: " + message.getSubject()); + + // 设置转发邮件内容 + MimeBodyPart bodyPart = new MimeBodyPart(); + bodyPart.setContent(message, "message/rfc822"); + + Multipart multipart = new MimeMultipart(); + multipart.addBodyPart(bodyPart); + forward.setContent(multipart); + forward.saveChanges(); + + Transport ts = session.getTransport("smtp"); + ts.connect(USER, PASSWORD); + ts.sendMessage(forward, forward.getAllRecipients()); + + folder.close(false); + store.close(); + ts.close(); + System.out.println("message forwarded successfully...."); +} +``` \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/05.Jsoup.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/05.Jsoup.md" new file mode 100644 index 00000000..04f0cbd5 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/05.Jsoup.md" @@ -0,0 +1,450 @@ +--- +title: Jsoup 快速入门 +date: 2022-02-17 22:34:30 +order: 05 +categories: + - Java + - 工具 + - 其他 +tags: + - Java + - Html + - Jsoup +permalink: /pages/5dd78d/ +--- + +# Jsoup 快速入门 + +## 简介 + +jsoup 是一款 Java 的 HTML 解析器,可直接解析某个 URL 地址、HTML 文本内容。它提供了一套非常省力的 API,可通过 DOM,CSS 以及类似于 JQuery 的操作方法来取出和操作数据。 + +jsoup 工作的流程主要如下: + +1. 从一个 URL,文件或字符串中解析 HTML,并加载为一个 `Document` 对象。 +2. 使用 DOM 或 CSS 选择器来取出数据; +3. 可操作 HTML 元素、属性、文本。 + +jsoup 是基于 MIT 协议发布的,可放心使用于商业项目。 + +## 加载 + +### 从 HTML 字符串加载一个文档 + +使用静态 `Jsoup.parse(String html)` 方法或 `Jsoup.parse(String html, String baseUri)` 示例代码: + +```java +String html = "First parse" + + "

    Parsed HTML into a doc.

    "; +Document doc = Jsoup.parse(html); +``` + +> **说明** +> +> `parse(String html, String baseUri)` 这方法能够将输入的 HTML 解析为一个新的文档 (Document),参数 baseUri 是用来将相对 URL 转成绝对 URL,并指定从哪个网站获取文档。如这个方法不适用,你可以使用 `parse(String html)` 方法来解析成 HTML 字符串如上面的示例。 +> +> 只要解析的不是空字符串,就能返回一个结构合理的文档,其中包含(至少) 一个 head 和一个 body 元素。 +> +> 一旦拥有了一个 Document,你就可以使用 Document 中适当的方法或它父类 `Element`和`Node`中的方法来取得相关数据。 + +### 解析一个 body 片断 + +**问题** + +假如你有一个 HTML 片断 (比如. 一个 `div` 包含一对 `p` 标签; 一个不完整的 HTML 文档) 想对它进行解析。这个 HTML 片断可以是用户提交的一条评论或在一个 CMS 页面中编辑 body 部分。 + +**办法** + +使用`Jsoup.parseBodyFragment(String html)`方法. + +``` +String html = "

    Lorem ipsum.

    "; +Document doc = Jsoup.parseBodyFragment(html); +Element body = doc.body(); +``` + +> **说明** +> +> `parseBodyFragment` 方法创建一个空壳的文档,并插入解析过的 HTML 到`body`元素中。假如你使用正常的 `Jsoup.parse(String html)` 方法,通常你也可以得到相同的结果,但是明确将用户输入作为 body 片段处理,以确保用户所提供的任何糟糕的 HTML 都将被解析成 body 元素。 +> +> `Document.body()` 方法能够取得文档 body 元素的所有子元素,与 `doc.getElementsByTag("body")`相同。 + +#### 保证安全 Stay safe + +假如你可以让用户输入 HTML 内容,那么要小心避免跨站脚本攻击。利用基于 `Whitelist` 的清除器和 `clean(String bodyHtml, Whitelist whitelist)`方法来清除用户输入的恶意内容。 + +### 从 URL 加载一个文档 + +使用 `Jsoup.connect(String url)`方法 + +```java +Document doc = Jsoup.connect("http://example.com/").get(); +``` + +> **说明** +> +> `connect(String url)` 方法创建一个新的 `Connection`, 和 `get()` 取得和解析一个 HTML 文件。如果从该 URL 获取 HTML 时发生错误,便会抛出 IOException,应适当处理。 + +`Connection` 接口还提供一个方法链来解决特殊请求,具体如下: + +```java +Document doc = Jsoup.connect("http://example.com") + .data("query", "Java") + .userAgent("Mozilla") + .cookie("auth", "token") + .timeout(3000) + .post(); +``` + +### 从一个文件加载一个文档 + +可以使用静态 `Jsoup.parse(File in, String charsetName, String baseUri)` 方法 + +```java +File input = new File("/tmp/input.html"); +Document doc = Jsoup.parse(input, "UTF-8", "http://example.com/"); +``` + +> **说明** +> +> `parse(File in, String charsetName, String baseUri)` 这个方法用来加载和解析一个 HTML 文件。如在加载文件的时候发生错误,将抛出 IOException,应作适当处理。 +> +> `baseUri` 参数用于解决文件中 URLs 是相对路径的问题。如果不需要可以传入一个空的字符串。 +> +> 另外还有一个方法`parse(File in, String charsetName)` ,它使用文件的路径做为 `baseUri`。 这个方法适用于如果被解析文件位于网站的本地文件系统,且相关链接也指向该文件系统。 + +## 解析 + +### 使用 DOM 方法来遍历一个文档 + +**问题** + +你有一个 HTML 文档要从中提取数据,并了解这个 HTML 文档的结构。 + +**方法** + +将 HTML 解析成一个`Document`之后,就可以使用类似于 DOM 的方法进行操作。示例代码: + +```java +File input = new File("/tmp/input.html"); +Document doc = Jsoup.parse(input, "UTF-8", "http://example.com/"); + +Element content = doc.getElementById("content"); +Elements links = content.getElementsByTag("a"); +for (Element link : links) { + String linkHref = link.attr("href"); + String linkText = link.text(); +} +``` + +**说明** + +`Elements` 这个对象提供了一系列类似于 DOM 的方法来查找元素,抽取并处理其中的数据。 + +具体如下: + +#### 查找元素 + +- `getElementById(String id)` +- `getElementsByTag(String tag)` +- `getElementsByClass(String className)` +- `getElementsByAttribute(String key)` (and related methods) +- Element siblings: `siblingElements()`, `firstElementSibling()`, `lastElementSibling()`;`nextElementSibling()`, `previousElementSibling()` +- Graph: `parent()`, `children()`, `child(int index)` + +#### 元素数据 + +- `attr(String key)`获取属性`attr(String key, String value)`设置属性 +- `attributes()`获取所有属性 +- `id()`, `className()` and `classNames()` +- `text()`获取文本内容`text(String value)` 设置文本内容 +- `html()`获取元素内 HTML`html(String value)`设置元素内的 HTML 内容 +- `outerHtml()`获取元素外 HTML 内容 +- `data()`获取数据内容(例如:script 和 style 标签) +- `tag()` and `tagName()` + +#### 操作 HTML 和文本 + +- `append(String html)`, `prepend(String html)` +- `appendText(String text)`, `prependText(String text)` +- `appendElement(String tagName)`, `prependElement(String tagName)` +- `html(String value)` + +### 使用选择器语法来查找元素 + +**问题** + +你想使用类似于 CSS 或 jQuery 的语法来查找和操作元素。 + +**方法** + +可以使用`Element.select(String selector)` 和 `Elements.select(String selector)` 方法实现: + +```java +File input = new File("/tmp/input.html"); +Document doc = Jsoup.parse(input, "UTF-8", "http://example.com/"); + +Elements links = doc.select("a[href]"); //带有href属性的a元素 +Elements pngs = doc.select("img[src$=.png]"); + //扩展名为.png的图片 + +Element masthead = doc.select("div.masthead").first(); + //class等于masthead的div标签 + +Elements resultLinks = doc.select("h3.r > a"); //在h3元素之后的a元素 +``` + +> **说明** +> +> jsoup elements 对象支持类似于[CSS](http://www.w3.org/TR/2009/PR-css3-selectors-20091215/) (或[jquery](http://jquery.com/))的选择器语法,来实现非常强大和灵活的查找功能。. +> +> 这个`select` 方法在`Document`, `Element`,或`Elements`对象中都可以使用。且是上下文相关的,因此可实现指定元素的过滤,或者链式选择访问。 +> +> Select 方法将返回一个`Elements`集合,并提供一组方法来抽取和处理结果。 + +#### Selector 选择器概述 + +- `tagname`: 通过标签查找元素,比如:`a` +- `ns|tag`: 通过标签在命名空间查找元素,比如:可以用 `fb|name` 语法来查找 `` 元素 +- `#id`: 通过 ID 查找元素,比如:`#logo` +- `.class`: 通过 class 名称查找元素,比如:`.masthead` +- `[attribute]`: 利用属性查找元素,比如:`[href]` +- `[^attr]`: 利用属性名前缀来查找元素,比如:可以用`[^data-]` 来查找带有 HTML5 Dataset 属性的元素 +- `[attr=value]`: 利用属性值来查找元素,比如:`[width=500]` +- `[attr^=value]`, `[attr$=value]`, `[attr*=value]`: 利用匹配属性值开头、结尾或包含属性值来查找元素,比如:`[href*=/path/]` +- `[attr\~=regex]`: 利用属性值匹配正则表达式来查找元素,比如: `img[src\~=(?i)\.(png|jpe?g)]` +- `*`: 这个符号将匹配所有元素 + +#### Selector 选择器组合使用 + +- `el##id`: 元素+ID,比如: `div##logo` +- `el.class`: 元素+class,比如: `div.masthead` +- `el[attr]`: 元素+class,比如: `a[href]` +- 任意组合,比如:`a[href].highlight` +- `ancestor child`: 查找某个元素下子元素,比如:可以用`.body p` 查找在"body"元素下的所有`p`元素 +- `parent > child`: 查找某个父元素下的直接子元素,比如:可以用`div.content > p` 查找 `p` 元素,也可以用`body > *` 查找 body 标签下所有直接子元素 +- `siblingA + siblingB`: 查找在 A 元素之前第一个同级元素 B,比如:`div.head + div` +- `siblingA \~ siblingX`: 查找 A 元素之前的同级 X 元素,比如:`h1 \~ p` +- `el, el, el`:多个选择器组合,查找匹配任一选择器的唯一元素,例如:`div.masthead, div.logo` + +#### 伪选择器 selectors + +- `:lt(n)`: 查找哪些元素的同级索引值(它的位置在 DOM 树中是相对于它的父节点)小于 n,比如:`td:lt(3)` 表示小于三列的元素 +- `:gt(n)`:查找哪些元素的同级索引值大于` n``,比如 `: `div p:gt(2)`表示哪些 div 中有包含 2 个以上的 p 元素 +- `:eq(n)`: 查找哪些元素的同级索引值与`n`相等,比如:`form input:eq(1)`表示包含一个 input 标签的 Form 元素 +- `:has(seletor)`: 查找匹配选择器包含元素的元素,比如:`div:has(p)`表示哪些 div 包含了 p 元素 +- `:not(selector)`: 查找与选择器不匹配的元素,比如: `div:not(.logo)` 表示不包含 class=logo 元素的所有 div 列表 +- `:contains(text)`: 查找包含给定文本的元素,搜索不区分大不写,比如: `p:contains(jsoup)` +- `:containsOwn(text)`: 查找直接包含给定文本的元素 +- `:matches(regex)`: 查找哪些元素的文本匹配指定的正则表达式,比如:`div:matches((?i)login)` +- `:matchesOwn(regex)`: 查找自身包含文本匹配指定正则表达式的元素 +- 注意:上述伪选择器索引是从 0 开始的,也就是说第一个元素索引值为 0,第二个元素 index 为 1 等 + +可以查看`Selector` API 参考来了解更详细的内容 + +### 从元素抽取属性,文本和 HTML + +**问题** + +在解析获得一个 Document 实例对象,并查找到一些元素之后,你希望取得在这些元素中的数据。 + +**方法** + +- 要取得一个属性的值,可以使用`Node.attr(String key)` 方法 +- 对于一个元素中的文本,可以使用`Element.text()`方法 +- 对于要取得元素或属性中的 HTML 内容,可以使用`Element.html()`, 或 `Node.outerHtml()`方法 + +示例: + +```java +String html = "

    An example link.

    "; +Document doc = Jsoup.parse(html);//解析HTML字符串返回一个Document实现 +Element link = doc.select("a").first();//查找第一个a元素 + +String text = doc.body().text(); // "An example link"//取得字符串中的文本 +String linkHref = link.attr("href"); // "http://example.com/"//取得链接地址 +String linkText = link.text(); // "example""//取得链接地址中的文本 + +String linkOuterH = link.outerHtml(); + // "example" +String linkInnerH = link.html(); // "example"//取得链接内的html内容 +``` + +> **说明** +> +> 上述方法是元素数据访问的核心办法。此外还其它一些方法可以使用: +> +> - `Element.id()` +> - `Element.tagName()` +> - `Element.className()` and `Element.hasClass(String className)` +> +> 这些访问器方法都有相应的 setter 方法来更改数据 + +**参见** + +- `Element`和`Elements`集合类的参考文档 +- [URLs 处理](http://www.open-open.com/jsoup/working-with-urls.htm) +- [使用 CSS 选择器语法来查找元素](http://www.open-open.com/jsoup/selector-syntax.htm) + +### 处理 URLs + +**问题** + +你有一个包含相对 URLs 路径的 HTML 文档,需要将这些相对路径转换成绝对路径的 URLs。 + +**方法** + +1. 在你解析文档时确保有指定`base URI`,然后 +2. 使用 `abs:` 属性前缀来取得包含`base URI`的绝对路径。代码如下: + +```java +Document doc = Jsoup.connect("http://www.open-open.com").get(); + +Element link = doc.select("a").first(); +String relHref = link.attr("href"); // == "/" +String absHref = link.attr("abs:href"); // "http://www.open-open.com/" + +``` + +> **说明** +> +> 在 HTML 元素中,URLs 经常写成相对于文档位置的相对路径: `...`. 当你使用 `Node.attr(String key)` 方法来取得 a 元素的 href 属性时,它将直接返回在 HTML 源码中指定定的值。 +> +> 假如你需要取得一个绝对路径,需要在属性名前加 `abs:` 前缀。这样就可以返回包含根路径的 URL 地址`attr("abs:href")` +> +> 因此,在解析 HTML 文档时,定义 base URI 非常重要。 +> +> 如果你不想使用`abs:` 前缀,还有一个方法能够实现同样的功能 `Node.absUrl(String key)`。 + +## 数据修改 + +### 设置属性的值 + +**问题** + +在你解析一个 `Document` 之后可能想修改其中的某些属性值,然后再保存到磁盘或都输出到前台页面。 + +**方法** + +可以使用属性设置方法 `Element.attr(String key, String value)`, 和 `Elements.attr(String key, String value)`. + +假如你需要修改一个元素的 `class` 属性,可以使用 `Element.addClass(String className)` 和`Element.removeClass(String className)` 方法。 + +`Elements` 提供了批量操作元素属性和 class 的方法,比如:要为 div 中的每一个 a 元素都添加一个`rel="nofollow"` 可以使用如下方法: + +``` +doc.select("div.comments a").attr("rel", "nofollow"); + +``` + +> **说明** +> +> 与`Element`中的其它方法一样,`attr` 方法也是返回当 `Element` (或在使用选择器是返回 `Elements`集合)。这样能够很方便使用方法连用的书写方式。比如: +> +> ``` +> doc.select("div.masthead").attr("title", "jsoup").addClass("round-box"); +> ``` + +### 设置一个元素的 HTML 内容 + +**问题** + +你需要一个元素中的 HTML 内容 + +**方法** + +可以使用`Element`中的 HTML 设置方法具体如下: + +```java +Element div = doc.select("div").first(); //
    +div.html("

    lorem ipsum

    "); //

    lorem ipsum

    +div.prepend("

    First

    ");//在div前添加html内容 +div.append("

    Last

    ");//在div之后添加html内容 +// 添完后的结果:

    First

    lorem ipsum

    Last

    + +Element span = doc.select("span").first(); // One +span.wrap("
  • "); +// 添完后的结果:
  • One
  • +``` + +> **说明** +> +> - `Element.html(String html)` 这个方法将先清除元素中的 HTML 内容,然后用传入的 HTML 代替。 +> - `Element.prepend(String first)` 和 `Element.append(String last)` 方法用于在分别在元素内部 HTML 的前面和后面添加 HTML 内容 +> - `Element.wrap(String around)` 对元素包裹一个外部 HTML 内容。 +> +> **参见** +> +> 可以查看 API 参考文档中 `Element.prependElement(String tag)`和`Element.appendElement(String tag)` 方法来创建新的元素并作为文档的子元素插入其中。 + +### 设置元素的文本内容 + +**问题** + +你需要修改一个 HTML 文档中的文本内容 + +**方法** + +可以使用`Element`的设置方法:: + +``` +Element div = doc.select("div").first(); //
    +div.text("five > four"); //
    five > four
    +div.prepend("First "); +div.append(" Last"); +// now:
    First five > four Last
    +``` + +> **说明** +> +> 文本设置方法与 [HTML setter](http://jsoup.org/cookbook/modifying-data/set-html) 方法一样: +> +> - `Element.text(String text)` 将清除一个元素中的内部 HTML 内容,然后提供的文本进行代替 +> - `Element.prepend(String first)` 和 `Element.append(String last)` 将分别在元素的内部 html 前后添加文本节点。 +> +> 对于传入的文本如果含有像 `<`, `>` 等这样的字符,将以文本处理,而非 HTML。 + +## HTML 清理 + +### 消除不受信任的 HTML (来防止 XSS 攻击) + +**问题** + +在做网站的时候,经常会提供用户评论的功能。有些不怀好意的用户,会搞一些脚本到评论内容中,而这些脚本可能会破坏整个页面的行为,更严重的是获取一些机要信息,此时需要清理该 HTML,以避免跨站脚本[cross-site scripting](http://en.wikipedia.org/wiki/Cross-site_scripting)攻击(XSS)。 + +**方法** + +使用 jsoup HTML `Cleaner` 方法进行清除,但需要指定一个可配置的 `Whitelist`。 + +```java +String unsafe = + "

    Link

    "; +String safe = Jsoup.clean(unsafe, Whitelist.basic()); +// now:

    Link

    +``` + +**说明** + +XSS 又叫 CSS (Cross Site Script) ,跨站脚本攻击。它指的是恶意攻击者往 Web 页面里插入恶意 html 代码,当用户浏览该页之时,嵌入其中 Web 里面的 html 代码会被执行,从而达到恶意攻击用户的特殊目的。XSS 属于被动式的攻击,因为其被动且不好利用,所以许多人常忽略其危害性。所以我们经常只让用户输入纯文本的内容,但这样用户体验就比较差了。 + +一个更好的解决方法就是使用一个富文本编辑器 WYSIWYG 如 [CKEditor](http://ckeditor.com/) 和 [TinyMCE](http://tinymce.moxiecode.com/)。这些可以输出 HTML 并能够让用户可视化编辑。虽然他们可以在客户端进行校验,但是这样还不够安全,需要在服务器端进行校验并清除有害的 HTML 代码,这样才能确保输入到你网站的 HTML 是安全的。否则,攻击者能够绕过客户端的 Javascript 验证,并注入不安全的 HMTL 直接进入您的网站。 + +jsoup 的 whitelist 清理器能够在服务器端对用户输入的 HTML 进行过滤,只输出一些安全的标签和属性。 + +jsoup 提供了一系列的 `Whitelist` 基本配置,能够满足大多数要求;但如有必要,也可以进行修改,不过要小心。 + +这个 cleaner 非常好用不仅可以避免 XSS 攻击,还可以限制用户可以输入的标签范围。 + +**参见** + +- 参阅[XSS cheat sheet](http://ha.ckers.org/xss.html) ,有一个例子可以了解为什么不能使用正则表达式,而采用安全的 whitelist parser-based 清理器才是正确的选择。 +- 参阅`Cleaner` ,了解如何返回一个 `Document` 对象,而不是字符串 +- 参阅`Whitelist`,了解如何创建一个自定义的 whitelist +- [nofollow](http://en.wikipedia.org/wiki/Nofollow) 链接属性了解 + +## 参考 + +- [jsoup github 托管代码](https://github.com/jhy/jsoup) +- [jsoup Cookbook](https://jsoup.org/cookbook/) +- [jsoup Cookbook(中文版)](http://www.open-open.com/jsoup/) +- [不错的 jsoup 学习笔记](https://github.com/code4craft/jsoup-learning) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" new file mode 100644 index 00000000..71151154 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" @@ -0,0 +1,242 @@ +--- +title: Thumbnailator 快速入门 +date: 2022-02-17 22:34:30 +order: 06 +categories: + - Java + - 工具 + - 其他 +tags: + - Java + - 图形处理 + - Thumbnailator +permalink: /pages/adacc5/ +--- + +# Thumbnailator 快速入门 + +## 简介 + +`Thumbnailator` 是一个开源的 **Java** 项目,它提供了非常简单的 API 来对图片进行缩放、旋转以及加水印的处理。 + +有多简单呢?简单到一行代码就可以完成图片处理。形式如下: + +```java +Thumbnails.of(new File("path/to/directory").listFiles()) + .size(640, 480) + .outputFormat("jpg") + .toFiles(Rename.PREFIX_DOT_THUMBNAIL); +``` + +当然,Thumbnailator 还有一些使用细节,下面我会一一道来。 + +## 核心 API + +### Thumbnails + +`Thumbnails` 是使用 Thumbnailator 创建缩略图的主入口。 + +它提供了一组初始化 `Thumbnails.Builder` 的接口。 + +先看下这组接口的声明: + +```java +// 可变长度参数列表 +public static Builder of(String... files) {...} +public static Builder of(File... files) {...} +public static Builder of(URL... urls) {...} +public static Builder of(InputStream... inputStreams) {...} +public static Builder of(BufferedImage... images) {...} +// 迭代器(所有实现 Iterable 接口的 Java 对象都可以,当然也包括 List、Set) +public static Builder fromFilenames(Iterable files) {...} +public static Builder fromFiles(Iterable files) {...} +public static Builder fromURLs(Iterable urls) {...} +public static Builder fromInputStreams(Iterable inputStreams) {...} +public static Builder fromImages(Iterable images) {...} +``` + +很显然,**Thumbnails 允许通过传入文件名、文件、网络图的 URL、图片流、图片缓存多种方式来初始化构造器**。 + +因此,你可以根据实际需求来灵活的选择图片的输入方式。 + +需要注意一点:**如果输入是多个对象(无论你是直接输入容器对象或使用可变参数方式传入多个对象),则输出也必须选用输出多个对象的方式,否则会报异常。** + +### Thumbnails.Builder + +`Thumbnails.Builder` 是 `Thumbnails` 的内部静态类。它用于设置生成缩略图任务的相关参数。 + +**_注:`Thumbnails.Builder` 的构造函数是私有函数。所以,它只允许通过 `Thumbnails` 的实例化函数来进行初始化。_** + +#### 设置参数的函数 + +`Thumbnails.Builder` 提供了一组函数链形式的接口来设置缩放图参数。 + +以设置大小函数为例: + +```java +public Builder size(int width, int height) +{ + updateStatus(Properties.SIZE, Status.ALREADY_SET); + updateStatus(Properties.SCALE, Status.CANNOT_SET); + + validateDimensions(width, height); + this.width = width; + this.height = height; + + return this; +} +``` + +通过返回 this 指针,使得设置参数函数可以以链式调用的方式来使用,形式如下: + +```java +Thumbnails.of(new File("original.jpg")) + .size(160, 160) + .rotate(90) + .watermark(Positions.BOTTOM_RIGHT, ImageIO.read(new File("watermark.png")), 0.5f) + .outputQuality(0.8) + .toFile(new File("image-with-watermark.jpg")); +``` + +好处,不言自明:那就是大大简化了代码。 + +#### 输出函数 + +`Thumbnails.Builder` 提供了一组重载函数来输出生成的缩放图。 + +函数声明如下: + +```java +// 返回图片缓存 +public List asBufferedImages() throws IOException {...} +public BufferedImage asBufferedImage() throws IOException {...} +// 返回文件列表 +public List asFiles(Iterable iterable) throws IOException {...} +public List asFiles(Rename rename) throws IOException {...} +public List asFiles(File destinationDir, Rename rename) throws IOException {...} +// 创建文件 +public void toFile(File outFile) throws IOException {...} +public void toFile(String outFilepath) throws IOException {...} +public void toFiles(Iterable iterable) throws IOException {...} +public void toFiles(Rename rename) throws IOException {...} +public void toFiles(File destinationDir, Rename rename) throws IOException {...} +// 创建输出流 +public void toOutputStream(OutputStream os) throws IOException {...} +public void toOutputStreams(Iterable iterable) throws IOException {...} +``` + +### 工作流 + +Thumbnailator 的工作步骤十分简单,可分为三步: + +1. **输入**:`Thumbnails` 根据输入初始化构造器—— `Thumbnails.Builder` 。 + +2. **设置**:`Thumbnails.Builder` 设置缩放图片的参数。 + +3. **输出**:`Thumbnails.Builder` 输出图片文件或图片流。 + +> 更多详情可以参考: [Thumbnailator 官网 javadoc](https://coobird.github.io/thumbnailator/javadoc/0.4.8/) + +## 实战 + +前文介绍了 Thumbnailator 的核心 API,接下来我们就可以通过实战来看看 Thumbnailator 究竟可以做些什么。 + +Thumbnailator 生成什么样的图片,是根据设置参数来决定的。 + +### 安装 + +maven 项目中引入依赖: + +```xml + + net.coobird + thumbnailator + [0.4, 0.5) + +``` + +### 图片缩放 + +`Thumbnails.Builder` 的 `size` 函数可以设置新图片精确的宽度和高度,也可以用 `scale` 函数设置缩放比例。 + +```java +Thumbnails.of("oldFile.png") + .size(16, 16) + .toFile("newFile_16_16.png"); + +Thumbnails.of("oldFile.png") + .scale(2.0) + .toFile("newFile_scale_2.0.png"); + +Thumbnails.of("oldFile.png") + .scale(1.0, 0.5) + .toFile("newFile_scale_1.0_0.5.png"); +``` + +**oldFile.png** + +![img](http://upload-images.jianshu.io/upload_images/3101171-ba63439898602e8f.png) + +**newFile_scale_1.0_0.5.png** + +![img](http://upload-images.jianshu.io/upload_images/3101171-a01ea4515fff865d.png) + +### 图片旋转 + +`Thumbnails.Builder` 的 `size` 函数可以设置新图片的旋转角度。 + +```java +Thumbnails.of("oldFile.png") + .scale(0.8) + .rotate(90) + .toFile("newFile_rotate_90.png"); + +Thumbnails.of("oldFile.png") + .scale(0.8) + .rotate(180) + .toFile("newFile_rotate_180.png"); +``` + +**newFile_rotate_90.png** + +![img](http://upload-images.jianshu.io/upload_images/3101171-17d54bc33b38d45b.png) + +### 加水印 + +`Thumbnails.Builder` 的 `watermark` 函数可以为图片添加水印图片。第一个参数是水印的位置;第二个参数是水印图片的缓存数据;第三个参数是透明度。 + +```java +BufferedImage watermarkImage = ImageIO.read(new File("wartermarkFile.png")); +Thumbnails.of("oldFile.png") + .scale(0.8) + .watermark(Positions.BOTTOM_LEFT, watermarkImage, 0.5f) + .toFile("newFile_watermark.png"); +``` + +**wartermarkFile.png** + +![img](http://upload-images.jianshu.io/upload_images/3101171-97909ee6c066c195.png?imageMogr2/auto-orient/strip) + +**newFile_watermark.png** + +![img](http://upload-images.jianshu.io/upload_images/3101171-93eb7ef71b811a0c.png) + +### 批量处理图片 + +下面以批量给图片加水印来展示一下如何处理多个图片文件。 + +```java +BufferedImage watermarkImage = ImageIO.read(new File("wartermarkFile.png")); + +File destinationDir = new File("D:\\watermark\\"); +Thumbnails.of("oldFile.png", "oldFile2.png") + .scale(0.8) + .watermark(Positions.BOTTOM_LEFT, watermarkImage, 0.5f) + .toFiles(destinationDir, Rename.PREFIX_DOT_THUMBNAIL); +``` + +> **需要参考完整测试例代码请** [**点击这里**](https://github.com/dunwu/JavaParty/blob/master/toolbox/image/src/test/java/org/zp/image/ThumbnailatorTest.java) + +## 参考 + +[Thumbnailator 官方示例文档](https://github.com/coobird/thumbnailator/wiki/Examples) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" new file mode 100644 index 00000000..d5f691bb --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" @@ -0,0 +1,97 @@ +--- +title: ZXing 快速入门 +date: 2022-02-17 22:34:30 +order: 07 +categories: + - Java + - 工具 + - 其他 +tags: + - Java + - 条形码 + - ZXing +permalink: /pages/b563af/ +--- + +# ZXing 快速入门 + +## 简介 + +`ZXing` 是一个开源 Java 类库用于解析多种格式的 1D/2D 条形码。目标是能够对 QR 编码、Data Matrix、UPC 的 1D 条形码进行解码。 其提供了多种平台下的客户端包括:J2ME、J2SE 和 Android。 + +官网:[ZXing github 仓库](https://github.com/zxing/zxing) + +## 实战 + +**_本例演示如何在一个非 android 的 Java 项目中使用 ZXing 来生成、解析二维码图片。_** + +### 安装 + +maven 项目只需引入依赖: + +```xml + + com.google.zxing + core + 3.3.0 + + + com.google.zxing + javase + 3.3.0 + +``` + +如果非 maven 项目,就去官网下载发布版本:[下载地址](https://github.com/zxing/zxing/releases) + +### 生成二维码图片 + +ZXing 生成二维码图片有以下步骤: + +1. `com.google.zxing.MultiFormatWriter` 根据内容以及图像编码参数生成图像 2D 矩阵。 +2. ​ `com.google.zxing.client.j2se.MatrixToImageWriter` 根据图像矩阵生成图片文件或图片缓存 `BufferedImage` 。 + +```java +public void encode(String content, String filepath) throws WriterException, IOException { + int width = 100; + int height = 100; + Map encodeHints = new HashMap(); + encodeHints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); + BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, encodeHints); + Path path = FileSystems.getDefault().getPath(filepath); + MatrixToImageWriter.writeToPath(bitMatrix, "png", path); +} +``` + +### 解析二维码图片 + +ZXing 解析二维码图片有以下步骤: + +1. 使用 `javax.imageio.ImageIO` 读取图片文件,并存为一个 `java.awt.image.BufferedImage` 对象。 + +2. 将 `java.awt.image.BufferedImage` 转换为 ZXing 能识别的 `com.google.zxing.BinaryBitmap` 对象。 + +3. `com.google.zxing.MultiFormatReader` 根据图像解码参数来解析 `com.google.zxing.BinaryBitmap` 。 + +```java +public String decode(String filepath) throws IOException, NotFoundException { + BufferedImage bufferedImage = ImageIO.read(new FileInputStream(filepath)); + LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage); + Binarizer binarizer = new HybridBinarizer(source); + BinaryBitmap bitmap = new BinaryBitmap(binarizer); + HashMap decodeHints = new HashMap(); + decodeHints.put(DecodeHintType.CHARACTER_SET, "UTF-8"); + Result result = new MultiFormatReader().decode(bitmap, decodeHints); + return result.getText(); +} +``` + +完整参考示例:[测试例代码](https://github.com/dunwu/JavaParty/blob/master/toolbox/image/src/test/java/org/zp/image/QRCodeUtilTest.java) + +以下是一个生成的二维码图片示例: + +![img](http://upload-images.jianshu.io/upload_images/3101171-26b73730088f0ab8.png) + +## 参考 + +[ZXing github 仓库](https://github.com/zxing/zxing) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/README.md" "b/docs/01.Java/12.\345\267\245\345\205\267/README.md" new file mode 100644 index 00000000..dbd1c924 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/README.md" @@ -0,0 +1,56 @@ +--- +title: Java 工具 +date: 2022-02-18 08:53:11 +categories: + - Java + - 工具 +tags: + - Java + - 工具 +permalink: /pages/1123e1/ +hidden: true +index: false +--- + +# Java 工具 + +## 📖 内容 + +### Java IO + +- [JSON 序列化](01.IO/01.JSON序列化.md) - [fastjson](https://github.com/alibaba/fastjson)、[Jackson](https://github.com/FasterXML/jackson)、[Gson](https://github.com/google/gson) +- [二进制序列化](01.IO/02.二进制序列化.md) - [Protobuf](https://developers.google.com/protocol-buffers)、[Thrift](https://thrift.apache.org/)、[Hessian](http://hessian.caucho.com/)、[Kryo](https://github.com/EsotericSoftware/kryo)、[FST](https://github.com/RuedigerMoeller/fast-serialization) + +### JavaBean 工具 + +- [Lombok](02.JavaBean/01.Lombok.md) +- [Dozer](02.JavaBean/02.Dozer.md) + +### Java 模板引擎 + +- [Freemark](03.模板引擎/01.Freemark.md) +- [Velocity](03.模板引擎/02.Thymeleaf.md) +- [Thymeleaf](03.模板引擎/03.Velocity.md) + +### Java 测试工具 + +- [Junit](04.测试/01.Junit.md) +- [Mockito](04.测试/02.Mockito.md) +- [Jmeter](04.测试/03.Jmeter.md) +- [JMH](04.测试/04.JMH.md) + +### 其他 + +- [Java 日志](99.其他/01.Java日志.md) +- [Java 工具包](99.其他/02.Java工具包.md) +- [Reflections](99.其他/03.Reflections.md) +- [JavaMail](99.其他/04.JavaMail.md) +- [Jsoup](99.其他/05.Jsoup.md) +- [Thumbnailator](99.其他/06.Thumbnailator.md) +- [Zxing](99.其他/07.Zxing.md) + +## 📚 资料 + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/01.Spring\346\246\202\350\277\260.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/01.Spring\346\246\202\350\277\260.md" new file mode 100644 index 00000000..3a202241 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/01.Spring\346\246\202\350\277\260.md" @@ -0,0 +1,188 @@ +--- +title: Spring Framework 综述 +date: 2019-11-22 10:46:02 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring +permalink: /pages/9d3091/ +--- + +# Spring Framework 综述 + +## Spring Framework 简介 + +Spring Framework 是最受欢迎的企业级 Java 应用程序开发框架。用于构建企业级应用的轻量级、一站式解决方案。 + +当谈论到大小和透明度时, Spring 是轻量级的。 Spring 框架的基础版本是在 2 MB 左右的。 + +Spring 框架的核心特性可以用于开发任何 Java 应用程序,但是在 Java EE 平台上构建 web 应用程序是需要扩展的。 Spring 框架的目标是使 J2EE 开发变得更容易使用,通过启用基于 POJO 编程模型来促进良好的编程实践。 + +Spring Framework 设计理念如下: + +- 力争让选择无处不在 +- 体现海纳百川的精神 +- 保持后向兼容性 +- 专注 API 设计 +- 追求严苛的代码质量 + +## 为什么使用 Spring + +下面列出的是使用 Spring 框架主要的好处: + +- Spring 可以使开发人员使用 POJOs 开发企业级的应用程序。只使用 POJOs 的好处是你不需要一个 EJB 容器产品,比如一个应用程序服务器,但是你可以选择使用一个健壮的 servlet 容器,比如 Tomcat 或者一些商业产品。 +- Spring 在一个单元模式中是有组织的。即使包和类的数量非常大,你只需要选择你需要的部分,而忽略剩余的那部分。 +- Spring 不会让你白费力气做重复工作,它真正的利用了一些现有的技术,像几个 ORM 框架、日志框架、JEE、Quartz 和 JDK 计时器,其他视图技术。 +- 测试一个用 Spring 编写的应用程序很容易,因为 environment-dependent 代码被放进了这个框架中。此外,通过使用 JavaBean-style POJOs,它在使用依赖注入注入测试数据时变得更容易。 +- Spring 的 web 框架是一个设计良好的 web MVC 框架,它为 web 框架,比如 Structs 或者其他工程上的或者很少受欢迎的 web 框架,提供了一个很好的供替代的选择。 +- 为将特定技术的异常(例如,由 JDBC、Hibernate,或者 JDO 抛出的异常)翻译成一致的, Spring 提供了一个方便的 API,而这些都是未经检验的异常。 +- 轻量级的 IOC 容器往往是轻量级的,例如,特别是当与 EJB 容器相比的时候。这有利于在内存和 CPU 资源有限的计算机上开发和部署应用程序。 +- Spring 提供了一个一致的事务管理界面,该界面可以缩小成一个本地事务(例如,使用一个单一的数据库)和扩展成一个全局事务(例如,使用 JTA)。 + +## 核心思想 + +Spring 最核心的两个技术思想是:IoC 和 Aop + +### IoC + +`IoC` 即 `Inversion of Control` ,意为控制反转。 + +Spring 最认同的技术是控制反转的**依赖注入(DI)**模式。控制反转(IoC)是一个通用的概念,它可以用许多不同的方式去表达,依赖注入仅仅是控制反转的一个具体的例子。 + +当编写一个复杂的 Java 应用程序时,应用程序类应该尽可能的独立于其他的 Java 类来增加这些类可重用可能性,当进行单元测试时,可以使它们独立于其他类进行测试。依赖注入(或者有时被称为配线)有助于将这些类粘合在一起,并且在同一时间让它们保持独立。 + +到底什么是依赖注入?让我们将这两个词分开来看一看。这里将依赖关系部分转化为两个类之间的关联。例如,类 A 依赖于类 B。现在,让我们看一看第二部分,注入。所有这一切都意味着类 B 将通过 IoC 被注入到类 A 中。 + +依赖注入可以以向构造函数传递参数的方式发生,或者通过使用 setter 方法 post-construction。由于依赖注入是 Spring 框架的核心部分,所以我将在一个单独的章节中利用很好的例子去解释这一概念。 + +### Aop + +Spring 框架的一个关键组件是**面向方面的程序设计(AOP)**框架。一个程序中跨越多个点的功能被称为**横切关注点**,这些横切关注点在概念上独立于应用程序的业务逻辑。有各种各样常见的很好的关于方面的例子,比如日志记录、声明性事务、安全性,和缓存等等。 + +在 OOP 中模块化的关键单元是类,而在 AOP 中模块化的关键单元是方面。AOP 帮助你将横切关注点从它们所影响的对象中分离出来,然而依赖注入帮助你将你的应用程序对象从彼此中分离出来。 + +Spring 框架的 AOP 模块提供了面向方面的程序设计实现,允许你定义拦截器方法和切入点,可以实现将应该被分开的代码干净的分开功能。我将在一个独立的章节中讨论更多关于 Spring AOP 的概念。 + +## Spring 体系结构 + +Spring 当前框架有**20**个 jar 包,大致可以分为**6**大模块: + +- 1. 为什么使用 Spring +- 2. 核心思想 + - 2.1. IoC + - 2.2. Aop +- 3. Spring 体系结构 + - 3.1. Core Container + - 3.1.1. BeanFactory + - 3.1.2. ApplicationContext + - 3.2. AOP and Instrumentation + - 3.3. Messaging + - 3.4. Data Access / Integaration + - 3.5. Web + - 3.6. Test +- 4. 术语 + +Spring 框架提供了非常丰富的功能,因此整个架构也很庞大。 +在我们实际的应用开发中,并不一定要使用所有的功能,而是可以根据需要选择合适的 Spring 模块。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/spring-framework.png) + +### Core Container + +IoC 容器是 Spring 框架的核心。spring 容器使用依赖注入管理构成应用的组件,它会创建相互协作的组件之间的关联。毫无疑问,这些对象更简单干净,更容易理解,也更容易重用和测试。 +Spring 自带了几种容器的实现,可归纳为两种类型: + +#### BeanFactory + +由 org.springframework.beans.factory.BeanFactory 接口定义。 +它是最简单的容器,提供基本的 DI 支持。 + +#### ApplicationContext + +由 org.springframework.context.ApplicationContext 接口定义。 +它是基于 BeanFactory 之上构建,并提供面向应用的服务,例如从属性文件解析文本信息的能力,以及发布应用事件给感兴趣的事件监听者的能力。 +**_注:Bean 工厂对于大多数应用来说往往太低级了,所以应用上下文使用更广泛。推荐在开发中使用应用上下文容器。_** + +Spring 自带了多种应用上下文,最可能遇到的有以下几种: +`ClassPathXmlApplicationContext`:从类路径下的 XML 配置文件中加载上下文定义,把应用上下文定义文件当做类资源。 +`FileSystemXmlApplicationContext`:读取文件系统下的 XML 配置文件并加载上下文定义。 +`XmlWebApplicationContext`:读取 Web 应用下的 XML 配置文件并装载上下文定义。 + +**_范例_** + +```java +ApplicationContext context = new FileSystemXmlApplicationContext("D:\Temp\build.xml"); +ApplicationContext context2 = new ClassPathXmlApplicationContext("build.xml"); +``` + +可以看到,加载 `FileSystemXmlApplicationContext` 和 `ClassPathXmlApplicationContext` 十分相似。 +差异在于:前者在指定文件系统路径下查找 build.xml 文件;而后在所有类路径(包含 JAR 文件)下查找 build.xml 文件。 +通过引用应用上下文,可以很方便的调用 getBean() 方法从 Spring 容器中获取 Bean。 + +**相关 jar 包** + +- `spring-core`, `spring-beans`, 提供框架的基础部分,包括 IoC 和依赖注入特性。 + +- `spring-context`, 在`spring-core`, `spring-beans`基础上构建。它提供一种框架式的访问对象的方法。它也支持类似 Java EE 特性,例如:EJB,JMX 和基本 remoting。ApplicationContext 接口是它的聚焦点。 +- `springcontext-support`, 集成第三方库到 Spring application context。 +- `spring-expression`,提供一种强有力的表达语言在运行时来查询和操纵一个对象图。 + +### AOP and Instrumentation + +**相关 jar 包** + +- `spring-aop`,提供了对面向切面编程的丰富支持。 +- `spring-aspects`,提供了对 AspectJ 的集成。 +- `spring-instrument`,提供了对类 instrumentation 的支持和类加载器。 +- `spring-instrument-tomcat`,包含了 Spring 对 Tomcat 的 instrumentation 代理。 + +### Messaging + +**相关 jar 包** + +- `spring-messaging`,包含 spring 的消息处理功能,如 Message,MessageChannel,MessageHandler。 + +### Data Access / Integaration + +Data Access/Integration 层包含了 JDBC / ORM / OXM / JMS 和 Transaction 模块。 + +**相关 jar 包** + +- `spring-jdbc`,提供了一个 JDBC 抽象层。 + +- `spring-tx`,支持编程和声明式事务管理类。 +- `spring-orm`,提供了流行的对象关系型映射 API 集,如 JPA,JDO,Hibernate。 +- `spring-oxm`,提供了一个抽象层以支持对象/XML 映射的实现,如 JAXB,Castor,XMLBeans,JiBX 和 XStream. +- `spring-jms`,包含了生产和消费消息的功能。 + +### Web + +**相关 jar 包** + +- `spring-web`,提供了基本的面向 web 的功能,如多文件上传、使用 Servlet 监听器的 Ioc 容器的初始化。一个面向 web 的应用层上下文。 + +- `spring-webmvc`,包括 MVC 和 REST web 服务实现。 +- `spring-webmvc-portlet`,提供在 Protlet 环境的 MVC 实现和`spring-webmvc`功能的镜像。 + +### Test + +**相关 jar 包** + +- `spring-test`,以 Junit 和 TestNG 来支持 spring 组件的单元测试和集成测试。 + +## 术语 + +- **应用程序**:是能完成我们所需要功能的成品,比如购物网站、OA 系统。 +- **框架**:是能完成一定功能的半成品,比如我们可以使用框架进行购物网站开发;框架做一部分功能,我们自己做一部分功能,这样应用程序就创建出来了。而且框架规定了你在开发应用程序时的整体架构,提供了一些基础功能,还规定了类和对象的如何创建、如何协作等,从而简化我们开发,让我们专注于业务逻辑开发。 +- **非侵入式设计**:从框架角度可以这样理解,无需继承框架提供的类,这种设计就可以看作是非侵入式设计,如果继承了这些框架类,就是侵入设计,如果以后想更换框架之前写过的代码几乎无法重用,如果非侵入式设计则之前写过的代码仍然可以继续使用。 +- **轻量级及重量级**:轻量级是相对于重量级而言的,轻量级一般就是非入侵性的、所依赖的东西非常少、资源占用非常少、部署简单等等,其实就是比较容易使用,而重量级正好相反。 +- **POJO**:POJO(Plain Old Java Objects)简单的 Java 对象,它可以包含业务逻辑或持久化逻辑,但不担当任何特殊角色且不继承或不实现任何其它 Java 框架的类或接口。 +- **容器**:在日常生活中容器就是一种盛放东西的器具,从程序设计角度看就是装对象的的对象,因为存在放入、拿出等操作,所以容器还要管理对象的生命周期。 +- **控制反转:**即 Inversion of Control,缩写为 IoC,控制反转还有一个名字叫做依赖注入(Dependency Injection),就是由容器控制程序之间的关系,而非传统实现中,由程序代码直接操控。 +- **JavaBean**:一般指容器管理对象,在 Spring 中指 Spring IoC 容器管理对象。 \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/21.SpringBoot\347\237\245\350\257\206\345\233\276\350\260\261.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/21.SpringBoot\347\237\245\350\257\206\345\233\276\350\260\261.md" new file mode 100644 index 00000000..3a5733b4 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/21.SpringBoot\347\237\245\350\257\206\345\233\276\350\260\261.md" @@ -0,0 +1,951 @@ +--- +title: SpringBoot 知识图谱 +date: 2020-08-12 07:01:26 +order: 21 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/430f53/ +--- + +# SpringBoot 知识图谱 + +> 1. 预警:本文非常长,建议先 mark 后看,也许是最后一次写这么长的文章 +> 2. 说明:前面有 4 个小节关于 Spring 的基础知识,分别是:IOC 容器、JavaConfig、事件监听、SpringFactoriesLoader 详解,它们占据了本文的大部分内容,虽然它们之间可能没有太多的联系,但这些知识对于理解 Spring Boot 的核心原理至关重要,如果你对 Spring 框架烂熟于心,完全可以跳过这 4 个小节。正是因为这个系列的文章是由这些看似不相关的知识点组成,因此取名知识清单。 + +在过去两三年的 Spring 生态圈,最让人兴奋的莫过于 Spring Boot 框架。或许从命名上就能看出这个框架的设计初衷:快速的启动 Spring 应用。因而 Spring Boot 应用本质上就是一个基于 Spring 框架的应用,它是 Spring 对“约定优先于配置”理念的最佳实践产物,它能够帮助开发者更快速高效地构建基于 Spring 生态圈的应用。 + +那 Spring Boot 有何魔法?**自动配置**、**起步依赖**、**Actuator**、**命令行界面(CLI)** 是 Spring Boot 最重要的 4 大核心特性,其中 CLI 是 Spring Boot 的可选特性,虽然它功能强大,但也引入了一套不太常规的开发模型,因而这个系列的文章仅关注其它 3 种特性。如文章标题,本文是这个系列的第一部分,将为你打开 Spring Boot 的大门,重点为你剖析其启动流程以及自动配置实现原理。要掌握这部分核心内容,理解一些 Spring 框架的基础知识,将会让你事半功倍。 + +## 一、抛砖引玉:探索 Spring IoC 容器 + +如果有看过`SpringApplication.run()`方法的源码,Spring Boot 冗长无比的启动流程一定会让你抓狂,透过现象看本质,SpringApplication 只是将一个典型的 Spring 应用的启动流程进行了扩展,因此,透彻理解 Spring 容器是打开 Spring Boot 大门的一把钥匙。 + +### 1.1、Spring IoC 容器 + +可以把 Spring IoC 容器比作一间餐馆,当你来到餐馆,通常会直接招呼服务员:点菜!至于菜的原料是什么?如何用原料把菜做出来?可能你根本就不关心。IoC 容器也是一样,你只需要告诉它需要某个 bean,它就把对应的实例(instance)扔给你,至于这个 bean 是否依赖其他组件,怎样完成它的初始化,根本就不需要你关心。 + +作为餐馆,想要做出菜肴,得知道菜的原料和菜谱,同样地,IoC 容器想要管理各个业务对象以及它们之间的依赖关系,需要通过某种途径来记录和管理这些信息。`BeanDefinition`对象就承担了这个责任:容器中的每一个 bean 都会有一个对应的 BeanDefinition 实例,该实例负责保存 bean 对象的所有必要信息,包括 bean 对象的 class 类型、是否是抽象类、构造方法和参数、其它属性等等。当客户端向容器请求相应对象时,容器就会通过这些信息为客户端返回一个完整可用的 bean 实例。 + +原材料已经准备好(把 BeanDefinition 看着原料),开始做菜吧,等等,你还需要一份菜谱,`BeanDefinitionRegistry`和`BeanFactory`就是这份菜谱,BeanDefinitionRegistry 抽象出 bean 的注册逻辑,而 BeanFactory 则抽象出了 bean 的管理逻辑,而各个 BeanFactory 的实现类就具体承担了 bean 的注册以及管理工作。它们之间的关系就如下图: + +![img](https://user-gold-cdn.xitu.io/2018/9/9/165bd49d06649b0b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) _BeanFactory、BeanDefinitionRegistry 关系图(来自:Spring 揭秘)_ + +`DefaultListableBeanFactory`作为一个比较通用的 BeanFactory 实现,它同时也实现了 BeanDefinitionRegistry 接口,因此它就承担了 Bean 的注册管理工作。从图中也可以看出,BeanFactory 接口中主要包含 getBean、containBean、getType、getAliases 等管理 bean 的方法,而 BeanDefinitionRegistry 接口则包含 registerBeanDefinition、removeBeanDefinition、getBeanDefinition 等注册管理 BeanDefinition 的方法。 + +下面通过一段简单的代码来模拟 BeanFactory 底层是如何工作的: + +``` +// 默认容器实现 +DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory(); +// 根据业务对象构造相应的BeanDefinition +AbstractBeanDefinition definition = new RootBeanDefinition(Business.class,true); +// 将bean定义注册到容器中 +beanRegistry.registerBeanDefinition("beanName",definition); +// 如果有多个bean,还可以指定各个bean之间的依赖关系 +// ........ + +// 然后可以从容器中获取这个bean的实例 +// 注意:这里的beanRegistry其实实现了BeanFactory接口,所以可以强转, +// 单纯的BeanDefinitionRegistry是无法强制转换到BeanFactory类型的 +BeanFactory container = (BeanFactory)beanRegistry; +Business business = (Business)container.getBean("beanName"); +``` + +这段代码仅为了说明 BeanFactory 底层的大致工作流程,实际情况会更加复杂,比如 bean 之间的依赖关系可能定义在外部配置文件(XML/Properties)中、也可能是注解方式。Spring IoC 容器的整个工作流程大致可以分为两个阶段: + +①、容器启动阶段 + +容器启动时,会通过某种途径加载`Configuration MetaData`。除了代码方式比较直接外,在大部分情况下,容器需要依赖某些工具类,比如:`BeanDefinitionReader`,BeanDefinitionReader 会对加载的`Configuration MetaData`进行解析和分析,并将分析后的信息组装为相应的 BeanDefinition,最后把这些保存了 bean 定义的 BeanDefinition,注册到相应的 BeanDefinitionRegistry,这样容器的启动工作就完成了。这个阶段主要完成一些准备性工作,更侧重于 bean 对象管理信息的收集,当然一些验证性或者辅助性的工作也在这一阶段完成。 + +来看一个简单的例子吧,过往,所有的 bean 都定义在 XML 配置文件中,下面的代码将模拟 BeanFactory 如何从配置文件中加载 bean 的定义以及依赖关系: + +``` +// 通常为BeanDefinitionRegistry的实现类,这里以DeFaultListabeBeanFactory为例 +BeanDefinitionRegistry beanRegistry = new DefaultListableBeanFactory(); +// XmlBeanDefinitionReader实现了BeanDefinitionReader接口,用于解析XML文件 +XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReaderImpl(beanRegistry); +// 加载配置文件 +beanDefinitionReader.loadBeanDefinitions("classpath:spring-bean.xml"); + +// 从容器中获取bean实例 +BeanFactory container = (BeanFactory)beanRegistry; +Business business = (Business)container.getBean("beanName"); +``` + +②、Bean 的实例化阶段 + +经过第一阶段,所有 bean 定义都通过 BeanDefinition 的方式注册到 BeanDefinitionRegistry 中,当某个请求通过容器的 getBean 方法请求某个对象,或者因为依赖关系容器需要隐式的调用 getBean 时,就会触发第二阶段的活动:容器会首先检查所请求的对象之前是否已经实例化完成。如果没有,则会根据注册的 BeanDefinition 所提供的信息实例化被请求对象,并为其注入依赖。当该对象装配完毕后,容器会立即将其返回给请求方法使用。 + +BeanFactory 只是 Spring IoC 容器的一种实现,如果没有特殊指定,它采用采用延迟初始化策略:只有当访问容器中的某个对象时,才对该对象进行初始化和依赖注入操作。而在实际场景下,我们更多的使用另外一种类型的容器:`ApplicationContext`,它构建在 BeanFactory 之上,属于更高级的容器,除了具有 BeanFactory 的所有能力之外,还提供对事件监听机制以及国际化的支持等。它管理的 bean,在容器启动时全部完成初始化和依赖注入操作。 + +### 1.2、Spring 容器扩展机制 + +IoC 容器负责管理容器中所有 bean 的生命周期,而在 bean 生命周期的不同阶段,Spring 提供了不同的扩展点来改变 bean 的命运。在容器的启动阶段,`BeanFactoryPostProcessor`允许我们在容器实例化相应对象之前,对注册到容器的 BeanDefinition 所保存的信息做一些额外的操作,比如修改 bean 定义的某些属性或者增加其他信息等。 + +如果要自定义扩展类,通常需要实现`org.springframework.beans.factory.config.BeanFactoryPostProcessor`接口,与此同时,因为容器中可能有多个 BeanFactoryPostProcessor,可能还需要实现`org.springframework.core.Ordered`接口,以保证 BeanFactoryPostProcessor 按照顺序执行。Spring 提供了为数不多的 BeanFactoryPostProcessor 实现,我们以`PropertyPlaceholderConfigurer`来说明其大致的工作流程。 + +在 Spring 项目的 XML 配置文件中,经常可以看到许多配置项的值使用占位符,而将占位符所代表的值单独配置到独立的 properties 文件,这样可以将散落在不同 XML 文件中的配置集中管理,而且也方便运维根据不同的环境进行配置不同的值。这个非常实用的功能就是由 PropertyPlaceholderConfigurer 负责实现的。 + +根据前文,当 BeanFactory 在第一阶段加载完所有配置信息时,BeanFactory 中保存的对象的属性还是以占位符方式存在的,比如`${jdbc.mysql.url}`。当 PropertyPlaceholderConfigurer 作为 BeanFactoryPostProcessor 被应用时,它会使用 properties 配置文件中的值来替换相应的 BeanDefinition 中占位符所表示的属性值。当需要实例化 bean 时,bean 定义中的属性值就已经被替换成我们配置的值。当然其实现比上面描述的要复杂一些,这里仅说明其大致工作原理,更详细的实现可以参考其源码。 + +与之相似的,还有`BeanPostProcessor`,其存在于对象实例化阶段。跟 BeanFactoryPostProcessor 类似,它会处理容器内所有符合条件并且已经实例化后的对象。简单的对比,BeanFactoryPostProcessor 处理 bean 的定义,而 BeanPostProcessor 则处理 bean 完成实例化后的对象。BeanPostProcessor 定义了两个接口: + +``` +public interface BeanPostProcessor { + // 前置处理 + Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; + // 后置处理 + Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException; +} +``` + +为了理解这两个方法执行的时机,简单的了解下 bean 的整个生命周期: + +`postProcessBeforeInitialization()`方法与`postProcessAfterInitialization()`分别对应图中前置处理和后置处理两个步骤将执行的方法。这两个方法中都传入了 bean 对象实例的引用,为扩展容器的对象实例化过程提供了很大便利,在这儿几乎可以对传入的实例执行任何操作。注解、AOP 等功能的实现均大量使用了`BeanPostProcessor`,比如有一个自定义注解,你完全可以实现 BeanPostProcessor 的接口,在其中判断 bean 对象的脑袋上是否有该注解,如果有,你可以对这个 bean 实例执行任何操作,想想是不是非常的简单? + +再来看一个更常见的例子,在 Spring 中经常能够看到各种各样的 Aware 接口,其作用就是在对象实例化完成以后将 Aware 接口定义中规定的依赖注入到当前实例中。比如最常见的`ApplicationContextAware`接口,实现了这个接口的类都可以获取到一个 ApplicationContext 对象。当容器中每个对象的实例化过程走到 BeanPostProcessor 前置处理这一步时,容器会检测到之前注册到容器的 ApplicationContextAwareProcessor,然后就会调用其 postProcessBeforeInitialization()方法,检查并设置 Aware 相关依赖。看看代码吧,是不是很简单: + +``` +// 代码来自:org.springframework.context.support.ApplicationContextAwareProcessor +// 其postProcessBeforeInitialization方法调用了invokeAwareInterfaces方法 +private void invokeAwareInterfaces(Object bean) { + if (bean instanceof EnvironmentAware) { + ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment()); + } + if (bean instanceof ApplicationContextAware) { + ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); + } + // ...... +} +``` + +最后总结一下,本小节内容和你一起回顾了 Spring 容器的部分核心内容,限于篇幅不能写更多,但理解这部分内容,足以让您轻松理解 Spring Boot 的启动原理,如果在后续的学习过程中遇到一些晦涩难懂的知识,再回过头来看看 Spring 的核心知识,也许有意想不到的效果。也许 Spring Boot 的中文资料很少,但 Spring 的中文资料和书籍有太多太多,总有东西能给你启发。 + +## 二、夯实基础:JavaConfig 与常见 Annotation + +### 2.1、JavaConfig + +我们知道`bean`是 Spring IOC 中非常核心的概念,Spring 容器负责 bean 的生命周期的管理。在最初,Spring 使用 XML 配置文件的方式来描述 bean 的定义以及相互间的依赖关系,但随着 Spring 的发展,越来越多的人对这种方式表示不满,因为 Spring 项目的所有业务类均以 bean 的形式配置在 XML 文件中,造成了大量的 XML 文件,使项目变得复杂且难以管理。 + +后来,基于纯 Java Annotation 依赖注入框架`Guice`出世,其性能明显优于采用 XML 方式的 Spring,甚至有部分人认为,`Guice`可以完全取代 Spring(`Guice`仅是一个轻量级 IOC 框架,取代 Spring 还差的挺远)。正是这样的危机感,促使 Spring 及社区推出并持续完善了`JavaConfig`子项目,它基于 Java 代码和 Annotation 注解来描述 bean 之间的依赖绑定关系。比如,下面是使用 XML 配置方式来描述 bean 的定义: + +``` + +``` + +而基于 JavaConfig 的配置形式是这样的: + +``` +@Configuration +public class MoonBookConfiguration { + + // 任何标志了@Bean的方法,其返回值将作为一个bean注册到Spring的IOC容器中 + // 方法名默认成为该bean定义的id + @Bean + public BookService bookService() { + return new BookServiceImpl(); + } +} +``` + +如果两个 bean 之间有依赖关系的话,在 XML 配置中应该是这样: + +``` + + + + + + + + + +``` + +而在 JavaConfig 中则是这样: + +``` +@Configuration +public class MoonBookConfiguration { + + // 如果一个bean依赖另一个bean,则直接调用对应JavaConfig类中依赖bean的创建方法即可 + // 这里直接调用dependencyService() + @Bean + public BookService bookService() { + return new BookServiceImpl(dependencyService()); + } + + @Bean + public OtherService otherService() { + return new OtherServiceImpl(dependencyService()); + } + + @Bean + public DependencyService dependencyService() { + return new DependencyServiceImpl(); + } +} +``` + +你可能注意到这个示例中,有两个 bean 都依赖于 dependencyService,也就是说当初始化 bookService 时会调用`dependencyService()`,在初始化 otherService 时也会调用`dependencyService()`,那么问题来了?这时候 IOC 容器中是有一个 dependencyService 实例还是两个?这个问题留着大家思考吧,这里不再赘述。 + +### 2.2、@ComponentScan + +`@ComponentScan`注解对应 XML 配置形式中的``元素,表示启用组件扫描,Spring 会自动扫描所有通过注解配置的 bean,然后将其注册到 IOC 容器中。我们可以通过`basePackages`等属性来指定`@ComponentScan`自动扫描的范围,如果不指定,默认从声明`@ComponentScan`所在类的`package`进行扫描。正因为如此,SpringBoot 的启动类都默认在`src/main/java`下。 + +### 2.3、@Import + +`@Import`注解用于导入配置类,举个简单的例子: + +``` +@Configuration +public class MoonBookConfiguration { + @Bean + public BookService bookService() { + return new BookServiceImpl(); + } +} +``` + +现在有另外一个配置类,比如:`MoonUserConfiguration`,这个配置类中有一个 bean 依赖于`MoonBookConfiguration`中的 bookService,如何将这两个 bean 组合在一起?借助`@Import`即可: + +``` +@Configuration +// 可以同时导入多个配置类,比如:@Import({A.class,B.class}) +@Import(MoonBookConfiguration.class) +public class MoonUserConfiguration { + @Bean + public UserService userService(BookService bookService) { + return new BookServiceImpl(bookService); + } +} +``` + +需要注意的是,在 4.2 之前,`@Import`注解只支持导入配置类,但是在 4.2 之后,它支持导入普通类,并将这个类作为一个 bean 的定义注册到 IOC 容器中。 + +### 2.4、@Conditional + +`@Conditional`注解表示在满足某种条件后才初始化一个 bean 或者启用某些配置。它一般用在由`@Component`、`@Service`、`@Configuration`等注解标识的类上面,或者由`@Bean`标记的方法上。如果一个`@Configuration`类标记了`@Conditional`,则该类中所有标识了`@Bean`的方法和`@Import`注解导入的相关类将遵从这些条件。 + +在 Spring 里可以很方便的编写你自己的条件类,所要做的就是实现`Condition`接口,并覆盖它的`matches()`方法。举个例子,下面的简单条件类表示只有在`Classpath`里存在`JdbcTemplate`类时才生效: + +``` +public class JdbcTemplateCondition implements Condition { + + @Override + public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { + try { + conditionContext.getClassLoader().loadClass("org.springframework.jdbc.core.JdbcTemplate"); + return true; + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + return false; + } +} +``` + +当你用 Java 来声明 bean 的时候,可以使用这个自定义条件类: + +``` +@Conditional(JdbcTemplateCondition.class) +@Service +public MyService service() { + ...... +} +``` + +这个例子中只有当`JdbcTemplateCondition`类的条件成立时才会创建 MyService 这个 bean。也就是说 MyService 这 bean 的创建条件是`classpath`里面包含`JdbcTemplate`,否则这个 bean 的声明就会被忽略掉。 + +`Spring Boot`定义了很多有趣的条件,并把他们运用到了配置类上,这些配置类构成了`Spring Boot`的自动配置的基础。`Spring Boot`运用条件化配置的方法是:定义多个特殊的条件化注解,并将它们用到配置类上。下面列出了`Spring Boot`提供的部分条件化注解: + +| 条件化注解 | 配置生效条件 | +| ------------------------------- | ------------------------------------------------------- | +| @ConditionalOnBean | 配置了某个特定 bean | +| @ConditionalOnMissingBean | 没有配置特定的 bean | +| @ConditionalOnClass | Classpath 里有指定的类 | +| @ConditionalOnMissingClass | Classpath 里没有指定的类 | +| @ConditionalOnExpression | 给定的 Spring Expression Language 表达式计算结果为 true | +| @ConditionalOnJava | Java 的版本匹配特定指或者一个范围值 | +| @ConditionalOnProperty | 指定的配置属性要有一个明确的值 | +| @ConditionalOnResource | Classpath 里有指定的资源 | +| @ConditionalOnWebApplication | 这是一个 Web 应用程序 | +| @ConditionalOnNotWebApplication | 这不是一个 Web 应用程序 | + +### 2.5、@ConfigurationProperties 与@EnableConfigurationProperties + +当某些属性的值需要配置的时候,我们一般会在`application.properties`文件中新建配置项,然后在 bean 中使用`@Value`注解来获取配置的值,比如下面配置数据源的代码。 + +``` +// jdbc config +jdbc.mysql.url=jdbc:mysql://localhost:3306/sampledb +jdbc.mysql.username=root +jdbc.mysql.password=123456 +...... + +// 配置数据源 +@Configuration +public class HikariDataSourceConfiguration { + + @Value("jdbc.mysql.url") + public String url; + @Value("jdbc.mysql.username") + public String user; + @Value("jdbc.mysql.password") + public String password; + + @Bean + public HikariDataSource dataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(url); + hikariConfig.setUsername(user); + hikariConfig.setPassword(password); + // 省略部分代码 + return new HikariDataSource(hikariConfig); + } +} +``` + +使用`@Value`注解注入的属性通常都比较简单,如果同一个配置在多个地方使用,也存在不方便维护的问题(考虑下,如果有几十个地方在使用某个配置,而现在你想改下名字,你改怎么做?)。对于更为复杂的配置,Spring Boot 提供了更优雅的实现方式,那就是`@ConfigurationProperties`注解。我们可以通过下面的方式来改写上面的代码: + +``` +@Component +// 还可以通过@PropertySource("classpath:jdbc.properties")来指定配置文件 +@ConfigurationProperties("jdbc.mysql") +// 前缀=jdbc.mysql,会在配置文件中寻找jdbc.mysql.*的配置项 +pulic class JdbcConfig { + public String url; + public String username; + public String password; +} + +@Configuration +public class HikariDataSourceConfiguration { + + @AutoWired + public JdbcConfig config; + + @Bean + public HikariDataSource dataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(config.url); + hikariConfig.setUsername(config.username); + hikariConfig.setPassword(config.password); + // 省略部分代码 + return new HikariDataSource(hikariConfig); + } +} +``` + +`@ConfigurationProperties`对于更为复杂的配置,处理起来也是得心应手,比如有如下配置文件: + +``` +#App +app.menus[0].title=Home +app.menus[0].name=Home +app.menus[0].path=/ +app.menus[1].title=Login +app.menus[1].name=Login +app.menus[1].path=/login + +app.compiler.timeout=5 +app.compiler.output-folder=/temp/ + +app.error=/error/ +``` + +可以定义如下配置类来接收这些属性 + +``` +@Component +@ConfigurationProperties("app") +public class AppProperties { + + public String error; + public List menus = new ArrayList<>(); + public Compiler compiler = new Compiler(); + + public static class Menu { + public String name; + public String path; + public String title; + } + + public static class Compiler { + public String timeout; + public String outputFolder; + } +} +``` + +`@EnableConfigurationProperties`注解表示对`@ConfigurationProperties`的内嵌支持,默认会将对应 Properties Class 作为 bean 注入的 IOC 容器中,即在相应的 Properties 类上不用加`@Component`注解。 + +## 三、削铁如泥:SpringFactoriesLoader 详解 + +JVM 提供了 3 种类加载器:`BootstrapClassLoader`、`ExtClassLoader`、`AppClassLoader`分别加载 Java 核心类库、扩展类库以及应用的类路径(`CLASSPATH`)下的类库。JVM 通过双亲委派模型进行类的加载,我们也可以通过继承`java.lang.classloader`实现自己的类加载器。 + +何为双亲委派模型?当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的 BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。 + +采用双亲委派模型的一个好处是保证使用不同类加载器最终得到的都是同一个对象,这样就可以保证 Java 核心库的类型安全,比如,加载位于 rt.jar 包中的`java.lang.Object`类,不管是哪个加载器加载这个类,最终都是委托给顶层的 BootstrapClassLoader 来加载的,这样就可以保证任何的类加载器最终得到的都是同样一个 Object 对象。查看 ClassLoader 的源码,对双亲委派模型会有更直观的认识: + +``` +protected Class loadClass(String name, boolean resolve) { + synchronized (getClassLoadingLock(name)) { + // 首先,检查该类是否已经被加载,如果从JVM缓存中找到该类,则直接返回 + Class c = findLoadedClass(name); + if (c == null) { + try { + // 遵循双亲委派的模型,首先会通过递归从父加载器开始找, + // 直到父类加载器是BootstrapClassLoader为止 + if (parent != null) { + c = parent.loadClass(name, false); + } else { + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) {} + if (c == null) { + // 如果还找不到,尝试通过findClass方法去寻找 + // findClass是留给开发者自己实现的,也就是说 + // 自定义类加载器时,重写此方法即可 + c = findClass(name); + } + } + if (resolve) { + resolveClass(c); + } + return c; + } +} +``` + +但双亲委派模型并不能解决所有的类加载器问题,比如,Java 提供了很多服务提供者接口(`Service Provider Interface`,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JNDI、JAXP 等,这些 SPI 的接口由核心类库提供,却由第三方实现,这样就存在一个问题:SPI 的接口是 Java 核心库的一部分,是由 BootstrapClassLoader 加载的;SPI 实现的 Java 类一般是由 AppClassLoader 来加载的。BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给 AppClassLoader,因为它是最顶层的类加载器。也就是说,双亲委派模型并不能解决这个问题。 + +线程上下文类加载器(`ContextClassLoader`)正好解决了这个问题。从名称上看,可能会误解为它是一种新的类加载器,实际上,它仅仅是 Thread 类的一个变量而已,可以通过`setContextClassLoader(ClassLoader cl)`和`getContextClassLoader()`来设置和获取该对象。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是 AppClassLoader。在核心类库使用 SPI 接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。但在 JDBC 中,你可能会看到一种更直接的实现方式,比如,JDBC 驱动管理`java.sql.Driver`中的`loadInitialDrivers()`方法中,你可以直接看到 JDK 是如何加载驱动的: + +``` +for (String aDriver : driversList) { + try { + // 直接使用AppClassLoader + Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); + } catch (Exception ex) { + println("DriverManager.Initialize: load failed: " + ex); + } +} +``` + +其实讲解线程上下文类加载器,最主要是让大家在看到`Thread.currentThread().getClassLoader()`和`Thread.currentThread().getContextClassLoader()`时不会一脸懵逼,这两者除了在许多底层框架中取得的 ClassLoader 可能会有所不同外,其他大多数业务场景下都是一样的,大家只要知道它是为了解决什么问题而存在的即可。 + +类加载器除了加载 class 外,还有一个非常重要功能,就是加载资源,它可以从 jar 包中读取任何资源文件,比如,`ClassLoader.getResources(String name)`方法就是用于读取 jar 包中的资源文件,其代码如下: + +``` +public Enumeration getResources(String name) throws IOException { + Enumeration[] tmp = (Enumeration[]) new Enumeration[2]; + if (parent != null) { + tmp[0] = parent.getResources(name); + } else { + tmp[0] = getBootstrapResources(name); + } + tmp[1] = findResources(name); + return new CompoundEnumeration<>(tmp); +} +``` + +是不是觉得有点眼熟,不错,它的逻辑其实跟类加载的逻辑是一样的,首先判断父类加载器是否为空,不为空则委托父类加载器执行资源查找任务,直到 BootstrapClassLoader,最后才轮到自己查找。而不同的类加载器负责扫描不同路径下的 jar 包,就如同加载 class 一样,最后会扫描所有的 jar 包,找到符合条件的资源文件。 + +类加载器的`findResources(name)`方法会遍历其负责加载的所有 jar 包,找到 jar 包中名称为 name 的资源文件,这里的资源可以是任何文件,甚至是.class 文件,比如下面的示例,用于查找 Array.class 文件: + +``` +// 寻找Array.class文件 +public static void main(String[] args) throws Exception{ + // Array.class的完整路径 + String name = "java/sql/Array.class"; + Enumeration urls = Thread.currentThread().getContextClassLoader().getResources(name); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + System.out.println(url.toString()); + } +} +``` + +运行后可以得到如下结果: + +``` +$JAVA_HOME/jre/lib/rt.jar!/java/sql/Array.class +``` + +根据资源文件的 URL,可以构造相应的文件来读取资源内容。 + +看到这里,你可能会感到挺奇怪的,你不是要详解`SpringFactoriesLoader`吗?上来讲了一堆 ClassLoader 是几个意思?看下它的源码你就知道了: + +``` +public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; +// spring.factories文件的格式为:key=value1,value2,value3 +// 从所有的jar包中找到META-INF/spring.factories文件 +// 然后从文件中解析出key=factoryClass类名称的所有value值 +public static List loadFactoryNames(Class factoryClass, ClassLoader classLoader) { + String factoryClassName = factoryClass.getName(); + // 取得资源文件的URL + Enumeration urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); + List result = new ArrayList(); + // 遍历所有的URL + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + // 根据资源文件URL解析properties文件 + Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); + String factoryClassNames = properties.getProperty(factoryClassName); + // 组装数据,并返回 + result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); + } + return result; +} +``` + +有了前面关于 ClassLoader 的知识,再来理解这段代码,是不是感觉豁然开朗:从`CLASSPATH`下的每个 Jar 包中搜寻所有`META-INF/spring.factories`配置文件,然后将解析 properties 文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去 ClassPath 路径下查找,会扫描所有路径下的 Jar 包,只不过这个文件只会在 Classpath 下的 jar 包中。来简单看下`spring.factories`文件的内容吧: + +``` +// 来自 org.springframework.boot.autoconfigure下的META-INF/spring.factories +// EnableAutoConfiguration后文会讲到,它用于开启Spring Boot自动配置功能 +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ +org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ +org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration\ +``` + +执行`loadFactoryNames(EnableAutoConfiguration.class, classLoader)`后,得到对应的一组`@Configuration`类, +我们就可以通过反射实例化这些类然后注入到 IOC 容器中,最后容器里就有了一系列标注了`@Configuration`的 JavaConfig 形式的配置类。 + +这就是`SpringFactoriesLoader`,它本质上属于 Spring 框架私有的一种扩展方案,类似于 SPI,Spring Boot 在 Spring 基础上的很多核心功能都是基于此,希望大家可以理解。 + +## 四、另一件武器:Spring 容器的事件监听机制 + +过去,事件监听机制多用于图形界面编程,比如:**点击**按钮、在文本框**输入**内容等操作被称为事件,而当事件触发时,应用程序作出一定的响应则表示应用监听了这个事件,而在服务器端,事件的监听机制更多的用于异步通知以及监控和异常处理。Java 提供了实现事件监听机制的两个基础类:自定义事件类型扩展自`java.util.EventObject`、事件的监听器扩展自`java.util.EventListener`。来看一个简单的实例:简单的监控一个方法的耗时。 + +首先定义事件类型,通常的做法是扩展 EventObject,随着事件的发生,相应的状态通常都封装在此类中: + +``` +public class MethodMonitorEvent extends EventObject { + // 时间戳,用于记录方法开始执行的时间 + public long timestamp; + + public MethodMonitorEvent(Object source) { + super(source); + } +} +``` + +事件发布之后,相应的监听器即可对该类型的事件进行处理,我们可以在方法开始执行之前发布一个 begin 事件,在方法执行结束之后发布一个 end 事件,相应地,事件监听器需要提供方法对这两种情况下接收到的事件进行处理: + +``` +// 1、定义事件监听接口 +public interface MethodMonitorEventListener extends EventListener { + // 处理方法执行之前发布的事件 + public void onMethodBegin(MethodMonitorEvent event); + // 处理方法结束时发布的事件 + public void onMethodEnd(MethodMonitorEvent event); +} +// 2、事件监听接口的实现:如何处理 +public class AbstractMethodMonitorEventListener implements MethodMonitorEventListener { + + @Override + public void onMethodBegin(MethodMonitorEvent event) { + // 记录方法开始执行时的时间 + event.timestamp = System.currentTimeMillis(); + } + + @Override + public void onMethodEnd(MethodMonitorEvent event) { + // 计算方法耗时 + long duration = System.currentTimeMillis() - event.timestamp; + System.out.println("耗时:" + duration); + } +} +``` + +事件监听器接口针对不同的事件发布实际提供相应的处理方法定义,最重要的是,其方法只接收 MethodMonitorEvent 参数,说明这个监听器类只负责监听器对应的事件并进行处理。有了事件和监听器,剩下的就是发布事件,然后让相应的监听器监听并处理。通常情况,我们会有一个事件发布者,它本身作为事件源,在合适的时机,将相应的事件发布给对应的事件监听器: + +``` +public class MethodMonitorEventPublisher { + + private List listeners = new ArrayList(); + + public void methodMonitor() { + MethodMonitorEvent eventObject = new MethodMonitorEvent(this); + publishEvent("begin",eventObject); + // 模拟方法执行:休眠5秒钟 + TimeUnit.SECONDS.sleep(5); + publishEvent("end",eventObject); + + } + + private void publishEvent(String status,MethodMonitorEvent event) { + // 避免在事件处理期间,监听器被移除,这里为了安全做一个复制操作 + List copyListeners = ➥ new ArrayList(listeners); + for (MethodMonitorEventListener listener : copyListeners) { + if ("begin".equals(status)) { + listener.onMethodBegin(event); + } else { + listener.onMethodEnd(event); + } + } + } + + public static void main(String[] args) { + MethodMonitorEventPublisher publisher = new MethodMonitorEventPublisher(); + publisher.addEventListener(new AbstractMethodMonitorEventListener()); + publisher.methodMonitor(); + } + // 省略实现 + public void addEventListener(MethodMonitorEventListener listener) {} + public void removeEventListener(MethodMonitorEventListener listener) {} + public void removeAllListeners() {} +``` + +对于事件发布者(事件源)通常需要关注两点: + +1. 在合适的时机发布事件。此例中的 methodMonitor()方法是事件发布的源头,其在方法执行之前和结束之后两个时间点发布 MethodMonitorEvent 事件,每个时间点发布的事件都会传给相应的监听器进行处理。在具体实现时需要注意的是,事件发布是顺序执行,为了不影响处理性能,事件监听器的处理逻辑应尽量简单。 +2. 事件监听器的管理。publisher 类中提供了事件监听器的注册与移除方法,这样客户端可以根据实际情况决定是否需要注册新的监听器或者移除某个监听器。如果这里没有提供 remove 方法,那么注册的监听器示例将一直被 MethodMonitorEventPublisher 引用,即使已经废弃不用了,也依然在发布者的监听器列表中,这会导致隐性的内存泄漏。 + +#### Spring 容器内的事件监听机制 + +Spring 的 ApplicationContext 容器内部中的所有事件类型均继承自`org.springframework.context.ApplicationEvent`,容器中的所有监听器都实现`org.springframework.context.ApplicationListener`接口,并且以 bean 的形式注册在容器中。一旦在容器内发布 ApplicationEvent 及其子类型的事件,注册到容器的 ApplicationListener 就会对这些事件进行处理。 + +你应该已经猜到是怎么回事了。 + +ApplicationEvent 继承自 EventObject,Spring 提供了一些默认的实现,比如:`ContextClosedEvent`表示容器在即将关闭时发布的事件类型,`ContextRefreshedEvent`表示容器在初始化或者刷新的时候发布的事件类型...... + +容器内部使用 ApplicationListener 作为事件监听器接口定义,它继承自 EventListener。ApplicationContext 容器在启动时,会自动识别并加载 EventListener 类型的 bean,一旦容器内有事件发布,将通知这些注册到容器的 EventListener。 + +ApplicationContext 接口继承了 ApplicationEventPublisher 接口,该接口提供了`void publishEvent(ApplicationEvent event)`方法定义,不难看出,ApplicationContext 容器担当的就是事件发布者的角色。如果有兴趣可以查看`AbstractApplicationContext.publishEvent(ApplicationEvent event)`方法的源码:ApplicationContext 将事件的发布以及监听器的管理工作委托给`ApplicationEventMulticaster`接口的实现类。在容器启动时,会检查容器内是否存在名为 applicationEventMulticaster 的 ApplicationEventMulticaster 对象实例。如果有就使用其提供的实现,没有就默认初始化一个 SimpleApplicationEventMulticaster 作为实现。 + +最后,如果我们业务需要在容器内部发布事件,只需要为其注入 ApplicationEventPublisher 依赖即可:实现 ApplicationEventPublisherAware 接口或者 ApplicationContextAware 接口(Aware 接口相关内容请回顾上文)。 + +## 五、出神入化:揭秘自动配置原理 + +典型的 Spring Boot 应用的启动类一般均位于`src/main/java`根路径下,比如`MoonApplication`类: + +``` +@SpringBootApplication +public class MoonApplication { + + public static void main(String[] args) { + SpringApplication.run(MoonApplication.class, args); + } +} +``` + +其中`@SpringBootApplication`开启组件扫描和自动配置,而`SpringApplication.run`则负责启动引导应用程序。`@SpringBootApplication`是一个复合`Annotation`,它将三个有用的注解组合在一起: + +``` +@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`就是`@Configuration`,它是 Spring 框架的注解,标明该类是一个`JavaConfig`配置类。而`@ComponentScan`启用组件扫描,前文已经详细讲解过,这里着重关注`@EnableAutoConfiguration`。 + +`@EnableAutoConfiguration`注解表示开启 Spring Boot 自动配置功能,Spring Boot 会根据应用的依赖、自定义的 bean、classpath 下有没有某个类 等等因素来猜测你需要的 bean,然后注册到 IOC 容器中。那`@EnableAutoConfiguration`是如何推算出你的需求?首先看下它的定义: + +``` +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@AutoConfigurationPackage +@Import(EnableAutoConfigurationImportSelector.class) +public @interface EnableAutoConfiguration { + // ...... +} +``` + +你的关注点应该在`@Import(EnableAutoConfigurationImportSelector.class)`上了,前文说过,`@Import`注解用于导入类,并将这个类作为一个 bean 的定义注册到容器中,这里它将把`EnableAutoConfigurationImportSelector`作为 bean 注入到容器中,而这个类会将所有符合条件的@Configuration 配置都加载到容器中,看看它的代码: + +``` +public String[] selectImports(AnnotationMetadata annotationMetadata) { + // 省略了大部分代码,保留一句核心代码 + // 注意:SpringBoot最近版本中,这句代码被封装在一个单独的方法中 + // SpringFactoriesLoader相关知识请参考前文 + List factories = new ArrayList(new LinkedHashSet( + SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, this.beanClassLoader))); +} +``` + +这个类会扫描所有的 jar 包,将所有符合条件的@Configuration 配置类注入的容器中,何为符合条件,看看`META-INF/spring.factories`的文件内容: + +``` +// 来自 org.springframework.boot.autoconfigure下的META-INF/spring.factories +// 配置的key = EnableAutoConfiguration,与代码中一致 +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\ +org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ +org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration\ +..... +``` + +以`DataSourceAutoConfiguration`为例,看看 Spring Boot 是如何自动配置的: + +``` +@Configuration +@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) +@EnableConfigurationProperties(DataSourceProperties.class) +@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class }) +public class DataSourceAutoConfiguration { +} +``` + +分别说一说: + +- `@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })`:当 Classpath 中存在 DataSource 或者 EmbeddedDatabaseType 类时才启用这个配置,否则这个配置将被忽略。 +- `@EnableConfigurationProperties(DataSourceProperties.class)`:将 DataSource 的默认配置类注入到 IOC 容器中,DataSourceproperties 定义为: + +``` +// 提供对datasource配置信息的支持,所有的配置前缀为:spring.datasource +@ConfigurationProperties(prefix = "spring.datasource") +public class DataSourceProperties { + private ClassLoader classLoader; + private Environment environment; + private String name = "testdb"; + ...... +} +``` + +- `@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })`:导入其他额外的配置,就以`DataSourcePoolMetadataProvidersConfiguration`为例吧。 + +``` +@Configuration +public class DataSourcePoolMetadataProvidersConfiguration { + + @Configuration + @ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class) + static class TomcatDataSourcePoolMetadataProviderConfiguration { + @Bean + public DataSourcePoolMetadataProvider tomcatPoolDataSourceMetadataProvider() { + ..... + } + } + ...... +} +``` + +DataSourcePoolMetadataProvidersConfiguration 是数据库连接池提供者的一个配置类,即 Classpath 中存在`org.apache.tomcat.jdbc.pool.DataSource.class`,则使用 tomcat-jdbc 连接池,如果 Classpath 中存在`HikariDataSource.class`则使用 Hikari 连接池。 + +这里仅描述了 DataSourceAutoConfiguration 的冰山一角,但足以说明 Spring Boot 如何利用条件话配置来实现自动配置的。回顾一下,`@EnableAutoConfiguration`中导入了 EnableAutoConfigurationImportSelector 类,而这个类的`selectImports()`通过 SpringFactoriesLoader 得到了大量的配置类,而每一个配置类则根据条件化配置来做出决策,以实现自动配置。 + +整个流程很清晰,但漏了一个大问题:`EnableAutoConfigurationImportSelector.selectImports()`是何时执行的?其实这个方法会在容器启动过程中执行:`AbstractApplicationContext.refresh()`,更多的细节在下一小节中说明。 + +## 六、启动引导:Spring Boot 应用启动的秘密 + +### 6.1 SpringApplication 初始化 + +SpringBoot 整个启动流程分为两个步骤:初始化一个 SpringApplication 对象、执行该对象的 run 方法。看下 SpringApplication 的初始化流程,SpringApplication 的构造方法中调用 initialize(Object[] sources)方法,其代码如下: + +``` +private void initialize(Object[] sources) { + if (sources != null && sources.length > 0) { + this.sources.addAll(Arrays.asList(sources)); + } + // 判断是否是Web项目 + this.webEnvironment = deduceWebEnvironment(); + setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); + setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); + // 找到入口类 + this.mainApplicationClass = deduceMainApplicationClass(); +} +``` + +初始化流程中最重要的就是通过 SpringFactoriesLoader 找到`spring.factories`文件中配置的`ApplicationContextInitializer`和`ApplicationListener`两个接口的实现类名称,以便后期构造相应的实例。`ApplicationContextInitializer`的主要目的是在`ConfigurableApplicationContext`做 refresh 之前,对 ConfigurableApplicationContext 实例做进一步的设置或处理。ConfigurableApplicationContext 继承自 ApplicationContext,其主要提供了对 ApplicationContext 进行设置的能力。 + +实现一个 ApplicationContextInitializer 非常简单,因为它只有一个方法,但大多数情况下我们没有必要自定义一个 ApplicationContextInitializer,即便是 Spring Boot 框架,它默认也只是注册了两个实现,毕竟 Spring 的容器已经非常成熟和稳定,你没有必要来改变它。 + +而`ApplicationListener`的目的就没什么好说的了,它是 Spring 框架对 Java 事件监听机制的一种框架实现,具体内容在前文 Spring 事件监听机制这个小节有详细讲解。这里主要说说,如果你想为 Spring Boot 应用添加监听器,该如何实现? + +Spring Boot 提供两种方式来添加自定义监听器: + +- 通过`SpringApplication.addListeners(ApplicationListener... listeners)`或者`SpringApplication.setListeners(Collection> listeners)`两个方法来添加一个或者多个自定义监听器 +- 既然 SpringApplication 的初始化流程中已经从`spring.factories`中获取到`ApplicationListener`的实现类,那么我们直接在自己的 jar 包的`META-INF/spring.factories`文件中新增配置即可: + +``` +org.springframework.context.ApplicationListener=\ +cn.moondev.listeners.xxxxListener\ +``` + +关于 SpringApplication 的初始化,我们就说这么多。 + +### 6.2 Spring Boot 启动流程 + +Spring Boot 应用的整个启动流程都封装在 SpringApplication.run 方法中,其整个流程真的是太长太长了,但本质上就是在 Spring 容器启动的基础上做了大量的扩展,按照这个思路来看看源码: + +``` +public ConfigurableApplicationContext run(String... args) { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + ConfigurableApplicationContext context = null; + FailureAnalyzers analyzers = null; + configureHeadlessProperty(); + // ① + SpringApplicationRunListeners listeners = getRunListeners(args); + listeners.starting(); + try { + // ② + ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); + ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments); + // ③ + Banner printedBanner = printBanner(environment); + // ④ + context = createApplicationContext(); + // ⑤ + analyzers = new FailureAnalyzers(context); + // ⑥ + prepareContext(context, environment, listeners, applicationArguments,printedBanner); + // ⑦ + refreshContext(context); + // ⑧ + afterRefresh(context, applicationArguments); + // ⑨ + listeners.finished(context, null); + stopWatch.stop(); + return context; + } + catch (Throwable ex) { + handleRunFailure(context, listeners, analyzers, ex); + throw new IllegalStateException(ex); + } + } +``` + +① 通过 SpringFactoriesLoader 查找并加载所有的`SpringApplicationRunListeners`,通过调用 starting()方法通知所有的 SpringApplicationRunListeners:应用开始启动了。SpringApplicationRunListeners 其本质上就是一个事件发布者,它在 SpringBoot 应用启动的不同时间点发布不同应用事件类型(ApplicationEvent),如果有哪些事件监听者(ApplicationListener)对这些事件感兴趣,则可以接收并且处理。还记得初始化流程中,SpringApplication 加载了一系列 ApplicationListener 吗?这个启动流程中没有发现有发布事件的代码,其实都已经在 SpringApplicationRunListeners 这儿实现了。 + +简单的分析一下其实现流程,首先看下 SpringApplicationRunListener 的源码: + +``` +public interface SpringApplicationRunListener { + + // 运行run方法时立即调用此方法,可以用户非常早期的初始化工作 + void starting(); + + // Environment准备好后,并且ApplicationContext创建之前调用 + void environmentPrepared(ConfigurableEnvironment environment); + + // ApplicationContext创建好后立即调用 + void contextPrepared(ConfigurableApplicationContext context); + + // ApplicationContext加载完成,在refresh之前调用 + void contextLoaded(ConfigurableApplicationContext context); + + // 当run方法结束之前调用 + void finished(ConfigurableApplicationContext context, Throwable exception); + +} +``` + +SpringApplicationRunListener 只有一个实现类:`EventPublishingRunListener`。① 处的代码只会获取到一个 EventPublishingRunListener 的实例,我们来看看 starting()方法的内容: + +``` +public void starting() { + // 发布一个ApplicationStartedEvent + this.initialMulticaster.multicastEvent(new ApplicationStartedEvent(this.application, this.args)); +} +``` + +顺着这个逻辑,你可以在 ② 处的`prepareEnvironment()`方法的源码中找到`listeners.environmentPrepared(environment);`即 SpringApplicationRunListener 接口的第二个方法,那不出你所料,`environmentPrepared()`又发布了另外一个事件`ApplicationEnvironmentPreparedEvent`。接下来会发生什么,就不用我多说了吧。 + +② 创建并配置当前应用将要使用的`Environment`,Environment 用于描述应用程序当前的运行环境,其抽象了两个方面的内容:配置文件(profile)和属性(properties),开发经验丰富的同学对这两个东西一定不会陌生:不同的环境(eg:生产环境、预发布环境)可以使用不同的配置文件,而属性则可以从配置文件、环境变量、命令行参数等来源获取。因此,当 Environment 准备好后,在整个应用的任何时候,都可以从 Environment 中获取资源。 + +总结起来,② 处的两句代码,主要完成以下几件事: + +- 判断 Environment 是否存在,不存在就创建(如果是 web 项目就创建`StandardServletEnvironment`,否则创建`StandardEnvironment`) +- 配置 Environment:配置 profile 以及 properties +- 调用 SpringApplicationRunListener 的`environmentPrepared()`方法,通知事件监听者:应用的 Environment 已经准备好 + +③、SpringBoot 应用在启动时会输出这样的东西: + +``` + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v1.5.6.RELEASE) +``` + +如果想把这个东西改成自己的涂鸦,你可以研究以下 Banner 的实现,这个任务就留给你们吧。 + +④、根据是否是 web 项目,来创建不同的 ApplicationContext 容器。 + +⑤、创建一系列`FailureAnalyzer`,创建流程依然是通过 SpringFactoriesLoader 获取到所有实现 FailureAnalyzer 接口的 class,然后在创建对应的实例。FailureAnalyzer 用于分析故障并提供相关诊断信息。 + +⑥、初始化 ApplicationContext,主要完成以下工作: + +- 将准备好的 Environment 设置给 ApplicationContext +- 遍历调用所有的 ApplicationContextInitializer 的`initialize()`方法来对已经创建好的 ApplicationContext 进行进一步的处理 +- 调用 SpringApplicationRunListener 的`contextPrepared()`方法,通知所有的监听者:ApplicationContext 已经准备完毕 +- 将所有的 bean 加载到容器中 +- 调用 SpringApplicationRunListener 的`contextLoaded()`方法,通知所有的监听者:ApplicationContext 已经装载完毕 + +⑦、调用 ApplicationContext 的`refresh()`方法,完成 IoC 容器可用的最后一道工序。从名字上理解为刷新容器,那何为刷新?就是插手容器的启动,联系一下第一小节的内容。那如何刷新呢?且看下面代码: + +``` +// 摘自refresh()方法中一句代码 +invokeBeanFactoryPostProcessors(beanFactory); +``` + +看看这个方法的实现: + +``` +protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) { + PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); + ...... +} +``` + +获取到所有的`BeanFactoryPostProcessor`来对容器做一些额外的操作。BeanFactoryPostProcessor 允许我们在容器实例化相应对象之前,对注册到容器的 BeanDefinition 所保存的信息做一些额外的操作。这里的 getBeanFactoryPostProcessors()方法可以获取到 3 个 Processor: + +``` +ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessor +SharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessor +ConfigFileApplicationListener$PropertySourceOrderingPostProcessor +``` + +不是有那么多 BeanFactoryPostProcessor 的实现类,为什么这儿只有这 3 个?因为在初始化流程获取到的各种 ApplicationContextInitializer 和 ApplicationListener 中,只有上文 3 个做了类似于如下操作: + +``` +public void initialize(ConfigurableApplicationContext context) { + context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks())); +} +``` + +然后你就可以进入到`PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()`方法了,这个方法除了会遍历上面的 3 个 BeanFactoryPostProcessor 处理外,还会获取类型为`BeanDefinitionRegistryPostProcessor`的 bean:`org.springframework.context.annotation.internalConfigurationAnnotationProcessor`,对应的 Class 为`ConfigurationClassPostProcessor`。`ConfigurationClassPostProcessor`用于解析处理各种注解,包括:@Configuration、@ComponentScan、@Import、@PropertySource、@ImportResource、@Bean。当处理`@import`注解的时候,就会调用自动配置这一小节中的`EnableAutoConfigurationImportSelector.selectImports()`来完成自动配置功能。其他的这里不再多讲,如果你有兴趣,可以查阅参考资料 6。 + +⑧、查找当前 context 中是否注册有 CommandLineRunner 和 ApplicationRunner,如果有则遍历执行它们。 + +⑨、执行所有 SpringApplicationRunListener 的 finished()方法。 + +这就是 Spring Boot 的整个启动流程,其核心就是在 Spring 容器初始化并启动的基础上加入各种扩展点,这些扩展点包括:ApplicationContextInitializer、ApplicationListener 以及各种 BeanFactoryPostProcessor 等等。你对整个流程的细节不必太过关注,甚至没弄明白也没有关系,你只要理解这些扩展点是在何时如何工作的,能让它们为你所用即可。 + +整个启动流程确实非常复杂,可以查询参考资料中的部分章节和内容,对照着源码,多看看,我想最终你都能弄清楚的。言而总之,Spring 才是核心,理解清楚 Spring 容器的启动流程,那 Spring Boot 启动流程就不在话下了。 + +## 参考资料 + +[1][王福强 著;springboot 揭秘:快速构建微服务体系; 机械工业出版社, 2016](https://link.jianshu.com/?t=http%3A%2F%2Funion-click.jd.com%2Fjdc%3Fd%3D4jESQ9) +[2][王福强 著;spring 揭秘; 人民邮件出版社, 2009](https://link.jianshu.com/?t=http%3A%2F%2Funion-click.jd.com%2Fjdc%3Fd%3DyzfgeF) +[3][craig walls 著;丁雪丰 译;spring boot 实战;中国工信出版集团 人民邮电出版社,2016](https://link.jianshu.com/?t=http%3A%2F%2Funion-click.jd.com%2Fjdc%3Fd%3DAQ6oHO) +[4][深入探讨 java 类加载器](https://link.jianshu.com/?t=https%3A%2F%2Fwww.ibm.com%2Fdeveloperworks%2Fcn%2Fjava%2Fj-lo-classloader%2F) : [www.ibm.com/developerwo…](https://link.jianshu.com/?t=https%3A%2F%2Fwww.ibm.com%2Fdeveloperworks%2Fcn%2Fjava%2Fj-lo-classloader%2F) +[5][spring boot 实战:自动配置原理分析](https://link.jianshu.com/?t=http%3A%2F%2Fblog.csdn.net%2Fliaokailin%2Farticle%2Fdetails%2F49559951) : [blog.csdn.net/liaokailin/…](https://link.jianshu.com/?t=http%3A%2F%2Fblog.csdn.net%2Fliaokailin%2Farticle%2Fdetails%2F49559951) +[6][spring boot实战:spring boot bean加载源码分析](https://link.jianshu.com/?t=http%3A%2F%2Fblog.csdn.net%2Fliaokailin%2Farticle%2Fdetails%2F49107209): [blog.csdn.net/liaokailin/…](https://link.jianshu.com/?t=http%3A%2F%2Fblog.csdn.net%2Fliaokailin%2Farticle%2Fdetails%2F49107209) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/22.SpringBoot\345\237\272\346\234\254\345\216\237\347\220\206.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/22.SpringBoot\345\237\272\346\234\254\345\216\237\347\220\206.md" new file mode 100644 index 00000000..a7ee34ae --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/22.SpringBoot\345\237\272\346\234\254\345\216\237\347\220\206.md" @@ -0,0 +1,286 @@ +--- +title: SpringBoot 基本原理 +date: 2020-08-12 07:01:26 +order: 22 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/dbf521/ +--- + +# SpringBoot 基本原理 + +SpringBoot 为我们做的自动配置,确实方便快捷,但一直搞不明白它的内部启动原理,这次就来一步步解开 SpringBoot 的神秘面纱,让它不再神秘。 + +![img](https:////upload-images.jianshu.io/upload_images/6430208-ebcb376f96103703.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) + +--- + +```java +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +从上面代码可以看出,**Annotation 定义(@SpringBootApplication)和类定义(SpringApplication.run)**最为耀眼,所以要揭开 SpringBoot 的神秘面纱,我们要从这两位开始就可以了。 + +## SpringBootApplication 背后的秘密 + +```kotlin +@Target(ElementType.TYPE) // 注解的适用范围,其中TYPE用于描述类、接口(包括包注解类型)或enum声明 +@Retention(RetentionPolicy.RUNTIME) // 注解的生命周期,保留到class文件中(三个生命周期) +@Documented // 表明这个注解应该被javadoc记录 +@Inherited // 子类可以继承该注解 +@SpringBootConfiguration // 继承了Configuration,表示当前是注解类 +@EnableAutoConfiguration // 开启springboot的注解功能,springboot的四大神器之一,其借助@import的帮助 +@ComponentScan(excludeFilters = { // 扫描路径设置(具体使用待确认) + @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), + @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) +public @interface SpringBootApplication { +... +} +``` + +虽然定义使用了多个 Annotation 进行了原信息标注,但实际上重要的只有三个 Annotation: + +**@Configuration**(@SpringBootConfiguration 点开查看发现里面还是应用了@Configuration) +**@EnableAutoConfiguration +@ComponentScan** +所以,如果我们使用如下的 SpringBoot 启动类,整个 SpringBoot 应用依然可以与之前的启动类功能对等: + +```java +@Configuration +@EnableAutoConfiguration +@ComponentScan +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +每次写这 3 个比较累,所以写一个@SpringBootApplication 方便点。接下来分别介绍这 3 个 Annotation。 + +## @Configuration + +这里的@Configuration 对我们来说不陌生,**它就是 JavaConfig 形式的 Spring Ioc 容器的配置类使用的那个@Configuration**,SpringBoot 社区推荐使用基于 JavaConfig 的配置形式,所以,这里的启动类标注了@Configuration 之后,本身其实也是一个 IoC 容器的配置类。 +举几个简单例子回顾下,XML 跟 config 配置方式的区别: + +表达形式层面 +基于 XML 配置的方式是这样: + +```xml + + + + +``` + +而基于 JavaConfig 的配置方式是这样: + +```java +@Configuration +public class MockConfiguration{ + //bean定义 +} +``` + +**任何一个标注了@Configuration 的 Java 类定义都是一个 JavaConfig 配置类。** + +注册 bean 定义层面 +基于 XML 的配置形式是这样: + +```csharp + + ... + +``` + +而基于 JavaConfig 的配置形式是这样的: + +```java +@Configuration +public class MockConfiguration{ + @Bean + public MockService mockService(){ + return new MockServiceImpl(); + } +} +``` + +**任何一个标注了@Bean 的方法,其返回值将作为一个 bean 定义注册到 Spring 的 IoC 容器,方法名将默认成该 bean 定义的 id。** + +表达依赖注入关系层面 +为了表达 bean 与 bean 之间的依赖关系,在 XML 形式中一般是这样: + +```jsx + + + + + +``` + +而基于 JavaConfig 的配置形式是这样的: + +```java +@Configuration +public class MockConfiguration{ + @Bean + public MockService mockService(){ + return new MockServiceImpl(dependencyService()); + } + + @Bean + public DependencyService dependencyService(){ + return new DependencyServiceImpl(); + } +} +``` + +**如果一个 bean 的定义依赖其他 bean,则直接调用对应的 JavaConfig 类中依赖 bean 的创建方法就可以了。** + +## @ComponentScan + +**@ComponentScan 这个注解在 Spring 中很重要,它对应 XML 配置中的元素,@ComponentScan 的功能其实就是自动扫描并加载符合条件的组件(比如@Component 和@Repository 等)或者 bean 定义,最终将这些 bean 定义加载到 IoC 容器中。** + +我们可以通过 basePackages 等属性来细粒度的定制@ComponentScan 自动扫描的范围,如果不指定,则默认 Spring 框架实现会从声明@ComponentScan 所在类的 package 进行扫描。 + +> 注:所以 SpringBoot 的启动类最好是放在 root package 下,因为默认不指定 basePackages。 + +## @EnableAutoConfiguration + +个人感觉**@EnableAutoConfiguration 这个 Annotation 最为重要**,所以放在最后来解读,大家是否还记得 Spring 框架提供的各种名字为@Enable 开头的 Annotation 定义?比如@EnableScheduling、@EnableCaching、@EnableMBeanExport 等,@EnableAutoConfiguration 的理念和做事方式其实一脉相承,简单概括一下就是,借助@Import 的支持,收集和注册特定场景相关的 bean 定义。 + +**@EnableScheduling**是通过@Import 将 Spring 调度框架相关的 bean 定义都加载到 IoC 容器。 +**@EnableMBeanExport**是通过@Import 将 JMX 相关的 bean 定义加载到 IoC 容器。 +而**@EnableAutoConfiguration**也是借助@Import 的帮助,将所有符合自动配置条件的 bean 定义加载到 IoC 容器,仅此而已! + +@EnableAutoConfiguration 作为一个复合 Annotation,其自身定义关键信息如下: + +```kotlin +@SuppressWarnings("deprecation") +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@AutoConfigurationPackage +@Import(EnableAutoConfigurationImportSelector.class) +public @interface EnableAutoConfiguration { + ... +} +``` + +两个比较重要的注解: + +**@AutoConfigurationPackage:自动配置包** + +**@Import: 导入自动配置的组件** + +#### AutoConfigurationPackage 注解: + +```java +static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { + + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, + BeanDefinitionRegistry registry) { + register(registry, new PackageImport(metadata).getPackageName()); + } +``` + +它其实是注册了一个 Bean 的定义。 + +new PackageImport(metadata).getPackageName(),它其实返回了当前主程序类的 同级以及子级 的包组件。 + +![img](https:////upload-images.jianshu.io/upload_images/6430208-439283a70a24c7a0.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/281/format/webp) + +以上图为例,DemoApplication 是和 demo 包同级,但是 demo2 这个类是 DemoApplication 的父级,和 example 包同级 + +也就是说,DemoApplication 启动加载的 Bean 中,并不会加载 demo2,这也就是为什么,我们要把 DemoApplication 放在项目的最高级中。 + +#### Import(AutoConfigurationImportSelector.class)注解: + +![img](https:////upload-images.jianshu.io/upload_images/6430208-1c448a69c41dc35c.png?imageMogr2/auto-orient/strip|imageView2/2/w/877/format/webp) + +可以从图中看出 AutoConfigurationImportSelector 继承了 DeferredImportSelector 继承了 ImportSelector + +ImportSelector 有一个方法为:selectImports。 + +```dart +@Override + public String[] selectImports(AnnotationMetadata annotationMetadata) { + if (!isEnabled(annotationMetadata)) { + return NO_IMPORTS; + } + AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader + .loadMetadata(this.beanClassLoader); + AnnotationAttributes attributes = getAttributes(annotationMetadata); + List configurations = getCandidateConfigurations(annotationMetadata, + attributes); + configurations = removeDuplicates(configurations); + Set exclusions = getExclusions(annotationMetadata, attributes); + checkExcludedClasses(configurations, exclusions); + configurations.removeAll(exclusions); + configurations = filter(configurations, autoConfigurationMetadata); + fireAutoConfigurationImportEvents(configurations, exclusions); + return StringUtils.toStringArray(configurations); + } +``` + +可以看到第九行,它其实是去加载 public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";外部文件。这个外部文件,有很多自动配置的类。如下: + +![img](https:////upload-images.jianshu.io/upload_images/6430208-250f3320c15e5c99.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) + +image + +其中,最关键的要属**@Import(EnableAutoConfigurationImportSelector.class)**,借助**EnableAutoConfigurationImportSelector**,**@EnableAutoConfiguration**可以帮助 SpringBoot 应用将所有符合条件的**@Configuration**配置都加载到当前 SpringBoot 创建并使用的 IoC 容器。就像一只“八爪鱼”一样。 + +![img](https:////upload-images.jianshu.io/upload_images/6430208-6f3a835755ee7710.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) + +### 自动配置幕后英雄:SpringFactoriesLoader 详解 + +借助于 Spring 框架原有的一个工具类:SpringFactoriesLoader 的支持,@EnableAutoConfiguration 可以智能的自动配置功效才得以大功告成! + +SpringFactoriesLoader 属于 Spring 框架私有的一种扩展方案,其主要功能就是从指定的配置文件 META-INF/spring.factories 加载配置。 + +```php +public abstract class SpringFactoriesLoader { + //... + public static List loadFactories(Class factoryClass, ClassLoader classLoader) { + ... + } + + + public static List loadFactoryNames(Class factoryClass, ClassLoader classLoader) { + .... + } +} +``` + +配合**@EnableAutoConfiguration**使用的话,它更多是提供一种配置查找的功能支持,即根据@EnableAutoConfiguration 的完整类名 org.springframework.boot.autoconfigure.EnableAutoConfiguration 作为查找的 Key,获取对应的一组**@Configuration**类 + +![img](https:////upload-images.jianshu.io/upload_images/6430208-fcdfcb56828a015a?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) + +上图就是从 SpringBoot 的 autoconfigure 依赖包中的 META-INF/spring.factories 配置文件中摘录的一段内容,可以很好地说明问题。 + +所以,@EnableAutoConfiguration 自动配置的魔法骑士就变成了:**从 classpath 中搜寻所有的 META-INF/spring.factories 配置文件,并将其中 org.springframework.boot.autoconfigure.EnableutoConfiguration 对应的配置项通过反射(Java Refletion)实例化为对应的标注了@Configuration 的 JavaConfig 形式的 IoC 容器配置类,然后汇总为一个并加载到 IoC 容器。** + +![img](https:////upload-images.jianshu.io/upload_images/6430208-10850d62d44c95ce.png?imageMogr2/auto-orient/strip|imageView2/2/w/822/format/webp) + +## 参考资料 + +- [一文搞懂 springboot 启动原理](https://www.jianshu.com/p/943650ab7dfd) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/99.Spring\351\235\242\350\257\225.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/99.Spring\351\235\242\350\257\225.md" new file mode 100644 index 00000000..461f16fb --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/99.Spring\351\235\242\350\257\225.md" @@ -0,0 +1,612 @@ +--- +title: Spring 面试 +date: 2018-08-02 17:33:32 +order: 99 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring + - 面试 +permalink: /pages/db33b0/ +--- + +# Spring 面试 + +## 综合篇 + +### 不同版本的 Spring Framework 有哪些主要功能? + +| Version | Feature | +| ---------- | ------------------------------------------------------------------- | +| Spring 2.5 | 发布于 2007 年。这是第一个支持注解的版本。 | +| Spring 3.0 | 发布于 2009 年。它完全利用了 Java5 中的改进,并为 JEE6 提供了支持。 | +| Spring 4.0 | 发布于 2013 年。这是第一个完全支持 JAVA8 的版本。 | + +### 什么是 Spring Framework? + +- Spring 是一个开源应用框架,旨在降低应用程序开发的复杂度。 +- 它是轻量级、松散耦合的。 +- 它具有分层体系结构,允许用户选择组件,同时还为 J2EE 应用程序开发提供了一个有凝聚力的框架。 +- 它可以集成其他框架,如 Structs、Hibernate、EJB 等,所以又称为框架的框架。 + +### 列举 Spring Framework 的优点。 + +- 由于 Spring Frameworks 的分层架构,用户可以自由选择自己需要的组件。 +- Spring Framework 支持 POJO(Plain Old Java Object) 编程,从而具备持续集成和可测试性。 +- 由于依赖注入和控制反转,JDBC 得以简化。 +- 它是开源免费的。 + +### Spring Framework 有哪些不同的功能? + +- **轻量级** - Spring 在代码量和透明度方面都很轻便。 +- **IOC** - 控制反转 +- **AOP** - 面向切面编程可以将应用业务逻辑和系统服务分离,以实现高内聚。 +- **容器** - Spring 负责创建和管理对象(Bean)的生命周期和配置。 +- **MVC** - 对 web 应用提供了高度可配置性,其他框架的集成也十分方便。 +- **事务管理** - 提供了用于事务管理的通用抽象层。Spring 的事务支持也可用于容器较少的环境。 +- **JDBC 异常** - Spring 的 JDBC 抽象层提供了一个异常层次结构,简化了错误处理策略。 + +### Spring Framework 中有多少个模块,它们分别是什么? + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/spring-framework.png) + +- **Spring 核心容器** – 该层基本上是 Spring Framework 的核心。它包含以下模块: + - Spring Core + - Spring Bean + - SpEL (Spring Expression Language) + - Spring Context +- **数据访问/集成** – 该层提供与数据库交互的支持。它包含以下模块: + - JDBC (Java DataBase Connectivity) + - ORM (Object Relational Mapping) + - OXM (Object XML Mappers) + - JMS (Java Messaging Service) + - Transaction +- **Web** – 该层提供了创建 Web 应用程序的支持。它包含以下模块: + - Web + - Web – Servlet + - Web – Socket + - Web – Portlet +- **AOP** – 该层支持面向切面编程 +- **Instrumentation** – 该层为类检测和类加载器实现提供支持。 +- **Test** – 该层为使用 JUnit 和 TestNG 进行测试提供支持。 +- **几个杂项模块:** + - Messaging – 该模块为 STOMP 提供支持。它还支持注解编程模型,该模型用于从 WebSocket 客户端路由和处理 STOMP 消息。 + - Aspects – 该模块为与 AspectJ 的集成提供支持。 + +### 什么是 Spring 配置文件? + +Spring 配置文件是 XML 文件。该文件主要包含类信息。它描述了这些类是如何配置以及相互引入的。但是,XML 配置文件冗长且更加干净。如果没有正确规划和编写,那么在大项目中管理变得非常困难。 + +### Spring 应用程序有哪些不同组件? + +Spring 应用一般有以下组件: + +- **接口** - 定义功能。 +- **Bean 类** - 它包含属性,setter 和 getter 方法,函数等。 +- **Spring 面向切面编程(AOP)** - 提供面向切面编程的功能。 +- **Bean 配置文件** - 包含类的信息以及如何配置它们。 +- **用户程序** - 它使用接口。 + +### 使用 Spring 有哪些方式? + +使用 Spring 有以下方式: + +- 作为一个成熟的 Spring Web 应用程序。 +- 作为第三方 Web 框架,使用 Spring Frameworks 中间层。 +- 用于远程使用。 +- 作为企业级 Java Bean,它可以包装现有的 POJO(Plain Old Java Objects)。 + +## 核心篇 + +### IoC + +#### 什么是 IoC?什么是依赖注入?什么是 Spring IoC? + +**IoC** 即**控制反转**(Inversion of Control,缩写为 IoC)。IoC 又称为**依赖倒置原则**(设计模式六大原则之一),它的要点在于:**程序要依赖于抽象接口,不要依赖于具体实现**。它的作用就是**用于降低代码间的耦合度**。 + +IoC 的实现方式有两种: + +- **依赖注入**(Dependency Injection,简称 DI):不通过 `new()` 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造器、函数参数等方式传递(或注入)给类使用。 +- **依赖查找**(Dependency Lookup):容器中的受控对象通过容器的 API 来查找自己所依赖的资源和协作对象。 + +Spring IoC 是 IoC 的一种实现。DI 是 Spring IoC 的主要实现原则。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221005163639.png) + +#### 依赖注入有哪些实现方式? + +依赖注入有如下方式: + +| 依赖注入方式 | 配置元数据举例 | +| --------------- | -------------------------------------------------- | +| Setter 方法注入 | `` | +| 构造器注入 | `` | +| 字段注入 | `@Autowired User user;` | +| 方法注入 | `@Autowired public void user(User user) { ... }` | +| 接口回调注入 | `class MyBean implements BeanFactoryAware { ... }` | + +#### 构造器注入 VS. setter 注入 + +| 构造器注入 | setter 注入 | +| -------------------------- | -------------------------- | +| 没有部分注入 | 有部分注入 | +| 不会覆盖 setter 属性 | 会覆盖 setter 属性 | +| 任意修改都会创建一个新实例 | 任意修改不会创建一个新实例 | +| 适用于设置很多属性 | 适用于设置少量属性 | + +官方推荐使用构造器注入。 + +#### BeanFactory VS. ApplicationContext + +在 Spring 中,有两种 IoC 容器:`BeanFactory` 和 `ApplicationContext`。 + +- `BeanFactory`:**`BeanFactory` 是 Spring 基础 IoC 容器**。`BeanFactory` 提供了 Spring 容器的配置框架和基本功能。 +- `ApplicationContext`:**`ApplicationContext` 是具备应用特性的 `BeanFactory` 的子接口**。它还扩展了其他一些接口,以支持更丰富的功能,如:国际化、访问资源、事件机制、更方便的支持 AOP、在 web 应用中指定应用层上下文等。 + +实际开发中,更推荐使用 `ApplicationContext` 作为 IoC 容器,因为它的功能远多于 `BeanFactory`。 + +#### BeanFactory VS. FactoryBean + +**`BeanFactory` 是 Spring 基础 IoC 容器**。 + +`FactoryBean` 是创建 Bean 的一种方式,帮助实现复杂的初始化逻辑。 + +#### Spring IoC 启动时做了哪些准备 + +IoC 配置元信息读取和解析 + +IoC 容器生命周期管理 + +Spring 事件发布 + +国际化 + +等等 + +#### Spring IoC 的实现机制是什么 + +Spring 中的 IoC 的实现原理就是工厂模式加反射机制。 + +示例: + +```java +interface Fruit { + public abstract void eat(); +} +class Apple implements Fruit { + public void eat(){ + System.out.println("Apple"); + } +} +class Orange implements Fruit { + public void eat(){ + System.out.println("Orange"); + } +} +class Factory { + public static Fruit getInstance(String ClassName) { + Fruit f=null; + try { + f=(Fruit)Class.forName(ClassName).newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + return f; + } +} +class Client { + public static void main(String[] a) { + Fruit f=Factory.getInstance("io.github.dunwu.spring.Apple"); + if(f!=null){ + f.eat(); + } + } +} +``` + +### Bean + +#### 什么是 Spring Bean + +在 Spring 中,构成应用程序主体由 Spring IoC 容器管理的对象称为 Bean。**Bean 是由 Spring IoC 容器实例化、装配和管理的对象**。 Bean 以及它们之间的依赖关系反映在容器使用的配置元数据中。 + +Spring IoC 容器本身,并不能识别配置的元数据。为此,要将这些配置信息转为 Spring 能识别的格式——`BeanDefinition` 对象。 + +**`BeanDefinition` 是 Spring 中定义 Bean 的配置元信息接口**,它包含: + +- Bean 类名 +- Bean 行为配置元素,如:作用域、自动绑定的模式、生命周期回调等 +- 其他 Bean 引用,也可称为合作者(Collaborators)或依赖(Dependencies) +- 配置设置,如 Bean 属性(Properties) + +#### 如何注册 Spring Bean + +通过 `BeanDefinition` 和外部单例对象来注册。 + +#### spring 提供了哪些配置方式? + +- 基于 xml 配置 + +bean 所需的依赖项和服务在 XML 格式的配置文件中指定。这些配置文件通常包含许多 bean 定义和特定于应用程序的配置选项。它们通常以 bean 标签开头。例如: + +```xml + + + +``` + +- 基于注解配置 + +您可以通过在相关的类,方法或字段声明上使用注解,将 bean 配置为组件类本身,而不是使用 XML 来描述 bean 装配。默认情况下,Spring 容器中未打开注解装配。因此,您需要在使用它之前在 Spring 配置文件中启用它。例如: + +```xml + + + + +``` + +- 基于 Java API 配置 + +Spring 的 Java 配置是通过使用 @Bean 和 @Configuration 来实现。 + +1. @Bean 注解扮演与 `` 元素相同的角色。 +2. @Configuration 类允许通过简单地调用同一个类中的其他 @Bean 方法来定义 bean 间依赖关系。 + +例如: + +```java +@Configuration +public class StudentConfig { + @Bean + public StudentBean myStudent() { + return new StudentBean(); + } +} +``` + +#### spring 支持集中 bean scope? + +Spring bean 支持 5 种 scope: + +- **Singleton** - 每个 Spring IoC 容器仅有一个单实例。 +- **Prototype** - 每次请求都会产生一个新的实例。 +- **Request** - 每一次 HTTP 请求都会产生一个新的实例,并且该 bean 仅在当前 HTTP 请求内有效。 +- **Session** - 每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前 HTTP session 内有效。 +- **Global-session** - 类似于标准的 HTTP Session 作用域,不过它仅仅在基于 portlet 的 web 应用中才有意义。Portlet 规范定义了全局 Session 的概念,它被所有构成某个 portlet web 应用的各种不同的 portlet 所共享。在 global session 作用域中定义的 bean 被限定于全局 portlet Session 的生命周期范围内。如果你在 web 中使用 global session 作用域来标识 bean,那么 web 会自动当成 session 类型来使用。 + +仅当用户使用支持 Web 的 ApplicationContext 时,最后三个才可用。 + +#### Spring Bean 的生命周期 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20211201102734.png) + +spring bean 容器的生命周期如下: + +1. Spring 对 Bean 进行实例化(相当于 new XXX()) + +2. Spring 将值和引用注入到 Bean 对应的属性中 + +3. 如果 Bean 实现了 `BeanNameAware` 接口,Spring 将 Bean 的 ID 传递给 `setBeanName` 方法 + - 作用是通过 Bean 的引用来获得 Bean ID,一般业务中是很少有用到 Bean 的 ID 的 +4. 如果 Bean 实现了 `BeanFactoryAware` 接口,Spring 将调用 `setBeanDactory` 方法,并把 `BeanFactory` 容器实例作为参数传入。 + - 作用是获取 Spring 容器,如 Bean 通过 Spring 容器发布事件等 +5. 如果 Bean 实现了 `ApplicationContextAware` 接口,Spring 容器将调用 `setApplicationContext` 方法,把应用上下文作为参数传入 + - 作用与 `BeanFactory` 类似都是为了获取 Spring 容器,不同的是 Spring 容器在调用 `setApplicationContext` 方法时会把它自己作为 `setApplicationContext` 的参数传入,而 Spring 容器在调用 `setBeanFactory` 前需要使用者自己指定(注入)`setBeanFactory` 里的参数 `BeanFactory` +6. 如果 Bean 实现了 `BeanPostProcess` 接口,Spring 将调用 `postProcessBeforeInitialization` 方法 + - 作用是在 Bean 实例创建成功后对其进行增强处理,如对 Bean 进行修改,增加某个功能 +7. 如果 Bean 实现了 `InitializingBean` 接口,Spring 将调用 `afterPropertiesSet` 方法,作用与在配置文件中对 Bean 使用 `init-method` 声明初始化的作用一样,都是在 Bean 的全部属性设置成功后执行的初始化方法。 +8. 如果 Bean 实现了 `BeanPostProcess` 接口,Spring 将调用 `postProcessAfterInitialization` 方法 + - `postProcessBeforeInitialization` 是在 Bean 初始化前执行的,而 `postProcessAfterInitialization` 是在 Bean 初始化后执行的 +9. 经过以上的工作后,Bean 将一直驻留在应用上下文中给应用使用,直到应用上下文被销毁 +10. 如果 Bean 实现了 `DispostbleBean` 接口,Spring 将调用它的 `destory` 方法,作用与在配置文件中对 Bean 使用 `destory-method` 属性的作用一样,都是在 Bean 实例销毁前执行的方法。 + +#### 什么是 spring 的内部 bean? + +只有将 bean 用作另一个 bean 的属性时,才能将 bean 声明为内部 bean。为了定义 bean,Spring 的基于 XML 的配置元数据在 `` 或 `` 中提供了 `` 元素的使用。内部 bean 总是匿名的,它们总是作为原型。 + +例如,假设我们有一个 Student 类,其中引用了 Person 类。这里我们将只创建一个 Person 类实例并在 Student 中使用它。 + +Student.java + +```java +public class Student { + private Person person; + //Setters and Getters +} +public class Person { + private String name; + private String address; + //Setters and Getters +} +``` + +bean.xml + +```xml + + + + + + + + + +``` + +#### 什么是 spring 装配 + +当 bean 在 Spring 容器中组合在一起时,它被称为装配或 bean 装配。 Spring 容器需要知道需要什么 bean 以及容器应该如何使用依赖注入来将 bean 绑定在一起,同时装配 bean。 + +#### 自动装配有哪些方式? + +Spring 容器能够自动装配 bean。也就是说,可以通过检查 BeanFactory 的内容让 Spring 自动解析 bean 的协作者。 + +自动装配的不同模式: + +- **no** - 这是默认设置,表示没有自动装配。应使用显式 bean 引用进行装配。 +- **byName** - 它根据 bean 的名称注入对象依赖项。它匹配并装配其属性与 XML 文件中由相同名称定义的 bean。 +- **byType** - 它根据类型注入对象依赖项。如果属性的类型与 XML 文件中的一个 bean 名称匹配,则匹配并装配属性。 +- **构造器** - 它通过调用类的构造器来注入依赖项。它有大量的参数。 +- **autodetect** - 首先容器尝试通过构造器使用 autowire 装配,如果不能,则尝试通过 byType 自动装配。 + +#### 自动装配有什么局限? + +- 覆盖的可能性 - 您始终可以使用 `` 和 `` 设置指定依赖项,这将覆盖自动装配。 +- 基本元数据类型 - 简单属性(如原数据类型,字符串和类)无法自动装配。 +- 令人困惑的性质 - 总是喜欢使用明确的装配,因为自动装配不太精确。 + +### AOP + +#### 什么是 AOP? + +AOP(Aspect-Oriented Programming), 即 **面向切面编程**, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角. +在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 **Aspect(切面)** + +#### AOP 中的 Aspect、Advice、Pointcut、JointPoint 和 Advice 参数分别是什么? + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/core/spring-aop.png) + +- **Aspect** - Aspect 是一个实现交叉问题的类,例如事务管理。方面可以是配置的普通类,然后在 Spring Bean 配置文件中配置,或者我们可以使用 Spring AspectJ 支持使用 @Aspect 注解将类声明为 Aspect。 +- **Advice** - Advice 是针对特定 JoinPoint 采取的操作。在编程方面,它们是在应用程序中达到具有匹配切入点的特定 JoinPoint 时执行的方法。您可以将 Advice 视为 Spring 拦截器(Interceptor)或 Servlet 过滤器(filter)。 +- **Advice Arguments** - 我们可以在 advice 方法中传递参数。我们可以在切入点中使用 args() 表达式来应用于与参数模式匹配的任何方法。如果我们使用它,那么我们需要在确定参数类型的 advice 方法中使用相同的名称。 +- **Pointcut** - Pointcut 是与 JoinPoint 匹配的正则表达式,用于确定是否需要执行 Advice。 Pointcut 使用与 JoinPoint 匹配的不同类型的表达式。Spring 框架使用 AspectJ Pointcut 表达式语言来确定将应用通知方法的 JoinPoint。 +- **JoinPoint** - JoinPoint 是应用程序中的特定点,例如方法执行,异常处理,更改对象变量值等。在 Spring AOP 中,JoinPoint 始终是方法的执行器。 + +#### 什么是通知(Advice)? + +特定 JoinPoint 处的 Aspect 所采取的动作称为 Advice。Spring AOP 使用一个 Advice 作为拦截器,在 JoinPoint “周围”维护一系列的拦截器。 + +#### 有哪些类型的通知(Advice)? + +- **Before** - 这些类型的 Advice 在 joinpoint 方法之前执行,并使用 @Before 注解标记进行配置。 +- **After Returning** - 这些类型的 Advice 在连接点方法正常执行后执行,并使用@AfterReturning 注解标记进行配置。 +- **After Throwing** - 这些类型的 Advice 仅在 joinpoint 方法通过抛出异常退出并使用 @AfterThrowing 注解标记配置时执行。 +- **After (finally)** - 这些类型的 Advice 在连接点方法之后执行,无论方法退出是正常还是异常返回,并使用 @After 注解标记进行配置。 +- **Around** - 这些类型的 Advice 在连接点之前和之后执行,并使用 @Around 注解标记进行配置。 + +#### 指出在 spring aop 中 concern 和 cross-cutting concern 的不同之处。 + +concern 是我们想要在应用程序的特定模块中定义的行为。它可以定义为我们想要实现的功能。 + +cross-cutting concern 是一个适用于整个应用的行为,这会影响整个应用程序。例如,日志记录,安全性和数据传输是应用程序几乎每个模块都需要关注的问题,因此它们是跨领域的问题。 + +#### AOP 有哪些实现方式? + +实现 AOP 的技术,主要分为两大类: + +- 静态代理 - 指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强; + - 编译时编织(特殊编译器实现) + - 类加载时编织(特殊的类加载器实现)。 +- 动态代理 - 在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。 + - JDK 动态代理 + - CGLIB + +#### Spring AOP and AspectJ AOP 有什么区别? + +Spring AOP 基于动态代理方式实现;AspectJ 基于静态代理方式实现。 +Spring AOP 仅支持方法级别的 PointCut;提供了完全的 AOP 支持,它还支持属性级别的 PointCut。 + +#### 如何理解 Spring 中的代理? + +将 Advice 应用于目标对象后创建的对象称为代理。在客户端对象的情况下,目标对象和代理对象是相同的。 + +``` +Advice + Target Object = Proxy +``` + +#### 什么是编织(Weaving)? + +为了创建一个 advice 对象而链接一个 aspect 和其它应用类型或对象,称为编织(Weaving)。在 Spring AOP 中,编织在运行时执行。请参考下图: + +![img](https://upload-images.jianshu.io/upload_images/3101171-cfaa92f0e4115b4a.png) + +## 注解 + +### 你用过哪些重要的 Spring 注解? + +- **@Controller** - 用于 Spring MVC 项目中的控制器类。 +- **@Service** - 用于服务类。 +- **@RequestMapping** - 用于在控制器处理程序方法中配置 URI 映射。 +- **@ResponseBody** - 用于发送 Object 作为响应,通常用于发送 XML 或 JSON 数据作为响应。 +- **@PathVariable** - 用于将动态值从 URI 映射到处理程序方法参数。 +- **@Autowired** - 用于在 spring bean 中自动装配依赖项。 +- **@Qualifier** - 使用 @Autowired 注解,以避免在存在多个 bean 类型实例时出现混淆。 +- **@Scope** - 用于配置 spring bean 的范围。 +- **@Configuration**,**@ComponentScan** 和 **@Bean** - 用于基于 java 的配置。 +- **@Aspect**,**@Before**,**@After**,**@Around**,**@Pointcut** - 用于切面编程(AOP)。 + +### 如何在 spring 中启动注解装配? + +默认情况下,Spring 容器中未打开注解装配。因此,要使用基于注解装配,我们必须通过配置`` 元素在 Spring 配置文件中启用它。 + +### @Component, @Controller, @Repository, @Service 有何区别? + +- @Component:这将 java 类标记为 bean。它是任何 Spring 管理组件的通用构造型。spring 的组件扫描机制现在可以将其拾取并将其拉入应用程序环境中。 +- @Controller:这将一个类标记为 Spring Web MVC 控制器。标有它的 Bean 会自动导入到 IoC 容器中。 +- @Service:此注解是组件注解的特化。它不会对 @Component 注解提供任何其他行为。您可以在服务层类中使用 @Service 而不是 @Component,因为它以更好的方式指定了意图。 +- @Repository:这个注解是具有类似用途和功能的 @Component 注解的特化。它为 DAO 提供了额外的好处。它将 DAO 导入 IoC 容器,并使未经检查的异常有资格转换为 Spring DataAccessException。 + +### @Required 注解有什么用? + +@Required 应用于 bean 属性 setter 方法。此注解仅指示必须在配置时使用 bean 定义中的显式属性值或使用自动装配填充受影响的 bean 属性。如果尚未填充受影响的 bean 属性,则容器将抛出 BeanInitializationException。 + +示例: + +```java +public class Employee { + private String name; + @Required + public void setName(String name){ + this.name=name; + } + public string getName(){ + return name; + } +} +``` + +### @Autowired 注解有什么用? + +@Autowired 可以更准确地控制应该在何处以及如何进行自动装配。此注解用于在 setter 方法,构造器,具有任意名称或多个参数的属性或方法上自动装配 bean。默认情况下,它是类型驱动的注入。 + +```java +public class Employee { + private String name; + @Autowired + public void setName(String name) { + this.name=name; + } + public string getName(){ + return name; + } +} +``` + +### @Qualifier 注解有什么用? + +当您创建多个相同类型的 bean 并希望仅使用属性装配其中一个 bean 时,您可以使用@Qualifier 注解和 @Autowired 通过指定应该装配哪个确切的 bean 来消除歧义。 + +例如,这里我们分别有两个类,Employee 和 EmpAccount。在 EmpAccount 中,使用@Qualifier 指定了必须装配 id 为 emp1 的 bean。 + +Employee.java + +```java +public class Employee { + private String name; + @Autowired + public void setName(String name) { + this.name=name; + } + public string getName() { + return name; + } +} +``` + +EmpAccount.java + +```java +public class EmpAccount { + private Employee emp; + + @Autowired + @Qualifier(emp1) + public void showName() { + System.out.println(“Employee name : ”+emp.getName); + } +} +``` + +### @RequestMapping 注解有什么用? + +@RequestMapping 注解用于将特定 HTTP 请求方法映射到将处理相应请求的控制器中的特定类/方法。此注解可应用于两个级别: + +- 类级别:映射请求的 URL +- 方法级别:映射 URL 以及 HTTP 请求方法 + +## 数据篇 + +### spring DAO 有什么用? + +Spring DAO 使得 JDBC,Hibernate 或 JDO 这样的数据访问技术更容易以一种统一的方式工作。这使得用户容易在持久性技术之间切换。它还允许您在编写代码时,无需考虑捕获每种技术不同的异常。 + +### 列举 Spring DAO 抛出的异常。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/data-access/spring-data-access-exception.png) + +### spring JDBC API 中存在哪些类? + +- JdbcTemplate +- SimpleJdbcTemplate +- NamedParameterJdbcTemplate +- SimpleJdbcInsert +- SimpleJdbcCall + +### 使用 Spring 访问 Hibernate 的方法有哪些? + +我们可以通过两种方式使用 Spring 访问 Hibernate: + +1. 使用 Hibernate 模板和回调进行控制反转 +2. 扩展 HibernateDAOSupport 并应用 AOP 拦截器节点 + +### 列举 spring 支持的事务管理类型 + +Spring 支持两种类型的事务管理: + +1. 程序化事务管理:在此过程中,在编程的帮助下管理事务。它为您提供极大的灵活性,但维护起来非常困难。 +2. 声明式事务管理:在此,事务管理与业务代码分离。仅使用注解或基于 XML 的配置来管理事务。 + +### spring 支持哪些 ORM 框架 + +- Hibernate +- iBatis +- JPA +- JDO +- OJB + +## MVC + +### Spring MVC 框架有什么用? + +Spring Web MVC 框架提供 **模型-视图-控制器** 架构和随时可用的组件,用于开发灵活且松散耦合的 Web 应用程序。 MVC 模式有助于分离应用程序的不同方面,如输入逻辑,业务逻辑和 UI 逻辑,同时在所有这些元素之间提供松散耦合。 + +### 描述一下 DispatcherServlet 的工作流程 + +DispatcherServlet 的工作流程可以用一幅图来说明: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/web/spring-dispatcher-servlet.png) + +1. 向服务器发送 HTTP 请求,请求被前端控制器 `DispatcherServlet` 捕获。 +2. `DispatcherServlet` 根据 **`-servlet.xml`** 中的配置对请求的 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用 `HandlerMapping` 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以`HandlerExecutionChain` 对象的形式返回。 +3. `DispatcherServlet` 根据获得的`Handler`,选择一个合适的 `HandlerAdapter`。(附注:如果成功获得`HandlerAdapter`后,此时将开始执行拦截器的 preHandler(...)方法)。 +4. 提取`Request`中的模型数据,填充`Handler`入参,开始执行`Handler`(`Controller`)。 在填充`Handler`的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作: + - HttpMessageConveter: 将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息。 + - 数据转换:对请求消息进行数据转换。如`String`转换成`Integer`、`Double`等。 + - 数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等。 + - 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到`BindingResult`或`Error`中。 +5. Handler(Controller)执行完成后,向 `DispatcherServlet` 返回一个 `ModelAndView` 对象; +6. 根据返回的`ModelAndView`,选择一个适合的 `ViewResolver`(必须是已经注册到 Spring 容器中的`ViewResolver`)返回给`DispatcherServlet`。 +7. `ViewResolver` 结合`Model`和`View`,来渲染视图。 +8. 视图负责将渲染结果返回给客户端。 + +### 介绍一下 WebApplicationContext + +WebApplicationContext 是 ApplicationContext 的扩展。它具有 Web 应用程序所需的一些额外功能。它与普通的 ApplicationContext 在解析主题和决定与哪个 servlet 关联的能力方面有所不同。 + +(完) + +--- + +:point_right: 想学习更多 Spring 内容可以访问我的 Spring 教程:**[spring-notes](https://github.com/dunwu/spring-notes)** + +## 资料 + +- [Top 50 Spring Interview Questions You Must Prepare In 2018](https://www.edureka.co/blog/interview-questions/spring-interview-questions/) +- [Spring Interview Questions and Answers](https://www.journaldev.com/2696/spring-interview-questions-and-answers) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/README.md" new file mode 100644 index 00000000..cb55bf58 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/README.md" @@ -0,0 +1,43 @@ +--- +title: Spring 综述 +date: 2020-02-26 23:48:06 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/9e0b67/ +hidden: true +index: false +--- + +# Spring 综述 + +## 📖 内容 + +- [Spring 概述](01.Spring概述.md) +- [SpringBoot 知识图谱](21.SpringBoot知识图谱.md) +- [SpringBoot 基本原理](22.SpringBoot基本原理.md) +- [Spring 面试](99.Spring面试.md) + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/01.SpringBean.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/01.SpringBean.md" new file mode 100644 index 00000000..1df26d7b --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/01.SpringBean.md" @@ -0,0 +1,214 @@ +--- +title: Spring Bean +date: 2021-12-10 19:15:42 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Bean + - BeanDefinition +permalink: /pages/68097d/ +--- + +# Spring Bean + +在 Spring 中,构成应用程序主体由 Spring IoC 容器管理的对象称为 Bean。**Bean 是由 Spring IoC 容器实例化、装配和管理的对象**。 Bean 以及它们之间的依赖关系反映在容器使用的配置元数据中。 + +## Spring Bean 定义 + +### BeanDefinition + +Spring IoC 容器本身,并不能识别配置的元数据。为此,要将这些配置信息转为 Spring 能识别的格式——`BeanDefinition` 对象。 + +**`BeanDefinition` 是 Spring 中定义 Bean 的配置元信息接口**,它包含: + +- Bean 类名 +- Bean 行为配置元素,如:作用域、自动绑定的模式、生命周期回调等 +- 其他 Bean 引用,也可称为合作者(Collaborators)或依赖(Dependencies) +- 配置设置,如 Bean 属性(Properties) + +#### BeanDefinition 元信息 + +`BeanDefinition` 元信息如下: + +| 属性(Property) | 说明 | +| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | +| [Class](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-class) | 全类名,必须是具体类,不能用抽象类或接口 | +| [Name](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-beanname) | Bean 的名称或者 ID | +| [Scope](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes) | Bean 的作用域(如:`singleton`、`prototype` 等) | +| [Constructor arguments](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-collaborators) | Bean 构造器参数(用于依赖注入) | +| [Properties](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-collaborators) | Bean 属性设置(用于依赖注入) | +| [Autowiring mode](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-autowire) | Bean 自动绑定模式(如:通过名称 byName) | +| [Lazy initialization mode](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-lazy-init) | Bean 延迟初始化模式(延迟和非延迟) | +| [Initialization method](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-lifecycle-initializingbean) | Bean 初始化回调方法名称 | +| [Destruction method](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-lifecycle-disposablebean) | Bean 销毁回调方法名称 | + +#### BeanDefinition 构建 + +BeanDefinition 构建方式: + +- 通过 `BeanDefinitionBuilder` + +- 通过 `AbstractBeanDefinition` 以及派生类 + +> 💻 Spring Bean 定义示例源码:[BeanDefinitionTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanDefinitionTests.java) + +### Spring Bean 命名 + +#### Spring Bean 命名规则 + +每个 Bean 拥有一个或多个标识符(identifiers),这些标识符在 Bean 所在的容器必须是唯一的。通常,一个 Bean 仅有一个标识符,如果需要额外的,可考虑使用别名(Alias)来扩充。 + +在基于 XML 的配置元信息中,开发人员**可以使用 `id` 属性、`name` 属性或来指定 Bean 标识符**。通常,Bean 的标识符由字母组成,允许出现特殊字符。如果要想引入 Bean 的别名的话,可在 `name` 属性使用半角逗号(“,”)或分号(“;”) 来间隔。 + +Spring 中,**为 Bean 指定 `id` 和 `name` 属性不是必须的**。如果不指定,Spring 会自动为 Bean 分配一个唯一的名称。尽管 Bean 的命名没有限制,不过**官方建议采用驼峰命名法来命名 Bean**。 + +#### Spring Bean 命名生成器 + +Spring 提供了两种 Spring Bean 命名生成器: + +- `DefaultBeanNameGenerator`:默认通用 `BeanNameGenerator` 实现。 +- `AnnotationBeanNameGenerator`:基于注解扫描的 `BeanNameGenerator` 实现。 + +```java +public interface BeanNameGenerator { + String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry); +} +``` + +#### Spring Bean 别名 + +Spring 支持通过 `` 属性为 Bean 设置别名。 + +Bean 别名(Alias)的作用: + +- 复用现有的 `BeanDefinition` +- 更具有场景化的命名方法,比如: + - `` + - `` + +```xml + + + + +``` + +## Spring Bean 生命周期 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20211201102734.png) + +1. Spring 对 Bean 进行实例化(相当于 new XXX()) + +2. Spring 将值和引用注入到 Bean 对应的属性中 + +3. 如果 Bean 实现了 `BeanNameAware` 接口,Spring 将 Bean 的 ID 传递给 `setBeanName` 方法 + - 作用是通过 Bean 的引用来获得 Bean ID,一般业务中是很少有用到 Bean 的 ID 的 +4. 如果 Bean 实现了 `BeanFactoryAware` 接口,Spring 将调用 `setBeanDactory` 方法,并把 `BeanFactory` 容器实例作为参数传入。 + - 作用是获取 Spring 容器,如 Bean 通过 Spring 容器发布事件等 +5. 如果 Bean 实现了 `ApplicationContextAware` 接口,Spring 容器将调用 `setApplicationContext` 方法,把应用上下文作为参数传入 + - 作用与 `BeanFactory` 类似都是为了获取 Spring 容器,不同的是 Spring 容器在调用 `setApplicationContext` 方法时会把它自己作为 `setApplicationContext` 的参数传入,而 Spring 容器在调用 `setBeanFactory` 前需要使用者自己指定(注入)`setBeanFactory` 里的参数 `BeanFactory` +6. 如果 Bean 实现了 `BeanPostProcess` 接口,Spring 将调用 `postProcessBeforeInitialization` 方法 + - 作用是在 Bean 实例创建成功后对其进行增强处理,如对 Bean 进行修改,增加某个功能 +7. 如果 Bean 实现了 `InitializingBean` 接口,Spring 将调用 `afterPropertiesSet` 方法,作用与在配置文件中对 Bean 使用 `init-method` 声明初始化的作用一样,都是在 Bean 的全部属性设置成功后执行的初始化方法。 +8. 如果 Bean 实现了 `BeanPostProcess` 接口,Spring 将调用 `postProcessAfterInitialization` 方法 + - `postProcessBeforeInitialization` 是在 Bean 初始化前执行的,而 `postProcessAfterInitialization` 是在 Bean 初始化后执行的 +9. 经过以上的工作后,Bean 将一直驻留在应用上下文中给应用使用,直到应用上下文被销毁 +10. 如果 Bean 实现了 `DispostbleBean` 接口,Spring 将调用它的 `destory` 方法,作用与在配置文件中对 Bean 使用 `destory-method` 属性的作用一样,都是在 Bean 实例销毁前执行的方法。 + +## Spring Bean 注册 + +注册 Spring Bean 实际上是将 `BeanDefinition` 注册到 IoC 容器中。 + +### XML 配置元信息 + +Spring 的传统配置方式。在 `` 标签中配置元数据内容。 + +缺点是当 JavaBean 过多时,产生的配置文件足以让你眼花缭乱。 + +### 注解配置元信息 + +使用 `@Bean`、`@Component`、`@Import` 注解注册 Spring Bean。 + +### Java API 配置元信息 + +- 命名方式:`BeanDefinitionRegistry#registerBeanDefinition(String,BeanDefinition)` +- 非命名方式:`BeanDefinitionReaderUtils#registerWithGeneratedName(AbstractBeanDefinition,BeanDefinitionRegistry)` +- 配置类方式:`AnnotatedBeanDefinitionReader#register(Class...)` + +> 💻 Spring Bean 注册示例源码:[BeanRegistryTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanRegistryTests.java) + +## Spring Bean 实例化 + +Spring Bean 实例化方式: + +- 常规方式 + - 通过构造器(配置元信息:XML、Java 注解和 Java API) + - 通过静态方法(配置元信息:XML、Java 注解和 Java API) + - 通过 Bean 工厂方法(配置元信息:XML、Java 注解和 Java API) + - 通过 `FactoryBean`(配置元信息:XML、Java 注解和 Java API) +- 特殊方式 + - 通过 `ServiceLoaderFactoryBean`(配置元信息:XML、Java 注解和 Java API ) + - 通过 `AutowireCapableBeanFactory#createBean(java.lang.Class, int, boolean)` + - 通过 `BeanDefinitionRegistry#registerBeanDefinition(String,BeanDefinition)` + +> 💻 Spring Bean 实例化示例源码:[BeanInstantiationTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanInstantiationTests.java)、[BeanInstantiationSpecialTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanInstantiationSpecialTests.java) + +## Spring Bean 初始化和销毁 + +Spring Bean 初始化和销毁的方式有以下几种: + +1. 使用 `@PostConstruct` 和 `@PreDestroy` 注解分别指定相应的初始化方法和销毁方法。 +2. 实现 `InitializingBean` 接口的 `afterPropertiesSet()` 方法来编写初始化方法;实现 `DisposableBean` 接口的 `destroy()` 方法来编写销毁方法。 + + - `InitializingBean` 接口包含一个 `afterPropertiesSet` 方法,可以通过实现该接口,然后在这个方法中编写初始化逻辑。 + - `DisposableBean`接口包含一个 `destory` 方法,可以通过实现该接口,然后在这个方法中编写销毁逻辑。 + +3. 自定义初始化方法 + - XML 配置:`` + - Java 注解:`@Bean(initMethod = "init", destroyMethod = "destroy")` + - Java API:`AbstractBeanDefinition#setInitMethodName(String)` 和 `AbstractBeanDefinition#setDestroyMethodName(String)` 分别定义初始化和销毁方法 + +注意:如果同时存在,执行顺序会按照序列执行。 + +Bean 的延迟初始化 + +- xml 方式:`` +- 注解方式:`@Lazy` + +Spring 提供了一个 `BeanPostProcessor` 接口,提供了两个方法 `postProcessBeforeInitialization` 和 `postProcessAfterInitialization`。其中`postProcessBeforeInitialization` 在组件的初始化方法调用之前执行,`postProcessAfterInitialization` 在组件的初始化方法调用之后执行。它们都包含两个入参: + +- `bean`:当前组件对象; +- `beanName`:当前组件在容器中的名称。 + +> 💻 Spring Bean 初始化和销毁示例源码:[BeanInitDestroyTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanInitDestroyTests.java) + +## Spring Bean 垃圾回收 + +Spring Bean 垃圾回收步骤: + +1. 关闭 Spring 容器(应用上下文) +2. 执行 GC +3. Spring Bean 覆盖的 `finalize()` 方法被回调 + +## Spring Bean 作用范围 + +| Scope | Description | +| :---------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [singleton](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-singleton) | (Default) Scopes a single bean definition to a single object instance for each Spring IoC container. | +| [prototype](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-prototype) | Scopes a single bean definition to any number of object instances. | +| [request](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-request) | Scopes a single bean definition to the lifecycle of a single HTTP request. That is, each HTTP request has its own instance of a bean created off the back of a single bean definition. Only valid in the context of a web-aware Spring `ApplicationContext`. | +| [session](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-session) | Scopes a single bean definition to the lifecycle of an HTTP `Session`. Only valid in the context of a web-aware Spring `ApplicationContext`. | +| [application](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-application) | Scopes a single bean definition to the lifecycle of a `ServletContext`. Only valid in the context of a web-aware Spring `ApplicationContext`. | +| [websocket](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket-stomp-websocket-scope) | Scopes a single bean definition to the lifecycle of a `WebSocket`. Only valid in the context of a web-aware Spring `ApplicationContext`. | + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/02.SpringIoC.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/02.SpringIoC.md" new file mode 100644 index 00000000..505172fb --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/02.SpringIoC.md" @@ -0,0 +1,921 @@ +--- +title: Spring IoC +date: 2020-08-30 16:06:10 +order: 02 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - IOC +permalink: /pages/915530/ +--- + +# Spring IoC + +## IoC 简介 + +### IoC 是什么 + +**IoC** 即**控制反转**(Inversion of Control,缩写为 IoC)。IoC 又称为**依赖倒置原则**(设计模式六大原则之一),它的要点在于:**程序要依赖于抽象接口,不要依赖于具体实现**。它的作用就是**用于降低代码间的耦合度**。 + +IoC 的实现方式有两种: + +- **依赖注入**(Dependency Injection,简称 DI):不通过 `new()` 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。 +- **依赖查找**(Dependency Lookup):容器中的受控对象通过容器的 API 来查找自己所依赖的资源和协作对象。 + +理解 Ioc 的关键是要明确两个要点: + +- **谁控制谁,控制什么**:传统 Java SE 程序设计,我们直接在对象内部通过 new 进行创建对象,是程序主动去创建依赖对象;而 IoC 是有专门一个容器来创建这些对象,即由 Ioc 容器来控制对象的创建;谁控制谁?当然是 IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。 +- **为何是反转,哪些方面反转了**:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221006120112.png) + +### IoC 能做什么 + +IoC 不是一种技术,而是编程思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了 IoC 容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。 + +其实 IoC 对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在 IoC/DI 思想中,应用程序就变成被动的了,被动的等待 IoC 容器来创建并注入它所需要的资源了。 + +IoC 很好的体现了面向对象设计法则之一—— **好莱坞法则:“别找我们,我们找你”**;即由 IoC 容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。 + +### IoC 和 DI + +其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以 2004 年大师级人物 Martin Fowler 又给出了一个新的名字:“依赖注入”,相对 IoC 而言,“依赖注入”明确描述了“被注入对象依赖 IoC 容器配置依赖对象”。 + +> 注:如果想要更加深入的了解 IoC 和 DI,请参考大师级人物 Martin Fowler 的一篇经典文章 [Inversion of Control Containers and the Dependency Injection pattern](http://www.martinfowler.com/articles/injection.html) 。 + +### IoC 容器 + +IoC 容器就是具有依赖注入功能的容器。IoC 容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。应用程序无需直接在代码中 new 相关的对象,应用程序由 IoC 容器进行组装。在 Spring 中 BeanFactory 是 IoC 容器的实际代表者。 + +Spring IoC 容器如何知道哪些是它管理的对象呢?这就需要配置文件,Spring IoC 容器通过读取配置文件中的配置元数据,通过元数据对应用中的各个对象进行实例化及装配。一般使用基于 xml 配置文件进行配置元数据,而且 Spring 与配置文件完全解耦的,可以使用其他任何可能的方式进行配置元数据,比如注解、基于 java 文件的、基于属性文件的配置都可以。 + +### Bean + +> **JavaBean** 是一种 JAVA 语言写成的可重用组件。为写成 JavaBean,类必须是具体的和公共的,并且具有无参数的构造器。JavaBean 对外部通过提供 getter / setter 方法来访问其成员。 + +由 IoC 容器管理的那些组成你应用程序的对象我们就叫它 Bean。Bean 就是由 Spring 容器初始化、装配及管理的对象,除此之外,bean 就与应用程序中的其他对象没有什么区别了。那 IoC 怎样确定如何实例化 Bean、管理 Bean 之间的依赖关系以及管理 Bean 呢?这就需要配置元数据,在 Spring 中由 BeanDefinition 代表,后边会详细介绍,配置元数据指定如何实例化 Bean、如何组装 Bean 等。 + +### Spring IoC + +Spring IoC 容器中的对象仅通过构造函数参数、工厂方法的参数或在对象实例被构造或从工厂方法返回后设置的属性来定义它们的依赖关系(即与它们一起工作的其他对象)。然后容器在创建 bean 时注入这些依赖项。这个过程基本上是 bean 本身通过使用类的直接构造或诸如服务定位器模式之类的机制来控制其依赖关系的实例化或位置的逆过程(因此称为控制反转)。 + +`org.springframework.beans` 和 `org.springframework.context` 是 IoC 容器的基础。 + +## IoC 容器 + +在 Spring 中,有两种 IoC 容器:`BeanFactory` 和 `ApplicationContext`。 + +- `BeanFactory`:**`BeanFactory` 是 Spring 基础 IoC 容器**。`BeanFactory` 提供了 Spring 容器的配置框架和基本功能。 +- `ApplicationContext`:**`ApplicationContext` 是具备应用特性的 `BeanFactory` 的子接口**。它还扩展了其他一些接口,以支持更丰富的功能,如:国际化、访问资源、事件机制、更方便的支持 AOP、在 web 应用中指定应用层上下文等。 + +实际开发中,更推荐使用 `ApplicationContext` 作为 IoC 容器,因为它的功能远多于 `BeanFactory`。 + +`org.springframework.context.ApplicationContext` 接口代表 Spring IoC 容器,负责实例化、配置和组装 bean。容器通过读取配置元数据来获取关于要实例化、配置和组装哪些对象的指令。配置元数据以 XML、Java 注释或 Java 代码表示。它允许您表达组成应用程序的对象以及这些对象之间丰富的相互依赖关系。 + +Spring 提供了 `ApplicationContext` 接口的几个实现,例如: + +- **[ClassPathXmlApplicationContext](https://docs.spring.io/spring-framework/docs/5.3.23/javadoc-api/org/springframework/context/support/ClassPathXmlApplicationContext.html)**:`ApplicationContext` 的实现,从 classpath 获取配置信息。 + +```java +BeanFactory beanFactory = new ClassPathXmlApplicationContext("classpath.xml"); +``` + +- **[FileSystemXmlApplicationContext](https://docs.spring.io/spring-framework/docs/5.3.23/javadoc-api/org/springframework/context/support/FileSystemXmlApplicationContext.html)**:`ApplicationContext` 的实现,从文件系统获取配置信息。 + +```java +BeanFactory beanFactory = new FileSystemXmlApplicationContext("fileSystemConfig.xml"); +``` + +在大多数应用场景中,不需要显式通过用户代码来实例化 Spring IoC 容器的一个或多个实例。 + +下图显示了 Spring IoC 容器的工作步骤 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200723102456.png) + +使用 IoC 容器可分为三步骤: + +1. **配置元数据**:需要配置一些元数据来告诉 Spring,你希望容器如何工作,具体来说,就是如何去初始化、配置、管理 JavaBean 对象。 +2. **实例化容器**:由 IoC 容器解析配置的元数据。IoC 容器的 Bean Reader 读取并解析配置文件,根据定义生成 BeanDefinition 配置元数据对象,IoC 容器根据 `BeanDefinition` 进行实例化、配置及组装 Bean。 +3. **使用容器**:由客户端实例化容器,获取需要的 Bean。 + +### 配置元数据 + +**元数据(Metadata)**又称中介数据、中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息。 + +配置元数据的方式: + +- **基于 xml 配置**:Spring 的传统配置方式。通常是在顶级元素 `` 中通过 ``元素配置元数据。这种方式的缺点是:如果 JavaBean 过多,则产生的配置文件足以让你眼花缭乱。 +- **[基于注解配置](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-annotation-config)**:Spring 2.5 引入了对基于注解的配置元数据的支持。可以大大简化你的配置。 +- **[基于 Java 配置](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-java)**:从 Spring 3.0 开始,Spring 支持使用 Java 代码来配置元数据。通常是在 `@Configuration` 修饰的类中通过 `@Bean` 指定实例化 Bean 的方法。更多详情,可以参阅 [`@Configuration`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Configuration.html)、[`@Bean`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Bean.html)、[`@Import`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Import.html) 和 [`@DependsOn`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/DependsOn.html) 注释。 + +这些 bean 定义对应于构成应用程序的实际对象。例如:定义服务层对象、数据访问对象 (DAO)、表示对象(如 Struts Action 实例)、基础设施对象(如 Hibernate SessionFactories、JMS 队列等)。通常,不会在容器中配置细粒度的域对象,因为创建和加载域对象通常是 DAO 和业务逻辑的责任。但是,可以使用 Spring 与 AspectJ 的集成来配置在 IoC 容器控制之外创建的对象。 + +以下示例显示了基于 XML 的配置元数据的基本结构: + +```xml + + + + + + + + + + + + + + + + +``` + +### 实例化容器 + +可以通过为 `ApplicationContext` 的构造函数指定外部资源路径,来加载配置元数据。 + +```java +ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); +``` + +以下示例显示了服务层对象 (services.xml) 配置文件: + +```xml + + + + + + + + + + + + + + +``` + +以下示例显示了数据访问对象 (daos.xml) 配置文件: + +```xml + + + + + + + + + + + + + + +``` + +上面的示例中,服务层由 `PetStoreServiceImpl` 类和类型为 `JpaAccountDao` 和 `JpaItemDao` 的两个数据访问对象(基于 JPA 对象关系映射标准)组成。 `property name` 元素指的是 JavaBean 属性的名称,`ref` 元素指的是另一个 bean 定义的名称。 `id` 和 `ref` 元素之间的这种联系表达了协作对象之间的依赖关系。 + +**Spring 支持通过多个 xml 文件来定义 Bean,每个单独的 XML 配置文件都代表架构中的一个逻辑层或模块。可以使用 `ApplicationContext` 构造函数从所有这些 XML 片段加载 bean 定义。或者,使用 `` 元素从另一个或多个文件加载 bean 定义**。如下所示: + +```xml + + + + + + + + +``` + +在上面的示例中,外部 bean 定义从三个文件加载:`services.xml`、`messageSource.xml` 和 `themeSource.xml`。`services.xml` 文件必须和当前 xml 文件位于同一目录或类路径位置;而 `messageSource.xml` 和 `themeSource.xml` 必须位于当前文件所在目录的子目录 `resources` 下。`/resources` 的 `/` 会被忽略。但是,鉴于这些路径是相对的,最好不要使用 `/`。根据 Spring Schema,被导入文件的内容,包括顶级 `` 元素,必须是有效的 XML bean 定义。 + +> 注意: +> +> 可以,但不推荐使用相对 `“../”` 路径来引用父目录中的文件。这样做会创建对当前应用程序之外的文件的依赖。特别是,不建议将此引用用于 `classpath`:URL(例如, `classpath:../services.xml`),其中运行时解析过程会选择“最近的”类路径根,然后查看其父目录。类路径配置更改可能会导致选择不同的、不正确的目录。 +> +> 可以使用完全限定的资源位置而不是相对路径:例如,`file:C:/config/services.xml` 或 `classpath:/config/services.xml`。建议为此类绝对路径保留一定的间接性  —  例如,通过 `“${...}”` 占位符来引用运行时指定 的 JVM 参数。 + +命名空间本身提供了导入指令功能。 Spring 提供的一系列 XML 命名空间中提供了除了普通 bean 定义之外的更多配置特性  —  例如,`context` 和 `util` 命名空间。 + +### 使用容器 + +`ApplicationContext` 能够维护不同 bean 及其依赖项的注册表。通过**使用方法 `T getBean(String name, Class T requiredType)`,可以检索并获取 bean 的实例**。 + +`ApplicationContext` 允许读取 bean 定义并访问它们,如以下示例所示: + +```java +// create and configure beans +ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); + +// retrieve configured instance +PetStoreService service = context.getBean("petStore", PetStoreService.class); + +// use configured instance +List userList = service.getUsernameList(); +``` + +最灵活的变体是 `GenericApplicationContext` 结合阅读器委托  —  例如,结合 XML 文件的 `XmlBeanDefinitionReader`,如下例所示: + +```java +GenericApplicationContext context = new GenericApplicationContext(); +new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml"); +context.refresh(); +``` + +可以在同一个 `ApplicationContext` 上混合和匹配此类读取器委托,从不同的配置源读取 bean 定义。 + +然后,可以使用 `getBean` 检索 bean 的实例。 `ApplicationContext` 接口还有一些其他方法用于检索 bean,但理想情况下,应用程序代码不应该使用它们。实际上,应用程序代码根本不应该调用 `getBean()` 方法,因此根本不依赖 Spring API。例如,Spring 与 Web 框架的集成为各种 Web 框架组件(例如控制器和 JSF 管理的 bean)提供了依赖注入,让您可以通过元数据(例如自动装配注释)声明对特定 bean 的依赖。 + +## IoC 依赖来源 + +自定义 Bean + +容器内建 Bean 对象 + +容器内建依赖 + +## IoC 配置元数据 + +IoC 容器的配置有三种方式: + +- 基于 xml 配置 +- 基于 properties 配置 +- 基于注解配置 +- 基于 Java 配置 + +作为 Spring 传统的配置方式,xml 配置方式一般为大家所熟知。 + +如果厌倦了 xml 配置,Spring 也提供了注解配置方式或 Java 配置方式来简化配置。 + +**本文,将对 Java 配置 IoC 容器做详细的介绍。** + +### Xml 配置 + +```xml + + + + + + + + + + +``` + +标签说明: + +- `` 是 Spring 配置文件的根节点。 +- `` 用来定义一个 JavaBean。`id` 属性是它的标识,在文件中必须唯一;`class` 属性是它关联的类。 +- `` 用来定义 Bean 的别名。 +- `` 用来导入其他配置文件的 Bean 定义。这是为了加载多个配置文件,当然也可以把这些配置文件构造为一个数组(new String[] {“config1.xml”, config2.xml})传给 `ApplicationContext` 实现类进行加载多个配置文件,那一个更适合由用户决定;这两种方式都是通过调用 Bean Definition Reader 读取 Bean 定义,内部实现没有任何区别。`` 标签可以放在 `` 下的任何位置,没有顺序关系。 + +#### 实例化容器 + +实例化容器的过程: +定位资源(XML 配置文件) +读取配置信息(Resource) +转化为 Spring 可识别的数据形式(BeanDefinition) + +```java +ApplicationContext context = + new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"}); +``` + +组合 xml 配置文件 +配置的 Bean 功能各不相同,都放在一个 xml 文件中,不便管理。 +Java 设计模式讲究职责单一原则。配置其实也是如此,功能不同的 JavaBean 应该被组织在不同的 xml 文件中。然后使用 import 标签把它们统一导入。 + +```xml + + +``` + +#### 使用容器 + +使用容器的方式就是通过`getBean`获取 IoC 容器中的 JavaBean。 +Spring 也有其他方法去获得 JavaBean,但是 Spring 并不推荐其他方式。 + +```java +// create and configure beans +ApplicationContext context = +new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"}); +// retrieve configured instance +PetStoreService service = context.getBean("petStore", PetStoreService.class); +// use configured instance +List userList = service.getUsernameList(); +``` + +### 注解配置 + +Spring2.5 引入了注解。 +于是,一个问题产生了:**使用注解方式注入 JavaBean 是不是一定完爆 xml 方式?** +未必。正所谓,仁者见仁智者见智。任何事物都有其优缺点,看你如何取舍。来看看注解的优缺点: +**优点**:大大减少了配置,并且可以使配置更加精细——类,方法,字段都可以用注解去标记。 +**缺点**:使用注解,不可避免产生了侵入式编程,也产生了一些问题。 + +- 你需要将注解加入你的源码并编译它; + +- 注解往往比较分散,不易管控。 + +> 注:spring 中,先进行注解注入,然后才是 xml 注入,因此如果注入的目标相同,后者会覆盖前者。 + +#### 启动注解 + +Spring 默认是不启用注解的。如果想使用注解,需要先在 xml 中启动注解。 +启动方式:在 xml 中加入一个标签,很简单吧。 + +```xml + +``` + +> 注:`` 只会检索定义它的上下文。什么意思呢?就是说,如果你 +> 为 DispatcherServlet 指定了一个`WebApplicationContext`,那么它只在 controller 中查找`@Autowired`注解,而不会检查其它的路径。 + +#### `@Required` + +`@Required` 注解只能用于修饰 bean 属性的 setter 方法。受影响的 bean 属性必须在配置时被填充在 xml 配置文件中,否则容器将抛出`BeanInitializationException`。 + +```java +public class AnnotationRequired { + private String name; + private String sex; + + public String getName() { + return name; + } + + /** + * @Required 注解用于bean属性的setter方法并且它指示,受影响的bean属性必须在配置时被填充在xml配置文件中, + * 否则容器将抛出BeanInitializationException。 + */ + @Required + public void setName(String name) { + this.name = name; + } + + public String getSex() { + return sex; + } + + public void setSex(String sex) { + this.sex = sex; + } +} +``` + +#### `@Autowired` + +`@Autowired`注解可用于修饰属性、setter 方法、构造方法。 + +@Autowired 注入过程 + +- 元信息解析 +- 依赖查找 +- 依赖注入(字段、方法) + +> 注:`@Autowired`注解也可用于修饰构造方法,但如果类中只有默认构造方法,则没有必要。如果有多个构造器,至少应该修饰一个,来告诉容器哪一个必须使用。 + +可以使用 JSR330 的注解`@Inject`来替代`@Autowired`。 + +**_范例_** + +```java +public class AnnotationAutowired { + private static final Logger log = LoggerFactory.getLogger(AnnotationRequired.class); + + @Autowired + private Apple fieldA; + + private Banana fieldB; + + private Orange fieldC; + + public Apple getFieldA() { + return fieldA; + } + + public void setFieldA(Apple fieldA) { + this.fieldA = fieldA; + } + + public Banana getFieldB() { + return fieldB; + } + + @Autowired + public void setFieldB(Banana fieldB) { + this.fieldB = fieldB; + } + + public Orange getFieldC() { + return fieldC; + } + + public void setFieldC(Orange fieldC) { + this.fieldC = fieldC; + } + + public AnnotationAutowired() {} + + @Autowired + public AnnotationAutowired(Orange fieldC) { + this.fieldC = fieldC; + } + + public static void main(String[] args) throws Exception { + AbstractApplicationContext ctx = + new ClassPathXmlApplicationContext("spring/spring-annotation.xml"); + + AnnotationAutowired annotationAutowired = + (AnnotationAutowired) ctx.getBean("annotationAutowired"); + log.debug("fieldA: {}, fieldB:{}, fieldC:{}", annotationAutowired.getFieldA().getName(), + annotationAutowired.getFieldB().getName(), + annotationAutowired.getFieldC().getName()); + ctx.close(); + } +} +``` + +xml 中的配置 + +```xml + + + + + +``` + +#### `@Qualifier` + +在`@Autowired`注解中,提到了如果发现有多个候选的 bean 都符合修饰类型,Spring 就会抓瞎了。 + +那么,如何解决这个问题。 + +可以通过`@Qualifier`指定 bean 名称来锁定真正需要的那个 bean。 + +**_范例_** + +```java +public class AnnotationQualifier { + private static final Logger log = LoggerFactory.getLogger(AnnotationQualifier.class); + + @Autowired + @Qualifier("dog") /** 去除这行,会报异常 */ + Animal dog; + + Animal cat; + + public Animal getDog() { + return dog; + } + + public void setDog(Animal dog) { + this.dog = dog; + } + + public Animal getCat() { + return cat; + } + + @Autowired + public void setCat(@Qualifier("cat") Animal cat) { + this.cat = cat; + } + + public static void main(String[] args) throws Exception { + AbstractApplicationContext ctx = + new ClassPathXmlApplicationContext("spring/spring-annotation.xml"); + + AnnotationQualifier annotationQualifier = + (AnnotationQualifier) ctx.getBean("annotationQualifier"); + + log.debug("Dog name: {}", annotationQualifier.getDog().getName()); + log.debug("Cat name: {}", annotationQualifier.getCat().getName()); + ctx.close(); + } +} + +abstract class Animal { + public String getName() { + return null; + } +} + +class Dog extends Animal { + public String getName() { + return "狗"; + } +} + +class Cat extends Animal { + public String getName() { + return "猫"; + } +} +``` + +xml 中的配置 + +```xml + + + + +``` + +#### `@Resource` + +Spring 支持 JSP250 规定的注解`@Resource`。这个注解根据指定的名称来注入 bean。 + +如果没有为`@Resource`指定名称,它会像`@Autowired`一样按照类型去寻找匹配。 + +在 Spring 中,由`CommonAnnotationBeanPostProcessor`来处理`@Resource`注解。 + +**_范例_** + +```java +public class AnnotationResource { + private static final Logger log = LoggerFactory.getLogger(AnnotationResource.class); + + @Resource(name = "flower") + Plant flower; + + @Resource(name = "tree") + Plant tree; + + public Plant getFlower() { + return flower; + } + + public void setFlower(Plant flower) { + this.flower = flower; + } + + public Plant getTree() { + return tree; + } + + public void setTree(Plant tree) { + this.tree = tree; + } + + public static void main(String[] args) throws Exception { + AbstractApplicationContext ctx = + new ClassPathXmlApplicationContext("spring/spring-annotation.xml"); + + AnnotationResource annotationResource = + (AnnotationResource) ctx.getBean("annotationResource"); + log.debug("type: {}, name: {}", annotationResource.getFlower().getClass(), annotationResource.getFlower().getName()); + log.debug("type: {}, name: {}", annotationResource.getTree().getClass(), annotationResource.getTree().getName()); + ctx.close(); + } +} +``` + +xml 的配置 + +```xml + + + + +``` + +#### `@PostConstruct` 和 `@PreDestroy` + +`@PostConstruct` 和 `@PreDestroy` 是 JSR 250 规定的用于生命周期的注解。 + +从其名号就可以看出,一个是在构造之后调用的方法,一个是销毁之前调用的方法。 + +```java +public class AnnotationPostConstructAndPreDestroy { + private static final Logger log = LoggerFactory.getLogger(AnnotationPostConstructAndPreDestroy.class); + + @PostConstruct + public void init() { + log.debug("call @PostConstruct method"); + } + + @PreDestroy + public void destroy() { + log.debug("call @PreDestroy method"); + } +} +``` + +#### `@Inject` + +从 Spring3.0 开始,Spring 支持 JSR 330 标准注解(依赖注入)。 + +注:如果要使用 JSR 330 注解,需要使用外部 jar 包。 + +若你使用 maven 管理 jar 包,只需要添加依赖到 pom.xml 即可: + +```xml + + javax.inject + javax.inject + 1 + +``` + +`@Inject` 和 `@Autowired` 一样,可以修饰属性、setter 方法、构造方法。 + +**_范例_** + +```java +public class AnnotationInject { + private static final Logger log = LoggerFactory.getLogger(AnnotationInject.class); + @Inject + Apple fieldA; + + Banana fieldB; + + Orange fieldC; + + public Apple getFieldA() { + return fieldA; + } + + public void setFieldA(Apple fieldA) { + this.fieldA = fieldA; + } + + public Banana getFieldB() { + return fieldB; + } + + @Inject + public void setFieldB(Banana fieldB) { + this.fieldB = fieldB; + } + + public Orange getFieldC() { + return fieldC; + } + + public AnnotationInject() {} + + @Inject + public AnnotationInject(Orange fieldC) { + this.fieldC = fieldC; + } + + public static void main(String[] args) throws Exception { + AbstractApplicationContext ctx = + new ClassPathXmlApplicationContext("spring/spring-annotation.xml"); + AnnotationInject annotationInject = (AnnotationInject) ctx.getBean("annotationInject"); + + log.debug("type: {}, name: {}", annotationInject.getFieldA().getClass(), + annotationInject.getFieldA().getName()); + + log.debug("type: {}, name: {}", annotationInject.getFieldB().getClass(), + annotationInject.getFieldB().getName()); + + log.debug("type: {}, name: {}", annotationInject.getFieldC().getClass(), + annotationInject.getFieldC().getName()); + + ctx.close(); + } +} +``` + +### Java 配置 + +基于 Java 配置 Spring IoC 容器,实际上是**Spring 允许用户定义一个类,在这个类中去管理 IoC 容器的配置**。 + +为了让 Spring 识别这个定义类为一个 Spring 配置类,需要用到两个注解:`@Configuration`和`@Bean`。 + +如果你熟悉 Spring 的 xml 配置方式,你可以将`@Configuration`等价于``标签;将`@Bean`等价于``标签。 + +#### `@Bean` + +@Bean 的修饰目标只能是方法或注解。 + +@Bean 只能定义在 `@Configuration` 或 `@Component` 注解修饰的类中。 + +#### 声明一个 bean + +此外,@Configuration 类允许在同一个类中通过@Bean 定义内部 bean 依赖。 + +声明一个 bean,只需要在 bean 属性的 set 方法上标注@Bean 即可。 + +```java +@Configuration +public class AnnotationConfiguration { + private static final Logger log = LoggerFactory.getLogger(JavaComponentScan.class); + + @Bean + public Job getPolice() { + return new Police(); + } + + public static void main(String[] args) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AnnotationConfiguration.class); + ctx.scan("org.zp.notes.spring.beans"); + ctx.refresh(); + Job job = (Job) ctx.getBean("police"); + log.debug("job: {}, work: {}", job.getClass(), job.work()); + } +} + +public interface Job { + String work(); +} + +@Component("police") +public class Police implements Job { + @Override + public String work() { + return "抓罪犯"; + } +} +``` + +这等价于配置 + +```xml + + + +``` + +@Bean 注解用来表明一个方法实例化、配置合初始化一个被 Spring IoC 容器管理的新对象。 + +如果你熟悉 Spring 的 xml 配置,你可以将@Bean 视为等价于``标签。 + +@Bean 注解可以用于任何的 Spring `@Component` bean,然而,通常被用于`@Configuration` bean。 + +#### `@Configuration` + +`@Configuration` 是一个类级别的注解,用来标记被修饰类的对象是一个`BeanDefinition`。 + +`@Configuration` 声明 bean 是通过被 `@Bean` 修饰的公共方法。此外,`@Configuration` 允许在同一个类中通过 `@Bean` 定义内部 bean 依赖。 + +```java +@Configuration +public class AppConfig { + @Bean + public MyService myService() { + return new MyServiceImpl(); + } +} +``` + +这等价于配置 + +```xml + + + +``` + +用 `AnnotationConfigApplicationContext` 实例化 IoC 容器。 + +## 依赖解决过程 + +容器执行 bean 依赖解析如下: + +- `ApplicationContext` 使用配置元数据创建和初始化 Bean。配置元数据可以由 XML、Java 代码或注解指定。 +- 对于每个 bean,其依赖关系以属性、构造函数参数或静态工厂方法的参数的形式表示。这些依赖项在实际创建 bean 时提供给 bean。 +- 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个 bean 的引用。 +- 作为值的每个属性或构造函数参数都从其指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将以字符串格式提供的值转换为所有内置类型,例如 int、long、String、boolean 等。 + +Spring 容器在创建容器时验证每个 bean 的配置。但是,在实际创建 bean 之前,不会设置 bean 属性本身。在创建容器时会创建 singleton 型的实例并设置为默认的 Bean。否则,只有在请求时才会创建 bean。 + +需注意:构造器注入,可能会导致无法解决循环依赖问题。 + +例如:A 类通过构造器注入需要 B 类的实例,B 类通过构造器注入需要 A 类的实例。Spring IoC 容器会在运行时检测到此循环引用,并抛出 `BeanCurrentlyInCreationException`。 + +一种解决方案是使用 setter 方法注入替代构造器注入。 + +另一种解决方案是:bean A 和 bean B 之间的循环依赖关系,强制其中一个 bean 在完全初始化之前注入另一个 bean(典型的先有鸡还是先有蛋的场景)。 + +Spring 会在容器加载时检测配置问题,例如引用不存在的 bean 或循环依赖。在实际创建 bean 时,Spring 会尽可能晚地设置属性并解析依赖关系。这意味着,如果在创建该对象或其依赖项之一时出现问题,则正确加载的 Spring 容器稍后可以在您请求对象时生成异常  —  例如,bean 由于丢失或无效而引发异常。某些配置问题的这种潜在的延迟可见性是默认情况下 ApplicationContext 实现预实例化单例 bean 的原因。以在实际需要之前创建这些 bean 的一些前期时间和内存为代价,您会在创建 ApplicationContext 时发现配置问题,而不是稍后。您仍然可以覆盖此默认行为,以便单例 bean 延迟初始化,而不是急切地预先实例化。 + +## 最佳实践 + +### singleton 的 Bean 如何注入 prototype 的 Bean + +Spring 创建的 Bean 默认是单例的,但当 Bean 遇到继承的时候,可能会忽略这一点。 + +假设有一个 SayService 抽象类,其中维护了一个类型是 ArrayList 的字段 data,用于保存方法处理的中间数据。每次调用 say 方法都会往 data 加入新数据,可以认为 SayService 是有状态,如果 SayService 是单例的话必然会 OOM。 + +```java +/** + * SayService 是有状态,如果 SayService 是单例的话必然会 OOM + */ +@Slf4j +public abstract class SayService { + + List data = new ArrayList<>(); + + public void say() { + data.add(IntStream.rangeClosed(1, 1000000) + .mapToObj(__ -> "a") + .collect(Collectors.joining("")) + UUID.randomUUID().toString()); + log.info("I'm {} size:{}", this, data.size()); + } + +} +``` + +但实际开发的时候,开发同学没有过多思考就把 SayHello 和 SayBye 类加上了 @Service 注解,让它们成为了 Bean,也没有考虑到父类是有状态的。 + +```java +@Service +@Slf4j +public class SayBye extends SayService { + + @Override + public void say() { + super.say(); + log.info("bye"); + } + +} + +@Service +@Slf4j +public class SayHello extends SayService { + + @Override + public void say() { + super.say(); + log.info("hello"); + } + +} +``` + +在为类标记上 @Service 注解把类型交由容器管理前,首先评估一下类是否有状态,然后为 Bean 设置合适的 Scope。 + +调用代码: + +```java +@Slf4j +@RestController +@RequestMapping("beansingletonandorder") +public class BeanSingletonAndOrderController { + + @Autowired + List sayServiceList; + @Autowired + private ApplicationContext applicationContext; + + @GetMapping("test") + public void test() { + log.info("===================="); + sayServiceList.forEach(SayService::say); + } + +} +``` + +可能有人认为,为 SayHello 和 SayBye 两个类都标记了 @Scope 注解,设置了 PROTOTYPE 的生命周期就可以解决上面的问题。 + +```java +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +``` + +但实际上还是有问题。因为@RestController 注解 =@Controller 注解 +@ResponseBody 注解,又因为 @Controller 标记了 @Component 元注解,所以 @RestController 注解其实也是一个 Spring Bean。 + +Bean 默认是单例的,所以单例的 Controller 注入的 Service 也是一次性创建的,即使 Service 本身标识了 prototype 的范围也没用。 + +修复方式是,让 Service 以代理方式注入。这样虽然 Controller 本身是单例的,但每次都能从代理获取 Service。这样一来,prototype 范围的配置才能真正生效。 + +```java +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProx) +``` + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/03.Spring\344\276\235\350\265\226\346\237\245\346\211\276.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/03.Spring\344\276\235\350\265\226\346\237\245\346\211\276.md" new file mode 100644 index 00000000..0d1fa338 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/03.Spring\344\276\235\350\265\226\346\237\245\346\211\276.md" @@ -0,0 +1,149 @@ +--- +title: Spring 依赖查找 +date: 2020-08-30 16:06:10 +order: 03 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - IOC + - 依赖查找 +permalink: /pages/9a6f6b/ +--- + +# Spring 依赖查找 + +**依赖查找是主动或手动的依赖查找方式,通常需要依赖容器或标准 API 实现**。 + +IoC 依赖查找大致可以分为以下几类: + +- 根据 Bean 名称查找 +- 根据 Bean 类型查找 +- 根据 Bean 名称 + 类型查找 +- 根据 Java 注解查找 + +此外,根据查找的 Bean 对象是单一或集合对象,是否需要延迟查找等特定常见,有相应不同的 API。 + +## 单一类型依赖查找 + +单一类型依赖查找接口- `BeanFactory` + +- 根据 Bean 名称查找 + - `getBean(String)` + - Spring 2.5 覆盖默认参数:`getBean(String,Object...)` +- 根据 Bean 类型查找 + - Bean 实时查找 + - Spring 3.0 `getBean(Class)` + - Spring 4.1 覆盖默认参数:`getBean(Class,Object...)` + - Spring 5.1 Bean 延迟查找 + - `getBeanProvider(Class)` + - `getBeanProvider(ResolvableType)` +- 根据 Bean 名称 + 类型查找:`getBean(String,Class)` + +## 集合类型依赖查找 + +集合类型依赖查找接口- `ListableBeanFactory` + +- 根据 Bean 类型查找 + - 获取同类型 Bean 名称列表 + - `getBeanNamesForType(Class)` + - Spring 4.2 `getBeanNamesForType(ResolvableType)` + - 获取同类型 Bean 实例列表 + - `getBeansOfType(Class)` 以及重载方法 +- 通过注解类型查找 + + - Spring 3.0 获取标注类型 Bean 名称列表 + + - `getBeanNamesForAnnotation(Class)` + + - Spring 3.0 获取标注类型 Bean 实例列表 + + - `getBeansWithAnnotation(Class)` + + - Spring 3.0 获取指定名称+ 标注类型 Bean 实例 + + - `findAnnotationOnBean(String,Class)` + +## 层次性依赖查找 + +层次性依赖查找接口- `HierarchicalBeanFactory` + +- 双亲 `BeanFactory`:`getParentBeanFactory()` +- 层次性查找 + - 根据 Bean 名称查找 + - 基于 `containsLocalBean` 方法实现 + - 根据 Bean 类型查找实例列表 + - 单一类型:`BeanFactoryUtils#beanOfType` + - 集合类型:`BeanFactoryUtils#beansOfTypeIncludingAncestors` + - 根据 Java 注解查找名称列表 + - `BeanFactoryUtils#beanNamesForTypeIncludingAncestors` + +## 延迟依赖查找 + +Bean 延迟依赖查找接口 + +- `org.springframework.beans.factory.ObjectFactory` +- `org.springframework.beans.factory.ObjectProvider`(Spring 5 对 Java 8 特性扩展) +- 函数式接口 + - `getIfAvailable(Supplier)` + - `ifAvailable(Consumer)` +- Stream 扩展- stream() + +## 安全依赖查找 + +| 依赖查找类型 | 代表实现 | 是否安全 | +| ------------ | ------------------------------------ | -------- | +| 单一类型查找 | `BeanFactory#getBean` | 否 | +| | `ObjectFactory#getObject` | 否 | +| | `ObjectProvider#getIfAvailable` | 是 | +| | | | +| 集合类型查找 | `ListableBeanFactory#getBeansOfType` | 是 | +| | `ObjectProvider#stream` | 是 | + +注意:层次性依赖查找的安全性取决于其扩展的单一或集合类型的 `BeanFactory` 接口 + +## 内建可查找的依赖 + +`AbstractApplicationContext` 内建可查找的依赖 + +| Bean | 名称 Bean | 实例使用场景 | +| --------------------------- | -------------------------------- | ----------------------- | +| environment | Environment 对象 | 外部化配置以及 Profiles | +| systemProperties | java.util.Properties 对象 | Java 系统属性 | +| systemEnvironment | java.util.Map 对象 | 操作系统环境变量 | +| messageSource | MessageSource 对象 | 国际化文案 | +| lifecycleProcessor | LifecycleProcessor 对象 | Lifecycle Bean 处理器 | +| applicationEventMulticaster | ApplicationEventMulticaster 对象 | Spring 事件广播器 | + +注解驱动 Spring 应用上下文内建可查找的依赖(部分) + +| Bean 名称 | Bean 实例 | 使用场景 | +| ------------------------------------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------- | +| org.springframework.context.annotation.internalConfigurationAnnotationProcessor | ConfigurationClassPostProcessor 对象 | 处理 Spring 配置类 | +| org.springframework.context.annotation.internalAutowiredAnnotationProcessor | AutowiredAnnotationBeanPostProcessor 对象 | 处理@Autowired 以及@Value 注解 | +| org.springframework.context.annotation.internalCommonAnnotationProcessor | CommonAnnotationBeanPostProcessor 对象 | (条件激活)处理 JSR-250 注解,如@PostConstruct 等 | +| org.springframework.context.event.internalEventListenerProcessor | EventListenerMethodProcessor 对象 | 处理标注@EventListener 的 Spring 事件监听方法 | +| org.springframework.context.event.internalEventListenerFactory | DefaultEventListenerFactory 对象 | @EventListener 事件监听方法适配为 ApplicationListener | +| org.springframework.context.annotation.internalPersistenceAnnotationProcessor | PersistenceAnnotationBeanPostProcessor 对象 | (条件激活)处理 JPA 注解场景 | + +## 依赖查找中的经典异常 + +`BeansException` 子类型 + +| 异常类型 | 触发条件(举例) | 场景举例 | +| --------------------------------- | ------------------------------------------ | -------------------------------------------- | +| `NoSuchBeanDefinitionException` | 当查找 Bean 不存在于 IoC 容器时 | `BeanFactory#getBeanObjectFactory#getObject` | +| `NoUniqueBeanDefinitionException` | 类型依赖查找时,IoC 容器存在多个 Bean 实例 | `BeanFactory#getBean(Class)` | +| `BeanInstantiationException` | 当 Bean 所对应的类型非具体类时 | `BeanFactory#getBean` | +| `BeanCreationException` | 当 Bean 初始化过程中 | Bean 初始化方法执行异常时 | +| `BeanDefinitionStoreException` | 当 `BeanDefinition` 配置元信息非法时 | XML 配置资源无法打开时 | + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/04.Spring\344\276\235\350\265\226\346\263\250\345\205\245.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/04.Spring\344\276\235\350\265\226\346\263\250\345\205\245.md" new file mode 100644 index 00000000..c44e438f --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/04.Spring\344\276\235\350\265\226\346\263\250\345\205\245.md" @@ -0,0 +1,378 @@ +--- +title: Spring 依赖注入 +date: 2020-08-30 16:06:10 +order: 04 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - IOC + - 依赖注入 +permalink: /pages/f61a1c/ +--- + +# Spring 依赖注入 + +DI,是 Dependency Injection 的缩写,即依赖注入。依赖注入是 IoC 的最常见形式。依赖注入是手动或自动绑定的方式,无需依赖特定的容器或 API。 + +依赖注入 (Dependency Injection,简称 DI) 是一个过程,其中对象仅通过构造函数参数、工厂方法的参数或对象实例在构造或从工厂方法返回。然后容器在创建 bean 时注入这些依赖项。这个过程基本上是 bean 本身的逆过程(因此得名,控制反转),它通过使用类的直接构造或服务定位器模式自行控制其依赖项的实例化或位置。 + +使用 DI,代码更干净,当对象具有依赖关系时,解耦更有效。对象不查找其依赖项,也不知道依赖项的位置或类别。结果,您的类变得更容易测试,特别是当依赖关系在接口或抽象基类上时,它们允许在单元测试中使用存根或模拟实现。 + +**容器全权负责组件的装配,它会把符合依赖关系的对象通过 JavaBean 属性或者构造函数传递给需要的对象**。 + +DI 是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。 + +理解 DI 的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下: + +- **谁依赖于谁:**当然是应用程序依赖于 IoC 容器; +- **为什么需要依赖:**应用程序需要 IoC 容器来提供对象需要的外部资源; +- **谁注入谁:**很明显是 IoC 容器注入应用程序某个对象,应用程序依赖的对象; +- **注入了什么**:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。 + +## IoC 依赖注入 API + +- 根据 Bean 名称注入 +- 根据 Bean 类型注入 +- 注入容器内建 Bean 对象 +- 注入非 Bean 对象 +- 注入类型 + - 实时注入 + - 延迟注入 + +## 依赖注入模式 + +依赖注入模式可以分为手动注入模式和自动注入模式。 + +### 手动注入模式 + +手动注入模式:配置或者编程的方式,提前安排注入规则 + +- XML 资源配置元信息 +- Java 注解配置元信息 +- API 配置元信息 + +### 自动注入模式 + +自动注入模式即自动装配。自动装配(Autowiring)是指 Spring 容器可以自动装配 Bean 之间的关系。Spring 可以通过检查 `ApplicationContext` 的内容,自动解析合作者(其他 Bean)。 + +- 自动装配可以显著减少属性或构造函数参数的配置。 +- 随着对象的发展,自动装配可以更新配置。 + +> 注:由于自动装配存在一些限制和不足,官方不推荐使用。 + +#### 自动装配策略 + +当使用基于 XML 的配置元数据时,可以使用 `` 元素的 `autowire` 属性为 Bean 指定自动装配模式。自动装配模式有以下类型: + +| 模式 | 说明 | +| ------------- | ---------------------------------------------------------------------- | +| `no` | 默认值,未激活 Autowiring,需要手动指定依赖注入对象。 | +| `byName` | 根据被注入属性的名称作为 Bean 名称进行依赖查找,并将对象设置到该属性。 | +| `byType` | 根据被注入属性的类型作为依赖类型进行查找,并将对象设置到该属性。 | +| `constructor` | 特殊 byType 类型,用于构造器参数。 | + +`org.springframework.beans.factory.config.AutowireCapableBeanFactory` 是 `BeanFactory` 的子接口,它是 Spring 中用于实现自动装配的容器。 + +#### @Autowired 注入过程 + +- 元信息解析 +- 依赖查找 +- 依赖注入(字段、方法) + +#### 自动装配的限制和不足 + +自动装配有以下限制和不足: + +- 属性和构造函数参数设置中的显式依赖项会覆盖自动装配。您不能自动装配简单属性,例如基础数据类型、字符串和类(以及此类简单属性的数组)。 +- 自动装配不如显式装配精准。Spring 会尽量避免猜测可能存在歧义的结果。 +- Spring 容器生成文档的工具可能无法解析自动装配信息。 +- 如果同一类型存在多个 Bean 时,自动装配时会存在歧义。容器内的多个 Bean 定义可能与要自动装配的 Setter 方法或构造函数参数指定的类型匹配。对于数组、集合或 Map 实例,这不一定是问题。但是,对于期望单值的依赖项,如果没有唯一的 Bean 定义可用,则会引发异常。 + +> 自动装配的限制和不足,详情可以参考官方文档:[Limitations and Disadvantages of Autowiring 小节](https://docs.spring.io/spring/docs/5.2.2.RELEASE/spring-frameworkreference/core.html#beans-autowired-exceptions) + +## 依赖注入方式 + +依赖注入有如下方式: + +| 依赖注入方式 | 配置元数据举例 | +| --------------- | -------------------------------------------------- | +| Setter 方法注入 | `` | +| 构造器注入 | `` | +| 字段注入 | `@Autowired User user;` | +| 方法注入 | `@Autowired public void user(User user) { ... }` | +| 接口回调注入 | `class MyBean implements BeanFactoryAware { ... }` | + +### 构造器注入 + +- 手动模式 + - xml 配置元信息 + - 注解配置元信息 + - Java 配置元信息 +- 自动模式 + - constructor + +构造器注入是通过容器调用具有多个参数的构造函数来完成的,每个参数代表一个依赖项。调用带有特定参数的静态工厂方法来构造 bean 几乎是等价的,并且本次讨论对构造函数和静态工厂方法的参数进行了类似的处理。 + +下面是一个构造器注入示例: + +```java +public class SimpleMovieLister { + + // the SimpleMovieLister has a dependency on a MovieFinder + private final MovieFinder movieFinder; + + // a constructor so that the Spring container can inject a MovieFinder + public SimpleMovieLister(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + // business logic that actually uses the injected MovieFinder is omitted... +} +``` + +构造函数参数解析匹配通过使用参数的类型进行。如果 bean 定义的构造函数参数中不存在潜在的歧义,则在 bean 定义中定义构造函数参数的顺序是在实例化 bean 时将这些参数提供给适当构造函数的顺序。 + +``` +package x.y; + +public class ThingOne { + + public ThingOne(ThingTwo thingTwo, ThingThree thingThree) { + // ... + } +} +``` + +假设 ThingTwo 和 ThingThree 类没有继承关系,则不存在潜在的歧义。因此,以下配置工作正常,您无需在 `` 元素中显式指定构造函数参数索引或类型。 + +```xml + + + + + + + + + + +``` + +当引用另一个 bean 时,类型是已知的,并且可以发生匹配(就像前面的示例一样)。当使用简单类型时,例如 `true` ,Spring 无法确定 value 的类型,因此无法在没有帮助的情况下按类型匹配。考虑以下类: + +```java +package examples; + +public class ExampleBean { + + // Number of years to calculate the Ultimate Answer + private final int years; + + // The Answer to Life, the Universe, and Everything + private final String ultimateAnswer; + + public ExampleBean(int years, String ultimateAnswer) { + this.years = years; + this.ultimateAnswer = ultimateAnswer; + } +} +``` + +构造函数参数类型匹配 + +在上述场景中,如果您使用 type 属性显式指定构造函数参数的类型,则容器可以使用简单类型的类型匹配,如以下示例所示: + +```xml + + + + +``` + +构造函数参数索引匹配 + +可以使用 `index` 属性显式指定构造函数参数的索引,如以下示例所示 + +```xml + + + + +``` + +构造函数参数名称匹配 + +```xml + + + + +``` + +可以使用 `@ConstructorProperties` 显式命名构造函数参数。 + +```java +package examples; + +public class ExampleBean { + + // Fields omitted + + @ConstructorProperties({"years", "ultimateAnswer"}) + public ExampleBean(int years, String ultimateAnswer) { + this.years = years; + this.ultimateAnswer = ultimateAnswer; + } +} +``` + +### Setter 方法注入 + +- 手动模式 + - xml 配置元信息 + - 注解配置元信息 + - Java 配置元信息 +- 自动模式 + - byName + - byType + +Setter 方法注入是通过容器在调用无参数构造函数或无参数静态工厂方法来实例化 bean 后调用 bean 上的 setter 方法来完成的。 + +以下示例显示了一个只能通过使用纯 setter 注入进行依赖注入的类。 + +```java +public class SimpleMovieLister { + + // the SimpleMovieLister has a dependency on the MovieFinder + private MovieFinder movieFinder; + + // a setter method so that the Spring container can inject a MovieFinder + public void setMovieFinder(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + // business logic that actually uses the injected MovieFinder is omitted... +} +``` + +在 Spring 中,可以混合使用构造器注入和 setter 方法注入。建议将构造器注入用于强制依赖项;并将 setter 方法注入或配置方法用于可选依赖项。需要注意的是,在 setter 方法上使用 `@Required` 注解可用于使属性成为必需的依赖项;然而,更建议使用构造器注入来完成这项工作。 + +### 字段注入 + +手动模式(Java 注解配置元信息) + +- `@Autowired` +- `@Resource` +- `@Inject`(可选) + +### 方法注入 + +手动模式(Java 注解配置元信息) + +- `@Autowired` +- `@Resource` +- `@Inject`(可选) +- `@Bean` + +### 接口回调注入 + +Aware 系列接口回调 + +| 內建接口 | 说明 | +| -------------------------------- | ---------------------------------------------------------- | +| `BeanFactoryAware` | 获取 IoC 容器- `BeanFactory` | +| `ApplicationContextAware` | 获取 Spring 应用上下文- `ApplicationContext` 对象 | +| `EnvironmentAware` | 获取 `Environment` 对象 | +| `ResourceLoaderAware` | 获取资源加载器对象- `ResourceLoader` | +| `BeanClassLoaderAware` | 获取加载当前 Bean Class 的 `ClassLoader` | +| `BeanNameAware` | 获取当前 Bean 的名称 | +| `MessageSourceAware` | 获取 `MessageSource` 对象,用于 Spring 国际化 | +| `ApplicationEventPublisherAware` | 获取 `ApplicationEventPublishAware` 对象,用于 Spring 事件 | +| `EmbeddedValueResolverAware` | 获取 `StringValueResolver` 对象,用于占位符处理 | + +### 依赖注入选型 + +- 低依赖:构造器注入 +- 多依赖:Setter 方法注入 +- 便利性:字段注入 +- 声明类:方法注入 + +## 限定注入和延迟注入 + +### 限定注入 + +- 使用 `@Qualifier` 注解限定 + - 通过 Bean 名称限定 + - 通过分组限定 +- 通过 `@Qualifier` 注解扩展限定 + - 自定义注解:如 Spring Cloud 的 `@LoadBalanced` + +### 延迟注入 + +- 使用 `ObjectFactory` +- 使用 `ObjectProvider`(推荐) + +## 依赖注入数据类型 + +### 基础类型 + +- 基础数据类型:`boolean`、`byte`、`char`、`short`、`int`、`float`、`long`、`double` +- 标量类型:`Number`、`Character`、`Boolean`、`Enum`、`Locale`、`Charset`、`Currency`、`Properties`、`UUID` +- 常规类型:`Object`、`String`、`TimeZone`、`Calendar`、`Optional` 等 +- Spring 类型:`Resource`、`InputSource`、`Formatter` 等。 + +### 集合类型 + +数组类型:基础数据类型、标量类型、常规类型、String 类型的数组 + +集合类型: + +- `Collection`:`List`、`Set` +- `Map`:`Properties` + +## 依赖处理过程 + +入口:`DefaultListableBeanFactory#resolveDependency` + +依赖描述符:`DependencyDescriptor` + +自定义绑定候选对象处理器:`AutowireCandidateResolver` + +`@Autowired`、`@Value`、`@javax.inject.Inject` 处理器:`AutowiredAnnotationBeanPostProcessor` + +通用注解处理器:`CommonAnnotationBeanPostProcessor` + +- 注入注解 + - `javax.xml.ws.WebServiceRef` + - `javax.ejb.EJB` + - `javax.annotation.Resources` +- 生命周期注解 + - `javax.annotation.PostConstruct` + - `javax.annotation.PreDestroy` + +自定义依赖注入注解: + +- 生命周期处理 + - `InstantiationAwareBeanPostProcessor` + - `MergedBeanDefinitionPostProcessor` +- 元数据 + - `InjectionMetadata` + - `InjectionMetadata.InjectedElement` + +## 依赖查找 VS. 依赖注入 + +| 类型 | 依赖处理 | 实现复杂度 | 代码侵入性 | API 依赖性 | 可读性 | +| -------- | -------- | ---------- | ------------ | -------------- | ------ | +| 依赖查找 | 主动 | 相对繁琐 | 侵入业务逻辑 | 依赖容器 API | 良好 | +| 依赖注入 | 被动 | 相对便利 | 低侵入性 | 不依赖容器 API | 一般 | + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/05.SpringIoC\344\276\235\350\265\226\346\235\245\346\272\220.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/05.SpringIoC\344\276\235\350\265\226\346\235\245\346\272\220.md" new file mode 100644 index 00000000..b633b853 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/05.SpringIoC\344\276\235\350\265\226\346\235\245\346\272\220.md" @@ -0,0 +1,124 @@ +--- +title: Spring IoC 依赖来源 +date: 2022-12-20 20:33:51 +order: 05 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - IOC + - 依赖注入 +permalink: /pages/a5f257/ +--- + +# Spring IoC 依赖来源 + +## 依赖查找的来源 + +查找来源 + +| 来源 | 配置元数据 | +| --------------------- | ---------------------------------------- | +| Spring BeanDefinition | `` | +| | `@Bean public User user() {...}` | +| | `BeanDefinitionBuilder` | +| 单例对象 | API 实现 | + +Spring 內建 BeanDefintion + +| Bean 名称 | Bean 实例 | 使用场景 | +| ------------------------------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------- | +| org.springframework.context.annotation.internalConfigurationAnnotationProcessor | ConfigurationClassPostProcessor 对象 | 处理 Spring 配置类 | +| org.springframework.context.annotation.internalAutowiredAnnotationProcessor | AutowiredAnnotationBeanPostProcessor 对象 | 处理 @Autowired 以及 @Value 注解 | +| org.springframework.context.annotation.internalCommonAnnotationProcessor | CommonAnnotationBeanPostProcessor 对象 | (条件激活)处理 JSR-250 注解,如 @PostConstruct 等 | +| org.springframework.context.event.internalEventListenerProcessor | EventListenerMethodProcessor 对象 | 处理标注 @EventListener 的 Spring 事件监听方法 | + +Spring 內建单例对象 + +| Bean 名称 | Bean 实例 | 使用场景 | +| --------------------------- | -------------------------------- | ----------------------- | +| environment | Environment 对象 | 外部化配置以及 Profiles | +| systemProperties | java.util.Properties 对象 | Java 系统属性 | +| systemEnvironment | java.util.Map 对象 | 操作系统环境变量 | +| messageSource | MessageSource 对象 | 国际化文案 | +| lifecycleProcessor | LifecycleProcessor 对象 | Lifecycle Bean 处理器 | +| applicationEventMulticaster | ApplicationEventMulticaster 对象 | Spring 事件广播器 | + +## 依赖注入的来源 + +| 来源 | 配置元数据 | +| ---------------------- | ---------------------------------------- | +| Spring BeanDefinition | `` | +| | `@Bean public User user() {...}` | +| | `BeanDefinitionBuilder` | +| 单例对象 | API 实现 | +| 非 Spring 容器管理对象 | | + +## Spring 容器管理和游离对象 + +| 来源 | Spring Bean 对象 | 生命周期管理 | 配置元信息 | 使用场景 | +| --------------------- | ---------------- | ------------ | ---------- | ------------------ | +| Spring BeanDefinition | 是 | 是 | 有 | 依赖查找、依赖注入 | +| 单体对象 | 是 | 否 | 无 | 依赖查找、依赖注入 | +| Resolvable Dependency | 否 | 否 | 无 | 依赖注入 | + +## Spring BeanDefinition 作为依赖来源 + +- 元数据:`BeanDefinition` +- 注册:`BeanDefinitionRegistry#registerBeanDefinition` +- 类型:延迟和非延迟 +- 顺序:Bean 生命周期顺序按照注册顺序 + +## 单例对象作为依赖来源 + +- 要素 + - 来源:外部普通 Java 对象(不一定是 POJO) + - 注册:`SingletonBeanRegistry#registerSingleton` +- 限制 + - 无生命周期管理 + - 无法实现延迟初始化 Bean + +## 非 Spring 对象容器管理对象作为依赖来源 + +- 要素 + - 注册:`ConfigurableListableBeanFactory#registerResolvableDependency` +- 限制 + - 无生命周期管理 + - 无法实现延迟初始化 Bean + - 无法通过依赖查找 + +## 外部化配置作为依赖来源 + +- 要素 + - 类型:非常规 Spring 对象依赖来源 +- 限制 + - 无生命周期管理 + - 无法实现延迟初始化 Bean + - 无法通过依赖查找 + +## 问题 + +注入和查找的依赖来源是否相同? + +否,依赖查找的来源仅限于 Spring `BeanDefinition` 以及单例对象,而依赖注入的来源还包括 Resolvable Dependency 以及 `@Value` 所标注的外部化配置 + +单例对象能在 IoC 容器启动后注册吗? + +可以的,单例对象的注册与 `BeanDefinition` 不同,`BeanDefinition` 会被 `ConfigurableListableBeanFactory#freezeConfiguration()` 方法影响,从而冻结注册,单例对象则没有这个限制。 + +Spring 依赖注入的来源有哪些? + +- Spring `BeanDefinition` +- 单例对象 +- Resolvable Dependency +- `@Value` 外部化配置 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/06.SpringBean\344\275\234\347\224\250\345\237\237.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/06.SpringBean\344\275\234\347\224\250\345\237\237.md" new file mode 100644 index 00000000..c07dacac --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/06.SpringBean\344\275\234\347\224\250\345\237\237.md" @@ -0,0 +1,102 @@ +--- +title: Spring Bean 作用域 +date: 2022-12-21 11:42:00 +order: 06 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Bean +permalink: /pages/8289f5/ +--- + +# Spring Bean 作用域 + +## Spring Bean 作用域 + +| 来源 | 说明 | +| ----------- | ---------------------------------------------------------- | +| singleton | 默认 Spring Bean 作用域,一个 BeanFactory 有且仅有一个实例 | +| prototype | 原型作用域,每次依赖查找和依赖注入生成新 Bean 对象 | +| request | 将 Spring Bean 存储在 ServletRequest 上下文中 | +| session | 将 Spring Bean 存储在 HttpSession 中 | +| application | 将 Spring Bean 存储在 ServletContext 中 | + +## "singleton" Bean 作用域 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221221170833.png) + +## "prototype" Bean 作用域 + +Spring 容器没有办法管理 prototype Bean 的完整生命周期,也没有办法记录实例的存在。销毁回调方法将不会执行,可以利用 `BeanPostProcessor` 进行清扫工作。 + +## "request" Bean 作用域 + +- 配置 + - XML - `` + - Java 注解 - `@RequestScope` 或 `@Scope(WebApplicationContext.SCOPE_REQUEST)` +- 实现 + - API - RequestScope + +## "session" Bean 作用域 + +- 配置 + - XML - `` + - Java 注解 - `@SessionScope` 或 `@Scope(WebApplicationContext.SCOPE_SESSION)` +- 实现 + - API - SessionScope + +## "application" Bean 作用域 + +- 配置 + - XML - `` + - Java 注解 - `@ApplicationScope` 或 `@Scope(WebApplicationContext.SCOPE_APPLICATION)` +- 实现 + - API - ServletContextScope + +## 自定义 Bean 作用域 + +- 实现 Scope + + - `org.springframework.beans.factory.config.Scope` + +- 注册 Scope + + - API - `org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope` + +- 配置 + + ```xml + + + + + + + + + ``` + +## 问题 + +Spring 內建的 Bean 作用域有几种? + +singleton、prototype、request、session、application 以及 websocket + +singleton Bean 是否在一个应用是唯一的? + +否。singleton bean 仅在当前 Spring IoC 容器(BeanFactory)中是单例对象。 + +application Bean 是否可以被其他方案替代? + +可以的,实际上,“application” Bean 与“singleton” Bean 没有本质区别 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/07.SpringBean\347\224\237\345\221\275\345\221\250\346\234\237.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/07.SpringBean\347\224\237\345\221\275\345\221\250\346\234\237.md" new file mode 100644 index 00000000..4a72cd8c --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/07.SpringBean\347\224\237\345\221\275\345\221\250\346\234\237.md" @@ -0,0 +1,186 @@ +--- +title: Spring Bean 生命周期 +date: 2022-12-21 19:26:01 +order: 07 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Bean +permalink: /pages/4ab176/ +--- + +# Spring Bean 生命周期 + +## Spring Bean 元信息配置阶段 + +BeanDefinition 配置 + +- 面向资源 + - XML 配置 + - Properties 资源配置 +- 面向注解 +- 面向 API + +## Spring Bean 元信息解析阶段 + +- 面向资源 BeanDefinition 解析 + - BeanDefinitionReader + - XML 解析器 - BeanDefinitionParser +- 面向注解 BeanDefinition 解析 + - AnnotatedBeanDefinitionReader + +## Spring Bean 注册阶段 + +BeanDefinition 注册接口:BeanDefinitionRegistry + +## Spring BeanDefinition 合并阶段 + +BeanDefinition 合并 + +父子 BeanDefinition 合并 + +- 当前 BeanFactory 查找 +- 层次性 BeanFactory 查找 + +## Spring Bean Class 加载阶段 + +- ClassLoader 类加载 +- Java Security 安全控制 +- ConfigurableBeanFactory 临时 ClassLoader + +## Spring Bean 实例化前阶段 + +实例化方式 + +- 传统实例化方式:实例化策略(InstantiationStrategy) +- 构造器依赖注入 + +## Spring Bean 实例化阶段 + +非主流生命周期 - Bean 实例化前阶段 + +InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation + +## Spring Bean 实例化后阶段 + +Bean 属性赋值(Populate)判断 + +InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation + +## Spring Bean 属性赋值前阶段 + +- Bean 属性值元信息 + - PropertyValues +- Bean 属性赋值前回调 + - Spring 1.2 - 5.0:InstantiationAwareBeanPostProcessor#postProcessPropertyValues + - Spring 5.1:InstantiationAwareBeanPostProcessor#postProcessProperties + +## Spring Bean Aware 接口回调阶段 + +Spring Aware 接口: + +- BeanNameAware +- BeanClassLoaderAware +- BeanFactoryAware +- EnvironmentAware +- EmbeddedValueResolverAware +- ResourceLoaderAware +- ApplicationEventPublisherAware +- MessageSourceAware +- ApplicationContextAware + +## Spring Bean 初始化前阶段 + +已完成: + +- Bean 实例化 + +- Bean 属性赋值 + +- Bean Aware 接口回调 + +方法回调: + +- BeanPostProcessor#postProcessBeforeInitialization + +## Spring Bean 初始化阶段 + +Bean 初始化(Initialization) + +- @PostConstruct 标注方法 +- 实现 InitializingBean 接口的 afterPropertiesSet() 方法 +- 自定义初始化方法 + +## Spring Bean 初始化后阶段 + +方法回调:BeanPostProcessor#postProcessAfterInitialization + +## Spring Bean 初始化完成阶段 + +方法回调:Spring 4.1 +:SmartInitializingSingleton#afterSingletonsInstantiated + +## Spring Bean 销毁前阶段 + +方法回调:DestructionAwareBeanPostProcessor#postProcessBeforeDestruction + +## Spring Bean 销毁阶段 + +Bean 销毁(Destroy) + +- @PreDestroy 标注方法 +- 实现 DisposableBean 接口的 destroy() 方法 +- 自定义销毁方法 + +## Spring Bean 垃圾收集 + +Bean 垃圾回收(GC) + +- 关闭 Spring 容器(应用上下文) +- 执行 GC +- Spring Bean 覆盖的 finalize() 方法被回调 + +## 问题 + +**BeanPostProcessor 的使用场景有哪些**? + +BeanPostProcessor 提供 Spring Bean 初始化前和初始化后的生命周期回调,分别对应 postProcessBeforeInitialization 以及 postProcessAfterInitialization 方法,允许对关心的 Bean 进行扩展,甚至是替换。 + +加分项:其中,ApplicationContext 相关的 Aware 回调也是基于 BeanPostProcessor 实现,即 ApplicationContextAwareProcessor。 + +**BeanFactoryPostProcessor 与 BeanPostProcessor 的区别**? + +BeanFactoryPostProcessor 是 Spring BeanFactory(实际为 ConfigurableListableBeanFactory) 的后置处理器,用于扩展 BeanFactory,或通过 BeanFactory 进行依赖查找和依赖注入。 + +BeanFactoryPostProcessor 必须有 Spring ApplicationContext 执行,BeanFactory 无法与其直接交互。 + +而 BeanPostProcessor 则直接与 BeanFactory 关联,属于 N 对 1 的关系。 + +**BeanFactory 是怎样处理 Bean 生命周期**? + +BeanFactory 的默认实现为 `DefaultListableBeanFactory`,其中 Bean生命周期与方法映射如下: + +- BeanDefinition 注册阶段 - registerBeanDefinition +- BeanDefinition 合并阶段 - getMergedBeanDefinition +- Bean 实例化前阶段 - resolveBeforeInstantiation +- Bean 实例化阶段 - createBeanInstance +- Bean 初始化后阶段 - populateBean +- Bean 属性赋值前阶段 - populateBean +- Bean 属性赋值阶段 - populateBean +- Bean Aware 接口回调阶段 - initializeBean +- Bean 初始化前阶段 - initializeBean +- Bean 初始化阶段 - initializeBean +- Bean 初始化后阶段 - initializeBean +- Bean 初始化完成阶段 - preInstantiateSingletons +- Bean 销毁前阶段 - destroyBean +- Bean 销毁阶段 - destroyBean + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/08.Spring\351\205\215\347\275\256\345\205\203\346\225\260\346\215\256.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/08.Spring\351\205\215\347\275\256\345\205\203\346\225\260\346\215\256.md" new file mode 100644 index 00000000..54150343 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/08.Spring\351\205\215\347\275\256\345\205\203\346\225\260\346\215\256.md" @@ -0,0 +1,299 @@ +--- +title: Spring 配置元数据 +date: 2022-12-21 19:49:48 +order: 08 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Bean +permalink: /pages/55f315/ +--- + +# Spring 配置元数据 + +## Spring 配置元信息 + +- Spring Bean 配置元信息 - BeanDefinition +- Spring Bean 属性元信息 - PropertyValues +- Spring 容器配置元信息 +- Spring 外部化配置元信息 - PropertySource +- Spring Profile 元信息 - @Profile + +## Spring Bean 配置元信息 + +Bean 配置元信息 - BeanDefinition + +- GenericBeanDefinition:通用型 BeanDefinition +- RootBeanDefinition:无 Parent 的 BeanDefinition 或者合并后 BeanDefinition +- AnnotatedBeanDefinition:注解标注的 BeanDefinition + +## Spring Bean 属性元信息 + +- Bean 属性元信息 - PropertyValues + - 可修改实现 - MutablePropertyValues + - 元素成员 - PropertyValue +- Bean 属性上下文存储 - AttributeAccessor +- Bean 元信息元素 - BeanMetadataElement + +## Spring 容器配置元信息 + +Spring XML 配置元信息 - beans 元素相关 + +| beans 元素属性 | 默认值 | 使用场景 | +| --------------------------- | ------------ | ----------------------------------------------------------------------- | +| profile | null(留空) | Spring Profiles 配置值 | +| default-lazy-init | default | 当 outter beans “default-lazy-init” 属性存在时,继承该值,否则为“false” | +| default-merge | default | 当 outter beans “default-merge” 属性存在时,继承该值,否则为“false” | +| default-autowire | default | 当 outter beans “default-autowire” 属性存在时,继承该值,否则为“no” | +| default-autowire-candidates | null(留空) | 默认 Spring Beans 名称 pattern | +| default-init-method | null(留空) | 默认 Spring Beans 自定义初始化方法 | +| default-destroy-method | null(留空) | 默认 Spring Beans 自定义销毁方法 | + +Spring XML 配置元信息 - 应用上下文相关 + +| XML 元素 | 使用场景 | +| ---------------------------------- | ------------------------------------ | +| `` | 激活 Spring 注解驱动 | +| `` | Spring @Component 以及自定义注解扫描 | +| `` | 激活 Spring LoadTimeWeaver | +| `` | 暴露 Spring Beans 作为 JMX Beans | +| `` | 将当前平台作为 MBeanServer | +| `` | 加载外部化配置资源作为 Spring 属性配 | +| `` | 利用外部化配置资源覆盖 Spring 属 | + +## 基于 XML 文件装载 Spring Bean 配置元信息 + +底层实现 - XmlBeanDefinitionReader + +| XML 元素 | 使用场景 | +| ------------------ | --------------------------------------------- | +| `` | 单 XML 资源下的多个 Spring Beans 配置 | +| `` | 单个 Spring Bean 定义(BeanDefinition)配置 | +| `` | 为 Spring Bean 定义(BeanDefinition)映射别名 | +| `` | 加载外部 Spring XML 配置资源 | + +## 基于 Properties 文件装载 Spring Bean 配置元信息 + +底层实现 - PropertiesBeanDefinitionReader + +| Properties 属性名 | 使用场景 | +| ----------------- | ------------------------------- | +| `class` | Bean 类全称限定名 | +| `abstract` | 是否为抽象的 BeanDefinition | +| `parent` | 指定 parent BeanDefinition 名称 | +| `lazy-init` | 是否为延迟初始化 | +| `ref` | 引用其他 Bean 的名称 | +| `scope` | 设置 Bean 的 scope 属性 | +| ${n} | n 表示第 n+1 个构造器参数 | + +## 基于 Java 注解装载 Spring Bean 配置元信息 + +Spring 模式注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ---------------- | ------------------ | -------- | +| `@Repository` | 数据仓储模式注解 | 2.0 | +| `@Component` | 通用组件模式注解 | 2.5 | +| `@Service` | 服务模式注解 | 2.5 | +| `@Controller` | Web 控制器模式注解 | 2.5 | +| `@Configuration` | 配置类模式注解 | 3.0 | + +Spring Bean 定义注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ------------ | ------------------------------------------ | ----------- | --- | +| `@Bean` | 替换 XML 元素 `` | 3.0 | +| `@DependsOn` | 替代 XML 属性 `` | 3.0 | +| `@Lazy` | 替代 XML 属性 `` | 3.0 | +| `@Primary` | 替换 XML 元素 `` | 3.0 | +| `@Role` | 替换 XML 元素 `` | 3.1 | +| `@Lookup` | 替代 XML 属性 `` | 4.1 | + +Spring Bean 依赖注入注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ------------ | ----------------------------------- | -------- | +| `@Autowired` | Bean 依赖注入,支持多种依赖查找方式 | 2.5 | +| `@Qualifier` | 细粒度的 @Autowired 依赖查找 | 2.5 | + +| Java 注解 | 场景说明 | 起始版本 | +| --------- | ----------------- | -------- | +| @Resource | 类似于 @Autowired | 2.5 | +| @Inject | 类似于 @Autowired | 2.5 | + +Spring Bean 条件装配注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ------------ | -------------- | -------- | +| @Profile | 配置化条件装配 | 3.1 | +| @Conditional | 编程条件装配 | 4.0 | + +Spring Bean 生命周期回调注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| -------------- | ------------------------------------------------------------- | -------- | +| @PostConstruct | 替换 XML 元素 或 InitializingBean | 2.5 | +| @PreDestroy | 替换 XML 元素 或 DisposableBean | 2.5 | + +Spring BeanDefinition 解析与注册 + +| Spring 注解 | 场景说明 | 起始版本 | +| --------------- | ------------------------------ | -------- | +| XML 资源 | XmlBeanDefinitionReader | 1.0 | +| Properties 资源 | PropertiesBeanDefinitionReader | 1.0 | +| Java 注解 | AnnotatedBeanDefinitionReader | 3.0 | + +## Spring Bean 配置元信息底层实现 + +### Spring XML 资源 BeanDefinition 解析与注册 + +核心 API - XmlBeanDefinitionReader + +- 资源 - Resource +- 底层 - BeanDefinitionDocumentReader + - XML 解析 - Java DOM Level 3 API + - BeanDefinition 解析 - BeanDefinitionParserDelegate + - BeanDefinition 注册 - BeanDefinitionRegistry + +### Spring Properties 资源 BeanDefinition 解析与注册 + +核心 API - PropertiesBeanDefinitionReader + +- 资源 + - 字节流 - Resource + - 字符流 - EncodedResouce +- 底层 + - 存储 - java.util.Properties + - BeanDefinition 解析 - API 内部实现 + - BeanDefinition 注册 - BeanDefinitionRegistry + +### Spring Java 注册 BeanDefinition 解析与注册 + +核心 API - AnnotatedBeanDefinitionReader + +- 资源 + - 类对象 - java.lang.Class +- 底层 + - 条件评估 - ConditionEvaluator + - Bean 范围解析 - ScopeMetadataResolver + - BeanDefinition 解析 - 内部 API 实现 + - BeanDefinition 处理 - AnnotationConfigUtils.processCommonDefinitionAnnotations + - BeanDefinition 注册 - BeanDefinitionRegistry + +## 基于 XML 文件装载 Spring IoC 容器配置元信息 + +Spring IoC 容器相关 XML 配置 + +| 命名空间 | 所属模块 | Schema 资源 URL | +| -------- | -------------- | ----------------------------------------------------------------- | +| beans | spring-beans | https://www.springframework.org/schema/beans/spring-beans.xsd | +| context | spring-context | https://www.springframework.org/schema/context/spring-context.xsd | +| aop | spring-aop | https://www.springframework.org/schema/aop/spring-aop.xsd | +| tx | spring-tx | https://www.springframework.org/schema/tx/spring-tx.xsd | +| util | spring-beans | beans https://www.springframework.org/schema/util/spring-util.xsd | +| tool | spring-beans | https://www.springframework.org/schema/tool/spring-tool.xsd | + +## 基于 Java 注解装载 Spring IoC 容器配置元信息 + +Spring IoC 容器装配注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| --------------- | ------------------------------------------- | -------- | +| @ImportResource | 替换 XML 元素 `` | 3.0 | +| @Import | 导入 Configuration Class | 3.0 | +| @ComponentScan | 扫描指定 package 下标注 Spring 模式注解的类 | 3.1 | + +Spring IoC 配属属性注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ---------------- | -------------------------------- | -------- | +| @PropertySource | 配置属性抽象 PropertySource 注解 | 3.1 | +| @PropertySources | @PropertySource 集合注解 | 4.0 | + +## 基于 Extensible XML authoring 扩展 SpringXML 元素 + +Spring XML 扩展 + +- 编写 XML Schema 文件:定义 XML 结构 +- 自定义 NamespaceHandler 实现:命名空间绑定 +- 自定义 BeanDefinitionParser 实现:XML 元素与 BeanDefinition 解析 +- 注册 XML 扩展:命名空间与 XML Schema 映射 + +## Extensible XML authoring 扩展原理 + +### 触发时机 + +- AbstractApplicationContext#obtainFreshBeanFactory + - AbstractRefreshableApplicationContext#refreshBeanFactory + - AbstractXmlApplicationContext#loadBeanDefinitions + - ... + - XmlBeanDefinitionReader#doLoadBeanDefinitions + - ... + - BeanDefinitionParserDelegate#parseCustomElement + +### 核心流程 + +BeanDefinitionParserDelegate#parseCustomElement(org.w3c.dom.Element, BeanDefinition) + +- 获取 namespace +- 通过 namespace 解析 NamespaceHandler +- 构造 ParserContext +- 解析元素,获取 BeanDefinintion + +## 基于 Properties 文件装载外部化配置 + +注解驱动 + +- @org.springframework.context.annotation.PropertySource +- @org.springframework.context.annotation.PropertySources + +API 编程 + +- org.springframework.core.env.PropertySource +- org.springframework.core.env.PropertySources + +## 基于 YAML 文件装载外部化配置 + +API 编程 + +- org.springframework.beans.factory.config.YamlProcessor + - org.springframework.beans.factory.config.YamlMapFactoryBean + - org.springframework.beans.factory.config.YamlPropertiesFactoryBean + +## 问题 + +**Spring 內建 XML Schema 常见有哪些**? + +| 命名空间 | 所属模块 | Schema 资源 URL | +| -------- | -------------- | ----------------------------------------------------------------- | +| beans | spring-beans | https://www.springframework.org/schema/beans/spring-beans.xsd | +| context | spring-context | https://www.springframework.org/schema/context/spring-context.xsd | +| aop | spring-aop | https://www.springframework.org/schema/aop/spring-aop.xsd | +| tx | spring-tx | https://www.springframework.org/schema/tx/spring-tx.xsd | +| util | spring-beans | beans https://www.springframework.org/schema/util/spring-util.xsd | +| tool | spring-beans | https://www.springframework.org/schema/tool/spring-tool.xsd | + +**Spring 配置元信息具体有哪些**? + +- Bean 配置元信息:通过媒介(如 XML、Proeprties 等),解析 BeanDefinition +- IoC 容器配置元信息:通过媒介(如 XML、Proeprties 等),控制 IoC 容器行为,比如注解驱动、AOP 等 +- 外部化配置:通过资源抽象(如 Proeprties、YAML 等),控制 PropertySource +- Spring Profile:通过外部化配置,提供条件分支流程 + +**Extensible XML authoring 的缺点**? + +- 高复杂度:开发人员需要熟悉 XML Schema,spring.handlers,spring.schemas 以及 Spring API +- 嵌套元素支持较弱:通常需要使用方法递归或者其嵌套解析的方式处理嵌套(子)元素 +- XML 处理性能较差:Spring XML 基于 DOM Level 3 API 实现,该 API 便于理解,然而性能较差 +- XML 框架移植性差:很难适配高性能和便利性的 XML 框架,如 JAXB + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/09.Spring\345\272\224\347\224\250\344\270\212\344\270\213\346\226\207\347\224\237\345\221\275\345\221\250\346\234\237.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/09.Spring\345\272\224\347\224\250\344\270\212\344\270\213\346\226\207\347\224\237\345\221\275\345\221\250\346\234\237.md" new file mode 100644 index 00000000..fea4b398 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/09.Spring\345\272\224\347\224\250\344\270\212\344\270\213\346\226\207\347\224\237\345\221\275\345\221\250\346\234\237.md" @@ -0,0 +1,171 @@ +--- +title: Spring 应用上下文生命周期 +date: 2022-12-23 09:58:09 +order: 09 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/ad472e/ +--- + +# Spring 应用上下文生命周期 + +## Spring 应用上下文启动准备阶段 + +AbstractApplicationContext#prepareRefresh() 方法 + +- 启动时间 - startupDate +- 状态标识 - closed(false)、active(true) +- 初始化 PropertySources - initPropertySources() +- 检验 Environment 中必须属性 +- 初始化事件监听器集合 +- 初始化早期 Spring 事件集合 + +## BeanFactory 创建阶段 + +AbstractApplicationContext#obtainFreshBeanFactory() 方法 + +- 刷新 Spring 应用上下文底层 BeanFactory - refreshBeanFactory() + - 销毁或关闭 BeanFactory,如果已存在的话 + - 创建 BeanFactory - createBeanFactory() + - 设置 BeanFactory Id + - 设置“是否允许 BeanDefinition 重复定义” - customizeBeanFactory(DefaultListableBeanFactory) + - 设置“是否允许循环引用(依赖)” - customizeBeanFactory(DefaultListableBeanFactory) + - 加载 BeanDefinition - loadBeanDefinitions(DefaultListableBeanFactory) 方法 + - 关联新建 BeanFactory 到 Spring 应用上下文 +- 返回 Spring 应用上下文底层 BeanFactory - getBeanFactory() + +## BeanFactory 准备阶段 + +AbstractApplicationContext#prepareBeanFactory(ConfigurableListableBeanFactory) 方法 + +- 关联 ClassLoader +- 设置 Bean 表达式处理器 +- 添加 PropertyEditorRegistrar 实现 - ResourceEditorRegistrar +- 添加 Aware 回调接口 BeanPostProcessor 实现 - ApplicationContextAwareProcessor +- 忽略 Aware 回调接口作为依赖注入接口 +- 注册 ResolvableDependency 对象 - BeanFactory、ResourceLoader、ApplicationEventPublisher 以及 ApplicationContext +- 注册 ApplicationListenerDetector 对象 +- 注册 LoadTimeWeaverAwareProcessor 对象 +- 注册单例对象 - Environment、Java System Properties 以及 OS 环境变量 + +## BeanFactory 后置处理阶段 + +- AbstractApplicationContext#postProcessBeanFactory(ConfigurableListableBeanFactory) 方法 + - 由子类覆盖该方法 +- AbstractApplicationContext#invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory 方法 + - 调用 BeanFactoryPostProcessor 或 BeanDefinitionRegistry 后置处理方法 + - 注册 LoadTimeWeaverAwareProcessor 对象 + +## BeanFactory 注册 BeanPostProcessor 阶段 + +AbstractApplicationContext#registerBeanPostProcessors(ConfigurableListableBeanFactory) 方法 + +- 注册 PriorityOrdered 类型的 BeanPostProcessor Beans +- 注册 Ordered 类型的 BeanPostProcessor Beans +- 注册普通 BeanPostProcessor Beans +- 注册 MergedBeanDefinitionPostProcessor Beans +- 注册 ApplicationListenerDetector 对象 + +## 初始化內建 Bean:MessageSource + +AbstractApplicationContext#initMessageSource() 方法 + +## 初始化內建 Bean:Spring 事件广播器 + +AbstractApplicationContext#initApplicationEventMulticaster() 方法 + +## Spring 应用上下文刷新阶段 + +AbstractApplicationContext#onRefresh() 方法 + +子类覆盖该方法 + +- org.springframework.web.context.support.AbstractRefreshableWebApplicationContext#onRefresh() +- org.springframework.web.context.support.GenericWebApplicationContext#onRefresh() +- org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext#onRefresh() +- org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh() +- org.springframework.web.context.support.StaticWebApplicationContext#onRefresh() + +## Spring 事件监听器注册阶段 + +AbstractApplicationContext#registerListeners() 方法 + +- 添加当前应用上下文所关联的 ApplicationListener 对象(集合) +- 添加 BeanFactory 所注册 ApplicationListener Beans +- 广播早期 Spring 事件 + +## BeanFactory 初始化完成阶段 + +AbstractApplicationContext#finishBeanFactoryInitialization(ConfigurableListableBeanFactory) 方法 + +- BeanFactory 关联 ConversionService Bean,如果存在 +- 添加 StringValueResolver 对象 +- 依赖查找 LoadTimeWeaverAware Bean +- BeanFactory 临时 ClassLoader 置为 null +- BeanFactory 冻结配置 +- BeanFactory 初始化非延迟单例 Beans + +## Spring 应用上下刷新完成阶段 + +AbstractApplicationContext#finishRefresh() 方法 + +- 清除 ResourceLoader 缓存 - clearResourceCaches() @since 5.0 +- 初始化 LifecycleProcessor 对象 - initLifecycleProcessor() +- 调用 LifecycleProcessor#onRefresh() 方法 +- 发布 Spring 应用上下文已刷新事件 - ContextRefreshedEvent +- 向 MBeanServer 托管 Live Beans + +## Spring 应用上下文启动阶段 + +AbstractApplicationContext#start() 方法 + +- 启动 LifecycleProcessor + - 依赖查找 Lifecycle Beans + - 启动 Lifecycle Beans +- 发布 Spring 应用上下文已启动事件 - ContextStartedEvent + +## Spring 应用上下文停止阶段 + +AbstractApplicationContext#stop() 方法 + +- 停止 LifecycleProcessor + - 依赖查找 Lifecycle Beans + - 停止 Lifecycle Beans +- 发布 Spring 应用上下文已停止事件 - ContextStoppedEvent + +## Spring 应用上下文关闭阶段 + +AbstractApplicationContext#close() 方法 + +- 状态标识:active(false)、closed(true) +- Live Beans JMX 撤销托管 + - LiveBeansView.unregisterApplicationContext(ConfigurableApplicationContext) +- 发布 Spring 应用上下文已关闭事件 - ContextClosedEvent +- 关闭 LifecycleProcessor + - 依赖查找 Lifecycle Beans + - 停止 Lifecycle Beans +- 销毁 Spring Beans +- 关闭 BeanFactory +- 回调 onClose() +- 注册 Shutdown Hook 线程(如果曾注册) + +## 问题 + +**Spring 应用上下文生命周期有哪些阶段**? + +- 刷新阶段 - ConfigurableApplicationContext#refresh() +- 启动阶段 - ConfigurableApplicationContext#start() +- 停止阶段 - ConfigurableApplicationContext#stop() +- 关闭阶段 - ConfigurableApplicationContext#close() + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/10.SpringAop.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/10.SpringAop.md" new file mode 100644 index 00000000..1a89468a --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/10.SpringAop.md" @@ -0,0 +1,399 @@ +--- +title: Spring AOP +date: 2020-02-26 23:47:47 +order: 10 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - AOP +permalink: /pages/53aedb/ +--- + +# Spring AOP + +## AOP 概念 + +### 什么是 AOP + +AOP(Aspect-Oriented Programming,即 **面向切面编程**)与 OOP( Object-Oriented Programming,面向对象编程) 相辅相成,提供了与 OOP 不同的抽象软件结构的视角。 + +在 OOP 中,我们以类(class)作为我们的基本单元,而 AOP 中的基本单元是 **Aspect(切面)** + +### 术语 + +#### Aspect(切面) + +`aspect` 由 `pointcount` 和 `advice` 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP 就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的连接点中. +AOP 的工作重心在于如何将增强织入目标对象的连接点上, 这里包含两个工作: + +1. 如何通过 pointcut 和 advice 定位到特定的 joinpoint 上 +2. 如何在 advice 中编写切面代码. + +**可以简单地认为, 使用 @Aspect 注解的类就是切面.** + +#### advice(增强) + +由 aspect 添加到特定的 join point(即满足 point cut 规则的 join point) 的一段代码. +许多 AOP 框架, 包括 Spring AOP, 会将 advice 模拟为一个拦截器(interceptor), 并且在 join point 上维护多个 advice, 进行层层拦截. +例如 HTTP 鉴权的实现, 我们可以为每个使用 RequestMapping 标注的方法织入 advice, 当 HTTP 请求到来时, 首先进入到 advice 代码中, 在这里我们可以分析这个 HTTP 请求是否有相应的权限, 如果有, 则执行 Controller, 如果没有, 则抛出异常. 这里的 advice 就扮演着鉴权拦截器的角色了. + +#### 连接点(join point) + +> a point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution. + +程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理. +`在 Spring AOP 中, join point 总是方法的执行点, 即只有方法连接点.` + +#### 切点(point cut) + +匹配 join point 的谓词(a predicate that matches join points). +Advice 是和特定的 point cut 关联的, 并且在 point cut 相匹配的 join point 中执行. +`在 Spring 中, 所有的方法都可以认为是 joinpoint, 但是我们并不希望在所有的方法上都添加 Advice, 而 pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice.` + +#### 关于 join point 和 point cut 的区别 + +在 Spring AOP 中, 所有的方法执行都是 join point. 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 因此 join point 和 point cut 本质上就是两个不同纬度上的东西. +`advice 是在 join point 上执行的, 而 point cut 规定了哪些 join point 可以执行哪些 advice` + +#### introduction + +为一个类型添加额外的方法或字段. Spring AOP 允许我们为 `目标对象` 引入新的接口(和对应的实现). 例如我们可以使用 introduction 来为一个 bean 实现 IsModified 接口, 并以此来简化 caching 的实现. + +#### 目标对象(Target) + +织入 advice 的目标对象. 目标对象也被称为 `advised object`. +`因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 总是一个代理对象(proxied object)` +`注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类.` + +#### AOP proxy + +一个类被 AOP 织入 advice, 就会产生一个结果类, 它是融合了原类和增强逻辑的代理类. +在 Spring AOP 中, 一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象. + +#### 织入(Weaving) + +将 aspect 和其他对象连接起来, 并创建 adviced object 的过程. +根据不同的实现技术, AOP 织入有三种方式: + +- 编译器织入, 这要求有特殊的 Java 编译器. +- 类装载期织入, 这需要有特殊的类装载器. +- 动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式. + Spring 采用动态代理织入, 而 AspectJ 采用编译器织入和类装载期织入. + +### advice 的类型 + +- before advice, 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码) +- after return advice, 在一个 join point 正常返回后执行的 advice +- after throwing advice, 当一个 join point 抛出异常后执行的 advice +- after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice. +- around advice, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice. + +### 关于 AOP Proxy + +Spring AOP 默认使用标准的 JDK 动态代理(dynamic proxy)技术来实现 AOP 代理, 通过它, 我们可以为任意的接口实现代理. +`如果需要为一个类实现代理, 那么可以使用 CGLIB 代理.` 当一个业务逻辑对象没有实现接口时, 那么 Spring AOP 就默认使用 CGLIB 来作为 AOP 代理了. 即如果我们需要为一个方法织入 advice, 但是这个方法不是一个接口所提供的方法, 则此时 Spring AOP 会使用 CGLIB 来实现动态代理. 鉴于此, Spring AOP 建议基于接口编程, 对接口进行 AOP 而不是类. + +### 彻底理解 aspect, join point, point cut, advice + +看完了上面的理论部分知识, 我相信还是会有不少朋友感觉到 AOP 的概念还是很模糊, 对 AOP 中的各种概念理解的还不是很透彻. 其实这很正常, 因为 AOP 中的概念是在是太多了, 我当时也是花了老大劲才梳理清楚的. +下面我以一个简单的例子来比喻一下 AOP 中 aspect, jointpoint, pointcut 与 advice 之间的关系. + +让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来. + +来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系. +首先我们知道, 在 Spring AOP 中 join point 指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 对应到我们在上面举的例子, 我们可以做一个简单的类比, join point 就相当于 **爪哇的小县城里的百姓**, point cut 就相当于 **老王所做的指控, 即凶手是个男性, 身高约七尺五寸**, 而 advice 则是施加在符合老王所描述的嫌疑人的动作: **抓过来审问**. +为什么可以这样类比呢? + +- join point --> 爪哇的小县城里的百姓: 因为根据定义, join point 是所有可能被织入 advice 的候选的点, 在 Spring AOP 中, 则可以认为所有方法执行点都是 join point. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人. +- point cut --> 男性, 身高约七尺五寸: 我们知道, 所有的方法(joint point) 都可以织入 advice, 但是我们并不希望在所有方法上都织入 advice, 而 pointcut 的作用就是提供一组规则来匹配 joinpoint, 给满足规则的 joinpoint 添加 advice. 同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据`凶手是个男性, 身高约七尺五寸`, 把符合条件的人抓起来. 在这里`凶手是个男性, 身高约七尺五寸` 就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问. +- advice --> 抓过来审问, advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 join point 上的. 同理, 对比到我们的例子中, `抓过来审问` 这个动作就是对作用于那些满足 `男性, 身高约七尺五寸` 的`爪哇的小县城里的百姓`. +- aspect: aspect 是 point cut 与 advice 的组合, 因此在这里我们就可以类比: **"根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问"** 这一整个动作可以被认为是一个 aspect. + +或则我们也可以从语法的角度来简单类比一下. 我们在学英语时, 经常会接触什么 `定语`, `被动句` 之类的概念, 那么可以做一个不严谨的类比, 即 `joinpoint` 可以认为是一个 `宾语`, 而 `pointcut` 则可以类比为修饰 `joinpoint` 的定语, 那么整个 `aspect` 就可以描述为: `满足 pointcut 规则的 joinpoint 会被添加相应的 advice 操作.` + +## @AspectJ 支持 + +**`@AspectJ`** 是一种使用 Java 注解来实现 AOP 的编码风格。 + +@AspectJ 风格的 AOP 是 AspectJ Project 在 AspectJ 5 中引入的, 并且 Spring 也支持 @AspectJ 的 AOP 风格. + +### 使能 @AspectJ 支持 + +@AspectJ 可以以 XML 的方式或以注解的方式来使能, 并且不论以哪种方式使能@ASpectJ, 我们都必须保证 aspectjweaver.jar 在 classpath 中. + +#### 使用 Java Configuration 方式使能@AspectJ + +```java +@Configuration +@EnableAspectJAutoProxy +public class AppConfig { +} +``` + +#### 使用 XML 方式使能@AspectJ + +``` + +``` + +### 定义 aspect(切面) + +当使用注解 **@Aspect** 标注一个 Bean 后, 那么 Spring 框架会自动收集这些 Bean, 并添加到 Spring AOP 中, 例如: + +```java +@Component +@Aspect +public class MyTest { +} +``` + +`注意, 仅仅使用@Aspect 注解, 并不能将一个 Java 对象转换为 Bean, 因此我们还需要使用类似 @Component 之类的注解.` +`注意, 如果一个 类被@Aspect 标注, 则这个类就不能是其他 aspect 的 **advised object** 了, 因为使用 @Aspect 后, 这个类就会被排除在 auto-proxying 机制之外.` + +### 声明 pointcut + +一个 pointcut 的声明由两部分组成: + +- 一个方法签名, 包括方法名和相关参数 +- 一个 pointcut 表达式, 用来指定哪些方法执行是我们感兴趣的(即因此可以织入 advice). + +在@AspectJ 风格的 AOP 中, 我们使用一个方法来描述 pointcut, 即: + +```java +@Pointcut("execution(* com.xys.service.UserService.*(..))") // 切点表达式 +private void dataAccessOperation() {} // 切点前面 +``` + +`这个方法必须无返回值.` +`这个方法本身就是 pointcut signature, pointcut 表达式使用@Pointcut 注解指定.` +上面我们简单地定义了一个 pointcut, 这个 pointcut 所描述的是: 匹配所有在包 **com.xys.service.UserService** 下的所有方法的执行. + +#### 切点标志符(designator) + +AspectJ5 的切点表达式由标志符(designator)和操作参数组成. 如 "execution(\* greetTo(..))" 的切点表达式, \*\*execution** 就是 标志符, 而圆括号里的 \*\*\***greetTo(..) 就是操作参数 + +##### execution + +匹配 join point 的执行, 例如 "execution(\* hello(..))" 表示匹配所有目标类中的 hello() 方法. 这个是最基本的 pointcut 标志符. + +##### within + +匹配特定包下的所有 join point, 例如 `within(com.xys.*)` 表示 com.xys 包中的所有连接点, 即包中的所有类的所有方法. 而`within(com.xys.service.*Service)` 表示在 com.xys.service 包中所有以 Service 结尾的类的所有的连接点. + +##### this 与 target + +this 的作用是匹配一个 bean, 这个 bean(Spring AOP proxy) 是一个给定类型的实例(instance of). 而 target 匹配的是一个目标对象(target object, 即需要织入 advice 的原始的类), 此对象是一个给定类型的实例(instance of). + +##### bean + +匹配 bean 名字为指定值的 bean 下的所有方法, 例如: + +``` +bean(*Service) // 匹配名字后缀为 Service 的 bean 下的所有方法 +bean(myService) // 匹配名字为 myService 的 bean 下的所有方法 +``` + +##### args + +匹配参数满足要求的的方法. +例如: + +```java +@Pointcut("within(com.xys.demo2.*)") +public void pointcut2() { +} + +@Before(value = "pointcut2() && args(name)") +public void doSomething(String name) { + logger.info("---page: {}---", name); +} +``` + +```java +@Service +public class NormalService { + private Logger logger = LoggerFactory.getLogger(getClass()); + + public void someMethod() { + logger.info("---NormalService: someMethod invoked---"); + } + + public String test(String name) { + logger.info("---NormalService: test invoked---"); + return "服务一切正常"; + } +} +``` + +当 NormalService.test 执行时, 则 advice `doSomething` 就会执行, test 方法的参数 name 就会传递到 `doSomething` 中. + +常用例子: + +```java +// 匹配只有一个参数 name 的方法 +@Before(value = "aspectMethod() && args(name)") +public void doSomething(String name) { +} + +// 匹配第一个参数为 name 的方法 +@Before(value = "aspectMethod() && args(name, ..)") +public void doSomething(String name) { +} + +// 匹配第二个参数为 name 的方法 +Before(value = "aspectMethod() && args(*, name, ..)") +public void doSomething(String name) { +} +``` + +##### @annotation + +匹配由指定注解所标注的方法, 例如: + +```java +@Pointcut("@annotation(com.xys.demo1.AuthChecker)") +public void pointcut() { +} +``` + +则匹配由注解 `AuthChecker` 所标注的方法. + +#### 常见的切点表达式 + +##### 匹配方法签名 + +``` +// 匹配指定包中的所有的方法 +execution(* com.xys.service.*(..)) + +// 匹配当前包中的指定类的所有方法 +execution(* UserService.*(..)) + +// 匹配指定包中的所有 public 方法 +execution(public * com.xys.service.*(..)) + +// 匹配指定包中的所有 public 方法, 并且返回值是 int 类型的方法 +execution(public int com.xys.service.*(..)) + +// 匹配指定包中的所有 public 方法, 并且第一个参数是 String, 返回值是 int 类型的方法 +execution(public int com.xys.service.*(String name, ..)) +``` + +##### 匹配类型签名 + +``` +// 匹配指定包中的所有的方法, 但不包括子包 +within(com.xys.service.*) + +// 匹配指定包中的所有的方法, 包括子包 +within(com.xys.service..*) + +// 匹配当前包中的指定类中的方法 +within(UserService) + + +// 匹配一个接口的所有实现类中的实现的方法 +within(UserDao+) +``` + +##### 匹配 Bean 名字 + +``` +// 匹配以指定名字结尾的 Bean 中的所有方法 +bean(*Service) +``` + +##### 切点表达式组合 + +``` +// 匹配以 Service 或 ServiceImpl 结尾的 bean +bean(*Service || *ServiceImpl) + +// 匹配名字以 Service 结尾, 并且在包 com.xys.service 中的 bean +bean(*Service) && within(com.xys.service.*) +``` + +### 声明 advice + +advice 是和一个 pointcut 表达式关联在一起的, 并且会在匹配的 join point 的方法执行的前/后/周围 运行. `pointcut 表达式可以是简单的一个 pointcut 名字的引用, 或者是完整的 pointcut 表达式`. +下面我们以几个简单的 advice 为例子, 来看一下一个 advice 是如何声明的. + +#### Before advice + +```java +/** + * @author xiongyongshun + * @version 1.0 + * @created 16/9/9 13:13 + */ +@Component +@Aspect +public class BeforeAspectTest { + // 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise. + @Pointcut("execution(* com.xys.service.UserService.*(..))") + public void dataAccessOperation() { + } +} +``` + +```java +@Component +@Aspect +public class AdviseDefine { + // 定义 advise + @Before("com.xys.aspect.PointcutDefine.dataAccessOperation()") + public void doBeforeAccessCheck(JoinPoint joinPoint) { + System.out.println("*****Before advise, method: " + joinPoint.getSignature().toShortString() + " *****"); + } +} +``` + +这里, **@Before** 引用了一个 pointcut, 即 "com.xys.aspect.PointcutDefine.dataAccessOperation()" 是一个 pointcut 的名字. +如果我们在 advice 在内置 pointcut, 则可以: + +```java +@Component +@Aspect +public class AdviseDefine { + // 将 pointcut 和 advice 同时定义 + @Before("within(com.xys.service..*)") + public void doAccessCheck(JoinPoint joinPoint) { + System.out.println("*****doAccessCheck, Before advise, method: " + joinPoint.getSignature().toShortString() + " *****"); + } +} +``` + +#### around advice + +around advice 比较特别, 它可以在一个方法的之前之前和之后添加不同的操作, 并且甚至可以决定何时, 如何, 是否调用匹配到的方法. + +```java +@Component +@Aspect +public class AdviseDefine { + // 定义 advise + @Around("com.xys.aspect.PointcutDefine.dataAccessOperation()") + public Object doAroundAccessCheck(ProceedingJoinPoint pjp) throws Throwable { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + // 开始 + Object retVal = pjp.proceed(); + stopWatch.stop(); + // 结束 + System.out.println("invoke method: " + pjp.getSignature().getName() + ", elapsed time: " + stopWatch.getTotalTimeMillis()); + return retVal; + } +} +``` + +around advice 和前面的 before advice 差不多, 只是我们把注解 **@Before** 改为了 **@Around** 了. + +## 参考资料 + +- [《 Spring 实战(第 4 版)》](https://item.jd.com/11899370.html) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/20.Spring\350\265\204\346\272\220\347\256\241\347\220\206.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/20.Spring\350\265\204\346\272\220\347\256\241\347\220\206.md" new file mode 100644 index 00000000..b285f663 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/20.Spring\350\265\204\346\272\220\347\256\241\347\220\206.md" @@ -0,0 +1,261 @@ +--- +title: Spring 资源管理 +date: 2019-09-04 19:46:41 +order: 20 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Resource +permalink: /pages/a1549f/ +--- + +# Spring 资源管理 + +> Version 6.0.3 + +## Resource 接口 + +相对标准 URL 访问机制,Spring 的 `org.springframework.core.io.Resource` 接口抽象了对底层资源的访问接口,提供了一套更好的访问方式。 + +```java +public interface Resource extends InputStreamSource { + + boolean exists(); + + boolean isReadable(); + + boolean isOpen(); + + boolean isFile(); + + URL getURL() throws IOException; + + URI getURI() throws IOException; + + File getFile() throws IOException; + + ReadableByteChannel readableChannel() throws IOException; + + long contentLength() throws IOException; + + long lastModified() throws IOException; + + Resource createRelative(String relativePath) throws IOException; + + String getFilename(); + + String getDescription(); +} +``` + +正如 `Resource` 接口的定义所示,它扩展了 `InputStreamSource` 接口。`Resource` 最核心的方法如下: + +- `getInputStream()` - 定位并且打开当前资源,返回当前资源的 `InputStream`。每次调用都会返回一个新的 `InputStream`。调用者需要负责关闭流。 +- `exists()` - 判断当前资源是否真的存在。 +- `isOpen()` - 判断当前资源是否是一个已打开的 `InputStream`。如果为 true,则 `InputStream` 不能被多次读取,必须只读取一次然后关闭以避免资源泄漏。对所有常用资源实现返回 false,`InputStreamResource` 除外。 +- `getDescription()` - 返回当前资源的描述,当处理资源出错时,资源的描述会用于错误信息的输出。一般来说,资源的描述是一个完全限定的文件名称,或者是当前资源的真实 URL。 + +常见 Spring 资源接口: + +| 类型 | 接口 | +| ---------- | ----------------------------------------------------- | +| 输入流 | `org.springframework.core.io.InputStreamSource` | +| 只读资源 | `org.springframework.core.io.Resource` | +| 可写资源 | `org.springframework.core.io.WritableResource` | +| 编码资源 | `org.springframework.core.io.support.EncodedResource` | +| 上下文资源 | `org.springframework.core.io.ContextResource` | + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221223155859.png) + +## 内置的 Resource 实现 + +Spring 包括几个内置的 Resource 实现: + +| 资源来源 | 前缀 | 说明 | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`UrlResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-urlresource) | `file:`、`https:`、`ftp:` 等 | `UrlResource` 封装了一个 `java.net.URL` 对象,**用于访问可通过 URL 访问的任何对象**,例如文件、HTTPS 目标、FTP 目标等。所有 URL 都可以通过标准化的字符串形式表示,因此可以使用适当的标准化前缀来指示一种 URL 类型与另一种 URL 类型的区别。 这包括:`file`:用于访问文件系统路径;`https`:用于通过 HTTPS 协议访问资源;`ftp`:用于通过 FTP 访问资源等等。 | +| [`ClassPathResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-classpathresource) | `classpath:` | `ClassPathResource` **从类路径上加载资源**。它使用线程上下文加载器、给定的类加载器或指定的 class 类型中的任意一个来加载资源。 | +| [`FileSystemResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-filesystemresource) | `file:` | `FileSystemResource` **是 `java.io.File` 的资源实现**。它还支持 `java.nio.file.Path` ,应用 Spring 的标准对字符串路径进行转换。`FileSystemResource` 支持解析为文件和 URL。 | +| [`PathResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-pathresource) | 无 | `PathResource` 是 `java.nio.file.Path` 的资源实现。 | +| [`ServletContextResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-servletcontextresource) | 无 | `ServletContextResource` **是 `ServletContext` 的资源实现**。它表示相应 Web 应用程序根目录中的相对路径。 | +| [`InputStreamResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-inputstreamresource) | 无 | `InputStreamResource` **是指定 `InputStream` 的资源实现**。注意:如果该 `InputStream` 已被打开,则不可以多次读取该流。 | +| [`ByteArrayResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-bytearrayresource) | 无 | `ByteArrayResource` 是指定的二进制数组的资源实现。它会为给定的字节数组创建一个 `ByteArrayInputStream`。 | + +## ResourceLoader 接口 + +`ResourceLoader` 接口用于加载 `Resource` 对象。其定义如下: + +```java +public interface ResourceLoader { + + Resource getResource(String location); + + ClassLoader getClassLoader(); +} +``` + +Spring 中主要的 ResourceLoader 实现: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221223164745.png) + +Spring 中,所有的 `ApplicationContext` 都实现了 `ResourceLoader` 接口。因此,所有 `ApplicationContext` 都可以通过 `getResource()` 方法获取 `Resource` 实例。 + +【示例】 + +```java +// 如果没有指定资源前缀,Spring 会尝试返回合适的资源 +Resource template = ctx.getResource("some/resource/path/myTemplate.txt"); +// 如果指定 classpath: 前缀,Spring 会强制使用 ClassPathResource +Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt"); +// 如果指定 file:、http 等 URL 前缀,Spring 会强制使用 UrlResource +Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt"); +Resource template = ctx.getResource("http://myhost.com/resource/path/myTemplate.txt"); +``` + +下表列举了 Spring 根据各种位置路径加载资源的策略: + +| 前缀 | 样例 | 说明 | +| ------------ | -------------------------------- | :----------------------------------- | +| `classpath:` | `classpath:com/myapp/config.xml` | 从类路径加载 | +| `file:` | `file:///data/config.xml` | 以 URL 形式从文件系统加载 | +| `http:` | `http://myserver/logo.png` | 以 URL 形式加载 | +| 无 | `/data/config.xml` | 由底层的 ApplicationContext 实现决定 | + +## ResourcePatternResolver 接口 + +`ResourcePatternResolver` 接口是 `ResourceLoader` 接口的扩展,它的作用是定义策略,根据位置模式解析 `Resource` 对象。 + +```java +public interface ResourcePatternResolver extends ResourceLoader { + + String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; + + Resource[] getResources(String locationPattern) throws IOException; +} +``` + +`PathMatchingResourcePatternResolver` 是一个独立的实现,可以在 `ApplicationContext` 之外使用,也可以被 `ResourceArrayPropertyEditor` 用于填充 `Resource[]` bean 属性。`PathMatchingResourcePatternResolver` 能够将指定的资源位置路径解析为一个或多个匹配的 `Resource` 对象。 + +> 注意:任何标准 `ApplicationContext` 中的默认 `ResourceLoader` 实际上是 `PathMatchingResourcePatternResolver` 的一个实例,它实现了 `ResourcePatternResolver` 接口。 + +## ResourceLoaderAware 接口 + +`ResourceLoaderAware` 接口是一个特殊的回调接口,用来标记提供 `ResourceLoader` 引用的对象。`ResourceLoaderAware` 接口定义如下: + +```java +public interface ResourceLoaderAware { + void setResourceLoader(ResourceLoader resourceLoader); +} +``` + +当一个类实现 `ResourceLoaderAware` 并部署到应用程序上下文中(作为 Spring 管理的 bean)时,它会被应用程序上下文识别为 `ResourceLoaderAware`,然后,应用程序上下文会调用 `setResourceLoader(ResourceLoader)`,将自身作为参数提供(请记住,Spring 中的所有应用程序上下文都实现 `ResourceLoader` 接口)。 + +由于 `ApplicationContext` 是一个 `ResourceLoader`,该 bean 还可以实现 `ApplicationContextAware` 接口并直接使用提供的应用程序上下文来加载资源。 但是,一般来说,如果您只需要这些,最好使用专门的 `ResourceLoader` 接口。 该代码将仅耦合到资源加载接口(可以被视为实用程序接口),而不耦合到整个 Spring `ApplicationContext` 接口。 + +在应用程序中,还可以使用 `ResourceLoader` 的自动装配作为实现 `ResourceLoaderAware` 接口的替代方法。传统的构造函数和 `byType` 自动装配模式能够分别为构造函数参数或 setter 方法参数提供 `ResourceLoader`。 为了获得更大的灵活性(包括自动装配字段和多参数方法的能力),请考虑使用基于注解的自动装配功能。 在这种情况下,`ResourceLoader` 会自动连接到需要 `ResourceLoader` 类型的字段、构造函数参数或方法参数中,只要相关字段、构造函数或方法带有 `@Autowired` 注解即可。 + +## 资源依赖 + +如果 bean 本身要通过某种动态过程来确定和提供资源路径,那么 bean 可以使用 `ResourceLoader` 或 `ResourcePatternResolver` 接口来加载资源。 例如,考虑加载某种模板,其中所需的特定资源取决于用户的角色。 如果资源是静态的,完全消除 `ResourceLoader` 接口(或 `ResourcePatternResolver` 接口)的使用,让 bean 公开它需要的 `Resource` 属性,并期望将它们注入其中是有意义的。 + +使注入这些属性变得简单的原因是所有应用程序上下文都注册并使用一个特殊的 JavaBeans `PropertyEditor`,它可以将 `String` 路径转换为 `Resource` 对象。 例如,下面的 MyBean 类有一个 `Resource` 类型的模板属性。 + +【示例】 + +```xml + + + +``` + +请注意,配置中引用的模板资源路径没有前缀,因为应用程序上下文本身将用作 `ResourceLoader`,资源本身将根据需要通过 `ClassPathResource`,`FileSystemResource` 或 ServletContextResource 加载,具体取决于上下文的确切类型。 + +如果需要强制使用特定的资源类型,则可以使用前缀。 以下两个示例显示如何强制使用 `ClassPathResource` 和 `UrlResource`(后者用于访问文件系统文件)。 + +```xml + + +``` + +可以通过 `@Value` 注解加载资源文件 `myTemplate.txt`,示例如下: + +```java +@Component +public class MyBean { + + private final Resource template; + + public MyBean(@Value("${template.path}") Resource template) { + this.template = template; + } + + // ... +} +``` + +Spring 的 `PropertyEditor` 会根据资源文件的路径字符串,加载 `Resource` 对象,并将其注入到 MyBean 的构造方法。 + +如果想要加载多个资源文件,可以使用 `classpath*:` 前缀,例如:`classpath*:/config/templates/*.txt`。 + +```java +@Component +public class MyBean { + + private final Resource[] templates; + + public MyBean(@Value("${templates.path}") Resource[] templates) { + this.templates = templates; + } + + // ... +} +``` + +## 应用上下文和资源路径 + +### 构造应用上下文 + +应用上下文构造函数(针对特定的应用上下文类型)通常将字符串或字符串数组作为资源的位置路径,例如构成上下文定义的 XML 文件。 + +【示例】 + +```java +ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml"); +ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml"); +ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:conf/appContext.xml"); +ApplicationContext ctx = new ClassPathXmlApplicationContext( + new String[] {"services.xml", "daos.xml"}, MessengerService.class); +``` + +### 使用通配符构造应用上下文 + +ApplicationContext 构造器的中的资源路径可以是单一的路径(即一对一地映射到目标资源);也可以是通配符形式——可包含 classpath\*:也可以是前缀或 ant 风格的正则表达式(使用 spring 的 PathMatcher 来匹配)。 + +示例: + +```java +ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml"); +``` + +使用 `classpath*` 表示类路径下所有匹配文件名称的资源都会被获取(本质上就是调用了 ClassLoader.getResources(…) 方法),接着将获取到的资源组装成最终的应用上下文。 + +在位置路径的其余部分,`classpath*:` 前缀可以与 PathMatcher 结合使用,如:`classpath*:META-INF/*-beans.xml`。 + +## 问题 + +Spring 配置资源中有哪些常见类型? + +- XML 资源 +- Properties 资源 +- YAML 资源 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/21.Spring\346\240\241\351\252\214.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/21.Spring\346\240\241\351\252\214.md" new file mode 100644 index 00000000..d99a6fcc --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/21.Spring\346\240\241\351\252\214.md" @@ -0,0 +1,637 @@ +--- +title: Spring 校验 +date: 2022-12-22 17:42:28 +order: 21 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/fe6aad/ +--- + +# Spring 校验 + +Java API 规范(`JSR303`)定义了`Bean`校验的标准`validation-api`,但没有提供实现。`hibernate validation`是对这个规范的实现,并增加了校验注解如`@Email`、`@Length`等。`Spring Validation`是对`hibernate validation`的二次封装,用于支持`spring mvc`参数自动校验。 + +## 快速入门 + +### 引入依赖 + +如果 spring-boot 版本小于 2.3.x,spring-boot-starter-web 会自动传入 hibernate-validator 依赖。如果 spring-boot 版本大于 2.3.x,则需要手动引入依赖: + +```xml + + org.hibernate.validator + hibernate-validator-parent + 6.2.5.Final + +``` + +对于 web 服务来说,为防止非法参数对业务造成影响,在 Controller 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式: + +- POST、PUT 请求,使用 requestBody 传递参数; +- GET 请求,使用 requestParam/PathVariable 传递参数。 + +实际上,不管是 requestBody 参数校验还是方法级别的校验,最终都是调用 Hibernate Validator 执行校验,Spring Validation 只是做了一层封装。 + +### 校验示例 + +(1)在实体上标记校验注解 + +```kotlin +@Data +@NoArgsConstructor +@AllArgsConstructor +public class User implements Serializable { + + @NotNull + private Long id; + + @NotBlank + @Size(min = 2, max = 10) + private String name; + + @Min(value = 1) + @Max(value = 100) + private Integer age; + +} +``` + +(2)在方法参数上声明校验注解 + +```less +@Slf4j +@Validated +@RestController +@RequestMapping("validate1") +public class ValidatorController { + + /** + * {@link RequestBody} 参数校验 + */ + @PostMapping(value = "save") + public DataResult save(@Valid @RequestBody User entity) { + log.info("保存一条记录:{}", JSONUtil.toJsonStr(entity)); + return DataResult.ok(true); + } + + /** + * {@link RequestParam} 参数校验 + */ + @GetMapping(value = "queryByName") + public DataResult queryByName( + @RequestParam("username") + @NotBlank + @Size(min = 2, max = 10) + String name + ) { + User user = new User(1L, name, 18); + return DataResult.ok(user); + } + + /** + * {@link PathVariable} 参数校验 + */ + @GetMapping(value = "detail/{id}") + public DataResult detail(@PathVariable("id") @Min(1L) Long id) { + User user = new User(id, "李四", 18); + return DataResult.ok(user); + } + +} +``` + +(3)如果请求参数不满足校验规则,则会抛出 `ConstraintViolationException` 或 `MethodArgumentNotValidException` 异常。 + +### 统一异常处理 + +在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。 + +```java +@Slf4j +@ControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理所有不可知的异常 + */ + @ResponseBody + @ResponseStatus(HttpStatus.OK) + @ExceptionHandler(Throwable.class) + public Result handleException(Throwable e) { + log.error("未知异常", e); + return new Result(ResultStatus.HTTP_SERVER_ERROR.getCode(), e.getMessage()); + } + + /** + * 统一处理请求参数校验异常(普通传参) + * + * @param e ConstraintViolationException + * @return {@link DataResult} + */ + @ResponseBody + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({ ConstraintViolationException.class }) + public Result handleConstraintViolationException(final ConstraintViolationException e) { + log.error("ConstraintViolationException", e); + List errors = new ArrayList<>(); + for (ConstraintViolation violation : e.getConstraintViolations()) { + Path path = violation.getPropertyPath(); + List pathArr = StrUtil.split(path.toString(), ','); + errors.add(pathArr.get(0) + " " + violation.getMessage()); + } + return new Result(ResultStatus.REQUEST_ERROR.getCode(), CollectionUtil.join(errors, ",")); + } + + /** + * 处理参数校验异常 + * + * @param e MethodArgumentNotValidException + * @return {@link DataResult} + */ + @ResponseBody + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({ MethodArgumentNotValidException.class }) + private Result handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) { + log.error("MethodArgumentNotValidException", e); + List errors = new ArrayList<>(); + for (ObjectError error : e.getBindingResult().getAllErrors()) { + errors.add(((FieldError) error).getField() + " " + error.getDefaultMessage()); + } + return new Result(ResultStatus.REQUEST_ERROR.getCode(), CollectionUtil.join(errors, ",")); + } + +} +``` + +## 进阶使用 + +### 分组校验 + +在实际项目中,可能多个方法需要使用同一个 DTO 类来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在 DTO 类的字段上加约束注解无法解决这个问题。因此,spring-validation 支持了分组校验的功能,专门用来解决这类问题。 + +(1)定义分组 + +```java +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface AddCheck { } + +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface EditCheck { } +``` + +(2)在实体上标记校验注解 + +```less +@Data +public class User2 { + + @NotNull(groups = EditCheck.class) + private Long id; + + @NotNull(groups = { AddCheck.class, EditCheck.class }) + @Size(min = 2, max = 10, groups = { AddCheck.class, EditCheck.class }) + private String name; + + @IsMobile(message = "不是有效手机号", groups = { AddCheck.class, EditCheck.class }) + private String mobile; + +} +``` + +(3)在方法上根据不同场景进行校验分组 + +```less +@Slf4j +@Validated +@RestController +@RequestMapping("validate2") +public class ValidatorController2 { + + /** + * {@link RequestBody} 参数校验 + */ + @PostMapping(value = "add") + public DataResult add(@Validated(AddCheck.class) @RequestBody User2 entity) { + log.info("添加一条记录:{}", JSONUtil.toJsonStr(entity)); + return DataResult.ok(true); + } + + /** + * {@link RequestBody} 参数校验 + */ + @PostMapping(value = "edit") + public DataResult edit(@Validated(EditCheck.class) @RequestBody User2 entity) { + log.info("编辑一条记录:{}", JSONUtil.toJsonStr(entity)); + return DataResult.ok(true); + } + +} +``` + +### 嵌套校验 + +前面的示例中,DTO 类里面的字段都是基本数据类型和 String 类型。但是实际场景中,有可能某个字段也是一个对象,这种情况先,可以使用嵌套校验。 +post +比如,上面保存 User 信息的时候同时还带有 Job 信息。需要注意的是,此时 DTO 类的对应字段必须标记@Valid 注解。 + +```less +@Data +public class UserDTO { + + @Min(value = 10000000000000000L, groups = Update.class) + private Long userId; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 2, max = 10, groups = {Save.class, Update.class}) + private String userName; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 6, max = 20, groups = {Save.class, Update.class}) + private String account; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 6, max = 20, groups = {Save.class, Update.class}) + private String password; + + @NotNull(groups = {Save.class, Update.class}) + @Valid + private Job job; + + @Data + public static class Job { + + @Min(value = 1, groups = Update.class) + private Long jobId; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 2, max = 10, groups = {Save.class, Update.class}) + private String jobName; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 2, max = 10, groups = {Save.class, Update.class}) + private String position; + } + + /** + * 保存的时候校验分组 + */ + public interface Save { + } + + /** + * 更新的时候校验分组 + */ + public interface Update { + } +} +复制代码 +``` + +嵌套校验可以结合分组校验一起使用。还有就是嵌套集合校验会对集合里面的每一项都进行校验,例如`List`字段会对这个 list 里面的每一个 Job 对象都进行校验 + +### 自定义校验注解 + +(1)自定义校验注解 `@IsMobile` + +```less +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Constraint(validatedBy = MobileValidator.class) +public @interface IsMobile { + + String message(); + + Class[] groups() default {}; + + Class[] payload() default {}; + +} +``` + +(2)实现 `ConstraintValidator` 接口,编写 `@IsMobile` 校验注解的解析器 + +```java +import cn.hutool.core.util.StrUtil; +import io.github.dunwu.spring.core.validation.annotation.IsMobile; +import io.github.dunwu.tool.util.ValidatorUtil; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class MobileValidator implements ConstraintValidator { + + @Override + public void initialize(IsMobile isMobile) { } + + @Override + public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { + if (StrUtil.isBlank(s)) { + return false; + } else { + return ValidatorUtil.isMobile(s); + } + } + +} +``` + +### 自定义校验 + +可以通过实现 `org.springframework.validation.Validator` 接口来自定义校验。 + +有以下要点 + +- 实现 `supports` 方法 +- 实现 `validate` 方法 + - 通过 `Errors` 对象收集错误 + - `ObjectError`:对象(Bean)错误: + - `FieldError`:对象(Bean)属性(Property)错误 + - 通过 `ObjectError` 和 `FieldError` 关联 `MessageSource` 实现获取最终的错误文案 + +```less +package io.github.dunwu.spring.core.validation; + +import io.github.dunwu.spring.core.validation.annotation.Valid; +import io.github.dunwu.spring.core.validation.config.CustomValidatorConfig; +import io.github.dunwu.spring.core.validation.entity.Person; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Component +public class CustomValidator implements Validator { + + private final CustomValidatorConfig validatorConfig; + + public CustomValidator(CustomValidatorConfig validatorConfig) { + this.validatorConfig = validatorConfig; + } + + /** + * 本校验器只针对 Person 对象进行校验 + */ + @Override + public boolean supports(Class clazz) { + return Person.class.equals(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + ValidationUtils.rejectIfEmpty(errors, "name", "name.empty"); + + List fields = getFields(target.getClass()); + for (Field field : fields) { + Annotation[] annotations = field.getAnnotations(); + for (Annotation annotation : annotations) { + if (annotation.annotationType().getAnnotation(Valid.class) != null) { + try { + ValidatorRule validatorRule = validatorConfig.findRule(annotation); + if (validatorRule != null) { + validatorRule.valid(annotation, target, field, errors); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + } + + private List getFields(Class clazz) { + // 声明Field数组 + List fields = new ArrayList<>(); + // 如果class类型不为空 + while (clazz != null) { + // 添加属性到属性数组 + Collections.addAll(fields, clazz.getDeclaredFields()); + clazz = clazz.getSuperclass(); + } + return fields; + } + +} +``` + +### 快速失败(Fail Fast) + +Spring Validation 默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启 Fali Fast 模式,一旦校验失败就立即返回。 + +```scss +@Bean +public Validator validator() { + ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + // 快速失败模式 + .failFast(true) + .buildValidatorFactory(); + return validatorFactory.getValidator(); +} +``` + +## Spring 校验原理 + +### Spring 校验使用场景 + +- Spring 常规校验(Validator) +- Spring 数据绑定(DataBinder) +- Spring Web 参数绑定(WebDataBinder) +- Spring WebMVC/WebFlux 处理方法参数校验 + +### Validator 接口设计 + +- 接口职责 + - Spring 内部校验器接口,通过编程的方式校验目标对象 +- 核心方法 + - `supports(Class)`:校验目标类能否校验 + - `validate(Object,Errors)`:校验目标对象,并将校验失败的内容输出至 Errors 对象 +- 配套组件 + - 错误收集器:`org.springframework.validation.Errors` + - Validator 工具类:`org.springframework.validation.ValidationUtils` + +### Errors 接口设计 + +- 接口职责 + - 数据绑定和校验错误收集接口,与 Java Bean 和其属性有强关联性 +- 核心方法 + - `reject` 方法(重载):收集错误文案 + - `rejectValue` 方法(重载):收集对象字段中的错误文案 +- 配套组件 + - Java Bean 错误描述:`org.springframework.validation.ObjectError` + - Java Bean 属性错误描述:`org.springframework.validation.FieldError` + +### Errors 文案来源 + +Errors 文案生成步骤 + +- 选择 Errors 实现(如:`org.springframework.validation.BeanPropertyBindingResult`) +- 调用 reject 或 rejectValue 方法 +- 获取 Errors 对象中 ObjectError 或 FieldError +- 将 ObjectError 或 FieldError 中的 code 和 args,关联 MessageSource 实现(如:`ResourceBundleMessageSource`) + +### spring web 校验原理 + +#### RequestBody 参数校验实现原理 + +在 spring-mvc 中,`RequestResponseBodyMethodProcessor` 是用于解析 `@RequestBody` 标注的参数以及处理`@ResponseBody` 标注方法的返回值的。其中,执行参数校验的逻辑肯定就在解析参数的方法 `resolveArgument()` 中: + +```java +@Override +public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + parameter = parameter.nestedIfOptional(); + Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); + String name = Conventions.getVariableNameForParameter(parameter); + + if (binderFactory != null) { + WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); + if (arg != null) { + // 尝试进行参数校验 + validateIfApplicable(binder, parameter); + if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { + // 如果存在校验错误,则抛出 MethodArgumentNotValidException + throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); + } + } + if (mavContainer != null) { + mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); + } + } + + return adaptArgumentIfNecessary(arg, parameter); +} +``` + +可以看到,resolveArgument()调用了 validateIfApplicable()进行参数校验。 + +```java +protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { + // 获取参数注解,如 @RequestBody、@Valid、@Validated + Annotation[] annotations = parameter.getParameterAnnotations(); + for (Annotation ann : annotations) { + // 先尝试获取 @Validated 注解 + Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); + // 如果标注了 @Validated,直接开始校验。 + // 如果没有,那么判断参数前是否有 Valid 开头的注解。 + if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { + Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); + Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + // 执行校验 + binder.validate(validationHints); + break; + } + } +} +``` + +以上代码,就解释了 Spring 为什么能同时支持 `@Validated`、`@Valid` 两个注解。 + +接下来,看一下 WebDataBinder.validate() 的实现: + +```typescript +@Override +public void validate(Object target, Errors errors, Object... validationHints) { + if (this.targetValidator != null) { + processConstraintViolations( + // 此处调用 Hibernate Validator 执行真正的校验 + this.targetValidator.validate(target, asValidationGroups(validationHints)), errors); + } +} +``` + +通过上面代码,可以看出 Spring 校验实际上是基于 Hibernate Validator 的封装。 + +#### 方法级别的参数校验实现原理 + +Spring 支持根据方法去进行拦截、校验,原理就在于应用了 AOP 技术。具体来说,是通过 `MethodValidationPostProcessor` 动态注册 AOP 切面,然后使用 `MethodValidationInterceptor` 对切点方法织入增强。 + +```java +public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean { + @Override + public void afterPropertiesSet() { + // 为所有 @Validated 标注的 Bean 创建切面 + Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); + // 创建 Advisor 进行增强 + this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator)); + } + + // 创建 Advice,本质就是一个方法拦截器 + protected Advice createMethodValidationAdvice(@Nullable Validator validator) { + return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor()); + } +} +``` + +接着看一下 `MethodValidationInterceptor`: + +```scss +public class MethodValidationInterceptor implements MethodInterceptor { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + // 无需增强的方法,直接跳过 + if (isFactoryBeanMetadataMethod(invocation.getMethod())) { + return invocation.proceed(); + } + // 获取分组信息 + Class[] groups = determineValidationGroups(invocation); + ExecutableValidator execVal = this.validator.forExecutables(); + Method methodToValidate = invocation.getMethod(); + Set> result; + try { + // 方法入参校验,最终还是委托给 Hibernate Validator 来校验 + result = execVal.validateParameters( + invocation.getThis(), methodToValidate, invocation.getArguments(), groups); + } + catch (IllegalArgumentException ex) { + ... + } + // 有异常直接抛出 + if (!result.isEmpty()) { + throw new ConstraintViolationException(result); + } + // 真正的方法调用 + Object returnValue = invocation.proceed(); + // 对返回值做校验,最终还是委托给Hibernate Validator来校验 + result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups); + // 有异常直接抛出 + if (!result.isEmpty()) { + throw new ConstraintViolationException(result); + } + return returnValue; + } +} +``` + +实际上,不管是 requestBody 参数校验还是方法级别的校验,最终都是调用 Hibernate Validator 执行校验,Spring Validation 只是做了一层封装。 + +## 问题 + +**Spring 有哪些校验核心组件**? + +- 检验器:`org.springframework.validation.Validator` +- 错误收集器:`org.springframework.validation.Errors` +- Java Bean 错误描述:`org.springframework.validation.ObjectError` +- Java Bean 属性错误描述:`org.springframework.validation.FieldError` +- Bean Validation 适配:`org.springframework.validation.beanvalidation.LocalValidatorFactoryBean` + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) +- https://juejin.cn/post/6856541106626363399 \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/22.Spring\346\225\260\346\215\256\347\273\221\345\256\232.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/22.Spring\346\225\260\346\215\256\347\273\221\345\256\232.md" new file mode 100644 index 00000000..3c5fe260 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/22.Spring\346\225\260\346\215\256\347\273\221\345\256\232.md" @@ -0,0 +1,181 @@ +--- +title: Spring 数据绑定 +date: 2022-12-22 19:26:57 +order: 22 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - 数据绑定 +permalink: /pages/267b4c/ +--- + +# Spring 数据绑定 + +**Spring 数据绑定(Data Binding)的作用是将用户的输入动态绑定到 JavaBean**。换句话说,Spring 数据绑定机制是将属性值设置到目标对象中。 + +在 Spring 中,数据绑定功能主要由 `DataBinder` 类实现。此外,`BeanWrapper` 也具有类似的功能,但 `DataBinder` 额外支持字段验证、字段格式化和绑定结果分析。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20230111150930.png) + +## 快速入门 + +定义一个用于测试的 JavaBean + +```java +public class TestBean { + + private int num; + + public int getNum() { + return num; + } + + public void setNum(int num) { + this.num = num; + } + + @Override + public String toString() { + return "TestBean{" + "num=" + num + '}'; + } + +} +``` + +数据绑定示例 + +```java +public class DataBindingDemo { + + public static void main(String[] args) { + + MutablePropertyValues mpv = new MutablePropertyValues(); + mpv.add("num", "10"); + + TestBean testBean = new TestBean(); + DataBinder db = new DataBinder(testBean); + + db.bind(mpv); + System.out.println(testBean); + } + +} +``` + +## Spring 数据绑定使用场景 + +- Spring `BeanDefinition` 到 Bean 实例创建 +- Spring 数据绑定(`DataBinder`) +- Spring Web 参数绑定(`WebDataBinder`) + +## DataBinder + +在 Spring 中,`DataBinder` 类是数据绑定功能的基类。`WebDataBinder` 是 `DataBinder` 的子类,主要用于 Spring Web 数据绑定,此外,还有一些 `WebDataBinder` 的扩展子类,其类族如下图所示: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20230111152225.png) + +DataBinder 核心属性: + +| 属性 | 说明 | +| ---------------------- | ------------------------------ | +| `target` | 关联目标 Bean | +| `objectName` | 目标 Bean 名称 | +| `bindingResult` | 属性绑定结果 | +| `typeConverter` | 类型转换器 | +| `conversionService` | 类型转换服务 | +| `messageCodesResolver` | 校验错误文案 Code 处理器 | +| `validators` | 关联的 Bean Validator 实例集合 | + +`DataBinder` 类的核心方法是 `bind(PropertyValues)`:将 PropertyValues Key-Value 内容映射到关联 Bean(target)中的属性上 + +- 假设 PropertyValues 中包含 name=dunwu 的键值对时, 同时 Bean 对象 User 中存在 name 属性, 当 bind 方法执行时, User 对象中的 name 属性值将被绑定为 dunwu + +## Spring 数据绑定元数据 + +DataBinder 元数据 - PropertyValues + +| 特征 | 说明 | +| ------------ | -------------------------------------------------------------------- | +| 数据来源 | BeanDefinition,主要来源 XML 资源配置 BeanDefinition | +| 数据结构 | 由一个或多个 PropertyValue 组成 | +| 成员结构 | PropertyValue 包含属性名称,以及属性值(包括原始值、类型转换后的值) | +| 常见实现 | MutablePropertyValues | +| Web 扩展实现 | ServletConfigPropertyValues、ServletRequestParameterPropertyValues | +| 相关生命周期 | InstantiationAwareBeanPostProcessor#postProcessProperties | + +## Spring 数据绑定控制参数 + +DataBinder 绑定特殊场景分析 + +- 当 PropertyValues 中包含名称 x 的 PropertyValue,目标对象 B 不存在 x 属性,当 bind 方法执 + 行时,会发生什么? +- 当 PropertyValues 中包含名称 x 的 PropertyValue,目标对象 B 中存在 x 属性,当 bind 方法执 + 行时,如何避免 B 属性 x 不被绑定? +- 当 PropertyValues 中包含名称 x.y 的 PropertyValue,目标对象 B 中存在 x 属性(嵌套 y 属性) + ,当 bind 方法执行时,会发生什么? + +### DataBinder 绑定控制参数 + +| 参数名称 | 说明 | +| ------------------- | ---------------------------------- | +| ignoreUnknownFields | 是否忽略未知字段,默认值:true | +| ignoreInvalidFields | 是否忽略非法字段,默认值:false | +| autoGrowNestedPaths | 是否自动增加嵌套路径,默认值:true | +| allowedFields | 绑定字段白名单 | +| disallowedFields | 绑定字段黑名单 | +| requiredFields | 必须绑定字段 | + +## BeanWrapper 的使用场景 + +- Spring 底层 JavaBeans 基础设施的中心化接口 +- 通常不会直接使用,间接用于 BeanFactory 和 DataBinder +- 提供标准 JavaBeans 分析和操作,能够单独或批量存储 Java Bean 的属性(properties) +- 支持嵌套属性路径(nested path) +- 实现类 org.springframework.beans.BeanWrapperImpl + +## Spring 底层 Java Beans 替换实现 + +JavaBeans 核心实现 - `java.beans.BeanInfo` + +- 属性(Property) + - `java.beans.PropertyEditor` +- 方法(Method) +- 事件(Event) +- 表达式(Expression) + +Spring 替代实现 - `org.springframework.beans.BeanWrapper` + +- 属性(Property) + - `java.beans.PropertyEditor` +- 嵌套属性路径(nested path) + +## DataBinder 数据校验 + +DataBinder 与 BeanWrapper + +- bind 方法生成 BeanPropertyBindingResult +- BeanPropertyBindingResult 关联 BeanWrapper + +## 问题 + +标准 JavaBeans 是如何操作属性的? + +| API | 说明 | +| ----------------------------- | ------------------------ | +| java.beans.Introspector | Java Beans 内省 API | +| java.beans.BeanInfo | Java Bean 元信息 API | +| java.beans.BeanDescriptor | Java Bean 信息描述符 | +| java.beans.PropertyDescriptor | Java Bean 属性描述符 | +| java.beans.MethodDescriptor | Java Bean 方法描述符 | +| java.beans.EventSetDescriptor | Java Bean 事件集合描述符 | + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/23.Spring\347\261\273\345\236\213\350\275\254\346\215\242.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/23.Spring\347\261\273\345\236\213\350\275\254\346\215\242.md" new file mode 100644 index 00000000..2da331a6 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/23.Spring\347\261\273\345\236\213\350\275\254\346\215\242.md" @@ -0,0 +1,214 @@ +--- +title: Spring 类型转换 +date: 2022-12-22 19:43:59 +order: 23 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/6662dc/ +--- + +# Spring 类型转换 + +## Spring 类型转换的实现 + +- 基于 JavaBeans 接口的类型转换实现 + - 基于 java.beans.PropertyEditor 接口扩展 +- Spring 3.0+ 通用类型转换实现 + +## 使用场景 + +| 场景 | 基于 JavaBeans 接口的类型转换实现 | Spring 3.0+ 通用类型转换实现 | +| ------------------ | --------------------------------- | ---------------------------- | +| 数据绑定 | YES | YES | +| BeanWrapper | YES | YES | +| Bean 属性类型转换 | YES | YES | +| 外部化属性类型转换 | NO | YES | + +## 基于 JavaBeans 接口的类型转换 + +核心职责 + +- 将 String 类型的内容转化为目标类型的对象 + +扩展原理 + +- Spring 框架将文本内容传递到 PropertyEditor 实现的 setAsText(String) 方法 +- PropertyEditor#setAsText(String) 方法实现将 String 类型转化为目标类型的对象 +- 将目标类型的对象传入 PropertyEditor#setValue(Object) 方法 +- PropertyEditor#setValue(Object) 方法实现需要临时存储传入对象 +- Spring 框架将通过 PropertyEditor#getValue() 获取类型转换后的对象 + +## Spring 內建 PropertyEditor 扩展 + +內建扩展(org.springframework.beans.propertyeditors 包下) + +| 转换场景 | 实现类 | +| ------------------- | ----------------------------------------------------------------- | +| String -> Byte 数组 | org.springframework.beans.propertyeditors.ByteArrayPropertyEditor | +| String -> Char | org.springframework.beans.propertyeditors.CharacterEditor | +| String -> Char 数组 | org.springframework.beans.propertyeditors.CharArrayPropertyEditor | +| String -> Charset | org.springframework.beans.propertyeditors.CharsetEditor | +| String -> Class | org.springframework.beans.propertyeditors.ClassEditor | +| String -> Currency | org.springframework.beans.propertyeditors.CurrencyEditor | +| | | + +## 自定义 PropertyEditor 扩展 + +扩展模式 + +- 扩展 `java.beans.PropertyEditorSupport` 类 + +实现 `org.springframework.beans.PropertyEditorRegistrar` + +- 实现 `registerCustomEditors(org.springframework.beans.PropertyEditorRegistry)` 方法 +- 将 `PropertyEditorRegistrar` 实现注册为 Spring Bean + +向 `org.springframework.beans.PropertyEditorRegistry` 注册自定义 PropertyEditor 实现 + +- 通用类型实现 `registerCustomEditor(Class, PropertyEditor)` +- Java Bean 属性类型实现:`registerCustomEditor(Class, String, PropertyEditor)` + +## Spring PropertyEditor 的设计缺陷 + +违反职责单一原则 + +- `java.beans.PropertyEditor` 接口职责太多,除了类型转换,还包括 Java Beans 事件和 Java GUI 交 + 互 + +`java.beans.PropertyEditor` 实现类型局限 + +- 来源类型只能为 `java.lang.String` 类型 + +`java.beans.PropertyEditor` 实现缺少类型安全 + +- 除了实现类命名可以表达语义,实现类无法感知目标转换类型 + +## Spring 3 通用类型转换接口 + +类型转换接口 - org.springframework.core.convert.converter.Converter + +- 泛型参数 S:来源类型,参数 T:目标类型 +- 核心方法:T convert(S) + +通用类型转换接口 - org.springframework.core.convert.converter.GenericConverter + +- 核心方法:convert(Object,TypeDescriptor,TypeDescriptor) +- 配对类型:org.springframework.core.convert.converter.GenericConverter.ConvertiblePair +- 类型描述:org.springframework.core.convert.TypeDescriptor + +## Spring 內建类型转换器 + +內建扩展 + +| 转换场景 | 实现类所在包名(package) | +| -------------------- | -------------------------------------------- | +| 日期/时间相关 | org.springframework.format.datetime | +| Java 8 日期/时间相关 | org.springframework.format.datetime.standard | +| 通用实现 | org.springframework.core.convert.support | + +## Converter 接口的局限性 + +局限一:缺少 Source Type 和 Target Type 前置判断 + +- 应对:增加 org.springframework.core.convert.converter.ConditionalConverter 实现 + +局限二:仅能转换单一的 Source Type 和 Target Type + +- 应对:使用 org.springframework.core.convert.converter.GenericConverter 代替 + +## GenericConverter 接口 + +`org.springframework.core.convert.converter.GenericConverter` + +| 核心要素 | 说明 | +| -------- | ----------------------------------------------------------------------------- | +| 使用场景 | 用于“复合”类型转换场景,比如 Collection、Map、数组等 | +| 转换范围 | `Set getConvertibleTypes()` | +| 配对类型 | `org.springframework.core.convert.converter.GenericConverter.ConvertiblePair` | +| 转换方法 | `convert(Object,TypeDescriptor,TypeDescriptor)` | +| 类型描述 | `org.springframework.core.convert.TypeDescriptor` | + +## 优化 GenericConverter 接口 + +GenericConverter 局限性 + +- 缺少 Source Type 和 Target Type 前置判断 +- 单一类型转换实现复杂 + +GenericConverter 优化接口 - `ConditionalGenericConverter` + +- 复合类型转换:`org.springframework.core.convert.converter.GenericConverter` +- 类型条件判断:`org.springframework.core.convert.converter.ConditionalConverter` + +## 扩展 Spring 类型转换器 + +实现转换器接口 + +- `org.springframework.core.convert.converter.Converter` +- `org.springframework.core.convert.converter.ConverterFactory` +- `org.springframework.core.convert.converter.GenericConverter` + +注册转换器实现 + +- 通过 `ConversionServiceFactoryBean` Spring Bean +- 通过 `org.springframework.core.convert.ConversionService API` + +## 统一类型转换服务 + +`org.springframework.core.convert.ConversionService` + +| 实现类型 | 说明 | +| ------------------------------------ | ----------------------------------------------------------------------------------------- | +| `GenericConversionService` | 通用 ConversionService 模板实现,不内置转化器实现 | +| `DefaultConversionService` | 基础 ConversionService 实现,内置常用转化器实现 | +| `FormattingConversionService` | 通用 Formatter + GenericConversionService 实现,不内置转化器和 Formatter 实现 | +| `DefaultFormattingConversionService` | DefaultConversionService + 格式化 实现(如:JSR-354 Money & Currency, JSR-310 Date-Time) | + +## ConversionService 作为依赖 + +类型转换器底层接口 - `org.springframework.beans.TypeConverter` + +- 起始版本:Spring 2.0 +- 核心方法 - convertIfNecessary 重载方法 +- 抽象实现 - `org.springframework.beans.TypeConverterSupport` +- 简单实现 - `org.springframework.beans.SimpleTypeConverter` + +类型转换器底层抽象实现 - `org.springframework.beans.TypeConverterSupport` + +- 实现接口 - `org.springframework.beans.TypeConverter` +- 扩展实现 - `org.springframework.beans.PropertyEditorRegistrySupport` +- 委派实现 - `org.springframework.beans.TypeConverterDelegate` + +类型转换器底层委派实现 - `org.springframework.beans.TypeConverterDelegate` + +- 构造来源 - `org.springframework.beans.AbstractNestablePropertyAccessor` 实现 + - `org.springframework.beans.BeanWrapperImpl` +- 依赖 - `java.beans.PropertyEditor` 实现 + - 默认內建实现 - `PropertyEditorRegistrySupport#registerDefaultEditors` +- 可选依赖 - `org.springframework.core.convert.ConversionService` 实现 + +## 问题 + +**Spring 类型转换实现有哪些**? + +- 基于 JavaBeans PropertyEditor 接口实现 +- Spring 3.0+ 通用类型转换实现 + +**Spring 类型转换器接口有哪些**? + +- 类型转换接口 - `org.springframework.core.convert.converter.Converter` +- 通用类型转换接口 - `org.springframework.core.convert.converter.GenericConverter` +- 类型条件接口 - `org.springframework.core.convert.converter.ConditionalConverter` +- 综合类型转换接口 - `org.springframework.core.convert.converter.ConditionalGenericConverter` + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/24.SpringEL.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/24.SpringEL.md" new file mode 100644 index 00000000..1e939038 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/24.SpringEL.md" @@ -0,0 +1,22 @@ +--- +title: Spring EL 表达式 +date: 2023-01-12 20:26:46 +order: 24 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/1f743f/ +--- + +# Spring EL 表达式 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/25.Spring\344\272\213\344\273\266.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/25.Spring\344\272\213\344\273\266.md" new file mode 100644 index 00000000..8dc880d0 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/25.Spring\344\272\213\344\273\266.md" @@ -0,0 +1,232 @@ +--- +title: Spring 事件 +date: 2022-12-22 20:31:02 +order: 25 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/cca414/ +--- + +# Spring 事件 + +## Java 事件/监听器编程模型 + +设计模式 - 观察者模式扩展 + +- 可观者对象(消息发送者) - java.util.Observable +- 观察者 - java.util.Observer + +标准化接口 + +- 事件对象 - java.util.EventObject +- 事件监听器 - java.util.EventListener + +## 面向接口的事件/监听器设计模式 + +事件/监听器场景举例 + +| Java 技术规范 | 事件接口 | 监听器接口 | +| --------------- | ------------------------------------- | ---------------------------------------- | +| JavaBeans | java.beans.PropertyChangeEvent | java.beans.PropertyChangeListener | +| Java AWT | java.awt.event.MouseEvent | java.awt.event.MouseListener | +| Java Swing | javax.swing.event.MenuEvent | javax.swing.event.MenuListener | +| Java Preference | java.util.prefs.PreferenceChangeEvent | java.util.prefs.PreferenceChangeListener | + +## 面向注解的事件/监听器设计模式 + +事件/监听器注解场景举例 + +| Java 技术规范 | 事件注解 | 监听器注解 | +| ------------- | ------------------------------ | ------------------------------------- | +| Servlet 3.0+ | | @javax.servlet.annotation.WebListener | +| JPA 1.0+ | @javax.persistence.PostPersist | | +| Java Common | @PostConstruct | | +| EJB 3.0+ | @javax.ejb.PrePassivate | | +| JSF 2.0+ | @javax.faces.event.ListenerFor | | + +## Spring 标准事件 - ApplicationEvent + +Java 标准事件 `java.util.EventObject` 扩展 + +- 扩展特性:事件发生事件戳 +- Spring 应用上下文 ApplicationEvent 扩展 - `ApplicationContextEvent` +- Spring 应用上下文(ApplicationContext)作为事件源 + +具体实现: + +- `org.springframework.context.event.ContextClosedEvent` +- `org.springframework.context.event.ContextRefreshedEvent` +- `org.springframework.context.event.ContextStartedEvent` +- `org.springframework.context.event.ContextStoppedEvent` + +## 基于接口的 Spring 事件监听器 + +Java 标准事件监听器 `java.util.EventListener` 扩展 + +- 扩展接口 - `org.springframework.context.ApplicationListener` +- 设计特点:单一类型事件处理 +- 处理方法:`onApplicationEvent(ApplicationEvent)` +- 事件类型:`org.springframework.context.ApplicationEvent` + +## 基于注解的 Spring 事件监听器 + +Spring 注解 - `@org.springframework.context.event.EventListener` + +| 特性 | 说明 | +| -------------------- | -------------------------------------------- | +| 设计特点 | 支持多 `ApplicationEvent` 类型,无需接口约束 | +| 注解目标 | 方法 | +| 是否支持异步执行 | 支持 | +| 是否支持泛型类型事件 | 支持 | +| 是指支持顺序控制 | 支持,配合 `@Order` 注解控制 | + +## 注册 Spring ApplicationListener + +- 方法一:ApplicationListener 作为 Spring Bean 注册 +- 方法二:通过 ConfigurableApplicationContext API 注册 + +## Spring 事件发布器 + +- 方法一:通过 ApplicationEventPublisher 发布 Spring 事件 + - 获取 ApplicationEventPublisher + - 依赖注入 +- 方法二:通过 ApplicationEventMulticaster 发布 Spring 事件 + - 获取 ApplicationEventMulticaster + - 依赖注入 + - 依赖查找 + +## Spring 层次性上下文事件传播 + +- 发生说明 +- 当 Spring 应用出现多层次 Spring 应用上下文(ApplicationContext)时,如 Spring WebMVC、Spring Boot 或 Spring Cloud 场景下,由子 ApplicationContext 发起 Spring 事件可能会传递到其 Parent ApplicationContext(直到 Root)的过程 +- 如何避免 +- 定位 Spring 事件源(ApplicationContext)进行过滤处理 + +## Spring 内建事件 + +ApplicationContextEvent 派生事件 + +- ContextRefreshedEvent :Spring 应用上下文就绪事件 +- ContextStartedEvent :Spring 应用上下文启动事件 +- ContextStoppedEvent :Spring 应用上下文停止事件 +- ContextClosedEvent :Spring 应用上下文关闭事件 + +## Spring 4.2 Payload 事件 + +Spring Payload 事件 - org.springframework.context.PayloadApplicationEvent + +- 使用场景:简化 Spring 事件发送,关注事件源主体 +- 发送方法:ApplicationEventPublisher#publishEvent(java.lang.Object) + +## 自定义 Spring 事件 + +- 扩展 org.springframework.context.ApplicationEvent +- 实现 org.springframework.context.ApplicationListener +- 注册 org.springframework.context.ApplicationListener + +## 依赖注入 ApplicationEventPublisher + +- 通过 ApplicationEventPublisherAware 回调接口 +- 通过 @Autowired ApplicationEventPublisher + +## 依赖查找 ApplicationEventMulticaster + +查找条件 + +- Bean 名称:"applicationEventMulticaster" +- Bean 类型:org.springframework.context.event.ApplicationEventMulticaster + +## ApplicationEventPublisher 底层实现 + +- 接口:org.springframework.context.event.ApplicationEventMulticaster +- 抽象类:org.springframework.context.event.AbstractApplicationEventMulticaster +- 实现类:org.springframework.context.event.SimpleApplicationEventMulticaster + +## 同步和异步 Spring 事件广播 + +基于实现类 - `org.springframework.context.event.SimpleApplicationEventMulticaster` + +- 模式切换:`setTaskExecutor(java.util.concurrent.Executor)` 方法 + - 默认模式:同步 + - 异步模式:如 `java.util.concurrent.ThreadPoolExecutor` +- 设计缺陷:非基于接口契约编程 + +基于注解 - `@org.springframework.context.event.EventListener` + +- 模式切换 + - 默认模式:同步 + - 异步模式:标注 `@org.springframework.scheduling.annotation.Async` +- 实现限制:无法直接实现同步/异步动态切换 + +## Spring 4.1 事件异常处理 + +Spring 3.0 错误处理接口 - org.springframework.util.ErrorHandler + +使用场景 + +- Spring 事件(Events) + - SimpleApplicationEventMulticaster Spring 4.1 开始支持 +- Spring 本地调度(Scheduling) + - org.springframework.scheduling.concurrent.ConcurrentTaskScheduler + - org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler + +## Spring 事件/监听器实现原理 + +核心类 - `org.springframework.context.event.SimpleApplicationEventMulticaster` + +- 设计模式:观察者模式扩展 + - 被观察者 - org.springframework.context.ApplicationListener + - API 添加 + - 依赖查找 + - 通知对象 - org.springframework.context.ApplicationEvent +- 执行模式:同步/异步 +- 异常处理:org.springframework.util.ErrorHandler +- 泛型处理:org.springframework.core.ResolvableType + +## 问题 + +**Spring Boot 事件** + +| 事件类型 | 发生时机 | +| ----------------------------------- | --------------------------------------- | +| ApplicationStartingEvent | 当 Spring Boot 应用已启动时 | +| ApplicationStartedEvent | 当 Spring Boot 应用已启动时 | +| ApplicationEnvironmentPreparedEvent | 当 Spring Boot Environment 实例已准备时 | +| ApplicationPreparedEvent | 当 Spring Boot 应用预备时 | +| ApplicationReadyEvent | 当 Spring Boot 应用完全可用时 | +| ApplicationFailedEvent | 当 Spring Boot 应用启动失败时 | + +**Spring Cloud 事件** + +| 事件类型 | 发生时机 | +| -------------------------- | ------------------------------------- | +| EnvironmentChangeEvent | 当 Environment 示例配置属性发生变化时 | +| HeartbeatEvent | 当 DiscoveryClient 客户端发送心跳时 | +| InstancePreRegisteredEvent | 当服务实例注册前 | +| InstanceRegisteredEvent | 当服务实例注册后 | +| RefreshEvent | 当 RefreshEndpoint 被调用时 | +| RefreshScopeRefreshedEvent | 当 Refresh Scope Bean 刷新后 | + +**Spring 事件核心接口/组件**? + +- Spring 事件 - org.springframework.context.ApplicationEvent +- Spring 事件监听器 - org.springframework.context.ApplicationListener +- Spring 事件发布器 - org.springframework.context.ApplicationEventPublisher +- Spring 事件广播器 - org.springframework.context.event.ApplicationEventMulticaster + +**Spring 同步和异步事件处理的使用场景**? + +- Spring 同步事件 - 绝大多数 Spring 使用场景,如 ContextRefreshedEvent +- Spring 异步事件 - 主要 @EventListener 与 @Async 配合,实现异步处理,不阻塞主线程,比如长时间的数据计算任务等。不要轻易调整 SimpleApplicationEventMulticaster 中关联的 taskExecutor 对象,除非使用者非常了解 Spring 事件机制,否则容易出现异常行为。 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/26.Spring\345\233\275\351\231\205\345\214\226.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/26.Spring\345\233\275\351\231\205\345\214\226.md" new file mode 100644 index 00000000..e07843fd --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/26.Spring\345\233\275\351\231\205\345\214\226.md" @@ -0,0 +1,121 @@ +--- +title: Spring 国际化 +date: 2022-12-22 11:44:54 +order: 26 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/b5b8ad/ +--- + +# Spring 国际化 + +## Spring 国际化使用场景 + +- 普通国际化文案 +- Bean Validation 校验国际化文案 +- Web 站点页面渲染 +- Web MVC 错误消息提示 + +## Spring 国际化接口 + +- 核心接口:`org.springframework.context.MessageSource` +- 主要概念 + - 文案模板编码(code) + - 文案模板参数(args) + - 区域(Locale) + +## 层次性 MessageSource + +- Spring 层次性接口回顾 + - `org.springframework.beans.factory.HierarchicalBeanFactory` + - `org.springframework.context.ApplicationContext` + - `org.springframework.beans.factory.config.BeanDefinition` +- Spring 层次性国际化接口 + - `org.springframework.context.HierarchicalMessageSource` + +## Java 国际化标准实现 + +核心接口: + +- 抽象实现 - `java.util.ResourceBundle` +- Properties 资源实现 - `java.util.PropertyResourceBundle` +- 例举实现 - `java.util.ListResourceBundle` + +`ResourceBundle` 核心特性 + +- Key-Value 设计 +- 层次性设计 +- 缓存设计 +- 字符编码控制 - `java.util.ResourceBundle.Control`(@since 1.6) +- Control SPI 扩展 - `java.util.spi.ResourceBundleControlProvider`(@since 1.8) + +## Java 文本格式化 + +- 核心接口 + - java.text.MessageFormat +- 基本用法 + - 设置消息格式模式- new MessageFormat(...) + - 格式化 - format(new Object[]{...}) +- 消息格式模式 + - 格式元素:{ArgumentIndex (,FormatType,(FormatStyle))} + - FormatType:消息格式类型,可选项,每种类型在 number、date、time 和 choice 类型选其一 + - FormatStyle:消息格式风格,可选项,包括:short、medium、long、full、integer、currency、 + percent +- 高级特性 + - 重置消息格式模式 + - 重置 java.util.Locale + - 重置 java.text.Format + +## MessageSource 开箱即用实现 + +- 基于 ResourceBundle + MessageFormat 组合 MessageSource 实现 +- org.springframework.context.support.ResourceBundleMessageSource +- 可重载 Properties + MessageFormat 组合 MessageSource 实现 +- org.springframework.context.support.ReloadableResourceBundleMessageSource + +## MessageSource 內建依赖 + +- MessageSource 內建 Bean 可能来源 +- 预注册 Bean 名称为:“messageSource”,类型为:MessageSource Bean +- 默认內建实现 - DelegatingMessageSource +- 层次性查找 MessageSource 对象 + +## 问题 + +**Spring Boot 为什么要新建 MessageSource Bean**? + +- AbstractApplicationContext 的实现决定了 MessageSource 內建实现 +- Spring Boot 通过外部化配置简化 MessageSource Bean 构建 +- Spring Boot 基于 Bean Validation 校验非常普遍 + +**Spring 国际化接口有哪些**? + +- 核心接口 - MessageSource +- 层次性接口 - `org.springframework.context.HierarchicalMessageSource` + +**Spring 有哪些 MessageSource 內建实现**? + +- `org.springframework.context.support.ResourceBundleMessageSource` +- `org.springframework.context.support.ReloadableResourceBundleMessageSource` +- `org.springframework.context.support.StaticMessageSource` +- `org.springframework.context.support.DelegatingMessageSource` + +**如何实现配置自动更新 MessageSource**? + +主要技术 + +- Java NIO 2:`java.nio.file.WatchService` +- Java Concurrency : `java.util.concurrent.ExecutorService` +- Spring:`org.springframework.context.support.AbstractMessageSource` + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/27.Spring\346\263\233\345\236\213\345\244\204\347\220\206.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/27.Spring\346\263\233\345\236\213\345\244\204\347\220\206.md" new file mode 100644 index 00000000..ffc2db65 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/27.Spring\346\263\233\345\236\213\345\244\204\347\220\206.md" @@ -0,0 +1,141 @@ +--- +title: Spring 泛型处理 +date: 2022-12-22 20:11:52 +order: 27 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/175cbd/ +--- + +# Spring 泛型处理 + +## Java 泛型基础 + +泛型类型 + +- 泛型类型是在类型上参数化的泛型类或接口 + +泛型使用场景 + +- 编译时强类型检查 +- 避免类型强转 +- 实现通用算法 + +泛型类型擦写 + +- 泛型被引入到 Java 语言中,以便在编译时提供更严格的类型检查并支持泛型编程。类型擦除确保不会 + 为参数化类型创建新类;因此,泛型不会产生运行时开销。为了实现泛型,编译器将类型擦除应用于: + - 将泛型类型中的所有类型参数替换为其边界,如果类型参数是无边界的,则将其替换为 + “Object”。因此,生成的字节码只包含普通类、接口和方法 + - 必要时插入类型转换以保持类型安全 + - 生成桥方法以保留扩展泛型类型中的多态性 + +## Java 5 类型接口 + +Java 5 类型接口 - `java.lang.reflect.Type` + +| 派生类或接口 | 说明 | +| ------------------------------------- | --------------------------------------- | +| `java.lang.Class` | Java 类 API,如 `java.lang.String` | +| `java.lang.reflect.GenericArrayType` | 泛型数组类型 | +| `java.lang.reflect.ParameterizedType` | 泛型参数类型 | +| `java.lang.reflect.TypeVariable` | 泛型类型变量,如 `Collection` 中的 E | +| `java.lang.reflect.WildcardType` | 泛型通配类型 | + +Java 泛型反射 API + +| 类型 | API | +| -------------------------------- | ---------------------------------------- | +| 泛型信息(Generics Info) | `java.lang.Class#getGenericInfo()` | +| 泛型参数(Parameters) | `java.lang.reflect.ParameterizedType` | +| 泛型父类(Super Classes) | `java.lang.Class#getGenericSuperclass()` | +| 泛型接口(Interfaces) | `java.lang.Class#getGenericInterfaces()` | +| 泛型声明(Generics Declaration) | `java.lang.reflect.GenericDeclaration` | + +## Spring 泛型类型辅助类 + +核心 API - `org.springframework.core.GenericTypeResolver` + +- 版本支持:[2.5.2 , ) +- 处理类型相关(Type)相关方法 + - `resolveReturnType` + - `resolveType` +- 处理泛型参数类型(`ParameterizedType`)相关方法 + - `resolveReturnTypeArgument` + - `resolveTypeArgument` + - `resolveTypeArguments` +- 处理泛型类型变量(`TypeVariable`)相关方法 + - `getTypeVariableMap` + +## Spring 泛型集合类型辅助类 + +核心 API - `org.springframework.core.GenericCollectionTypeResolver` + +- 版本支持:[2.0 , 4.3] +- 替换实现:`org.springframework.core.ResolvableType` +- 处理 Collection 相关 + - `getCollection*Type` +- 处理 Map 相关 + - `getMapKey*Type` + - `getMapValue*Type` + +## Spring 方法参数封装 - MethodParameter + +核心 API - `org.springframework.core.MethodParameter` + +- 起始版本:[2.0 , ) +- 元信息 + - 关联的方法 - Method + - 关联的构造器 - Constructor + - 构造器或方法参数索引 - parameterIndex + - 构造器或方法参数类型 - parameterType + - 构造器或方法参数泛型类型 - genericParameterType + - 构造器或方法参数参数名称 - parameterName + - 所在的类 - containingClass + +## Spring 4.0 泛型优化实现 - ResolvableType + +核心 API - `org.springframework.core.ResolvableType` + +- 起始版本:[4.0 , ) +- 扮演角色:`GenericTypeResolver` 和 `GenericCollectionTypeResolver` 替代者 +- 工厂方法:`for*` 方法 +- 转换方法:`as*` 方法 +- 处理方法:`resolve*` 方法 + +## ResolvableType 的局限性 + +- 局限一:ResolvableType 无法处理泛型擦写 +- 局限二:ResolvableType 无法处理非具体化的 ParameterizedType + +## 问题 + +**Java 泛型擦写发生在编译时还是运行时**? + +运行时 + +**请介绍 Java 5 Type 类型的派生类或接口** + +- `java.lang.Class` +- `java.lang.reflect.GenericArrayType` +- `java.lang.reflect.ParameterizedType` +- `java.lang.reflect.TypeVariable` +- `java.lang.reflect.WildcardType` + +**请说明 ResolvableType 的设计优势**? + +- 简化 Java 5 Type API 开发,屏蔽复杂 API 的运用,如 ParameterizedType +- 不变性设计(Immutability) +- Fluent API 设计(Builder 模式),链式(流式)编程 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/28.Spring\346\263\250\350\247\243.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/28.Spring\346\263\250\350\247\243.md" new file mode 100644 index 00000000..e8ce595c --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/28.Spring\346\263\250\350\247\243.md" @@ -0,0 +1,147 @@ +--- +title: Spring 注解 +date: 2022-12-23 09:08:15 +order: 28 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/b6556f/ +--- + +# Spring 注解 + +## Spring 注解驱动编程发展历程 + +- 注解驱动启蒙时代:Spring Framework 1.x +- 注解驱动过渡时代:Spring Framework 2.x +- 注解驱动黄金时代:Spring Framework 3.x +- 注解驱动完善时代:Spring Framework 4.x +- 注解驱动当下时代:Spring Framework 5.x + +## Spring 核心注解场景分类 + +Spring 模式注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| -------------- | ------------------ | -------- | +| @Repository | 数据仓储模式注解 | 2.0 | +| @Component | 通用组件模式注解 | 2.5 | +| @Service | 服务模式注解 | 2.5 | +| @Controller | Web 控制器模式注解 | 2.5 | +| @Configuration | 配置类模式注解 | 3.0 | + +装配注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| --------------- | ------------------------------------------- | -------- | +| @ImportResource | 替换 XML 元素 `` | 2.5 | +| @Import | 导入 Configuration 类 | 2.5 | +| @ComponentScan | 扫描指定 package 下标注 Spring 模式注解的类 | 3.1 | + +依赖注入注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ----------- | ----------------------------------- | -------- | +| @Autowired | Bean 依赖注入,支持多种依赖查找方式 | 2.5 | +| @Qualifier | 细粒度的 @Autowired 依赖查找 | 2.5 | + +## Spring 注解编程模型 + +- 元注解(Meta-Annotations) +- Spring 模式注解(Stereotype Annotations) +- Spring 组合注解(Composed Annotations) +- Spring 注解属性别名和覆盖(Attribute Aliases and Overrides) + +## Spring 元注解(Meta-Annotations) + +- java.lang.annotation.Documented +- java.lang.annotation.Inherited +- java.lang.annotation.Repeatable + +## Spring 模式注解(Stereotype Annotations) + +理解 @Component “派⽣性”:元标注 @Component 的注解在 XML 元素 或注解 @ComponentScan 扫描中“派生”了 @Component 的特性,并且从 Spring Framework 4.0 开始支持多层次“派⽣性”。 + +举例说明: + +- @Repository +- @Service +- @Controller +- @Configuration +- @SpringBootConfiguration(Spring Boot) + +@Component “派⽣性”原理 + +- 核心组件 - org.springframework.context.annotation.ClassPathBeanDefinitionScanner +- org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider +- 资源处理 - org.springframework.core.io.support.ResourcePatternResolver +- 资源-类元信息 +- org.springframework.core.type.classreading.MetadataReaderFactory +- 类元信息 - org.springframework.core.type.ClassMetadata +- ASM 实现 - org.springframework.core.type.classreading.ClassMetadataReadingVisitor +- 反射实现 - org.springframework.core.type.StandardAnnotationMetadata +- 注解元信息 - org.springframework.core.type.AnnotationMetadata +- ASM 实现 - org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor +- 反射实现 - org.springframework.core.type.StandardAnnotationMetadata + +## Spring 组合注解(Composed Annotations) + +Spring 组合注解(Composed Annotations)中的元注允许是 Spring 模式注解(Stereotype Annotation)与其他 Spring 功能性注解的任意组合。 + +## Spring 注解属性别名(Attribute Aliases) + +## Spring 注解属性覆盖(Attribute Overrides) + +## Spring @Enable 模块驱动 + +@Enable 模块驱动 + +@Enable 模块驱动是以 @Enable 为前缀的注解驱动编程模型。所谓“模块”是指具备相同领域的功能组件集合,组合所形成⼀个独⽴的单元。⽐如 Web MVC 模块、AspectJ 代理模块、Caching(缓存)模块、JMX(Java 管理扩展)模块、Async(异步处理)模块等。 + +举例说明 + +- @EnableWebMvc +- @EnableTransactionManagement +- @EnableCaching +- @EnableMBeanExport +- @EnableAsync + +@Enable 模块驱动编程模式 + +- 驱动注解:@EnableXXX +- 导入注解:@Import 具体实现 +- 具体实现 +- 基于 Configuration Class +- 基于 ImportSelector 接口实现 +- 基于 ImportBeanDefinitionRegistrar 接口实现 + +## Spring 条件注解 + +基于配置条件注解 - @org.springframework.context.annotation.Profile + +- 关联对象 - org.springframework.core.env.Environment 中的 Profiles +- 实现变化:从 Spring 4.0 开始,@Profile 基于 @Conditional 实现 + +基于编程条件注解 - @org.springframework.context.annotation.Conditional + +- 关联对象 - org.springframework.context.annotation.Condition 具体实现 + +@Conditional 实现原理 + +- 上下文对象 - org.springframework.context.annotation.ConditionContext +- 条件判断 - org.springframework.context.annotation.ConditionEvaluator +- 配置阶段 - org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase +- 判断入口 + - org.springframework.context.annotation.ConfigurationClassPostProcessor + - org.springframework.context.annotation.ConfigurationClassParser + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/29.SpringEnvironment\346\212\275\350\261\241.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/29.SpringEnvironment\346\212\275\350\261\241.md" new file mode 100644 index 00000000..e95e298a --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/29.SpringEnvironment\346\212\275\350\261\241.md" @@ -0,0 +1,167 @@ +--- +title: Spring Environment 抽象 +date: 2022-12-23 09:27:44 +order: 29 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/03d838/ +--- + +# Spring Environment 抽象 + +## 理解 Spring Environment 抽象 + +统一的 Spring 配置属性管理 + +Spring Framework 3.1 开始引入 Environment 抽象,它统一 Spring 配置属性的存储,包括占位符处理和类型转换,不仅完整地替换 PropertyPlaceholderConfigurer,而且还支持更丰富的配置属性源(PropertySource) + +条件化 Spring Bean 装配管理 + +通过 Environment Profiles 信息,帮助 Spring 容器提供条件化地装配 Bean + +## Spring Environment 接口使用场景 + +- ⽤于属性占位符处理 +- 用于转换 Spring 配置属性类型 +- 用于存储 Spring 配置属性源(PropertySource) +- 用于 Profiles 状态的维护 + +## Environment 占位符处理 + +Spring 3.1 前占位符处理 + +- 组件:org.springframework.beans.factory.config.PropertyPlaceholderConfigurer +- 接口:org.springframework.util.StringValueResolver + +Spring 3.1 + 占位符处理 + +- 组件:org.springframework.context.support.PropertySourcesPlaceholderConfigurer +- 接口:org.springframework.beans.factory.config.EmbeddedValueResolver + +## 理解条件配置 Spring Profiles + +Spring 3.1 条件配置 + +- API:org.springframework.core.env.ConfigurableEnvironment +- 修改:addActiveProfile(String)、setActiveProfiles(String...) 和 setDefaultProfiles(String...) +- 获取:getActiveProfiles() 和 getDefaultProfiles() +- 匹配:#acceptsProfiles(String...) 和 acceptsProfiles(Profiles) +- 注解:@org.springframework.context.annotation.Profile + +## Spring 4 重构 @Profile + +基于 Spring 4 org.springframework.context.annotation.Condition 接口实现 + +org.springframework.context.annotation.ProfileCondition + +## 依赖注入 Environment + +直接依赖注入 + +- 通过 EnvironmentAware 接口回调 +- 通过 @Autowired 注入 Environment + +间接依赖注入 + +- 通过 ApplicationContextAware 接口回调 +- 通过 @Autowired 注入 ApplicationContext + +## 依赖查找 Environment + +直接依赖查找 + +- 通过 org.springframework.context.ConfigurableApplicationContext#ENVIRONMENT_BEAN_NAME + +间接依赖查找 + +- 通过 org.springframework.context.ConfigurableApplicationContext#getEnvironment + +## 依赖注入 @Value + +通过注入 @Value + +实现 - org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + +## Spring 类型转换在 Environment 中的运用 + +Environment 底层实现 + +- 底层实现 - org.springframework.core.env.PropertySourcesPropertyResolver +- 核心方法 - convertValueIfNecessary(Object,Class) +- 底层服务 - org.springframework.core.convert.ConversionService +- 默认实现 - org.springframework.core.convert.support.DefaultConversionService + +## Spring 类型转换在 @Value 中的运用 + +@Value 底层实现 + +- 底层实现 - org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + - org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency +- 底层服务 - org.springframework.beans.TypeConverter + - 默认实现 - org.springframework.beans.TypeConverterDelegate + - java.beans.PropertyEditor + - org.springframework.core.convert.ConversionService + +## Spring 配置属性源 PropertySource + +- API + - 单配置属性源 - org.springframework.core.env.PropertySource + - 多配置属性源 - org.springframework.core.env.PropertySources +- 注解 + - 单配置属性源 - @org.springframework.context.annotation.PropertySource + - 多配置属性源 - @org.springframework.context.annotation.PropertySources +- 关联 + - 存储对象 - org.springframework.core.env.MutablePropertySources + - 关联方法 - org.springframework.core.env.ConfigurableEnvironment#getPropertySources() + +## Spring 內建的配置属性源 + +內建 PropertySource + +| PropertySource 类型 | 说明 | +| -------------------------------------------------------------------- | ------------------------- | +| org.springframework.core.env.CommandLinePropertySource | 命令行配置属性源 | +| org.springframework.jndi.JndiPropertySource | JDNI 配置属性源 | +| org.springframework.core.env.PropertiesPropertySource | Properties 配置属性源 | +| org.springframework.web.context.support.ServletConfigPropertySource | Servlet 配置属性源 | +| org.springframework.web.context.support.ServletContextPropertySource | ServletContext 配置属性源 | +| org.springframework.core.env.SystemEnvironmentPropertySource | 环境变量配置属性源 | + +## 基于注解扩展 Spring 配置属性源 + +@org.springframework.context.annotation.PropertySource 实现原理 + +- 入口 - org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass + - org.springframework.context.annotation.ConfigurationClassParser#processPropertySource +- 4.3 新增语义 + - 配置属性字符编码 - encoding + - org.springframework.core.io.support.PropertySourceFactory +- 适配对象 - org.springframework.core.env.CompositePropertySource + +## 基于 API 扩展 Spring 配置属性源 + +- Spring 应用上下文启动前装配 PropertySource +- Spring 应用上下文启动后装配 PropertySource + +## 问题 + +简单介绍 Spring Environment 接口? + +- 核心接口 - org.springframework.core.env.Environment +- 父接口 - org.springframework.core.env.PropertyResolver +- 可配置接口 - org.springframework.core.env.ConfigurableEnvironment +- 职责: + - 管理 Spring 配置属性源 + - 管理 Profiles + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/31.SpringBoot\344\271\213\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/31.SpringBoot\344\271\213\345\277\253\351\200\237\345\205\245\351\227\250.md" new file mode 100644 index 00000000..da73823d --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/31.SpringBoot\344\271\213\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -0,0 +1,387 @@ +--- +title: SpringBoot 之快速入门 +date: 2021-12-10 18:22:26 +order: 31 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/950e4d/ +--- + +# SpringBoot 之快速入门 + +## Spring Boot 简介 + +Spring Boot 可以让使用者非常方便的创建 Spring 应用。 + +Spring Boot 的目标是: + +- 为所有 Spring 开发者提供更快且可广泛访问的入门体验。 +- 开箱即用 +- 提供一系列通用的非功能特性(例如嵌入式服务、安全、指标、健康检查和外部化配置) +- 完全不需要代码生成,也不需要 XML 配置。 + +## Spring Boot 系统要求 + +Spring Boot 的构建工具要求: + +| Build Tool | Version | +| :--------- | :-------------------- | +| Maven | 3.5+ | +| Gradle | 6.8.x, 6.9.x, and 7.x | + +Spring Boot 支持的 Servlet 容器: + +| Name | Servlet Version | +| :----------- | :-------------- | +| Tomcat 9.0 | 4.0 | +| Jetty 9.4 | 3.1 | +| Jetty 10.0 | 4.0 | +| Undertow 2.0 | 4.0 | + +## 部署第一个 Spring Boot 项目 + +> 本节介绍如何开发一个小的“Hello World!” web 应用示例,来展示 Spring Boot 的一些关键功能。我们使用 Maven 来构建这个项目,因为大多数 IDE 都支持它。 + +### 环境检查 + +Spring Boot 项目依赖于 Java 环境和 Mave,开始项目之前需要先检查一下环境。 + +本地是否已安装 Java: + +```shell +$ java -version +java version "1.8.0_102" +Java(TM) SE Runtime Environment (build 1.8.0_102-b14) +Java HotSpot(TM) 64-Bit Server VM (build 25.102-b14, mixed mode) +``` + +本地是否已安装 Maven: + +```java +$ mvn -v +Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-17T14:33:14-04:00) +Maven home: /usr/local/Cellar/maven/3.3.9/libexec +Java version: 1.8.0_102, vendor: Oracle Corporation +``` + +### 创建 pom + +我们需要从创建 Maven pom.xml 文件开始。 pom.xml 是 Maven 用于构建项目的配置文件。 + +```xml + + + 4.0.0 + + com.example + myproject + 0.0.1-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.6.1 + + + + + +``` + +使用者可以通过运行 mvn package 来测试它 + +### 添加依赖 + +Spring Boot 提供了许多启动器(Starters)以应对不同的使用场景。使用者可将 jars 添加到类路径中。我们的示例程序在 POM 的 parent 使用 spring-boot-starter-parent。 spring-boot-starter-parent 是一个特殊的启动器,提供有用的 Maven 默认值。它还提供了一个依赖项的版本管理,可以让使用者使用时不必显示指定版本。 + +其他启动器(Starters)提供了各种针对不同使用场景的功能。比如,我们需要开发一个 Web 应用程序,就可以添加了一个 spring-boot-starter-web 依赖项。在此之前,我们可以通过运行以下命令来查看我们当前拥有的 maven 依赖: + +```shell +$ mvn dependency:tree + +[INFO] com.example:myproject:jar:0.0.1-SNAPSHOT +``` + +mvn dependency:tree 命令打印项目依赖项的层级结构。可以看到 spring-boot-starter-parent 本身没有提供任何依赖。要添加必要的依赖,需要编辑 pom.xml 并在 `` 部分添加 spring-boot-starter-web 依赖项: + +```xml + + + org.springframework.boot + spring-boot-starter-web + + +``` + +### 编写代码 + +要运行应用程序,我们需要创建一个启动类。默认情况下,Maven 从 `src/main/java` 编译源代码,因此您需要创建该目录结构,然后添加一个名为 `src/main/java/MyApplication.java` 的文件以包含以下代码: + +```java +@RestController +@EnableAutoConfiguration +public class MyApplication { + + @RequestMapping("/") + String home() { + return "Hello World!"; + } + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + +} +``` + +说明: + +@RestController 注解告诉 Spring,这个类是用来处理 Rest 请求的。 + +`@RequestMapping` 注解提供了“路由”信息。它告诉 Spring 任何带有 `/` 路径的 HTTP 请求都应该映射到 `home` 方法。 `@RestController` 注解告诉 Spring 将结果字符串直接呈现给调用者。 + +`@EnableAutoConfiguration` 注解告诉 Spring Boot 根据你添加的 jar 依赖去自动装配 Spring。 + +> 自动配置旨在与“Starters”配合使用,但这两个概念并没有直接联系。您可以自由选择 starters 之外的 jar 依赖项。 Spring Boot 仍然尽力自动配置您的应用程序。 + +Spring Boot 的 main 方法通过调用 run 委托给 Spring Boot 的 `SpringApplication` 类。 `SpringApplication` 引导我们的应用程序,启动 Spring,进而启动自动配置的 Tomcat Web 服务器。我们需要将 `MyApplication.class` 作为参数传递给 run 方法,以告诉 `SpringApplication` 哪个是入口类。还传递 args 数组以公开任何命令行参数。 + +### 运行示例 + +此时,您的应用程序应该可以工作了。由于您使用了 spring-boot-starter-parent POM,因此您有一个有用的运行目标,可用于启动应用程序。从项目根目录键入 mvn spring-boot:run 以启动应用程序。您应该会看到类似于以下内容的输出: + +```shell +$ mvn spring-boot:run + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v2.6.1) +....... . . . +....... . . . (log output here) +....... . . . +........ Started MyApplication in 2.222 seconds (JVM running for 6.514) +``` + +如果您打开 Web 浏览器访问 localhost:8080,您应该会看到以下输出: + +``` +Hello World! +``` + +要正常退出应用程序,请按 `ctrl-c`。 + +### 创建可执行 jar + +要创建一个可执行的 jar,我们需要将 spring-boot-maven-plugin 添加到我们的 pom.xml 中。为此,请在依赖项部分下方插入以下行: + +```xml + + + + org.springframework.boot + spring-boot-maven-plugin + + + +``` + +保存 pom.xml 并从命令行运行 mvn package,如下所示: + +```shell +$ mvn package + +[INFO] Scanning for projects... +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] Building myproject 0.0.1-SNAPSHOT +[INFO] ------------------------------------------------------------------------ +[INFO] .... .. +[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ myproject --- +[INFO] Building jar: /Users/developer/example/spring-boot-example/target/myproject-0.0.1-SNAPSHOT.jar +[INFO] +[INFO] --- spring-boot-maven-plugin:2.6.1:repackage (default) @ myproject --- +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +``` + +如果您查看 target 目录,应该会看到 `myproject-0.0.1-SNAPSHOT.jar`。该文件的大小应约为 10 MB。如果想看里面,可以使用 jar tvf,如下: + +```shell +$ jar tvf target/myproject-0.0.1-SNAPSHOT.jar +``` + +您还应该在目标目录中看到一个更小的名为 `myproject-0.0.1-SNAPSHOT.jar.original` 的文件。这是 Maven 在 Spring Boot 重新打包之前创建的原始 jar 文件。 + +要运行该应用程序,请使用 java -jar 命令,如下所示: + +``` +$ java -jar target/myproject-0.0.1-SNAPSHOT.jar + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v2.6.1) +....... . . . +....... . . . (log output here) +....... . . . +........ Started MyApplication in 2.536 seconds (JVM running for 2.864) +``` + +和以前一样,要退出应用程序,请按 `ctrl-c`。 + +## 通过 SPRING INITIALIZR 创建 Spring Boot 项目 + +### 创建项目 + +通过 `SPRING INITIALIZR` 工具产生基础项目 + +1. 访问:`http://start.spring.io/` +2. 选择构建工具`Maven Project`、Spring Boot 版本 `1.5.10` 以及一些工程基本信息,可参考下图所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/start.spring.io.png) + +3. 点击`Generate Project`下载项目压缩包 +4. 解压压缩包,包中已是一个完整的项目。 + +如果你使用 Intellij 作为 IDE,那么你可以直接使用 SPRING INITIALIZR,参考下图操作: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/intellij-spring-initializr.gif) + +### 项目说明 + +**重要文件** + +- `src/main/java` 路径下的 `Chapter1Application` 类 :程序入口 +- `src/main/resources` 路径下的 `application.properties` :项目配置文件 +- `src/test/java` 路径下的 `Chapter01ApplicationTests` :程序测试入口 + +**pom.xml** + +pom 中指定 parent 为以下内容,表示此项目继承了 `spring-boot-starter-parent` 的 maven 配置(主要是指定了常用依赖、插件的版本)。 + +```xml + + org.springframework.boot + spring-boot-starter-parent + 1.5.10.RELEASE + + +``` + +此外,pom 中默认引入两个依赖包,和一个插件。 + +```xml + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + +``` + +- `spring-boot-starter-web`:核心模块,包括自动配置支持、日志和 YAML。 +- `spring-boot-starter-test`:测试模块,包括 JUnit、Hamcrest、Mockito。 +- `spring-boot-maven-plugin`:spring boot 插件, 提供了一系列 spring boot 相关的 maven 操作。 + - `spring-boot:build-info`,生成 Actuator 使用的构建信息文件 build-info.properties + - `spring-boot:repackage`,默认 goal。在 mvn package 之后,再次打包可执行的 jar/war,同时保留 mvn package 生成的 jar/war 为.origin + - `spring-boot:run`,运行 Spring Boot 应用 + - `spring-boot:start`,在 mvn integration-test 阶段,进行 Spring Boot 应用生命周期的管理 + - `spring-boot:stop`,在 mvn integration-test 阶段,进行 Spring Boot 应用生命周期的管理 + +### 编写 REST 服务 + +- 创建 `package` ,名为 `io.github.zp.springboot.chapter1.web`(根据项目情况修改) +- 创建 `HelloController` 类,内容如下: + +```java +@RestController +public class HelloController { + + @RequestMapping("/hello") + public String index() { + return "Hello World"; + } + +} +``` + +- 启动主程序 `XXXApplication`,打开浏览器访问`http://localhost:8080/hello` ,可以看到页面输出`Hello World` + +### 编写单元测试用例 + +在 `XXXApplicationTests` 类中编写一个简单的单元测试来模拟 HTTP 请求,具体如下: + +```java +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = MockServletContext.class) +@WebAppConfiguration +public class SpringBootHelloWorldApplicationTest { + + private MockMvc mvc; + + @Before + public void setUp() { + mvc = MockMvcBuilders.standaloneSetup(new HelloController()).build(); + } + + @Test + public void getHello() throws Exception { + mvc.perform(MockMvcRequestBuilders.get("/hello").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string(equalTo("Hello World"))); + } + +} +``` + +使用`MockServletContext`来构建一个空的`WebApplicationContext`,这样我们创建的`HelloController`就可以在`@Before`函数中创建并传递到`MockMvcBuilders.standaloneSetup()`函数中。 + +- 注意引入下面内容,让`status`、`content`、`equalTo`函数可用 + +```java +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +``` + +至此已完成目标,通过 Maven 构建了一个空白 Spring Boot 项目,再通过引入 web 模块实现了一个简单的请求处理。 + +### 示例源码 + +> 示例源码:[spring-boot-web-helloworld](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/web/spring-boot-web-helloworld) + +## 参考资料 + +- [Spring Boot 官方文档之 Getting Started](https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started.html#getting-started) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/32.SpringBoot\344\271\213\345\261\236\346\200\247\345\212\240\350\275\275.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/32.SpringBoot\344\271\213\345\261\236\346\200\247\345\212\240\350\275\275.md" new file mode 100644 index 00000000..8a6ab562 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/32.SpringBoot\344\271\213\345\261\236\346\200\247\345\212\240\350\275\275.md" @@ -0,0 +1,387 @@ +--- +title: SpringBoot 之属性加载详解 +date: 2019-01-10 11:55:54 +order: 32 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/0fb992/ +--- + +# SpringBoot 之属性加载详解 + +## 加载 property 顺序 + +Spring Boot 加载 property 顺序如下: + +1. [Devtools 全局配置](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-devtools-globalsettings) (当 devtools 被激活 `~/.spring-boot-devtools.properties`). +2. [测试环境中的 `@TestPropertySource` 注解配置](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/test/context/TestPropertySource.html) +3. 测试环境中的属性 `properties`:[`@SpringBootTest`](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/context/SpringBootTest.html) 和 [测试注解](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-testing-spring-boot-applications-testing-autoconfigured-tests). +4. 命令行参数 +5. `SPRING_APPLICATION_JSON` 属性 +6. `ServletConfig` 初始化参数 +7. `ServletContext` 初始化参数 +8. JNDI attributes from 通过 `java:comp/env` 配置的 JNDI 属性 +9. Java 系统属性 (`System.getProperties()`) +10. 操作系统环境比那里 +11. `RandomValuePropertySource` 加载 `random.*` 形式的属性 +12. jar 包外的 `application-{profile}.properties` 或 `application-{profile}.yml` 配置 +13. jar 包内的 `application-{profile}.properties` 或 `application-{profile}.yml` 配置 +14. jar 包外的 `application.properties` 或 `application.yml` 配置 +15. jar 包内的 `application.properties` 或 `application.yml` 配置 +16. `@PropertySource` 绑定的配置 +17. 默认属性 (通过 `SpringApplication.setDefaultProperties` 指定) + +## 随机属性 + +`RandomValuePropertySource` 类用于配置随机值。 + +示例: + +```properties +my.secret=${random.value} +my.number=${random.int} +my.bignumber=${random.long} +my.uuid=${random.uuid} +my.number.less.than.ten=${random.int(10)} +my.number.in.range=${random.int[1024,65536]} +``` + +## 命令行属性 + +默认情况下, `SpringApplication` 会获取 `--` 参数(例如 `--server.port=9000` ),并将这个 `property` 添加到 Spring 的 `Environment` 中。 + +如果不想加载命令行属性,可以通过 `SpringApplication.setAddCommandLineProperties(false)` 禁用。 + +## Application 属性文件 + +`SpringApplication` 会自动加载以下路径下的 `application.properties` 配置文件,将其中的属性读到 Spring 的 `Environment` 中。 + +1. 当前目录的 `/config` 子目录 +2. 当前目录 +3. classpath 路径下的 `/config` package +4. classpath 根路径 + +> 注: +> +> 以上列表的配置文件会根据顺序,后序的配置会覆盖前序的配置。 +> +> 你可以选择 [YAML(yml)](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-yaml) 配置文件替换 properties 配置文件。 + +如果不喜欢 `application.properties` 作为配置文件名,可以使用 `spring.config.name` 环境变量替换: + +``` +$ java -jar myproject.jar --spring.config.name=myproject +``` + +可以使用 `spring.config.location` 环境变量指定配置文件路径: + +```properties +$ java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties +``` + +## Profile 特定属性 + +如果定义 `application-{profile}.properties` 形式的配置文件,将被视为 `profile` 环境下的特定配置。 + +可以通过 `spring.profiles.active` 参数来激活 profile,如果没有激活的 profile,默认会加载 `application-default.properties` 中的配置。 + +## 属性中的占位符 + +`application.properties` 中的值会被 `Environment` 过滤,所以,可以引用之前定义的属性。 + +``` +app.name=MyApp +app.description=${app.name} is a Spring Boot application +``` + +> 注:你可以使用此技术来创建 Spring Boot 属性变量。请参考: [Section 77.4, “Use ‘Short’ Command Line Arguments](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-use-short-command-line-arguments) + +## YAML 属性 + +Spring 框架有两个类支持加载 YAML 文件。 + +- `YamlPropertiesFactoryBean` 将 YAML 文件的配置加载为 `Properties` 。 +- `YamlMapFactoryBean` 将 YAML 文件的配置加载为 `Map` 。 + +示例 1 + +```yaml +environments: + dev: + url: http://dev.example.com + name: Developer Setup + prod: + url: http://another.example.com + name: My Cool App +``` + +等价于: + +```properties +environments.dev.url=http://dev.example.com +environments.dev.name=Developer Setup +environments.prod.url=http://another.example.com +environments.prod.name=My Cool App +``` + +YAML 支持列表形式,等价于 property 中的 `[index]` : + +```yaml +my: +servers: + - dev.example.com + - another.example.com +``` + +等价于 + +```properties +my.servers[0]=dev.example.com +my.servers[1]=another.example.com +``` + +### 访问属性 + +`YamlPropertySourceLoader` 类会将 YAML 配置转化为 Spring `Environment` 类中的 `PropertySource` 。然后,你可以如同 properties 文件中的属性一样,使用 `@Value` 注解来访问 YAML 中配置的属性。 + +### 多 profile 配置 + +```yaml +server: + address: 192.168.1.100 +--- +spring: + profiles: development +server: + address: 127.0.0.1 +--- +spring: + profiles: production & eu-central +server: + address: 192.168.1.120 +``` + +### YAML 的缺点 + +注:YAML 注解中的属性不能通过 `@PropertySource` 注解来访问。所以,如果你的项目中使用了一些自定义属性文件,建议不要用 YAML。 + +## 属性前缀 + +```java +package com.example; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix="acme") +public class AcmeProperties { + + private boolean enabled; + + private InetAddress remoteAddress; + + private final Security security = new Security(); + + public boolean isEnabled() { ... } + + public void setEnabled(boolean enabled) { ... } + + public InetAddress getRemoteAddress() { ... } + + public void setRemoteAddress(InetAddress remoteAddress) { ... } + + public Security getSecurity() { ... } + + public static class Security { + + private String username; + + private String password; + + private List roles = new ArrayList<>(Collections.singleton("USER")); + + public String getUsername() { ... } + + public void setUsername(String username) { ... } + + public String getPassword() { ... } + + public void setPassword(String password) { ... } + + public List getRoles() { ... } + + public void setRoles(List roles) { ... } + + } +} +``` + +相当于支持配置以下属性: + +- `acme.enabled` +- `acme.remote-address` +- `acme.security.username` +- `acme.security.password` +- `acme.security.roles` + +然后,你需要使用 `@EnableConfigurationProperties` 注解将属性类注入配置类中。 + +```java +@Configuration +@EnableConfigurationProperties(AcmeProperties.class) +public class MyConfiguration { +} +``` + +## 属性松散绑定规则 + +Spring Boot 属性名绑定比较松散。 + +以下属性 key 都是等价的: + +| Property | Note | +| ----------------------------------- | -------- | +| `acme.my-project.person.first-name` | `-` 分隔 | +| `acme.myProject.person.firstName` | 驼峰命名 | +| `acme.my_project.person.first_name` | `_` 分隔 | +| `ACME_MYPROJECT_PERSON_FIRSTNAME` | 大写字母 | + +## 属性转换 + +如果需要类型转换,你可以提供一个 `ConversionService` bean (一个名叫 `conversionService` 的 bean) 或自定义属性配置 (一个 `CustomEditorConfigurer` bean) 或自定义的 `Converters` (被 `@ConfigurationPropertiesBinding` 注解修饰的 bena)。 + +### 时间单位转换 + +Spring 使用 `java.time.Duration` 类代表时间大小,以下场景适用: + +- 除非指定 `@DurationUnit` ,否则一个 long 代表的时间为毫秒。 +- ISO-8601 标准格式( [`java.time.Duration`](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html#parse-java.lang.CharSequence-) 的实现就是参照此标准) +- 你也可以使用以下支持的单位: + - `ns` - 纳秒 + - `us` - 微秒 + - `ms` - 毫秒 + - `s` - 秒 + - `m` - 分 + - `h` - 时 + - `d` - 天 + +示例: + +```java +@ConfigurationProperties("app.system") +public class AppSystemProperties { + + @DurationUnit(ChronoUnit.SECONDS) + private Duration sessionTimeout = Duration.ofSeconds(30); + + private Duration readTimeout = Duration.ofMillis(1000); + + public Duration getSessionTimeout() { + return this.sessionTimeout; + } + + public void setSessionTimeout(Duration sessionTimeout) { + this.sessionTimeout = sessionTimeout; + } + + public Duration getReadTimeout() { + return this.readTimeout; + } + + public void setReadTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + } + +} +``` + +### 数据大小转换 + +Spring 使用 `DataSize` 类代表数据大小,以下场景适用: + +- long 值(默认视为 byte) +- 你也可以使用以下支持的单位: + - `B` + - `KB` + - `MB` + - `GB` + - `TB` + +示例: + +```java +@ConfigurationProperties("app.io") +public class AppIoProperties { + + @DataSizeUnit(DataUnit.MEGABYTES) + private DataSize bufferSize = DataSize.ofMegabytes(2); + + private DataSize sizeThreshold = DataSize.ofBytes(512); + + public DataSize getBufferSize() { + return this.bufferSize; + } + + public void setBufferSize(DataSize bufferSize) { + this.bufferSize = bufferSize; + } + + public DataSize getSizeThreshold() { + return this.sizeThreshold; + } + + public void setSizeThreshold(DataSize sizeThreshold) { + this.sizeThreshold = sizeThreshold; + } + +} +``` + +## 校验属性 + +```java +@ConfigurationProperties(prefix="acme") +@Validated +public class AcmeProperties { + + @NotNull + private InetAddress remoteAddress; + + @Valid + private final Security security = new Security(); + + // ... getters and setters + + public static class Security { + + @NotEmpty + public String username; + + // ... getters and setters + + } + +} +``` + +你也可以自定义一个名为 `configurationPropertiesValidator` 的校验器 Bean。获取这个 `@Bean` 的方法必须声明为 `static`。 + +## 示例源码 + +> 示例源码:[spring-boot-property](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-property) + +## 参考资料 + +- [Spring Boot 官方文档之 boot-features-external-config](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/33.SpringBoot\344\271\213Profile.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/33.SpringBoot\344\271\213Profile.md" new file mode 100644 index 00000000..667df738 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/33.SpringBoot\344\271\213Profile.md" @@ -0,0 +1,200 @@ +--- +title: SpringBoot 之 Profile +date: 2019-11-18 14:55:01 +order: 33 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/cb598e/ +--- + +# SpringBoot 之 Profile + +> 一个应用为了在不同的环境下工作,常常会有不同的配置,代码逻辑处理。Spring Boot 对此提供了简便的支持。 +> +> 关键词: `@Profile`、`spring.profiles.active` + +## 区分环境的配置 + +### properties 配置 + +假设,一个应用的工作环境有:dev、test、prod + +那么,我们可以添加 4 个配置文件: + +- `applcation.properties` - 公共配置 +- `application-dev.properties` - 开发环境配置 +- `application-test.properties` - 测试环境配置 +- `application-prod.properties` - 生产环境配置 + +在 `applcation.properties` 文件中可以通过以下配置来激活 profile: + +```properties +spring.profiles.active = test +``` + +### yml 配置 + +与 properties 文件类似,我们也可以添加 4 个配置文件: + +- `applcation.yml` - 公共配置 +- `application-dev.yml` - 开发环境配置 +- `application-test.yml` - 测试环境配置 +- `application-prod.yml` - 生产环境配置 + +在 `applcation.yml` 文件中可以通过以下配置来激活 profile: + +```yml +spring: + profiles: + active: prod +``` + +此外,yml 文件也可以在一个文件中完成所有 profile 的配置: + +```yml +# 激活 prod +spring: + profiles: + active: prod +# 也可以同时激活多个 profile +# spring.profiles.active: prod,proddb,prodlog +--- +# dev 配置 +spring: + profiles: dev + +# 略去配置 + +--- +spring: + profiles: test + +# 略去配置 + +--- +spring.profiles: prod +spring.profiles.include: + - proddb + - prodlog + +--- +spring: + profiles: proddb + +# 略去配置 + +--- +spring: + profiles: prodlog +# 略去配置 +``` + +注意:不同 profile 之间通过 `---` 分割 + +## 区分环境的代码 + +使用 `@Profile` 注解可以指定类或方法在特定的 Profile 环境生效。 + +### 修饰类 + +```java +@Configuration +@Profile("production") +public class JndiDataConfig { + + @Bean(destroyMethod="") + public DataSource dataSource() throws Exception { + Context ctx = new InitialContext(); + return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); + } +} +``` + +### 修饰注解 + +```java +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Profile("production") +public @interface Production { +} +``` + +### 修饰方法 + +```java +@Configuration +public class AppConfig { + + @Bean("dataSource") + @Profile("development") + public DataSource standaloneDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .addScript("classpath:com/bank/config/sql/schema.sql") + .addScript("classpath:com/bank/config/sql/test-data.sql") + .build(); + } + + @Bean("dataSource") + @Profile("production") + public DataSource jndiDataSource() throws Exception { + Context ctx = new InitialContext(); + return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); + } +} +``` + +## 激活 profile + +### 插件激活 profile + +``` +spring-boot:run -Drun.profiles=prod +``` + +### main 方法激活 profile + +``` +--spring.profiles.active=prod +``` + +### jar 激活 profile + +``` +java -jar -Dspring.profiles.active=prod *.jar +``` + +### 在 Java 代码中激活 profile + +直接指定环境变量来激活 profile: + +```java +System.setProperty("spring.profiles.active", "test"); +``` + +在 Spring 容器中激活 profile: + +```java +AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); +ctx.getEnvironment().setActiveProfiles("development"); +ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class); +ctx.refresh(); +``` + +## 示例源码 + +> 示例源码:[spring-boot-profile](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-profile) + +## 参考资料 + +- [Spring 官方文档之 Bean Definition Profiles](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-definition-profiles) +- [Spring Boot 官方文档之 boot-features-profiles](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-profiles) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/README.md" new file mode 100644 index 00000000..544eb1c1 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/README.md" @@ -0,0 +1,62 @@ +--- +title: Spring 核心 +date: 2020-02-26 23:47:47 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/5e7c20/ +hidden: true +index: false +--- + +# Spring 核心 + +> 章节主要针对:Spring & Spring Boot 框架的核心技术。如;Spring Bean、IoC、依赖查找、依赖注入、AOP、数据绑定、资源管理等。 + +## 📖 内容 + +- [Spring Bean](01.SpringBean.md) +- [Spring IoC](02.SpringIoC.md) +- [Spring 依赖查找](03.Spring依赖查找.md) +- [Spring 依赖注入](04.Spring依赖注入.md) +- [Spring IoC 依赖来源](05.SpringIoC依赖来源.md) +- [Spring Bean 作用域](06.SpringBean作用域.md) +- [Spring Bean 生命周期](07.SpringBean生命周期.md) +- [Spring 配置元数据](08.Spring配置元数据.md) +- [Spring AOP](10.SpringAop.md) +- [Spring 资源管理](20.Spring资源管理.md) +- [Spring 校验](21.Spring校验.md) +- [Spring 数据绑定](22.Spring数据绑定.md) +- [Spring 类型转换](23.Spring类型转换.md) +- [Spring EL 表达式](24.SpringEL.md) +- [Spring 事件](25.Spring事件.md) +- [Spring 国际化](26.Spring国际化.md) +- [Spring 泛型处理](27.Spring泛型处理.md) +- [Spring 注解](28.Spring注解.md) +- [SpringBoot 教程之快速入门](31.SpringBoot之快速入门.md) +- [SpringBoot 之属性加载](32.SpringBoot之属性加载.md) +- [SpringBoot 之 Profile](33.SpringBoot之Profile.md) + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/01.Spring\344\271\213\346\225\260\346\215\256\346\272\220.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/01.Spring\344\271\213\346\225\260\346\215\256\346\272\220.md" new file mode 100644 index 00000000..5d01d703 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/01.Spring\344\271\213\346\225\260\346\215\256\346\272\220.md" @@ -0,0 +1,555 @@ +--- +title: Spring 之数据源 +date: 2017-10-20 09:27:55 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 数据库 + - DataSource +permalink: /pages/1b774c/ +--- + +# Spring 之数据源 + +> 本文基于 Spring Boot 2.7.3 版本。 + +## Spring Boot 数据源基本配置 + +Spring Boot 提供了一系列 `spring.datasource.*` 配置来控制 `DataSource` 的配置。用户可以在 `application.properties` 或 `application.yml` 文件中指定数据源配置。这些配置项维护在 [`DataSourceProperties`](https://github.com/spring-projects/spring-boot/tree/v2.7.4/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java) 。 + +下面是一个最基本的 mysql 数据源配置示例(都是必填项): + +```properties +# 数据库访问地址 +spring.datasource.url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +# 数据库驱动类,必须保证驱动类是可加载的 +spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver +# 数据库账号 +spring.datasource.username = root +# 数据库账号密码 +spring.datasource.password = root +``` + +需要根据实际情况,替换 `url`、`username`、`password`。 + +## Spring Boot 连接嵌入式数据源 + +使用内存嵌入式数据库开发应用程序通常很方便。显然,内存数据库不提供持久存储。使用者需要在应用程序启动时填充数据库,并准备在应用程序结束时丢弃数据。 + +Spring Boot 可以自动配置嵌入式数据库 [H2](https://www.h2database.com/)、[HSQL](https://hsqldb.org/) 和 [Derby](https://db.apache.org/derby/)。使用者无需提供任何连接 URL,只需要包含对要使用的嵌入式数据库的构建依赖项。如果类路径上有多个嵌入式数据库,需要设置 `spring.datasource.embedded-database-connection` 配置属性来控制使用哪一个。将该属性设置为 none 会禁用嵌入式数据库的自动配置。 + +> 注意:如果在测试中使用此功能,无论使用多少应用程序上下文,整个测试套件都会重用同一个数据库。如果要确保每个上下文都有一个单独的嵌入式数据库,则应将 `spring.datasource.generate-unique-name` 设置为 true。 + +下面,通过一个实例展示如何连接 H2 嵌入式数据库。 + +(1)在 pom.xml 中引入所需要的依赖: + +```xml + + org.springframework.boot + spring-boot-starter-data-jdbc + + + com.h2database + h2 + +``` + +(2)数据源配置 + +```properties +spring.datasource.jdbc-url = jdbc:h2:mem:test +spring.datasource.driver-class-name = org.h2.Driver +spring.datasource.username = sa +spring.datasource.password = +``` + +## Spring Boot 连接池化数据源 + +> 完整示例:[spring-boot-data-jdbc](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/spring-boot-data-jdbc) + +在生产环境中,出于性能考虑,一般会通过数据库连接池连接数据源。 + +除了 [`DataSourceProperties`](https://github.com/spring-projects/spring-boot/tree/v2.7.4/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java) 中的数据源通用配置以外,Spring Boot 还支持通过使用类似`spring.datasource.hikari.*`、`spring.datasource.tomcat.*`、`spring.datasource.dbcp2.*` 和 `spring.datasource.oracleucp.*` 的前缀来配置指定的数据库连接池属性。 + +下面,就是一份 hikari 的连接池配置示例: + +```properties +# 连接池名称 +spring.datasource.hikari.pool-name = SpringTutorialHikariPool +# 最大连接数,小于等于 0 会被重置为默认值 10;大于零小于 1 会被重置为 minimum-idle 的值 +spring.datasource.hikari.maximum-pool-size = 10 +# 最小空闲连接,默认值10,小于 0 或大于 maximum-pool-size,都会重置为 maximum-pool-size +spring.datasource.hikari.minimum-idle = 10 +# 连接超时时间(单位:毫秒),小于 250 毫秒,会被重置为默认值 30 秒 +spring.datasource.hikari.connection-timeout = 60000 +# 空闲连接超时时间,默认值 600000(10分钟),大于等于 max-lifetime 且 max-lifetime>0,会被重置为0;不等于 0 且小于 10 秒,会被重置为 10 秒 +# 只有空闲连接数大于最大连接数且空闲时间超过该值,才会被释放 +spring.datasource.hikari.idle-timeout = 600000 +# 连接最大存活时间,不等于 0 且小于 30 秒,会被重置为默认值 30 分钟。该值应该比数据库所设置的超时时间短 +spring.datasource.hikari.max-lifetime = 540000 +``` + +Spring Boot 会按以下顺序检测连接池是否可用,如果可用就选择对应的池化 `DataSource`: + +HikariCP -> Tomcat pooling DataSource -> DBCP2 -> Oracle UCP + +用户也可以通过 `spring.datasource.type` 来指定数据源类型。 + +此外,也可以使用 `DataSourceBuilder` 手动配置其他连接池。如果自定义 DataSource bean,则不会发生自动配置。 `DataSourceBuilder` 支持以下连接池: + +- HikariCP +- Tomcat pooling `Datasource` +- Commons DBCP2 +- Oracle UCP & `OracleDataSource` +- Spring Framework’s `SimpleDriverDataSource` +- H2 `JdbcDataSource` +- PostgreSQL `PGSimpleDataSource` +- C3P0 + +### 引入 Spring Boot 依赖 + +你可以通过 Spring Boot 官方的初始化器([Spring Initializr](https://start.spring.io/))选择需要的组件来创建一个 Spring Boot 工程。或者,直接在 pom.xml 中引入所需要的依赖: + +```xml + + org.springframework.boot + spring-boot-starter-data-jdbc + + + mysql + mysql-connector-java + +``` + +### 测试单数据源连接 + +```java +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Connection; +import javax.sql.DataSource; + +@Slf4j +@SpringBootApplication +public class SpringBootDataJdbcApplication implements CommandLineRunner { + + private final JdbcTemplate jdbcTemplate; + + public SpringBootDataJdbcApplication(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public static void main(String[] args) { + SpringApplication.run(SpringBootDataJdbcApplication.class, args); + } + + @Override + public void run(String... args) throws Exception { + DataSource dataSource = jdbcTemplate.getDataSource(); + + Connection connection; + if (dataSource != null) { + connection = dataSource.getConnection(); + } else { + log.error("连接数据源失败!"); + return; + } + + if (connection != null) { + log.info("数据源 Url: {}", connection.getMetaData().getURL()); + } else { + log.error("连接数据源失败!"); + } + } + +} +``` + +运行 `main` 方法后,控制台会输出以下内容,表示数据源连接成功: + +``` +20:50:18.449 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcApplication.run - 数据源 Url: jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +``` + +## Spring Boot 连接多数据源 + +> 完整示例:[spring-boot-data-jdbc-multi-datasource](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/spring-boot-data-jdbc-multi-datasource) + +Spring Boot 连接多数据源所需要的依赖并无不同,主要差异在于数据源的配置。Spring Boot 默认的数据源配置类为 `org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration`。使用者只要指定一些必要的 spring.datasource 配置,`DataSourceAutoConfiguration` 类就会自动完成剩下的数据源实例化工作。 + +### 多数据源配置 + +下面的示例中,自定义了一个数据源配置类,通过读取不同的 spring.datasource.xxx 来完成对于不同数据源的实例化工作。对于 JDBC 来说,最重要的,就是实例化 `DataSource` 和 `JdbcTemplate`。 + +```java +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; + +@Configuration +public class DataSourceConfig { + + @Primary + @Bean("mysqlDataSource") + @ConfigurationProperties(prefix = "spring.datasource.mysql") + public DataSource mysqlDataSource() { + return DataSourceBuilder.create().build(); + } + + @Primary + @Bean("mysqlJdbcTemplate") + public JdbcTemplate mysqlJdbcTemplate(@Qualifier("mysqlDataSource") DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Bean("h2DataSource") + @ConfigurationProperties(prefix = "spring.datasource.h2") + public DataSource h2DataSource() { + return DataSourceBuilder.create().build(); + } + + @Bean(name = "h2JdbcTemplate") + public JdbcTemplate h2JdbcTemplate(@Qualifier("h2DataSource") DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + +} +``` + +`application.properties` 或 `application.yml` 配置文件中也必须以 `@ConfigurationProperties` 所指定的配置前缀进行配置: + +```properties +# 数据源一:Mysql +spring.datasource.mysql.jdbc-url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false +spring.datasource.mysql.driver-class-name = com.mysql.cj.jdbc.Driver +spring.datasource.mysql.username = root +spring.datasource.mysql.password = root +# 数据源一:H2 +spring.datasource.h2.jdbc-url = jdbc:h2:mem:test +spring.datasource.h2.driver-class-name = org.h2.Driver +spring.datasource.h2.username = sa +spring.datasource.h2.password = +``` + +### 测试多数据源连接 + +```java + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; + +@SpringBootApplication +public class SpringBootDataJdbcMultiDataSourceApplication implements CommandLineRunner { + + private static final Logger log = LoggerFactory.getLogger(SpringBootDataJdbcMultiDataSourceApplication.class); + + private final UserDao mysqlUserDao; + + private final UserDao h2UserDao; + + public SpringBootDataJdbcMultiDataSourceApplication(@Qualifier("mysqlUserDao") UserDao mysqlUserDao, + @Qualifier("h2UserDao") UserDao h2UserDao) { + this.mysqlUserDao = mysqlUserDao; + this.h2UserDao = h2UserDao; + } + + public static void main(String[] args) { + SpringApplication.run(SpringBootDataJdbcMultiDataSourceApplication.class, args); + } + + @Override + public void run(String... args) throws Exception { + + if (mysqlUserDao != null && mysqlUserDao.getJdbcTemplate() != null) { + printDataSourceInfo(mysqlUserDao.getJdbcTemplate()); + log.info("Connect to mysql datasource success."); + } else { + log.error("Connect to mysql datasource failed!"); + return; + } + + if (h2UserDao != null) { + printDataSourceInfo(h2UserDao.getJdbcTemplate()); + log.info("Connect to h2 datasource success."); + } else { + log.error("Connect to h2 datasource failed!"); + return; + } + + // 主数据源执行 JDBC SQL + mysqlUserDao.recreateTable(); + + // 次数据源执行 JDBC SQL + h2UserDao.recreateTable(); + } + + private void printDataSourceInfo(JdbcTemplate jdbcTemplate) throws SQLException { + + DataSource dataSource = jdbcTemplate.getDataSource(); + + Connection connection; + if (dataSource != null) { + connection = dataSource.getConnection(); + } else { + log.error("Get dataSource failed!"); + return; + } + + if (connection != null) { + log.info("DataSource Url: {}", connection.getMetaData().getURL()); + } else { + log.error("Connect to datasource failed!"); + } + } + +} +``` + +运行 `main` 方法后,控制台会输出以下内容,表示数据源连接成功: + +``` +21:16:44.654 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.printDataSourceInfo - DataSource Url: jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false +21:16:44.654 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.run - Connect to mysql datasource success. + +21:16:44.726 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.printDataSourceInfo - DataSource Url: jdbc:h2:mem:test +21:16:44.726 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.run - Connect to h2 datasource success. +``` + +## Spring 之数据源 + +如果你的项目是传统的 Spring 项目,当然也可以轻松建立数据源连接,只是需要自行设置的配置更多一些。 + +### 引入 Spring 依赖 + +在 pom.xml 中引入所需要的依赖: + +```xml + + + com.alibaba + druid + + + mysql + mysql-connector-java + + + + org.springframework + spring-context-support + + + org.springframework + spring-jdbc + + + org.springframework + spring-tx + + + +``` + +### Spring 配置数据源 + +Spring 配置数据源有多种方式,下面一一列举: + +#### 使用 JNDI 数据源 + +如果 Spring 应用部署在支持 JNDI 的 WEB 服务器上(如 WebSphere、JBoss、Tomcat 等),就可以使用 JNDI 获取数据源。 + +```xml + + + + + + + + + + + +``` + +#### 使用数据库连接池 + +Spring 本身并没有提供数据库连接池的实现,需要自行选择合适的数据库连接池。下面是一个使用 [Druid](https://github.com/alibaba/druid) 作为数据库连接池的示例: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +#### 基于 JDBC 驱动的数据源 + +```xml + + + + + + +``` + +## SpringBoot 数据源配置 + +> Spring Boot 数据库配置官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.sql + +通过前面的实战,我们已经知道了 Spring、Spring Boot 是如何连接数据源,并通过 JDBC 方式访问数据库。 + +SpringBoot 数据源的配置方式是在 `application.properties` 或 `application.yml` 文件中指定 `spring.datasource.*` 的配置。 + +(1)数据源基本配置方式是指定 url、用户名、密码 + +```properties +spring.datasource.url=jdbc:mysql://localhost/test +spring.datasource.username=dbuser +spring.datasource.password=dbpass +``` + +(2)配置 JNDI + +如果想要通过 JNDI 方式连接数据源,可以采用如下方式: + +```properties +spring.datasource.jndi-name=java:jboss/datasources/customers +``` + +## DataSourceAutoConfiguration 类 + +显而易见,Spring Boot 的配置更加简化,那么, Spring Boot 做了哪些工作,使得接入更加便捷呢?奥秘就在于 `spring-boot-autoconfigure` jar 包,其中定义了大量的 Spring Boot 自动配置类。其中,与数据库访问相关的比较核心的配置类有: + +- `DataSourceAutoConfiguration`:数据源自动配置类 +- `JdbcTemplateAutoConfiguration`:`JdbcTemplate` 自动配置类 +- `DataSourceTransactionManagerAutoConfiguration`:数据源事务管理自动配置类 +- `JndiDataSourceAutoConfiguration`:JNDI 数据源自动配置类 +- `EmbeddedDataSourceConfiguration`:嵌入式数据库数据源自动配置类 +- 等等 + +这些自动配置类会根据各种条件控制核心类的实例化。 + +`DataSourceAutoConfiguration` 是数据源自动配置类,它负责实例化 `DataSource`。 + +`DataSourceAutoConfiguration` 的源码如下(省略部分代码): + +```java +@AutoConfiguration(before = SqlInitializationAutoConfiguration.class) +@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) +@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory") +@EnableConfigurationProperties(DataSourceProperties.class) +@Import(DataSourcePoolMetadataProvidersConfiguration.class) +public class DataSourceAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + @Conditional(EmbeddedDatabaseCondition.class) + @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) + @Import(EmbeddedDataSourceConfiguration.class) + protected static class EmbeddedDatabaseConfiguration { + } + + @Configuration(proxyBeanMethods = false) + @Conditional(PooledDataSourceCondition.class) + @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) + @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, + DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, + DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class }) + protected static class PooledDataSourceConfiguration { + } + + static class PooledDataSourceCondition extends AnyNestedCondition { + // 略 + } + + static class PooledDataSourceAvailableCondition extends SpringBootCondition { + // 略 + } + + static class EmbeddedDatabaseCondition extends SpringBootCondition { + // 略 + } +} +``` + +`DataSourceAutoConfiguration` 类的源码解读: + +- `DataSourceProperties` 是 `DataSourceAutoConfiguration` 的配置选项类,允许使用者通过设置选项控制 `DataSource` 初始化行为。 +- `DataSourceAutoConfiguration` 通过 `@Import` 注解引入 `DataSourcePoolMetadataProvidersConfiguration` 类。 +- `DataSourceAutoConfiguration` 中定义了两个内部类:嵌入式数据源配置类 `EmbeddedDatabaseConfiguration` 和 池化数据源配置类 `PooledDataSourceConfiguration`,分别标记了不同的实例化条件。 + - 当满足 `EmbeddedDatabaseConfiguration` 的示例化条件时,将引入 `EmbeddedDataSourceConfiguration` 类初始化数据源,这个类实际上是加载嵌入式数据源驱动的 ClassLoader 去进行初始化。 + - 当满足 `PooledDataSourceConfiguration` 的示例化条件时,将引入 `DataSourceConfiguration.Hikari.class`、`DataSourceConfiguration.Tomcat.class`、`DataSourceConfiguration.Dbcp2.class`、`DataSourceConfiguration.OracleUcp.class`、`DataSourceConfiguration.Generic.class`、`DataSourceJmxConfiguration.class` 这些配置类,分别对应不同的数据库连接池方式。具体选用哪种数据库连接池,可以通过 `spring.datasource.type` 配置指定。其中,Hikari 是 Spring Boot 默认的数据库连接池,spring-boot-starter-data-jdbc 中内置了 Hikari 连接池驱动包。如果想要替换其他数据库连接池,前提是必须先手动引入对应的连接池驱动包。 + +## 参考资料 + +- [Spring 官网](https://spring.io/) +- [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/02.Spring\344\271\213JDBC.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/02.Spring\344\271\213JDBC.md" new file mode 100644 index 00000000..e1990392 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/02.Spring\344\271\213JDBC.md" @@ -0,0 +1,768 @@ +--- +title: Spring 之 JDBC +date: 2019-02-18 14:33:55 +order: 02 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - JDBC + - JdbcTemplate +permalink: /pages/cf19fd/ +--- + +# Spring 之 JDBC + +JDBC 是 Java 语言中用来规范客户端程序如何访问数据库的应用程序接口,提供了增、删、改、查数据库的方法。 + +## JDBC 入门示例 + +JDBC 的工作步骤大致如下: + +1. 创建实体类。 +2. 声明数据库读写接口的 DAO 接口。定义 DAO 的好处在于对于数据层上层的业务,调用 DAO 时仅关注对外暴露的读写方法,而不考虑底层的具体持久化方式。这样,便于替换持久化方式。 +3. 创建一个 DAO 接口的实现类,使用 Spring 的 JDBC 模板去实现接口。 +4. 最后,定义一个 DAO 接口的实现类的 JavaBean,并将数据源注入进去。 + +假设,我们要通过 Spring + JDBC 访问一张 Mysql 数据表 `user`,`user` 表的数据结构如下: + +```sql +-- 创建用户表 +CREATE TABLE `user` ( + `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID', + `name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '用户名', + `age` INT(3) NOT NULL DEFAULT 0 COMMENT '年龄', + `address` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '地址', + `email` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '邮件', + PRIMARY KEY (`id`), + UNIQUE (`name`) +) COMMENT = '用户表'; + +INSERT INTO `user` (`name`, `age`, `address`, `email`) +VALUES ('张三', 18, '北京', 'xxx@163.com'); +INSERT INTO `user` (`name`, `age`, `address`, `email`) +VALUES ('李四', 19, '上海', 'xxx@163.com'); +``` + +### 定义实体 + +```java +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Objects; + +@Data +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class User { + private Long id; + private String name; + private Integer age; + private String address; + private String email; +} +``` + +### 定义 DAO 接口 + +```java +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.List; + +/** + * user 表 Dao 接口 + * + * @author Zhang Peng + * @since 2019-11-18 + */ +public interface UserDao { + + // DML + // ------------------------------------------------------------------- + void insert(User user); + + void batchInsert(List users); + + void deleteByName(String name); + + void deleteAll(); + + void update(User user); + + Integer count(); + + List list(); + + User queryByName(String name); + + JdbcTemplate getJdbcTemplate(); + + // DDL + // ------------------------------------------------------------------- + void truncate(); + + void recreateTable(); + +} +``` + +### 定义 DAO 实现类 + +通过 `JdbcTemplate` 执行对应数据源符合语法的 SQL,即可完成各种数据库访问。 + +```java +package io.github.dunwu.springboot.core.data.jdbc; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +/** + * user 表 Dao 接口实现类 + * + * @author Zhang Peng + * @since 2019-11-18 + */ +@Repository +public class UserDaoImpl implements UserDao { + + private JdbcTemplate jdbcTemplate; + + public UserDaoImpl(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public void insert(User user) { + jdbcTemplate.update("INSERT INTO user(name, age, address, email) VALUES(?, ?, ?, ?)", + user.getName(), user.getAge(), user.getAddress(), user.getEmail()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchInsert(List users) { + String sql = "INSERT INTO user(name, age, address, email) VALUES(?, ?, ?, ?)"; + + List params = new ArrayList<>(); + + users.forEach(user -> { + params.add(new Object[] { user.getName(), user.getAge(), user.getAddress(), user.getEmail() }); + }); + jdbcTemplate.batchUpdate(sql, params); + } + + @Override + public void deleteByName(String name) { + jdbcTemplate.update("DELETE FROM user WHERE name = ?", name); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteAll() { + jdbcTemplate.execute("DELETE FROM user"); + } + + @Override + public void update(User user) { + jdbcTemplate.update("UPDATE user SET name=?, age=?, address=?, email=? WHERE id=?", + user.getName(), user.getAge(), user.getAddress(), user.getEmail(), user.getId()); + } + + @Override + public Integer count() { + try { + return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM user", Integer.class); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public List list() { + return jdbcTemplate.query("SELECT * FROM user", new BeanPropertyRowMapper<>(User.class)); + } + + @Override + public User queryByName(String name) { + try { + return jdbcTemplate.queryForObject("SELECT * FROM user WHERE name = ?", + new BeanPropertyRowMapper<>(User.class), name); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public JdbcTemplate getJdbcTemplate() { + return jdbcTemplate; + } + + @Override + public void truncate() { + jdbcTemplate.execute("TRUNCATE TABLE user"); + } + + @Override + public void recreateTable() { + jdbcTemplate.execute("DROP TABLE IF EXISTS user"); + + String sqlStatement = + "CREATE TABLE IF NOT EXISTS user (\n" + + " id BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Id',\n" + + " name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '用户名',\n" + + " age INT(3) NOT NULL DEFAULT 0 COMMENT '年龄',\n" + + " address VARCHAR(255) NOT NULL DEFAULT '' COMMENT '地址',\n" + + " email VARCHAR(255) NOT NULL DEFAULT '' COMMENT '邮件',\n" + + " PRIMARY KEY (id)\n" + + ") COMMENT = '用户表';"; + jdbcTemplate.execute(sqlStatement); + } + +} +``` + +### 测试类 + +```java +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +@Rollback +@SpringBootTest(classes = { SpringBootDataJdbcApplication.class }) +public class DataJdbcMysqlDataSourceTest { + + @Autowired + private UserDao userDAO; + + @BeforeEach + public void before() { + userDAO.truncate(); + } + + @Test + public void insert() { + userDAO.insert(new User("张三", 18, "北京", "user1@163.com")); + User linda = userDAO.queryByName("张三"); + assertThat(linda).isNotNull(); + } + + @Test + public void batchInsert() { + List users = new ArrayList<>(); + users.add(new User("张三", 18, "北京", "user1@163.com")); + users.add(new User("李四", 19, "上海", "user1@163.com")); + users.add(new User("王五", 18, "南京", "user1@163.com")); + users.add(new User("赵六", 20, "武汉", "user1@163.com")); + + userDAO.batchInsert(users); + int count = userDAO.count(); + assertThat(count).isEqualTo(4); + + List list = userDAO.list(); + assertThat(list).isNotEmpty().hasSize(4); + list.forEach(user -> { + log.info(user.toString()); + }); + } + + @Test + public void delete() { + List users = new ArrayList<>(); + users.add(new User("张三", 18, "北京", "user1@163.com")); + users.add(new User("李四", 19, "上海", "user1@163.com")); + users.add(new User("王五", 18, "南京", "user1@163.com")); + users.add(new User("赵六", 20, "武汉", "user1@163.com")); + userDAO.batchInsert(users); + + userDAO.deleteByName("张三"); + User user = userDAO.queryByName("张三"); + assertThat(user).isNull(); + + userDAO.deleteAll(); + List list = userDAO.list(); + assertThat(list).isEmpty(); + } + + @Test + public void update() { + userDAO.insert(new User("张三", 18, "北京", "user1@163.com")); + User oldUser = userDAO.queryByName("张三"); + oldUser.setName("张三丰"); + userDAO.update(oldUser); + User newUser = userDAO.queryByName("张三丰"); + assertThat(newUser).isNotNull(); + } + +} +``` + +## Spring Boot JDBC + +> 完整示例:[spring-boot-data-jdbc](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/spring-boot-data-jdbc) + +### 引入 Spring Boot 依赖 + +你可以通过 Spring Boot 官方的初始化器([Spring Initializr](https://start.spring.io/))选择需要的组件来创建一个 Spring Boot 工程。或者,直接在 pom.xml 中引入所需要的依赖: + +```xml + + org.springframework.boot + spring-boot-starter-parent + 2.7.7 + + + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + mysql + mysql-connector-java + + +``` + +### 配置数据源 + +引入依赖后,需要在 `application.properties` 或 `application.yml` 文件中指定数据源配置。 + +下面是一个最基本的数据源配置示例: + +```properties +spring.datasource.url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver +spring.datasource.username = root +spring.datasource.password = root +``` + +需要根据实际情况,替换 `url`、`username`、`password`。 + +### 测试 + +```java +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Connection; +import javax.sql.DataSource; + +@Slf4j +@SpringBootApplication +public class SpringBootDataJdbcApplication implements CommandLineRunner { + + private final JdbcTemplate jdbcTemplate; + + public SpringBootDataJdbcApplication(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public static void main(String[] args) { + SpringApplication.run(SpringBootDataJdbcApplication.class, args); + } + + @Override + public void run(String... args) throws Exception { + DataSource dataSource = jdbcTemplate.getDataSource(); + + Connection connection; + if (dataSource != null) { + connection = dataSource.getConnection(); + } else { + log.error("连接数据源失败!"); + return; + } + + if (connection != null) { + log.info("数据源 Url: {}", connection.getMetaData().getURL()); + } else { + log.error("连接数据源失败!"); + } + } + +} +``` + +运行 `main` 方法后,控制台会输出以下内容,表示数据源连接成功: + +``` +20:50:18.449 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcApplication.run - 数据源 Url: jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +``` + +## Spring JDBC + +> 完整示例:[spring-data-jdbc](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/spring-data-jdbc) + +`spring-boot-starter-data-jdbc` 引入了 `spring-jdbc` ,其 JDBC 特性就是基于 `spring-jdbc`。 + +### 引入 Spring 依赖 + +在 pom.xml 中引入所需要的依赖: + +```xml + + + com.alibaba + druid + + + mysql + mysql-connector-java + + + + org.springframework + spring-context-support + + + org.springframework + spring-jdbc + + + org.springframework + spring-tx + + + +``` + +### 基于 JDBC 驱动的数据源配置 + +下面是一个 mysql 的 JDBC 数据源配置实例: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### 测试 + +```java + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.io.IOException; +import java.sql.SQLException; + +@SuppressWarnings("all") +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = { "classpath:data/spring-mysql.xml" }) +public class MysqlJdbcTest { + + @Autowired + private ApplicationContext ctx; + + @Before + public void before() { + ctx = JdbcDemo.getMysqlApplicationContext(); + } + + @Test + public void testExecJdbcOper() throws SQLException, IOException { + UserDao userDao = (UserDaoImpl) ctx.getBean("userDao"); + JdbcDemo.execJdbcOper(userDao); + } + + @After + public void after() { + ((ClassPathXmlApplicationContext) ctx).close(); + } + +} +``` + +## JdbcTemplate API + +Spring 将数据访问的样板式代码提取到模板类中。Spring 提供了 3 个 JDBC 模板类: + +- `JdbcTemplate`:最基本的 Spring JDBC 模板,这个模板支持最简单的 JDBC 数据库访问功能以及简单的索引参数查询。 +- `SimpleJdbcTemplate`:改模板类利用 Java 5 的一些特性,如自动装箱、泛型以及可变参数列表来简化 JDBC 模板的使用。 +- `NamedParameterJdbcTemplate`:使用该模板类执行查询时,可以将查询值以命名参数的形式绑定到 SQL 中,而不是使用简单的索引参数。 + +`spring-jdbc` 最核心的 API 无疑就是 `JdbcTemplate`,可以说所有的 JDBC 数据访问,几乎都是围绕着这个类去工作的。Spring 对数据库的操作在 Jdbc 层面做了深层次的封装,利用依赖注入,把数据源配置装配到 `JdbcTemplate` 中,再由 `JdbcTemplate` 负责具体的数据访问。 + +`JdbcTemplate` 主要提供以下几类方法: + +- `execute` 方法:可以用于执行任何 SQL 语句,一般用于执行 DDL 语句; +- `update` 方法及 `batchUpdate` 方法:`update` 方法用于执行新增、修改、删除等语句;`batchUpdate` 方法用于执行批处理相关语句; +- `query` 方法及 `queryForXXX` 方法:用于执行查询相关语句; +- `call` 方法:用于执行存储过程、函数相关语句。 + +为了方便演示,以下增删改查操作都围绕一个名为 user 的表(该表的主键 id 是自增序列)进行,该表的数据实体如下: + +```java +public class User { + private Integer id; + private String name; + private Integer age; + + // 省略 getter/setter +} +``` + +数据实体只要是一个纯粹的 Java Bean 即可,无需任何注解修饰。 + +### execute 方法 + +使用 execute 执行 DDL 语句,创建一个名为 test 的数据库,并在此数据库下新建一个名为 user 的表。 + +```java +public void recreateTable() { + jdbcTemplate.execute("DROP DATABASE IF EXISTS test"); + jdbcTemplate.execute("CREATE DATABASE test"); + jdbcTemplate.execute("USE test"); + jdbcTemplate.execute("DROP TABLE if EXISTS user"); + jdbcTemplate.execute("DROP TABLE if EXISTS user"); + // @formatter:off + StringBuilder sb = new StringBuilder(); + sb.append("CREATE TABLE user (id int (10) unsigned NOT NULL AUTO_INCREMENT,\n") + .append("name varchar (64) NOT NULL DEFAULT '',\n") + .append("age tinyint (3) NOT NULL DEFAULT 0,\n") + .append("PRIMARY KEY (ID));\n"); + // @formatter:on + jdbcTemplate.execute(sb.toString()); +} +``` + +### update 方法 + +新增数据 + +```java +public void insert(String name, Integer age) { + jdbcTemplate.update("INSERT INTO user(name, age) VALUES(?, ?)", name, age); +} +``` + +删除数据 + +```java +public void delete(String name) { + jdbcTemplate.update("DELETE FROM user WHERE name = ?", name); +} +``` + +修改数据 + +```java +public void update(User user) { + jdbcTemplate.update("UPDATE USER SET name=?, age=? WHERE id=?", user.getName(), user.getAge(), user.getId()); +} +``` + +批处理 + +```java +public void batchInsert(List users) { + String sql = "INSERT INTO user(name, age) VALUES(?, ?)"; + + List params = new ArrayList<>(); + + users.forEach(item -> { + params.add(new Object[] {item.getName(), item.getAge()}); + }); + jdbcTemplate.batchUpdate(sql, params); +} +``` + +### query 方法 + +查单个对象 + +```java +public User queryByName(String name) { + try { + return jdbcTemplate + .queryForObject("SELECT * FROM user WHERE name = ?", new BeanPropertyRowMapper<>(User.class), name); + } catch (EmptyResultDataAccessException e) { + return null; + } +} +``` + +查多个对象 + +```java +public List list() { + return jdbcTemplate.query("select * from USER", new BeanPropertyRowMapper(User.class)); +} +``` + +获取某个记录某列或者 count、avg、sum 等函数返回唯一值 + +```java +public Integer count() { + try { + return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM user", Integer.class); + } catch (EmptyResultDataAccessException e) { + return null; + } +} +``` + +## SpringBoot JDBC 配置 + +### JdbcTemplateAutoConfiguration 类 + +`JdbcTemplateAutoConfiguration` 是 `JdbcTemplate` 自动配置类,它负责实例化 `JdbcTemplate`。 + +```java +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ DataSource.class, JdbcTemplate.class }) +@ConditionalOnSingleCandidate(DataSource.class) +@AutoConfigureAfter(DataSourceAutoConfiguration.class) +@EnableConfigurationProperties(JdbcProperties.class) +@Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class }) +public class JdbcTemplateAutoConfiguration { + +} +``` + +`JdbcTemplateAutoConfiguration` 类的源码解读: + +- `@AutoConfigureAfter(DataSourceAutoConfiguration.class)` 表明 `JdbcTemplateAutoConfiguration` 必须在 `DataSourceAutoConfiguration` 执行完之后才开始工作,这意味着:`JdbcTemplate` 的初始化必须在 `DataSource` 初始化之后。 +- `JdbcProperties` 是 `JdbcTemplateAutoConfiguration` 的配置选项类,允许使用者通过设置选项控制 `JdbcTemplate` 初始化行为。 +- `@Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class })` 表明引入 `JdbcTemplateConfiguration`、`NamedParameterJdbcTemplateConfiguration` 两个配置类,具体的实例化 `JdbcTemplate` 的工作也是放在这两个配置中完成。 + +### JdbcTemplateConfiguration 类 + +`JdbcTemplateConfiguration` 源码如下: + +```java +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(JdbcOperations.class) +class JdbcTemplateConfiguration { + + @Bean + @Primary + JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + JdbcProperties.Template template = properties.getTemplate(); + jdbcTemplate.setFetchSize(template.getFetchSize()); + jdbcTemplate.setMaxRows(template.getMaxRows()); + if (template.getQueryTimeout() != null) { + jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds()); + } + return jdbcTemplate; + } + +} +``` + +`JdbcTemplateConfiguration` 源码解读:`JdbcTemplateConfiguration` 中根据 `DataSource` 和 `JdbcProperties` 实例化了一个 `JdbcTemplate`。 + +### NamedParameterJdbcTemplateConfiguration 类 + +`NamedParameterJdbcTemplateConfiguration` 源码如下: + +```java +@Configuration(proxyBeanMethods = false) +@ConditionalOnSingleCandidate(JdbcTemplate.class) +@ConditionalOnMissingBean(NamedParameterJdbcOperations.class) +class NamedParameterJdbcTemplateConfiguration { + + @Bean + @Primary + NamedParameterJdbcTemplate namedParameterJdbcTemplate(JdbcTemplate jdbcTemplate) { + return new NamedParameterJdbcTemplate(jdbcTemplate); + } + +} +``` + +`NamedParameterJdbcTemplateConfiguration` 源码解读:`NamedParameterJdbcTemplateConfiguration` 中根据 `JdbcTemplate` 实例化了一个 `NamedParameterJdbcTemplate`。 + +## spring-data-jdbc + +Spring Data 项目包含了对 JDBC 的存储库支持,并将自动为 `CrudRepository` 上的方法生成 SQL。对于更高级的查询,提供了 `@Query` 注解。 + +当 classpath 上存在必要的依赖项时,Spring Boot 将自动配置 Spring Data 的 JDBC 存储库。它们可以通过 `spring-boot-starter-data-jdbc` 的单一依赖项添加到项目中。如有必要,可以通过将 `@EnableJdbcRepositories` 批注或 `JdbcConfiguration` 子类添加到应用程序来控制 Spring Data JDBC 的配置。 + +> 更多 Spring Data JDBC 细节,可以参考 [Spring Data JDBC 官方文档](http://spring.io/projects/spring-data-jdbc)。 + +## 参考资料 + +- [Spring 官网](https://spring.io/) +- [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/03.Spring\344\271\213\344\272\213\345\212\241.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/03.Spring\344\271\213\344\272\213\345\212\241.md" new file mode 100644 index 00000000..5a9b29fe --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/03.Spring\344\271\213\344\272\213\345\212\241.md" @@ -0,0 +1,1272 @@ +--- +title: Spring 之事务 +date: 2022-09-22 07:46:49 +order: 03 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 事务 +permalink: /pages/128c54/ +--- + +# Spring 之事务 + +Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API(JPA) 等事务 API,实现了一致的编程模型,而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置,大多数 Spring Boot 项目只需要在方法上标记 `@Transactional` 注解,即可一键开启方法的事务性配置。 + +## 理解事务 + +在软件开发领域,全有或全无的操作被称为**事务(transaction)**。事务允许你将几个操作组合成一个要么全部发生要么全部不发生的工作单元。传统上 Java EE 开发对事务管理有两种选择:**全局事务**或**本地事务**,两者都有很大的局限性。 + +### 事务的特性 + +事务应该具有 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为 **ACID**。 + +- **原子性(Atomic)**:一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。 +- **一致性(Consistent)**:事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。 +- **隔离性(Isolated)**:一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 +- **持久性(Durable)**:持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。 + +### 全局事务 + +全局事务允许您使用多个事务资源,通常是关系数据库和消息队列。应用服务器通过 JTA 管理全局事务,这是一个繁琐的 API(部分原因在于其异常模型)。此外,JTA UserTransaction 通常需要来自 JNDI,这意味着您还需要使用 JNDI 才能使用 JTA。全局事务的使用限制了应用程序代码的任何潜在重用,因为 JTA 通常仅在应用程序服务器环境中可用。 + +以前,使用全局事务的首选方式是通过 EJB CMT(容器管理事务)。 CMT 是一种声明式事务管理(不同于程序化事务管理)。 EJB CMT 消除了对与事务相关的 JNDI 查找的需要,尽管使用 EJB 本身就需要使用 JNDI。它消除了大部分(但不是全部)编写 Java 代码来控制事务的需要。其明显的缺点是 CMT 与 JTA 和应用程序服务器环境相关联。此外,它仅在选择在 EJB 中实现业务逻辑(或至少在事务性 EJB 外观之后)时才可用。一般来说,EJB 的负面影响是如此之大,以至于这不是一个有吸引力的提议,尤其是在面对声明式事务管理的引人注目的替代方案时。 + +### 本地事务 + +本地事务是指定资源的,例如与 JDBC 连接关联的事务。本地事务可能更容易使用,但有一个明显的缺点:它们不能跨多个事务资源工作。例如,使用 JDBC 连接管理事务的代码不能在全局 JTA 事务中运行。因为应用服务器不参与事务管理,它不能确保跨多个资源的正确性(值得注意的是,大多数应用程序使用单个事务资源。)。另一个缺点是本地事务对编程模型具有侵入性。 + +### Spring 对事务的支持 + +Spring 通过回调机制将实际的事务实现从事务性的代码中抽象出来。Spring 解决了全局和本地事务的缺点。它允许开发人员在任何环境中使用一致的编程模型。您只需编写一次代码,它就可以从不同环境中的不同事务管理策略中受益。Spring 提供了对编码式和声明式事务管理的支持,大多数情况下都推荐使用声明式事务管理。 + +- 编码式事务允许用户在代码中精确定义事务的边界 +- 声明式事务(基于 AOP)有助于用户将操作与事务规则进行解耦 + +通过程序化事务管理,开发人员可以使用 Spring 事务抽象,它可以在任何底层事务基础上运行。使用首选的声明性模型,开发人员通常编写很少或根本不编写与事务管理相关的代码,因此不依赖 Spring 事务 API 或任何其他事务 API。 + +### Spring 事务的优点 + +Spring 框架为事务管理提供了一致的抽象,具有以下好处: + +- 跨不同事务 API 的一致编程模型,例如 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA)。 +- 支持声明式事务管理。 +- 用于编程事务管理的 API 比复杂事务 API(如 JTA)更简单。 +- 与 Spring 的数据访问抽象完美集成。 + +## 核心 API + +### TransactionManager + +Spring 事务抽象的关键是事务策略的概念。事务策略由 `TransactionManager` 定义,特别是用于命令式事务管理的 `org.springframework.transaction.PlatformTransactionManager` 接口和用于响应式事务管理的 `org.springframework.transaction.ReactiveTransactionManager` 接口。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220922073737.png) + +#### PlatformTransactionManager + +以下清单显示了 `PlatformTransactionManager` API 的定义: + +```java +public interface PlatformTransactionManager extends TransactionManager { + + TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; + + void commit(TransactionStatus status) throws TransactionException; + + void rollback(TransactionStatus status) throws TransactionException; +} +``` + +`PlatformTransactionManager` 是一个 SPI 接口,所以使用者可以以编程方式使用它。因为 `PlatformTransactionManager` 是一个接口,所以可以根据需要轻松地 MOCK 或存根。它不依赖于查找策略,例如 JNDI。 `PlatformTransactionManager` 实现的定义与 Spring IoC 容器中的任何其他对象(或 bean)一样。仅此一项优势就使 Spring 事务成为有价值的抽象,即使您使用 JTA 也是如此。与直接使用 JTA 相比,您可以更轻松地测试事务代码。 + +同样,为了与 Spring 的理念保持一致,任何 `PlatformTransactionManager` 接口的方法可以抛出的 `TransactionException` 都是未经检查的(也就是说,它扩展了 `java.lang.RuntimeException` 类)。事务架构故障几乎总是致命的。极少数情况下,应用程序可以从事务失败中恢复,开发人员可以选择捕获和处理 `TransactionException`。重点是开发人员并非被迫这样做。 + +`getTransaction(..)` 方法根据 `TransactionDefinition` 参数返回一个 `TransactionStatus` 对象。如果当前调用堆栈中存在匹配的事务,则返回的 `TransactionStatus` 可能表示新事务或可以表示现有事务。后一种情况的含义是,与 Java EE 事务上下文一样,`TransactionStatus` 与执行线程相关联。 + +从以上可以看出,具体的事务管理机制对 Spring 来说是透明的,它并不关心那些,那些是对应各个平台需要关心的,所以 Spring 事务管理的一个优点就是为不同的事务 API 提供一致的编程模型,如 JTA、JDBC、Hibernate、JPA。下面分别介绍各个平台框架实现事务管理的机制。 + +#### JDBC 事务 + +如果应用程序中直接使用 JDBC 来进行持久化,`DataSourceTransactionManager` 会为你处理事务边界。为了使用 `DataSourceTransactionManager`,你需要使用如下的 XML 将其装配到应用程序的上下文定义中: + +```xml + + + +``` + +实际上,`DataSourceTransactionManager` 是通过调用 `java.sql.Connection` 来管理事务,而后者是通过 `DataSource` 获取到的。通过调用连接的 `commit()` 方法来提交事务,同样,事务失败则通过调用 `rollback()` 方法进行回滚。 + +#### Hibernate 事务 + +如果应用程序的持久化是通过 Hibernate 实现的,那么你需要使用 `HibernateTransactionManager`。对于 Hibernate3,需要在 Spring 上下文定义中添加如下的 `bean` 声明: + +```xml + + + +``` + +`sessionFactory` 属性需要装配一个 Hibernate 的 session 工厂,`HibernateTransactionManager` 的实现细节是它将事务管理的职责委托给 `org.hibernate.Transaction` 对象,而后者是从 Hibernate Session 中获取到的。当事务成功完成时,`HibernateTransactionManager` 将会调用 `Transaction` 对象的 `commit()` 方法,反之,将会调用 `rollback()` 方法。 + +#### Java 持久化 API 事务(JPA) + +Hibernate 多年来一直是事实上的 Java 持久化标准,但是现在 Java 持久化 API 作为真正的 Java 持久化标准进入大家的视野。如果你计划使用 JPA 的话,那你需要使用 Spring 的 `JpaTransactionManager` 来处理事务。你需要在 Spring 中这样配置 `JpaTransactionManager`: + +```xml + + + +``` + +`JpaTransactionManager` 只需要装配一个 JPA 实体管理工厂(`javax.persistence.EntityManagerFactory` 接口的任意实现)。`JpaTransactionManager` 将与由工厂所产生的 JPA EntityManager 合作来构建事务。 + +#### Java 原生 API 事务(JTA) + +如果你没有使用以上所述的事务管理,或者是跨越了多个事务管理源(比如两个或者是多个不同的数据源),你就需要使用`JtaTransactionManager`: + +```xml + + + +``` + +`JtaTransactionManager` 将事务管理的责任委托给 `javax.transaction.UserTransaction` 和 `javax.transaction.TransactionManager` 对象,其中事务成功完成通过 `UserTransaction.commit()` 方法提交,事务失败通过 `UserTransaction.rollback()` 方法回滚。 + +#### ReactiveTransactionManager + +Spring 还为使用响应式类型或 Kotlin 协程的响应式应用程序提供了事务管理抽象。以下清单显示了 `org.springframework.transaction.ReactiveTransactionManager` 定义的事务策略: + +```java +public interface ReactiveTransactionManager extends TransactionManager { + + Mono getReactiveTransaction(TransactionDefinition definition) throws TransactionException; + + Mono commit(ReactiveTransaction status) throws TransactionException; + + Mono rollback(ReactiveTransaction status) throws TransactionException; +} +``` + +响应式事务管理器主要是一个 SPI,所以使用者可以以编程方式使用它。因为 `ReactiveTransactionManager` 是一个接口,所以可以根据需要轻松地 MOCK 或存根。 + +### TransactionDefinition + +`PlatformTransactionManager` 通过 `getTransaction(TransactionDefinition definition)` 方法来得到事务,这个方法里面的参数是 `TransactionDefinition` 类,这个类就定义了一些基本的事务属性。事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。 + +`TransactionDefinition` 接口内容如下: + +```java +public interface TransactionDefinition { + int getPropagationBehavior(); // 返回事务的传播行为 + int getIsolationLevel(); // 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据 + int getTimeout(); // 返回事务必须在多少秒内完成 + boolean isReadOnly(); // 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的 +} +``` + +我们可以发现 `TransactionDefinition` 正好用来定义事务属性,下面详细介绍一下各个事务属性。 + +#### 传播行为 + +事务的传播行为(propagation behavior)是指:当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring 定义了七种传播行为: + +| 传播行为 | 含义 | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PROPAGATION_REQUIRED` | 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务 | +| `PROPAGATION_SUPPORTS` | 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行 | +| `PROPAGATION_MANDATORY` | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 | +| `PROPAGATION_REQUIRED_NEW` | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用 JTATransactionManager 的话,则需要访问 TransactionManager | +| `PROPAGATION_NOT_SUPPORTED` | 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用 JTATransactionManager 的话,则需要访问 TransactionManager | +| `PROPAGATION_NEVER` | 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 | +| `PROPAGATION_NESTED` | 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与 PROPAGATION_REQUIRED 一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务 | + +_注:以下具体讲解传播行为的内容参考自 Spring 事务机制详解_ + +1. PROPAGATION_REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。 + +```java +// 事务属性 PROPAGATION_REQUIRED +methodA { + …… + methodB(); + …… +} +``` + +```java +// 事务属性 PROPAGATION_REQUIRED +methodB { + …… +} +``` + +使用 spring 声明式事务,spring 使用 AOP 来支持声明式事务,会根据事务属性,自动在方法调用之前决定是否开启一个事务,并在方法执行之后决定事务提交或回滚事务。 + +单独调用 methodB 方法: + +```java +main { + metodB(); +} +``` + +相当于 + +```java +Main { + Connection con=null; + try{ + con = getConnection(); + con.setAutoCommit(false); + + //方法调用 + methodB(); + + //提交事务 + con.commit(); + } Catch(RuntimeException ex) { + //回滚事务 + con.rollback(); + } finally { + //释放资源 + closeCon(); + } +} +``` + +Spring 保证在 methodB 方法中所有的调用都获得到一个相同的连接。在调用 methodB 时,没有一个存在的事务,所以获得一个新的连接,开启了一个新的事务。 +单独调用 MethodA 时,在 MethodA 内又会调用 MethodB. + +执行效果相当于: + +```java +main{ + Connection con = null; + try{ + con = getConnection(); + methodA(); + con.commit(); + } catch(RuntimeException ex) { + con.rollback(); + } finally { + closeCon(); + } +} +``` + +调用 MethodA 时,环境中没有事务,所以开启一个新的事务.当在 MethodA 中调用 MethodB 时,环境中已经有了一个事务,所以 methodB 就加入当前事务。 + +2. `PROPAGATION_SUPPORTS` 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,`PROPAGATION_SUPPORTS` 与不使用事务有少许不同。 + +```java +//事务属性 PROPAGATION_REQUIRED +methodA(){ + methodB(); +} + +//事务属性 PROPAGATION_SUPPORTS +methodB(){ + …… +} +``` + +单纯的调用 methodB 时,methodB 方法是非事务的执行的。当调用 methdA 时,methodB 则加入了 methodA 的事务中,事务地执行。 + +3. `PROPAGATION_MANDATORY` 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。 + +``` +//事务属性 PROPAGATION_REQUIRED +methodA(){ + methodB(); +} + +//事务属性 PROPAGATION_MANDATORY + methodB(){ + …… +} +``` + +当单独调用 methodB 时,因为当前没有一个活动的事务,则会抛出异常 throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);当调用 methodA 时,methodB 则加入到 methodA 的事务中,事务地执行。 + +4. `PROPAGATION_REQUIRES_NEW` 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。 + +``` +//事务属性 PROPAGATION_REQUIRED +methodA(){ + doSomeThingA(); + methodB(); + doSomeThingB(); +} + +//事务属性 PROPAGATION_REQUIRES_NEW +methodB(){ + …… +} +``` + +调用 A 方法: + +``` +main(){ + methodA(); +} +``` + +相当于 + +``` +main(){ + TransactionManager tm = null; + try{ + //获得一个JTA事务管理器 + tm = getTransactionManager(); + tm.begin();//开启一个新的事务 + Transaction ts1 = tm.getTransaction(); + doSomeThing(); + tm.suspend();//挂起当前事务 + try{ + tm.begin();//重新开启第二个事务 + Transaction ts2 = tm.getTransaction(); + methodB(); + ts2.commit();//提交第二个事务 + } Catch(RunTimeException ex) { + ts2.rollback();//回滚第二个事务 + } finally { + //释放资源 + } + //methodB执行完后,恢复第一个事务 + tm.resume(ts1); + doSomeThingB(); + ts1.commit();//提交第一个事务 + } catch(RunTimeException ex) { + ts1.rollback();//回滚第一个事务 + } finally { + //释放资源 + } +} +``` + +在这里,我把 ts1 称为外层事务,ts2 称为内层事务。从上面的代码可以看出,ts2 与 ts1 是两个独立的事务,互不相干。Ts2 是否成功并不依赖于 ts1。如果 methodA 方法在调用 methodB 方法后的 doSomeThingB 方法失败了,而 methodB 方法所做的结果依然被提交。而除了 methodB 之外的其它代码导致的结果却被回滚了。使用 PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager 作为事务管理器。 + +5. `PROPAGATION_NOT_SUPPORTED` 总是非事务地执行,并挂起任何存在的事务。使用 PROPAGATION_NOT_SUPPORTED,也需要使用 JtaTransactionManager 作为事务管理器。(代码示例同上,可同理推出) +6. PROPAGATION_NEVER 总是非事务地执行,如果存在一个活动事务,则抛出异常。 +7. PROPAGATION_NESTED 如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按 TransactionDefinition.PROPAGATION_REQUIRED 属性执行。这是一个嵌套事务,使用 JDBC 3.0 驱动时,仅仅支持 DataSourceTransactionManager 作为事务管理器。需要 JDBC 驱动的 java.sql.Savepoint 类。有一些 JTA 的事务管理器实现可能也提供了同样的功能。使用 PROPAGATION_NESTED,还需要把 PlatformTransactionManager 的 nestedTransactionAllowed 属性设为 true;而 nestedTransactionAllowed 属性值默认为 false。 + +``` +//事务属性 PROPAGATION_REQUIRED +methodA(){ + doSomeThingA(); + methodB(); + doSomeThingB(); +} + +//事务属性 PROPAGATION_NESTED +methodB(){ + …… +} +``` + +如果单独调用 methodB 方法,则按 REQUIRED 属性执行。如果调用 methodA 方法,相当于下面的效果: + +``` +main(){ + Connection con = null; + Savepoint savepoint = null; + try{ + con = getConnection(); + con.setAutoCommit(false); + doSomeThingA(); + savepoint = con2.setSavepoint(); + try{ + methodB(); + } catch(RuntimeException ex) { + con.rollback(savepoint); + } finally { + //释放资源 + } + doSomeThingB(); + con.commit(); + } catch(RuntimeException ex) { + con.rollback(); + } finally { + //释放资源 + } +} +``` + +当 methodB 方法调用之前,调用 setSavepoint 方法,保存当前的状态到 savepoint。如果 methodB 方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码(doSomeThingB()方法)调用失败,则回滚包括 methodB 方法的所有操作。 + +嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。 + +PROPAGATION_NESTED 与 PROPAGATION_REQUIRES_NEW 的区别:它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。使用 PROPAGATION_REQUIRES_NEW 时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要 JTA 事务管理器的支持。 + +使用 PROPAGATION_NESTED 时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager 使用 savepoint 支持 PROPAGATION_NESTED 时,需要 JDBC 3.0 以上驱动及 1.4 以上的 JDK 版本支持。其它的 JTA TrasactionManager 实现可能有不同的支持方式。 + +PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。 + +另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。 + +由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back. + +PROPAGATION_REQUIRED 应该是我们首先的事务传播行为。它能够满足我们大多数的事务需求。 + +#### 隔离级别 + +事务的第二个维度就是隔离级别(isolation level)。隔离级别定义了一个事务可能受其他并发事务影响的程度。 + +1. 并发事务引起的问题 + +在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务。并发虽然是必须的,但可能会导致一下的问题。 + +- 脏读(Dirty reads)——脏读发生在一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。 +- 不可重复读(Nonrepeatable read)——不可重复读发生在一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间进行了更新。 +- 幻读(Phantom read)——幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。 + +**不可重复读与幻读的区别** + +不可重复读的重点是修改: +同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 +例如:在事务 1 中,Mary 读取了自己的工资为 1000,操作并没有完成 + +``` + con1 = getConnection(); + select salary from employee empId ="Mary"; +``` + +在事务 2 中,这时财务人员修改了 Mary 的工资为 2000,并提交了事务. + +``` + con2 = getConnection(); + update employee set salary = 2000; + con2.commit(); +``` + +在事务 1 中,Mary 再次读取自己的工资时,工资变为了 2000 + +``` + //con1 + select salary from employee empId ="Mary"; +``` + +在一个事务中前后两次读取的结果并不一致,导致了不可重复读。 + +幻读的重点在于新增或者删除: +同样的条件, 第 1 次和第 2 次读出来的记录数不一样 +例如:目前工资为 1000 的员工有 10 人。事务 1,读取所有工资为 1000 的员工。 + +``` + con1 = getConnection(); + Select * from employee where salary =1000; +``` + +共读取 10 条记录 + +这时另一个事务向 employee 表插入了一条员工记录,工资也为 1000 + +``` + con2 = getConnection(); + Insert into employee(empId,salary) values("Lili",1000); + con2.commit(); +``` + +事务 1 再次读取所有工资为 1000 的员工 + +``` + //con1 + select * from employee where salary =1000; +``` + +共读取到了 11 条记录,这就产生了幻像读。 + +从总的结果来看, 似乎不可重复读和幻读都表现为两次读取的结果不一致。但如果你从控制的角度来看, 两者的区别就比较大。 +对于前者, 只需要锁住满足条件的记录。 +对于后者, 要锁住满足条件及其相近的记录。 + +2. 隔离级别 + +| 隔离级别 | 含义 | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 | +| ISOLATION_READ_UNCOMMITTED | 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 | +| ISOLATION_READ_COMMITTED | 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 | +| ISOLATION_REPEATABLE_READ | 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生 | +| ISOLATION_SERIALIZABLE | 最高的隔离级别,完全服从 ACID 的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的 | + +#### 只读 + +事务的第三个特性是它是否为只读事务。如果事务只对后端的数据库进行该操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读,你就可以给数据库一个机会,让它应用它认为合适的优化措施。 + +#### 事务超时 + +为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。 + +#### 回滚规则 + +事务五边形的最后一个方面是一组规则,这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚(这一行为与 EJB 的回滚行为是一致的) +但是你可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。同样,你还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。 + +### TransactionStatus + +`TransactionStatus` 接口为事务代码提供了一种简单的方式来控制事务执行和查询事务状态。这些概念应该很熟悉,因为它们对所有事务 API 都是通用的。以下清单显示了 `TransactionStatus` 接口: + +```java +public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable { + + @Override + boolean isNewTransaction(); + + boolean hasSavepoint(); + + @Override + void setRollbackOnly(); + + @Override + boolean isRollbackOnly(); + + void flush(); + + @Override + boolean isCompleted(); +} +``` + +可以发现这个接口描述的是一些处理事务提供简单的控制事务执行和查询事务状态的方法,在回滚或提交的时候需要应用对应的事务状态。 + +### TransactionTemplate + +Spring 提供了对编程式事务和声明式事务的支持。编程式事务允许用户在代码中精确定义事务的边界,而声明式事务(基于 AOP)有助于用户将操作与事务规则进行解耦。TransactionTemplate 就是用于支持编程式事务的核心 API。 + +采用 TransactionTemplate 和采用其他 Spring 模板,如 JdbcTempalte 和 HibernateTemplate 是一样的方法。它使用回调方法,把应用程序从处理取得和释放资源中解脱出来。如同其他模板,TransactionTemplate 是线程安全的。代码片段: + +```java + TransactionTemplate tt = new TransactionTemplate(); // 新建一个TransactionTemplate + Object result = tt.execute( + new TransactionCallback(){ + public Object doTransaction(TransactionStatus status){ + updateOperation(); + return resultOfUpdateOperation(); + } + }); // 执行execute方法进行事务管理 +``` + +使用 TransactionCallback()可以返回一个值。如果使用 TransactionCallbackWithoutResult 则没有返回值。 + +## 声明式事务管理 + +> 大多数 Spring 用户选择声明式事务管理。此选项对应用程序代码的影响最小,因此最符合非侵入式轻量级容器的理想。 + +Spring 框架的声明式事务管理是通过 Spring AOP 实现的。然而,由于事务方面代码随 Spring 发行版一起提供并且可以以样板方式使用,因此通常不必理解 AOP 概念即可有效地使用此代码。 + +Spring 框架的声明式事务管理类似于 EJB CMT,因为您可以指定事务行为(或缺少它)到单个方法级别。如有必要,您可以在事务上下文中进行 `setRollbackOnly()` 调用。两种类型的事务管理之间的区别是: + +- 与绑定到 JTA 的 EJB CMT 不同,Spring 框架的声明式事务管理适用于任何环境。通过调整配置文件,它可以使用 JDBC、JPA 或 Hibernate 处理 JTA 事务或本地事务。 +- 您可以将 Spring 声明式事务管理应用于任何类,而不仅仅是诸如 EJB 之类的特殊类。 +- Spring 提供声明性回滚规则,这是一个没有 EJB 等效功能的特性。提供了对回滚规则的编程和声明性支持。 +- Spring 允许您使用 AOP 自定义事务行为。例如,您可以在事务回滚的情况下插入自定义行为。您还可以添加任意 advice 以及事务性 advice。使用 EJB CMT,您无法影响容器的事务管理,除非使用 `setRollbackOnly()`。 +- Spring 不像高端应用服务器那样支持跨远程调用传播事务上下文。如果您需要此功能,我们建议您使用 EJB。但是,在使用这种特性之前要仔细考虑,因为通常情况下,不希望事务跨越远程调用。 + +回滚规则的概念很重要。它们让您指定哪些异常(和 throwable)应该导致自动回滚。您可以在配置中以声明方式指定它,而不是在 Java 代码中。因此,尽管您仍然可以在 TransactionStatus 对象上调用 setRollbackOnly() 来回滚当前事务,但通常您可以指定 MyApplicationException 必须始终导致回滚的规则。此选项的显着优势是业务对象不依赖于事务基础架构。例如,它们通常不需要导入 Spring 事务 API 或其他 Spring API。 + +尽管 EJB 容器默认行为会在系统异常(通常是运行时异常)上自动回滚事务,但 EJB CMT 不会在应用程序异常(即除 java.rmi.RemoteException 之外的检查异常)上自动回滚事务。虽然声明式事务管理的 Spring 默认行为遵循 EJB 约定(回滚仅在未经检查的异常上自动),但自定义此行为通常很有用。 + +### Spring 声明式事务管理的实现 + +关于 Spring 框架的声明式事务支持,最重要的概念是这种支持是通过 AOP 代理启用的,并且事务 advice 是由元数据驱动的(目前是基于 XML 或基于注释的)。 AOP 与事务元数据的结合产生了一个 AOP 代理,它使用 `TransactionInterceptor` 和适当的 `TransactionManager` 实现来驱动围绕方法调用的事务。 + +Spring 的 `TransactionInterceptor` 为命令式和响应式编程模型提供事务管理。拦截器通过检查方法返回类型来检测所需的事务管理风格。返回响应式类型的方法,例如 Publisher 或 Kotlin Flow(或它们的子类型)有资格进行响应式事务管理。包括 void 在内的所有其他返回类型都使用代码路径进行命令式事务管理。 + +事务管理风格会影响需要哪个事务管理器。命令式事务需要 `PlatformTransactionManager`,而响应式事务使用 `ReactiveTransactionManager` 实现。 + +> `@Transactional` 通常与 `PlatformTransactionManager` 管理的线程绑定事务一起使用,将事务公开给当前执行线程中的所有数据访问操作。注意:这不会传播到方法中新启动的线程。 +> +> 由 `ReactiveTransactionManager` 管理的反应式事务使用 Reactor 上下文而不是线程本地属性。因此,所有参与的数据访问操作都需要在同一个反应式管道中的同一个 Reactor 上下文中执行。 + +下图显示了在事务代理上调用方法的概念视图: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220927093737.png) + +### 声明式事务示例 + +考虑以下接口及其伴随的实现。此示例使用 Foo 和 Bar 类作为占位符,以便您可以专注于事务使用,而无需关注特定的域模型。就本示例而言,DefaultFooService 类在每个已实现方法的主体中抛出 `UnsupportedOperationException` 实例这一事实很好。该行为使您可以看到正在创建的事务,然后回滚以响应 `UnsupportedOperationException` 实例。 + +以下清单显示了 FooService 接口: + +```java +// the service interface that we want to make transactional + +package x.y.service; + +public interface FooService { + + Foo getFoo(String fooName); + + Foo getFoo(String fooName, String barName); + + void insertFoo(Foo foo); + + void updateFoo(Foo foo); + +} +``` + +以下示例显示了上述接口的实现: + +```java +package x.y.service; + +public class DefaultFooService implements FooService { + + @Override + public Foo getFoo(String fooName) { + // ... + } + + @Override + public Foo getFoo(String fooName, String barName) { + // ... + } + + @Override + public void insertFoo(Foo foo) { + // ... + } + + @Override + public void updateFoo(Foo foo) { + // ... + } +} +``` + +假设 FooService 接口的前两个方法 getFoo(String) 和 getFoo(String, String) 必须在具有只读语义的事务上下文中运行,并且其他方法 insertFoo(Foo) 和 updateFoo(Foo ),必须在具有读写语义的事务上下文中运行。以下配置将在接下来的几段中详细说明: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +检查前面的配置。它假定您要使服务对象 fooService bean 具有事务性。要应用的事务语义封装在 `` 定义中。`` 定义读作“所有以 get 开头的方法都将在只读事务的上下文中运行,所有其他方法都将以默认事务语义运行”。`` 标签的 `transaction-manager` 属性设置为将驱动事务的 TransactionManager bean 的名称(在本例中为 txManager bean)。 + +> 如果要连接的 TransactionManager 的 bean 名称具有名称 transactionManager,则可以省略事务 advice (tx:advice/) 中的 transaction-manager 属性。如果要连接的 TransactionManager bean 有任何其他名称,则必须显式使用 transaction-manager 属性,如前面的示例所示。 + +`` 定义确保由 `txAdvice` bean 定义的事务性建议在程序中的适当位置运行。首先,您定义一个切入点,该切入点与 `FooService` 接口 (fooServiceOperation) 中定义的任何操作的执行相匹配。然后,您使用一个 adviser 将切入点与 `txAdvice` 相关联。结果表明,在执行 fooServiceOperation 时,会运行 `txAdvice` 定义的建议。 + +一个常见的要求是使整个服务层具有事务性。最好的方法是更改切入点表达式以匹配服务层中的任何操作。以下示例显示了如何执行此操作: + +```xml + + + + +``` + +前面显示的配置用于围绕从 fooService bean 定义创建的对象创建事务代理。代理配置了事务 advice,以便在代理上调用适当的方法时,根据与该方法关联的事务配置,启动、暂停、标记为只读等事务。考虑以下测试驱动前面显示的配置的程序: + +```java +public final class Boot { + + public static void main(final String[] args) throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml"); + FooService fooService = ctx.getBean(FooService.class); + fooService.insertFoo(new Foo()); + } +} +``` + +### 回滚一个声明性事务 + +Spring 框架中,触发事务回滚的推荐方式是在事务上下文的代码中抛出异常。Spring 事务框架会捕获任何未处理的异常,并确定是否将事务标记为回滚。 + +在其默认配置中,Spring 事务框架只会将存在运行时且未经检查异常的事务标记为回滚。也就是说,当抛出的异常是 `RuntimeException` 的实例或子类时。 (默认情况下,错误实例也会导致回滚)。从事务方法抛出的检查异常不会导致默认配置中的回滚。 + +您可以通过指定回滚规则,明确指定哪些异常类型将导致事务回滚。 + +> 回滚规则约定在抛出指定异常时是否应回滚事务,并且规则基于模式。模式可以是完全限定的类名或异常类型的完全限定类名的子字符串(必须是 `Throwable` 的子类),目前不支持通配符。例如,`javax.servlet.ServletException` 或 `ServletException` 的值将匹配 `javax.servlet.ServletException` 及其子类。 +> +> 回滚规则可以通过 `rollback-for` 和 `no-rollback-for` 属性在 XML 中配置,这允许将模式指定为字符串。使用 `@Transactional` 时,可以通过 `rollbackFor` / `noRollbackFor` 和`rollbackForClassName` / `noRollbackForClassName` 属性配置回滚规则,它们允许将模式分别指定为类引用或字符串。当异常类型被指定为类引用时,其完全限定名称将用作模式。因此,`@Transactional(rollbackFor = example.CustomException.class)` 等价于 `@Transactional(rollbackForClassName = 'example.CustomException')`。 + +以下 XML 片段演示了如何通过 `rollback-for` 属性提供异常模式来为已检查的、特定的 `Exception` 类型配置回滚: + +```xml + + + + + + +``` + +如果您不希望在抛出异常时回滚事务,您还可以指定“不回滚”规则。下面的例子告诉 Spring 事务框架,即使在面对未处理的 InstrumentNotFoundException 时也要提交伴随事务。 + +```xml + + + + + + +``` + +当 Spring Framework 事务框架捕获到异常,并检查配置的回滚规则以确定是否将事务标记为回滚时,由最重要的匹配规则决定。因此,在以下配置的情况下,除 `InstrumentNotFoundException` 之外的任何异常都会导致伴随事务的回滚。 + +```xml + + + + + +``` + +您还可以以编程方式指示所需的回滚。虽然很简单,但这个过程非常具有侵入性,并且将您的代码与 Spring Framework 的事务基础设施紧密耦合。以下示例显示如何以编程方式指示所需的回滚。 + +```java +public void resolvePosition() { + try { + // some business logic... + } catch (NoProductInStockException ex) { + // trigger rollback programmatically + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + } +} +``` + +如果可能的话,强烈建议您使用声明性方法进行回滚。如果您绝对需要,可以使用程序化回滚,但它的使用与实现干净的基于 POJO 的架构背道而驰。 + +### 为不同的 Bean 配置不同的事务语义 + +考虑您有许多服务层对象的场景,并且您希望对每个对象应用完全不同的事务配置。您可以通过定义具有不同 `` 元素和不同 `advice-ref` 属性值的切点来实现这一点。 + +作为一个比较点,首先假设您的所有服务层类都定义在根 x.y.service 包中。 要使作为该包(或子包)中定义的类的实例并且名称以 Service 结尾的所有 bean 都具有默认的事务配置,您可以编写以下内容: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +以下示例显示了如何使用完全不同的事务设置配置两个不同的 bean + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### `` 配置 + +`` 的默认配置为: + +- 传播设置是 `REQUIRED` + +- 隔离级别为 `DEFAULT` + +- 事务是 read-write + +- 事务超时默认为底层事务系统的默认超时,如果不支持超时,则为无。 + +- 任何 `RuntimeException` 都会触发回滚,而任何已检查的 `Exception` 都不会 + +`` 配置属性 + +| 属性 | 是否必要 | 默认值 | 描述 | +| :---------------- | :------- | :--------- | :--------------------------------------------------------------------------- | +| `name` | Yes | | 与事务属性关联的方法名称。支持通配符,如:`get*`、`handle*`、`on*Event` | +| `propagation` | No | `REQUIRED` | 事务传播行为 | +| `isolation` | No | `DEFAULT` | 事务隔离级别。仅适用于 `REQUIRED` 或 `REQUIRES_NEW` 的传播设置。 | +| `timeout` | No | -1 | 事务超时时间(单位:秒)。仅适用于 `REQUIRED` 或 `REQUIRES_NEW` 的传播设置。 | +| `read-only` | No | false | read-write 或 read-only 事务。 | +| `rollback-for` | No | | 触发回滚的 `Exception` 实例列表(通过逗号分隔)。 | +| `no-rollback-for` | No | | 不触发回滚的 `Exception` 实例列表(通过逗号分隔)。 | + +### 使用 `@Transactional` 注解 + +除了基于 XML 的声明式事务配置方法之外,您还可以使用基于注解的方法。 + +下面是一个使用 `@Transactional` 注解的示例: + +```java +@Transactional +public class DefaultFooService implements FooService { + + @Override + public Foo getFoo(String fooName) { + // ... + } + + @Override + public Foo getFoo(String fooName, String barName) { + // ... + } + + @Override + public void insertFoo(Foo foo) { + // ... + } + + @Override + public void updateFoo(Foo foo) { + // ... + } +} +``` + +如上所述在类级别使用,`@Transactional` 注解表明声明类(及其子类)的所有方法都使用默认事务配置。 或者,可以单独为每个方法指定注解。请注意,类级别的注解不适用于类层次结构中的祖先类; 在这种情况下,继承的方法需要在本地重新声明才能参与子类级别的注解。 + +当上面的 POJO 类在 Spring 上下文中定义为 bean 时,您可以通过 `@Configuration` 类中的 `@EnableTransactionManagement` 注解使 bean 实例具有事务性。 + +在 XML 配置中, `` 标签提供了类似的便利: + +```xml + + + + + + + + + + + + + + + + + + + +``` + +#### `@Transactional` 配置 + +| Property | Type | Description | +| :------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------- | +| [value](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-multiple-tx-mgrs-with-attransactional) | `String` | Optional qualifier that specifies the transaction manager to be used. | +| `transactionManager` | `String` | Alias for `value`. | +| `label` | Array of `String` labels to add an expressive description to the transaction. | Labels may be evaluated by transaction managers to associate implementation-specific behavior with the actual transaction. | +| [propagation](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-propagation) | `enum`: `Propagation` | Optional propagation setting. | +| `isolation` | `enum`: `Isolation` | Optional isolation level. Applies only to propagation values of `REQUIRED` or `REQUIRES_NEW`. | +| `timeout` | `int` (in seconds of granularity) | Optional transaction timeout. Applies only to propagation values of `REQUIRED` or `REQUIRES_NEW`. | +| `timeoutString` | `String` (in seconds of granularity) | Alternative for specifying the `timeout` in seconds as a `String` value — for example, as a placeholder. | +| `readOnly` | `boolean` | Read-write versus read-only transaction. Only applicable to values of `REQUIRED` or `REQUIRES_NEW`. | +| `rollbackFor` | Array of `Class` objects, which must be derived from `Throwable.` | Optional array of exception types that must cause rollback. | +| `rollbackForClassName` | Array of exception name patterns. | Optional array of exception name patterns that must cause rollback. | +| `noRollbackFor` | Array of `Class` objects, which must be derived from `Throwable.` | Optional array of exception types that must not cause rollback. | +| `noRollbackForClassName` | Array of exception name patterns. | Optional array of exception name patterns that must not cause rollback. | + +#### 多事务管理器场景下使用 `@Transactional` + +某些情况下,应用程序中可能需要接入多个数据源,相应的,也需要多个独立的事务管理器。使用者可以使用 `@Transactional` 注释的 value 或 `transactionManager` 属性来选择性地指定要使用的 `TransactionManager` 的标识。这可以是 bean 名称或事务管理器 bean 的限定符值。 + +```java +public class TransactionalService { + + @Transactional("order") + public void setSomething(String name) { ... } + + @Transactional("account") + public void doSomething() { ... } + + @Transactional("reactive-account") + public Mono doSomethingReactive() { ... } +} +``` + +下面展示如何定义 `TransactionManager`: + +```xml + + + + ... + + + + + ... + + + + + ... + + +``` + +在这种情况下,`TransactionalService` 上的各个方法在单独的事务管理器下运行,由 order、account 和 reactive-account 限定符区分。 如果没有找到明确指定的 `TransactionManager` bean,则仍使用默认的 `` 目标 bean 名称。 + +#### 自定义组合注解 + +如果您发现在许多不同的方法上重复使用 `@Transactional` 相同的属性,可以使用 Spring 的元注解自定义组合注解。 + +```java +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Transactional(transactionManager = "order", label = "causal-consistency") +public @interface OrderTx { +} + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Transactional(transactionManager = "account", label = "retryable") +public @interface AccountTx { +} +``` + +使用示例: + +```java +public class TransactionalService { + + @OrderTx + public void setSomething(String name) { + // ... + } + + @AccountTx + public void doSomething() { + // ... + } +} +``` + +在上面的示例中,我们使用语法来定义事务管理器限定符和事务标签,但我们也可以包括传播行为、回滚规则、超时和其他特性。 + +#### 事务传播 + +在 Spring 管理的事务中,请注意物理事务和逻辑事务之间的差异,以及传播设置如何应用于这种差异。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220928114544.png) + +`PROPAGATION_REQUIRED` 强制执行物理事务,如果尚不存在事务,则在当前范围的本地执行或参与更大范围定义的现有“外部”事务。 这是同一线程内的常见调用堆栈安排中的一个很好的默认设置(例如,委托给多个存储库方法的服务外观,其中所有底层资源都必须参与服务级事务)。 + +当传播设置为 PROPAGATION_REQUIRED 时,将为应用该设置的每个方法创建一个逻辑事务范围。每个这样的逻辑事务范围可以单独确定仅回滚状态,外部事务范围在逻辑上独立于内部事务范围。在标准 PROPAGATION_REQUIRED 行为的情况下,所有这些范围都映射到同一个物理事务。因此,在内部事务范围内设置的仅回滚标记确实会影响外部事务实际提交的机会。 + +但是,在内部事务范围设置了仅回滚标记的情况下,外部事务尚未决定回滚本身,因此回滚(由内部事务范围静默触发)是意外的。此时会引发相应的 `UnexpectedRollbackException`。这是预期的行为,因此事务的调用者永远不会被误导以为执行了提交,而实际上并没有执行。因此,如果内部事务(外部调用者不知道)默默地将事务标记为仅回滚,外部调用者仍会调用提交。外部调用者需要接收 `UnexpectedRollbackException` 以清楚地指示执行了回滚。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220928115243.png) + +PROPAGATION_REQUIRES_NEW 与 PROPAGATION_REQUIRED 相比,始终为每个受影响的事务范围使用独立的物理事务,从不参与外部范围的现有事务。 在这种安排下,底层资源事务是不同的,因此可以独立提交或回滚,外部事务不受内部事务回滚状态的影响,内部事务的锁在完成后立即释放。 这样一个独立的内部事务也可以声明自己的隔离级别、超时和只读设置,而不是继承外部事务的特性。 + +## JDBC 异常抽象 + +Spring 会将数据操作的异常转换为 `DataAccessException`。 + +Spring 是怎么认识那些错误码的 + +通过 SQLErrorCodeSQLExceptionTranslator 解析错误码 + +ErrorCode 定义(sql-error-codes.xml 文件) + +## Spring 事务最佳实践 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200805171418.png) + +### Spring 事务未生效 + +使用 `@Transactional` 注解开启声明式事务时, 最容易忽略的问题是,很可能事务并没有生效。 + +`@Transactional` 生效原则: + +#### @Transactional 方法必须是 public + +原则一:除非特殊配置(比如使用 AspectJ 静态织入实现 AOP),否则**只有定义在 `public` 方法上的 `@Transactional` 才能生效**。原因是,Spring 默认通过动态代理的方式实现 AOP,对目标方法进行增强,private 方法无法代理到,Spring 自然也无法动态增强事务处理逻辑。 + +【示例】错误使用 `@Transactional` 案例一 + +```java + @Transactional + void createUserPrivate(UserEntity entity) { + userRepository.save(entity); + if (entity.getName().contains("test")) { throw new RuntimeException("invalid username!"); } + } + + //私有方法 + public int createUserWrong1(String name) { + try { + this.createUserPrivate(new UserEntity(name)); + } catch (Exception ex) { + log.error("create user failed because {}", ex.getMessage()); + } + return userRepository.findByName(name).size(); + } +``` + +当传入名为 test 的用户实体,会抛出异常,但 `@Transactional` 未生效,不会触发回滚。 + +#### 必须通过 Spring 注入的 Bean 进行调用 + +原则二:**必须通过代理过的类从外部调用目标方法才能生效**。 + +【示例】错误使用 `@Transactional` 案例二 + +```java + //自调用 + public int createUserWrong2(String name) { + try { + this.createUserPublic(new UserEntity(name)); + } catch (Exception ex) { + log.error("create user failed because {}", ex.getMessage()); + } + return userRepository.findByName(name).size(); + } + + //可以传播出异常 + @Transactional + public void createUserPublic(UserEntity entity) { + userRepository.save(entity); + if (entity.getName().contains("test")) { throw new RuntimeException("invalid username!"); } + } +``` + +当传入名为 test 的用户实体,会抛出异常,但 `@Transactional` 未生效,不会触发回滚。 + +说明:Spring 通过 AOP 技术对方法进行字节码增强,要调用增强过的方法必然是调用代理后的对象。 + +### 事务虽然生效但未回滚 + +通过 AOP 实现事务处理可以理解为,使用 `try…catch…` 来包裹标记了 `@Transactional` 注解的方法,当方法出现了异常并且满足**一定条件**的时候,在 `catch` 里面我们可以设置事务回滚,没有异常则直接提交事务。 + +“一定条件”,主要包括两点: + +第一,只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚。在 Spring 的 TransactionAspectSupport 里有个 invokeWithinTransaction 方法,里面就是处理事务的逻辑。 + +第二,默认情况下,**出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务**。 + +```java +@Service +@Slf4j +public class UserService { + + @Autowired + private UserRepository userRepository; + + //异常无法传播出方法,导致事务无法回滚 + @Transactional + public void createUserWrong1(String name) { + try { + userRepository.save(new UserEntity(name)); + throw new RuntimeException("error"); + } catch (Exception ex) { + log.error("create user failed", ex); + } + } + + //即使出了受检异常也无法让事务回滚 + @Transactional + public void createUserWrong2(String name) throws IOException { + userRepository.save(new UserEntity(name)); + otherTask(); + } + + //因为文件不存在,一定会抛出一个IOException + private void otherTask() throws IOException { + Files.readAllLines(Paths.get("file-that-not-exist")); + } + +} +``` + +在 createUserWrong1 方法中会抛出一个 RuntimeException,但由于方法内 catch 了所有异常,异常无法从方法传播出去,事务自然无法回滚。 + +在 createUserWrong2 方法中,注册用户的同时会有一次 otherTask 文件读取操作,如果文件读取失败,我们希望用户注册的数据库操作回滚。虽然这里没有捕获异常,但因为 otherTask 方法抛出的是受检异常,createUserWrong2 传播出去的也是受检异常,事务同样不会回滚。 + +【解决方案一】如果你希望自己捕获异常进行处理的话,也没关系,**可以手动设置 `TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();` 让当前事务处于回滚状态**: + +```java +@Transactional +public void createUserRight1(String name) { + try { + userRepository.save(new UserEntity(name)); + throw new RuntimeException("error"); + } catch (Exception ex) { + log.error("create user failed", ex); + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + } +} +``` + +【解决方案二】在注解中声明 `@Transactional(rollbackFor = Exception.class)`,期望遇到所有的 Exception 都回滚事务(来突破默认不回滚受检异常的限制): + +```java +@Transactional(rollbackFor = Exception.class) +public void createUserRight2(String name) throws IOException { + userRepository.save(new UserEntity(name)); + otherTask(); +} +``` + +### 细化事务传播方式 + +如果方法涉及多次数据库操作,并希望将它们作为独立的事务进行提交或回滚,那么 +我们需要考虑进一步细化配置事务传播方式,也就是 `@Transactional` 注解的 `Propagation` 属性。 + +```java +/** + * {@link Propagation#REQUIRES_NEW} 表示执行到这个方法时需要开启新的事务,并挂起当前事务 + */ +@Transactional(propagation = Propagation.REQUIRES_NEW) +public void createSubUserWithExceptionRight(UserEntity entity) { + log.info("createSubUserWithExceptionRight start"); + userRepository.save(entity); + throw new RuntimeException("invalid status"); +} +``` + +## 参考资料 + +- [Spring 官网](https://spring.io/) +- [Spring 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html) +- [《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/04.Spring\344\271\213JPA.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/04.Spring\344\271\213JPA.md" new file mode 100644 index 00000000..8d362789 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/04.Spring\344\271\213JPA.md" @@ -0,0 +1,609 @@ +--- +title: Spring 之 JPA +date: 2019-02-18 14:33:55 +order: 04 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - JPA +permalink: /pages/a03d7b/ +--- + +# Spring 之 JPA + +JPA 为对象关系映射提供了一种基于 POJO 的持久化模型。 + +- 简化数据持久化代码的开发 +- 为 Java 社区屏蔽不同持久化 API 的差异 + +## 快速入门 + +(1)在 pom.xml 中引入依赖 + +```xml + + org.springframework.boot + spring-boot-starter-data-jpa + +``` + +(2)设置启动注解 + +```java +// 【可选】指定扫描的 Entity 目录,如果不指定,会扫描全部目录 +@EntityScan("io.github.dunwu.springboot.data.jpa") +// 【可选】指定扫描的 Repository 目录,如果不指定,会扫描全部目录 +@EnableJpaRepositories(basePackages = {"io.github.dunwu.springboot.data.jpa"}) +// 【可选】开启 JPA auditing 能力,可以自动赋值一些字段,比如创建时间、最后一次修改时间等等 +@EnableJpaAuditing +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +(3)配置 + +```properties +# 数据库连接 +spring.datasource.url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver +spring.datasource.username = root +spring.datasource.password = root +# 是否打印 JPA SQL 日志 +spring.jpa.show-sql = true +# Hibernate的DDL策略 +spring.jpa.hibernate.ddl-auto = create-drop +``` + +(4)定义实体 + +```java +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Objects; +import javax.persistence.*; + +@Entity +@Data +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Column(unique = true) + private String name; + + private Integer age; + + private String address; + + private String email; + + public User(String name, Integer age, String address, String email) { + this.name = name; + this.age = age; + this.address = address; + this.email = email; + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof User)) { + return false; + } + + User user = (User) o; + + if (id != null && id.equals(user.id)) { + return true; + } + + return name.equals(user.name); + } + +} +``` + +(5)定义 Repository + +```java + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PathVariable; + +import java.util.List; + +@RepositoryRestResource(collectionResourceRel = "user", path = "user") +public interface UserRepository extends JpaRepository { + + User findUserById(@PathVariable("id") Long id); + + /** + * 根据用户名查找用户 + *

    + * 示例:http://localhost:8080/user/search/findByName?name=lisi + * + * @param name 用户名 + * @return {@link User} + */ + User findUserByName(@Param("name") String name); + + /** + * 根据邮箱查找用户 + *

    + * 示例:http://localhost:8080/user/search/findByEmail?email=xxx@163.com + * + * @param email 邮箱 + * @return {@link User} + */ + @Query("from User u where u.email=:email") + List findByEmail(@Param("email") String email); + + /** + * 根据用户名删除用户 + * + * @param name 用户名 + */ + @Transactional(rollbackFor = Exception.class) + void deleteByName(@Param("name") String name); + +} +``` + +(6)测试 + +```java +@Slf4j +@SpringBootTest(classes = { DataJpaApplication.class }) +public class DataJpaTests { + + @Autowired + private UserRepository repository; + + @BeforeEach + public void before() { + repository.deleteAll(); + } + + @Test + public void insert() { + User user = new User("张三", 18, "北京", "user1@163.com"); + repository.save(user); + Optional optional = repository.findById(user.getId()); + assertThat(optional).isNotNull(); + assertThat(optional.isPresent()).isTrue(); + } + + @Test + public void batchInsert() { + List users = new ArrayList<>(); + users.add(new User("张三", 18, "北京", "user1@163.com")); + users.add(new User("李四", 19, "上海", "user1@163.com")); + users.add(new User("王五", 18, "南京", "user1@163.com")); + users.add(new User("赵六", 20, "武汉", "user1@163.com")); + repository.saveAll(users); + + long count = repository.count(); + assertThat(count).isEqualTo(4); + + List list = repository.findAll(); + assertThat(list).isNotEmpty().hasSize(4); + list.forEach(this::accept); + } + + private void accept(User user) { log.info(user.toString()); } + + @Test + public void delete() { + List users = new ArrayList<>(); + users.add(new User("张三", 18, "北京", "user1@163.com")); + users.add(new User("李四", 19, "上海", "user1@163.com")); + users.add(new User("王五", 18, "南京", "user1@163.com")); + users.add(new User("赵六", 20, "武汉", "user1@163.com")); + repository.saveAll(users); + + repository.deleteByName("张三"); + assertThat(repository.findUserByName("张三")).isNull(); + + repository.deleteAll(); + List list = repository.findAll(); + assertThat(list).isEmpty(); + } + + @Test + public void findAllInPage() { + List users = new ArrayList<>(); + users.add(new User("张三", 18, "北京", "user1@163.com")); + users.add(new User("李四", 19, "上海", "user1@163.com")); + users.add(new User("王五", 18, "南京", "user1@163.com")); + users.add(new User("赵六", 20, "武汉", "user1@163.com")); + repository.saveAll(users); + + PageRequest pageRequest = PageRequest.of(1, 2); + Page page = repository.findAll(pageRequest); + assertThat(page).isNotNull(); + assertThat(page.isEmpty()).isFalse(); + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getTotalPages()).isEqualTo(2); + + List list = page.get().collect(Collectors.toList()); + System.out.println("user list: "); + list.forEach(System.out::println); + } + + @Test + public void update() { + User oldUser = new User("张三", 18, "北京", "user1@163.com"); + oldUser.setName("张三丰"); + repository.save(oldUser); + + User newUser = repository.findUserByName("张三丰"); + assertThat(newUser).isNotNull(); + } + +} +``` + +## 常用 JPA 注解 + +### 实体 + +#### `@Entity` + +#### `@MappedSuperclass` + +当多个实体有共同的属性字段,比如说 id,则可以把它提炼出一个父类,并且加上 `@MappedSuperclass`,则实体基类就可以继承了。 + +#### `@Table` + +当实体名和表名不一致时,可以通过 `@Table(name="CUSTOMERS")` 的形式来明确指定一个表名。 + +### 主键 + +#### `@Id` + +@Id 注解用于声明一个实体类的属性映射为数据库的主键。 + +#### `@GeneratedValue` + +`@GeneratedValue` 用于标注主键的生成策略,通过 `strategy` 属性指定。 + +默认情况下,JPA 自动选择一个最适合底层数据库的主键生成策略:SqlServer 对应 identity,MySQL 对应 auto increment。 + +在 `javax.persistence.GenerationType` 中定义了以下几种可供选择的策略: + +```java +public enum GenerationType { + TABLE, + SEQUENCE, + IDENTITY, + AUTO +} +``` + +- `IDENTITY`:采用数据库 ID 自增长的方式来自增主键字段,Oracle 不支持这种方式; +- `AUTO`: JPA 自动选择合适的策略,是默认选项; +- `SEQUENCE`:通过序列产生主键,通过 `@SequenceGenerator` 注解指定序列名,MySql 不支持这种方式 +- `TABLE`:通过表产生主键,框架借由表模拟序列产生主键,使用该策略可以使应用更易于数据库移植。 + +也就是如果你没有指定 strategy 属性,默认策略是 AUTO,JPA 会根据你使用的数据库来自动选择策略,比如说我使用的是 mysql 则,自动的主键策略就是 IDENTITY (auto increment)。 + +### 映射 + +#### `@Column` + +当你的 entity 属性名和数据库中的字段名不一致,可以使用 `@Column` 明确指定,它也可以设置一些属性 + +```java +@Column(length = 10, nullable = false, unique = true) +``` + +```java +@Column(columnDefinition = "INT(3)") +private int age; +``` + +`@Column` 支持的参数: + +- `unique` 属性表示该字段是否为唯一标识,默认为 false。如果表中有一个字段需要唯一标识,则既可以使用该标记,也可以使用 `@Table` 标记中的 `@UniqueConstraint`。 +- `nullable` 属性表示该字段是否可以为 `null` 值,默认为 true。 +- `insertable` 属性表示在使用 `INSERT` 插入数据时,是否需要插入该字段的值。 +- `updatable` 属性表示在使用 `UPDATE` 更新数据时,是否需要更新该字段的值。`insertable` 和 `updatable` 属性一般多用于只读的属性,例如主键和外键等。这些字段的值通常是自动生成的。 +- `columnDefinition` 属性表示创建表时,该字段创建的 SQL 语句,一般用于通过 Entity 生成表定义时使用。 +- `table` 属性表示当映射多个表时,指定表的表中的字段。默认值为主表的表名。 +- `length` 属性表示字段的长度,当字段的类型为 `varchar` 时,该属性才有效,默认为 255 个字符。 +- `precision` 属性和 scale 属性表示精度,当字段类型为 `double` 时,`precision` 表示数值的总长度,`scale` 表示小数点所占的位数。 + +`@JoinTable` + +`@JoinColumn` + +### 关系 + +表关系映射(双向映射) + +- `@OneToOne`:一对一关系 +- `@OneToMany`:一对多 +- `@ManyToMany`(不推荐使用,而是采用用中间对象,把多对多拆成两个对多一关系) + +字段映射(单向映射): + +- `@Embedded`、`@Embeddable` 嵌入式关系(单向映射) +- `@ElementCollection` 集合一对多关系(单向映射) + +#### `@OneToOne` + +`@OneToOne` 表示一对一关系 + +#### `@OneToMany` + +`@OneToMany` 表示一对多关系 + +`@ManyToOne` + +`@ManyToMany` + +`OrderBy` + +## 查询 + +查询方式有: + +- 方法名字方式查询 + +- `@Query` 注解方式查询 +- 动态 SQL 方式查询 + +- Example 方式查询 + +`JpaRepository` 提供了如下表所述的内置查询 + +- `List findAll();` - 返回所有实体 +- `List findAllById(Iterable var1);` - 返回指定 id 的所有实体 +- `T getOne(ID var1);` - 根据 id 返回对应的实体,如果未找到,则返回空。 +- `List findAll(Sort var1);` - 返回所有实体,按照指定顺序返回。 +- `Page findAll(Pageable var1);` - 返回实体列表,实体的 offset 和 limit 通过 pageable 来指定 + +### 方法名字方式查询方式 + +Spring Data 通过查询的方法名和参数名来自动构造一个 JPA QQL 查询。 + +```java +public interface UserRepository extends JpaRepository { + public User findByName(String name); +} +``` + +方法名和参数名要遵守一定的规则,Spring Data JPA 才能自动转换为 JPQL: + +- 方法名通常包含多个实体属性用于查询,属性之间可以使用 `AND` 和 `OR` 连接,也支持 `Between`、`LessThan`、`GreaterThan`、`Like`; + +- 方法名可以以 `findBy`、`getBy`、`queryBy` 开头; + +- 查询结果可以排序,方法名包含 OrderBy+属性+ASC(DESC); + +- 可以通过 `Top`、`First` 来限定查询的结果集; + +- 一些特殊的参数可以出现在参数列表里,比如 `Pageeable`、`Sort` + +示例: + +```java +// 根据名字查询,且按照名字升序 +List findByLastnameOrderByFirstnameAsc(String name); + +// 根据名字查询,且使用翻页查询 +Page findByLastname(String lastname, Pageable pageable); + +// 查询满足条件的前10个用户 +List findFirst10ByLastname(String lastname, Sort sort); + +// 使用And联合查询 +List findByFirstnameAndLastname(String firstname, String lastname); + +// 使用Or查询 +List findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); + +// 使用like查询,name 必须包含like中的%或者? +public User findByNameLike(String name); +``` + +| Keyword | Sample | JPQL snippet | +| ------------------- | --------------------------------------------------------- | ------------------------------------------------------------------ | +| `And` | `findByLastnameAndFirstname` | `… where x.lastname = ?1 and x.firstname = ?2` | +| `Or` | `findByLastnameOrFirstname` | `… where x.lastname = ?1 or x.firstname = ?2` | +| `Is,Equals` | `findByFirstname,findByFirstnameIs,findByFirstnameEquals` | `… where x.firstname = 1?` | +| `Between` | `findByStartDateBetween` | `… where x.startDate between 1? and ?2` | +| `LessThan` | `findByAgeLessThan` | `… where x.age < ?1` | +| `LessThanEqual` | `findByAgeLessThanEqual` | `… where x.age <= ?1` | +| `GreaterThan` | `findByAgeGreaterThan` | `… where x.age > ?1` | +| `GreaterThanEqual` | `findByAgeGreaterThanEqual` | `… where x.age >= ?1` | +| `After` | `findByStartDateAfter` | `… where x.startDate > ?1` | +| `Before` | `findByStartDateBefore` | `… where x.startDate < ?1` | +| `IsNull` | `findByAgeIsNull` | `… where x.age is null` | +| `IsNotNull,NotNull` | `findByAge(Is)NotNull` | `… where x.age not null` | +| `Like` | `findByFirstnameLike` | `… where x.firstname like ?1` | +| `NotLike` | `findByFirstnameNotLike` | `… where x.firstname not like ?1` | +| `StartingWith` | `findByFirstnameStartingWith` | `… where x.firstname like ?1` (parameter bound with appended `%`) | +| `EndingWith` | `findByFirstnameEndingWith` | `… where x.firstname like ?1` (parameter bound with prepended `%`) | +| `Containing` | `findByFirstnameContaining` | `… where x.firstname like ?1` (parameter bound wrapped in `%`) | +| `OrderBy` | `findByAgeOrderByLastnameDesc` | `… where x.age = ?1 order by x.lastname desc` | +| `Not` | `findByLastnameNot` | `… where x.lastname <> ?1` | +| `In` | `findByAgeIn(Collection ages)` | `… where x.age in ?1` | +| `NotIn` | `findByAgeNotIn(Collection age)` | `… where x.age not in ?1` | +| `True` | `findByActiveTrue()` | `… where x.active = true` | +| `False` | `findByActiveFalse()` | `… where x.active = false` | +| `IgnoreCase` | `findByFirstnameIgnoreCase` | `… where UPPER(x.firstame) = UPPER(?1)` | + +### @Query 注解方式查询 + +注解 `@Query` 允许在方法上使用 JPQL。 + +其中操作针对的是对象名和对象属性名,而非数据库中的表名和字段名。 + +```java +@Query("select u form User u where u.name=?1 and u.depantment.id=?2"); +public User findUser(String name, Integer departmentId); +``` + +```java +@Query("form User u where u.name=?1 and u.depantment.id=?2"); +public User findUser(String name, Integer departmentId); +``` + +如果使用 SQL 而不是 JPSQL,可以使用 `nativeQuery` 属性,设置为 true。 + +```java +@Query(value="select * from user where name=?1 and department_id=?2", nativeQuery=true) +public User nativeQuery(String name, Integer departmentId); +``` + +无论 JPQL,还是 SQL,都支持"命名参数": + +```java +@Query(value="select * from user where name=:name and department_id=:departmentId", nativeQuery=true) +public User nativeQuery2(String name, Integer departmentId); +``` + +如果 SQL 活着 JPQL 查询结果集并非 Entity,可以用 `Object[]` 数组代替,比如分组统计每个部分的用户数 + +```java +@Query(value="select department_id,count(*) from user group by department_id", nativeQuery=true) +public List queryUserCount() +``` + +这条查询将返回数组,对象类型依赖于查询结果,被示例中,返回的是 `String` 和 `BigInteger` 类型 + +查询时可以使用 `Pageable` 和 `Sort` 来完成翻页和排序。 + +```java +@Query("select u from User u where department.id=?1") +public Page QueryUsers(Integer departmentId, Pageable page); +``` + +`@Query` 还允许 SQL 更新、删除语句,此时必须搭配 `@Modifying` 使用,比如: + +```java +@Modifying +@Query("update User u set u.name= ?1 where u.id= ?2") +int updateName(String name, Integer id); +``` + +### 动态 SQL 方式查询 + +可参考:[SpringDataJpa 中的复杂查询和动态查询,多表查询](https://juejin.cn/post/6844904160807092237) + +### Example 方式查询 + +允许根据实体创建一个 Example 对象,Spring Data 通过 Example 对象来构造 JPQL。但是使用不灵活条件是 AND,不能使用 or,时间的大于小于,between 等。 + +继承 `JpaRepository` + +```java + List findAll(Example var1); + List findAll(Example var1, Sort var2); +``` + +```java +public List getByExample(String name) { + Department dept = new Department(); + dept.setId(1); + + User user = new User(); + user.setName(name); + user.setDepartment(dept); + Example example = Example.of(user); + List list = userDao.findAll(example); + return list +} +``` + +以上代码首先创建了 User 对象,设置 查询条件,名称为参数 name,部门 id 为 1,通过 `Example.of` 构造了此查询。 + +大部分查询并非完全匹配查询,ExampleMatcher 提供了更多的条件指定.比如以 xxx 开头的所有用户,则可以使用以下代码构造 + +```java +ExampleMatcher matcher = ExampleMatcher.matching().withMatcher("xxx", + GenericPropertyMatchers.startsWith().ignoreCase()); +Example example = Example.of(user, matcher); +``` + +### 排序 Sort + +Sort 对象用来指定排序,最简单的 Sort 对象构造可以传入一个属性名列表(不是数据库列名,是属性名)。默认采用升序排序。 + +```java +Sort sort = new Sort("id"); +//Sort sort = new Sort(Direction.DESC, "id"); +return userDao.findAll(sort); +``` + +Hibernate 根据 Sort 构造了排序条件,Sort("id") 表示按照 id 采用默认 升序进行排序 + +其他 Sort 的构造方法还包括以下主要的一些: + +- `public Sort(String... properties)`,按照指定的属性列表升序排序。 +- `public Sort(Sort.Direction direction, String... properties)`,按照指定属性列表排序,排序由 direction 指定,direction 是一个枚举类型,有 `Direction.ASC` 和 `Direction.DESC`。 +- `public Sort(Sort.Order... orders)`,可以通过 Order 静态方法来创建 + - `public static Sort.Order asc(String property)` + - `public static Sort.Order desc(String property)` + +### 分页 Page 和 Pageable + +Pageable 接口用于构造翻页查询,PageRequest 是其实现类,可以通过提供的工厂方法创建 PageRequest: + +注意我这边使用的是 sring boot 2.0.2 ,jpa 版本是 2.0.8,新版本与之前版本的操作方法有所不同。 + +- `public static PageRequest of(int page, int size)` + +- `public static PageRequest of(int page, int size, Sort sort)` - 也可以在 PageRequest 中加入排序 + +- `public static PageRequest of(int page, int size, Direction direction, String... properties)`,或者自定义排序规则 + +page 是从 0 开始,表示查询页,size 指每页的期望行数。 + +Spring Data 翻页查询总是返回 Page 对象,Page 对象提供了以下常用的方法 + +- `int getTotalPages();`,总的页数 +- `long getTotalElements();` - 返回总数 +- `List getContent();` - 返回此次查询的结果集 + +## 核心 API + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20230123160810.png) + +## 参考资料 + +- [Spring 官网](https://spring.io/) +- [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/10.Spring\351\233\206\346\210\220Mybatis.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/10.Spring\351\233\206\346\210\220Mybatis.md" new file mode 100644 index 00000000..d20f39b6 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/10.Spring\351\233\206\346\210\220Mybatis.md" @@ -0,0 +1,252 @@ +--- +title: Spring 集成 Mybatis +date: 2019-05-09 17:09:25 +order: 10 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - MyBatis + - PageHelper + - Mapper +permalink: /pages/88219e/ +--- + +# Spring 集成 Mybatis + +[Mybatis 官网](http://www.mybatis.org/mybatis-3/) 是一款持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。 + +## 快速入门 + +要使用 MyBatis, 只需将 [mybatis-x.x.x.jar](https://github.com/mybatis/mybatis-3/releases) 文件置于类路径(classpath)中即可。 + +如果使用 Maven 来构建项目,则需将下面的依赖代码置于 pom.xml 文件中: + +```xml + + org.mybatis + mybatis + x.x.x + +``` + +### 从 XML 中构建 SqlSessionFactory + +每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。 + +从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置。 但也可以使用任意的输入流(InputStream)实例,比如用文件路径字符串或 file:// URL 构造的输入流。MyBatis 包含一个名叫 Resources 的工具类,它包含一些实用方法,使得从类路径或其它位置加载资源文件更加容易。 + +```java +String resource = "org/mybatis/example/mybatis-config.xml"; +InputStream inputStream = Resources.getResourceAsStream(resource); +SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); +``` + +XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。后面会再探讨 XML 配置文件的详细内容,这里先给出一个简单的示例: + +```xml + + + + + + + + + + + + + + + + + + +``` + +当然,还有很多可以在 XML 文件中配置的选项,上面的示例仅罗列了最关键的部分。 注意 XML 头部的声明,它用来验证 XML 文档的正确性。environment 元素体中包含了事务管理和连接池的配置。mappers 元素则包含了一组映射器(mapper),这些映射器的 XML 映射文件包含了 SQL 代码和映射定义信息。 + +### 不使用 XML 构建 SqlSessionFactory + +如果你更愿意直接从 Java 代码而不是 XML 文件中创建配置,或者想要创建你自己的配置构建器,MyBatis 也提供了完整的配置类,提供了所有与 XML 文件等价的配置项。 + +```java +DataSource dataSource = BlogDataSourceFactory.getBlogDataSource(); +TransactionFactory transactionFactory = new JdbcTransactionFactory(); +Environment environment = new Environment("development", transactionFactory, dataSource); +Configuration configuration = new Configuration(environment); +configuration.addMapper(BlogMapper.class); +SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration); +``` + +注意该例中,configuration 添加了一个映射器类(mapper class)。映射器类是 Java 类,它们包含 SQL 映射注解从而避免依赖 XML 映射文件。不过,由于 Java 注解的一些限制以及某些 MyBatis 映射的复杂性,要使用大多数高级映射(比如:嵌套联合映射),仍然需要使用 XML 映射文件进行映射。有鉴于此,如果存在一个同名 XML 映射文件,MyBatis 会自动查找并加载它(在这个例子中,基于类路径和 BlogMapper.class 的类名,会加载 BlogMapper.xml)。具体细节稍后讨论。 + +### 从 SqlSessionFactory 中获取 SqlSession + +既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。例如: + +```java +try (SqlSession session = sqlSessionFactory.openSession()) { + Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101); +} +``` + +诚然,这种方式能够正常工作,对使用旧版本 MyBatis 的用户来说也比较熟悉。但现在有了一种更简洁的方式——使用和指定语句的参数和返回值相匹配的接口(比如 BlogMapper.class),现在你的代码不仅更清晰,更加类型安全,还不用担心可能出错的字符串字面值以及强制类型转换。 + +例如: + +```java +try (SqlSession session = sqlSessionFactory.openSession()) { + BlogMapper mapper = session.getMapper(BlogMapper.class); + Blog blog = mapper.selectBlog(101); +} +``` + +现在我们来探究一下这段代码究竟做了些什么。 + +### 探究已映射的 SQL 语句 + +现在你可能很想知道 SqlSession 和 Mapper 到底具体执行了些什么操作,但 SQL 语句映射是个相当广泛的话题,可能会占去文档的大部分篇幅。 但为了让你能够了解个大概,这里先给出几个例子。 + +在上面提到的例子中,一个语句既可以通过 XML 定义,也可以通过注解定义。我们先看看 XML 定义语句的方式,事实上 MyBatis 提供的所有特性都可以利用基于 XML 的映射语言来实现,这使得 MyBatis 在过去的数年间得以流行。如果你用过旧版本的 MyBatis,你应该对这个概念比较熟悉。 但相比于之前的版本,新版本改进了许多 XML 的配置,后面我们会提到这些改进。这里给出一个基于 XML 映射语句的示例,它应该可以满足上个示例中 SqlSession 的调用。 + +```xml + + + + + +``` + +为了这个简单的例子,我们似乎写了不少配置,但其实并不多。在一个 XML 映射文件中,可以定义无数个映射语句,这样一来,XML 头部和文档类型声明部分就显得微不足道了。文档的其它部分很直白,容易理解。 它在命名空间 “org.mybatis.example.BlogMapper” 中定义了一个名为 “selectBlog” 的映射语句,这样你就可以用全限定名 “org.mybatis.example.BlogMapper.selectBlog” 来调用映射语句了,就像上面例子中那样: + +```java +Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101); +``` + +你可能会注意到,这种方式和用全限定名调用 Java 对象的方法类似。这样,该命名就可以直接映射到在命名空间中同名的映射器类,并将已映射的 select 语句匹配到对应名称、参数和返回类型的方法。因此你就可以像上面那样,不费吹灰之力地在对应的映射器接口调用方法,就像下面这样: + +```java +BlogMapper mapper = session.getMapper(BlogMapper.class); +Blog blog = mapper.selectBlog(101); +``` + +第二种方法有很多优势,首先它不依赖于字符串字面值,会更安全一点;其次,如果你的 IDE 有代码补全功能,那么代码补全可以帮你快速选择到映射好的 SQL 语句。 + +**提示** **对命名空间的一点补充** + +在之前版本的 MyBatis 中,**命名空间(Namespaces)**的作用并不大,是可选的。 但现在,随着命名空间越发重要,你必须指定命名空间。 + +命名空间的作用有两个,一个是利用更长的全限定名来将不同的语句隔离开来,同时也实现了你上面见到的接口绑定。就算你觉得暂时用不到接口绑定,你也应该遵循这里的规定,以防哪天你改变了主意。 长远来看,只要将命名空间置于合适的 Java 包命名空间之中,你的代码会变得更加整洁,也有利于你更方便地使用 MyBatis。 + +**命名解析:**为了减少输入量,MyBatis 对所有具有名称的配置元素(包括语句,结果映射,缓存等)使用了如下的命名解析规则。 + +- 全限定名(比如 “com.mypackage.MyMapper.selectAllThings)将被直接用于查找及使用。 +- 短名称(比如 “selectAllThings”)如果全局唯一也可以作为一个单独的引用。 如果不唯一,有两个或两个以上的相同名称(比如 “com.foo.selectAllThings” 和 “com.bar.selectAllThings”),那么使用时就会产生“短名称不唯一”的错误,这种情况下就必须使用全限定名。 + +对于像 BlogMapper 这样的映射器类来说,还有另一种方法来完成语句映射。 它们映射的语句可以不用 XML 来配置,而可以使用 Java 注解来配置。比如,上面的 XML 示例可以被替换成如下的配置: + +```java +package org.mybatis.example; +public interface BlogMapper { + @Select("SELECT * FROM blog WHERE id = #{id}") + Blog selectBlog(int id); +} +``` + +使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。 + +选择何种方式来配置映射,以及是否应该要统一映射语句定义的形式,完全取决于你和你的团队。 换句话说,永远不要拘泥于一种方式,你可以很轻松地在基于注解和 XML 的语句映射方式间自由移植和切换。 + +### 作用域(Scope)和生命周期 + +理解我们之前讨论过的不同作用域和生命周期类别是至关重要的,因为错误的使用会导致非常严重的并发问题。 + +**提示** **对象生命周期和依赖注入框架** + +依赖注入框架可以创建线程安全的、基于事务的 SqlSession 和映射器,并将它们直接注入到你的 bean 中,因此可以直接忽略它们的生命周期。 如果对如何通过依赖注入框架使用 MyBatis 感兴趣,可以研究一下 MyBatis-Spring 或 MyBatis-Guice 两个子项目。 + +#### SqlSessionFactoryBuilder + +这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。 + +#### SqlSessionFactory + +SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。 + +#### SqlSession + +每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式: + +```java +try (SqlSession session = sqlSessionFactory.openSession()) { + // 你的应用逻辑代码 +} +``` + +在所有代码中都遵循这种使用模式,可以保证所有数据库资源都能被正确地关闭。 + +#### 映射器实例 + +映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的。虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。 也就是说,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃。 映射器实例并不需要被显式地关闭。尽管在整个请求作用域保留映射器实例不会有什么问题,但是你很快会发现,在这个作用域上管理太多像 SqlSession 的资源会让你忙不过来。 因此,最好将映射器放在方法作用域内。就像下面的例子一样: + +```java +try (SqlSession session = sqlSessionFactory.openSession()) { + BlogMapper mapper = session.getMapper(BlogMapper.class); + // 你的应用逻辑代码 +} +``` + +## Mybatis 扩展工具 + +### Mybatis Plus + +[MyBatis-Plus](https://github.com/baomidou/mybatis-plus)(简称 MP)是一个 [MyBatis](https://www.mybatis.org/mybatis-3/) 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。 + +【集成示例】[spring-boot-data-mybatis-plus](https://github.com/dunwu/spring-tutorial/tree/develop/codes/data/orm/spring-boot-data-mybatis-plus) + +### Mapper + +[Mapper](https://github.com/abel533/Mapper) 是一个 Mybatis CRUD 扩展插件。 + +Mapper 的基本原理是将实体类映射为数据库中的表和字段信息,因此实体类需要通过注解配置基本的元数据,配置好实体后, 只需要创建一个继承基础接口的 Mapper 接口就可以开始使用了。 + +【集成示例】[spring-boot-data-mybatis-mapper](https://github.com/dunwu/spring-tutorial/tree/develop/codes/data/orm/spring-boot-data-mybatis-mapper) + +### PageHelper + +[PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) 是一个 Mybatis 通用分页插件。 + +【集成示例】[spring-boot-data-mybatis-mapper](https://github.com/dunwu/spring-tutorial/tree/develop/codes/data/orm/spring-boot-data-mybatis-mapper) + +## 参考资料 + +- **官方** + - [Mybatis Github](https://github.com/mybatis/mybatis-3) + - [Mybatis 官网](http://www.mybatis.org/mybatis-3/) + - [MyBatis 官方代码生成(mybatis-generator)](https://github.com/mybatis/generator) + - [MyBatis 官方集成 Spring(mybatis-spring)](https://github.com/mybatis/spring) + - [Mybatis 官方集成 Spring Boot(mybatis-spring-boot)](https://github.com/mybatis/spring-boot-starter) +- **扩展插件** + - [MyBatis-Plus](https://github.com/baomidou/mybatis-plus) - CRUD 扩展插件、代码生成器、分页器等多功能 + - [Mapper](https://github.com/abel533/Mapper) - Mybatis CRUD 扩展插件 + - [PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) - Mybatis 通用分页插件 +- **文章** + - [深入理解 mybatis 原理](https://blog.csdn.net/luanlouis/article/details/40422941) + - [mybatis 源码中文注释](https://github.com/tuguangquan/mybatis) + - [MyBatis Generator 详解](https://blog.csdn.net/isea533/article/details/42102297) + - [Mybatis 常见面试题](https://juejin.im/post/5aa646cdf265da237e095da1) + - [Mybatis 中强大的 resultMap](https://juejin.im/post/5cee8b61e51d455d88219ea4) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/20.SpringData\347\273\274\345\220\210.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/20.SpringData\347\273\274\345\220\210.md" new file mode 100644 index 00000000..d4120edf --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/20.SpringData\347\273\274\345\220\210.md" @@ -0,0 +1,301 @@ +--- +title: Spring Data 综合 +date: 2023-02-08 09:10:35 +order: 20 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/191cdb/ +--- + +# Spring Data 综合 + +Spring Data Repository 抽象的目标是显著减少各种访问持久化存储的样板式代码。 + +## 核心概念 + +Repository 是 Spring Data 的核心接口。此接口主要用作标记接口,以捕获要使用的类型并帮助您发现扩展此接口的接口。`CrudRepository` 和 `ListCrudRepository` 接口为被管理的实体类提供复杂的 CRUD 功能。`ListCrudRepository` 提供等效方法,但它们返回 `List`,而 `CrudRepository` 方法返回 `Iterable`。 + +`CrudRepository` 接口定义: + +```java +public interface CrudRepository extends Repository { + + S save(S entity); + + Optional findById(ID primaryKey); + + Iterable findAll(); + + long count(); + + void delete(T entity); + + boolean existsById(ID primaryKey); + + // … more functionality omitted. +} +``` + +> Spring Data 项目也提供了一些特定持久化技术的抽象接口,如:JpaRepository 或 MongoRepository。这些接口扩展了 CrudRepository 并暴露了一些持久化技术的底层功能。 + +除了 `CrudRepository` 之外,还有一个 `PagingAndSortingRepository` 接口,它添加了额外的方法来简化对实体的分页访问: + +```java +public interface PagingAndSortingRepository { + + Iterable findAll(Sort sort); + + Page findAll(Pageable pageable); +} +``` + +【示例】要按页面大小 20 访问 User 的第二页,可以执行如下操作 + +```java +PagingAndSortingRepository repository = // … get access to a bean +Page users = repository.findAll(PageRequest.of(1, 20)); +``` + +除了查询方法之外,计数和删除时的查询也是可用的。 + +【示例】根据姓氏计数 + +```java +interface UserRepository extends CrudRepository { + long countByLastname(String lastname); +} +``` + +【示例】根据姓氏删除 + +```java +interface UserRepository extends CrudRepository { + + long deleteByLastname(String lastname); + + List removeByLastname(String lastname); +} +``` + +## 查询方法 + +使用 Spring Data 对数据库进行查询有以下四步: + +1. 声明一个扩展 `Repository` 或其子接口的接口,并指定泛型类型(实体类和 ID 类型),如以下示例所示: + + ```java + interface PersonRepository extends Repository { … } + ``` + +2. 在接口中声明查询方法 + + ```java + interface PersonRepository extends Repository { + List findByLastname(String lastname); + } + ``` + +3. 使用 [JavaConfig](https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#repositories.create-instances.java-config) 或 [XML 配置](https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#repositories.create-instances)为这些接口创建代理实例 + + ```java + @EnableJpaRepositories + class Config { … } + ``` + +4. 注入 `Repository` 实例并使用 + + ```java + class SomeClient { + + private final PersonRepository repository; + + SomeClient(PersonRepository repository) { + this.repository = repository; + } + + void doSomething() { + List persons = repository.findByLastname("Matthews"); + } + } + ``` + +## 定义 Repository + +首先需要定义一个 Repository 接口,该接口必须扩展 Repository 并且指定泛型类型(实体类和 ID 类型)。如果想为该实体暴露 CRUD 方法,可以扩展 CrudRepository 接口。 + +### 微调 Repository 定义 + +Spring Data 提供了很多种 Repository 以应对不同的需求场景。 + +`CrudRepository` 提供了 CRUD 功能。 + +`ListCrudRepository` 和 `CrudRepository` 类似,但对于那些返回多个实体的方法,它返回一个 `List` 而不是 `Iterable`,这样使用可能更方便。 + +如果使用响应式框架,可以使用 `ReactiveCrudRepository` 或 `RxJava3CrudRepository`。 + +`CoroutineCrudRepository` 支持 Kotlin 的协程特性。 + +`PagingAndSortingRepository` 提供了分页、排序功能。 + +如果不想扩展 Spring Data 接口,还可以使用 `@RepositoryDefinition` 注释您的 `Repository` 接口。 扩展一个 CRUD Repository 接口,需要暴露一组完整的方法来操作实体。如果希望对暴露的方法有选择性,可以将要暴露的方法从 CRUD Repository 复制到自定义的 Repository 中。 这样做时,可以更改方法的返回类型。 如果可能,Spring Data 将遵循返回类型。 例如,对于返回多个实体的方法,可以选择 `Iterable`、`List`、`Collection` 或 `VAVR` 列表。 + +自定义基础 `Repository` 接口,必须用 `@NoRepositoryBean` 标记。 这可以防止 Spring Data 尝试直接创建它的实例并失败,因为它无法确定该 Repository 的实体,因为它仍然包含一个通用类型变量。 + +以下示例显示了如何有选择地暴露 CRUD 方法(在本例中为 findById 和 save): + +```java +@NoRepositoryBean +interface MyBaseRepository extends Repository { + + Optional findById(ID id); + + S save(S entity); +} + +interface UserRepository extends MyBaseRepository { + User findByEmailAddress(EmailAddress emailAddress); +} +``` + +### 使用多个 Spring 数据模块 + +有时,程序中需要使用多个 Spring Data 模块。在这种情况下,必须区分持久化技术。当检测到类路径上有多个 Repository 工厂时,Spring Data 进入严格的配置模式。 + +如果定义的 Repository 扩展了特定模块中的 Repository,则它是特定 Spring Data 模块的有效候选者。 + +如果实体类使用了特定模块的类型注解,则它是特定 Spring Data 模块的有效候选者。 Spring Data 模块接受第三方注解(例如 JPA 的 `@Entity`)或提供自己的注解(例如用于 Spring Data MongoDB 和 Spring Data Elasticsearch 的 `@Document`)。 + +以下示例显示了一个使用模块特定接口(在本例中为 JPA)的 Repository: + +```java +interface MyRepository extends JpaRepository { } + +@NoRepositoryBean +interface MyBaseRepository extends JpaRepository { … } + +interface UserRepository extends MyBaseRepository { … } +``` + +MyRepository 和 UserRepository 扩展了 JpaRepository。它们是 Spring Data JPA 模块的有效候选者。 + +以下示例显示了一个使用通用接口的 Repository + +```java +interface AmbiguousRepository extends Repository { … } + +@NoRepositoryBean +interface MyBaseRepository extends CrudRepository { … } + +interface AmbiguousUserRepository extends MyBaseRepository { … } +``` + +AmbiguousRepository 和 AmbiguousUserRepository 仅扩展了 Repository 和 CrudRepository。 虽然这在使用唯一的 Spring Data 模块时很好,但是存在多个模块时,无法区分这些 Repository 应该绑定到哪个特定的 Spring Data。 + +以下示例显示了一个使用带注解的实体类的 Repository + +```java +interface PersonRepository extends Repository { … } + +@Entity +class Person { … } + +interface UserRepository extends Repository { … } + +@Document +class User { … } +``` + +PersonRepository 引用 Person,它使用 JPA @Entity 注解进行标记,因此这个 Repository 显然属于 Spring Data JPA。 UserRepository 引用 User,它使用 Spring Data MongoDB 的 @Document 注解进行标记。 + +以下错误示例显示了一个使用带有混合注解的实体类的 Repository + +```java +interface JpaPersonRepository extends Repository { … } + +interface MongoDBPersonRepository extends Repository { … } + +@Entity +@Document +class Person { … } +``` + +此示例中的实体类同时使用了 JPA 和 Spring Data MongoDB 的注解。示例中定义了两个 Repository:JpaPersonRepository 和 MongoDBPersonRepository。 一个用于 JPA,另一个用于 MongoDB。 Spring Data 不再能够区分 Repository,这会导致未定义的行为。 + +区分 Repository 的最后一种方法是确定 Repository 扫描 package 的范围。 + +```java +@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa") +@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo") +class Configuration { … } +``` + +## 定义查询方法 + +Repository 代理有两种方法可以从方法名称派生特定于存储的查询: + +- 通过直接从方法名称派生查询。 +- 通过使用手动定义的查询。 + +可用选项取决于实际存储。但是,必须有一个策略来决定创建什么实际查询。 + +### 查询策略 + +以下策略可用于Repository 基础结构来解析查询。 对于 Java 配置,您可以使用 EnableJpaRepositories 注释的 queryLookupStrategy 属性。 特定数据存储可能不支持某些策略。 + +- `CREATE` 尝试从查询方法名称构造特定存储的查询。 +- `USE_DECLARED_QUERY` 尝试查找已声明的查询,如果找不到则抛出异常。 +- `CREATE_IF_NOT_FOUND` (默认)结合了 `CREATE` 和 `USE_DECLARED_QUERY`。 + +### 查询创建 + +Spring Data 中有一套内置的查询构建器机制,可以自动映射符合命名和参数规则的方法。 + +```java +interface PersonRepository extends Repository { + + List findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname); + + // Enables the distinct flag for the query + List findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname); + List findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname); + + // Enabling ignoring case for an individual property + List findByLastnameIgnoreCase(String lastname); + // Enabling ignoring case for all suitable properties + List findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname); + + // Enabling static ORDER BY for a query + List findByLastnameOrderByFirstnameAsc(String lastname); + List findByLastnameOrderByFirstnameDesc(String lastname); +} +``` + +解析查询方法名称分为主语和谓语。第一部分 (find…By, exists…By) 定义查询的主语,第二部分构成谓词。 主语可以包含更多的表达。 `find`(或其他引入关键字)和 `By` 之间的任何文本都被认为是描述性的,除非使用其中一个结果限制关键字,例如 `Distinct` 在要创建的查询上设置不同的标志或 `Top`/`First` 限制查询结果。 + +> 参考: +> +> [Spring Data 支持的查询主语关键词](https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#appendix.query.method.subject) +> +> [Spring Data 支持的查询谓语关键词](https://docs.spring.io/spring-data/jdbc/docs/current/reference/html/#appendix.query.method.predicate) + +## 创建 Repository 实例 + +## 自定义 Repository 实现 + +## Spring Data 扩展 + +## 参考资料 + +- [Redis 官网](https://redis.io/) +- [Redis Github](https://github.com/redis/redis) +- [spring-data-redis Github](https://github.com/spring-projects/spring-data-redis) +- [Spring Data Redis 官方文档](https://docs.spring.io/spring-data/redis/docs/current/reference/html/) +- [Spring Data 官方示例](https://github.com/spring-projects/spring-data-examples/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/21.Spring\350\256\277\351\227\256Redis.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/21.Spring\350\256\277\351\227\256Redis.md" new file mode 100644 index 00000000..c8bf04e3 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/21.Spring\350\256\277\351\227\256Redis.md" @@ -0,0 +1,248 @@ +--- +title: Spring 访问 Redis +date: 2023-01-31 20:54:42 +order: 21 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - Redis +permalink: /pages/65e4a2/ +--- + +# Spring 访问 Redis + +## 简介 + +[Redis](https://redis.io/) 是一个被数百万开发人员用作数据库、缓存、流引擎和消息代理的开源内存数据库。 + +在 Spring 中,[spring-data-redis](https://github.com/spring-projects/spring-data-redis) 项目对访问 [Redis](https://redis.io/) 进行了 API 封装,提供了便捷的访问方式。 [spring-data-redis](https://github.com/spring-projects/spring-data-redis) + +[spring-boot](https://github.com/spring-projects/spring-boot) 项目中的子模块 [spring-boot-starter-data-redis](https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-starters/spring-boot-starter-data-redis) 基于 [spring-data-redis](https://github.com/spring-projects/spring-data-redis) 项目,做了二次封装,大大简化了 Redis 的相关配置。 + +## Spring Boot 快速入门 + +### 引入依赖 + +在 pom.xml 中引入依赖: + +```xml + + org.springframework.boot + spring-boot-starter-data-redis + +``` + +### 数据源配置 + +```properties +spring.redis.database = 0 +spring.redis.host = localhost +spring.redis.port = 6379 +spring.redis.password = +``` + +### 定义实体 + +```java +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.io.Serializable; + +@Data +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class User implements Serializable { + + private static final long serialVersionUID = 4142994984277644695L; + + private Long id; + private String name; + private Integer age; + private String address; + private String email; + +} +``` + +### 定义 CRUD 接口 + +```java +import java.util.Map; + +public interface UserService { + + void batchSetUsers(Map users); + + long count(); + + User getUser(Long id); + + void setUser(User user); + +} +``` + +### 创建 CRUD 接口实现 + +```java + +import cn.hutool.core.bean.BeanUtil; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +public class UserServiceImpl implements UserService { + + public static final String DEFAULT_KEY = "spring:tutorial:user"; + + private final RedisTemplate redisTemplate; + + public UserServiceImpl(RedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + @Override + public void batchSetUsers(Map users) { + redisTemplate.opsForHash().putAll(DEFAULT_KEY, users); + } + + @Override + public long count() { + return redisTemplate.opsForHash().size(DEFAULT_KEY); + } + + @Override + public User getUser(Long id) { + Object obj = redisTemplate.opsForHash().get(DEFAULT_KEY, id.toString()); + return BeanUtil.toBean(obj, User.class); + } + + @Override + public void setUser(User user) { + redisTemplate.opsForHash().put(DEFAULT_KEY, user.getId().toString(), user); + } + +} +``` + +### 创建 Application + +创建 Application,实例化一个 `RedisTemplate` 对象。 + +```java +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Slf4j +@SpringBootApplication +public class RedisQuickstartApplication { + + @Autowired + private ObjectMapper objectMapper; + + @Bean + @Primary + public RedisTemplate redisTemplate(RedisConnectionFactory factory) { + + // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public + objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + // // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常 + // objectMapper.activateDefaultTyping(new DefaultBaseTypeLimitingValidator(), + // ObjectMapper.DefaultTyping.NON_FINAL); + + // 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式) + Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer<>(Object.class); + serializer.setObjectMapper(objectMapper); + + RedisTemplate template = new RedisTemplate<>(); + // 配置连接工厂 + template.setConnectionFactory(factory); + // 值采用json序列化 + template.setValueSerializer(serializer); + // 使用StringRedisSerializer来序列化和反序列化redis的key值 + template.setKeySerializer(new StringRedisSerializer()); + // 设置hash key 和value序列化模式 + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(serializer); + template.afterPropertiesSet(); + + return template; + } + + public static void main(String[] args) { + SpringApplication.run(RedisQuickstartApplication.class, args); + } + +} +``` + +### 测试 + +```java +@Slf4j +@SpringBootTest(classes = { RedisQuickstartApplication.class }) +public class RedisQuickstartTests { + + @Autowired + private UserService userService; + + @Test + public void test() { + final long SIZE = 1000L; + Map map = new HashMap<>(); + for (long i = 0; i < SIZE; i++) { + User user = new User(i, RandomUtil.randomChineseName(), + RandomUtil.randomInt(1, 100), + RandomUtil.randomEnum(Location.class).name(), + RandomUtil.randomEmail()); + map.put(String.valueOf(i), user); + } + userService.batchSetUsers(map); + long count = userService.count(); + Assertions.assertThat(count).isEqualTo(SIZE); + + for (int i = 0; i < 100; i++) { + long id = RandomUtil.randomLong(0, 1000); + User user = userService.getUser(id); + log.info("user-{}: {}", id, user.toString()); + } + } + +} +``` + +## 示例源码 + +更多 Spring 访问 Redis 示例请参考:[Redis 示例源码](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/redis) + +## 参考资料 + +- [Redis 官网](https://redis.io/) +- [Redis Github](https://github.com/redis/redis) +- [spring-data-redis Github](https://github.com/spring-projects/spring-data-redis) +- [Spring Data Redis 官方文档](https://docs.spring.io/spring-data/redis/docs/current/reference/html/) +- [Spring Data 官方示例](https://github.com/spring-projects/spring-data-examples/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/22.Spring\350\256\277\351\227\256MongoDB.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/22.Spring\350\256\277\351\227\256MongoDB.md" new file mode 100644 index 00000000..03ddf78c --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/22.Spring\350\256\277\351\227\256MongoDB.md" @@ -0,0 +1,186 @@ +--- +title: Spring 访问 MongoDB +date: 2018-12-15 17:29:36 +order: 22 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - MongoDB +permalink: /pages/db2a41/ +--- + +# Spring 访问 MongoDB + +## 简介 + +[MongoDB](https://www.mongodb.org/) 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。MongoDB 将数据存储为一个文档,数据结构由键值对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。 + +在 Spring 中,[spring-data-mongodb](https://github.com/spring-projects/spring-data-mongodb) 项目对访问 [MongoDB](https://www.mongodb.org/) 进行了 API 封装,提供了便捷的访问方式。 Spring Data MongoDB 的核心是一个以 POJO 为中心的模型,用于与 MongoDB `DBCollection` 交互并轻松编写 `Repository` 样式的数据访问层。 + +[spring-boot](https://github.com/spring-projects/spring-boot) 项目中的子模块 [spring-boot-starter-data-mongodb](https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-starters/spring-boot-starter-data-mongodb) 基于 [spring-data-mongodb](https://github.com/spring-projects/spring-data-mongodb) 项目,做了二次封装,大大简化了 MongoDB 的相关配置。 + +## Spring Boot 快速入门 + +### 引入依赖 + +在 pom.xml 中引入依赖: + +```xml + + org.springframework.boot + spring-boot-starter-data-mongodb + +``` + +### 数据源配置 + +```properties +spring.data.mongodb.host = localhost +spring.data.mongodb.port = 27017 +spring.data.mongodb.database = test +spring.data.mongodb.username = root +spring.data.mongodb.password = root +``` + +### 定义实体 + +定义一个具有三个属性的 `Customer` 类:`id`、`firstName` 和 `lastName` + +```java +import org.springframework.data.annotation.Id; + +public class Customer { + + @Id + public String id; + + public String firstName; + + public String lastName; + + public Customer(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + @Override + public String toString() { + return String.format( + "Customer[id=%s, firstName='%s', lastName='%s']", + id, firstName, lastName); + } + +} +``` + +[spring-data-mongodb](https://github.com/spring-projects/spring-data-mongodb) 会将 `Customer` 类映射到一个名为 `customer` 的集合中。如果要更改集合的名称,可以在类上使用 `@Document` 注解。 + +### 创建 Repository + +[spring-data-mongodb](https://github.com/spring-projects/spring-data-mongodb) 继承了 [Spring Data Commons](https://github.com/spring-projects/spring-data-commons) 项目的能力,所以可以使用其通用 API——`Repository`。 + +先定义一个 `CustomerRepository` 类,继承 `MongoRepository` 接口,并指定其泛型参数:`Customer` 和 `String`。MongoRepository 接口支持多种操作,包括 CRUD 和分页查询。在下面的例子中,定义了两个查询方法: + +```java +import java.util.List; + +import org.springframework.data.mongodb.repository.MongoRepository; + +public interface CustomerRepository extends MongoRepository { + + Customer findByFirstName(String firstName); + List findByLastName(String lastName); + +} +``` + +### 创建 Application + +创建一个 Spring Boot 的启动类 Application,并在启动的 main 方法中使用 `CustomerRepository` 实例访问 MongoDB。 + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class DataMongodbApplication implements CommandLineRunner { + + @Autowired + private CustomerRepository repository; + + public static void main(String[] args) { + SpringApplication.run(DataMongodbApplication.class, args); + } + + @Override + public void run(String... args) { + + repository.deleteAll(); + + // save a couple of customers + repository.save(new Customer("Alice", "Smith")); + repository.save(new Customer("Bob", "Smith")); + + // fetch all customers + System.out.println("Customers found with findAll():"); + System.out.println("-------------------------------"); + for (Customer customer : repository.findAll()) { + System.out.println(customer); + } + System.out.println(); + + // fetch an individual customer + System.out.println("Customer found with findByFirstName('Alice'):"); + System.out.println("--------------------------------"); + System.out.println(repository.findByFirstName("Alice")); + + System.out.println("Customers found with findByLastName('Smith'):"); + System.out.println("--------------------------------"); + for (Customer customer : repository.findByLastName("Smith")) { + System.out.println(customer); + } + } + +} +``` + +运行 `DataMongodbApplication` 的 main 方法后,输出类似如下类容: + +``` +Customers found with findAll(): +------------------------------- +Customer(id=63d6157b265e7c5e48077f63, firstName=Alice, lastName=Smith) +Customer(id=63d6157b265e7c5e48077f64, firstName=Bob, lastName=Smith) + +Customer found with findByFirstName('Alice'): +-------------------------------- +Customer(id=63d6157b265e7c5e48077f63, firstName=Alice, lastName=Smith) +Customers found with findByLastName('Smith'): +-------------------------------- +Customer(id=63d6157b265e7c5e48077f63, firstName=Alice, lastName=Smith) +Customer(id=63d6157b265e7c5e48077f64, firstName=Bob, lastName=Smith) +``` + +## 示例源码 + +更多 Spring 访问 MongoDB 示例请参考:[MongoDB 示例源码](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/mongodb) + +## 参考资料 + +- [MongoDB 官网](https://www.mongodb.com/) +- [MongoDB Github](https://github.com/mongodb/mongo) +- [MongoDB 官方免费教程](https://university.mongodb.com/) +- [spring-data-mongodb Github](https://github.com/spring-projects/spring-data-mongodb) +- [Spring Data MongoDB 官方文档](https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/) +- [Spring Data 官方示例](https://github.com/spring-projects/spring-data-examples/) +- [Accessing Data with MongoDB](https://spring.io/guides/gs/accessing-data-mongodb/) +- [Accessing MongoDB Data with REST](https://spring.io/guides/gs/accessing-mongodb-data-rest/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/23.Spring\350\256\277\351\227\256Elasticsearch.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/23.Spring\350\256\277\351\227\256Elasticsearch.md" new file mode 100644 index 00000000..695ae7eb --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/23.Spring\350\256\277\351\227\256Elasticsearch.md" @@ -0,0 +1,133 @@ +--- +title: Spring 访问 Elasticsearch +date: 2018-12-25 14:06:36 +order: 23 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - Elasticsearch +permalink: /pages/fac14c/ +--- + +# Spring 访问 Elasticsearch + +## 简介 + +[Elasticsearch](https://www.elastic.co/products/elasticsearch) 是一个开源的、分布式的搜索和分析引擎。 + +### 通过 REST 客户端连接 Elasticsearch + +如果在 classpath 路径下存在 `org.elasticsearch.client:elasticsearch-rest-client` jar 包,Spring Boot 会自动配置并注册一个 `RestClient` Bean,它的默认访问路径为:`localhost:9200`。 + +你可以使用如下方式进行定制: + +```properties +spring.elasticsearch.rest.uris=http://search.example.com:9200 +spring.elasticsearch.rest.username=user +spring.elasticsearch.rest.password=secret +``` + +您还可以注册实现任意数量的 `RestClientBuilderCustomizer` bean,以进行更高级的定制。要完全控制注册,请定义 `RestClient` bean。 + +如果 classpath 路径有 `org.elasticsearch.client:elasticsearch-rest-high-level-client` jar 包,Spring Boot 将自动配置一个 `RestHighLevelClient`,它包装任何现有的 `RestClient` bean,重用其 HTTP 配置。 + +### 通过 Jest 连接 Elasticsearch + +如果 classpath 上有 Jest,你可以注入一个自动配置的 `JestClient`,默认情况下是 `localhost:9200`。您可以进一步调整客户端的配置方式,如以下示例所示: + +```properties +spring.elasticsearch.jest.uris=http://search.example.com:9200 +spring.elasticsearch.jest.read-timeout=10000 +spring.elasticsearch.jest.username=user +spring.elasticsearch.jest.password=secret +``` + +您还可以注册实现任意数量的 `HttpClientConfigBuilderCustomizer` bean,以进行更高级的定制。以下示例调整为其他 HTTP 设置: + +```java +static class HttpSettingsCustomizer implements HttpClientConfigBuilderCustomizer { + + @Override + public void customize(HttpClientConfig.Builder builder) { + builder.maxTotalConnection(100).defaultMaxTotalConnectionPerRoute(5); + } + +} +``` + +要完全控制注册,请定义 `JestClient` bean。 + +### 通过 Spring Data 访问 Elasticsearch + +要连接到 Elasticsearch,您必须提供一个或多个集群节点的地址。可以通过将 `spring.data.elasticsearch.cluster-nodes` 属性设置为以逗号分隔的 `host:port` 列表来指定地址。使用此配置,可以像任何其他 Spring bean 一样注入 `ElasticsearchTemplate` 或 `TransportClient`,如以下示例所示: + +```java +spring.data.elasticsearch.cluster-nodes=localhost:9300 +@Component +public class MyBean { + + private final ElasticsearchTemplate template; + + public MyBean(ElasticsearchTemplate template) { + this.template = template; + } + + // ... + +} +``` + +如果你添加了自定义的 `ElasticsearchTemplate` 或 `TransportClient` `@Bean` ,就会替换默认的配置。 + +### Elasticsearch Repositories + +Spring Data 包含对 Elasticsearch 的 repository 支持。基本原则是根据方法名称自动为您构建查询。 + +事实上,Spring Data JPA 和 Spring Data Elasticsearch 共享相同的通用基础架构。 + +## 源码 + +完整示例:[源码](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-data-elasticsearch) + +使用方法: + +```bash +mvn clean package +cd target +java -jar spring-boot-data-elasticsearch.jar +``` + +## 版本 + +Spring 和 Elasticsearch 匹配版本: + +| Spring Data Elasticsearch | Elasticsearch | Spring Framework | Spring Boot | +| :------------------------------------------------------------------------------------------------------: | :-----------: | :--------------: | :---------: | +| 5.0.x | 8.5.3 | 6.0.x | 3.0.x | +| 4.4.x | 7.17.3 | 5.3.x | 2.7.x | +| 4.3.x | 7.15.2 | 5.3.x | 2.6.x | +| 4.2.x[[1](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#_footnotedef_1)] | 7.12.0 | 5.3.x | 2.5.x | +| 4.1.x[[1](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#_footnotedef_1)] | 7.9.3 | 5.3.2 | 2.4.x | +| 4.0.x[[1](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#_footnotedef_1)] | 7.6.2 | 5.2.12 | 2.3.x | +| 3.2.x[[1](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#_footnotedef_1)] | 6.8.12 | 5.2.12 | 2.2.x | +| 3.1.x[[1](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#_footnotedef_1)] | 6.2.2 | 5.1.19 | 2.1.x | +| 3.0.x[[1](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#_footnotedef_1)] | 5.5.0 | 5.0.13 | 2.0.x | +| 2.1.x[[1](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#_footnotedef_1)] | 2.4.0 | 4.3.25 | 1.5.x | + +## 参考资料 + +- **官方** + - [Elasticsearch 官网](https://www.elastic.co/cn/products/elasticsearch) + - [Elasticsearch Github](https://github.com/elastic/elasticsearch) + - [Elasticsearch 官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) + - [Elasticsearch: The Definitive Guide](https://www.elastic.co/guide/en/elasticsearch/guide/master/index.html) - ElasticSearch 官方学习资料 +- [Spring Boot 官方文档之 boot-features-elasticsearch](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-elasticsearch) +- [Spring Data Elasticsearch Github](https://github.com/spring-projects/spring-data-elasticsearch) +- [Spring Data Elasticsearch 官方文档](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/README.md" new file mode 100644 index 00000000..a079aa02 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/README.md" @@ -0,0 +1,75 @@ +--- +title: Spring 数据篇 +date: 2022-09-18 11:05:36 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 数据库 +permalink: /pages/b912d1/ +hidden: true +index: false +--- + +# Spring 数据篇 + +## 📖 内容 + +- [Spring 之数据源](01.Spring之数据源.md) +- [Spring 之 JDBC](02.Spring之JDBC.md) +- [Spring 之事务](03.Spring之事务.md) +- [Spring 之 JPA](04.Spring之JPA.md) +- [Spring 集成 Mybatis](10.Spring集成Mybatis.md) +- [Spring 访问 Redis](21.Spring访问Redis.md) +- [Spring 访问 MongoDB](22.Spring访问MongoDB.md) +- [Spring 访问 Elasticsearch](23.Spring访问Elasticsearch.md) + +## 💻 示例 + +- **JDBC** + - [spring-data-jdbc-basics](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/basics) - Spring Boot 以 JDBC 方式访问关系型数据库,通过 `JdbcTemplate` 执行基本的 CRUD 操作。 + - [spring-data-jdbc-druid](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/druid) - SpringBoot 使用 [Druid](https://github.com/alibaba/druid) 作为数据库连接池。 + - [spring-data-jdbc-multi-datasource](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/multi-datasource) - SpringBoot 连接多数据源示例。 + - [spring-data-jdbc-xml](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/xml) - Spring 以 JDBC 方式访问关系型数据库,通过 `JdbcTemplate` 执行基本的 CRUD 操作。 +- **ORM** + - [spring-data-orm-jpa](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/jpa) - SpringBoot 使用 JPA 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis) - Spring 使用 [MyBatis](https://github.com/mybatis/mybatis-3) 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis-mapper](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis-mapper) - SpringBoot 使用 [MyBatis](https://github.com/mybatis/mybatis-3) + [Mapper](https://github.com/abel533/Mapper) + [PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis-multi-datasource](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis-multi-datasource) - SpringBoot 连接多数据源,并使用 [MyBatis Plus](https://github.com/baomidou/mybatis-plus) 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis-plus](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis-plus) - SpringBoot 使用 [MyBatis Plus](https://github.com/baomidou/mybatis-plus) 作为 ORM 框架访问数据库示例。 +- **Nosql** + - [spring-data-nosql-basics](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/basics) - Spring 访问各种 NoSQL 的示例。 + - [spring-data-nosql-mongodb](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/mongodb) - SpringBoot 访问 [MongoDB](https://www.mongodb.com/) 的示例。 + - [spring-data-nosql-redis](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/redis) - SpringBoot 访问 [Redis](https://redis.io/) 单节点、集群的示例。 + - [spring-data-nosql-elasticsearch](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/elasticsearch) - SpringBoot 访问 [Elasticsearch](https://www.elastic.co/guide/index.html) 的示例。 + - [spring-data-nosql-hdfs](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/hdfs) - SpringBoot 访问 HDFS 的示例。 +- **Cache** + - [spring-data-cache-basics](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/cache/basics) - SpringBoot 默认缓存框架的示例。 + - [spring-data-cache-j2cache](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/cache/j2cache) - SpringBoot 使用 [j2cache](https://gitee.com/ld/J2Cache) 作为缓存框架的示例。 + - [spring-data-cache-jetcache](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/cache/jetcache) - SpringBoot 使用 [jetcache](https://github.com/alibaba/jetcache) 作为缓存框架的示例。 +- **中间件** + - [spring-data-middleware-flyway](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/middleware/flyway) - Spring 使用版本管理中间件 Flyway 示例。 + - [spring-data-middleware-sharding](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/middleware/sharding) - Spring 使用分库分表中间件示例。 + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/01.SpringWebMvc.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/01.SpringWebMvc.md" new file mode 100644 index 00000000..3e8d4b44 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/01.SpringWebMvc.md" @@ -0,0 +1,37 @@ +--- +title: spring-mvc +date: 2017-11-08 16:53:27 +order: 01 +categories: + - Java + - 框架 + - Spring + - SpringWeb +tags: + - Java + - 框架 + - Spring + - Web +permalink: /pages/65351b/ +--- + +# SpringMVC 简介 + +## SpringMVC 工作流程描述 + +Spring MVC 的工作流程可以用一幅图来说明: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/web/spring-dispatcher-servlet.png) + +1. 向服务器发送 HTTP 请求,请求被前端控制器 `DispatcherServlet` 捕获。 +2. `DispatcherServlet` 根据 **`-servlet.xml`** 中的配置对请求的 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用 `HandlerMapping` 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以`HandlerExecutionChain` 对象的形式返回。 +3. `DispatcherServlet` 根据获得的`Handler`,选择一个合适的 `HandlerAdapter`。(附注:如果成功获得`HandlerAdapter`后,此时将开始执行拦截器的 preHandler(...)方法)。 +4. 提取`Request`中的模型数据,填充`Handler`入参,开始执行`Handler`(`Controller`)。 在填充`Handler`的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作: + - HttpMessageConveter: 将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息。 + - 数据转换:对请求消息进行数据转换。如`String`转换成`Integer`、`Double`等。 + - 数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等。 + - 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到`BindingResult`或`Error`中。 +5. Handler(Controller)执行完成后,向 `DispatcherServlet` 返回一个 `ModelAndView` 对象; +6. 根据返回的`ModelAndView`,选择一个适合的 `ViewResolver`(必须是已经注册到 Spring 容器中的`ViewResolver`)返回给`DispatcherServlet`。 +7. `ViewResolver` 结合`Model`和`View`,来渲染视图。 +8. 视图负责将渲染结果返回给客户端。 \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/21.SpringBoot\344\271\213\345\272\224\347\224\250EasyUI.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/21.SpringBoot\344\271\213\345\272\224\347\224\250EasyUI.md" new file mode 100644 index 00000000..ef62068a --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/21.SpringBoot\344\271\213\345\272\224\347\224\250EasyUI.md" @@ -0,0 +1,509 @@ +--- +title: SpringBoot 之应用 EasyUI +date: 2019-01-08 17:19:34 +order: 21 +categories: + - Java + - 框架 + - Spring + - SpringWeb +tags: + - Java + - 框架 + - Spring + - SpringBoot + - Web +permalink: /pages/ad0516/ +--- + +# SpringBoot 之应用 EasyUI + +> EasyUI 是一个简单的用户界面组件的集合。由于 EasyUI 已经封装好大部分 UI 基本功能,能帮用户减少大量的 js 和 css 代码。所以,EasyUI 非常适合用于开发简单的系统或原型系统。 +> +> 本文示例使用技术点: +> +> - Spring Boot:主要使用了 spring-boot-starter-web、spring-boot-starter-data-jpa +> - EasyUI:按需加载,并没有引入所有的 EasyUI 特性 +> - 数据库:为了测试方便,使用 H2 + +![img](http://www.jeasyui.cn/images/easyui.png) + +## 简介 + +### 什么是 EasyUI? + +- easyui 是基于 jQuery、Angular.、Vue 和 React 的用户界面组件的集合。 +- easyui 提供了构建现代交互式 javascript 应用程序的基本功能。 +- 使用 easyui,您不需要编写许多 javascript 代码,通常通过编写一些 HTML 标记来定义用户界面。 +- 完整的 HTML5 网页框架。 +- 使用 easyui 开发你的产品时可以大量节省你的时间和规模。 +- easyui 使用非常简单但功能非常强大。 + +## Spring Boot 整合 EasyUI + +### 配置 + +application.properties 修改: + +```properties +spring.mvc.view.prefix = /views/ +spring.mvc.view.suffix = .html +``` + +### 引入 easyui + +EasyUI 下载地址:http://www.jeasyui.cn/download.html + +在 `src/main/resources/static` 目录下引入 easyui。 + +然后在 html 中引用: + +```html + + + + + + + + + + + + + + + +``` + +引入 easyui 后,需要使用哪种组件,可以查看相关文档或 API,十分简单,此处不一一赘述。 + +## 实战 + +### 引入 maven 依赖 + +```xml + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + + com.h2database + h2 + + + org.springframework.boot + spring-boot-devtools + + + commons-collections + commons-collections + 3.2.2 + + +``` + +### 使用 JPA + +为了使用 JPA 技术访问数据,我们需要定义 Entity 和 Repository + +定义一个 Entity: + +```java +@Entity +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + private String firstName; + private String lastName; + private String phone; + private String email; + + protected User() {} + + public User(String firstName, String lastName, String phone, String email) { + this.firstName = firstName; + this.lastName = lastName; + this.phone = phone; + this.email = email; + } + + // 略 getter/setter +} +``` + +定义一个 Repository: + +``` +public interface UserRepository extends CrudRepository { + + List findByLastName(String lastName); +} +``` + +### 使用 Web + +首页 Controller,将 web 请求定向到指定页面(下面的例子定向到 index.html) + +```java +@Controller +public class IndexController { + + @RequestMapping(value = {"", "/", "index"}) + public String index() { + return "index"; + } + +} +``` + +此外,需要定义一个 Controller,提供后台的 API 接口 + +```java +@Controller +public class UserController { + + @Autowired + private UserRepository customerRepository; + + @RequestMapping(value = "/user", method = RequestMethod.GET) + public String user() { + return "user"; + } + + @ResponseBody + @RequestMapping(value = "/user/list") + public ResponseDTO list() { + Iterable all = customerRepository.findAll(); + List list = IteratorUtils.toList(all.iterator()); + return new ResponseDTO<>(true, list.size(), list); + } + + @ResponseBody + @RequestMapping(value = "/user/add") + public ResponseDTO add(User user) { + User result = customerRepository.save(user); + List list = new ArrayList<>(); + list.add(result); + return new ResponseDTO<>(true, 1, list); + } + + @ResponseBody + @RequestMapping(value = "/user/save") + public ResponseDTO save(@RequestParam("id") Long id, User user) { + user.setId(id); + customerRepository.save(user); + List list = new ArrayList<>(); + list.add(user); + return new ResponseDTO<>(true, 1, list); + } + + @ResponseBody + @RequestMapping(value = "/user/delete") + public ResponseDTO delete(@RequestParam("id") Long id) { + customerRepository.deleteById(id); + return new ResponseDTO<>(true, null, null); + } + +} +``` + +### 使用 EasyUI + +接下来,我们要使用前面定义的后台接口,仅需要在 EasyUI API 中指定 `url` 即可。 + +请留意下面示例中的 url 字段,和实际接口是一一对应的。 + +```html + + + + Complex Layout - jQuery EasyUI Demo + + + + + + + + + + +
    +

    基本的 CRUD 应用

    +

    数据来源于后台系统

    + + + + + + + + + + + +
    IDFirst NameLast NamePhoneEmail
    +
    + 添加 + 修改 + 删除 +
    + +
    +
    +

    User Information

    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + Save + Cancel +
    +
    + + + + +``` + +## 完整示例 + +请参考 [源码](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-web-ui/spring-boot-web-ui-easyui) + +运行方式: + +``` +mvn clean package -DskipTests=true +java -jar target/ +``` + +在浏览器中访问:http://localhost:8080/ + +## 引用和引申 + +- [EasyUI 官网](http://www.jeasyui.com/) +- [EasyUI 中文网](http://www.jeasyui.cn/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/README.md" new file mode 100644 index 00000000..0d69c4ff --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/README.md" @@ -0,0 +1,44 @@ +--- +title: Spring Web +date: 2020-02-26 23:48:06 +categories: + - Java + - 框架 + - Spring + - SpringWeb +tags: + - Java + - 框架 + - Spring + - SpringBoot + - Web +permalink: /pages/e2586a/ +hidden: true +index: false +--- + +# Spring Web + +> 章节主要针对:Spring 在 web 领域的应用。如:Spring MVC、WebSocket 等。 + +## 📖 内容 + +- [Spring WebMvc](01.SpringWebMvc.md) +- [SpringBoot 之应用 EasyUI](21.SpringBoot之应用EasyUI.md) + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/01.SpringBoot\344\271\213\345\274\202\346\255\245\350\257\267\346\261\202.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/01.SpringBoot\344\271\213\345\274\202\346\255\245\350\257\267\346\261\202.md" new file mode 100644 index 00000000..605bc8ad --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/01.SpringBoot\344\271\213\345\274\202\346\255\245\350\257\267\346\261\202.md" @@ -0,0 +1,139 @@ +--- +title: spring-boot-async +date: 2019-11-18 14:55:01 +order: 01 +categories: + - Java + - 框架 + - Spring + - SpringIO +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 异步 +permalink: /pages/92add2/ +--- + +# SpringBoot 教程之处理异步请求 + +## `@EnableAsync` 注解 + +要使用 `@Async`,首先需要使用 `@EnableAsync` 注解开启 Spring Boot 中的异步特性。 + +```java +@Configuration +@EnableAsync +public class AppConfig { +} +``` + +更详细的配置说明,可以参考:[`AsyncConfigurer`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/annotation/AsyncConfigurer.html) + +## `@Async` 注解 + +### 支持的用法 + +(1)**无入参无返回值方法** + +您可以用 `@Async` 注解修饰方法,这表明这个方法是异步方式调用。换句话说,程序在调用此方法时会立即返回,而方法的实际执行发生在已提交给 Spring `TaskExecutor` 的任务中。在最简单的情况下,您可以将注解应用于返回 void 的方法,如以下示例所示: + +```java +@Async +void doSomething() { + // this will be executed asynchronously +} +``` + +(2)**有入参无返回值方法** + +与使用 `@Scheduled` 注释注释的方法不同,这些方法可以指定参数,因为它们在运行时由调用者以“正常”方式调用,而不是由容器管理的调度任务调用。例如,以下代码是 `@Async` 注解的合法应用: + +```java +@Async +void doSomething(String s) { + // this will be executed asynchronously +} +``` + +(3)**有入参有返回值方法** + +甚至可以异步调用返回值的方法。但是,这些方法需要具有 `Future` 类型的返回值。这仍然提供了异步执行的好处,以便调用者可以在调用 `Future` 上的 `get()` 之前执行其他任务。以下示例显示如何在返回值的方法上使用`@Async`: + +```java +@Async +Future returnSomething(int i) { + // this will be executed asynchronously +} +``` + +### 不支持的用法 + +`@Async` 不能与生命周期回调一起使用,例如 `@PostConstruct`。 + +要异步初始化 Spring bean,必须使用单独的初始化 Spring bean,然后在目标上调用 `@Async` 带注释的方法,如以下示例所示: + +```java +public class SampleBeanImpl implements SampleBean { + + @Async + void doSomething() { + // ... + } + +} + +public class SampleBeanInitializer { + + private final SampleBean bean; + + public SampleBeanInitializer(SampleBean bean) { + this.bean = bean; + } + + @PostConstruct + public void initialize() { + bean.doSomething(); + } + +} +``` + +## 明确指定执行器 + +默认情况下,在方法上指定 `@Async` 时,使用的执行器是在启用异步支持时配置的执行器,即如果使用 XML 或 `AsyncConfigurer` 实现(如果有),则为 `annotation-driven` 元素。但是,如果需要指示在执行给定方法时应使用默认值以外的执行器,则可以使用 `@Async` 注解的 value 属性。以下示例显示了如何执行此操作: + +```java +@Async("otherExecutor") +void doSomething(String s) { + // this will be executed asynchronously by "otherExecutor" +} +``` + +在这种情况下,“otherExecutor”可以是 Spring 容器中任何 Executor bean 的名称,也可以是与任何 Executor 关联的限定符的名称(例如,使用 `` 元素或 Spring 的 `@Qualifier` 注释指定) )。 + +## 管理 `@Async` 的异常 + +当 `@Async` 方法的返回值类型为 `Future` 型时,很容易管理在方法执行期间抛出的异常,因为在调用 `get` 结果时会抛出此异常。但是,对于返回值类型为 void 型的方法,异常不会被捕获且无法传输。您可以提供 `AsyncUncaughtExceptionHandler` 来处理此类异常。以下示例显示了如何执行此操作: + +```java +public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler { + + @Override + public void handleUncaughtException(Throwable ex, Method method, Object... params) { + // handle exception + } +} +``` + +默认情况下,仅记录异常。您可以使用 `AsyncConfigurer` 或 `` XML 元素定义自定义 `AsyncUncaughtExceptionHandler`。 + +## 示例源码 + +> 示例源码:[spring-boot-async](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-async) + +## 参考资料 + +- [Spring Boot 官方文档之 boot-features-external-config](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config) +- [Spring Boot 官方文档之 scheduling-annotation-support](https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#scheduling-annotation-support) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/02.SpringBoot\344\271\213Json.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/02.SpringBoot\344\271\213Json.md" new file mode 100644 index 00000000..0e5da987 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/02.SpringBoot\344\271\213Json.md" @@ -0,0 +1,272 @@ +--- +title: SpringBoot 之集成 Json +date: 2018-12-30 22:24:16 +order: 02 +categories: + - Java + - 框架 + - Spring + - SpringIO +tags: + - Java + - 框架 + - Spring + - SpringBoot + - JSON +permalink: /pages/676725/ +--- + +# SpringBoot 之集成 Json + +## 简介 + +### Spring Boot 支持的 Json 库 + +Spring Boot 支持三种 Json 库: + +- Gson +- Jackson +- JSON-B + +**Jackson 是 Spring Boot 官方推荐的默认库。** + +Spring Boot 提供了 Jackson 的自动配置,Jackson 是 `spring-boot-starter-json` 的一部分。当 Jackson 在类路径上时,会自动配置 ObjectMapper bean。 + +Spring Boot 提供了 Gson 的自动配置。当 Gson 在 classpath 上时,会自动配置 Gson bean。提供了几个 `spring.gson.*` 配置属性来自定义配置。为了获得更多控制,可以使用一个或多个 `GsonBuilderCustomizer` bean。 + +Spring Boot 提供了 JSON-B 的自动配置。当 JSON-B API 在 classpath 上时,将自动配置 Jsonb bean。首选的 JSON-B 实现是 Apache Johnzon,它提供了依赖关系管理。 + +### Spring Web 中的序列化、反序列化 + +以下注解都是 `spring-web` 中提供的支持。 + +#### `@ResponseBody` + +`@Responsebody` 注解用于将 Controller 的方法返回的对象,通过适当的 `HttpMessageConverter` 转换为指定格式后,写入到 HTTP Response 对象的 body 数据区。一般在异步获取数据时使用。通常是在使用 `@RequestMapping` 后,返回值通常解析为跳转路径,加上 @Responsebody 后返回结果不会被解析为跳转路径,而是直接写入 HTTP 响应正文中。 + +示例: + +```java +@ResponseBody +@RequestMapping(name = "/getInfo", method = RequestMethod.GET) +public InfoDTO getInfo() { + return new InfoDTO(); +} +``` + +#### `@RequestBody` + +@RequestBody 注解用于读取 HTTP Request 请求的 body 部分数据,使用系统默认配置的 `HttpMessageConverter` 进行解析,然后把相应的数据绑定到要返回的对象上;再把 `HttpMessageConverter` 返回的对象数据绑定到 controller 中方法的参数上。 + +request 的 body 部分的数据编码格式由 header 部分的 `Content-Type` 指定。 + +示例: + +```java +@RequestMapping(name = "/postInfo", method = RequestMethod.POST) +public void postInfo(@RequestBody InfoDTO infoDTO) { + // ... +} +``` + +#### `@RestController` + +Spring 4 以前: + +如果需要返回到指定页面,则需要用 `@Controller` 配合视图解析器 `InternalResourceViewResolver` 。 + +如果需要返回 JSON,XML 或自定义 mediaType 内容到页面,则需要在对应的方法上加上 `@ResponseBody` 注解。 + +Spring 4 以后,新增了 `@RestController` 注解: + +它相当于 `@Controller` + `@RequestBody` 。 + +如果使用 `@RestController` 注解 Controller,则 Controller 中的方法无法返回 jsp 页面,或者 html,配置的视图解析器 `InternalResourceViewResolver` 将不起作用,直接返回内容。 + +## 指定类的 Json 序列化、反序列化 + +如果使用 Jackson 序列化和反序列化 JSON 数据,您可能需要编写自己的 `JsonSerializer` 和 `JsonDeserializer` 类。自定义序列化程序通常通过模块向 Jackson 注册,但 Spring Boot 提供了另一种 `@JsonComponent` 注释,可以更容易地直接注册 Spring Beans。 + +您可以直接在 `JsonSerializer` 或 `JsonDeserializer` 实现上使用 `@JsonComponent` 注释。您还可以在包含序列化程序/反序列化程序作为内部类的类上使用它,如以下示例所示: + +```java +import java.io.*; +import com.fasterxml.jackson.core.*; +import com.fasterxml.jackson.databind.*; +import org.springframework.boot.jackson.*; + +@JsonComponent +public class Example { + + public static class Serializer extends JsonSerializer { + // ... + } + + public static class Deserializer extends JsonDeserializer { + // ... + } + +} +``` + +`ApplicationContext` 中的所有 `@JsonComponent` bean 都会自动注册到 Jackson。因为 `@JsonComponent` 是使用 `@Component` 进行元注释的,所以通常的组件扫描规则适用。 + +Spring Boot 还提供了 [`JsonObjectSerializer`](https://github.com/spring-projects/spring-boot/tree/v2.1.1.RELEASE/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonObjectSerializer.java) 和 [`JsonObjectDeserializer`](https://github.com/spring-projects/spring-boot/tree/v2.1.1.RELEASE/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jackson/JsonObjectDeserializer.java) 基类,它们在序列化对象时提供了标准 Jackson 版本的有用替代方法。有关详细信息,请参阅 Javadoc 中的 [`JsonObjectSerializer`](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/jackson/JsonObjectSerializer.html) 和 [`JsonObjectDeserializer`](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/jackson/JsonObjectDeserializer.html)。 + +## @JsonTest + +使用 `@JsonTest` 可以很方便的在 Spring Boot 中测试序列化、反序列化。 + +使用 `@JsonTest` 相当于使用以下自动配置: + +``` +org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration org.springframework.boot.test.autoconfigure.json.JsonTestersAutoConfiguration +``` + +`@JsonTest` 使用示例: + +想试试完整示例,可以参考:[源码](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-web-fastjson) + +```java +@JsonTest +@RunWith(SpringRunner.class) +public class SimpleJsonTest { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private JacksonTester json; + + @Test + public void testSerialize() throws Exception { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + InfoDTO infoDTO = new InfoDTO("JSON测试应用", "1.0.0", sdf.parse("2019-01-01 12:00:00")); + JsonContent jsonContent = json.write(infoDTO); + log.info("json content: {}", jsonContent.getJson()); + // 或者使用基于JSON path的校验 + assertThat(jsonContent).hasJsonPathStringValue("@.appName"); + assertThat(jsonContent).extractingJsonPathStringValue("@.appName").isEqualTo("JSON测试应用"); + assertThat(jsonContent).hasJsonPathStringValue("@.version"); + assertThat(jsonContent).extractingJsonPathStringValue("@.version").isEqualTo("1.0.0"); + assertThat(jsonContent).hasJsonPathStringValue("@.date"); + assertThat(jsonContent).extractingJsonPathStringValue("@.date").isEqualTo("2019-01-01 12:00:00"); + } + + @Test + public void testDeserialize() throws Exception { + String content = "{\"appName\":\"JSON测试应用\",\"version\":\"1.0.0\",\"date\":\"2019-01-01\"}"; + InfoDTO actual = json.parseObject(content); + assertThat(actual.getAppName()).isEqualTo("JSON测试应用"); + assertThat(actual.getVersion()).isEqualTo("1.0.0"); + } +} +``` + +## Spring Boot 中的 json 配置 + +### Jackson 配置 + +当 Spring Boot 的 json 库为 jackson 时,可以使用以下配置属性(对应 [`JacksonProperties`](https://github.com/spring-projects/spring-boot/tree/v2.1.1.RELEASE/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jackson/JacksonProperties.java) 类): + +```properties +spring.jackson.date-format= # Date format string or a fully-qualified date format class name. For instance, `yyyy-MM-dd HH:mm:ss`. +spring.jackson.default-property-inclusion= # Controls the inclusion of properties during serialization. Configured with one of the values in Jackson's JsonInclude.Include enumeration. +spring.jackson.deserialization.*= # Jackson on/off features that affect the way Java objects are deserialized. +spring.jackson.generator.*= # Jackson on/off features for generators. +spring.jackson.joda-date-time-format= # Joda date time format string. If not configured, "date-format" is used as a fallback if it is configured with a format string. +spring.jackson.locale= # Locale used for formatting. +spring.jackson.mapper.*= # Jackson general purpose on/off features. +spring.jackson.parser.*= # Jackson on/off features for parsers. +spring.jackson.property-naming-strategy= # One of the constants on Jackson's PropertyNamingStrategy. Can also be a fully-qualified class name of a PropertyNamingStrategy subclass. +spring.jackson.serialization.*= # Jackson on/off features that affect the way Java objects are serialized. +spring.jackson.time-zone= # Time zone used when formatting dates. For instance, "America/Los_Angeles" or "GMT+10". +spring.jackson.visibility.*= # Jackson visibility thresholds that can be used to limit which methods (and fields) are auto-detected. +``` + +### GSON 配置 + +当 Spring Boot 的 json 库为 gson 时,可以使用以下配置属性(对应 [`GsonProperties`](https://github.com/spring-projects/spring-boot/tree/v2.1.1.RELEASE/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/gson/GsonProperties.java) 类): + +```properties +spring.gson.date-format= # Format to use when serializing Date objects. +spring.gson.disable-html-escaping= # Whether to disable the escaping of HTML characters such as '<', '>', etc. +spring.gson.disable-inner-class-serialization= # Whether to exclude inner classes during serialization. +spring.gson.enable-complex-map-key-serialization= # Whether to enable serialization of complex map keys (i.e. non-primitives). +spring.gson.exclude-fields-without-expose-annotation= # Whether to exclude all fields from consideration for serialization or deserialization that do not have the "Expose" annotation. +spring.gson.field-naming-policy= # Naming policy that should be applied to an object's field during serialization and deserialization. +spring.gson.generate-non-executable-json= # Whether to generate non executable JSON by prefixing the output with some special text. +spring.gson.lenient= # Whether to be lenient about parsing JSON that doesn't conform to RFC 4627. +spring.gson.long-serialization-policy= # Serialization policy for Long and long types. +spring.gson.pretty-printing= # Whether to output serialized JSON that fits in a page for pretty printing. +spring.gson.serialize-nulls= # Whether to serialize null fields. +``` + +## Spring Boot 中使用 Fastjson + +国内很多的 Java 程序员更喜欢使用阿里的 fastjson 作为 json lib。那么,如何在 Spring Boot 中将其替换默认的 jackson 库呢? + +你需要做如下处理: + +(1)引入 fastjson jar 包: + +```xml + + com.alibaba + fastjson + 1.2.54 + +``` + +(2)实现 WebMvcConfigurer 接口,自定义 `configureMessageConverters` 接口。如下所示: + +```java +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + /** + * 自定义消息转换器 + * @param converters + */ + @Override + public void configureMessageConverters(List> converters) { + // 清除默认 Json 转换器 + converters.removeIf(converter -> converter instanceof MappingJackson2HttpMessageConverter); + + // 配置 FastJson + FastJsonConfig config = new FastJsonConfig(); + config.setSerializerFeatures(SerializerFeature.QuoteFieldNames, SerializerFeature.WriteEnumUsingToString, + SerializerFeature.WriteMapNullValue, SerializerFeature.WriteDateUseDateFormat, + SerializerFeature.DisableCircularReferenceDetect); + + // 添加 FastJsonHttpMessageConverter + FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); + fastJsonHttpMessageConverter.setFastJsonConfig(config); + List fastMediaTypes = new ArrayList<>(); + fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8); + fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes); + converters.add(fastJsonHttpMessageConverter); + + // 添加 StringHttpMessageConverter,解决中文乱码问题 + StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter(Charset.forName("UTF-8")); + converters.add(stringHttpMessageConverter); + } + + // ... +} +``` + +## 示例源码 + +完整示例:[源码](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-web-fastjson) + +## 引申和引用 + +**引申** + +- [Spring Boot 教程](https://github.com/dunwu/spring-boot-tutorial) + +**引用** + +- [Spring Boot 官方文档之 boot-features-json](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-json) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/03.SpringBoot\344\271\213\351\202\256\344\273\266.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/03.SpringBoot\344\271\213\351\202\256\344\273\266.md" new file mode 100644 index 00000000..f2c2259c --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/03.SpringBoot\344\271\213\351\202\256\344\273\266.md" @@ -0,0 +1,278 @@ +--- +title: SpringBoot 之发送邮件 +date: 2019-11-20 15:20:44 +order: 03 +categories: + - Java + - 框架 + - Spring + - SpringIO +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 邮件 +permalink: /pages/2586f1/ +--- + +# SpringBoot 之发送邮件 + +## 简介 + +Spring Boot 收发邮件最简便方式是通过 `spring-boot-starter-mail`。 + +```xml + + org.springframework.boot + spring-boot-starter-mail + +``` + +spring-boot-starter-mail 本质上是使用 JavaMail(javax.mail)。如果想对 JavaMail 有进一步了解,可以参考: [JavaMail 使用指南](https://dunwu.github.io/java-tutorial/#/javalib/javamail) + +## API + +Spring Framework 提供了一个使用 `JavaMailSender` 接口发送电子邮件的简单抽象,这是发送邮件的核心 API。 + +`JavaMailSender` 接口提供的 API 如下: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20190110111102.png) + +## 配置 + +Spring Boot 为 `JavaMailSender` 提供了自动配置以及启动器模块。 + +如果 `spring.mail.host` 和相关库(由 spring-boot-starter-mail 定义)可用,则 Spring Boot 会创建默认 `JavaMailSender`(如果不存在)。可以通过 `spring.mail` 命名空间中的配置项进一步自定义发件人。 +特别是,某些默认超时值是无限的,您可能希望更改它以避免线程被无响应的邮件服务器阻塞,如以下示例所示: + +```properties +spring.mail.properties.mail.smtp.connectiontimeout=5000 +spring.mail.properties.mail.smtp.timeout=3000 +spring.mail.properties.mail.smtp.writetimeout=5000 +``` + +也可以使用 JNDI 中的现有会话配置 `JavaMailSender`: + +``` +spring.mail.jndi-name=mail/Session +``` + +以下为 Spring Boot 关于 Mail 的配置: + +有关更多详细信息,请参阅 [`MailProperties`](https://github.com/spring-projects/spring-boot/tree/v2.1.1.RELEASE/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/mail/MailProperties.java)。 + +```properties +# Email (MailProperties) +spring.mail.default-encoding=UTF-8 # Default MimeMessage encoding. +spring.mail.host= # SMTP server host. For instance, `smtp.example.com`. +spring.mail.jndi-name= # Session JNDI name. When set, takes precedence over other Session settings. +spring.mail.password= # Login password of the SMTP server. +spring.mail.port= # SMTP server port. +spring.mail.properties.*= # Additional JavaMail Session properties. +spring.mail.protocol=smtp # Protocol used by the SMTP server. +spring.mail.test-connection=false # Whether to test that the mail server is available on startup. +spring.mail.username= # Login user of the SMTP server. +``` + +## 实战 + +### 引入依赖 + +```xml + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.projectlombok + lombok + + + com.github.dozermapper + dozer-spring-boot-starter + 6.4.0 + + +``` + +### 配置邮件属性 + +在 `src/main/resources` 目录下添加 `application-163.properties` 配置文件,内容如下: + +```properties +spring.mail.host = smtp.163.com +spring.mail.username = xxxxxx +spring.mail.password = xxxxxx +spring.mail.properties.mail.smtp.auth = true +spring.mail.properties.mail.smtp.starttls.enable = true +spring.mail.properties.mail.smtp.starttls.required = true +spring.mail.default-encoding = UTF-8 + +mail.domain = 163.com +mail.from = ${spring.mail.username}@${mail.domain} +``` + +注:需替换有效的 `spring.mail.username`、`spring.mail.password`。 + +`application-163.properties` 配置文件表示使用 163 邮箱时的配置,为了使之生效,需要通过 `spring.profiles.active = 163` 来激活它。 + +在 `src/main/resources` 目录下添加 `application.properties` 配置文件,内容如下: + +```properties +spring.profiles.active = 163 +``` + +### Java 代码 + +首先,需要读取部分配置属性,方法如下: + +```java +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.validation.annotation.Validated; + +@Validated +@Component +@ConfigurationProperties(prefix = "mail") +public class MailProperties { + private String domain; + private String from; + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public String getFrom() { + return from; + } + + public void setFrom(String from) { + this.from = from; + } +} +``` + +接着,定义一个邮件参数实体类(使用 lombok 简化了 getter、setter): + +```java +import lombok.Data; +import java.util.Date; + +@Data +public class MailDTO { + private String from; + private String replyTo; + private String[] to; + private String[] cc; + private String[] bcc; + private Date sentDate; + private String subject; + private String text; + private String[] filenames; +} +``` + +接着,实现发送邮件的功能接口: + +```java +import com.github.dozermapper.core.Mapper; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.stereotype.Service; + +import javax.mail.MessagingException; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import java.io.IOException; + +@Service +public class MailService { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Autowired + private MailProperties mailProperties; + + @Autowired + private JavaMailSender javaMailSender; + + @Autowired + private Mapper mapper; + + public void sendSimpleMailMessage(MailDTO mailDTO) { + SimpleMailMessage simpleMailMessage = mapper.map(mailDTO, SimpleMailMessage.class); + if (StringUtils.isEmpty(mailDTO.getFrom())) { + mailDTO.setFrom(mailProperties.getFrom()); + } + javaMailSender.send(simpleMailMessage); + } + + public void sendMimeMessage(MailDTO mailDTO) { + + MimeMessage mimeMessage = javaMailSender.createMimeMessage(); + MimeMessageHelper messageHelper; + try { + messageHelper = new MimeMessageHelper(mimeMessage, true); + + if (StringUtils.isEmpty(mailDTO.getFrom())) { + messageHelper.setFrom(mailProperties.getFrom()); + } + messageHelper.setTo(mailDTO.getTo()); + messageHelper.setSubject(mailDTO.getSubject()); + + mimeMessage = messageHelper.getMimeMessage(); + MimeBodyPart mimeBodyPart = new MimeBodyPart(); + mimeBodyPart.setContent(mailDTO.getText(), "text/html;charset=UTF-8"); + + // 描述数据关系 + MimeMultipart mm = new MimeMultipart(); + mm.setSubType("related"); + mm.addBodyPart(mimeBodyPart); + + // 添加邮件附件 + for (String filename : mailDTO.getFilenames()) { + MimeBodyPart attachPart = new MimeBodyPart(); + try { + attachPart.attachFile(filename); + } catch (IOException e) { + e.printStackTrace(); + } + mm.addBodyPart(attachPart); + } + mimeMessage.setContent(mm); + mimeMessage.saveChanges(); + + } catch (MessagingException e) { + e.printStackTrace(); + } + + javaMailSender.send(mimeMessage); + } +} +``` + +## 示例源码 + +> 示例源码:[spring-boot-mail](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-mail) + +## 参考资料 + +- [Spring Boot 官方文档之 Sending Email](https://docs.spring.io/spring-boot/docs/2.1.1.RELEASE/reference/htmlsingle/#boot-features-email) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/README.md" new file mode 100644 index 00000000..32a77f97 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/README.md" @@ -0,0 +1,43 @@ +--- +title: Spring IO +date: 2022-09-18 11:34:00 +categories: + - Java + - 框架 + - Spring + - SpringIO +tags: + - Java + - 框架 + - Spring + - SpringBoot + - IO +permalink: /pages/56581b/ +hidden: true +index: false +--- + +# Spring IO + +## 📖 内容 + +- [SpringBoot 之异步请求](01.SpringBoot之异步请求.md) +- [SpringBoot 之 Json](02.SpringBoot之Json.md) +- [SpringBoot 之邮件](03.SpringBoot之邮件.md) + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/01.Spring\351\233\206\346\210\220\347\274\223\345\255\230.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/01.Spring\351\233\206\346\210\220\347\274\223\345\255\230.md" new file mode 100644 index 00000000..58c15632 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/01.Spring\351\233\206\346\210\220\347\274\223\345\255\230.md" @@ -0,0 +1,230 @@ +--- +title: Spring集成缓存 +date: 2017-11-08 16:53:27 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring集成 +tags: + - Java + - 框架 + - Spring + - 集成 + - 缓存 +permalink: /pages/a311cb/ +--- + +# Spring 集成缓存中间件 + +> Spring 中提供了缓存功能的抽象,允许你在底层灵活的替换缓存实现,而对上层暴露相同的缓存接口。 + +## 缓存接口 + +Spring 的缓存 API 以注解方式提供。 + +### 开启注解 + +Spring 为缓存功能提供了注解功能,但是你必须启动注解。 +你有两个选择: +(1) 在 xml 中声明 +像上一节 spring-ehcache.xml 中的做法一样,使用`` + +```xml + +``` + +(2) 使用标记注解 +你也可以通过对一个类进行注解修饰的方式在这个类中使用缓存注解。 +范例如下: + +```java +@Configuration +@EnableCaching +public class AppConfig { +} +``` + +### 缓存注解使用 + +Spring 对缓存的支持类似于对事务的支持。 +首先使用注解标记方法,相当于定义了切点,然后使用 Aop 技术在这个方法的调用前、调用后获取方法的入参和返回值,进而实现了缓存的逻辑。 +下面三个注解都是方法级别: + +#### @Cacheable + +表明所修饰的方法是可以缓存的:当第一次调用这个方法时,它的结果会被缓存下来,在缓存的有效时间内,以后访问这个方法都直接返回缓存结果,不再执行方法中的代码段。 +这个注解可以用`condition`属性来设置条件,如果不满足条件,就不使用缓存能力,直接执行方法。 +可以使用`key`属性来指定 key 的生成规则。 + +#### @CachePut + +与`@Cacheable`不同,`@CachePut`不仅会缓存方法的结果,还会执行方法的代码段。 +它支持的属性和用法都与`@Cacheable`一致。 + +#### @CacheEvict + +与`@Cacheable`功能相反,`@CacheEvict`表明所修饰的方法是用来删除失效或无用的缓存数据。 +下面是`@Cacheable`、`@CacheEvict`和`@CachePut`基本使用方法的一个集中展示: + +```java +@Service +public class UserService { + // @Cacheable可以设置多个缓存,形式如:@Cacheable({"books", "isbns"}) + @Cacheable(value={"users"}, key="#user.id") + public User findUser(User user) { + return findUserInDB(user.getId()); + } + + @Cacheable(value = "users", condition = "#user.getId() <= 2") + public User findUserInLimit(User user) { + return findUserInDB(user.getId()); + } + + @CachePut(value = "users", key = "#user.getId()") + public void updateUser(User user) { + updateUserInDB(user); + } + + @CacheEvict(value = "users") + public void removeUser(User user) { + removeUserInDB(user.getId()); + } + + @CacheEvict(value = "users", allEntries = true) + public void clear() { + removeAllInDB(); + } +} +``` + +#### @Caching + +如果需要使用同一个缓存注解(`@Cacheable`、`@CacheEvict`或`@CachePut`)多次修饰一个方法,就需要用到`@Caching`。 + +```java +@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") }) +public Book importBooks(String deposit, Date date) +``` + +#### @CacheConfig + +与前面的缓存注解不同,这是一个类级别的注解。 +如果类的所有操作都是缓存操作,你可以使用`@CacheConfig`来指定类,省去一些配置。 + +```java +@CacheConfig("books") +public class BookRepositoryImpl implements BookRepository { + @Cacheable + public Book findBook(ISBN isbn) {...} +} +``` + +## 缓存存储 + +Spring 允许通过配置方式接入多种不同的缓存存储。用户可以根据实际需要选择。 + +不同的缓存存储,具有不同的性能和特性,如果想了解具体原理,可以参考:[全面理解缓存原理](https://dunwu.github.io/javatech/#/technology/cache/cache-theory?id=%e5%85%a8%e9%9d%a2%e7%90%86%e8%a7%a3%e7%bc%93%e5%ad%98%e5%8e%9f%e7%90%86)。这里不再赘述。 + +### 使用 ConcurrentHashMap 作为缓存 + +参考配置: + +```xml + + + + 使用 ConcurrentHashMap 作为 Spring 缓存 + + + + + + + + + + + + + + + + +``` + +### 使用 Ehcache 作为缓存 + +参考配置: + +```xml + + + + 使用 EhCache 作为 Spring 缓存 + + + + + + + + + + + + + + + +``` + +ehcache.xml 中的配置内容完全符合 Ehcache 的官方配置标准。 + +### 使用 Caffeine 作为缓存 + +参考配置: + +```xml + + + + 使用 Caffeine 作为 Spring 缓存 + + + + + + + + + +``` + +## 示例代码 + +我的示例代码地址:[spring-tutorial-integration-cache](https://github.com/dunwu/spring-tutorial/tree/master/spring-tutorial/spring-tutorial-integration/spring-tutorial-integration-cache) + +## 参考资料 + +- [Spring 官方文档之缓存抽象](https://docs.spring.io/spring/docs/current/spring-framework-reference/integration.html#cache) +- [注释驱动的 Spring cache 缓存介绍](http://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/02.Spring\351\233\206\346\210\220\350\260\203\345\272\246\345\231\250.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/02.Spring\351\233\206\346\210\220\350\260\203\345\272\246\345\231\250.md" new file mode 100644 index 00000000..2f460e46 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/02.Spring\351\233\206\346\210\220\350\260\203\345\272\246\345\231\250.md" @@ -0,0 +1,350 @@ +--- +title: Spring 集成调度器 +date: 2017-11-08 16:53:27 +order: 02 +categories: + - Java + - 框架 + - Spring + - Spring集成 +tags: + - Java + - 框架 + - Spring + - 集成 + - 调度器 +permalink: /pages/a187f0/ +--- + +# Spring 集成调度器 + +## 概述 + +如果想在 Spring 中使用任务调度功能,除了集成调度框架 Quartz 这种方式,也可以使用 Spring 自己的调度任务框架。 +使用 Spring 的调度框架,优点是:支持注解`@Scheduler`,可以省去大量的配置。 + +## 实时触发调度任务 + +### TaskScheduler 接口 + +Spring3 引入了`TaskScheduler`接口,这个接口定义了调度任务的抽象方法。 +TaskScheduler 接口的声明: + +```java +public interface TaskScheduler { + + ScheduledFuture schedule(Runnable task, Trigger trigger); + + ScheduledFuture schedule(Runnable task, Date startTime); + + ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period); + + ScheduledFuture scheduleAtFixedRate(Runnable task, long period); + + ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay); + + ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay); + +} +``` + +从以上方法可以看出 TaskScheduler 有两类重要参数: + +- 一个是要调度的方法,即一个实现了 Runnable 接口的线程类的 run()方法; +- 另一个就是触发条件。 + +**TaskScheduler 接口的实现类** +它有三个实现类:`DefaultManagedTaskScheduler`、`ThreadPoolTaskScheduler`、`TimerManagerTaskScheduler`。 +**DefaultManagedTaskScheduler**:基于 JNDI 的调度器。 +**TimerManagerTaskScheduler**:托管`commonj.timers.TimerManager`实例的调度器。 +**ThreadPoolTaskScheduler**:提供线程池管理的调度器,它也实现了`TaskExecutor`接口,从而使的单一的实例可以尽可能快地异步执行。 + +#### Trigger 接口 + +Trigger 接口抽象了触发条件的方法。 +Trigger 接口的声明: + +``` +public interface Trigger { + Date nextExecutionTime(TriggerContext triggerContext); +} +``` + +**Trigger 接口的实现类** +**CronTrigger**:实现了 cron 规则的触发器类(和 Quartz 的 cron 规则相同)。 +**PeriodicTrigger**:实现了一个周期性规则的触发器类(例如:定义触发起始时间、间隔时间等)。 + +#### 完整范例 + +实现一个调度任务的功能有以下几个关键点: +**(1) 定义调度器** +在 spring-bean.xml 中进行配置 +使用`task:scheduler`标签定义一个大小为 10 的线程池调度器,spring 会实例化一个`ThreadPoolTaskScheduler`。 + +```xml + + + + + +``` + +**_注:不要忘记引入 xsd:_** + +```xml +http://www.springframework.org/schema/task +http://www.springframework.org/schema/task/spring-task-3.1.xsd +``` + +**(2) 定义调度任务** +定义实现`Runnable`接口的线程类。 + +``` +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DemoTask implements Runnable { + final Logger logger = LoggerFactory.getLogger(this.getClass()); + + @Override + public void run() { + logger.info("call DemoTask.run"); + } +} +``` + +**(3) 装配调度器,并执行调度任务** +在一个`Controller`类中用`@Autowired`注解装配`TaskScheduler`。 +然后调动 TaskScheduler 对象的 schedule 方法启动调度器,就可以执行调度任务了。 + +```java +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.scheduling.support.CronTrigger; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +@Controller +@RequestMapping("/scheduler") +public class SchedulerController { + @Autowired + TaskScheduler scheduler; + + @RequestMapping(value = "/start", method = RequestMethod.POST) + public void start() { + scheduler.schedule(new DemoTask(), new CronTrigger("0/5 * * * * *")); + } +} +``` + +访问/scheduler/start 接口,启动调度器,可以看到如下日志内容: + +``` +13:53:15.010 myScheduler-1 o.zp.notes.spring.scheduler.DemoTask.run - call DemoTask.run +13:53:20.003 myScheduler-1 o.zp.notes.spring.scheduler.DemoTask.run - call DemoTask.run +13:53:25.004 myScheduler-2 o.zp.notes.spring.scheduler.DemoTask.run - call DemoTask.run +13:53:30.005 myScheduler-1 o.zp.notes.spring.scheduler.DemoTask.run - call DemoTask.run +``` + +### @Scheduler 的使用方法 + +Spring 的调度器一个很大的亮点在于`@Scheduler`注解,这可以省去很多繁琐的配置。 + +#### 启动注解 + +使用@Scheduler 注解先要使用``启动注解开关。 +**_例:_** + +```xml + + + + + + + +``` + +#### @Scheduler 定义触发条件 + +例:使用`fixedDelay`指定触发条件为每 5000 毫秒执行一次。注意:必须在上一次调度成功后的 5000 秒才能执行。 + +```java +@Scheduled(fixedDelay=5000) +public void doSomething() { + // something that should execute periodically +} +``` + +例:使用`fixedRate`指定触发条件为每 5000 毫秒执行一次。注意:无论上一次调度是否成功,5000 秒后必然执行。 + +```java +@Scheduled(fixedRate=5000) +public void doSomething() { + // something that should execute periodically +} +``` + +例:使用`initialDelay`指定方法在初始化 1000 毫秒后才开始调度。 + +```java +@Scheduled(initialDelay=1000, fixedRate=5000) +public void doSomething() { + // something that should execute periodically +} +``` + +例:使用`cron`表达式指定触发条件为每 5000 毫秒执行一次。cron 规则和 Quartz 中的 cron 规则一致。 + +```java +@Scheduled(cron="*/5 * * * * MON-FRI") +public void doSomething() { + // something that should execute on weekdays only +} +``` + +#### 完整范例 + +**(1) 启动注解开关,并定义调度器和执行器** + +```xml + + + + + + + + +``` + +**(2) 使用@Scheduler 注解来修饰一个要调度的方法** +下面的例子展示了@Scheduler 注解定义触发条件的不同方式。 + +```java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.text.SimpleDateFormat; +import java.util.Date; + +/** + * @description 使用@Scheduler注解调度任务范例 + * @author Vicotr Zhang + * @date 2016年8月31日 + */ +@Component +public class ScheduledMgr { + private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + final Logger logger = LoggerFactory.getLogger(this.getClass()); + + /** + * 构造函数中打印初始化时间 + */ + public ScheduledMgr() { + logger.info("Current time: {}", dateFormat.format(new Date())); + } + + /** + * fixedDelay属性定义调度间隔时间。调度需要等待上一次调度执行完成。 + */ + @Scheduled(fixedDelay = 5000) + public void testFixedDelay() throws Exception { + Thread.sleep(6000); + logger.info("Current time: {}", dateFormat.format(new Date())); + } + + /** + * fixedRate属性定义调度间隔时间。调度不等待上一次调度执行完成。 + */ + @Scheduled(fixedRate = 5000) + public void testFixedRate() throws Exception { + Thread.sleep(6000); + logger.info("Current time: {}", dateFormat.format(new Date())); + } + + /** + * initialDelay属性定义初始化后的启动延迟时间 + */ + @Scheduled(initialDelay = 1000, fixedRate = 5000) + public void testInitialDelay() throws Exception { + Thread.sleep(6000); + logger.info("Current time: {}", dateFormat.format(new Date())); + } + + /** + * cron属性支持使用cron表达式定义触发条件 + */ + @Scheduled(cron = "0/5 * * * * ?") + public void testCron() throws Exception { + Thread.sleep(6000); + logger.info("Current time: {}", dateFormat.format(new Date())); + } +} +``` + +我刻意设置触发方式的间隔都是 5s,且方法中均有 Thread.sleep(6000);语句。从而确保方法在下一次调度触发时间点前无法完成执行,来看一看各种方式的表现吧。 +启动 spring 项目后,spring 会扫描`@Component`注解,然后初始化 ScheduledMgr。 +接着,spring 会扫描`@Scheduler`注解,初始化调度器。调度器在触发条件匹配的情况下开始工作,输出日志。 +截取部分打印日志来进行分析。 + +``` +10:58:46.479 localhost-startStop-1 o.z.n.s.scheduler.ScheduledTasks. - Current time: 2016-08-31 10:58:46 +10:58:52.523 myScheduler-1 o.z.n.s.scheduler.ScheduledTasks.testFixedRate - Current time: 2016-08-31 10:58:52 +10:58:52.523 myScheduler-3 o.z.n.s.scheduler.ScheduledTasks.testFixedDelay - Current time: 2016-08-31 10:58:52 +10:58:53.524 myScheduler-2 o.z.n.s.scheduler.ScheduledTasks.testInitialDelay - Current time: 2016-08-31 10:58:53 +10:58:55.993 myScheduler-4 o.z.n.s.scheduler.ScheduledTasks.testCron - Current time: 2016-08-31 10:58:55 +10:58:58.507 myScheduler-1 o.z.n.s.scheduler.ScheduledTasks.testFixedRate - Current time: 2016-08-31 10:58:58 +10:58:59.525 myScheduler-5 o.z.n.s.scheduler.ScheduledTasks.testInitialDelay - Current time: 2016-08-31 10:58:59 +10:59:03.536 myScheduler-3 o.z.n.s.scheduler.ScheduledTasks.testFixedDelay - Current time: 2016-08-31 10:59:03 +10:59:04.527 myScheduler-1 o.z.n.s.scheduler.ScheduledTasks.testFixedRate - Current time: 2016-08-31 10:59:04 +10:59:05.527 myScheduler-4 o.z.n.s.scheduler.ScheduledTasks.testInitialDelay - Current time: 2016-08-31 10:59:05 +10:59:06.032 myScheduler-2 o.z.n.s.scheduler.ScheduledTasks.testCron - Current time: 2016-08-31 10:59:06 +10:59:10.534 myScheduler-9 o.z.n.s.scheduler.ScheduledTasks.testFixedRate - Current time: 2016-08-31 10:59:10 +10:59:11.527 myScheduler-10 o.z.n.s.scheduler.ScheduledTasks.testInitialDelay - Current time: 2016-08-31 10:59:11 +10:59:14.524 myScheduler-4 o.z.n.s.scheduler.ScheduledTasks.testFixedDelay - Current time: 2016-08-31 10:59:14 +10:59:15.987 myScheduler-6 o.z.n.s.scheduler.ScheduledTasks.testCron - Current time: 2016-08-31 10:59:15 +``` + +构造方法打印一次,时间点在 10:58:46。 +testFixedRate 打印四次,每次间隔 6 秒。说明,fixedRate 不等待上一次调度执行完成,在间隔时间达到时立即执行。 +testFixedDelay 打印三次,每次间隔大于 6 秒,且时间不固定。说明,fixedDelay 等待上一次调度执行成功后,开始计算间隔时间,再执行。 +testInitialDelay 第一次调度时间和构造方法调度时间相隔 7 秒。说明,initialDelay 在初始化后等待指定的延迟时间才开始调度。 +testCron 打印三次,时间间隔并非 5 秒或 6 秒,显然,cron 等待上一次调度执行成功后,开始计算间隔时间,再执行。 +此外,可以从日志中看出,打印日志的线程最多只有 10 个,说明 2.1 中的调度器线程池配置生效。 + +## 参考 + +[Spring Framework 官方文档](http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/03.Spring\351\233\206\346\210\220Dubbo.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/03.Spring\351\233\206\346\210\220Dubbo.md" new file mode 100644 index 00000000..a36f1168 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/03.Spring\351\233\206\346\210\220Dubbo.md" @@ -0,0 +1,260 @@ +--- +title: Spring集成Dubbo +date: 2017-10-27 17:30:41 +order: 03 +categories: + - Java + - 框架 + - Spring + - Spring集成 +tags: + - Java + - 框架 + - Spring + - 集成 + - Dubbo +permalink: /pages/274fd7/ +--- + +# Spring 集成 Dubbo + +## ZooKeeper + +ZooKeeper 可以作为 Dubbo 的注册中心。 + +Dubbo 未对 Zookeeper 服务器端做任何侵入修改,只需安装原生的 Zookeeper 服务器即可,所有注册中心逻辑适配都在调用 Zookeeper 客户端时完成。 + +**安装** + +在 [ZooKeeper 发布中心](http://zookeeper.apache.org/releases.html) 选择需要的版本,下载后解压到本地。 + +**配置** + +``` +vi conf/zoo.cfg + +``` + +如果不需要集群,`zoo.cfg` 的内容如下 [2](https://dubbo.gitbooks.io/dubbo-admin-book/content/install/zookeeper.html#fn_2): + +``` +tickTime=2000 +initLimit=10 +syncLimit=5 +dataDir=/home/dubbo/zookeeper-3.3.3/data +clientPort=2181 +``` + +如果需要集群,`zoo.cfg` 的内容如下 [3](https://dubbo.gitbooks.io/dubbo-admin-book/content/install/zookeeper.html#fn_3): + +``` +tickTime=2000 +initLimit=10 +syncLimit=5 +dataDir=/home/dubbo/zookeeper-3.3.3/data +clientPort=2181 +server.1=10.20.153.10:2555:3555 +server.2=10.20.153.11:2555:3555 + +``` + +并在 data 目录 [4](https://dubbo.gitbooks.io/dubbo-admin-book/content/install/zookeeper.html#fn_4) 下放置 myid 文件: + +``` +mkdir data +vi myid + +``` + +myid 指明自己的 id,对应上面 `zoo.cfg` 中 `server.` 后的数字,第一台的内容为 1,第二台的内容为 2,内容如下: + +``` +1 + +``` + +**启动** + +Linux 下执行 `bin/zkServer.sh` ;Windows `bin/zkServer.cmd` 启动 ZooKeeper 。 + +**命令行** + +``` +telnet 127.0.0.1 2181 +dump +``` + +或者: + +``` +echo dump | nc 127.0.0.1 2181 +``` + +用法: + +``` +dubbo.registry.address=zookeeper://10.20.153.10:2181?backup=10.20.153.11:2181 + +``` + +或者: + +``` + + +``` + +> 1. Zookeeper 是 Apache Hadoop 的子项目,强度相对较好,建议生产环境使用该注册中心 +> 2. 其中 data 目录需改成你真实输出目录 +> 3. 其中 data 目录和 server 地址需改成你真实部署机器的信息 +> 4. 上面 `zoo.cfg` 中的 `dataDir` +> 5. [http://zookeeper.apache.org/doc/r3.3.3/zookeeperAdmin.html](http://zookeeper.apache.org/doc/r3.3.3/zookeeperAdmin.html) + +## Dubbo + +Dubbo 采用全 Spring 配置方式,透明化接入应用,对应用没有任何 API 侵入,只需用 Spring 加载 Dubbo 的配置即可,Dubbo 基于 Spring 的 Schema 扩展进行加载。 + +如果不想使用 Spring 配置,可以通过 [API 的方式](https://dubbo.gitbooks.io/configuration/api.md) 进行调用。 + +## 服务提供者 + +完整安装步骤,请参见:[示例提供者安装](https://dubbo.gitbooks.io/dubbo-admin-book/install/provider-demo.html) + +### 定义服务接口 + +DemoService.java [1](https://dubbo.gitbooks.io/dubbo-user-book/quick-start.html#fn_1): + +```java +package com.alibaba.dubbo.demo; + +public interface DemoService { + String sayHello(String name); +} +``` + +### 在服务提供方实现接口 + +DemoServiceImpl.java [2](https://dubbo.gitbooks.io/dubbo-user-book/quick-start.html#fn_2): + +```java +package com.alibaba.dubbo.demo.provider; + +import com.alibaba.dubbo.demo.DemoService; + +public class DemoServiceImpl implements DemoService { + public String sayHello(String name) { + return "Hello " + name; + } +} +``` + +### 用 Spring 配置声明暴露服务 + +provider.xml: + +```xml + + + + + + + + + + + + + + + + + + +``` + +如果注册中心使用 ZooKeeper,可以将 dubbo:registry 改为 zookeeper://127.0.0.1:2181 + +### 加载 Spring 配置 + +Provider.java: + +```java +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class Provider { + public static void main(String[] args) throws Exception { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"http://10.20.160.198/wiki/display/dubbo/provider.xml"}); + context.start(); + System.in.read(); // 按任意键退出 + } +} +``` + +## 服务消费者 + +完整安装步骤,请参见:[示例消费者安装](https://dubbo.gitbooks.io/dubbo-admin-book/install/consumer-demo.html) + +### 通过 Spring 配置引用远程服务 + +consumer.xml: + +```xml + + + + + + + + + + + + +``` + +如果注册中心使用 ZooKeeper,可以将 dubbo:registry 改为 zookeeper://127.0.0.1:2181 + +### 加载 Spring 配置,并调用远程服务 + +Consumer.java [3](https://dubbo.gitbooks.io/dubbo-user-book/quick-start.html#fn_3): + +``` +import org.springframework.context.support.ClassPathXmlApplicationContext; +import com.alibaba.dubbo.demo.DemoService; + +public class Consumer { + public static void main(String[] args) throws Exception { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"http://10.20.160.198/wiki/display/dubbo/consumer.xml"}); + context.start(); + DemoService demoService = (DemoService)context.getBean("demoService"); // 获取远程服务代理 + String hello = demoService.sayHello("world"); // 执行远程方法 + System.out.println( hello ); // 显示调用结果 + } +} +``` + +> 1. 该接口需单独打包,在服务提供方和消费方共享 +> 2. 对服务消费方隐藏实现 +> 3. 也可以使用 IoC 注入 + +## FAQ + +建议使用 `dubbo-2.3.3` 以上版本的 zookeeper 注册中心客户端。 + +## 资料 + +**Dubbo** + +[Github](https://github.com/alibaba/dubbo) | [用户手册](https://dubbo.gitbooks.io/dubbo-user-book/content/) | [开发手册](https://dubbo.gitbooks.io/dubbo-dev-book/content/) | [管理员手册](https://dubbo.gitbooks.io/dubbo-admin-book/content/) + +**ZooKeeper** + +[官网](http://zookeeper.apache.org/) | [官方文档](http://zookeeper.apache.org/doc/trunk/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/README.md" new file mode 100644 index 00000000..068cd0a8 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/README.md" @@ -0,0 +1,45 @@ +--- +title: Spring 集成 +date: 2020-02-26 23:47:47 +categories: + - Java + - 框架 + - Spring + - Spring集成 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 集成 +permalink: /pages/d6025b/ +hidden: true +index: false +--- + +# Spring 集成 + +> 章节主要针对:Spring 与第三方框架、库集成。如:Cache、Scheduling、JMS、JMX 等。 + +## 📖 内容 + +- [Spring 集成缓存中间件](01.Spring集成缓存.md) +- [Spring 集成定时任务中间件](02.Spring集成调度器.md) +- [Spring 集成 Dubbo](03.Spring集成Dubbo.md) + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/10.Spring\345\256\211\345\205\250/01.SpringBoot\344\271\213\345\256\211\345\205\250\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/10.Spring\345\256\211\345\205\250/01.SpringBoot\344\271\213\345\256\211\345\205\250\345\277\253\351\200\237\345\205\245\351\227\250.md" new file mode 100644 index 00000000..90cd3fd6 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/10.Spring\345\256\211\345\205\250/01.SpringBoot\344\271\213\345\256\211\345\205\250\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -0,0 +1,46 @@ +--- +title: SpringBoot 之安全快速入门 +date: 2021-05-13 18:21:56 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring安全 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 安全 +permalink: /pages/568352/ +--- + +# SpringBoot 之安全快速入门 + +## QuickStart + +(1)添加依赖 + +```xml + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + +``` + +(2)添加配置 + +```properties +spring.security.user.name = root +spring.security.user.password = root +spring.security.user.roles = USER +``` + +(3)启动应用后,访问任意路径,都会出现以下页面,提示你先执行登录操作。输入配置的用户名、密码(root/root)即可访问应用页面。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/image-20191118150326556.png) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/01.Spring4\345\215\207\347\272\247.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/01.Spring4\345\215\207\347\272\247.md" new file mode 100644 index 00000000..486e5600 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/01.Spring4\345\215\207\347\272\247.md" @@ -0,0 +1,345 @@ +--- +title: Spring 4 升级踩雷指南 +date: 2017-12-15 15:10:32 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring其他 +tags: + - Java + - 框架 + - Spring +permalink: /pages/752c6a/ +--- + +# Spring 4 升级踩雷指南 + +## 前言 + +最近,一直在为公司老项目做核心库升级工作。本来只是想升级一下 JDK8 ,却因为兼容性问题而不得不升级一些其他的库,而其他库本身依赖的一些库可能也要同步升级。这是一系列连锁问题,你很难一一识别,往往只有在编译时、运行时才能发现问题。 + +总之,这是个费劲的活啊。 + +本文小结一下升级 Spring4 的连锁问题。 + +## 为什么升级 spring4 + +升级 Spring4 的原因是:Spring 4 以前的版本不兼容 JDK8。当你的项目同时使用 Spring3 和 JDK8,如果代码中有使用 JDK8 字节码或 Lambada 表达式,那么会出问题。 + +也许你会问,为什么不使用最新的 Spring 5 呢?因为作为企业软件,一般更倾向使用稳定的版本(bug 少),而不是最新的版本,尤其是一些核心库。 + +更多细节可以参考: + +https://spring.io/blog/2013/05/21/spring-framework-4-0-m1-3-2-3-available/ + +## spring 4 重要新特性 + +Spring 4 相比 Spring 3,引入许多新特性,这里列举几条较为重要的: + +1. 支持 `JDK8` (这个是最主要的)。 +2. `Groovy Bean Definition DSL` 风格配置。 +3. 支持 WebSocket、SockJS、STOMP 消息 +4. 移除 Deprecated 包和方法 +5. 一些功能加强,如:核心容器、Web、Test 等等,不一一列举。 + +更多 Spring 4 新特性可以参考: + +https://docs.spring.io/spring/docs/4.3.14.BUILD-SNAPSHOT/spring-framework-reference/htmlsingle/#spring-whats-new + +http://jinnianshilongnian.iteye.com/blog/1995111 + +## 升级 spring 4 步骤 + +了解了前面内容,我们知道了升级 Spring 4 带来的好处。现在开始真刀真枪的升级了。 + +不要以为升级一下 Spring 4,仅仅是改一下版本号,那么简单,细节处多着呢。 + +下面,结合我在公司项目升级 Spring4 时遇到的一系列坑,希望能帮助各位少走弯路。 + +> **注** +> +> 下文内容基于假设你的项目是用 maven 管理这一前提。如果不满足这一前提,那么这篇文章对你没什么太大帮助。 + +### 修改 spring 版本 + +第一步,当然是修改 pom.xml 中的 spring 版本。 + +`3.x.x.RELEASE` > `4.x.x.RELEASE` + +实例:升级 spring-core + +其它 spring 库的升级也如此: + +```xml + + 4.3.13.RELEASE + + + org.springframework + spring-core + ${spring.version} + +``` + +### 修改 spring xml 文件的 xsd + +用过 spring 的都知道,spring 通常依赖于大量的 xml 配置。 + +spring 的 xml 解析器在解析 xml 时,需要读取 xml schema,schema 定义了 xml 的命名空间。它的好处在于可以避免命名冲突,有点像 Java 中的 package。 + +实例:一个 spring xml 的 schema + +```xml + + +``` + +> **说明** +> +> - `xmlns="http://www.springframework.org/schema/beans"` 声明 xml 文件默认的命名空间,表示未使用其他命名空间的所有标签的默认命名空间。 +> +> - `xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"` 声明 XML Schema 实例,声明后就可以使用 schemaLocation 属性了。 +> +> - `xmlns:mvc="http://www.springframework.org/schema/mvc"` +> 声明前缀为 mvc 的命名空间,后面的 URL 用于标示命名空间的地址不会被解析器用于查找信息。其惟一的作用是赋予命名空间一个惟一的名称。当命名空间被定义在元素的开始标签中时,所有带有相同前缀的子元素都会与同一个命名空间相关联。 其它的类似 `xmlns:context` 、`xmlns:jdbc` 等等同样如此。 +> +> - ``` +> xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd +> ..." +> ``` +> +> 这个从命名可以看出个大概,指定 schema 位置这个属性必须结合命名空间使用。这个属性有两个值,第一个值表示需要使用的命名空间。第二个值表示供命名空间使用的 xml schema 的位置。 + +上面示例中的 xsd 版本是 `3.1.xsd` ,表示 spring 的 xml 解析器会将其视为 3.1 版本的 xml 文件来处理。 + +现在,我们使用了 Spring 4,`3.1.xsd` 版本显然就不正确了,我们可以根据自己引入的 Spring 4 的子版本号将其改为 `4.x.xsd` 。 + +但是,还有一种更好的做法:把这个指定 xsd 版本的关键字干掉,类似这样:`http://www.springframework.org/schema/tx/spring-tx.xsd` 。 + +**这么做的原因如下:** + +- Spring 默认在启动时要加载 xsd 文件来验证 xml 文件。 +- 如果没有提供 `schemaLocation`,那么 spring 的 xml 解析器会从 namespace 的 uri 里加载 xsd 文件。 +- `schemaLocation` 提供了一个 xml namespace 到对应的 xsd 文件的一个映射。 +- 如果不指定 spring xsd 的版本号,spring 取的就是当前本地 jar 里的 xsd 文件,减少了各种风险(比如 xsd 与实际 spring jar 版本不一致)。 + +更多详细内容可以参考这篇文章:[为什么在 Spring 的配置里,最好不要配置 xsd 文件的版本号](http://blog.csdn.net/hengyunabc/article/details/22295749) + +### 修改 spring xml 文件 + +spring 4 对 xml 做了一些改动。这里说一个最常用的改动: + +#### ref local + +spring 不再支持 `ref` 元素的 `local` 属性,如果你的项目中使用了,需要改为 `bean`。 + +shi + +spring 4 以前: + +```xml + + + + + +``` + +spring 4 以后: + +```xml + + + + + +``` + +如果不改启动会报错: + +``` +Caused by: org.xml.sax.SAXParseException: cvc-complex-type.3.2.2: Attribute 'local' is not allowed to appear in element 'ref'. +``` + +当然,可能还有一些其他配置改动,这个只能说兵来将挡水来土掩,遇到了再去查官方文档吧。 + +### 加入 spring support + +spring 3 中很多的扩展内容不需要引入 support 。但是 spring 4 中分离的更彻底了,如果不分离,会有很多`ClassNotFound` 。 + +```xml + + org.springframework + spring-context-support + 4.2.3.RELEASE + +``` + +### 更换 spring-mvc jackson + +spring mvc 中如果返回结果为 json 需要依赖 jackson 的 jar 包,但是他升级到了 2, 以前是 `codehaus.jackson`,现在换成了 `fasterxml.jackson` + +```xml + + com.fasterxml.jackson.core + jackson-core + 2.7.0 + + + com.fasterxml.jackson.core + jackson-databind + 2.7.0 + +``` + +同时修改 spring mvc 的配置文件: + +```xml + + + + + + + + + + + + + + text/plain;charset=UTF-8 + + + +``` + +### 解决 ibatis 兼容问题 + +**问题** + +如果你的项目中使用了 ibatis (mybatis 的前身)这个 orm 框架,当 spring3 升级 spring4 后,会出现兼容性问题,编译都不能通过。 + +这是因为 Spring4 官方已经不再支持 ibatis。 + +**解决方案** + +添加兼容性 jar 包 + +```xml + + org.mybatis + mybatis-2-spring + 1.0.1 + +``` + +更多内容可参考:https://stackoverflow.com/questions/32353286/no-support-for-ibatis-in-spring4-2-0 + +### 升级 Dubbo + +我们的项目中使用了 soa 框架 Dubbo 。由于 Dubbo 是老版本的,具体来说是(2013 年的 2.4.10),而老版本中使用的 spirng 版本为 2.x,有兼容性问题。 + +Dubbo 项目从今年开始恢复维护了,首先把一些落后的库升级到较新版本,比如 jdk8,spring4 等,并修复了一些 bug。所以,我们可以通过升级一下 Dubbo 版本来解决问题。 + +```xml + + com.alibaba + dubbo + 2.5.8 + + + org.springframework + spring-context + + + org.springframework + spring-web + + + org.javassist + javassist + + + +``` + +### 升级 Jedis + +升级 Dubbo 为当前最新的 2.5.8 版本后,运行时报错: + +- **JedisPoolConfig 配置错误** + +``` +Caused by: java.lang.ClassNotFoundException: org.apache.commons.pool2.impl.GenericObjectPoolConfig +``` + +由于项目中使用了 redis,版本为 2.0.0 ,这个问题是由于 jedis 需要升级: + +```xml + + redis.clients + jedis + 2.9.0 + +``` + +jedis 2.4.1 以上版本的 `JedisPoolConfig` 已经没有了`maxActive` 和 `maxWait` 属性。 + +修改方法如下: + +**maxActive** > **maxTotal** + +**maxWait** > **maxWaitMillis** + +```xml + + + + + + +``` + +JedisPool 配置错误 + +``` +InvalidURIException: Cannot open Redis connection due invalid URI +``` + +原来的配置如下: + +```xml + + + + + +``` + +查看源码可以发现,初始化 JedisPool 时未指定结构方法参数的类型,导致 host 字符串值被视为 URI 类型,当然类型不匹配。 + +解决方法是修改上面的 host 配置,为:`` + +--- + +至此,spring 4 升级结束。后面如果遇到其他升级问题再补充。 + +## 资料 + +- https://spring.io/blog/2013/05/21/spring-framework-4-0-m1-3-2-3-available/ +- https://docs.spring.io/spring/docs/4.3.14.BUILD-SNAPSHOT/spring-framework-reference/htmlsingle/#spring-whats-new +- [Spring 3.x 升级到 Spring 4.x 注意事项和步骤,错误解决方法](http://www.sojson.com/blog/145.html) +- http://jinnianshilongnian.iteye.com/blog/1995111 +- [为什么在 Spring 的配置里,最好不要配置 xsd 文件的版本号](http://blog.csdn.net/hengyunabc/article/details/22295749) +- https://stackoverflow.com/questions/32353286/no-support-for-ibatis-in-spring4-2-0 diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/21.SpringBoot\344\271\213banner.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/21.SpringBoot\344\271\213banner.md" new file mode 100644 index 00000000..09a1e61c --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/21.SpringBoot\344\271\213banner.md" @@ -0,0 +1,131 @@ +--- +title: SpringBoot 之 banner 定制 +date: 2018-12-21 23:22:44 +order: 21 +categories: + - Java + - 框架 + - Spring + - Spring其他 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/bac2ce/ +--- + +# SpringBoot 之 banner 定制 + +## 简介 + +Spring Boot 启动时默认会显示以下 LOGO: + +``` + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v2.1.1.RELEASE) +``` + +实际上,Spring Boot 支持自定义 logo 的功能。 + +让我们来看看如何实现的。 + +只要你在 `resources` 目录下放置名为 `banner.txt`、`banner.gif` 、`banner.jpg` 或 `banner.png` 的文件,Spring Boot 会自动加载,将其作为启动时打印的 logo。 + +- 对于文本文件,Spring Boot 会将其直接输出。 +- 对于图像文件( `banner.gif` 、`banner.jpg` 或 `banner.png` ),Spring Boot 会将图像转为 ASCII 字符,然后输出。 + +## 变量 + +banner.txt 文件中还可以使用变量来设置字体、颜色、版本号。 + +| 变量 | 描述 | +| :------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `${application.version}` | `MANIFEST.MF` 中定义的版本。如:`1.0` | +| `${application.formatted-version}` | `MANIFEST.MF` 中定义的版本,并添加一个 `v` 前缀。如:`v1.0` | +| `${spring-boot.version}` | Spring Boot 版本。如:`2.1.1.RELEASE`. | +| `${spring-boot.formatted-version}` | Spring Boot 版本,并添加一个 `v` 前缀。如:`v2.1.1.RELEASE` | +| `${Ansi.NAME}` (or `${AnsiColor.NAME}`, `${AnsiBackground.NAME}`, `${AnsiStyle.NAME}`) | ANSI 颜色、字体。更多细节,参考:[`AnsiPropertySource`](https://github.com/spring-projects/spring-boot/tree/v2.1.1.RELEASE/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/ansi/AnsiPropertySource.java)。 | +| `${application.title}` | `MANIFEST.MF` 中定义的应用名。 | + +示例: + +在 Spring Boot 项目中的 `resources` 目录下添加一个名为 banner.txt 的文件,内容如下: + +``` +${AnsiColor.BRIGHT_YELLOW}${AnsiStyle.BOLD} + ________ ___ ___ ________ ___ __ ___ ___ +|\ ___ \|\ \|\ \|\ ___ \|\ \ |\ \|\ \|\ \ +\ \ \_|\ \ \ \\\ \ \ \\ \ \ \ \ \ \ \ \ \\\ \ + \ \ \ \\ \ \ \\\ \ \ \\ \ \ \ \ __\ \ \ \ \\\ \ + \ \ \_\\ \ \ \\\ \ \ \\ \ \ \ \|\__\_\ \ \ \\\ \ + \ \_______\ \_______\ \__\\ \__\ \____________\ \_______\ + \|_______|\|_______|\|__| \|__|\|____________|\|_______| +${AnsiBackground.WHITE}${AnsiColor.RED}${AnsiStyle.UNDERLINE} +:: Spring Boot :: (v${spring-boot.version}) +:: Spring Boot Tutorial :: (v1.0.0) +``` + +> 注:`${}` 设置字体颜色的变量之间不能换行或空格分隔,否则会导致除最后一个变量外,都不生效。 + +启动应用后,控制台将打印如下 logo: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181221231330.png) +推荐两个生成字符画的网站,可以将生成的字符串放入这个`banner.txt` 文件: + +- +- + +## 配置 + +`application.properties` 中与 Banner 相关的配置: + +```properties +# banner 模式。有三种模式:console/log/off +# console 打印到控制台(通过 System.out) +# log - 打印到日志中 +# off - 关闭打印 +spring.main.banner-mode = off +# banner 文件编码 +spring.banner.charset = UTF-8 +# banner 文本文件路径 +spring.banner.location = classpath:banner.txt +# banner 图像文件路径(可以选择 png,jpg,gif 文件) +spring.banner.image.location = classpath:banner.gif +used). +# 图像 banner 的宽度(字符数) +spring.banner.image.width = 76 +# 图像 banner 的高度(字符数) +spring.banner.image.height = +# 图像 banner 的左边界(字符数) +spring.banner.image.margin = 2 +# 是否将图像转为黑色控制台主题 +spring.banner.image.invert = false +``` + +当然,你也可以在 YAML 文件中配置,例如: + +```yml +spring: + main: + banner-mode: off +``` + +## 编程 + +默认,Spring Boot 会注册一个 `SpringBootBanner` 的单例 Bean,用来负责打印 Banner。 + +如果想完全个人定制 Banner,可以这么做:先实现 `org.springframework.boot.Banner#printBanner` 接口来自己定制 Banner。在将这个 Banner 通过 `SpringApplication.setBanner(…)` 方法注入 Spring Boot。 + +## 示例源码 + +> 示例源码:[spring-boot-banner](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-banner) + +## 参考资料 + +- [Spring Boot 官方文档之 Customizing the Banner](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-banner) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/22.SpringBoot\344\271\213Actuator.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/22.SpringBoot\344\271\213Actuator.md" new file mode 100644 index 00000000..f49e516c --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/22.SpringBoot\344\271\213Actuator.md" @@ -0,0 +1,371 @@ +--- +title: SpringBoot Actuator 快速入门 +date: 2022-06-14 20:51:22 +order: 22 +categories: + - Java + - 框架 + - Spring + - Spring其他 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/c013cc/ +--- + +# SpringBoot Actuator 快速入门 + +[`spring-boot-actuator`](https://github.com/spring-projects/spring-boot/tree/v2.7.0/spring-boot-project/spring-boot-actuator) 模块提供了 Spring Boot 的所有生产就绪功能。启用这些功能的推荐方法是添加 `spring-boot-starter-actuator` 依赖。 + +如果是 Maven 项目,添加以下依赖: + +```xml + + + org.springframework.boot + spring-boot-starter-actuator + + +``` + +如果是 Gradle 项目,添加以下声明: + +```groovy +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-actuator' +} +``` + +## 端点(Endpoint) + +Actuator Endpoint 使 Spring Boot 用户可以监控应用,并和应用进行交互。Spring Boot 内置了许多 端点,并允许用户自定义端点。例如,`health` 端点提供基本的应用健康信息。 + +用户可以启用或禁用每个单独的端点并通过 HTTP 或 JMX 暴露它们(使它们可远程访问)。当端点被启用和公开时,它被认为是可用的。内置端点仅在可用时才会自动配置。大多数应用程序选择通过 HTTP 公开。例如,默认情况下,`health` 端点映射到 `/actuator/health`。 + +### 启用端点 + +默认情况下,除了 `shutdown` 之外的所有端点都已启用。要配置端点的启用,请使用 `management.endpoint..enabled` 属性。以下示例启用 `shutdown` 端点: + +```properties +management.endpoint.shutdown.enabled=true +``` + +如果您希望端点是明确指定才启用,请将 `management.endpoints.enabled-by-default` 属性设置为 false 并根据需要明确指定启用的端点,以下为示例: + +```properties +management.endpoints.enabled-by-default=false +management.endpoint.info.enabled=true +``` + +### 暴露端点 + +由于端点可能包含敏感信息,您应该仔细考虑何时暴露它们。下表显示了内置端点的默认曝光: + +| ID | JMX | Web | +| :----------------- | :-- | :-- | +| `auditevents` | Yes | No | +| `beans` | Yes | No | +| `caches` | Yes | No | +| `conditions` | Yes | No | +| `configprops` | Yes | No | +| `env` | Yes | No | +| `flyway` | Yes | No | +| `health` | Yes | Yes | +| `heapdump` | N/A | No | +| `httptrace` | Yes | No | +| `info` | Yes | No | +| `integrationgraph` | Yes | No | +| `jolokia` | N/A | No | +| `logfile` | N/A | No | +| `loggers` | Yes | No | +| `liquibase` | Yes | No | +| `metrics` | Yes | No | +| `mappings` | Yes | No | +| `prometheus` | N/A | No | +| `quartz` | Yes | No | +| `scheduledtasks` | Yes | No | +| `sessions` | Yes | No | +| `shutdown` | Yes | No | +| `startup` | Yes | No | +| `threaddump` | Yes | No | + +要更改暴露的端点,请使用以下特定于技术的包含和排除属性: + +| Property | Default | +| :------------------------------------------ | :------- | +| `management.endpoints.jmx.exposure.exclude` | | +| `management.endpoints.jmx.exposure.include` | `*` | +| `management.endpoints.web.exposure.exclude` | | +| `management.endpoints.web.exposure.include` | `health` | + +`include` 属性列出了暴露的端点的 ID。 `exclude` 属性列出了不应暴露的端点的 ID。 `exclude` 属性优先于 `include` 属性。您可以使用端点 ID 列表配置包含和排除属性。 + +例如,仅暴露 `health` 和 info 端点,其他端点都不通过 JMX 暴露,可以按如下配置: + +```properties +management.endpoints.jmx.exposure.include=health,info +``` + +注意:`*` 可用于选择所有端点。 + +### 安全 + +出于安全考虑,只有 `/health` 端点会通过 HTTP 方式暴露。用户可以通过 `management.endpoints.web.exposure.include` 决定哪些端点可以通过 HTTP 方式暴露。 + +如果 Spring Security 在类路径上并且不存在其他 `WebSecurityConfigurerAdapter` 或 `SecurityFilterChain` bean,则除 `/health` 之外的所有 actuator 都由 Spring Boot 自动启用安全控制。如果用户自定义了 `WebSecurityConfigurerAdapter` 或 `SecurityFilterChain` bean,Spring Boot 不再启用安全控制,由用户自行控制访问规则。 + +如果您希望为 HTTP 端点定义安全控制(例如,只允许具有特定角色的用户访问它们),Spring Boot 提供了一些方便的 `RequestMatcher` 对象,您可以将它们与 Spring Security 结合使用。 + +下面是一个典型的 Spring Security 配置示例: + +```java +@Configuration(proxyBeanMethods = false) +public class MySecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.requestMatcher(EndpointRequest.toAnyEndpoint()) + .authorizeRequests((requests) -> requests.anyRequest().hasRole("ENDPOINT_ADMIN")); + http.httpBasic(); + return http.build(); + } + +} +``` + +前面的示例使用 EndpointRequest.toAnyEndpoint() 将请求匹配到任何端点,然后确保所有端点都具有 ENDPOINT_ADMIN 角色。 EndpointRequest 上还提供了其他几种匹配器方法。 + +如果希望无需身份验证即可访问所有执行器端点。可以通过更改 management.endpoints.web.exposure.include 属性来做到这一点,如下所示: + +```properties +management.endpoints.web.exposure.include=* +``` + +此外,如果存在 Spring Security,您将需要添加自定义安全配置,以允许未经身份验证的访问端点,如以下示例所示: + +```java +@Configuration(proxyBeanMethods = false) +public class MySecurityConfiguration { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.requestMatcher(EndpointRequest.toAnyEndpoint()) + .authorizeRequests((requests) -> requests.anyRequest().permitAll()); + return http.build(); + } + +} +``` + +由于 Spring Boot 依赖于 Spring Security 的默认设置,因此 CSRF 保护默认开启。这意味着在使用默认安全配置时,需要 POST(关闭和记录器端点)、PUT 或 DELETE 的执行器端点会收到 403(禁止)错误。 + +> 建议仅在创建非浏览器客户端使用的服务时完全禁用 CSRF 保护。 + +### 配置端点 + +端点会自动缓存对不带任何参数的读操作的响应数据。要配置端点缓存响应的时间量,请使用其 `cache.time-to-live` 属性。以下示例将 bean 端点缓存的生存时间设置为 10 秒: + +```properties +management.endpoint.beans.cache.time-to-live=10s +``` + +### Actuator Web 端点的超媒体 + +Spring Boot Actuator 中内置了一个“发现页面”端点,其中包含了所有端点的链接。默认情况下,“发现页面”在 `/actuator` 上可用。 + +要禁用“发现页面”,请将以下属性添加到您的应用程序属性中: + +```properties +management.endpoints.web.discovery.enabled=false +``` + +配置自定义管理上下文路径后,“发现页面”会自动从 `/actuator` 移动到应用管理上下文的根目录。例如,如果管理上下文路径是 `/management`,则发现页面可从 `/management` 获得。当管理上下文路径设置为 / 时,发现页面被禁用以防止与其他映射发生冲突的可能性。 + +### 跨域支持 + +CORS 是一种 W3C 规范,可让用户以灵活的方式指定授权哪种跨域请求。如果使用 Spring MVC 或 Spring WebFlux,则可以配置 Actuator 的 Web 端点以支持此类场景。 + +CORS 支持默认是禁用的,只有在设置 `management.endpoints.web.cors.allowed-origins` 属性后才会启用。以下配置允许来自 example.com 域的 GET 和 POST 调用: + +```properties +management.endpoints.web.cors.allowed-origins=https://example.com +management.endpoints.web.cors.allowed-methods=GET,POST +``` + +### 自定义端点 + +如果添加带有 `@Endpoint` 注释的 `@Bean`,则任何带有 `@ReadOperation`、`@WriteOperation` 或 `@DeleteOperation` 注释的方法都会自动通过 JMX 公开,并且在 Web 应用程序中,也可以通过 HTTP 公开。可以使用 Jersey、Spring MVC 或 Spring WebFlux 通过 HTTP 公开端点。如果 Jersey 和 Spring MVC 都可用,则使用 Spring MVC。 + +以下示例公开了一个返回自定义对象的读取操作: + +```java +@ReadOperation +public CustomData getData() { + return new CustomData("test", 5); +} +``` + +您还可以使用 `@JmxEndpoint` 或 `@WebEndpoint` 编写特定技术的端点。这些端点仅限于各自的技术。例如,`@WebEndpoint` 仅通过 HTTP 而不是通过 JMX 公开。 + +您可以使用 `@EndpointWebExtension` 和 `@EndpointJmxExtension` 编写特定技术的扩展。这些注释让您可以提供特定技术的操作来扩充现有端点。 + +最后,如果您需要访问 Web 框架的功能,您可以实现 servlet 或 Spring `@Controller` 和 `@RestController` 端点,但代价是它们无法通过 JMX 或使用不同的 Web 框架获得。 + +## 通过 HTTP 进行监控和管理 + +### 自定义管理端点路径 + +如果是 Web 应用,Spring Boot Actuator 会自动将所有启用的端点通过 HTTP 方式暴露。默认约定是使用前缀为 `/actuator` 的端点的 id 作为 URL 路径。例如,健康被暴露为 `/actuator/health`。 + +有时,自定义管理端点的前缀很有用。例如,您的应用程序可能已经将 `/actuator` 用于其他目的。您可以使用 `management.endpoints.web.base-path` 属性更改管理端点的前缀,如以下示例所示: + +```properties +management.endpoints.web.base-path=/manage +``` + +该示例将端点从 `/actuator/{id}` 更改为 `/manage/{id}`(例如,`/manage/info`)。 + +### 自定义管理服务器端口 + +```properties +management.server.port=8081 +``` + +### 配置 SSL + +当配置为使用自定义端口时,还可以使用各种 `management.server.ssl.*` 属性为管理服务器配置自己的 SSL。例如,这样做可以让管理服务器在主应用程序使用 HTTPS 时通过 HTTP 可用,如以下属性设置所示: + +```properties +server.port=8443 +server.ssl.enabled=true +server.ssl.key-store=classpath:store.jks +server.ssl.key-password=secret +management.server.port=8080 +management.server.ssl.enabled=false +``` + +或者,主服务器和管理服务器都可以使用 SSL,但使用不同的密钥存储,如下所示: + +```properties +server.port=8443 +server.ssl.enabled=true +server.ssl.key-store=classpath:main.jks +server.ssl.key-password=secret +management.server.port=8080 +management.server.ssl.enabled=true +management.server.ssl.key-store=classpath:management.jks +management.server.ssl.key-password=secret +``` + +### 自定义管理服务器地址 + +```properties +management.server.port=8081 +management.server.address=127.0.0.1 +``` + +### 禁用 HTTP 端点 + +如果您不想通过 HTTP 方式暴露端点,可以将管理端口设置为 -1,如以下示例所示: + +```properties +management.server.port=-1 +``` + +也可以通过使用 management.endpoints.web.exposure.exclude 属性来实现这一点,如以下示例所示: + +```properties +management.endpoints.web.exposure.exclude=* +``` + +## 通过 JMX 进行监控和管理 + +Java 管理扩展 (JMX) 提供了一种标准机制来监视和管理应用程序。默认情况下,此功能未启用。您可以通过将 `spring.jmx.enabled` 配置属性设置为 true 来打开它。 Spring Boot 将最合适的 `MBeanServer` 暴露为 ID 为 `mbeanServer` 的 bean。使用 Spring JMX 注释(`@ManagedResource`、`@ManagedAttribute` 或 `@ManagedOperation`)注释的任何 bean 都会暴露给它。 + +如果您的平台提供标准 `MBeanServer`,则 Spring Boot 会使用该标准并在必要时默认使用 VM `MBeanServer`。如果一切都失败了,则创建一个新的 `MBeanServer`。 + +有关更多详细信息,请参阅 [`JmxAutoConfiguration`](https://github.com/spring-projects/spring-boot/tree/v2.7.0/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jmx/JmxAutoConfiguration.java) 类。 + +默认情况下,Spring Boot 还将管理端点公开为 `org.springframework.boot` 域下的 JMX MBean。要完全控制 JMX 域中的端点注册,请考虑注册您自己的 `EndpointObjectNameFactory` 实现。 + +### 定制化 MBean Names + +MBean 的名称通常由端点的 id 生成。例如,健康端点公开为 `org.springframework.boot:type=Endpoint,name=Health`。 + +如果您的应用程序包含多个 Spring `ApplicationContext`,您可能会发现名称冲突。要解决此问题,您可以将 `spring.jmx.unique-names` 属性设置为 true,以便 MBean 名称始终是唯一的。 + +如果需要定制,跨域按如下配置: + +```properties +spring.jmx.unique-names=true +management.endpoints.jmx.domain=com.example.myapp +``` + +### 禁用 JMX 端点 + +想禁用 JMX 端点,可以按如下配置: + +``` +management.endpoints.jmx.exposure.exclude=* +``` + +### 将 Jolokia 用于基于 HTTP 的 JMX + +Jolokia 是一个 JMX-HTTP 的桥接工具,它提供了另一种访问 JMX bean 的方法。要使用 Jolokia,需要先添加依赖: + +```xml + + org.jolokia + jolokia-core + 🍃 **`spring-tutorial`** 是一个 Spring & Spring Boot 教程。 +> +> - 🔁 项目同步维护:[Github](https://github.com/dunwu/spring-tutorial/) | [Gitee](https://gitee.com/turnon/spring-tutorial/) +> - 📖 电子书阅读:[Github Pages](https://dunwu.github.io/spring-tutorial/) | [Gitee Pages](http://turnon.gitee.io/spring-tutorial/) + +## 📖 内容 + +### 综合 + +- [Spring 概述](00.Spring综合/01.Spring概述.md) +- [SpringBoot 知识图谱](00.Spring综合/21.SpringBoot知识图谱.md) +- [SpringBoot 基本原理](00.Spring综合/22.SpringBoot基本原理.md) +- [Spring 面试](00.Spring综合/99.Spring面试.md) + +### 核心 + +- [Spring Bean](01.Spring核心/01.SpringBean.md) +- [Spring IoC](01.Spring核心/02.SpringIoC.md) +- [Spring 依赖查找](01.Spring核心/03.Spring依赖查找.md) +- [Spring 依赖注入](01.Spring核心/04.Spring依赖注入.md) +- [Spring IoC 依赖来源](01.Spring核心/05.SpringIoC依赖来源.md) +- [Spring Bean 作用域](01.Spring核心/06.SpringBean作用域.md) +- [Spring Bean 生命周期](01.Spring核心/07.SpringBean生命周期.md) +- [Spring 配置元数据](01.Spring核心/08.Spring配置元数据.md) +- [Spring AOP](01.Spring核心/10.SpringAop.md) +- [Spring 资源管理](01.Spring核心/20.Spring资源管理.md) +- [Spring 校验](01.Spring核心/21.Spring校验.md) +- [Spring 数据绑定](01.Spring核心/22.Spring数据绑定.md) +- [Spring 类型转换](01.Spring核心/23.Spring类型转换.md) +- [Spring EL 表达式](01.Spring核心/24.SpringEL.md) +- [Spring 事件](01.Spring核心/25.Spring事件.md) +- [Spring 国际化](01.Spring核心/26.Spring国际化.md) +- [Spring 泛型处理](01.Spring核心/27.Spring泛型处理.md) +- [Spring 注解](01.Spring核心/28.Spring注解.md) +- [Spring Environment 抽象](01.Spring核心/29.SpringEnvironment抽象.md) +- [SpringBoot 教程之快速入门](01.Spring核心/31.SpringBoot之快速入门.md) +- [SpringBoot 之属性加载](01.Spring核心/32.SpringBoot之属性加载.md) +- [SpringBoot 之 Profile](01.Spring核心/33.SpringBoot之Profile.md) + +### 数据 + +- [Spring 之数据源](02.Spring数据/01.Spring之数据源.md) +- [Spring 之 JDBC](02.Spring数据/02.Spring之JDBC.md) +- [Spring 之事务](02.Spring数据/03.Spring之事务.md) +- [Spring 之 JPA](02.Spring数据/04.Spring之JPA.md) +- [Spring 集成 Mybatis](02.Spring数据/10.Spring集成Mybatis.md) +- [Spring 访问 Redis](02.Spring数据/21.Spring访问Redis.md) +- [Spring 访问 MongoDB](02.Spring数据/22.Spring访问MongoDB.md) +- [Spring 访问 Elasticsearch](02.Spring数据/23.Spring访问Elasticsearch.md) + +### Web + +- [Spring WebMvc](03.SpringWeb/01.SpringWebMvc.md) +- [SpringBoot 之应用 EasyUI](03.SpringWeb/21.SpringBoot之应用EasyUI.md) + +### IO + +- [SpringBoot 之异步请求](04.SpringIO/01.SpringBoot之异步请求.md) +- [SpringBoot 之 Json](04.SpringIO/02.SpringBoot之Json.md) +- [SpringBoot 之邮件](04.SpringIO/03.SpringBoot之邮件.md) + +### 集成 + +- [Spring 集成缓存中间件](05.Spring集成/01.Spring集成缓存.md) +- [Spring 集成定时任务中间件](05.Spring集成/02.Spring集成调度器.md) +- [Spring 集成 Dubbo](05.Spring集成/03.Spring集成Dubbo.md) + +### 其他 + +- [Spring4 升级](99.Spring其他/01.Spring4升级.md) +- [SpringBoot 之 banner](99.Spring其他/21.SpringBoot之banner.md) +- [SpringBoot 之 Actuator](99.Spring其他/22.SpringBoot之Actuator.md) + +## 💻 示例 + +### 核心篇示例 + +- [spring-core-actuator](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/actuator) - Spring 应用监控示例。 +- [spring-core-aop](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/aop) - Spring AOP 编程示例。 +- [spring-core-async](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/async) - Spring 使用异步接口示例。 +- [spring-core-banner](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/banner) - Spring 定制启动时的输出 Logo。 +- [spring-core-bean](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/bean) - Spring 管理 JavaBean 生命周期示例。 +- [spring-core-conversion](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/conversion) - Spring 数据转换示例。 +- [spring-core-data-binding](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/data-binding) - Spring 数据绑定示例。 +- [spring-core-ioc](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/ioc) - Spring IOC 示例。 +- [spring-core-profile](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/profile) - 在 Spring 中根据 profile 在不同的环境下执行不同的行为。 +- [spring-core-property](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/property) - 全方位的演示 Spring 加载属性的方式:记载 `properties` 和 `yaml` 两种文件;通过 `@Value`、`@ConfigurationProperties`、`Environment` 读取属性。 +- [spring-core-resource](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/resource) - Spring 资源加载示例。 +- [spring-core-validation](https://github.com/dunwu/spring-tutorial/tree/master/codes/core/validation) - Spring 数据校验示例。 + +### 数据篇示例 + +- **JDBC** + - [spring-data-jdbc-basics](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/basics) - Spring Boot 以 JDBC 方式访问关系型数据库,通过 `JdbcTemplate` 执行基本的 CRUD 操作。 + - [spring-data-jdbc-druid](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/druid) - SpringBoot 使用 [Druid](https://github.com/alibaba/druid) 作为数据库连接池。 + - [spring-data-jdbc-multi-datasource](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/multi-datasource) - SpringBoot 连接多数据源示例。 + - [spring-data-jdbc-xml](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/xml) - Spring 以 JDBC 方式访问关系型数据库,通过 `JdbcTemplate` 执行基本的 CRUD 操作。 +- **ORM** + - [spring-data-orm-jpa](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/jpa) - SpringBoot 使用 JPA 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis) - Spring 使用 [MyBatis](https://github.com/mybatis/mybatis-3) 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis-mapper](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis-mapper) - SpringBoot 使用 [MyBatis](https://github.com/mybatis/mybatis-3) + [Mapper](https://github.com/abel533/Mapper) + [PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis-multi-datasource](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis-multi-datasource) - SpringBoot 连接多数据源,并使用 [MyBatis Plus](https://github.com/baomidou/mybatis-plus) 作为 ORM 框架访问数据库示例。 + - [spring-data-orm-mybatis-plus](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/orm/mybatis-plus) - SpringBoot 使用 [MyBatis Plus](https://github.com/baomidou/mybatis-plus) 作为 ORM 框架访问数据库示例。 +- **Nosql** + - [spring-data-nosql-basics](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/basics) - Spring 访问各种 NoSQL 的示例。 + - [spring-data-nosql-mongodb](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/mongodb) - SpringBoot 访问 [MongoDB](https://www.mongodb.com/) 的示例。 + - [spring-data-nosql-redis](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/redis) - SpringBoot 访问 [Redis](https://redis.io/) 单节点、集群的示例。 + - [spring-data-nosql-elasticsearch](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/elasticsearch) - SpringBoot 访问 [Elasticsearch](https://www.elastic.co/guide/index.html) 的示例。 + - [spring-data-nosql-hdfs](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/nosql/hdfs) - SpringBoot 访问 HDFS 的示例。 +- **Cache** + - [spring-data-cache-basics](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/cache/basics) - SpringBoot 默认缓存框架的示例。 + - [spring-data-cache-j2cache](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/cache/j2cache) - SpringBoot 使用 [j2cache](https://gitee.com/ld/J2Cache) 作为缓存框架的示例。 + - [spring-data-cache-jetcache](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/cache/jetcache) - SpringBoot 使用 [jetcache](https://github.com/alibaba/jetcache) 作为缓存框架的示例。 +- **中间件** + - [spring-data-middleware-flyway](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/middleware/flyway) - Spring 使用版本管理中间件 Flyway 示例。 + - [spring-data-middleware-sharding](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/middleware/sharding) - Spring 使用分库分表中间件示例。 + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Github](https://github.com/spring-projects/spring-framework) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html) +- **书籍** + - [《 Spring 实战(第 5 版)》](https://book.douban.com/subject/34949443/) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/01.Mybatis\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/01.Mybatis\345\277\253\351\200\237\345\205\245\351\227\250.md" new file mode 100644 index 00000000..c6bbb278 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/01.Mybatis\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -0,0 +1,396 @@ +--- +title: Mybatis快速入门 +date: 2022-02-17 22:34:30 +order: 01 +categories: + - Java + - 框架 + - ORM +tags: + - Java + - 框架 + - ORM + - Mybatis +permalink: /pages/d4e6ee/ +--- + +# MyBatis 快速入门 + +> MyBatis 的前身就是 iBatis ,是一个作用在数据持久层的对象关系映射(Object Relational Mapping,简称 ORM)框架。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200716162305.png) + +## Mybatis 简介 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210510164925.png) + +### 什么是 MyBatis + +MyBatis 是一款持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。 + +### MyBatis vs. Hibernate + +MyBatis 和 Hibernate 都是 Java 世界中比较流行的 ORM 框架。我们应该了解其各自的优势,根据项目的需要去抉择在开发中使用哪个框架。 + +**Mybatis 优势** + +- MyBatis 可以进行更为细致的 SQL 优化,可以减少查询字段。 +- MyBatis 容易掌握,而 Hibernate 门槛较高。 + +**Hibernate 优势** + +- Hibernate 的 DAO 层开发比 MyBatis 简单,Mybatis 需要维护 SQL 和结果映射。 +- Hibernate 对对象的维护和缓存要比 MyBatis 好,对增删改查的对象的维护要方便。 +- Hibernate 数据库移植性很好,MyBatis 的数据库移植性不好,不同的数据库需要写不同 SQL。 +- Hibernate 有更好的二级缓存机制,可以使用第三方缓存。MyBatis 本身提供的缓存机制不佳。 + +## 快速入门 + +> 这里,我将以一个入门级的示例来演示 Mybatis 是如何工作的。 +> +> 注:本文后面章节中的原理、源码部分也将基于这个示例来进行讲解。 + +### 数据库准备 + +在本示例中,需要针对一张用户表进行 CRUD 操作。其数据模型如下: + +```sql +CREATE TABLE IF NOT EXISTS user ( + id BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Id', + name VARCHAR(10) NOT NULL DEFAULT '' COMMENT '用户名', + age INT(3) NOT NULL DEFAULT 0 COMMENT '年龄', + address VARCHAR(32) NOT NULL DEFAULT '' COMMENT '地址', + email VARCHAR(32) NOT NULL DEFAULT '' COMMENT '邮件', + PRIMARY KEY (id) +) COMMENT = '用户表'; + +INSERT INTO user (name, age, address, email) +VALUES ('张三', 18, '北京', 'xxx@163.com'); +INSERT INTO user (name, age, address, email) +VALUES ('李四', 19, '上海', 'xxx@163.com'); +``` + +### 添加 Mybatis + +如果使用 Maven 来构建项目,则需将下面的依赖代码置于 pom.xml 文件中: + +```xml + + org.mybatis + mybatis + x.x.x + +``` + +### Mybatis 配置 + +XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。 + +本示例中只是给出最简化的配置。 + +【示例】mybatis-config.xml 文件 + +```xml + + + + + + + + + + + + + + + + + + +``` + +> 说明:上面的配置文件中仅仅指定了数据源连接方式和 User 表的映射配置文件。 + +### Mapper + +#### Mapper.xml + +个人理解,Mapper.xml 文件可以看做是 Mybatis 的 JDBC SQL 模板。 + +【示例】UserMapper.xml 文件 + +下面是一个通过 Mybatis Generator 自动生成的完整的 Mapper 文件。 + +```xml + + + + + + + + + + + + delete from user + where id = #{id,jdbcType=BIGINT} + + + insert into user (id, name, age, + address, email) + values (#{id,jdbcType=BIGINT}, #{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER}, + #{address,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR}) + + + update user + set name = #{name,jdbcType=VARCHAR}, + age = #{age,jdbcType=INTEGER}, + address = #{address,jdbcType=VARCHAR}, + email = #{email,jdbcType=VARCHAR} + where id = #{id,jdbcType=BIGINT} + + + + +``` + +#### Mapper.java + +Mapper.java 文件是 Mapper.xml 对应的 Java 对象。 + +【示例】UserMapper.java 文件 + +```java +public interface UserMapper { + + int deleteByPrimaryKey(Long id); + + int insert(User record); + + User selectByPrimaryKey(Long id); + + List selectAll(); + + int updateByPrimaryKey(User record); + +} +``` + +对比 UserMapper.java 和 UserMapper.xml 文件,不难发现: + +UserMapper.java 中的方法和 UserMapper.xml 的 CRUD 语句元素( ``、``、``、`` 的 `parameterType` 属性以及 `` 的 `type` 属性都可能会绑定到数据实体。这样就可以把 JDBC 操作的输入输出和 JavaBean 结合起来,更加方便、易于理解。 + +### 测试程序 + +【示例】MybatisDemo.java 文件 + +```java +public class MybatisDemo { + + public static void main(String[] args) throws Exception { + // 1. 加载 mybatis 配置文件,创建 SqlSessionFactory + // 注:在实际的应用中,SqlSessionFactory 应该是单例 + InputStream inputStream = Resources.getResourceAsStream("mybatis/mybatis-config.xml"); + SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); + SqlSessionFactory factory = builder.build(inputStream); + + // 2. 创建一个 SqlSession 实例,进行数据库操作 + SqlSession sqlSession = factory.openSession(); + + // 3. Mapper 映射并执行 + Long params = 1L; + List list = sqlSession.selectList("io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey", params); + for (User user : list) { + System.out.println("user name: " + user.getName()); + } + // 输出:user name: 张三 + } + +} +``` + +> 说明: +> +> `SqlSession` 接口是 Mybatis API 核心中的核心,它代表了 Mybatis 和数据库的一次完整会话。 +> +> - Mybatis 会解析配置,并根据配置创建 `SqlSession` 。 +> - 然后,Mybatis 将 Mapper 映射为 `SqlSession`,然后传递参数,执行 SQL 语句并获取结果。 + +## Mybatis xml 配置 + +> 配置的详细内容请参考:“ [Mybatis 官方文档之配置](http://www.mybatis.org/mybatis-3/zh/configuration.html) ” 。 + +MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息。主要配置项有以下: + +- [properties(属性)](http://www.mybatis.org/mybatis-3/zh/configuration.html#properties) +- [settings(设置)](http://www.mybatis.org/mybatis-3/zh/configuration.html#settings) +- [typeAliases(类型别名)](http://www.mybatis.org/mybatis-3/zh/configuration.html#typeAliases) +- [typeHandlers(类型处理器)](http://www.mybatis.org/mybatis-3/zh/configuration.html#typeHandlers) +- [objectFactory(对象工厂)](http://www.mybatis.org/mybatis-3/zh/configuration.html#objectFactory) +- [plugins(插件)](http://www.mybatis.org/mybatis-3/zh/configuration.html#plugins) +- [environments(环境配置)](http://www.mybatis.org/mybatis-3/zh/configuration.html#environments) + - environment(环境变量) + - transactionManager(事务管理器) + - dataSource(数据源) +- [databaseIdProvider(数据库厂商标识)](http://www.mybatis.org/mybatis-3/zh/configuration.html#databaseIdProvider) +- [mappers(映射器)](http://www.mybatis.org/mybatis-3/zh/configuration.html#mappers) + +## Mybatis xml 映射器 + +> SQL XML 映射文件详细内容请参考:“ [Mybatis 官方文档之 XML 映射文件](http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html) ”。 + +XML 映射文件只有几个顶级元素: + +- `cache` – 对给定命名空间的缓存配置。 +- `cache-ref` – 对其他命名空间缓存配置的引用。 +- `resultMap` – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。 +- ~~`parameterMap` – 已被废弃!老式风格的参数映射。更好的办法是使用内联参数,此元素可能在将来被移除。文档中不会介绍此元素。~~ +- `sql` – 可被其他语句引用的可重用语句块。 +- `insert` – 映射插入语句 +- `update` – 映射更新语句 +- `delete` – 映射删除语句 +- `select` – 映射查询语句 + +## Mybatis 扩展 + +### Mybatis 类型处理器 + +MyBatis 在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成 Java 类型。下表描述了一些默认的类型处理器。 + +从 3.4.5 开始,MyBatis 默认支持 JSR-310(日期和时间 API) 。 + +你可以重写已有的类型处理器或创建你自己的类型处理器来处理不支持的或非标准的类型。 具体做法为:实现 `org.apache.ibatis.type.TypeHandler` 接口, 或继承一个很便利的类 `org.apache.ibatis.type.BaseTypeHandler`, 并且可以(可选地)将它映射到一个 JDBC 类型。比如: + +```java +// ExampleTypeHandler.java +@MappedJdbcTypes(JdbcType.VARCHAR) +public class ExampleTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { + ps.setString(i, parameter); + } + + @Override + public String getNullableResult(ResultSet rs, String columnName) throws SQLException { + return rs.getString(columnName); + } + + @Override + public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException { + return rs.getString(columnIndex); + } + + @Override + public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { + return cs.getString(columnIndex); + } +} +``` + +```xml + + + + +``` + +使用上述的类型处理器将会覆盖已有的处理 Java String 类型的属性以及 VARCHAR 类型的参数和结果的类型处理器。 要注意 MyBatis 不会通过检测数据库元信息来决定使用哪种类型,所以你必须在参数和结果映射中指明字段是 VARCHAR 类型, 以使其能够绑定到正确的类型处理器上。这是因为 MyBatis 直到语句被执行时才清楚数据类型。 + +### Mybatis 插件 + +MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括: + +- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) +- ParameterHandler (getParameterObject, setParameters) +- ResultSetHandler (handleResultSets, handleOutputParameters) +- StatementHandler (prepare, parameterize, batch, update, query) + +这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。 + +通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。 + +```java +// ExamplePlugin.java +@Intercepts({@Signature( + type= Executor.class, + method = "update", + args = {MappedStatement.class,Object.class})}) +public class ExamplePlugin implements Interceptor { + private Properties properties = new Properties(); + public Object intercept(Invocation invocation) throws Throwable { + // implement pre processing if need + Object returnObject = invocation.proceed(); + // implement post processing if need + return returnObject; + } + public void setProperties(Properties properties) { + this.properties = properties; + } +} +``` + +```xml + + + + + + +``` + +上面的插件将会拦截在 Executor 实例中所有的 “update” 方法调用, 这里的 Executor 是负责执行底层映射语句的内部对象。 + +## 参考资料 + +- **官方** + - [Mybatis Github](https://github.com/mybatis/mybatis-3) + - [Mybatis 官网](http://www.mybatis.org/mybatis-3/) + - [MyBatis 官方代码生成(mybatis-generator)](https://github.com/mybatis/generator) + - [MyBatis 官方集成 Spring(mybatis-spring)](https://github.com/mybatis/spring) + - [Mybatis 官方集成 Spring Boot(mybatis-spring-boot)](https://github.com/mybatis/spring-boot-starter) +- **扩展插件** + - [MyBatis-Plus](https://github.com/baomidou/mybatis-plus) - CRUD 扩展插件、代码生成器、分页器等多功能 + - [Mapper](https://github.com/abel533/Mapper) - CRUD 扩展插件 + - [Mybatis-PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) - Mybatis 通用分页插件 +- **文章** + - [深入理解 mybatis 原理](https://blog.csdn.net/luanlouis/article/details/40422941) + - [mybatis 源码中文注释](https://github.com/tuguangquan/mybatis) + - [MyBatis Generator 详解](https://blog.csdn.net/isea533/article/details/42102297) + - [Mybatis 常见面试题](https://juejin.im/post/5aa646cdf265da237e095da1) + - [Mybatis 中强大的 resultMap](https://juejin.im/post/5cee8b61e51d455d88219ea4) diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/02.Mybatis\345\216\237\347\220\206.md" "b/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/02.Mybatis\345\216\237\347\220\206.md" new file mode 100644 index 00000000..bd8b3760 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/02.Mybatis\345\216\237\347\220\206.md" @@ -0,0 +1,863 @@ +--- +title: Mybatis原理 +date: 2022-02-17 22:34:30 +order: 02 +categories: + - Java + - 框架 + - ORM +tags: + - Java + - 框架 + - ORM + - Mybatis +permalink: /pages/d55184/ +--- + +# Mybatis 原理 + +> Mybatis 的前身就是 iBatis ,是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。本文以一个 Mybatis 完整示例为切入点,结合 Mybatis 底层源码分析,图文并茂的讲解 Mybatis 的核心工作机制。 + +## Mybatis 完整示例 + +> 这里,我将以一个入门级的示例来演示 Mybatis 是如何工作的。 +> +> 注:本文后面章节中的原理、源码部分也将基于这个示例来进行讲解。 +> +> [完整示例源码地址](https://github.com/dunwu/spring-tutorial/blob/master/codes/data/spring-data-mybatis/src/main/java/io/github/dunwu/spring/orm/MybatisDemo.java) + +### 数据库准备 + +在本示例中,需要针对一张用户表进行 CRUD 操作。其数据模型如下: + +```sql +CREATE TABLE IF NOT EXISTS user ( + id BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Id', + name VARCHAR(10) NOT NULL DEFAULT '' COMMENT '用户名', + age INT(3) NOT NULL DEFAULT 0 COMMENT '年龄', + address VARCHAR(32) NOT NULL DEFAULT '' COMMENT '地址', + email VARCHAR(32) NOT NULL DEFAULT '' COMMENT '邮件', + PRIMARY KEY (id) +) COMMENT = '用户表'; + +INSERT INTO user (name, age, address, email) +VALUES ('张三', 18, '北京', 'xxx@163.com'); +INSERT INTO user (name, age, address, email) +VALUES ('李四', 19, '上海', 'xxx@163.com'); +``` + +### 添加 Mybatis + +如果使用 Maven 来构建项目,则需将下面的依赖代码置于 pom.xml 文件中: + +```xml + + org.Mybatis + Mybatis + x.x.x + +``` + +### Mybatis 配置 + +XML 配置文件中包含了对 Mybatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。 + +本示例中只是给出最简化的配置。 + +【示例】Mybatis-config.xml 文件 + +```xml + + + + + + + + + + + + + + + + + + +``` + +> 说明:上面的配置文件中仅仅指定了数据源连接方式和 User 表的映射配置文件。 + +### Mapper + +#### Mapper.xml + +个人理解,Mapper.xml 文件可以看做是 Mybatis 的 JDBC SQL 模板。 + +【示例】UserMapper.xml 文件 + +下面是一个通过 Mybatis Generator 自动生成的完整的 Mapper 文件。 + +```xml + + + + + + + + + + + + delete from user + where id = #{id,jdbcType=BIGINT} + + + insert into user (id, name, age, + address, email) + values (#{id,jdbcType=BIGINT}, #{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER}, + #{address,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR}) + + + update user + set name = #{name,jdbcType=VARCHAR}, + age = #{age,jdbcType=INTEGER}, + address = #{address,jdbcType=VARCHAR}, + email = #{email,jdbcType=VARCHAR} + where id = #{id,jdbcType=BIGINT} + + + + +``` + +#### Mapper.java + +Mapper.java 文件是 Mapper.xml 对应的 Java 对象。 + +【示例】UserMapper.java 文件 + +```java +public interface UserMapper { + + int deleteByPrimaryKey(Long id); + + int insert(User record); + + User selectByPrimaryKey(Long id); + + List selectAll(); + + int updateByPrimaryKey(User record); + +} +``` + +对比 UserMapper.java 和 UserMapper.xml 文件,不难发现: + +UserMapper.java 中的方法和 UserMapper.xml 的 CRUD 语句元素( ``、``、``、`` 的 `parameterType` 属性以及 `` 的 `type` 属性都可能会绑定到数据实体。这样就可以把 JDBC 操作的输入输出和 JavaBean 结合起来,更加方便、易于理解。 + +### 测试程序 + +【示例】MybatisDemo.java 文件 + +```java +public class MybatisDemo { + + public static void main(String[] args) throws Exception { + // 1. 加载 Mybatis 配置文件,创建 SqlSessionFactory + // 注:在实际的应用中,SqlSessionFactory 应该是单例 + InputStream inputStream = Resources.getResourceAsStream("Mybatis/Mybatis-config.xml"); + SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); + SqlSessionFactory factory = builder.build(inputStream); + + // 2. 创建一个 SqlSession 实例,进行数据库操作 + SqlSession sqlSession = factory.openSession(); + + // 3. Mapper 映射并执行 + Long params = 1L; + List list = sqlSession.selectList("io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey", params); + for (User user : list) { + System.out.println("user name: " + user.getName()); + } + // 输出:user name: 张三 + } + +} +``` + +> 说明: +> +> `SqlSession` 接口是 Mybatis API 核心中的核心,它代表了 Mybatis 和数据库的一次完整会话。 +> +> - Mybatis 会解析配置,并根据配置创建 `SqlSession` 。 +> - 然后,Mybatis 将 Mapper 映射为 `SqlSession`,然后传递参数,执行 SQL 语句并获取结果。 + +## Mybatis 生命周期 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210510113446.png) + +### SqlSessionFactoryBuilder + +#### SqlSessionFactoryBuilder 的职责 + +**`SqlSessionFactoryBuilder` 负责创建 `SqlSessionFactory` 实例**。`SqlSessionFactoryBuilder` 可以从 XML 配置文件或一个预先定制的 `Configuration` 的实例构建出 `SqlSessionFactory` 的实例。 + +`Configuration` 类包含了对一个 `SqlSessionFactory` 实例你可能关心的所有内容。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210508173040.png) + +`SqlSessionFactoryBuilder` 应用了建造者设计模式,它有五个 `build` 方法,允许你通过不同的资源创建 `SqlSessionFactory` 实例。 + +```java +SqlSessionFactory build(InputStream inputStream) +SqlSessionFactory build(InputStream inputStream, String environment) +SqlSessionFactory build(InputStream inputStream, Properties properties) +SqlSessionFactory build(InputStream inputStream, String env, Properties props) +SqlSessionFactory build(Configuration config) +``` + +#### SqlSessionFactoryBuilder 的生命周期 + +`SqlSessionFactoryBuilder` 可以被实例化、使用和丢弃,一旦创建了 `SqlSessionFactory`,就不再需要它了。 因此 `SqlSessionFactoryBuilder` 实例的最佳作用域是方法作用域(也就是局部方法变量)。你可以重用 `SqlSessionFactoryBuilder` 来创建多个 `SqlSessionFactory` 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。 + +### SqlSessionFactory + +#### SqlSessionFactory 职责 + +**`SqlSessionFactory` 负责创建 `SqlSession` 实例。** + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210510105641.png) + +`SqlSessionFactory` 应用了工厂设计模式,它提供了一组方法,用于创建 SqlSession 实例。 + +```java +SqlSession openSession() +SqlSession openSession(boolean autoCommit) +SqlSession openSession(Connection connection) +SqlSession openSession(TransactionIsolationLevel level) +SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level) +SqlSession openSession(ExecutorType execType) +SqlSession openSession(ExecutorType execType, boolean autoCommit) +SqlSession openSession(ExecutorType execType, Connection connection) +Configuration getConfiguration(); +``` + +方法说明: + +- 默认的 `openSession()` 方法没有参数,它会创建具备如下特性的 `SqlSession`: + - 事务作用域将会开启(也就是不自动提交)。 + - 将由当前环境配置的 `DataSource` 实例中获取 `Connection` 对象。 + - 事务隔离级别将会使用驱动或数据源的默认设置。 + - 预处理语句不会被复用,也不会批量处理更新。 +- `TransactionIsolationLevel` 表示事务隔离级别,它对应着 JDBC 的五个事务隔离级别。 +- `ExecutorType` 枚举类型定义了三个值: + - `ExecutorType.SIMPLE`:该类型的执行器没有特别的行为。它为每个语句的执行创建一个新的预处理语句。 + - `ExecutorType.REUSE`:该类型的执行器会复用预处理语句。 + - `ExecutorType.BATCH`:该类型的执行器会批量执行所有更新语句,如果 SELECT 在多个更新中间执行,将在必要时将多条更新语句分隔开来,以方便理解。 + +#### SqlSessionFactory 生命周期 + +`SqlSessionFactory` 应该以单例形式在应用的运行期间一直存在。 + +### SqlSession + +#### SqlSession 职责 + +**Mybatis 的主要 Java 接口就是 `SqlSession`。它包含了所有执行语句,获取映射器和管理事务等方法。** + +> 详细内容可以参考:“ [Mybatis 官方文档之 SqlSessions](http://www.Mybatis.org/Mybatis-3/zh/java-api.html#sqlSessions) ” 。 + +SqlSession 类的方法可以按照下图进行大致分类: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210510110638.png) + +#### SqlSession 生命周期 + +`SqlSessions` 是由 `SqlSessionFactory` 实例创建的;而 `SqlSessionFactory` 是由 `SqlSessionFactoryBuilder` 创建的。 + +> 🔔 注意:当 Mybatis 与一些依赖注入框架(如 Spring 或者 Guice)同时使用时,`SqlSessions` 将被依赖注入框架所创建,所以你不需要使用 `SqlSessionFactoryBuilder` 或者 `SqlSessionFactory`。 + +**每个线程都应该有它自己的 `SqlSession` 实例。** + +`SqlSession` 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 `SqlSession` 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 `SqlSession` 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 `HttpSession`。 正确在 Web 中使用 `SqlSession` 的场景是:每次收到的 HTTP 请求,就可以打开一个 `SqlSession`,返回一个响应,就关闭它。 + +编程模式: + +```java +try (SqlSession session = sqlSessionFactory.openSession()) { + // 你的应用逻辑代码 +} +``` + +### 映射器 + +#### 映射器职责 + +映射器是一些由用户创建的、绑定 SQL 语句的接口。 + +`SqlSession` 中的 `insert`、`update`、`delete` 和 `select` 方法都很强大,但也有些繁琐。更通用的方式是使用映射器类来执行映射语句。**一个映射器类就是一个仅需声明与 `SqlSession` 方法相匹配的方法的接口类**。 + +Mybatis 将配置文件中的每一个 `` 节点抽象为一个 `Mapper` 接口,而这个接口中声明的方法和跟 `` 节点中的 `` 节点相对应,即 `` 节点的 id 值为 Mapper 接口中的方法名称,`parameterType` 值表示 Mapper 对应方法的入参类型,而 `resultMap` 值则对应了 Mapper 接口表示的返回值类型或者返回结果集的元素类型。 + +Mybatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个 Mapper 实例;Mybatis 会根据这个方法的方法名和参数类型,确定 Statement Id,然后和 SqlSession 进行映射,底层还是通过 SqlSession 完成和数据库的交互。 + +下面的示例展示了一些方法签名以及它们是如何映射到 `SqlSession` 上的。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210512111723.png) + +> **注意** +> +> - 映射器接口不需要去实现任何接口或继承自任何类。只要方法可以被唯一标识对应的映射语句就可以了。 +> - 映射器接口可以继承自其他接口。当使用 XML 来构建映射器接口时要保证语句被包含在合适的命名空间中。而且,唯一的限制就是你不能在两个继承关系的接口中拥有相同的方法签名(潜在的危险做法不可取)。 + +#### 映射器生命周期 + +映射器接口的实例是从 `SqlSession` 中获得的。因此从技术层面讲,任何映射器实例的最大作用域是和请求它们的 `SqlSession` 相同的。尽管如此,映射器实例的最佳作用域是方法作用域。 也就是说,映射器实例应该在调用它们的方法中被请求,用过之后即可丢弃。 + +编程模式: + +```java +try (SqlSession session = sqlSessionFactory.openSession()) { + BlogMapper mapper = session.getMapper(BlogMapper.class); + // 你的应用逻辑代码 +} +``` + +- **映射器注解** + +Mybatis 是一个 XML 驱动的框架。配置信息是基于 XML 的,而且映射语句也是定义在 XML 中的。Mybatis 3 以后,支持注解配置。注解配置基于配置 API;而配置 API 基于 XML 配置。 + +Mybatis 支持诸如 `@Insert`、`@Update`、`@Delete`、`@Select`、`@Result` 等注解。 + +> 详细内容请参考:[Mybatis 官方文档之 sqlSessions](http://www.Mybatis.org/Mybatis-3/zh/java-api.html#sqlSessions),其中列举了 Mybatis 支持的注解清单,以及基本用法。 + +## Mybatis 的架构 + +从 Mybatis 代码实现的角度来看,Mybatis 的主要组件有以下几个: + +- **SqlSession** - 作为 Mybatis 工作的主要顶层 API,表示和数据库交互的会话,完成必要数据库增删改查功能。 +- **Executor** - Mybatis 执行器,是 Mybatis 调度的核心,负责 SQL 语句的生成和查询缓存的维护。 +- **StatementHandler** - 封装了 JDBC Statement 操作,负责对 JDBC statement 的操作,如设置参数、将 Statement 结果集转换成 List 集合。 +- **ParameterHandler** - 负责对用户传递的参数转换成 JDBC Statement 所需要的参数。 +- **ResultSetHandler** - 负责将 JDBC 返回的 ResultSet 结果集对象转换成 List 类型的集合。 +- **TypeHandler** - 负责 java 数据类型和 jdbc 数据类型之间的映射和转换。 +- **MappedStatement** - `MappedStatement` 维护了一条 `` 节点的封装。 +- **SqlSource** - 负责根据用户传递的 parameterObject,动态地生成 SQL 语句,将信息封装到 BoundSql 对象中,并返回。 +- **BoundSql** - 表示动态生成的 SQL 语句以及相应的参数信息。 +- **Configuration** - Mybatis 所有的配置信息都维持在 Configuration 对象之中。 + +这些组件的架构层次如下: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210512114852.png) + +### 配置层 + +配置层决定了 Mybatis 的工作方式。 + +Mybatis 提供了两种配置方式: + +- 基于 XML 配置文件的方式 +- 基于 Java API 的方式 + +`SqlSessionFactoryBuilder` 会根据配置创建 `SqlSessionFactory` ; + +`SqlSessionFactory` 负责创建 `SqlSessions` 。 + +### 接口层 + +接口层负责和数据库交互的方式。 + +Mybatis 和数据库的交互有两种方式: + +- **使用 SqlSession**:SqlSession 封装了所有执行语句,获取映射器和管理事务的方法。 + - 用户只需要传入 Statement Id 和查询参数给 SqlSession 对象,就可以很方便的和数据库进行交互。 + - 这种方式的缺点是不符合面向对象编程的范式。 +- **使用 Mapper 接口**:Mybatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个 Mapper 实例;Mybatis 会根据这个方法的方法名和参数类型,确定 Statement Id,然后和 SqlSession 进行映射,底层还是通过 SqlSession 完成和数据库的交互。 + +### 数据处理层 + +数据处理层可以说是 Mybatis 的核心,从大的方面上讲,它要完成两个功能: + +- 根据传参 `Statement` 和参数构建动态 SQL 语句 + - 动态语句生成可以说是 Mybatis 框架非常优雅的一个设计,Mybatis 通过传入的参数值,**使用 Ognl 来动态地构造 SQL 语句**,使得 Mybatis 有很强的灵活性和扩展性。 + - 参数映射指的是对于 java 数据类型和 jdbc 数据类型之间的转换:这里有包括两个过程:查询阶段,我们要将 java 类型的数据,转换成 jdbc 类型的数据,通过 `preparedStatement.setXXX()` 来设值;另一个就是对 resultset 查询结果集的 jdbcType 数据转换成 java 数据类型。 +- 执行 SQL 语句以及处理响应结果集 ResultSet + - 动态 SQL 语句生成之后,Mybatis 将执行 SQL 语句,并将可能返回的结果集转换成 `List` 列表。 + - Mybatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询。 + +### 框架支撑层 + +- **事务管理机制** - Mybatis 将事务抽象成了 Transaction 接口。Mybatis 的事务管理分为两种形式: + - 使用 JDBC 的事务管理机制:即利用 `java.sql.Connection` 对象完成对事务的提交(`commit`)、回滚(`rollback`)、关闭(`close`)等。 + - 使用 MANAGED 的事务管理机制:Mybatis 自身不会去实现事务管理,而是让程序的容器如(JBOSS,Weblogic)来实现对事务的管理。 +- **连接池管理** +- **SQL 语句的配置** - 支持两种方式: + - xml 配置 + - 注解配置 +- 缓存机制 - Mybatis 采用两级缓存结构 + + - **一级缓存是 Session 会话级别的缓存** - 一级缓存又被称之为本地缓存。一般而言,一个 `SqlSession` 对象会使用一个 `Executor` 对象来完成会话操作,`Executor` 对象会维护一个 Cache 缓存,以提高查询性能。 + - 一级缓存的生命周期是 Session 会话级别的。 + - **二级缓存是 Application 应用级别的缓存** - 用户配置了 `"cacheEnabled=true"`,才会开启二级缓存。 + - 如果开启了二级缓存,`SqlSession` 会先使用 `CachingExecutor` 对象来处理查询请求。`CachingExecutor` 会在二级缓存中查看是否有匹配的数据,如果匹配,则直接返回缓存结果;如果缓存中没有,再交给真正的 `Executor` 对象来完成查询,之后 `CachingExecutor` 会将真正 `Executor` 返回的查询结果放置到缓存中,然后在返回给用户。 + - 二级缓存的生命周期是应用级别的。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210512185709.png) + +## SqlSession 内部工作机制 + +从前文,我们已经了解了,Mybatis 封装了对数据库的访问,把对数据库的会话和事务控制放到了 SqlSession 对象中。那么具体是如何工作的呢?接下来,我们通过源码解读来进行分析。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210512173437.png) + +`SqlSession` 对于 insert、update、delete、select 的内部处理机制基本上大同小异。所以,接下来,我会以一次完整的 select 查询流程为例讲解 `SqlSession` 内部的工作机制。相信读者如果理解了 select 的处理流程,对于其他 CRUD 操作也能做到一通百通。 + +### SqlSession 子组件 + +前面的内容已经介绍了:SqlSession 是 Mybatis 的顶层接口,它提供了所有执行语句,获取映射器和管理事务等方法。 + +实际上,SqlSession 是通过聚合多个子组件,让每个子组件负责各自功能的方式,实现了任务的下发。 + +在了解各个子组件工作机制前,先让我们简单认识一下 SqlSession 的核心子组件。 + +#### Executor + +Executor 即执行器,它负责生成动态 SQL 以及管理缓存。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210512150000.png) + +- `Executor` 即执行器接口。 +- `BaseExecutor` 是 `Executor` 的抽象类,它采用了模板方法设计模式,内置了一些共性方法,而将定制化方法留给子类去实现。 +- `SimpleExecutor` 是最简单的执行器。它只会直接执行 SQL,不会做额外的事。 +- `BatchExecutor` 是批处理执行器。它的作用是通过批处理来优化性能。值得注意的是,批量更新操作,由于内部有缓存机制,使用完后需要调用 `flushStatements` 来清除缓存。 +- `ReuseExecutor` 是可重用的执行器。重用的对象是 `Statement`,也就是说,该执行器会缓存同一个 SQL 的 `Statement`,避免重复创建 `Statement`。其内部的实现是通过一个 `HashMap` 来维护 `Statement` 对象的。由于当前 `Map` 只在该 session 中有效,所以使用完后需要调用 `flushStatements` 来清除 Map。 +- `CachingExecutor` 是缓存执行器。它只在启用二级缓存时才会用到。 + +#### StatementHandler + +`StatementHandler` 对象负责设置 `Statement` 对象中的查询参数、处理 JDBC 返回的 resultSet,将 resultSet 加工为 List 集合返回。 + +`StatementHandler` 的家族成员: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210512160243.png) + +- `StatementHandler` 是接口; +- `BaseStatementHandler` 是实现 `StatementHandler` 的抽象类,内置一些共性方法; +- `SimpleStatementHandler` 负责处理 `Statement`; +- `PreparedStatementHandler` 负责处理 `PreparedStatement`; +- `CallableStatementHandler` 负责处理 `CallableStatement`。 +- `RoutingStatementHandler` 负责代理 `StatementHandler` 具体子类,根据 `Statement` 类型,选择实例化 `SimpleStatementHandler`、`PreparedStatementHandler`、`CallableStatementHandler`。 + +#### ParameterHandler + +`ParameterHandler` 负责将传入的 Java 对象转换 JDBC 类型对象,并为 `PreparedStatement` 的动态 SQL 填充数值。 + +`ParameterHandler` 只有一个具体实现类,即 `DefaultParameterHandler`。 + +#### ResultSetHandler + +`ResultSetHandler` 负责两件事: + +- 处理 `Statement` 执行后产生的结果集,生成结果列表 +- 处理存储过程执行后的输出参数 + +`ResultSetHandler` 只有一个具体实现类,即 `DefaultResultSetHandler`。 + +#### TypeHandler + +TypeHandler 负责将 Java 对象类型和 JDBC 类型进行相互转换。 + +### SqlSession 和 Mapper + +先来回忆一下 Mybatis 完整示例章节的 测试程序部分的代码。 + +MybatisDemo.java 文件中的代码片段: + +```java +// 2. 创建一个 SqlSession 实例,进行数据库操作 +SqlSession sqlSession = factory.openSession(); + +// 3. Mapper 映射并执行 +Long params = 1L; +List list = sqlSession.selectList("io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey", params); +for (User user : list) { + System.out.println("user name: " + user.getName()); +} +``` + +示例代码中,给 sqlSession 对象的传递一个配置的 Sql 语句的 Statement Id 和参数,然后返回结果 + +`io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey` 是配置在 `UserMapper.xml` 的 Statement ID,params 是 SQL 参数。 + +UserMapper.xml 文件中的代码片段: + +```xml + +``` + +Mybatis 通过方法的全限定名,将 SqlSession 和 Mapper 相互映射起来。 + +### SqlSession 和 Executor + +`org.apache.ibatis.session.defaults.DefaultSqlSession` 中 `selectList` 方法的源码: + +```java +@Override +public List selectList(String statement) { + return this.selectList(statement, null); +} + +@Override +public List selectList(String statement, Object parameter) { + return this.selectList(statement, parameter, RowBounds.DEFAULT); +} + +@Override +public List selectList(String statement, Object parameter, RowBounds rowBounds) { + try { + // 1. 根据 Statement Id,在配置对象 Configuration 中查找和配置文件相对应的 MappedStatement + MappedStatement ms = configuration.getMappedStatement(statement); + // 2. 将 SQL 语句交由执行器 Executor 处理 + return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } +} +``` + +说明: + +Mybatis 所有的配置信息都维持在 `Configuration` 对象之中。中维护了一个 `Map` 对象。其中,key 为 Mapper 方法的全限定名(对于本例而言,key 就是 `io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey` ),value 为 `MappedStatement` 对象。所以,传入 Statement Id 就可以从 Map 中找到对应的 `MappedStatement`。 + +`MappedStatement` 维护了一个 Mapper 方法的元数据信息,其数据组织可以参考下面的 debug 截图: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210511150650.png) + +> 小结: +> +> 通过 "SqlSession 和 Mapper" 以及 "SqlSession 和 Executor" 这两节,我们已经知道: +> +> SqlSession 的职能是:根据 Statement ID, 在 `Configuration` 中获取到对应的 `MappedStatement` 对象,然后调用 `Executor` 来执行具体的操作。 + +### Executor 工作流程 + +继续上一节的流程,`SqlSession` 将 SQL 语句交由执行器 `Executor` 处理。`Executor` 又做了哪些事儿呢? + +(1)执行器查询入口 + +```java +public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException { + // 1. 根据传参,动态生成需要执行的 SQL 语句,用 BoundSql 对象表示 + BoundSql boundSql = ms.getBoundSql(parameter); + // 2. 根据传参,创建一个缓存Key + CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql); + return query(ms, parameter, rowBounds, resultHandler, key, boundSql); + } +``` + +执行器查询入口主要做两件事: + +- **生成动态 SQL**:根据传参,动态生成需要执行的 SQL 语句,用 BoundSql 对象表示。 +- **管理缓存**:根据传参,创建一个缓存 Key。 + +(2)执行器查询第二入口 + +```java + @SuppressWarnings("unchecked") + @Override + public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { + // 略 + List list; + try { + queryStack++; + list = resultHandler == null ? (List) localCache.getObject(key) : null; + // 3. 缓存中有值,则直接从缓存中取数据;否则,查询数据库 + if (list != null) { + handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); + } else { + list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); + } + } finally { + queryStack--; + } + // 略 + return list; + } +``` + +实际查询方法主要的职能是判断缓存 key 是否能命中缓存: + +- 命中,则将缓存中数据返回; +- 不命中,则查询数据库: + +(3)查询数据库 + +```java + private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { + List list; + localCache.putObject(key, EXECUTION_PLACEHOLDER); + try { + // 4. 执行查询,获取 List 结果,并将查询的结果更新本地缓存中 + list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); + } finally { + localCache.removeObject(key); + } + localCache.putObject(key, list); + if (ms.getStatementType() == StatementType.CALLABLE) { + localOutputParameterCache.putObject(key, parameter); + } + return list; + } +``` + +`queryFromDatabase` 方法的职责是调用 doQuery,向数据库发起查询,并将返回的结果更新到本地缓存。 + +(4)实际查询方法 + +SimpleExecutor 类的 doQuery()方法实现 + +```java + @Override + public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { + Statement stmt = null; + try { + Configuration configuration = ms.getConfiguration(); + // 5. 根据既有的参数,创建StatementHandler对象来执行查询操作 + StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); + // 6. 创建java.Sql.Statement对象,传递给StatementHandler对象 + stmt = prepareStatement(handler, ms.getStatementLog()); + // 7. 调用StatementHandler.query()方法,返回List结果 + return handler.query(stmt, resultHandler); + } finally { + closeStatement(stmt); + } + } +``` + +上述的 Executor.query()方法几经转折,最后会创建一个 `StatementHandler` 对象,然后将必要的参数传递给 `StatementHandler`,使用 `StatementHandler` 来完成对数据库的查询,最终返回 List 结果集。 +从上面的代码中我们可以看出,`Executor` 的功能和作用是: + +1. 根据传递的参数,完成 SQL 语句的动态解析,生成 BoundSql 对象,供 `StatementHandler` 使用; + +2. 为查询创建缓存,以提高性能 + +3. 创建 JDBC 的 `Statement` 连接对象,传递给 `StatementHandler` 对象,返回 List 查询结果。 + +prepareStatement() 方法的实现: + +```java + private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { + Statement stmt; + Connection connection = getConnection(statementLog); + stmt = handler.prepare(connection, transaction.getTimeout()); + //对创建的Statement对象设置参数,即设置SQL 语句中 ? 设置为指定的参数 + handler.parameterize(stmt); + return stmt; + } +``` + +对于 JDBC 的 `PreparedStatement` 类型的对象,创建的过程中,我们使用的是 SQL 语句字符串会包含 若干个? 占位符,我们其后再对占位符进行设值。 + +### StatementHandler 工作流程 + +`StatementHandler` 有一个子类 `RoutingStatementHandler`,它负责代理其他 `StatementHandler` 子类的工作。 + +它会根据配置的 `Statement` 类型,选择实例化相应的 `StatementHandler`,然后由其代理对象完成工作。 + +【源码】RoutingStatementHandler + +```java +public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + + switch (ms.getStatementType()) { + case STATEMENT: + delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + case PREPARED: + delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + case CALLABLE: + delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + default: + throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); + } + +} +``` + +【源码】`RoutingStatementHandler` 的 `parameterize` 方法源码 + +【源码】`PreparedStatementHandler` 的 `parameterize` 方法源码 + +`StatementHandler` 使用 `ParameterHandler` 对象来完成对 `Statement` 的赋值。 + +```java +@Override +public void parameterize(Statement statement) throws SQLException { + // 使用 ParameterHandler 对象来完成对 Statement 的设值 + parameterHandler.setParameters((PreparedStatement) statement); +} +``` + +【源码】`StatementHandler` 的 `query` 方法源码 + +`StatementHandler` 使用 `ResultSetHandler` 对象来完成对 `ResultSet` 的处理。 + +```java +@Override +public List query(Statement statement, ResultHandler resultHandler) throws SQLException { + PreparedStatement ps = (PreparedStatement) statement; + ps.execute(); + // 使用ResultHandler来处理ResultSet + return resultSetHandler.handleResultSets(ps); +} +``` + +### ParameterHandler 工作流程 + +【源码】`DefaultParameterHandler` 的 `setParameters` 方法 + +```java + @Override + public void setParameters(PreparedStatement ps) { + // parameterMappings 是对占位符 #{} 对应参数的封装 + List parameterMappings = boundSql.getParameterMappings(); + if (parameterMappings != null) { + for (int i = 0; i < parameterMappings.size(); i++) { + ParameterMapping parameterMapping = parameterMappings.get(i); + // 不处理存储过程中的参数 + if (parameterMapping.getMode() != ParameterMode.OUT) { + Object value; + String propertyName = parameterMapping.getProperty(); + if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params + // 获取对应的实际数值 + value = boundSql.getAdditionalParameter(propertyName); + } else if (parameterObject == null) { + value = null; + } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { + value = parameterObject; + } else { + // 获取对象中相应的属性或查找 Map 对象中的值 + MetaObject metaObject = configuration.newMetaObject(parameterObject); + value = metaObject.getValue(propertyName); + } + + TypeHandler typeHandler = parameterMapping.getTypeHandler(); + JdbcType jdbcType = parameterMapping.getJdbcType(); + if (value == null && jdbcType == null) { + jdbcType = configuration.getJdbcTypeForNull(); + } + try { + // 通过 TypeHandler 将 Java 对象参数转为 JDBC 类型的参数 + // 然后,将数值动态绑定到 PreparedStaement 中 + typeHandler.setParameter(ps, i + 1, value, jdbcType); + } catch (TypeException | SQLException e) { + throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e); + } + } + } + } + } +``` + +### ResultSetHandler 工作流程 + +`ResultSetHandler` 的实现可以概括为:将 `Statement` 执行后的结果集,按照 `Mapper` 文件中配置的 `ResultType` 或 `ResultMap` 来转换成对应的 JavaBean 对象,最后将结果返回。 + +【源码】`DefaultResultSetHandler` 的 `handleResultSets` 方法 + +`handleResultSets` 方法是 `DefaultResultSetHandler` 的最关键方法。其实现如下: + +```java +@Override +public List handleResultSets(Statement stmt) throws SQLException { + ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); + + final List multipleResults = new ArrayList<>(); + + int resultSetCount = 0; + // 第一个结果集 + ResultSetWrapper rsw = getFirstResultSet(stmt); + List resultMaps = mappedStatement.getResultMaps(); + // 判断结果集的数量 + int resultMapCount = resultMaps.size(); + validateResultMapsCount(rsw, resultMapCount); + // 遍历处理结果集 + while (rsw != null && resultMapCount > resultSetCount) { + ResultMap resultMap = resultMaps.get(resultSetCount); + handleResultSet(rsw, resultMap, multipleResults, null); + rsw = getNextResultSet(stmt); + cleanUpAfterHandlingResultSet(); + resultSetCount++; + } + + String[] resultSets = mappedStatement.getResultSets(); + if (resultSets != null) { + while (rsw != null && resultSetCount < resultSets.length) { + ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); + if (parentMapping != null) { + String nestedResultMapId = parentMapping.getNestedResultMapId(); + ResultMap resultMap = configuration.getResultMap(nestedResultMapId); + handleResultSet(rsw, resultMap, null, parentMapping); + } + rsw = getNextResultSet(stmt); + cleanUpAfterHandlingResultSet(); + resultSetCount++; + } + } + + return collapseSingleResultList(multipleResults); +} +``` + +## 参考资料 + +- **官方** + - [Mybatis Github](https://github.com/Mybatis/Mybatis-3) + - [Mybatis 官网](http://www.Mybatis.org/Mybatis-3/) +- **文章** + - [深入理解 Mybatis 原理](https://blog.csdn.net/luanlouis/article/details/40422941) + - [Mybatis 源码中文注释](https://github.com/tuguangquan/Mybatis) + - [Mybatis 中强大的 resultMap](https://juejin.im/post/5cee8b61e51d455d88219ea4) diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/README.md" new file mode 100644 index 00000000..1cb04ab8 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/11.ORM/README.md" @@ -0,0 +1,50 @@ +--- +title: Java ORM 框架 +date: 2022-02-17 22:34:30 +categories: + - Java + - 框架 + - ORM +tags: + - Java + - 框架 + - ORM +permalink: /pages/fe879a/ +hidden: true +index: false +--- + +# Java ORM 框架 + +## 📖 内容 + +> Mybatis 的前身就是 iBatis ,是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。本文以一个 Mybatis 完整示例为切入点,结合 Mybatis 底层源码分析,图文并茂的讲解 Mybatis 的核心工作机制。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210522101005.png) + +### [Mybatis 快速入门](01.Mybatis快速入门.md) + +### [Mybatis 原理](02.Mybatis原理.md) + +## 📚 资料 + +- **官方** + - [Mybatis Github](https://github.com/mybatis/mybatis-3) + - [Mybatis 官网](http://www.mybatis.org/mybatis-3/) + - [MyBatis 官方代码生成(mybatis-generator)](https://github.com/mybatis/generator) + - [MyBatis 官方集成 Spring(mybatis-spring)](https://github.com/mybatis/spring) + - [Mybatis 官方集成 Spring Boot(mybatis-spring-boot)](https://github.com/mybatis/spring-boot-starter) +- **扩展插件** + - [MyBatis-Plus](https://github.com/baomidou/mybatis-plus) - CRUD 扩展插件、代码生成器、分页器等多功能 + - [Mapper](https://github.com/abel533/Mapper) - CRUD 扩展插件 + - [Mybatis-PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) - Mybatis 通用分页插件 +- **文章** + - [深入理解 mybatis 原理](https://blog.csdn.net/luanlouis/article/details/40422941) + - [mybatis 源码中文注释](https://github.com/tuguangquan/mybatis) + - [MyBatis Generator 详解](https://blog.csdn.net/isea533/article/details/42102297) + - [Mybatis 常见面试题](https://juejin.im/post/5aa646cdf265da237e095da1) + - [Mybatis 中强大的 resultMap](https://juejin.im/post/5cee8b61e51d455d88219ea4) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/01.Shiro.md" "b/docs/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/01.Shiro.md" new file mode 100644 index 00000000..70d8f3dd --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/01.Shiro.md" @@ -0,0 +1,441 @@ +--- +title: Shiro 快速入门 +date: 2022-02-17 22:34:30 +order: 01 +categories: + - Java + - 框架 + - 安全 +tags: + - Java + - 框架 + - 安全 + - Shiro +permalink: /pages/3295c4/ +--- + +# Shiro 快速入门 + +> Shiro 是一个安全框架,具有认证、授权、加密、会话管理功能。 + +## 一、Shiro 简介 + +### Shiro 特性 + +

    + +

    + +核心功能: + +- **Authentication** - **认证**。验证用户是不是拥有相应的身份。 +- **Authorization** - **授权**。验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限。 +- **Session Manager** - **会话管理**。即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中。会话可以是普通 JavaSE 环境的,也可以是如 Web 环境的。 +- **Cryptography** - **加密**。保护数据的安全性,如密码加密存储到数据库,而不是明文存储。 + +辅助功能: + +- **Web Support** - **Web 支持**。可以非常容易的集成到 Web 环境; +- **Caching** - **缓存**。比如用户登录后,其用户信息、拥有的角色 / 权限不必每次去查,这样可以提高效率; +- **Concurrency** - **并发**。Shiro 支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去; +- **Testing** - **测试**。提供测试支持; +- **Run As** - **运行方式**。允许一个用户假装为另一个用户(如果他们允许)的身份进行访问; +- **Remember Me** - **记住我**。即一次登录后,下次再访问免登录。 + +> :bell: 注意:Shiro 不会去维护用户、维护权限;这些需要我们自己去提供;然后通过相应的接口注入给 Shiro 即可。 + +### Shiro 架构概述 + +

    + +

    + +- **Subject** - **主题**。它代表当前用户,`Subject` 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它——当前和软件交互的任何事件。`Subject` 是 Shiro 的入口。 + + - `Principals` 是 `Subject` 的“识别属性”。`Principals` 可以是任何可以识别 `Subject` 的东西,例如名字(姓氏),姓氏(姓氏或姓氏),用户名,社会保险号等。当然,`Principals` 在应用程序中最好是惟一的。 + - `Credentials` 通常是仅由 `Subject` 知道的秘密值,用作他们实际上“拥有”所主张身份的佐证 凭据的一些常见示例是密码,生物特征数据(例如指纹和视网膜扫描)以及 X.509 证书。 + +- **SecurityManager** - **安全管理**。它是 Shiro 的核心,所有与安全有关的操作(认证、授权、及会话、缓存的管理)都与 `SecurityManager` 交互,且它管理着所有 `Subject`。 +- **Realm** - **域**。用于访问安全相关数据,可以视为应用自身的数据源,需要开发者自己实现。Shiro 会通过 `Realm` 获取安全数据(如用户、角色、权限),就是说 `SecurityManager` 要验证用户身份,那么它需要从 `Realm` 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 `Realm` 看成 DataSource,即安全数据源。 + +### SecurityManager + +`SecurityManager` 是 Shiro 框架核心中的核心,它相当于 Shiro 的总指挥,负责调度所有行为,包括:认证、授权、获取安全数据(调用 `Realm`)、会话管理等。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/standalone/security/shiro/ShiroArchitecture.png) + +`SecurityManager` 聚合了以下组件: + +- **Authenticator** - 认证器,负责认证。如果用户需要定制认证策略,可以实现此接口。 +- **Authorizer** - 授权器,负责权限控制。用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能; +- **SessionManager** - 会话管理器。Shiro 抽象了一个自己的 Session 来管理主体与应用之间交互的数据。 +- **SessionDAO** - 会话 DAO 用于存储会话,需要用户自己实现。 +- **CacheManager** - 缓存控制器。用于管理如用户、角色、权限等信息的缓存。 +- **Cryptography** - 密码器。用于对数据加密、解密。 + +## 二、Shiro 认证 + +### 认证 Subject + +验证 Subject 的过程可以有效地分为三个不同的步骤: + +(1)收集 `Subject` 提交的 `Principals` 和 `Credentials` + +```java +//Example using most common scenario of username/password pair: +UsernamePasswordToken token = new UsernamePasswordToken(username, password); + +//"Remember Me" built-in: +token.setRememberMe(true); +``` + +(2)提交 `Principals` 和 `Credentials` 以进行身份验证。 + +```java +Subject currentUser = SecurityUtils.getSubject(); + +currentUser.login(token); +``` + +(3)如果提交成功,则允许访问,否则重试身份验证或阻止访问。 + +```java +try { + currentUser.login(token); +} catch ( UnknownAccountException uae ) { ... +} catch ( IncorrectCredentialsException ice ) { ... +} catch ( LockedAccountException lae ) { ... +} catch ( ExcessiveAttemptsException eae ) { ... +} ... catch your own ... +} catch ( AuthenticationException ae ) { + //unexpected error? +} +``` + +### Remembered 和 Authenticated + +- `Remembered` - 记住我。被记住的 `Subject` 不是匿名的,并且具有已知的身份(即 `subject.getPrincipals()` 是非空的)。 但是,在先前的会话期间,通过先前的身份验证会记住此身份。 如果 `subject.isRemembered()` 返回 `true`,则认为该主题已被记住。 +- `Authenticated` - 已认证。已认证的 `Subject` 是在当前会话期间已成功认证的 `Subject`。 如果 `subject.isAuthenticated()` 返回 `true`,则认为该 `Subject` 已通过身份验证。 + +### 登出 + +当 Subject 与应用程序完成交互后,可以调用 `subject.logout()` 登出,即放弃所有标识信息。 + +```java +currentUser.logout(); +``` + +### 认证流程 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200317092427.png) + +1. 应用程序代码调用 `Subject.login` 方法,传入构造的 `AuthenticationToken` 实例,该实例代表最终用户的 `Principals` 和 `Credentials`。 + +2. `Subject` 实例(通常是 `DelegatingSubject`(或子类))通过调用 `securityManager.login`(token)委托应用程序的 `SecurityManager`,在此处开始实际的身份验证工作。 +3. `SecurityManager` 接收令牌,并通过调用 `authenticator.authenticate`(token)来简单地委派给其内部 `Authenticator` 实例。这几乎总是一个 `ModularRealmAuthenticator` 实例,它支持在身份验证期间协调一个或多个 `Realm` 实例。 +4. 如果为该应用程序配置了多个 `Realm`,则 `ModularRealmAuthenticator` 实例将利用其配置的 `AuthenticationStrategy` 发起多域验证尝试。在调用领域进行身份验证之前,期间和之后,将调用 `AuthenticationStrategy` 以使其对每个领域的结果做出反应。 +5. 请咨询每个已配置的 `Realm`,以查看其是否支持提交的 `AuthenticationToken`。 如果是这样,将使用提交的令牌调用支持 `Realm` 的 `getAuthenticationInfo` 方法。 `getAuthenticationInfo` 方法有效地表示对该特定 `Realm` 的单个身份验证尝试。 + +### 认证策略 + +当为一个应用程序配置两个或多个领域时,`ModularRealmAuthenticator` 依赖于内部 `AuthenticationStrategy` 组件来确定认证尝试成功或失败的条件。 + +例如,如果只有一个 Realm 成功地对 AuthenticationToken 进行身份验证,而所有其他 Realm 都失败了,那么该身份验证尝试是否被视为成功?还是必须所有领域都成功进行身份验证才能将整体尝试视为成功?或者,如果某个领域成功通过身份验证,是否有必要进一步咨询其他领域? AuthenticationStrategy 根据应用程序的需求做出适当的决定。 + +`AuthenticationStrategy` 是无状态组件,在尝试进行身份验证时会被查询 4 次(这 4 种交互所需的任何必要状态都将作为方法参数给出): + +- 在任何领域被调用之前 +- 在调用单个 `Realm` 的 `getAuthenticationInfo` 方法之前 +- 在调用单个 `Realm` 的 `getAuthenticationInfo` 方法之后 +- 在所有领域都被调用之后 + +`AuthenticationStrategy` 还负责汇总每个成功 `Realm` 的结果,并将它们“捆绑”成单个 `AuthenticationInfo` 表示形式。最终的聚合 `AuthenticationInfo` 实例是 `Authenticator` 实例返回的结果,也是 Shiro 用来表示主体的最终身份(也称为委托人)的东西。 + +| `AuthenticationStrategy` | 描述 | +| :-------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------- | +| [`AtLeastOneSuccessfulStrategy`](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/authc/pam/AtLeastOneSuccessfulStrategy.html) | 只要有一个 `Realm` 成功认证,则整个尝试都被视为成功。 | +| [`FirstSuccessfulStrategy`](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/authc/pam/FirstSuccessfulStrategy.html) | 仅使用从第一个成功通过身份验证的 `Realm` 返回的信息,所有其他 Realm 将被忽略。 | +| [`AllSuccessfulStrategy`](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/authc/pam/AllSuccessfulStrategy.html) | 只有所有 `Realm` 成功认证,则整个尝试才被视为成功。 | + +> :link: 更多认证细节可以参考:[Apache Shiro Authentication](http://shiro.apache.org/authentication.html#apache-shiro-authentication) + +## 三、Shiro 授权 + +授权,也称为访问控制,是管理对资源的访问的过程。 换句话说,控制谁有权访问应用程序中的内容。 + +### 授权元素 + +授权有三个核心要素:权限、角色和用户。 + +#### 权限 + +权限示例: + +- 打开一个文件 +- 查看 `/user/list` web 页面 +- 查询记录 +- 删除一条记录 +- ... + +大多数资源都支持一般的 CRUD 操作。除此以外,对于一些特定的资源,任何有意义的行为都是可以的。基本的设计思路是:权限控制,至少是基于资源和行为。 + +#### 角色 + +角色是一个命名实体,通常代表一组行为或职责。这些行为会转化为:谁可以在应用程序中执行哪些行为?谁不可以在程序中执行哪些行为? + +角色通常是分配给用户帐户的,因此通过关联,用户可以获得自身角色所赋予的权限。 + +#### 用户 + +用户本质上是应用程序的“用户”。 + +用户(即 Shiro 的 `Subject`)通过与角色或直接权限的关联在应用程序中执行某些行为。 + +### 基于角色的授权 + +如果授权是基于角色赋予权限的数据模型,编程模式如下: + +【示例一】 + +``` +Subject currentUser = SecurityUtils.getSubject(); + +if (currentUser.hasRole("administrator")) { + //show the admin button +} else { + //don't show the button? Grey it out? +} +``` + +【示例二】 + +``` +Subject currentUser = SecurityUtils.getSubject(); + +// 检查当前 Subject 是否有某种权限 +// 如果有,直接跳过;如果没有,Shiro 会抛出 AuthorizationException +currentUser.checkRole("bankTeller"); +openBankAccount(); +``` + +> 提示:方式二相比方式一,代码更简洁 + +### 基于权限的授权 + +**更好的授权策略通常是基于权限的授权**。基于权限的授权,由于它和应用程序的原始功能(针对具体资源上的行为)紧密相关,所以基于权限的授权源代码会在功能更改时同步更改(而不是在安全策略发生更改时)。 这意味着与类似的基于角色的授权代码相比,修改代码的影响面要小得多。 + +【示例】基于对象的权限检查 + +```java +Permission printPermission = new PrinterPermission("laserjet4400n", "print"); + +Subject currentUser = SecurityUtils.getSubject(); + +if (currentUser.isPermitted(printPermission)) { + //show the Print button +} else { + //don't show the button? Grey it out? +} +``` + +在对象中存储权限控制信息,但这种方式较为繁重 + +【示例】字符串定义权限控制信息 + +```java +Subject currentUser = SecurityUtils.getSubject(); + +if (currentUser.isPermitted("printer:print:laserjet4400n")) { + //show the Print button +} else { + //don't show the button? Grey it out? +} +``` + +使用 : 分隔,表示资源类型、行为、资源 ID,Shiro 提供了默认实现: `org.apache.shiro.authz.permission.WildcardPermission`。 + +这种权限控制方式的好处在于:轻量、灵活。 + +### 基于注解的授权 + +Shiro 提供了一些用于授权的注解,来进一步简化授权代码。 + +#### `@RequiresAuthentication` + +`@RequiresAuthentication` 注解要求当前 `Subject` 必须是已认证用户才可以访问被修饰的方法。 + +【示例】 + +```java +@RequiresAuthentication +public void updateAccount(Account userAccount) { + //this method will only be invoked by a + //Subject that is guaranteed authenticated + ... +} +``` + +#### `@RequiresGuest` + +`@RequiresGuest` 注解要求当前 `Subject` 的角色是 `guest` 才可以访问被修饰的方法。 + +### 授权流程 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200317092618.png) + +1. 应用程序或框架代码调用任何 `Subject` 的 `hasRole*`,`checkRole*`,`isPermitted*` 或 `checkPermission*` 方法,并传入所需的权限或角色。 + +2. `Subject` 实例,通常是 `DelegatingSubject`(或子类),通过调用 `securityManager` 几乎相同的各自 `hasRole*`,`checkRole*`,`isPermitted*` 或 `checkPermission*` 方法来委托 `SecurityManager` (实现了 [`org.apache.shiro.authz.Authorizer`](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/authz/Authorizer.html) 接口)处理授权。 + +3. `SecurityManager` 通过调用授权者各自的 `hasRole*`,`checkRole*`,`isPermitted*` 或 `checkPermission*` 方法来中继/委托其内部的 `org.apache.shiro.authz.Authorizer` 实例。默认情况下,`authorizer` 实例是 `ModularRealmAuthorizer` 实例,该实例支持在任何授权操作期间协调一个或多个 `Realm` 实例。 + +4. 检查每个已配置的 `Realm`,以查看其是否实现相同的 `Authorizer` 接口。如果是这样,则将调用 `Realm` 各自的 `hasRole*`,`checkRole*`,`isPermitted*` 或 `checkPermission*` 方法。 + +> :link: 更多授权细节可以参考:[Apache Shiro Authorization](http://shiro.apache.org/authorization.html#apache-shiro-authorization) + +## 四、Shiro 会话管理 + +Shiro 提供了一套独特的会话管理方案:其 Session 可以使用 Java SE 程序,也可以使用于 Java Web 程序。 + +在 Shiro 中,[SessionManager](http://shiro.apache.org/session-management.html#the-sessionmanager) 负责管理应用所有 `Subject` 的会话,如:创建、删除、失效、验证等。 + +【示例】会话使用示例 + +```java +Subject currentUser = SecurityUtils.getSubject(); + +Session session = currentUser.getSession(); +session.setAttribute( "someKey", someValue); +``` + +### 会话超时 + +默认情况下,Shiro 中的会话有效期为 30 分钟,超时后,该会话将被 Shiro 视为无效。 + +可以通过 `globalSessionTimeout` 方法设置 Shiro 会话超时时间。 + +### 会话监听 + +Shiro 提供了 `SessionListener` 接口(或 `SessionListenerAdapter` 接口),用于监听重要的会话事件,并允许使用者在事件触发时做定制化处理。 + +【示例】 + +```java +public class ShiroSessionListener implements SessionListener { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private final AtomicInteger sessionCount = new AtomicInteger(0); + + @Override + public void onStart(Session session) { + sessionCount.incrementAndGet(); + } + + @Override + public void onStop(Session session) { + sessionCount.decrementAndGet(); + } + + @Override + public void onExpiration(Session session) { + sessionCount.decrementAndGet(); + } +} +``` + +### 会话存储 + +大多数情况下,应用需要保存会话信息,以便在稍后可以使用它。 + +Shiro 提供了 `SessionManager` 接口,负责将针对会话的 CRUD 操作委派给内部组件 `SessionDAO`,该组件反映了数据访问对象(DAO)设计模式。 + +> :bell: 注意:由于会话通常具有时效性,所以一般会话天然适合存储于缓存中。存储于 Redis 中是一个不错的选择。 + +## 五、Realm + +`Realm` 是 Shiro 访问程序安全相关数据(如:用户、角色、权限)的接口。 + +`Realm` 是有开发者自己实现的,开发者可以通过实现 Realm 接口,接入应用的数据源,如:JDBC、文件、Nosql 等等。 + +### 认证令牌 + +Shiro 支持身份验证令牌。在咨询 Realm 进行认证尝试之前,将调用其支持方法。 如果返回值为 true,则仅会调用其 [getAuthenticationInfo(token)](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/realm/Realm.html#getAuthenticationInfo-org.apache.shiro.authc.AuthenticationToken-) 方法。通常,Realm 会检查所提交令牌的类型(接口或类),以查看其是否可以处理它。 + +令牌认证处理流程如下: + +1. 检查用于标识 principal 的令牌(帐户标识信息)。 +2. 根据 principal,在数据源中查找相应的帐户数据。 +3. 确保令牌提供的凭证与数据存储中存储的凭证匹配。 +4. 如果 credentials 匹配,则返回 `AuthenticationInfo` 实例。 +5. 如果 credentials 不匹配,则抛出 `AuthenticationException` 异常。 + +### 加密 + +通过前文,可以了解:Shiro 需要通过一对 principal 和 credentials 来确认身份是否匹配(即认证)。 + +一般来说,成熟软件是不允许存储账户、密码这些敏感数据时,使用明文存储。所以,通常要将密码加密后存储。 + +Shiro 提供了一些加密器,其思想就是用 MD5、SHA 这种数字签名算法,加 Salt,然后转为 Base64 字符串。为了避免被暴力破解,Shiro 使用多次加密的方式获得最终的 credentials 字符串。 + +【示例】Shiro 加密密码示例 + +```java +import org.apache.shiro.crypto.hash.Sha256Hash; +import org.apache.shiro.crypto.RandomNumberGenerator; +import org.apache.shiro.crypto.SecureRandomNumberGenerator; +... + +//We'll use a Random Number Generator to generate salts. This +//is much more secure than using a username as a salt or not +//having a salt at all. Shiro makes this easy. +// +//Note that a normal app would reference an attribute rather +//than create a new RNG every time: +RandomNumberGenerator rng = new SecureRandomNumberGenerator(); +Object salt = rng.nextBytes(); + +//Now hash the plain-text password with the random salt and multiple +//iterations and then Base64-encode the value (requires less space than Hex): +String hashedPasswordBase64 = new Sha256Hash(plainTextPassword, salt, 1024).toBase64(); + +User user = new User(username, hashedPasswordBase64); +//save the salt with the new account. The HashedCredentialsMatcher +//will need it later when handling login attempts: +user.setPasswordSalt(salt); +userDAO.create(user); +``` + +## 六、配置 + +### 过滤链 + +运行 Web 应用程序时,Shiro 将创建一些有用的默认 Filter 实例。 + +| Filter Name | Class | +| :---------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| anon | [org.apache.shiro.web.filter.authc.AnonymousFilter](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/web/filter/authc/AnonymousFilter.html) | +| authc | [org.apache.shiro.web.filter.authc.FormAuthenticationFilter](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/web/filter/authc/FormAuthenticationFilter.html) | +| authcBasic | [org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/web/filter/authc/BasicHttpAuthenticationFilter.html) | +| logout | [org.apache.shiro.web.filter.authc.LogoutFilter](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/web/filter/authc/LogoutFilter.html) | +| noSessionCreation | [org.apache.shiro.web.filter.session.NoSessionCreationFilter](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/web/filter/session/NoSessionCreationFilter.html) | +| perms | [org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/web/filter/authz/PermissionsAuthorizationFilter.html) | +| port | [org.apache.shiro.web.filter.authz.PortFilter](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/web/filter/authz/PortFilter.html) | +| rest | [org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/web/filter/authz/HttpMethodPermissionFilter.html) | +| roles | [org.apache.shiro.web.filter.authz.RolesAuthorizationFilter](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/web/filter/authz/RolesAuthorizationFilter.html) | +| ssl | [org.apache.shiro.web.filter.authz.SslFilter](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/web/filter/authz/SslFilter.html) | +| user | [org.apache.shiro.web.filter.authc.UserFilter](http://shiro.apache.org/static/current/apidocs/org/apache/shiro/web/filter/authc/UserFilter.html) | + +### RememberMe + +```java +UsernamePasswordToken token = new UsernamePasswordToken(username, password); +token.setRememberMe(true); +SecurityUtils.getSubject().login(token); +``` + +## 参考资料 + +- [Shiro 官方文档](http://shiro.apache.org/reference.html) +- [跟我学 Shiro](http://jinnianshilongnian.iteye.com/category/305053) +- [The New RBAC: Resource-Based Access Control](https://stormpath.com/blog/new-rbac-resource-based-access-control) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/02.SpringSecurity.md" "b/docs/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/02.SpringSecurity.md" new file mode 100644 index 00000000..3434c2dd --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/02.SpringSecurity.md" @@ -0,0 +1,237 @@ +--- +title: Spring Security 快速入门 +date: 2022-02-17 22:34:30 +order: 02 +categories: + - Java + - 框架 + - 安全 +tags: + - Java + - 框架 + - 安全 + - SpringSecurity +permalink: /pages/050cdd/ +--- + +# Spring Security 快速入门 + +## 快速开始 + +参考:[Securing a Web Application](https://spring.io/guides/gs/securing-web/) + +## 核心 API + +## 设计原理 + +Spring Security 对于 Servlet 的支持基于过滤链(`FilterChain`)实现。 + +Spring 提供了一个名为 `DelegatingFilterProxy` 的 `Filter` 实现,该实现允许在 Servlet 容器的生命周期和 Spring 的 `ApplicationContext` 之间进行桥接。 Servlet 容器允许使用其自己的标准注册 Filters,但它不了解 Spring 定义的 Bean。 `DelegatingFilterProxy` 可以通过标准的 Servlet 容器机制进行注册,但是可以将所有工作委托给实现 Filter 的 Spring Bean。 + +```java +public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { + // Lazily get Filter that was registered as a Spring Bean + // For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0 + Filter delegate = getFilterBean(someBeanName); + // delegate work to the Spring Bean + delegate.doFilter(request, response); +} +``` + +`FilterChainProxy` 使用 `SecurityFilterChain` 确定应对此请求调用哪些 Spring Security 过滤器。 + +`SecurityFilterChain` 中的安全过滤器通常是 Bean,但它们是使用 `FilterChainProxy` 而不是 `DelegatingFilterProxy` 注册的。 + +实际上,`FilterChainProxy` 可用于确定应使用哪个 `SecurityFilterChain`。如果您的应用程序可以为不同的模块提供完全独立的配置。 + +![multi securityfilterchain](https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5/images/servlet/architecture/multi-securityfilterchain.png) + +ExceptionTranslationFilter 可以将 AccessDeniedException 和 AuthenticationException 转换为 HTTP 响应。 + +![exceptiontranslationfilter](https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5/images/servlet/architecture/exceptiontranslationfilter.png) + +核心源码: + +```java +try { + filterChain.doFilter(request, response); +} catch (AccessDeniedException | AuthenticationException e) { + if (!authenticated || e instanceof AuthenticationException) { + startAuthentication(); + } else { + accessDenied(); + } +} +``` + +## 认证 + +### 数据模型 + +Spring Security 框架中的认证数据模型如下: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200331115710.png) + +- `Authentication` - 认证信息实体。 + - `principal` - 用户标识。如:用户名、账户名等。通常是 `UserDetails` 的实例(后面详细讲解)。 + - `credentials` - 认证凭证。如:密码等。 + - `authorities` - 授权信息。如:用户的角色、权限等信息。 +- `SecurityContext` - 安全上下文。包含一个 `Authentication` 对象。 +- `SecurityContextHolder` - 安全上下文持有者。用于存储认证信息。 + +【示例】注册认证信息 + +```java +SecurityContext context = SecurityContextHolder.createEmptyContext(); +Authentication authentication = + new TestingAuthenticationToken("username", "password", "ROLE_USER"); +context.setAuthentication(authentication); +SecurityContextHolder.setContext(context); +``` + +【示例】访问认证信息 + +### 认证基本流程 + +AbstractAuthenticationProcessingFilter 用作验证用户凭据的基本过滤器。 在对凭证进行身份验证之前,Spring Security 通常使用 AuthenticationEntryPoint 请求凭证。 + +![abstractauthenticationprocessingfilter](https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5/images/servlet/authentication/architecture/abstractauthenticationprocessingfilter.png) + +- (1)当用户提交其凭据时,`AbstractAuthenticationProcessingFilter` 从要验证的 `HttpServletRequest` 创建一个 `Authentication`。创建的身份验证类型取决于 `AbstractAuthenticationProcessingFilter` 的子类。例如,`UsernamePasswordAuthenticationFilter` 根据在 `HttpServletRequest` 中提交的用户名和密码来创建 `UsernamePasswordAuthenticationToken`。 +- (2)接下来,将身份验证传递到 `AuthenticationManager` 进行身份验证。 +- (3)如果身份验证失败,则认证失败 + - 清除 `SecurityContextHolder`。 + - 调用 `RememberMeServices.loginFail`。如果没有配置 remember me,则为空。 + - 调用 `AuthenticationFailureHandler`。 +- (4)如果身份验证成功,则认证成功。 + - 如果是新的登录,则通知 `SessionAuthenticationStrategy`。 + - 身份验证是在 `SecurityContextHolder` 上设置的。之后,`SecurityContextPersistenceFilter` 将 `SecurityContext` 保存到 `HttpSession` 中。 + - 调用 `RememberMeServices.loginSuccess`。如果没有配置 remember me,则为空。 + - `ApplicationEventPublisher` 发布一个 `InteractiveAuthenticationSuccessEvent`。 + +### 用户名/密码认证 + +读取用户名和密码的方式: + +- 表单 +- 基本认证 +- 数字认证 + +存储机制 + +- 内存 +- JDBC +- [UserDetailsService](https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5/#servlet-authentication-userdetailsservice) +- LDAP + +#### 表单认证 + +spring security 支持通过从 html 表单获取登录时提交的用户名、密码。 + +![loginurlauthenticationentrypoint](https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5/images/servlet/authentication/unpwd/loginurlauthenticationentrypoint.png) + +一旦,登录信息被提交,`UsernamePasswordAuthenticationFilter` 就会验证用户名和密码。 + +![usernamepasswordauthenticationfilter](https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5/images/servlet/authentication/unpwd/usernamepasswordauthenticationfilter.png) + +#### 基本认证 + +```java +protected void configure(HttpSecurity http) { + http + // ... + .httpBasic(withDefaults()); +} +``` + +#### 内存认证 + +`InMemoryUserDetailsManager` 实现了 [UserDetailsService](https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5/#servlet-authentication-userdetailsservice) ,提供了基本的用户名、密码认证,其认证数据存储在内存中。 + +```java +@Bean +public UserDetailsService users() { + // The builder will ensure the passwords are encoded before saving in memory + UserBuilder users = User.withDefaultPasswordEncoder(); + UserDetails user = users + .username("user") + .password("password") + .roles("USER") + .build(); + UserDetails user = users + .username("admin") + .password("password") + .roles("USER", "ADMIN") + .build(); + return new InMemoryUserDetailsManager(user, admin); +} +``` + +#### JDBC 认证 + +JdbcUserDetailsManager 实现了 [UserDetailsService](https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5/#servlet-authentication-userdetailsservice) ,提供了基本的用户名、密码认证,其认证数据存储在关系型数据库中,通过 JDBC 方式访问。 + +``` +@Bean +UserDetailsManager users(DataSource dataSource) { + UserDetails user = User.builder() + .username("user") + .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") + .roles("USER") + .build(); + UserDetails admin = User.builder() + .username("admin") + .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW") + .roles("USER", "ADMIN") + .build(); + JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource); + users.createUser() +} +``` + +基本的 scheam: + +```sql +create table users( + username varchar_ignorecase(50) not null primary key, + password varchar_ignorecase(50) not null, + enabled boolean not null +); + +create table authorities ( + username varchar_ignorecase(50) not null, + authority varchar_ignorecase(50) not null, + constraint fk_authorities_users foreign key(username) references users(username) +); +create unique index ix_auth_username on authorities (username,authority); +``` + +#### UserDetailsService + +`UserDetails` 由 `UserDetailsService` 返回。 `DaoAuthenticationProvider` 验证 `UserDetails`,然后返回身份验证,该身份验证的主体是已配置的 `UserDetailsService` 返回的 `UserDetails`。 + +`DaoAuthenticationProvider` 使用 `UserDetailsService` 检索用户名,密码和其他用于使用用户名和密码进行身份验证的属性。 Spring Security 提供 `UserDetailsService` 的内存中和 JDBC 实现。 + +您可以通过将自定义 `UserDetailsService` 公开为 bean 来定义自定义身份验证。 + +#### PasswordEncoder + +Spring Security 的 servlet 支持通过与 `PasswordEncoder` 集成来安全地存储密码。 可以通过公开一个 PasswordEncoder Bean 来定制 Spring Security 使用的 PasswordEncoder 实现。 + +![daoauthenticationprovider](https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5/images/servlet/authentication/unpwd/daoauthenticationprovider.png) + +### Remember-Me + +## Spring Boot 集成 + +`@EnableWebSecurity` 和 `@Configuration` 注解一起使用, 注解 `WebSecurityConfigurer` 类型的类。 + +或者利用`@EnableWebSecurity`注解继承 `WebSecurityConfigurerAdapter` 的类,这样就构成了 _Spring Security_ 的配置。 + +- configure(WebSecurity):通过重载该方法,可配置 Spring Security 的 Filter 链。 +- configure(HttpSecurity):通过重载该方法,可配置如何通过拦截器保护请求。 + +## 参考资料 + +- [Spring Security Architecture](https://spring.io/guides/topicals/spring-security-architecture) +- [Securing a Web Application](https://spring.io/guides/gs/securing-web/) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/13.IO/01.Netty.md" "b/docs/01.Java/13.\346\241\206\346\236\266/13.IO/01.Netty.md" new file mode 100644 index 00000000..8bf182aa --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/13.IO/01.Netty.md" @@ -0,0 +1,146 @@ +--- +title: Netty 快速入门 +date: 2022-02-17 22:34:30 +order: 01 +categories: + - Java + - 框架 + - IO +tags: + - Java + - IO + - Netty +permalink: /pages/10bd70/ +--- + +# Netty 快速入门 + +## Netty 简介 + +> **Netty 是一款基于 NIO(Nonblocking I/O,非阻塞 IO)开发的网络通信框架**。 + +### Netty 的特性 + +- **高并发**:Netty 是一款**基于 NIO**(Nonblocking IO,非阻塞 IO)开发的网络通信框架,对比于 BIO(Blocking I/O,阻塞 IO),他的并发性能得到了很大提高。 +- **传输快**:Netty 的传输依赖于**内存零拷贝**特性,尽量减少不必要的内存拷贝,实现了更高效率的传输。 +- **封装好**:Netty **封装了 NIO 操作**的很多细节,提供了易于使用调用接口。 + +## 核心组件 + +- `Channel`:Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 等。 +- `EventLoop`:主要是配合 Channel 处理 I/O 操作,用来处理连接的生命周期中所发生的事情。 +- `ChannelFuture`:Netty 框架中所有的 I/O 操作都为异步的,因此我们需要 ChannelFuture 的 addListener()注册一个 ChannelFutureListener 监听事件,当操作执行成功或者失败时,监听就会自动触发返回结果。 +- `ChannelHandler`:充当了所有处理入站和出站数据的逻辑容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。 +- `ChannelPipeline`:为 ChannelHandler 链提供了容器,当 channel 创建时,就会被自动分配到它专属的 ChannelPipeline,这个关联是永久性的。 + +Netty 有两种发送消息的方式: + +- 直接写入 Channel 中,消息从 ChannelPipeline 当中尾部开始移动; +- 写入和 ChannelHandler 绑定的 ChannelHandlerContext 中,消息从 ChannelPipeline 中的下一个 ChannelHandler 中移动。 + +## 高性能 + +Netty 高性能表现在哪些方面: + +- **NIO 线程模型**:同步非阻塞,用最少的资源做更多的事。 +- **内存零拷贝**:尽量减少不必要的内存拷贝,实现了更高效率的传输。 +- **内存池设计**:申请的内存可以重用,主要指直接内存。内部实现是用一颗二叉查找树管理内存分配情况。 +- **串形化处理读写**:避免使用锁带来的性能开销。 +- **高性能序列化协议**:支持 protobuf 等高性能序列化协议。 + +## 零拷贝 + +### 传统意义的拷贝 + +是在发送数据的时候,传统的实现方式是: + +`File.read(bytes)` + +`Socket.send(bytes)` + +这种方式需要四次数据拷贝和四次上下文切换: + +1. 数据从磁盘读取到内核的 read buffer + +2. 数据从内核缓冲区拷贝到用户缓冲区 +3. 数据从用户缓冲区拷贝到内核的 socket buffer +4. 数据从内核的 socket buffer 拷贝到网卡接口(硬件)的缓冲区 + +### 零拷贝的概念 + +明显上面的第二步和第三步是非必要的,通过 java 的 FileChannel.transferTo 方法,可以避免上面两次多余的拷贝(当然这需要底层操作系统支持) + +- 调用 transferTo,数据从文件由 DMA 引擎拷贝到内核 read buffer +- 接着 DMA 从内核 read buffer 将数据拷贝到网卡接口 buffer + +上面的两次操作都不需要 CPU 参与,所以就达到了零拷贝。 + +### Netty 中的零拷贝 + +主要体现在三个方面: + +**bytebuffer** + +Netty 发送和接收消息主要使用 bytebuffer,bytebuffer 使用对外内存(DirectMemory)直接进行 Socket 读写。 + +原因:如果使用传统的堆内存进行 Socket 读写,JVM 会将堆内存 buffer 拷贝一份到直接内存中然后再写入 socket,多了一次缓冲区的内存拷贝。DirectMemory 中可以直接通过 DMA 发送到网卡接口 + +**Composite Buffers** + +传统的 ByteBuffer,如果需要将两个 ByteBuffer 中的数据组合到一起,我们需要首先创建一个 size=size1+size2 大小的新的数组,然后将两个数组中的数据拷贝到新的数组中。但是使用 Netty 提供的组合 ByteBuf,就可以避免这样的操作,因为 CompositeByteBuf 并没有真正将多个 Buffer 组合起来,而是保存了它们的引用,从而避免了数据的拷贝,实现了零拷贝。 + +**对于 FileChannel.transferTo 的使用** + +Netty 中使用了 FileChannel 的 transferTo 方法,该方法依赖于操作系统实现零拷贝。 + +## Netty 流程 + +## 应用 + +> Netty 是一个广泛使用的 Java 网络编程框架。很多著名软件都使用了它,如:Dubbo、Cassandra、Elasticsearch、Vert.x 等。 + +有了 Netty,你可以实现自己的 HTTP 服务器,FTP 服务器,UDP 服务器,RPC 服务器,WebSocket 服务器,Redis 的 Proxy 服务器,MySQL 的 Proxy 服务器等等。 + +```java +public class NettyOioServer { + + public void server(int port) throws Exception { + final ByteBuf buf = Unpooled.unreleasableBuffer( + Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8"))); + EventLoopGroup group = new OioEventLoopGroup(); + try { + ServerBootstrap b = new ServerBootstrap(); //1 + + b.group(group) //2 + .channel(OioServerSocketChannel.class) + .localAddress(new InetSocketAddress(port)) + .childHandler(new ChannelInitializer() {//3 + @Override + public void initChannel(SocketChannel ch) + throws Exception { + ch.pipeline().addLast(new ChannelInboundHandlerAdapter() { //4 + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);//5 + } + }); + } + }); + ChannelFuture f = b.bind().sync(); //6 + f.channel().closeFuture().sync(); + } finally { + group.shutdownGracefully().sync(); //7 + } + } +} +``` + +## 参考资料 + +- **官方** + - [Netty 官网](https://netty.io/) + - [Netty Github](https://github.com/netty/netty) +- **文章** + - [Netty 入门教程——认识 Netty](https://www.jianshu.com/p/b9f3f6a16911) + - [彻底理解 Netty,这一篇文章就够了](https://juejin.im/post/5bdaf8ea6fb9a0227b02275a) + - [Java 200+ 面试题补充 ② Netty 模块](https://juejin.im/post/5c81b08f5188257a323f4cef) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/README.md" new file mode 100644 index 00000000..7843b3d3 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/README.md" @@ -0,0 +1,121 @@ +--- +title: Java 框架 +date: 2022-02-18 08:53:11 +categories: + - Java + - 框架 +tags: + - Java + - 框架 +permalink: /pages/e373d7/ +hidden: true +index: false +--- + +# Java 框架 + +## 📖 内容 + +### Spring + +#### 综合 + +- [Spring 概述](01.Spring/00.Spring综合/01.Spring概述.md) +- [SpringBoot 知识图谱](01.Spring/00.Spring综合/21.SpringBoot知识图谱.md) +- [SpringBoot 基本原理](01.Spring/00.Spring综合/22.SpringBoot基本原理.md) +- [Spring 面试](01.Spring/00.Spring综合/99.Spring面试.md) + +#### 核心 + +- [Spring Bean](01.Spring/01.Spring核心/01.SpringBean.md) +- [Spring IoC](01.Spring/01.Spring核心/02.SpringIoC.md) +- [Spring 依赖查找](01.Spring/01.Spring核心/03.Spring依赖查找.md) +- [Spring 依赖注入](01.Spring/01.Spring核心/04.Spring依赖注入.md) +- [Spring IoC 依赖来源](01.Spring/01.Spring核心/05.SpringIoC依赖来源.md) +- [Spring Bean 作用域](01.Spring/01.Spring核心/06.SpringBean作用域.md) +- [Spring Bean 生命周期](01.Spring/01.Spring核心/07.SpringBean生命周期.md) +- [Spring 配置元数据](01.Spring/01.Spring核心/08.Spring配置元数据.md) +- [Spring AOP](01.Spring/01.Spring核心/10.SpringAop.md) +- [Spring 资源管理](01.Spring/01.Spring核心/20.Spring资源管理.md) +- [Spring 校验](01.Spring/01.Spring核心/21.Spring校验.md) +- [Spring 数据绑定](01.Spring/01.Spring核心/22.Spring数据绑定.md) +- [Spring 类型转换](01.Spring/01.Spring核心/23.Spring类型转换.md) +- [Spring EL 表达式](01.Spring/01.Spring核心/24.SpringEL.md) +- [Spring 事件](01.Spring/01.Spring核心/25.Spring事件.md) +- [Spring 国际化](01.Spring/01.Spring核心/26.Spring国际化.md) +- [Spring 泛型处理](01.Spring/01.Spring核心/27.Spring泛型处理.md) +- [Spring 注解](01.Spring/01.Spring核心/28.Spring注解.md) +- [Spring Environment 抽象](01.Spring/01.Spring核心/29.SpringEnvironment抽象.md) +- [SpringBoot 教程之快速入门](01.Spring/01.Spring核心/31.SpringBoot之快速入门.md) +- [SpringBoot 之属性加载](01.Spring/01.Spring核心/32.SpringBoot之属性加载.md) +- [SpringBoot 之 Profile](01.Spring/01.Spring核心/33.SpringBoot之Profile.md) + +#### 数据 + +- [Spring 之数据源](01.Spring/02.Spring数据/01.Spring之数据源.md) +- [Spring 之 JDBC](01.Spring/02.Spring数据/02.Spring之JDBC.md) +- [Spring 之事务](01.Spring/02.Spring数据/03.Spring之事务.md) +- [Spring 之 JPA](01.Spring/02.Spring数据/04.Spring之JPA.md) +- [Spring 集成 Mybatis](01.Spring/02.Spring数据/10.Spring集成Mybatis.md) +- [Spring 访问 Redis](01.Spring/02.Spring数据/21.Spring访问Redis.md) +- [Spring 访问 MongoDB](01.Spring/02.Spring数据/22.Spring访问MongoDB.md) +- [Spring 访问 Elasticsearch](01.Spring/02.Spring数据/23.Spring访问Elasticsearch.md) + +#### Web + +- [Spring WebMvc](01.Spring/03.SpringWeb/01.SpringWebMvc.md) +- [SpringBoot 之应用 EasyUI](01.Spring/03.SpringWeb/21.SpringBoot之应用EasyUI.md) + +#### IO + +- [SpringBoot 之异步请求](01.Spring/04.SpringIO/01.SpringBoot之异步请求.md) +- [SpringBoot 之 Json](01.Spring/04.SpringIO/02.SpringBoot之Json.md) +- [SpringBoot 之邮件](01.Spring/04.SpringIO/03.SpringBoot之邮件.md) + +#### 集成 + +- [Spring 集成缓存中间件](01.Spring/05.Spring集成/01.Spring集成缓存.md) +- [Spring 集成定时任务中间件](01.Spring/05.Spring集成/02.Spring集成调度器.md) +- [Spring 集成 Dubbo](01.Spring/05.Spring集成/03.Spring集成Dubbo.md) + +#### 其他 + +- [Spring4 升级](01.Spring/99.Spring其他/01.Spring4升级.md) +- [SpringBoot 之 banner](01.Spring/99.Spring其他/21.SpringBoot之banner.md) +- [SpringBoot 之 Actuator](01.Spring/99.Spring其他/22.SpringBoot之Actuator.md) + +### ORM + +- [Mybatis 快速入门](11.ORM/01.Mybatis快速入门.md) +- [Mybatis 原理](11.ORM/02.Mybatis原理.md) + +### 安全 + +> Java 领域比较流行的安全框架就是 shiro 和 spring-security。 +> +> shiro 更为简单、轻便,容易理解,能满足大多数基本安全场景下的需要。 +> +> spring-security 功能更丰富,也比 shiro 更复杂。值得一提的是由于 spring-security 是 spring 团队开发,所以集成 spring 和 spring-boot 框架更容易。 + +- [Shiro](12.安全/01.Shiro.md) +- [SpringSecurity](12.安全/02.SpringSecurity.md) + +### IO + +- [Netty](13.IO/01.Netty.md) + +## 📚 资料 + +- **Mybatis** + - [Mybatis Github](https://github.com/mybatis/mybatis-3) + - [Mybatis 官网](http://www.mybatis.org/mybatis-3/) + - [MyBatis 官方代码生成(mybatis-generator)](https://github.com/mybatis/generator) + - [MyBatis 官方集成 Spring(mybatis-spring)](https://github.com/mybatis/spring) + - [Mybatis 官方集成 Spring Boot(mybatis-spring-boot)](https://github.com/mybatis/spring-boot-starter) + - [MyBatis-Plus](https://github.com/baomidou/mybatis-plus) - CRUD 扩展插件、代码生成器、分页器等多功能 + - [Mapper](https://github.com/abel533/Mapper) - CRUD 扩展插件 + - [Mybatis-PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) - Mybatis 通用分页插件 + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/02.Java\347\274\223\345\255\230\344\270\255\351\227\264\344\273\266.md" "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/02.Java\347\274\223\345\255\230\344\270\255\351\227\264\344\273\266.md" new file mode 100644 index 00000000..9adf2b83 --- /dev/null +++ "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/02.Java\347\274\223\345\255\230\344\270\255\351\227\264\344\273\266.md" @@ -0,0 +1,340 @@ +--- +title: Java 缓存中间件 +date: 2022-02-17 22:34:30 +order: 02 +categories: + - Java + - 中间件 + - 缓存 +tags: + - Java + - 中间件 + - 缓存 +permalink: /pages/85460d/ +--- + +# Java 缓存中间件 + +> 关键词:Spring Cache、J2Cache、JetCache + +## 一 、JSR 107 + +[JSR107](https://www.jcp.org/en/jsr/detail?id=107) 中制订了 Java 缓存的规范。 + +因此,在很多缓存框架、缓存库中,其 API 都参考了 JSR 107 规范。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200709174139.png) + +Java Caching 定义了 5 个核心接口 + +- **CachingProvider** - 定义了创建、配置、获取、管理和控制多个 `CacheManager`。一个应用可以在运行期访问多个 `CachingProvider`。 +- **CacheManager** - 定义了创建、配置、获取、管理和控制多个唯一命名的 Cache,这些 Cache 存在于 CacheManager 的上下文中。一个 CacheManager 仅被一个 CachingProvider 所拥有。 +- **Cache** - 是一个类似 Map 的数据结构并临时存储以 Key 为索引的值。一个 Cache 仅被一个 CacheManager 所拥有。 +- **Entry** - 是一个存储在 Cache 中的 key-value 对。 +- **Expiry** - 每一个存储在 Cache 中的条目有一个定义的有效期,即 Expiry Duration。一旦超过这个时间,条目为过期的状态。一旦过期,条目将不可访问、更新和删除。缓存有效期可以通过 ExpiryPolicy 设置。 + +## 二、Spring Cache + +> 详见:[Spring Cache 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/integration.html#cache) + +Spring 作为 Java 开发最著名的框架,也提供了缓存功能的框架—— Spring Cache。 + +Spring 支持基于注释(annotation)的缓存(cache)技术,它本质上不是一个具体的缓存实现方案(例如:EHCache 或 OSCache),而是一个对缓存使用的抽象,通过在既有代码中添加少量它定义的各种 annotation,即能够达到缓存方法的返回对象的效果。 + +Spring Cache 的特点: + +- 通过缓存注解即可支持缓存功能 +- 支持 Spring EL 表达式 +- 支持 AspectJ +- 支持自定义 key 和缓存管理 + +### 开启缓存注解 + +Spring 为缓存功能提供了注解功能,但是你必须启动注解。 + +有两种方式: + +(一)使用标记注解 `@EnableCaching` + +这种方式对于 Spring 或 Spring Boot 项目都适用。 + +```java +@Configuration +@EnableCaching +public class AppConfig { +} +``` + +(二)在 xml 中声明 + +```xml + +``` + +### spring 缓存注解 API + +Spring 对缓存的支持类似于对事务的支持。 + +首先使用注解标记方法,相当于定义了切点,然后使用 Aop 技术在这个方法的调用前、调用后获取方法的入参和返回值,进而实现了缓存的逻辑。 + +#### @Cacheable + +**`@Cacheable` 用于触发缓存**。 + +表明所修饰的方法是可以缓存的:当第一次调用这个方法时,它的结果会被缓存下来,在缓存的有效时间内,以后访问这个方法都直接返回缓存结果,不再执行方法中的代码段。 + +这个注解可以用`condition`属性来设置条件,如果不满足条件,就不使用缓存能力,直接执行方法。 + +可以使用`key`属性来指定 key 的生成规则。 + +#### @CachePut + +**`@CachePut` 用于更新缓存**。 + +与`@Cacheable`不同,`@CachePut`不仅会缓存方法的结果,还会执行方法的代码段。 + +它支持的属性和用法都与`@Cacheable`一致。 + +#### @CacheEvict + +**`@CacheEvict` 用于清除缓存**。 + +与`@Cacheable`功能相反,`@CacheEvict`表明所修饰的方法是用来删除失效或无用的缓存数据。 + +下面是`@Cacheable`、`@CacheEvict`和`@CachePut`基本使用方法的一个集中展示: + +```java +@Service +public class UserService { + // @Cacheable可以设置多个缓存,形式如:@Cacheable({"books", "isbns"}) + @Cacheable(value={"users"}, key="#user.id") + public User findUser(User user) { + return findUserInDB(user.getId()); + } + + @Cacheable(value = "users", condition = "#user.getId() <= 2") + public User findUserInLimit(User user) { + return findUserInDB(user.getId()); + } + + @CachePut(value = "users", key = "#user.getId()") + public void updateUser(User user) { + updateUserInDB(user); + } + + @CacheEvict(value = "users") + public void removeUser(User user) { + removeUserInDB(user.getId()); + } + + @CacheEvict(value = "users", allEntries = true) + public void clear() { + removeAllInDB(); + } +} +``` + +#### @Caching + +**`@Caching` 用于组合定义多种缓存功能**。 + +如果需要使用同一个缓存注解(`@Cacheable`、`@CacheEvict`或`@CachePut`)多次修饰一个方法,就需要用到`@Caching`。 + +```java +@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") }) +public Book importBooks(String deposit, Date date) +``` + +#### @CacheConfig + +**`@CacheConfig` 用于定义公共缓存配置**。 + +与前面的缓存注解不同,这是一个类级别的注解。 + +如果类的所有操作都是缓存操作,你可以使用`@CacheConfig`来指定类,省去一些配置。 + +```java +@CacheConfig("books") +public class BookRepositoryImpl implements BookRepository { + @Cacheable + public Book findBook(ISBN isbn) {...} +} +``` + +## 三、Spring Boot Cache + +> 详见:[Spring Boot Cache 特性官方文档](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/html/spring-boot-features.html#boot-features-caching) + +Spring Boot Cache 是在 Spring Cache 的基础上做了封装,使得使用更为便捷。 + +### Spring Boot Cache 快速入门 + +(1)引入依赖 + +```xml + + org.springframework.boot + spring-boot-starter-cache + + + + + org.springframework.boot + spring-boot-starter-data-redis + +``` + +(2)缓存配置 + +例如,选用缓存为 redis,则需要配置 redis 相关的配置项(如:数据源、连接池等配置信息) + +```properties +# 缓存类型,支持类型:GENERIC、JCACHE、EHCACHE、HAZELCAST、INFINISPAN、COUCHBASE、REDIS、CAFFEINE、SIMPLE +spring.cache.type = redis +# 全局缓存时间 +spring.cache.redis.time-to-live = 60s + +# Redis 配置 +spring.redis.database = 0 +spring.redis.host = localhost +spring.redis.port = 6379 +spring.redis.password = +``` + +(3)使用 `@EnableCaching` 开启缓存 + +```java +@EnableCaching +@SpringBootApplication +public class Application { + // ... +} +``` + +(4)缓存注解(`@Cacheable`、`@CachePut`、`@CacheEvit` 等)使用方式与 Spring Cache 完全一样 + +## 四、JetCache + +> JetCache 是一个基于 Java 的缓存系统封装,提供统一的 API 和注解来简化缓存的使用。 JetCache 提供了比 SpringCache 更加强大的注解,可以原生的支持 TTL、两级缓存、分布式自动刷新,还提供了`Cache`接口用于手工缓存操作。 当前有四个实现,`RedisCache`、`TairCache`(此部分未在 github 开源)、`CaffeineCache`(in memory)和一个简易的`LinkedHashMapCache`(in memory),要添加新的实现也是非常简单的。 +> +> 详见:[jetcache Github](https://github.com/alibaba/jetcache) + +### jetcache 快速入门 + +如果使用 Spring Boot,可以按如下的方式配置(这里使用了 jedis 客户端连接 redis,如果需要集群、读写分离、异步等特性支持请使用[lettuce](https://github.com/alibaba/jetcache/wiki/RedisWithLettuce_CN)客户端)。 + +(1)引入 POM + +```xml + + com.alicp.jetcache + jetcache-starter-redis + 2.5.14 + +``` + +(2)配置 + +配置一个 spring boot 风格的 application.yml 文件,把他放到资源目录中 + +```yml +jetcache: + statIntervalMinutes: 15 + areaInCacheName: false + local: + default: + type: linkedhashmap + keyConvertor: fastjson + remote: + default: + type: redis + keyConvertor: fastjson + valueEncoder: java + valueDecoder: java + poolConfig: + minIdle: 5 + maxIdle: 20 + maxTotal: 50 + host: 127.0.0.1 + port: 6379 +``` + +(3)开启缓存 + +然后创建一个 App 类放在业务包的根下,EnableMethodCache,EnableCreateCacheAnnotation 这两个注解分别激活 Cached 和 CreateCache 注解,其他和标准的 Spring Boot 程序是一样的。这个类可以直接 main 方法运行。 + +```java +package com.company.mypackage; + +import com.alicp.jetcache.anno.config.EnableCreateCacheAnnotation; +import com.alicp.jetcache.anno.config.EnableMethodCache; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@EnableMethodCache(basePackages = "com.company.mypackage") +@EnableCreateCacheAnnotation +public class MySpringBootApp { + public static void main(String[] args) { + SpringApplication.run(MySpringBootApp.class); + } +} +``` + +(4)API 基本使用 + +创建缓存实例 + +通过 @CreateCache 注解创建一个缓存实例,默认超时时间是 100 秒 + +```java +@CreateCache(expire = 100) +private Cache userCache; +``` + +用起来就像 map 一样 + +```java +UserDO user = userCache.get(123L); +userCache.put(123L, user); +userCache.remove(123L); +``` + +创建一个两级(内存+远程)的缓存,内存中的元素个数限制在 50 个。 + +```java +@CreateCache(name = "UserService.userCache", expire = 100, cacheType = CacheType.BOTH, localLimit = 50) +private Cache userCache; +``` + +name 属性不是必须的,但是起个名字是个好习惯,展示统计数据的使用,会使用这个名字。如果同一个 area 两个 @CreateCache 的 name 配置一样,它们生成的 Cache 将指向同一个实例。 + +创建方法缓存 + +使用 @Cached 方法可以为一个方法添加上缓存。JetCache 通过 Spring AOP 生成代理,来支持缓存功能。注解可以加在接口方法上也可以加在类方法上,但需要保证是个 Spring bean。 + +```java +public interface UserService { + @Cached(name="UserService.getUserById", expire = 3600) + User getUserById(long userId); +} +``` + +## 五、j2cache + +## 六、总结 + +使用缓存框架,使得开发缓存功能非常便捷。 + +如果你的系统只需要使用一种缓存,那么推荐使用 Spring Boot Cache。Spring Boot Cache 在 Spring Cache 基础上做了封装,使用更简单、方便。 + +如果你的系统需要使用多级缓存,那么推荐使用 jetcache。 + +## 参考资料 + +- [JSR107](https://www.jcp.org/en/jsr/detail?id=107) +- [Spring Cache 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/integration.html#cache) +- [Spring Boot Cache 特性官方文档](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/html/spring-boot-features.html#boot-features-caching) +- [J2Cache Gitee](https://gitee.com/ld/J2Cache) +- [jetcache Github](https://github.com/alibaba/jetcache) +- [jetcache wiki](https://github.com/alibaba/jetcache/wiki/Home_CN) \ No newline at end of file diff --git "a/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/04.Ehcache.md" "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/04.Ehcache.md" new file mode 100644 index 00000000..dd76153f --- /dev/null +++ "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/04.Ehcache.md" @@ -0,0 +1,533 @@ +--- +title: Ehcache 快速入门 +date: 2022-02-17 22:34:30 +order: 04 +categories: + - Java + - 中间件 + - 缓存 +tags: + - Java + - 中间件 + - 缓存 + - Ehcache +permalink: /pages/5f7893/ +--- + +# Ehcache 快速入门 + +> EhCache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/cache/ehcache-architecture.png) + +## 一、简介 + +> Ehcache 虽然也支持分布式模式,但是分布式方案不是很好好,建议只将其作为单机的进程内缓存使用。 + +### Ehcache 特性 + +优点 + +- 快速、简单 +- 支持多种缓存策略:LRU、LFU、FIFO 淘汰算法 +- 缓存数据有两级:内存和磁盘,因此无需担心容量问题 +- 缓存数据会在虚拟机重启的过程中写入磁盘 +- 可以通过 RMI、可插入 API 等方式进行分布式缓存 +- 具有缓存和缓存管理器的侦听接口 +- 支持多缓存管理器实例,以及一个实例的多个缓存区域 +- 提供 Hibernate 的缓存实现 + +缺点 + +- **使用磁盘 Cache 的时候非常占用磁盘空间** +- **不保证数据的安全** +- 虽然支持分布式缓存,但效率不高(通过组播方式,在不同节点之间同步数据)。 + +### Ehcache 集群 + +Ehcache 目前支持五种集群方式: + +- RMI +- JMS +- JGroup +- Terracotta +- Ehcache Server + +#### RMI + +使用组播方式通知所有节点同步数据。 + +如果网络有问题,或某台服务宕机,则存在数据无法同步的可能,导致数据不一致。 + +![Ehcache Image](https://www.ehcache.org/images/documentation/rmi_replication.png) + +#### JMS + +JMS 类似 MQ,所有节点订阅消息,当某节点缓存发生变化,就向 JMS 发消息,其他节点感知变化后,同步数据。 + +![Ehcache Image](https://www.ehcache.org/images/documentation/jms_replication.png) + +#### Cache Server + +![Ehcache Image](https://www.ehcache.org/images/documentation/loadbalancer_hashing.png) + +## 二、快速入门 + +### 引入 Ehcache + +如果你的项目使用 maven 管理,添加以下依赖到你的 pom.xml 中。 + +```xml + + net.sf.ehcache + ehcache + 2.10.2 + pom + +``` + +如果你的项目不使用 maven 管理,请在 [Ehcache 官网下载地址](http://www.ehcache.org/downloads/) 下载 jar 包。 + +Spring 提供了对于 Ehcache 接口的封装,可以更简便的使用其功能。接入方式如下: + +如果你的项目使用 maven 管理,添加以下依赖到你的*pom.xml*中。 + +`spring-context-support`这个 jar 包中含有 Spring 对于缓存功能的抽象封装接口。 + +```xml + + org.springframework + spring-context-support + 4.1.4.RELEASE + +``` + +### 添加配置文件 + +(1)在 classpath 下添加 `ehcache.xml` +添加一个名为 _helloworld_ 的缓存。 + +```xml + + + + + + + + + + + + +``` + +### Ehcache 工作示例 + +Ehcache 会自动加载 `classpath` 根目录下名为 `ehcache.xml` 文件。 + +EhcacheDemo 的工作步骤如下: + +1. 在 EhcacheDemo 中,我们引用 `ehcache.xml` 声明的名为 _helloworld_ 的缓存来创建`Cache`对象; +2. 然后我们用一个键值对来实例化`Element`对象; +3. 将`Element`对象添加到`Cache`; +4. 然后用`Cache`的 get 方法获取`Element`对象。 + +```java +public class EhcacheDemo { + public static void main(String[] args) throws Exception { + // Create a cache manager + final CacheManager cacheManager = new CacheManager(); + + // create the cache called "helloworld" + final Cache cache = cacheManager.getCache("helloworld"); + + // create a key to map the data to + final String key = "greeting"; + + // Create a data element + final Element putGreeting = new Element(key, "Hello, World!"); + + // Put the element into the data store + cache.put(putGreeting); + + // Retrieve the data element + final Element getGreeting = cache.get(key); + + // Print the value + System.out.println(getGreeting.getObjectValue()); + } +} +``` + +输出 + +``` +Hello, World! +``` + +## 三、Ehcache API + +`Element`、`Cache`、`CacheManager`是 Ehcache 最重要的 API。 + +- `Element` - 缓存的元素,它维护着一个键值对。 +- `Cache` - 它是 Ehcache 的核心类,它有多个`Element`,并被`CacheManager`管理。它实现了对缓存的逻辑行为。 +- `CacheManager` - `Cache`的容器对象,并管理着`Cache`的生命周期。CacheManager 支持两种创建模式:单例(Singleton mode)和实例(InstanceMode)。 + +### 创建 CacheManager + +下面的代码列举了创建 `CacheManager` 的五种方式。 + +使用静态方法`create()`会以默认配置来创建单例的`CacheManager`实例。 + +`newInstance()`方法是一个工厂方法,以默认配置创建一个新的`CacheManager`实例。 + +此外,`newInstance()`还有几个重载函数,分别可以通过传入`String`、`URL`、`InputStream`参数来加载配置文件,然后创建`CacheManager`实例。 + +```java +// 使用Ehcache默认配置获取单例的CacheManager实例 +CacheManager.create(); +String[] cacheNames = CacheManager.getInstance().getCacheNames(); + +// 使用Ehcache默认配置新建一个CacheManager实例 +CacheManager.newInstance(); +String[] cacheNames = manager.getCacheNames(); + +// 使用不同的配置文件分别创建一个CacheManager实例 +CacheManager manager1 = CacheManager.newInstance("src/config/ehcache1.xml"); +CacheManager manager2 = CacheManager.newInstance("src/config/ehcache2.xml"); +String[] cacheNamesForManager1 = manager1.getCacheNames(); +String[] cacheNamesForManager2 = manager2.getCacheNames(); + +// 基于classpath下的配置文件创建CacheManager实例 +URL url = getClass().getResource("/anotherconfigurationname.xml"); +CacheManager manager = CacheManager.newInstance(url); + +// 基于文件流得到配置文件,并创建CacheManager实例 +InputStream fis = new FileInputStream(new File +("src/config/ehcache.xml").getAbsolutePath()); +try { + CacheManager manager = CacheManager.newInstance(fis); +} finally { + fis.close(); +} +``` + +### 添加缓存 + +**需要强调一点,`Cache`对象在用`addCache`方法添加到`CacheManager`之前,是无效的。** + +使用 CacheManager 的 addCache 方法可以根据缓存名将 ehcache.xml 中声明的 cache 添加到容器中;它也可以直接将 Cache 对象添加到缓存容器中。 + +`Cache`有多个构造函数,提供了不同方式去加载缓存的配置参数。 + +有时候,你可能需要使用 API 来动态的添加缓存,下面的例子就提供了这样的范例。 + +```java +// 除了可以使用xml文件中配置的缓存,你也可以使用API动态增删缓存 +// 添加缓存 +manager.addCache(cacheName); + +// 使用默认配置添加缓存 +CacheManager singletonManager = CacheManager.create(); +singletonManager.addCache("testCache"); +Cache test = singletonManager.getCache("testCache"); + +// 使用自定义配置添加缓存,注意缓存未添加进CacheManager之前并不可用 +CacheManager singletonManager = CacheManager.create(); +Cache memoryOnlyCache = new Cache("testCache", 5000, false, false, 5, 2); +singletonManager.addCache(memoryOnlyCache); +Cache test = singletonManager.getCache("testCache"); + +// 使用特定的配置添加缓存 +CacheManager manager = CacheManager.create(); +Cache testCache = new Cache( + new CacheConfiguration("testCache", maxEntriesLocalHeap) + .memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LFU) + .eternal(false) + .timeToLiveSeconds(60) + .timeToIdleSeconds(30) + .diskExpiryThreadIntervalSeconds(0) + .persistence(new PersistenceConfiguration().strategy(Strategy.LOCALTEMPSWAP))); + manager.addCache(testCache); +``` + +### 删除缓存 + +删除缓存比较简单,你只需要将指定的缓存名传入`removeCache`方法即可。 + +```java +CacheManager singletonManager = CacheManager.create(); +singletonManager.removeCache("sampleCache1"); +``` + +### 基本缓存操作 + +Cache 最重要的两个方法就是 put 和 get,分别用来添加 Element 和获取 Element。 + +Cache 还提供了一系列的 get、set 方法来设置或获取缓存参数,这里不一一列举,更多 API 操作可参考[官方 API 开发手册](http://www.ehcache.org/generated/2.10.2/pdf/Ehcache_API_Developer_Guide.pdf)。 + +```java +/** + * 测试:使用默认配置或使用指定配置来创建CacheManager + * + * @author Zhang Peng + */ +public class CacheOperationTest { + private final Logger log = LoggerFactory.getLogger(CacheOperationTest.class); + + /** + * 使用Ehcache默认配置(classpath下的ehcache.xml)获取单例的CacheManager实例 + */ + @Test + public void operation() { + CacheManager manager = CacheManager.newInstance("src/test/resources/ehcache/ehcache.xml"); + + // 获得Cache的引用 + Cache cache = manager.getCache("userCache"); + + // 将一个Element添加到Cache + cache.put(new Element("key1", "value1")); + + // 获取Element,Element类支持序列化,所以下面两种方法都可以用 + Element element1 = cache.get("key1"); + // 获取非序列化的值 + log.debug("key:{}, value:{}", element1.getObjectKey(), element1.getObjectValue()); + // 获取序列化的值 + log.debug("key:{}, value:{}", element1.getKey(), element1.getValue()); + + // 更新Cache中的Element + cache.put(new Element("key1", "value2")); + Element element2 = cache.get("key1"); + log.debug("key:{}, value:{}", element2.getObjectKey(), element2.getObjectValue()); + + // 获取Cache的元素数 + log.debug("cache size:{}", cache.getSize()); + + // 获取MemoryStore的元素数 + log.debug("MemoryStoreSize:{}", cache.getMemoryStoreSize()); + + // 获取DiskStore的元素数 + log.debug("DiskStoreSize:{}", cache.getDiskStoreSize()); + + // 移除Element + cache.remove("key1"); + log.debug("cache size:{}", cache.getSize()); + + // 关闭当前CacheManager对象 + manager.shutdown(); + + // 关闭CacheManager单例实例 + CacheManager.getInstance().shutdown(); + } +} +``` + +## 四、Ehcache 配置 + +> Ehcache 支持通过 xml 文件和 API 两种方式进行配置。 +> +> 详情参考:[Ehcache 官方 XML 配置手册](http://www.ehcache.org/documentation/3.8/xml.html) + +### xml 配置方式 + +Ehcache 的`CacheManager`构造函数或工厂方法被调用时,会默认加载 classpath 下名为*ehcache.xml*的配置文件。如果加载失败,会加载 Ehcache jar 包中的*ehcache-failsafe.xml*文件,这个文件中含有简单的默认配置。 +**ehcache.xml 配置参数说明:** + +- **name**:缓存名称。 +- **maxElementsInMemory**:缓存最大个数。 +- **eternal**:缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期。 +- **timeToIdleSeconds**:置对象在失效前的允许闲置时间(单位:秒)。仅当 eternal=false 对象不是永久有效时使用,可选属性,默认值是 0,也就是可闲置时间无穷大。 +- **timeToLiveSeconds**:缓存数据的生存时间(TTL),也就是一个元素从构建到消亡的最大时间间隔值,这只能在元素不是永久驻留时有效,如果该值是 0 就意味着元素可以停顿无穷长的时间。 +- **maxEntriesLocalDisk**:当内存中对象数量达到 maxElementsInMemory 时,Ehcache 将会对象写到磁盘中。 +- **overflowToDisk**:内存不足时,是否启用磁盘缓存。 +- **diskSpoolBufferSizeMB**:这个参数设置 DiskStore(磁盘缓存)的缓存区大小。默认是 30MB。每个 Cache 都应该有自己的一个缓冲区。 +- **maxElementsOnDisk**:硬盘最大缓存个数。 +- **diskPersistent**:是否在 VM 重启时存储硬盘的缓存数据。默认值是 false。 +- **diskExpiryThreadIntervalSeconds**:磁盘失效线程运行时间间隔,默认是 120 秒。 +- **memoryStoreEvictionPolicy**:当达到 maxElementsInMemory 限制时,Ehcache 将会根据指定的策略去清理内存。默认策略是 LRU(最近最少使用)。你可以设置为 FIFO(先进先出)或是 LFU(较少使用)。 +- **clearOnFlush**:内存数量最大时是否清除。 + +### API 配置方式 + +xml 配置的参数也可以直接通过编程方式来动态的进行配置(dynamicConfig 没有设为 false)。 + +```java +Cache cache = manager.getCache("sampleCache"); +CacheConfiguration config = cache.getCacheConfiguration(); +config.setTimeToIdleSeconds(60); +config.setTimeToLiveSeconds(120); +config.setmaxEntriesLocalHeap(10000); +config.setmaxEntriesLocalDisk(1000000); +``` + +也可以通过`disableDynamicFeatures()`方式关闭动态配置开关。配置以后你将无法再以编程方式配置参数。 + +```java +Cache cache = manager.getCache("sampleCache"); +cache.disableDynamicFeatures(); +``` + +## 五、Spring 集成 Ehcache + +Spring3.1 开始添加了对缓存的支持。和事务功能的支持方式类似,缓存抽象允许底层使用不同的缓存解决方案来进行整合。 + +Spring4.1 开始支持 JSR-107 注解。 + +> **注:我本人使用的 Spring 版本为 4.1.4.RELEASE,目前 Spring 版本仅支持 Ehcache2.5 以上版本,但不支持 Ehcache3。** + +### 绑定 Ehcache + +`org.springframework.cache.ehcache.EhCacheManagerFactoryBean`这个类的作用是加载 Ehcache 配置文件。 +`org.springframework.cache.ehcache.EhCacheCacheManager`这个类的作用是支持 net.sf.ehcache.CacheManager。 + +*spring-ehcache.xml*的配置 + +```xml + + + + ehcache缓存配置管理文件 + + + + + + + + + + + + +``` + +### 使用 Spring 的缓存注解 + +#### 开启注解 + +Spring 为缓存功能提供了注解功能,但是你必须启动注解。 +你有两个选择: +(1) 在 xml 中声明 +像上一节 spring-ehcache.xml 中的做法一样,使用`` + +```xml + +``` + +(2) 使用标记注解 +你也可以通过对一个类进行注解修饰的方式在这个类中使用缓存注解。 +范例如下: + +```java +@Configuration +@EnableCaching +public class AppConfig { +} +``` + +### 注解基本使用方法 + +Spring 对缓存的支持类似于对事务的支持。 +首先使用注解标记方法,相当于定义了切点,然后使用 Aop 技术在这个方法的调用前、调用后获取方法的入参和返回值,进而实现了缓存的逻辑。 +下面三个注解都是方法级别: + +#### @Cacheable + +表明所修饰的方法是可以缓存的:当第一次调用这个方法时,它的结果会被缓存下来,在缓存的有效时间内,以后访问这个方法都直接返回缓存结果,不再执行方法中的代码段。 +这个注解可以用`condition`属性来设置条件,如果不满足条件,就不使用缓存能力,直接执行方法。 +可以使用`key`属性来指定 key 的生成规则。 + +#### @CachePut + +与`@Cacheable`不同,`@CachePut`不仅会缓存方法的结果,还会执行方法的代码段。 +它支持的属性和用法都与`@Cacheable`一致。 + +#### @CacheEvict + +与`@Cacheable`功能相反,`@CacheEvict`表明所修饰的方法是用来删除失效或无用的缓存数据。 +下面是`@Cacheable`、`@CacheEvict`和`@CachePut`基本使用方法的一个集中展示: + +```java +@Service +public class UserService { + // @Cacheable可以设置多个缓存,形式如:@Cacheable({"books", "isbns"}) + @Cacheable(value={"users"}, key="#user.id") + public User findUser(User user) { + return findUserInDB(user.getId()); + } + + @Cacheable(value = "users", condition = "#user.getId() <= 2") + public User findUserInLimit(User user) { + return findUserInDB(user.getId()); + } + + @CachePut(value = "users", key = "#user.getId()") + public void updateUser(User user) { + updateUserInDB(user); + } + + @CacheEvict(value = "users") + public void removeUser(User user) { + removeUserInDB(user.getId()); + } + + @CacheEvict(value = "users", allEntries = true) + public void clear() { + removeAllInDB(); + } +} +``` + +#### @Caching + +如果需要使用同一个缓存注解(`@Cacheable`、`@CacheEvict`或`@CachePut`)多次修饰一个方法,就需要用到`@Caching`。 + +```java +@Caching(evict = { @CacheEvict("primary"), @CacheEvict(cacheNames="secondary", key="#p0") }) +public Book importBooks(String deposit, Date date) +``` + +#### @CacheConfig + +与前面的缓存注解不同,这是一个类级别的注解。 +如果类的所有操作都是缓存操作,你可以使用`@CacheConfig`来指定类,省去一些配置。 + +```java +@CacheConfig("books") +public class BookRepositoryImpl implements BookRepository { + @Cacheable + public Book findBook(ISBN isbn) {...} +} +``` + +## 参考资料 + +- **官方** + - [Ehcache 官网](http://www.ehcache.org/) + - [Ehcache Github](https://github.com/ehcache/ehcache3) +- **文章** + - [Ehcache 优缺点以及分布式详解](https://yq.aliyun.com/articles/72885?utm_campaign=wenzhang&utm_medium=article&utm_source=QQ-qun&2017331&utm_content=m_15513) + - [Ehcache 详细解读](http://raychase.iteye.com/blog/1545906) + - [注释驱动的 Spring cache 缓存介绍](http://www.ibm.com/developerworks/cn/opensource/os-cn-spring-cache/) + - [Spring 官方文档第 36 章缓存抽象](http://docs.spring.io/spring/docs/current/spring-framework-reference/htmlsingle/) \ No newline at end of file diff --git "a/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/05.Java\350\277\233\347\250\213\345\206\205\347\274\223\345\255\230.md" "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/05.Java\350\277\233\347\250\213\345\206\205\347\274\223\345\255\230.md" new file mode 100644 index 00000000..53562aa9 --- /dev/null +++ "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/05.Java\350\277\233\347\250\213\345\206\205\347\274\223\345\255\230.md" @@ -0,0 +1,184 @@ +--- +title: Java 进程内缓存 +date: 2022-02-17 22:34:30 +order: 05 +categories: + - Java + - 中间件 + - 缓存 +tags: + - Java + - 中间件 + - 缓存 +permalink: /pages/59f078/ +--- + +# Java 进程内缓存 + +> 关键词:ConcurrentHashMap、LRUHashMap、Guava Cache、Caffeine、Ehcache + +## 一、ConcurrentHashMap + +最简单的进程内缓存可以通过 JDK 自带的 `HashMap` 或 `ConcurrentHashMap` 实现。 + +适用场景:**不需要淘汰的缓存数据**。 + +缺点:无法进行缓存淘汰,内存会无限制的增长。 + +## 二、LRUHashMap + +可以通过**继承 `LinkedHashMap` 来实现一个简单的 `LRUHashMap`**,即可完成一个简单的 **LRU (最近最少使用)**算法。 + +缺点: + +- 锁竞争严重,性能比较低。 +- 不支持过期时间 +- 不支持自动刷新 + +【示例】LRUHashMap 的简单实现 + +```java +class LRUCache extends LinkedHashMap { + + private final int max; + private Object lock; + + public LRUCache(int max) { + //无需扩容 + super((int) (max * 1.4f), 0.75f, true); + this.max = max; + this.lock = new Object(); + } + + /** + * 重写LinkedHashMap的removeEldestEntry方法即可 在Put的时候判断,如果为true,就会删除最老的 + * + * @param eldest + * @return + */ + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > max; + } + + public Object getValue(Object key) { + synchronized (lock) { + return get(key); + } + } + + public void putValue(Object key, Object value) { + synchronized (lock) { + put(key, value); + } + } + + public boolean removeValue(Object key) { + synchronized (lock) { + return remove(key) != null; + } + } + + public boolean removeAll() { + clear(); + return true; + } + +} +``` + +## 三、Guava Cache + +Guava Cache 解决了 `LRUHashMap` 中的几个缺点。 + +Guava Cache 提供了**基于容量,时间和引用的缓存回收方式**。基于容量的方式内部实现采用 LRU 算法,基于引用回收很好的利用了 Java 虚拟机的垃圾回收机制。 + +其中的缓存构造器 CacheBuilder 采用构建者模式提供了设置好各种参数的缓存对象。缓存核心类 LocalCache 里面的内部类 Segment 与 jdk1.7 及以前的 `ConcurrentHashMap` 非常相似,分段加锁,减少锁竞争,并且都继承于 `ReetrantLock`,还有六个队列,以实现丰富的本地缓存方案。Guava Cache 对于过期的 Entry 并没有马上过期(也就是并没有后台线程一直在扫),而是通过进行读写操作的时候进行过期处理,这样做的好处是避免后台线程扫描的时候进行全局加锁。 + +直接通过查询,判断其是否满足刷新条件,进行刷新。 + +### Guava Cache 缓存回收 + +Guava Cache 提供了三种基本的缓存回收方式。 + +### 基于容量回收 + +`maximumSize(long)`:当缓存中的元素数量超过指定值时触发回收。 + +### 基于定时回收 + +- `expireAfterAccess(long, TimeUnit)`:缓存项在给定时间内没有被读/写访问,则回收。请注意这种缓存的回收顺序和基于大小回收一样。 +- `expireAfterWrite(long, TimeUnit)`:缓存项在给定时间内没有被写访问(创建或覆盖),则回收。如果认为缓存数据总是在固定时候后变得陈旧不可用,这种回收方式是可取的。 + +如下文所讨论,定时回收周期性地在写操作中执行,偶尔在读操作中执行。 + +### 基于引用回收 + +- `CacheBuilder.weakKeys()`:使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。 +- `CacheBuilder.weakValues()`:使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。 +- `CacheBuilder.softValues()`:使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。 + +### Guava Cache 核心 API + +#### CacheBuilder + +缓存构建器。构建缓存的入口,指定缓存配置参数并初始化本地缓存。 +主要采用 builder 的模式,CacheBuilder 的每一个方法都返回这个 CacheBuilder 知道 build 方法的调用。 +注意 build 方法有重载,带有参数的为构建一个具有数据加载功能的缓存,不带参数的构建一个没有数据加载功能的缓存。 + +#### LocalManualCache + +作为 LocalCache 的一个内部类,在构造方法里面会把 LocalCache 类型的变量传入,并且调用方法时都直接或者间接调用 LocalCache 里面的方法。 + +#### LocalLoadingCache + +可以看到该类继承了 LocalManualCache 并实现接口 LoadingCache。 +覆盖了 get,getUnchecked 等方法。 + +#### LocalCache + +Guava Cache 中的核心类,重点了解。 + +LocalCache 的数据结构与 ConcurrentHashMap 很相似,都由多个 segment 组成,且各 segment 相对独立,互不影响,所以能支持并行操作。每个 segment 由一个 table 和若干队列组成。缓存数据存储在 table 中,其类型为 AtomicReferenceArray。 + +## 四、Caffeine + +> [caffeine](https://github.com/ben-manes/caffeine) 是一个使用 JDK8 改进 Guava 缓存的高性能缓存库。 + +Caffeine 实现了 W-TinyLFU(**LFU** + **LRU** 算法的变种),其**命中率和读写吞吐量大大优于 Guava Cache**。 + +其实现原理较复杂,可以参考[你应该知道的缓存进化史](https://juejin.im/post/5b7593496fb9a009b62904fa#comment)。 + +## 五、Ehcache + +> 参考:[Ehcache](04.Ehcache.md) + +## 六、进程内缓存对比 + +常用进程内缓存技术对比: + +| 比较项 | ConcurrentHashMap | LRUMap | Ehcache | Guava Cache | Caffeine | +| ------------ | ----------------- | ------------------------ | ----------------------------- | ----------------------------------- | ----------------------- | +| 读写性能 | 很好,分段锁 | 一般,全局加锁 | 好 | 好,需要做淘汰操作 | 很好 | +| 淘汰算法 | 无 | LRU,一般 | 支持多种淘汰算法,LRU,LFU,FIFO | LRU,一般 | W-TinyLFU, 很好 | +| 功能丰富程度 | 功能比较简单 | 功能比较单一 | 功能很丰富 | 功能很丰富,支持刷新和虚引用等 | 功能和 Guava Cache 类似 | +| 工具大小 | jdk 自带类,很小 | 基于 LinkedHashMap,较小 | 很大,最新版本 1.4MB | 是 Guava 工具类中的一个小部分,较小 | 一般,最新版本 644KB | +| 是否持久化 | 否 | 否 | 是 | 否 | 否 | +| 是否支持集群 | 否 | 否 | 是 | 否 | 否 | + +- **`ConcurrentHashMap`** - 比较适合缓存比较固定不变的元素,且缓存的数量较小的。虽然从上面表格中比起来有点逊色,但是其由于是 JDK 自带的类,在各种框架中依然有大量的使用,比如我们可以用来缓存我们反射的 Method,Field 等等;也可以缓存一些链接,防止其重复建立。在 Caffeine 中也是使用的 `ConcurrentHashMap` 来存储元素。 +- **`LRUMap`** - 如果不想引入第三方包,又想使用淘汰算法淘汰数据,可以使用这个。 +- **`Ehcache`** - 由于其 jar 包很大,较重量级。对于需要持久化和集群的一些功能的,可以选择 Ehcache。需要注意的是,虽然 Ehcache 也支持分布式缓存,但是由于其节点间通信方式为 rmi,表现不如 Redis,所以一般不建议用它来作为分布式缓存。 +- **`Guava Cache`** - Guava 这个 jar 包在很多 Java 应用程序中都有大量的引入,所以很多时候其实是直接用就好了,并且其本身是轻量级的而且功能较为丰富,在不了解 Caffeine 的情况下可以选择 Guava Cache。 +- **`Caffeine`** - 其在命中率,读写性能上都比 Guava Cache 好很多,并且其 API 和 Guava cache 基本一致,甚至会多一点。在真实环境中使用 Caffeine,取得过不错的效果。 + +总结一下:**如果不需要淘汰算法则选择 `ConcurrentHashMap`,如果需要淘汰算法和一些丰富的 API,推荐选择 `Caffeine`**。 + +## 参考资料 + +- [caffeine github](https://github.com/ben-manes/caffeine) +- [深入解密来自未来的缓存-Caffeine](https://juejin.im/post/5b8df63c6fb9a019e04ebaf4) +- [Caffeine 缓存](https://www.jianshu.com/p/9a80c662dac4) +- [Google Guava 官方教程(中文版)](https://wizardforcel.gitbooks.io/guava-tutorial/content/1.html) +- [Google Guava Cache 全解析](https://www.jianshu.com/p/38bd5f1cf2f2) +- [注释驱动的 Spring cache 缓存介绍](https://developer.ibm.com/zh/articles/os-cn-spring-cache/) \ No newline at end of file diff --git "a/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/06.Http\347\274\223\345\255\230.md" "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/06.Http\347\274\223\345\255\230.md" new file mode 100644 index 00000000..b9c26d34 --- /dev/null +++ "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/06.Http\347\274\223\345\255\230.md" @@ -0,0 +1,77 @@ +--- +title: Http 缓存 +date: 2022-02-17 22:34:30 +order: 06 +categories: + - Java + - 中间件 + - 缓存 +tags: + - 缓存 + - Http +permalink: /pages/30abaa/ +--- + +# Http 缓存 + +HTTP 缓存分为 2 种,一种是强缓存,另一种是协商缓存。主要作用是可以加快资源获取速度,提升用户体验,减少网络传输,缓解服务端的压力。 + +## Http 强缓存 + +不需要发送请求到服务端,直接读取浏览器本地缓存,在 Chrome 的 Network 中显示的 HTTP 状态码是 200 ,在 Chrome 中,强缓存又分为 Disk Cache (存放在硬盘中)和 Memory Cache (存放在内存中),存放的位置是由浏览器控制的。是否强缓存由 `Expires`、`Cache-Control` 和 `Pragma` 3 个 Header 属性共同来控制。 + +### Expires + +`Expires` 的值是一个 HTTP 日期,在浏览器发起请求时,会根据系统时间和 Expires 的值进行比较,如果系统时间超过了 Expires 的值,缓存失效。由于和系统时间进行比较,所以当系统时间和服务器时间不一致的时候,会有缓存有效期不准的问题。Expires 的优先级在三个 Header 属性中是最低的。 + +### Cache-Control + +`Cache-Control` 是 HTTP/1.1 中新增的属性,在请求头和响应头中都可以使用,常用的属性值如有: + +- `max-age`:单位是秒,缓存时间计算的方式是距离发起的时间的秒数,超过间隔的秒数缓存失效 +- `no-cache`:不使用强缓存,需要与服务器验证缓存是否新鲜 +- `no-store`:禁止使用缓存(包括协商缓存),每次都向服务器请求最新的资源 +- `private`:专用于个人的缓存,中间代理、CDN 等不能缓存此响应 +- `public`:响应可以被中间代理、CDN 等缓存 +- `must-revalidate`:在缓存过期前可以使用,过期后必须向服务器验证 + +### Pragma + +`Pragma` 只有一个属性值,就是 no-cache ,效果和 Cache-Control 中的 no-cache 一致,不使用强缓存,需要与服务器验证缓存是否新鲜,在 3 个头部属性中的优先级最高。 + +## 协商缓存 + +当浏览器的强缓存失效的时候或者请求头中设置了不走强缓存,并且在请求头中设置了 If-Modified-Since 或者 If-None-Match 的时候,会将这两个属性值到服务端去验证是否命中协商缓存,如果命中了协商缓存,会返回 304 状态,加载浏览器缓存,并且响应头会设置 Last-Modified 或者 ETag 属性。 + +### ETag/If-None-Match + +Etag: 服务器响应请求时,通过此字段告诉浏览器当前资源在服务器生成的唯一标识(生成规则由服务器决定) + +If-None-Match: 再次请求服务器时,浏览器的请求报文头部会包含此字段,后面的值为在缓存中获取的标识。服务器接收到次报文后发现 If-None-Match 则与被请求资源的唯一标识进行对比。 + +1. 不同,说明资源被改动过,则响应整个资源内容,返回状态码 200。 +2. 相同,说明资源无心修改,则响应 header,浏览器直接从缓存中获取数据信息。返回状态码 304. + +但是实际应用中由于 Etag 的计算是使用算法来得出的,而算法会占用服务端计算的资源,所有服务端的资源都是宝贵的,所以就很少使用 Etag 了。 + +### Last-Modified/If-Modified-Since + +Last-Modified: 服务器在响应请求时,会告诉浏览器资源的最后修改时间。 + +if-Modified-Since: 浏览器再次请求服务器的时候,请求头会包含此字段,后面跟着在缓存中获得的最后修改时间。服务端收到此请求头发现有 if-Modified-Since,则与被请求资源的最后修改时间进行对比,如果一致则返回 304 和响应报文头,浏览器只需要从缓存中获取信息即可。 从字面上看,就是说:从某个时间节点算起,是否文件被修改了 + +1. 如果真的被修改:那么开始传输响应一个整体,服务器返回:200 OK +2. 如果没有被修改:那么只需传输响应 header,服务器返回:304 Not Modified + +if-Unmodified-Since: 从字面上看, 就是说: 从某个时间点算起, 是否文件没有被修改 + +1. 如果没有被修改:则开始`继续'传送文件: 服务器返回: 200 OK +2. 如果文件被修改:则不传输,服务器返回: 412 Precondition failed (预处理错误) + +这两个的区别是一个是修改了才下载一个是没修改才下载。 Last-Modified 说好却也不是特别好,因为如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为 Last-Modified 时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源)。为了解决这个问题,HTTP1.1 推出了 Etag。 + +## 参考资料 + +- [图解 HTTP 缓存](https://juejin.im/post/5eb7f811f265da7bbc7cc5bd) +- [HTTP----HTTP 缓存机制](https://juejin.im/post/5a1d4e546fb9a0450f21af23) +- [缓存详解](https://juejin.im/post/5a6c87c46fb9a01ca560b4d7) \ No newline at end of file diff --git "a/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/README.md" "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/README.md" new file mode 100644 index 00000000..b5487f64 --- /dev/null +++ "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/README.md" @@ -0,0 +1,48 @@ +--- +title: Java 缓存 +date: 2022-02-17 22:34:30 +categories: + - Java + - 中间件 + - 缓存 +tags: + - Java + - 中间件 + - 缓存 +permalink: /pages/c4efe9/ +hidden: true +index: false +--- + +# Java 缓存 + +> 缓存可以说是优化系统性能的第一手段,在各种技术中都会有缓存的应用。 +> +> 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://dunwu.github.io/design/distributed/分布式缓存.html),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200710163555.png) + +## 📖 内容 + +- [Java 缓存框架](02.Java缓存中间件.md) +- [Ehcache 快速入门](04.Ehcache.md) +- [Java 缓存库](05.Java进程内缓存.md) +- [Http 缓存](06.Http缓存.md) + +## 📚 资料 + +- [JSR107](https://www.jcp.org/en/jsr/detail?id=107) +- [Spring Cache 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/integration.html#cache) +- [Spring Boot Cache 特性官方文档](https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/html/spring-boot-features.html#boot-features-caching) +- [J2Cache Gitee](https://gitee.com/ld/J2Cache) +- [JetCache Github](https://github.com/alibaba/jetcache) +- [JetCache wiki](https://github.com/alibaba/jetcache/wiki/Home_CN) +- [Memcached 官网](https://memcached.org/) +- [Memcached Github](https://github.com/memcached/memcached/) +- [Redis 官网](https://redis.io/) +- [Redis github](https://github.com/antirez/redis) +- [Redis 官方文档中文版](http://redis.cn/) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/03.\346\265\201\351\207\217\346\216\247\345\210\266/01.Hystrix.md" "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/03.\346\265\201\351\207\217\346\216\247\345\210\266/01.Hystrix.md" new file mode 100644 index 00000000..5e347d9a --- /dev/null +++ "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/03.\346\265\201\351\207\217\346\216\247\345\210\266/01.Hystrix.md" @@ -0,0 +1,517 @@ +--- +title: Hystrix 快速入门 +date: 2022-02-17 22:34:30 +order: 01 +categories: + - Java + - 中间件 + - 流量控制 +tags: + - Java + - 中间件 + - 流量控制 + - Hystrix +permalink: /pages/364124/ +--- + +# Hystrix 快速入门 + +## Hystrix 简介 + +### Hystrix 是什么 + +Hystrix 是由 Netflix 开源,用于处理分布式系统的延迟和容错的一个开源组件。在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等。Hystrix 采用**断路器模式**来实现服务间的彼此隔离,从而避免级联故障,以提高分布式系统整体的弹性。 + +“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),**向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常**,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。 + +Hystrix 官方已宣布**不再发布新版本**。但是,Hystrix 的断路器设计理念,有非常高的学习价值。 + +### 为什么需要 Hystrix + +复杂的分布式系统架构中的应用程序往往具有数十个依赖项,每个依赖项都会不可避免地在某个时刻失败。 如果主机应用程序未与这些外部故障隔离开来,则可能会被波及。 + +例如,对于依赖于 30 个服务的应用程序,假设每个服务的正常运行时间为 99.99%,则可以期望: + +> 99.9930 = 99.7% 的正常运行时间 +> +> 10 亿个请求中的 0.3%= 3,000,000 个失败 +> +> 即使所有依赖项都具有出色的正常运行时间,每月也会有 2 个小时以上的停机时间。 +> +> 然而,现实情况一般比这种估量情况更糟糕。 + +--- + +当一切正常时,整体系统如下所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717141615.png) + +在高并发场景,这些依赖的稳定性与否对系统的影响非常大,但是依赖有很多不可控问题:如网络连接、资源繁忙、服务宕机等。例如:下图中有一个 QPS 为 50 的依赖 I 出现不可用,但是其他依赖服务是可用的。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717141749.png) + +但是,在高并发场景下,当依赖 I 阻塞时,大多数服务器的线程池就出现阻塞(BLOCK)。当这种级联故障愈演愈烈,就可能造成整个线上服务不可用的雪崩效应,如下图: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717141859.png) + +Hystrix 就是为了解决这类问题而应运而生。 + +### Hystrix 的功能 + +Hystrix 具有以下功能: + +- 避免资源耗尽:阻止任何一个依赖服务耗尽所有的资源,比如 tomcat 中的所有线程资源。 +- 避免请求排队和积压:采用限流和 `fail fast` 来控制故障。 +- 支持降级:提供 fallback 降级机制来应对故障。 +- 资源隔离:比如 `bulkhead`(舱壁隔离技术)、`swimlane`(泳道技术)、`circuit breaker`(断路技术)来限制任何一个依赖服务的故障的影响。 +- 统计/监控/报警:通过近实时的统计/监控/报警功能,来提高故障发现的速度。 +- 通过近实时的属性和配置**热修改**功能,来提高故障处理和恢复的速度。 +- 保护依赖服务调用的所有故障情况,而不仅仅只是网络故障情况。 + +如果使用 Hystrix 对每个基础依赖服务进行过载保护,则整个系统架构将会类似下图所示,每个依赖项彼此隔离,受到延迟时发生饱和的资源的被限制访问,并包含 fallback 逻辑(用于降级处理),该逻辑决定了在依赖项中发生任何类型的故障时做出对应的处理。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717142842.png) + +## Hystrix 原理 + +如下图所示,Hystrix 的工作流程大致可以分为 9 个步骤。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717143247.png) + +### (一)构建一个 HystrixCommand 或 HystrixObservableCommand 对象 + +Hystrix 进行资源隔离,其实是提供了一个抽象,叫做命令模式。这也是 Hystrix 最基本的资源隔离技术。 + +在使用 Hystrix 的过程中,会对依赖服务的调用请求封装成命令对象,Hystrix 对 命令对象抽象了两个抽象类:`HystrixCommand` 和 `HystrixObservableCommand` 。 + +- `HystrixCommand` 表示的命令对象会返回一个唯一返回值。 +- `HystrixObservableCommand` 表示的命令对象 会返回多个返回值。 + +```java +HystrixCommand command = new HystrixCommand(arg1, arg2); +HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2); +``` + +### (二)执行命令 + +Hystrix 中共有 4 种方式执行命令,如下所示: + +| 执行方式 | 说明 | 可用对象 | +| :------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------ | :------------------------- | +| [`execute()`]() | 阻塞式同步执行,返回依赖服务的单一返回结果(或者抛出异常) | `HystrixCommand` | +| [`queue()`]() | 异步执行,通过 `Future` 返回依赖服务的单一返回结果(或者抛出异常) | `HystrixCommand` | +| [`observe()`]() | 基于 Rxjava 的 Observable 方式,返回通过 Observable 表示的依赖服务返回结果。代调用代码先执行(Hot Obserable) | `HystrixObservableCommand` | +| [`toObservable()`]() | 基于 Rxjava 的 Observable 方式,返回通过 Observable 表示的依赖服务返回结果。执行代码等到真正订阅的时候才会执行(cold observable) | `HystrixObservableCommand` | + +这四种命令中,`exeucte()`、`queue()`、`observe()` 的表示其实是通过 `toObservable()` 实现的,其转换关系如下图所示: + +![img](https:////upload-images.jianshu.io/upload_images/14126519-60964d9fa41614c1.png?imageMogr2/auto-orient/strip|imageView2/2/w/563/format/webp) + +`HystrixCommand` 执行方式 + +```java +K value = command.execute(); +// 等价语句: +K value = command.execute().queue().get(); + + +Future fValue = command.queue(); +//等价语句: +Future fValue = command.toObservable().toBlocking().toFuture(); + + +Observable ohValue = command.observe(); //hot observable,立刻订阅,命令立刻执行 +//等价语句: +Observable ohValue = command.toObservable().subscribe(subject); + +// 上述执行最终实现还是基于 toObservable() +Observable ocValue = command.toObservable(); //cold observable,延后订阅,订阅发生后,执行才真正执行 +``` + +### (三)是否缓存 + +如果当前命令对象启用了请求缓存,并且请求的响应存在于缓存中,则缓存的响应会立刻以 `Observable` 的形式返回。 + +### (四)是否开启断路器 + +如果第三步没有缓存没有命中,则判断一下当前断路器的断路状态是否打开。如果断路器状态为打开状态,则 Hystrix 将不会执行此 Command 命令,直接执行步骤 8 调用 Fallback; + +如果断路器状态是关闭,则执行步骤 5 检查是否有足够的资源运行 Command 命令 + +### (五)信号量、线程池是否拒绝 + +当您执行该命令时,Hystrix 会检查断路器以查看电路是否打开。 + +如果电路开路(或“跳闸”),则 Hystrix 将不会执行该命令,而是将流程路由到 (8) 获取回退。 + +如果电路闭合,则流程前进至 (5) 以检查是否有可用容量来运行命令。 + +如果当前要执行的 Command 命令 先关连的线程池 和队列(或者信号量)资源已经满了,Hystrix 将不会运行 Command 命令,直接执行 **步骤 8**的 Fallback 降级处理;如果未满,表示有剩余的资源执行 Command 命令,则执行**步骤 6** + +### (六)construct() 或 run() + +当经过**步骤 5** 判断,有足够的资源执行 Command 命令时,本步骤将调用 Command 命令运行方法,基于不同类型的 Command,有如下两种两种运行方式: + +| 运行方式 | 说明 | +| :------------------------------------- | :--------------------------------------------------------------------- | +| `HystrixCommand.run()` | 返回一个处理结果或者抛出一个异常 | +| `HystrixObservableCommand.construct()` | 返回一个 Observable 表示的结果(可能多个),或者 基于`onError`的错误通知 | + +如果`run()` 或者`construct()`方法 的`真实执行时间`超过了 Command 设置的`超时时间阈值`, 则**当前则执行线程**(或者是独立的定时器线程)将会抛出`TimeoutException`。抛出超时异常 TimeoutException,后,将执行**步骤 8**的 Fallback 降级处理。即使`run()`或者`construct()`执行没有被取消或中断,最终能够处理返回结果,但在降级处理逻辑中,将会抛弃`run()`或`construct()`方法的返回结果,而返回 Fallback 降级处理结果。 + +> **注意事项** +> 需要注意的是,Hystrix 无法强制 将正在运行的线程停止掉--Hystrix 能够做的最好的方式就是在 JVM 中抛出一个`InterruptedException`。如果 Hystrix 包装的工作不抛出中断异常`InterruptedException`, 则在 Hystrix 线程池中的线程将会继续执行,尽管`调用的客户端`已经接收到了`TimeoutException`。这种方式会使 Hystrix 的线程池处于饱和状态。大部分的 Java Http Client 开源库并不会解析 `InterruptedException`。所以确认 HTTP client 相关的连接和读/写相关的超时时间设置。 +> 如果 Command 命令没有抛出任何异常,并且有返回结果,则 Hystrix 将会在做完日志记录和统计之后会将结果返回。 如果是通过`run()`方式运行,则返回一个`Obserable`对象,包含一个唯一值,并且发送一个`onCompleted`通知;如果是通过`consturct()`方式运行 ,则返回一个`Observable对象`。 + +### (七)健康检查 + +Hystrix 会统计 Command 命令执行执行过程中的**成功数**、**失败数**、**拒绝数**和**超时数**,将这些信息记录到**断路器(Circuit Breaker)**中。断路器将上述统计按照**时间窗**的形式记录到一个定长数组中。断路器根据时间窗内的统计数据去判定请求什么时候可以被熔断,熔断后,在接下来一段恢复周期内,相同的请求过来后会直接被熔断。当再次校验,如果健康监测通过后,熔断开关将会被关闭。 + +### (八)获取 Fallback + +当以下场景出现后,Hystrix 将会尝试触发 `Fallback`: + +> - 步骤 6 Command 执行时抛出了任何异常; +> - 步骤 4 断路器已经被打开 +> - 步骤 5 执行命令的线程池、队列或者信号量资源已满 +> - 命令执行的时间超过阈值 + +### (九)返回结果 + +如果 Hystrix 命令对象执行成功,将会返回结果,或者以`Observable`形式包装的结果。根据**步骤 2**的 command 调用方式,返回的`Observable` 会按照如下图说是的转换关系进行返回: + +![img](https:////upload-images.jianshu.io/upload_images/14126519-8790f97df332d9a2.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) + +- `execute()` — 用和 `.queue()` 相同的方式获取 `Future`,然后调用 `Future` 的 `get()` 以获取 `Observable` 的单个值。 +- `queue()` —将 `Observable` 转换为 `BlockingObservable`,以便可以将其转换为 `Future` 并返回。 +- `watch()` —订阅 `Observable` 并开始执行命令的流程; 返回一个 `Observable`,当订阅该 `Observable` 时,它会重新通知。 +- `toObservable()` —返回不变的 `Observable`; 必须订阅它才能真正开始执行命令的流程。 + +## 断路器工作原理 + +![img](https:////upload-images.jianshu.io/upload_images/14126519-dce007513bf90794.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) + +1. 断路器时间窗内的请求数 是否超过了**请求数断路器生效阈值**`circuitBreaker.requestVolumeThreshold`,如果超过了阈值,则将会触发断路,断路状态为**开启** + 例如,如果当前阈值设置的是`20`,则当时间窗内统计的请求数共计 19 个,即使 19 个全部失败了,都不会触发断路器。 +2. 并且请求错误率超过了**请求错误率阈值**`errorThresholdPercentage` +3. 如果两个都满足,则将断路器由**关闭**迁移到**开启** +4. 如果断路器开启,则后续的所有相同请求将会被断路掉; +5. 直到过了**沉睡时间窗**`sleepWindowInMilliseconds`后,再发起请求时,允许其通过(此时的状态为**半开起状态**)。如果请求失败了,则保持断路器状态为**开启**状态,并更新**沉睡时间窗**。如果请求成功了,则将断路器状态改为**关闭**状态; + +核心的逻辑如下: + +```java + @Override + public void onNext(HealthCounts hc) { + // check if we are past the statisticalWindowVolumeThreshold + if (hc.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) { + // we are not past the minimum volume threshold for the stat window, + // so no change to circuit status. + // if it was CLOSED, it stays CLOSED + // if it was half-open, we need to wait for a successful command execution + // if it was open, we need to wait for sleep window to elapse + } else { + if (hc.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) { + //we are not past the minimum error threshold for the stat window, + // so no change to circuit status. + // if it was CLOSED, it stays CLOSED + // if it was half-open, we need to wait for a successful command execution + // if it was open, we need to wait for sleep window to elapse + } else { + // our failure rate is too high, we need to set the state to OPEN + if (status.compareAndSet(Status.CLOSED, Status.OPEN)) { + circuitOpened.set(System.currentTimeMillis()); + } + } + } + } +``` + +### 系统指标 + +Hystrix 对系统指标的统计是基于时间窗模式的: + +> **时间窗**:最近的一个时间区间内,比如前一小时到现在,那么时间窗的长度就是`1小时`; +> **桶**:桶是在特定的**时间窗**内,等分的指标收集的统计集合;比如时间窗的长度为`1小时`,而桶的数量为`10`,那么每个桶在时间轴上依次排开,时间由远及近,每个桶统计的时间分片为 `1h / 10 = 6 min` 6 分钟。一个桶中,包含了`成功数`、`失败数`、`超时数`、`拒绝数` 四个指标。 + +在系统内,时间窗会随着系统的运行逐渐向前移动,而时间窗的长度和桶的数量是固定不变的,那么随着时间的移动,会出现较久的过期的桶被移除出去,新的桶被添加进来,如下图所示: + +![img](https:////upload-images.jianshu.io/upload_images/14126519-11710915e1a5dcda.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) + +## 资源隔离技术 + +### 线程池隔离 + +如下图所示,由于计算机系统的基本执行单位就是线程,线程具备独立的执行能力,所以,为了做到资源保护,需要对系统的线程池进行划分,对于外部调用方 + +``` +User Request +``` + +的请求,调用各个线程池的服务,各个线程池独立完成调用,然后将结果返回 + +``` +调用方 +``` + +。在调用服务的过程中,如果 + +``` +服务提供方 +``` + +执行时间过长,则 + +``` +调用方 +``` + +可以直接以超时的方式直接返回,快速失败。 + +![img](https:////upload-images.jianshu.io/upload_images/14126519-55a0be64ecac4cda.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) + +线程池隔离的几点好处 + +> 1. 使用超时返回的机制,避免同步调用服务时,调用时间过长,无法释放,导致资源耗尽的情况 +> 2. 服务方可以控制请求数量,请求过多,可以直接拒绝,达到快速失败的目的; +> 3. 请求排队,线程池可以维护执行队列,将请求压到队列中处理 + +举个例子,如下代码段,模拟了同步调用服务的过程: + +```java + //服务提供方,执行服务的时候模拟2分钟的耗时 + Callable callableService = ()->{ + long start = System.currentTimeMillis(); + while(System.currentTimeMillis()-start> 1000 * 60 *2){ + //模拟服务执行时间过长的情况 + } + return "OK"; + }; + + //模拟10个客户端调用服务 + ExecutorService clients = Executors.newFixedThreadPool(10); + //模拟给10个客户端提交处理请求 + for (int i = 0; i < 20; i++) { + clients.execute(()->{ + //同步调用 + try { + String result = callableService.call(); + System.out.println("当前客户端:"+Thread.currentThread().getName()+"调用服务完成,得到结果:"+result); + } catch (Exception e) { + e.printStackTrace(); + } + }); + } +``` + +在此环节中,客户端 `clients`必须等待服务方返回结果之后,才能接收新的请求。如果用吞吐量来衡量系统的话,会发现系统的处理能力比较低。为了提高相应时间,可以借助线程池的方式,设置超时时间,这样的话,客户端就不需要必须等待服务方返回,如果时间过长,可以提前返回,改造后的代码如下所示: + +```java + //服务提供方,执行服务的时候模拟2分钟的耗时 + Callable callableService = ()->{ + long start = System.currentTimeMillis(); + while(System.currentTimeMillis()-start> 1000 * 60 *2){ + //模拟服务执行时间过长的情况 + } + return "OK"; + }; + + //创建线程池作为服务方 + ExecutorService executorService = Executors.newFixedThreadPool(30); + + + //模拟10个客户端调用服务 + ExecutorService clients = Executors.newFixedThreadPool(10); + for (int i = 0; i < 10; i++) { + clients.execute(()->{ + //同步调用 + //将请求提交给线程池执行,Callable 和 Runnable在某种意义上,也是Command对象 + Future future = executorService.submit(callableService::call); + //在指定的时间内获取结果,如果超时,调用方可以直接返回 + try { + String result = future.get(1000, TimeUnit.SECONDS); + //客户端等待时间之后,快速返回 + System.out.println("当前客户端:"+Thread.currentThread().getName()+"调用服务完成,得到结果:"+result); + }catch (TimeoutException timeoutException){ + System.out.println("服务调用超时,返回处理"); + } catch (InterruptedException e) { + + } catch (ExecutionException e) { + } + }); + } +``` + +如果我们将服务方的线程池设置为: + +```java +ThreadPoolExecutor executorService = new ThreadPoolExecutor(10,1000,TimeUnit.SECONDS, +new ArrayBlockingQueue<>(100), +new ThreadPoolExecutor.DiscardPolicy() // 提交请求过多时,可以丢弃请求,避免死等阻塞的情况。 +) +``` + +**线程池隔离模式的弊端** + +> 线程池隔离模式,会根据服务划分出独立的线程池,系统资源的线程并发数是有限的,当线程数过多,系统话费大量的 CPU 时间来做线程上下文切换的无用操作,反而降低系统性能;如果线程池隔离的过多,会导致真正用于接收用户请求的线程就相应地减少,系统吞吐量反而下降; +> **在实践上,应当对像远程方法调用,网络资源请求这种服务时间不太可控的场景下使用线程池隔离模式处理** +> 如下图所示,是线程池隔离模式的三种场景: + +![img](https:////upload-images.jianshu.io/upload_images/14126519-8e16e7f8072475eb.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) + +### 信号量隔离 + +由于基于线程池隔离的模式占用系统线程池资源,Hystrix 还提供了另外一个隔离技术:基于信号量的隔离。 + +基于信号量的隔离方式非常地简单,其核心就是使用共用变量 + +``` +semaphore +``` + +进行原子操作,控制线程的并发量,当并发量达到一定量级时,服务禁止调用。如下图所示:信号量本身不会消耗多余的线程资源,所以就非常轻量。 + +![img](https:////upload-images.jianshu.io/upload_images/14126519-9af3442e03df941e.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) + +基于信号量隔离的利弊 + +> 利:基于信号量的隔离,利用 JVM 的原子性 CAS 操作,避免了资源锁的竞争,省去了线程池开销,效率非常高; +> 弊:本质上基于信号量的隔离是同步行为,所以无法做到超时熔断,所以服务方自身要控制住执行时间,避免超时。 +> 应用场景:**业务服务上,有并发上限限制时,可以考虑此方式** > `Alibaba Sentinel`开源框架,就是基于信号量的熔断和断路器框架。 + +## Hystrix 应用 + +- **Hystrix 配置无法动态调节生效**。Hystrix 框架本身是使用的[Archaius](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2FNetflix%2Farchaius)框架完成的配置加载和刷新,但是集成自 Spring Cloud 下,无法有效地根据实时监控结果,动态调整熔断和系统参数 +- **线程池和 Command 之间的配置比较复杂**,在 Spring Cloud 在做 feigin-hystrix 集成的时候,还有些 BUG,对 command 的默认配置没有处理好,导致所有 command 占用公共的 command 线程池,没有细粒度控制,还需要做框架适配调整 + +```php +public interface SetterFactory { + + /** + * Returns a hystrix setter appropriate for the given target and method + */ + HystrixCommand.Setter create(Target target, Method method); + + /** + * Default behavior is to derive the group key from {@link Target#name()} and the command key from + * {@link Feign#configKey(Class, Method)}. + */ + final class Default implements SetterFactory { + + @Override + public HystrixCommand.Setter create(Target target, Method method) { + String groupKey = target.name(); + String commandKey = Feign.configKey(target.type(), method); + return HystrixCommand.Setter + .withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey)) + .andCommandKey(HystrixCommandKey.Factory.asKey(commandKey)); + //没有处理好default配置项的加载 + } + } +} +``` + +## Hystrix 配置 + +> 详细配置可以参考 [Hystrix 官方配置手册](https://github.com/Netflix/Hystrix/wiki/Configuration),这里仅介绍比较核心的配置 + +### 执行配置 + +以下配置用于控制 [`HystrixCommand.run()`]() 如何执行。 + +| 配置项 | 说明 | 默认值 | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------- | -------- | +| [`execution.isolation.strategy`](https://github.com/Netflix/Hystrix/wiki/Configuration#execution.isolation.strategy) | 线程隔离(THREAD)或信号量隔离(SEMAPHORE) | THREAD | +| [`execution.isolation.thread.timeoutInMilliseconds`](https://github.com/Netflix/Hystrix/wiki/Configuration#execution.isolation.thread.timeoutInMilliseconds) | 方法执行超时时间 | 1000(ms) | +| [`execution.isolation.semaphore.maxConcurrentRequests`](https://github.com/Netflix/Hystrix/wiki/Configuration#execution.isolation.semaphore.maxConcurrentRequests) | 信号量隔离最大并发数 | 10 | + +### 断路配置 + +以下配置用于控制 [`HystrixCircuitBreaker`](http://netflix.github.io/Hystrix/javadoc/index.html?com/netflix/hystrix/HystrixCircuitBreaker.html) 的断路处理。 + +| 配置项 | 说明 | 默认值 | +| :------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------- | :------- | +| [`circuitBreaker.enabled`](https://github.com/Netflix/Hystrix/wiki/Configuration#circuitBreaker.enabled) | 是否开启断路器 | true | +| [`circuitBreaker.requestVolumeThreshold`](https://github.com/Netflix/Hystrix/wiki/Configuration#circuitBreaker.requestVolumeThreshold) | 断路器启用请求数阈值 | 20 | +| [`circuitBreaker.sleepWindowInMilliseconds`](https://github.com/Netflix/Hystrix/wiki/Configuration#circuitBreaker.sleepWindowInMilliseconds) | 断路器启用后的休眠时间 | 5000(ms) | +| [`circuitBreaker.errorThresholdPercentage`](https://github.com/Netflix/Hystrix/wiki/Configuration#circuitBreaker.errorThresholdPercentage) | 断路器启用失败率阈值 | 50(%) | +| [`circuitBreaker.forceOpen`](https://github.com/Netflix/Hystrix/wiki/Configuration#circuitBreaker.forceOpen) | 是否强制将断路器设置成开启状态 | false | +| [`circuitBreaker.forceClosed`](https://github.com/Netflix/Hystrix/wiki/Configuration#circuitBreaker.forceClosed) | 是否强制将断路器设置成关闭状态 | false | + +### 指标配置 + +以下配置用于从 HystrixCommand 和 HystrixObservableCommand 执行中捕获相关指标。 + +| 配置项 | 说明 | 默认值 | +| :----------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------- | :-------- | +| [`metrics.rollingStats.timeInMilliseconds`](https://github.com/Netflix/Hystrix/wiki/Configuration#metrics.rollingStats.timeInMilliseconds) | 时间窗的长度 | 10000(ms) | +| [`metrics.rollingStats.numBuckets`](https://github.com/Netflix/Hystrix/wiki/Configuration#metrics.rollingStats.numBuckets) | 桶的数量,需要保证`timeInMilliseconds % numBuckets =0` | 10 | +| [`metrics.rollingPercentile.enabled`](https://github.com/Netflix/Hystrix/wiki/Configuration#metrics.rollingPercentile.enabled) | 是否统计运行延迟的占比 | true | +| [`metrics.rollingPercentile.timeInMilliseconds`](https://github.com/Netflix/Hystrix/wiki/Configuration#metrics.rollingPercentile.timeInMilliseconds) | **运行延迟占比**统计的时间窗 | 60000(ms) | +| [`metrics.rollingPercentile.numBuckets`](https://github.com/Netflix/Hystrix/wiki/Configuration#metrics.rollingPercentile.numBuckets) | **运行延迟占比**统计的桶数 | 6 | +| [`metrics.rollingPercentile.bucketSize`](https://github.com/Netflix/Hystrix/wiki/Configuration#metrics.rollingPercentile.bucketSize) | 百分比统计桶的容量,桶内最多保存的运行时间统计 | 100 | +| [`metrics.healthSnapshot.intervalInMilliseconds`](https://github.com/Netflix/Hystrix/wiki/Configuration#metrics.healthSnapshot.intervalInMilliseconds) | 统计快照刷新间隔 | 500 (ms) | + +### 线程池配置 + +以下配置用于控制 Hystrix Command 执行所使用的线程池。 + +| 配置项 | 说明 | 默认值 | +| :------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----- | +| [`coreSize`](https://github.com/Netflix/Hystrix/wiki/Configuration#coreSize) | 线程池核心线程数 | 10 | +| [`maximumSize`](https://github.com/Netflix/Hystrix/wiki/Configuration#maximumSize) | 线程池最大线程数 | 10 | +| [`maxQueueSize`](https://github.com/Netflix/Hystrix/wiki/Configuration#maxQueueSize) | 最大 LinkedBlockingQueue 的大小,-1 表示用 SynchronousQueue | -1 | +| [`queueSizeRejectionThreshold`](https://github.com/Netflix/Hystrix/wiki/Configuration#queueSizeRejectionThreshold) | 队列大小阈值,超过则拒绝 | 5 | +| [`allowMaximumSizeToDivergeFromCoreSize`](https://github.com/Netflix/Hystrix/wiki/Configuration#allowMaximumSizeToDivergeFromCoreSize) | 此属性允许 maximumSize 的配置生效。该值可以等于或大于 coreSize。设置 coreSize 数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个;释放空闲时间超过最大空闲时间的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。这项技术能明显提高对数据库操作的性能。——摘自百度百科 + +## 什么是数据库连接池 + +数据库连接是一种关键的有限的昂贵的资源,这一点在多用户的网页应用程序中体现得尤为突出。 一个数据库连接对象均对应一个物理数据库连接,每次操作都打开一个物理连接,使用完都关闭连接,这样造成系统的 性能低下。 数据库连接池的解决方案是在应用程序启动时建立足够的数据库连接,并讲这些连接组成一个连接池(简单说:在一个“池”里放了好多半成品的数据库联接对象),由应用程序动态地对池中的连接进行申请、使用和释放。对于多于连接池中连接数的并发请求,应该在请求队列中排队等待。并且应用程序可以根据池中连接的使用率,动态增加或减少池中的连接数。 连接池技术尽可能多地重用了消耗内存地资源,大大节省了内存,提高了服务器地服务效率,能够支持更多的客户服务。通过使用连接池,将大大提高程序运行效率,同时,我们可以通过其自身的管理机制来监视数据库连接的数量、使用情况等。 + +## 为什么需要数据库连接池 + +### 不使用数据库连接池 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220921231353.png) + +不使用数据库连接池的**步骤**: + +1. TCP 建立连接的三次握手 +2. MySQL 认证的三次握手 +3. 真正的 SQL 执行 +4. MySQL 的关闭 +5. TCP 的四次握手关闭 + +不使用数据库连接池的特性: + +- **优点**:实现简单 +- **缺点**: + - 网络 IO 较多 + - 数据库的负载较高 + - 响应时间较长及 QPS 较低 + - 应用频繁的创建连接和关闭连接,导致临时对象较多,GC 频繁 + - 在关闭连接后,会出现大量 TIME_WAIT 的 TCP 状态(在 2 个 MSL 之后关闭) + +### 使用数据库连接池 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220921231500.png) + +使用数据库连接池的步骤:只有第一次访问的时候,需要建立连接。 但是之后的访问,均会**复用**之前创建的连接,直接执行 SQL 语句。 + +使用数据库连接池的**优点**: + +- 减少了网络开销 +- 系统的性能会有一个实质的提升 +- 没有了 TIME_WAIT 状态 + +## 数据库连接池如何工作 + +数据库连接池工作的核心在于以下几点: + +1. **创建连接池**:与线程池等池化对象类似,数据库连接池会在进程启动之初,根据配置初始化,并在池中创建了几个连接对象,以便使用时能从连接池中获取。连接池中的连接不能随意创建和关闭,以避免创建、关闭所带来的系统开销。 +2. **使用、管理连接池中**:连接池管理策略是连接池机制的核心,连接池内连接的分配和释放对系统的性能有很大的影响。合理的策略可以保证数据库连接的有效复用,避免频繁的建立、释放连接所带来的系统资源开销。通常,数据库连接池的管理策略如下: + + 1. 当请求数据库连接时,首先查看连接池中是否有空闲连接。 + 2. 如果存在空闲连接,则将连接分配给客户使用。 + 3. 如果没有空闲连接,则查看当前所开的连接数是否已经达到最大连接数。若未达到,就重新创建一个连接,并分配给请求的客户;如果达到,就按设定的最大等待时间进行等待,若超出最大等待时间,则抛出异常给客户。 + 4. 当客户释放数据库连接时,先判断该连接的引用次数是否超过了规定值。如果超过,就从连接池中删除该连接;否则保留为其他客户服务。 + +3. **关闭连接池**:当应用程序退出时,关闭连接池中所有的连接,释放连接池相关的资源,该过程正好与创建相反。 + +## 数据库连接池的核心参数 + +使用数据库连接池,需要为其配置一些参数,以控制其工作。 + +通常,数据库连接池都会包含以下核心参数: + +- **最小连接数**:是连接池一直保持的数据库连接,所以如果应用程序对数据库连接的使用量不大,将会有大量的数据库连接资源被浪费. +- **最大连接数**:是连接池能申请的最大连接数,如果数据库连接请求超过次数,后面的数据库连接请求将被加入到等待队列中,这会影响以后的数据库操作 +- 最大空闲时间 +- 获取连接超时时间 +- 超时重试连接次数 + +## 数据库连接池的问题 + +**并发问题**:为了保证连接管理服务具有最大的通用性,必须考虑多线程环境,即并发问题。 + +**事务处理**:我们知道,事务具有原子性,此时要求对数据库的操作符合“ALL-OR-NOTHING”原则,即对于一组 SQL 语句要么全做,要么全不做。我们知道当 2 个线程共用一个连接 Connection 对象,而且各自都有自己的事务要处理时候,对于连接池是一个很头疼的问题,因为即使 Connection 类提供了相应的事务支持,可是我们仍然不能确定那个数据库操作是对应那个事务的,这是由于我们有2个线程都在进行事务操作而引起的。为此我们可以使用每一个事务独占一个连接来实现,虽然这种方法有点浪费连接池资源但是可以大大降低事务管理的复杂性。 + +**连接池的分配与释放**:连接池的分配与释放,对系统的性能有很大的影响。合理的分配与释放,可以提高连接的复用度,从而降低建立新连接的开销,同时还可以加快用户的访问速度。 对于连接的管理可使用一个 List。即把已经创建的连接都放入 List 中去统一管理。每当用户请求一个连接时,系统检查这个 List 中有没有可以分配的连接。如果有就把那个最合适的连接分配给他;如果没有就抛出一个异常给用户。 + +**连接池的配置与维护**:连接池中到底应该放置多少连接,才能使系统的性能最佳?系统可采取设置最小连接数(minConnection)和最大连接数(maxConnection)等参数来控制连接池中的连接。比方说,最小连接数是系统启动时连接池所创建的连接数。如果创建过多,则系统启动就慢,但创建后系统的响应速度会很快;如果创建过少,则系统启动的很快,响应起来却慢。这样,可以在开发时,设置较小的最小连接数,开发起来会快,而在系统实际使用时设置较大的,因为这样对访问客户来说速度会快些。最大连接数是连接池中允许连接的最大数目,具体设置多少,要看系统的访问量,可通过软件需求上得到。 如何确保连接池中的最小连接数呢?有动态和静态两种策略。动态即每隔一定时间就对连接池进行检测,如果发现连接数量小于最小连接数,则补充相应数量的新连接,以保证连接池的正常运转。静态是发现空闲连接不够时再去检查。 + +## 数据库连接池技术选型 + +常见的数据库连接池: + +- [HikariCP](https://github.com/brettwooldridge/HikariCP):HiKariCP 号称是跑的最快的连接池,并且是 SpringBoot 框架的默认连接池。 +- [Druid](https://github.com/apache/druid):Druid 是阿里巴巴开源的数据库连接池。Druid 内置强大的监控功能,监控特性不影响性能。功能强大,能防 SQL 注入,内置 Loging 能诊断 Hack 应用行为。 +- [DBCP](https://commons.apache.org/proper/commons-dbcp/): 由 Apache 开发的一个 Java 数据库连接池。`commons-dbcp2` 基于 `commons-pool2` 来实现底层的对象池机制。单线程,性能较差,适用于小型系统。官方自 2021 年后没有再更新。 +- [C3P0](https://github.com/swaldman/c3p0):开源的 JDBC 连接池,实现了数据源和 JNDI 绑定,支持 JDBC3 规范和 JDBC2 的标准扩展。单线程,性能较差,适用于小型系统。官方自 2019 年后再没有更新。 +- Tomcat-jdbc:Tomcat 在 7.0 以前使用 DBCP 做为连接池组件,从 7.0 后新增了 Tomcat jdbc pool 模块,基于 Tomcat JULI,使用 Tomcat 日志框架,完全兼容 dbcp,通过异步方式获取连接,支持高并发应用环境,超级简单核心文件只有 8 个,支持 JMX,支持 XA Connection。 + +来自 Druid 的竞品对比(https://github.com/alibaba/druid/wiki/Druid%E8%BF%9E%E6%8E%A5%E6%B1%A0%E4%BB%8B%E7%BB%8D): + +| 功能类别 | 功能 | Druid | HikariCP | DBCP | Tomcat-jdbc | C3P0 | +| ------------------- | --------------- | ------------ | ----------- | ---- | --------------- | ---- | +| 性能 | PSCache | 是 | 否 | 是 | 是 | 是 | +| LRU | 是 | 否 | 是 | 是 | 是 | | +| SLB 负载均衡支持 | 是 | 否 | 否 | 否 | 否 | | +| 稳定性 | ExceptionSorter | 是 | 否 | 否 | 否 | 否 | +| 扩展 | 扩展 | Filter | | | JdbcIntercepter | | +| 监控 | 监控方式 | jmx/log/http | jmx/metrics | jmx | jmx | jmx | +| 支持 SQL 级监控 | 是 | 否 | 否 | 否 | 否 | | +| Spring/Web 关联监控 | 是 | 否 | 否 | 否 | 否 | | +| | 诊断支持 | LogFilter | 否 | 否 | 否 | 否 | +| 连接泄露诊断 | logAbandoned | 否 | 否 | 否 | 否 | | +| 安全 | SQL 防注入 | 是 | 无 | 无 | 无 | 无 | +| 支持配置加密 | 是 | 否 | 否 | 否 | 否 | | + +从数据库连接池最重要的性能角度来看:HikariCP 应该性能最好;Druid 也不错,并且有更多、更久的生产实践,更为可靠;而其他常见的数据库连接池性能远远不如。 + +从功能角度来看:Druid 功能最全面,除基本的数据库连接池能力以外,还支持 sql 级监控、扩展、SQL 防注入以及监控等功能。 + +综合来看:HikariCP 是 Spring Boot 首选数据库连接池,对于 Spring Boot 项目来说,无疑适配性最好。而非 Spring Boot 项目,可以优先考虑 Druid,在国内有大规模应用,中文社区支持良好。 + +## HikariCP + +HiKariCP 号称是跑的最快的连接池,并且是 SpringBoot 框架的默认连接池。 + +HiKariCP 为了提升性能,做了很多细节上的优化,例如: + +- 使用 FastList 替代 ArrayList,通过初始化的默认值,减少了越界检查的操作 +- 优化并精简了字节码,通过使用 Javassist,减少了动态代理的性能损耗,比如使用 invokestatic 指令代替 invokevirtual 指令 +- 实现了无锁的 ConcurrentBag,减少了并发场景下的锁竞争 + +HikariCP 关键配置: + +- `maximum-pool-size`:池中最大连接数(包括空闲和正在使用的连接)。默认值是 10,这个一般预估应用的最大连接数,后期根据监测得到一个最大值的一个平均值。要知道,最大连接并不是越多越好,一个 connection 会占用系统的带宽和存储。但是 当连接池没有空闲连接并且已经到达最大值,新来的连接池请求(HikariPool#getConnection)会被阻塞直到`connectionTimeout`(毫秒),超时后便抛出 SQLException。 +- `minimum-idle`:池中最小空闲连接数量。默认值 10,小于池中最大连接数,一般根据系统大部分情况下的数据库连接情况取一个平均值。Hikari 会尽可能、尽快地将空闲连接数维持在这个数量上。如果为了获得最佳性能和对峰值需求的响应能力,我们也不妨让他和最大连接数保持一致,使得 HikariCP 成为一个固定大小的数据库连接池。 +- `connection-timeout`:连接超时时间。默认值为 30s,可以接收的最小超时时间为 250ms。但是连接池请求也可以自定义超时时间(com.zaxxer.hikari.pool.HikariPool#getConnection(long))。 +- `idle-timeout`:空闲连接存活最大时间,默认 600000(十分钟) +- `max-lifetime`:连接池中连接的最大生命周期。当连接一致处于闲置状态时,超过 8 小时数据库会主动断开连接。为了防止大量的同一时间处于空闲连接因为数据库方的闲置超时策略断开连接(可以理解为连接雪崩),一般将这个值设置的比数据库的“闲置超时时间”小几秒,以便这些连接断开后,HikariCP 能迅速的创建新一轮的连接。 +- `pool-name`:连接池的名字。一般会出现在日志和 JMX 控制台中。默认值:auto-genenrated。建议取一个合适的名字,便于监控。 +- `auto-commit`:是否自动提交池中返回的连接。默认值为 true。一般是有必要自动提交上一个连接中的事物的。如果为 false,那么就需要应用层手动提交事物。 + +参考配置: + +```properties +# 连接池名称 +spring.datasource.hikari.pool-name = SpringTutorialHikariPool +# 最大连接数,小于等于 0 会被重置为默认值 10;大于零小于 1 会被重置为 minimum-idle 的值 +spring.datasource.hikari.maximum-pool-size = 10 +# 最小空闲连接,默认值10,小于 0 或大于 maximum-pool-size,都会重置为 maximum-pool-size +spring.datasource.hikari.minimum-idle = 10 +# 连接超时时间(单位:毫秒),小于 250 毫秒,会被重置为默认值 30 秒 +spring.datasource.hikari.connection-timeout = 60000 +# 空闲连接超时时间,默认值 600000(10分钟),大于等于 max-lifetime 且 max-lifetime>0,会被重置为0;不等于 0 且小于 10 秒,会被重置为 10 秒 +# 只有空闲连接数大于最大连接数且空闲时间超过该值,才会被释放 +spring.datasource.hikari.idle-timeout = 600000 +# 连接最大存活时间,不等于 0 且小于 30 秒,会被重置为默认值 30 分钟。该值应该比数据库所设置的超时时间短 +spring.datasource.hikari.max-lifetime = 1800000 +``` + +## Druid + +Druid 是阿里巴巴开源的数据库连接池。Druid 连接池为监控而生,内置强大的监控功能,监控特性不影响性能。功能强大,能防 SQL 注入,内置 Loging 能诊断 Hack 应用行为。 + +Druid 关键配置: + +```properties +# 数据库访问配置 +# 主数据源,默认的 +spring.datasource.type=com.alibaba.druid.pool.DruidDataSource +spring.datasource.driver-class-name=com.mysql.jdbc.Driver +spring.datasource.url=jdbc:mysql://localhost:3306/druid +spring.datasource.username=root +spring.datasource.password=root + +# 下面为连接池的补充设置,应用到上面所有数据源中 +# 初始化大小,最小,最大 +spring.datasource.initialSize=5 +spring.datasource.minIdle=5 +spring.datasource.maxActive=20 +# 配置获取连接等待超时的时间 +spring.datasource.maxWait=60000 +# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 +spring.datasource.timeBetweenEvictionRunsMillis=60000 +# 配置一个连接在池中最小生存的时间,单位是毫秒 +spring.datasource.minEvictableIdleTimeMillis=300000 +spring.datasource.validationQuery=SELECT 1 FROM DUAL +spring.datasource.testWhileIdle=true +spring.datasource.testOnBorrow=false +spring.datasource.testOnReturn=false +# 打开PSCache,并且指定每个连接上PSCache的大小 +spring.datasource.poolPreparedStatements=true +spring.datasource.maxPoolPreparedStatementPerConnectionSize=20 +# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 +spring.datasource.filters=stat,wall,log4j +# 通过connectProperties属性来打开mergeSql功能;慢SQL记录 +spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 +# 合并多个DruidDataSource的监控数据 +#spring.datasource.useGlobalDataSourceStat=true +``` + +## 参考资料 + +- [数据库连接池学习笔记(一):原理介绍+常用连接池介绍](https://blog.csdn.net/crankz/article/details/82874158) +- [高性能数据库连接池的内幕](https://mp.weixin.qq.com/s?__biz=MzI3MzEzMDI1OQ==&mid=2651814835&idx=1&sn=cb775d3926ce39d12fa420a292c1f83d&scene=0#wechat_redirect) +- [HikariCP](https://github.com/brettwooldridge/HikariCP) +- [druid](https://github.com/apache/druid) \ No newline at end of file diff --git "a/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/README.md" "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/README.md" new file mode 100644 index 00000000..fd3311e0 --- /dev/null +++ "b/docs/01.Java/14.\344\270\255\351\227\264\344\273\266/README.md" @@ -0,0 +1,49 @@ +--- +title: Java 中间件 +date: 2022-04-09 15:11:37 +categories: + - Java + - 中间件 +tags: + - 编程 + - Java + - 中间件 +permalink: /pages/fe6d83/ +hidden: true +index: false +--- + +# Java 中间件 + +## 📖 内容 + +#### 缓存 + +> 缓存可以说是优化系统性能的第一手段,在各种技术中都会有缓存的应用。 +> +> 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://dunwu.github.io/design/distributed/分布式缓存.html),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 + +- [Java 缓存框架](02.缓存/02.Java缓存中间件.md) +- [Ehcache 快速入门](02.缓存/04.Ehcache.md) +- [Java 进程内缓存](02.缓存/05.Java进程内缓存.md) +- [Http 缓存](02.缓存/06.Http缓存.md) + +#### 流量控制 + +- [Hystrix](03.流量控制/01.Hystrix.md) + +## 📚 资料 + +- **Mybatis** + - [Mybatis Github](https://github.com/mybatis/mybatis-3) + - [Mybatis 官网](http://www.mybatis.org/mybatis-3/) + - [MyBatis 官方代码生成(mybatis-generator)](https://github.com/mybatis/generator) + - [MyBatis 官方集成 Spring(mybatis-spring)](https://github.com/mybatis/spring) + - [Mybatis 官方集成 Spring Boot(mybatis-spring-boot)](https://github.com/mybatis/spring-boot-starter) + - [MyBatis-Plus](https://github.com/baomidou/mybatis-plus) - CRUD 扩展插件、代码生成器、分页器等多功能 + - [Mapper](https://github.com/abel533/Mapper) - CRUD 扩展插件 + - [Mybatis-PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) - Mybatis 通用分页插件 + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git a/docs/01.Java/README.md b/docs/01.Java/README.md new file mode 100644 index 00000000..168f4e05 --- /dev/null +++ b/docs/01.Java/README.md @@ -0,0 +1,291 @@ +--- +title: Java 教程 +date: 2022-04-27 18:23:47 +categories: + - Java +tags: + - Java +permalink: /pages/0d2474/ +hidden: true +index: false +--- + +

    + + logo + +

    + +

    + + + star + + + + fork + + + + build + + + + code style + + +

    + +

    JavaTutorial

    + +> ☕ **java-tutorial** 是一个 Java 教程,汇集一个老司机在 Java 领域的十年积累。 +> +> - 🔁 项目同步维护:[Github](https://github.com/dunwu/java-tutorial/) | [Gitee](https://gitee.com/turnon/java-tutorial/) +> - 📖 电子书阅读:[Github Pages](https://dunwu.github.io/java-tutorial/) | [Gitee Pages](https://turnon.gitee.io/java-tutorial/) +> +> 说明: +> +> - 下面的内容清单中,凡是有 📚 标记的技术,都已整理成详细的教程。 +> - 部分技术因为可以应用于不同领域,所以可能会同时出现在不同的类别下。 + +## 📖 内容 + +### JavaEE + +#### JavaWeb + +- [JavaWeb 面经](02.JavaEE/01.JavaWeb/99.JavaWeb面经.md) +- [JavaWeb 之 Servlet 指南](02.JavaEE/01.JavaWeb/01.JavaWeb之Servlet指南.md) +- [JavaWeb 之 Jsp 指南](02.JavaEE/01.JavaWeb/02.JavaWeb之Jsp指南.md) +- [JavaWeb 之 Filter 和 Listener](02.JavaEE/01.JavaWeb/03.JavaWeb之Filter和Listener.md) +- [JavaWeb 之 Cookie 和 Session](02.JavaEE/01.JavaWeb/04.JavaWeb之Cookie和Session.md) + +#### Java 服务器 + +> Tomcat 和 Jetty 都是 Java 比较流行的轻量级服务器。 +> +> Nginx 是目前最流行的反向代理服务器,也常用于负载均衡。 + +- [Tomcat 快速入门](02.JavaEE/02.服务器/01.Tomcat/01.Tomcat快速入门.md) +- [Tomcat 连接器](02.JavaEE/02.服务器/01.Tomcat/02.Tomcat连接器.md) +- [Tomcat 容器](02.JavaEE/02.服务器/01.Tomcat/03.Tomcat容器.md) +- [Tomcat 优化](02.JavaEE/02.服务器/01.Tomcat/04.Tomcat优化.md) +- [Tomcat 和 Jetty](02.JavaEE/02.服务器/01.Tomcat/05.Tomcat和Jetty.md) +- [Jetty](02.JavaEE/02.服务器/02.Jetty.md) + +### Java 软件 + +#### Java 构建 + +> Java 项目需要通过 [**构建工具**](11.软件/01.构建) 来管理项目依赖,完成编译、打包、发布、生成 JavaDoc 等任务。 +> +> - 目前最主流的构建工具是 Maven,它的功能非常强大。 +> - Gradle 号称是要替代 Maven 等构件工具,它的版本管理确实简洁,但是需要学习 Groovy,学习成本比 Maven 高。 +> - Ant 功能比 Maven 和 Gradle 要弱,现代 Java 项目基本不用了,但也有一些传统的 Java 项目还在使用。 + +- [Maven](11.软件/01.构建/01.Maven) 📚 + - [Maven 快速入门](11.软件/01.构建/01.Maven/01.Maven快速入门.md) + - [Maven 教程之 pom.xml 详解](11.软件/01.构建/01.Maven/02.Maven教程之pom.xml详解.md) + - [Maven 教程之 settings.xml 详解](11.软件/01.构建/01.Maven/03.Maven教程之settings.xml详解.md) + - [Maven 实战问题和最佳实践](11.软件/01.构建/01.Maven/04.Maven实战问题和最佳实践.md) + - [Maven 教程之发布 jar 到私服或中央仓库](11.软件/01.构建/01.Maven/05.Maven教程之发布jar到私服或中央仓库.md) + - [Maven 插件之代码检查](11.软件/01.构建/01.Maven/06.Maven插件之代码检查.md) +- [Ant 简易教程](11.软件/01.构建/02.Ant.md) + +#### Java IDE + +> 自从有了 [**IDE**](11.软件/02.IDE),写代码从此就告别了刀耕火种的蛮荒时代。 +> +> - [Eclipse](11.软件/02.IDE/02.Eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 +> - 曾经抗拒从转 [Intellij Idea](11.软件/02.IDE/01.Intellij.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 +> - 你可以在 [vscode](11.软件/02.IDE/03.VsCode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 + +- [Intellij Idea](11.软件/02.IDE/01.Intellij.md) +- [Eclipse](11.软件/02.IDE/02.Eclipse.md) +- [vscode](11.软件/02.IDE/03.VsCode.md) + +#### Java 监控诊断 + +> [监控/诊断](11.软件/03.监控诊断) 工具主要用于 Java 应用的运维。通过采集、分析、存储、可视化应用的有效数据,帮助开发者、使用者快速定位问题,找到性能瓶颈。 + +- [监控工具对比](11.软件/03.监控诊断/01.监控工具对比.md) +- [CAT](11.软件/03.监控诊断/02.CAT.md) +- [Zipkin](11.软件/03.监控诊断/03.Zipkin.md) +- [SkyWalking](11.软件/03.监控诊断/04.Skywalking.md) +- [Arthas](11.软件/03.监控诊断/05.Arthas.md) + +### Java 工具 + +#### Java IO + +- [JSON 序列化](12.工具/01.IO/01.JSON序列化.md) - [fastjson](https://github.com/alibaba/fastjson)、[Jackson](https://github.com/FasterXML/jackson)、[Gson](https://github.com/google/gson) +- [二进制序列化](12.工具/01.IO/02.二进制序列化.md) - [Protobuf](https://developers.google.com/protocol-buffers)、[Thrift](https://thrift.apache.org/)、[Hessian](http://hessian.caucho.com/)、[Kryo](https://github.com/EsotericSoftware/kryo)、[FST](https://github.com/RuedigerMoeller/fast-serialization) + +#### JavaBean 工具 + +- [Lombok](12.工具/02.JavaBean/01.Lombok.md) +- [Dozer](12.工具/02.JavaBean/02.Dozer.md) + +#### Java 模板引擎 + +- [Freemark](12.工具/03.模板引擎/01.Freemark.md) +- [Velocity](12.工具/03.模板引擎/02.Thymeleaf.md) +- [Thymeleaf](12.工具/03.模板引擎/03.Velocity.md) + +#### Java 测试工具 + +- [Junit](12.工具/04.测试/01.Junit.md) +- [Mockito](12.工具/04.测试/02.Mockito.md) +- [Jmeter](12.工具/04.测试/03.Jmeter.md) +- [JMH](12.工具/04.测试/04.JMH.md) + +#### 其他 + +- [Java 日志](12.工具/99.其他/01.Java日志.md) +- [Java 工具包](12.工具/99.其他/02.Java工具包.md) +- [Reflections](12.工具/99.其他/03.Reflections.md) +- [JavaMail](12.工具/99.其他/04.JavaMail.md) +- [Jsoup](12.工具/99.其他/05.Jsoup.md) +- [Thumbnailator](12.工具/99.其他/06.Thumbnailator.md) +- [Zxing](12.工具/99.其他/07.Zxing.md) + +### Java 框架 + +#### Spring + +##### 综合 + +- [Spring 概述](13.框架/01.Spring/00.Spring综合/01.Spring概述.md) +- [SpringBoot 知识图谱](13.框架/01.Spring/00.Spring综合/21.SpringBoot知识图谱.md) +- [SpringBoot 基本原理](13.框架/01.Spring/00.Spring综合/22.SpringBoot基本原理.md) +- [Spring 面试](13.框架/01.Spring/00.Spring综合/99.Spring面试.md) + +##### 核心 + +- [Spring Bean](13.框架/01.Spring/01.Spring核心/01.SpringBean.md) +- [Spring IoC](13.框架/01.Spring/01.Spring核心/02.SpringIoC.md) +- [Spring 依赖查找](13.框架/01.Spring/01.Spring核心/03.Spring依赖查找.md) +- [Spring 依赖注入](13.框架/01.Spring/01.Spring核心/04.Spring依赖注入.md) +- [Spring IoC 依赖来源](13.框架/01.Spring/01.Spring核心/05.SpringIoC依赖来源.md) +- [Spring Bean 作用域](13.框架/01.Spring/01.Spring核心/06.SpringBean作用域.md) +- [Spring Bean 生命周期](13.框架/01.Spring/01.Spring核心/07.SpringBean生命周期.md) +- [Spring 配置元数据](13.框架/01.Spring/01.Spring核心/08.Spring配置元数据.md) +- [Spring AOP](13.框架/01.Spring/01.Spring核心/10.SpringAop.md) +- [Spring 资源管理](13.框架/01.Spring/01.Spring核心/20.Spring资源管理.md) +- [Spring 校验](13.框架/01.Spring/01.Spring核心/21.Spring校验.md) +- [Spring 数据绑定](13.框架/01.Spring/01.Spring核心/22.Spring数据绑定.md) +- [Spring 类型转换](13.框架/01.Spring/01.Spring核心/23.Spring类型转换.md) +- [Spring EL 表达式](13.框架/01.Spring/01.Spring核心/24.SpringEL.md) +- [Spring 事件](13.框架/01.Spring/01.Spring核心/25.Spring事件.md) +- [Spring 国际化](13.框架/01.Spring/01.Spring核心/26.Spring国际化.md) +- [Spring 泛型处理](13.框架/01.Spring/01.Spring核心/27.Spring泛型处理.md) +- [Spring 注解](13.框架/01.Spring/01.Spring核心/28.Spring注解.md) +- [Spring Environment 抽象](13.框架/01.Spring/01.Spring核心/29.SpringEnvironment抽象.md) +- [SpringBoot 教程之快速入门](13.框架/01.Spring/01.Spring核心/31.SpringBoot之快速入门.md) +- [SpringBoot 之属性加载](13.框架/01.Spring/01.Spring核心/32.SpringBoot之属性加载.md) +- [SpringBoot 之 Profile](13.框架/01.Spring/01.Spring核心/33.SpringBoot之Profile.md) + +##### 数据 + +- [Spring 之数据源](13.框架/01.Spring/02.Spring数据/01.Spring之数据源.md) +- [Spring 之 JDBC](13.框架/01.Spring/02.Spring数据/02.Spring之JDBC.md) +- [Spring 之事务](13.框架/01.Spring/02.Spring数据/03.Spring之事务.md) +- [Spring 之 JPA](13.框架/01.Spring/02.Spring数据/04.Spring之JPA.md) +- [Spring 集成 Mybatis](13.框架/01.Spring/02.Spring数据/10.Spring集成Mybatis.md) +- [Spring 访问 Redis](13.框架/01.Spring/02.Spring数据/21.Spring访问Redis.md) +- [Spring 访问 MongoDB](13.框架/01.Spring/02.Spring数据/22.Spring访问MongoDB.md) +- [Spring 访问 Elasticsearch](13.框架/01.Spring/02.Spring数据/23.Spring访问Elasticsearch.md) + +##### Web + +- [Spring WebMvc](13.框架/01.Spring/03.SpringWeb/01.SpringWebMvc.md) +- [SpringBoot 之应用 EasyUI](13.框架/01.Spring/03.SpringWeb/21.SpringBoot之应用EasyUI.md) + +##### IO + +- [SpringBoot 之异步请求](13.框架/01.Spring/04.SpringIO/01.SpringBoot之异步请求.md) +- [SpringBoot 之 Json](13.框架/01.Spring/04.SpringIO/02.SpringBoot之Json.md) +- [SpringBoot 之邮件](13.框架/01.Spring/04.SpringIO/03.SpringBoot之邮件.md) + +##### 集成 + +- [Spring 集成缓存中间件](13.框架/01.Spring/05.Spring集成/01.Spring集成缓存.md) +- [Spring 集成定时任务中间件](13.框架/01.Spring/05.Spring集成/02.Spring集成调度器.md) +- [Spring 集成 Dubbo](13.框架/01.Spring/05.Spring集成/03.Spring集成Dubbo.md) + +##### 其他 + +- [Spring4 升级](13.框架/01.Spring/99.Spring其他/01.Spring4升级.md) +- [SpringBoot 之 banner](13.框架/01.Spring/99.Spring其他/21.SpringBoot之banner.md) +- [SpringBoot 之 Actuator](13.框架/01.Spring/99.Spring其他/22.SpringBoot之Actuator.md) + +#### ORM + +- [Mybatis 快速入门](13.框架/11.ORM/01.Mybatis快速入门.md) +- [Mybatis 原理](13.框架/11.ORM/02.Mybatis原理.md) + +#### 安全 + +> Java 领域比较流行的安全框架就是 shiro 和 spring-security。 +> +> shiro 更为简单、轻便,容易理解,能满足大多数基本安全场景下的需要。 +> +> spring-security 功能更丰富,也比 shiro 更复杂。值得一提的是由于 spring-security 是 spring 团队开发,所以集成 spring 和 spring-boot 框架更容易。 + +- [Shiro](13.框架/12.安全/01.Shiro.md) +- [SpringSecurity](13.框架/12.安全/02.SpringSecurity.md) + +#### IO + +- [Shiro](13.框架/13.IO/01.Netty.md) + +### Java 中间件 + +#### 缓存 + +> 缓存可以说是优化系统性能的第一手段,在各种技术中都会有缓存的应用。 +> +> 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://dunwu.github.io/design/distributed/分布式缓存.html),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 + +- [Java 缓存中间件](14.中间件/02.缓存/02.Java缓存中间件.md) +- [Ehcache 快速入门](14.中间件/02.缓存/04.Ehcache.md) +- [Java 进程内缓存](14.中间件/02.缓存/05.Java进程内缓存.md) +- [Http 缓存](14.中间件/02.缓存/06.Http缓存.md) + +#### 流量控制 + +- [Hystrix](14.中间件/03.流量控制/01.Hystrix.md) + +### [大数据](https://dunwu.github.io/bigdata-tutorial) + +> 大数据技术点以归档在:[bigdata-tutorial](https://dunwu.github.io/bigdata-tutorial) + +- [Hdfs](https://dunwu.github.io/bigdata-tutorial/hdfs) 📚 +- [Hbase](https://dunwu.github.io/bigdata-tutorial/hbase) 📚 +- [Hive](https://dunwu.github.io/bigdata-tutorial/hive) 📚 +- [MapReduce](https://dunwu.github.io/bigdata-tutorial/mapreduce) +- [Yarn](https://dunwu.github.io/bigdata-tutorial/yarn) +- [ZooKeeper](https://dunwu.github.io/bigdata-tutorial/zookeeper) 📚 +- [Kafka](https://dunwu.github.io/bigdata-tutorial/kafka) 📚 +- Spark +- Storm +- [Flink](https://dunwu.github.io/bigdata-tutorial/tree/master/docs/flink) + +## 📚 资料 + +- Java 经典书籍 + - [《Effective Java 中文版》](https://item.jd.com/12507084.html) - 本书介绍了在 Java 编程中 78 条极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。同推荐《重构 : 改善既有代码的设计》、《代码整洁之道》、《代码大全》,有一定的内容重叠。 + - [《Java 并发编程实战》](https://item.jd.com/10922250.html) - 本书深入浅出地介绍了 Java 线程和并发,是一本完美的 Java 并发参考手册。 + - [《深入理解 Java 虚拟机》](https://item.jd.com/11252778.html) - 不去了解 JVM 的工程师,和咸鱼有什么区 + - [《Maven 实战》](https://item.jd.com/10476794.html) - 国内最权威的 Maven 专家的力作,唯一一本哦! +- 其他领域书籍 + - [《Redis 设计与实现》](https://item.jd.com/11486101.html) - 系统而全面地描述了 Redis 内部运行机制。图示丰富,描述清晰,并给出大量参考信息,是 NoSQL 数据库开发人员案头必备。 + - [《鸟哥的 Linux 私房菜 (基础学习篇)》](https://item.jd.com/12443890.html) - 本书是最具知名度的 Linux 入门书《鸟哥的 Linux 私房菜基础学习篇》的最新版,全面而详细地介绍了 Linux 操作系统。内容非常全面,建议挑选和自己实际工作相关度较高的,其他部分有需要再阅读。 + - [《Head First 设计模式》](https://item.jd.com/10100236.html) - 《Head First 设计模式》(中文版)共有 14 章,每章都介绍了几个设计模式,完整地涵盖了四人组版本全部 23 个设计模式。 + - [《HTTP 权威指南》](https://item.jd.com/11056556.html) - 本书尝试着将 HTTP 中一些互相关联且常被误解的规则梳理清楚,并编写了一系列基于各种主题的章节,对 HTTP 各方面的特性进行了介绍。纵观全书,对 HTTP“为什么”这样做进行了详细的解释,而不仅仅停留在它是“怎么做”的。 + - [《TCP/IP 详解 系列》](https://item.jd.com/11966296.html) - 完整而详细的 TCP/IP 协议指南。针对任何希望理解 TCP/IP 协议是如何实现的读者设计。 + - [《剑指 Offer:名企面试官精讲典型编程题》](https://item.jd.com/12163054.html) - 剖析了 80 个典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这 5 个面试要点。 + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git a/docs/@pages/archivesPage.md b/docs/@pages/archivesPage.md new file mode 100644 index 00000000..4e2d4eda --- /dev/null +++ b/docs/@pages/archivesPage.md @@ -0,0 +1,6 @@ +--- +archivesPage: true +title: 归档 +permalink: /archives/ +article: false +--- diff --git a/docs/@pages/categoriesPage.md b/docs/@pages/categoriesPage.md new file mode 100644 index 00000000..e69ff980 --- /dev/null +++ b/docs/@pages/categoriesPage.md @@ -0,0 +1,6 @@ +--- +categoriesPage: true +title: 分类 +permalink: /categories/ +article: false +--- \ No newline at end of file diff --git a/docs/@pages/tagsPage.md b/docs/@pages/tagsPage.md new file mode 100644 index 00000000..cf1bde53 --- /dev/null +++ b/docs/@pages/tagsPage.md @@ -0,0 +1,6 @@ +--- +tagsPage: true +title: 标签 +permalink: /tags/ +article: false +--- \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index d73c10ce..f525fbba 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,96 +1,211 @@ --- home: true -heroImage: http://dunwu.test.upcdn.net/common/logo/dunwu-logo.png +heroImage: img/bg.gif heroText: JAVA-TUTORIAL tagline: ☕ java-tutorial 是一个 Java 教程,汇集一个老司机在 Java 领域的十年积累。 -actionLink: / +bannerBg: none +postList: none footer: CC-BY-SA-4.0 Licensed | Copyright © 2018-Now Dunwu --- +

    + + + star + + + + fork + + + + build + + + + code style + + +

    + > ☕ **java-tutorial** 是一个 Java 教程,汇集一个老司机在 Java 领域的十年积累。 > > - 🔁 项目同步维护:[Github](https://github.com/dunwu/java-tutorial/) | [Gitee](https://gitee.com/turnon/java-tutorial/) > - 📖 电子书阅读:[Github Pages](https://dunwu.github.io/java-tutorial/) | [Gitee Pages](https://turnon.gitee.io/java-tutorial/) +> +> 说明: +> +> - 下面的内容清单中,凡是有 📚 标记的技术,都已整理成详细的教程。 +> - 部分技术因为可以应用于不同领域,所以可能会同时出现在不同的类别下。 -## javacore - -> 📚 [javacore](https://dunwu.github.io/javacore/) 是一个 Java 核心技术教程。内容包含:Java 基础特性、Java 高级特性、Java 并发、JVM、Java IO 等。 - -## javaee +## 📖 内容 -> [☕ JavaEE](javaee/README.md) 技术是 Java Web 的基石 +### JavaSE -- [JavaEE 面经](javaee/javaee-interview.md) -- [JavaEE 之 Servlet 指南](javaee/javaee-servlet.md) -- [JavaEE 之 Jsp 指南](javaee/javaee-jsp.md) -- [JavaEE 之 Filter 和 Listener](javaee/javaee-filter-listener.md) -- [JavaEE 之 Cookie 和 Session](javaee/javaee-cookie-sesion.md) +> 📚 [javacore](https://dunwu.github.io/javacore/) 是一个 Java 核心技术教程。内容包含:Java 基础特性、Java 高级特性、Java 并发、JVM、Java IO 等。 -## javatech +### JavaEE -> 📚 [javatech](https://dunwu.github.io/javatech/) 是一个 Java 应用技术教程。内容包含 Java 开发中常见应用技术,如:框架、缓存、消息队列、存储、安全、微服务、测试、服务器等。 +#### JavaWeb -## spring-tutorial +- [JavaWeb 面经](01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb面经.md) +- [JavaWeb 之 Servlet 指南](01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb之Servlet指南.md) +- [JavaWeb 之 Jsp 指南](01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb之Jsp指南.md) +- [JavaWeb 之 Filter 和 Listener](01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb之Filter和Listener.md) +- [JavaWeb 之 Cookie 和 Session](01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb之Cookie和Session.md) -> 📚 [spring-tutorial](https://dunwu.github.io/spring-tutorial/) 是一个 Spring 实战教程。 +#### Java 服务器 -## spring-boot-tutorial +> Tomcat 和 Jetty 都是 Java 比较流行的轻量级服务器。 +> +> Nginx 是目前最流行的反向代理服务器,也常用于负载均衡。 -> 📚 [Spring Boot 教程](https://dunwu.github.io/spring-boot-tutorial/) 是一个 Spring Boot 实战教程。 +- [Tomcat 快速入门](01.Java/02.JavaEE/02.服务器/01.Tomcat/01.Tomcat快速入门.md) +- [Tomcat 连接器](01.Java/02.JavaEE/02.服务器/01.Tomcat/02.Tomcat连接器.md) +- [Tomcat 容器](01.Java/02.JavaEE/02.服务器/01.Tomcat/03.Tomcat容器.md) +- [Tomcat 优化](01.Java/02.JavaEE/02.服务器/01.Tomcat/04.Tomcat优化.md) +- [Tomcat 和 Jetty](01.Java/02.JavaEE/02.服务器/01.Tomcat/05.Tomcat和Jetty.md) +- [Jetty](01.Java/02.JavaEE/02.服务器/02.Jetty.md) -## javatool +### Java 软件 -### 构建 +#### Java 构建 -> Java 项目需要通过 [**构建工具**](javatool/build) 来管理项目依赖,完成编译、打包、发布、生成 JavaDoc 等任务。 +> Java 项目需要通过 [**构建工具**](01.Java/11.软件/01.构建) 来管理项目依赖,完成编译、打包、发布、生成 JavaDoc 等任务。 > > - 目前最主流的构建工具是 Maven,它的功能非常强大。 > - Gradle 号称是要替代 Maven 等构件工具,它的版本管理确实简洁,但是需要学习 Groovy,学习成本比 Maven 高。 > - Ant 功能比 Maven 和 Gradle 要弱,现代 Java 项目基本不用了,但也有一些传统的 Java 项目还在使用。 -- [Maven](javatool/build/maven) 📚 - - [Maven 入门指南](javatool/build/maven/maven-quickstart.md) - - [Maven 教程之 pom.xml 详解](javatool/build/maven/maven-pom.md) - - [Maven 教程之 settings.xml 详解](javatool/build/maven/maven-settings.md) - - [Maven 实战问题和最佳实践](javatool/build/maven/maven-action.md) - - [Maven 教程之发布 jar 到私服或中央仓库](javatool/build/maven/maven-deploy.md) - - [Maven 插件之代码检查](javatool/build/maven/maven-checkstyle-plugin.md) -- [Ant 简易教程](javatool/build/ant.md) +- [Maven](01.Java/11.软件/01.构建/01.Maven) 📚 + - [Maven 快速入门](01.Java/11.软件/01.构建/01.Maven/01.Maven快速入门.md) + - [Maven 教程之 pom.xml 详解](01.Java/11.软件/01.构建/01.Maven/02.Maven教程之pom.xml详解.md) + - [Maven 教程之 settings.xml 详解](01.Java/11.软件/01.构建/01.Maven/03.Maven教程之settings.xml详解.md) + - [Maven 实战问题和最佳实践](01.Java/11.软件/01.构建/01.Maven/04.Maven实战问题和最佳实践.md) + - [Maven 教程之发布 jar 到私服或中央仓库](01.Java/11.软件/01.构建/01.Maven/05.Maven教程之发布jar到私服或中央仓库.md) + - [Maven 插件之代码检查](01.Java/11.软件/01.构建/01.Maven/06.Maven插件之代码检查.md) +- [Ant 简易教程](01.Java/11.软件/01.构建/02.Ant.md) -### IDE +#### Java IDE -> 自动有了 [**IDE**](javatool/ide),写代码从此就告别了刀耕火种的蛮荒时代。 +> 自从有了 [**IDE**](01.Java/11.软件/02.IDE),写代码从此就告别了刀耕火种的蛮荒时代。 > -> - [Eclipse](javatool/ide/eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 -> - 曾经抗拒从转 [Intellij Idea](javatool/ide/intellij-idea.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 -> - 你可以在 [vscode](javatool/ide/vscode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 +> - [Eclipse](01.Java/11.软件/02.IDE/02.Eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 +> - 曾经抗拒从转 [Intellij Idea](01.Java/11.软件/02.IDE/01.Intellij.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 +> - 你可以在 [vscode](01.Java/11.软件/02.IDE/03.VsCode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 -- [Intellij Idea](javatool/ide/intellij-idea.md) -- [Eclipse](javatool/ide/eclipse.md) -- [vscode](javatool/ide/vscode.md) +- [Intellij Idea](01.Java/11.软件/02.IDE/01.Intellij.md) +- [Eclipse](01.Java/11.软件/02.IDE/02.Eclipse.md) +- [vscode](01.Java/11.软件/02.IDE/03.VsCode.md) -### 监控/诊断 +#### Java 监控诊断 -> [监控/诊断](javatool/monitor) 工具主要用于 Java 应用的运维。通过采集、分析、存储、可视化应用的有效数据,帮助开发者、使用者快速定位问题,找到性能瓶颈。 +> [监控/诊断](01.Java/11.软件/03.监控诊断) 工具主要用于 Java 应用的运维。通过采集、分析、存储、可视化应用的有效数据,帮助开发者、使用者快速定位问题,找到性能瓶颈。 -- [监控工具对比](javatool/monitor/monitor-summary.md) -- [CAT](javatool/monitor/cat.md) -- [Zipkin](javatool/monitor/zipkin.md) -- [SkyWalking](javatool/monitor/skywalking.md) -- [Arthas](javatool/monitor/arthas.md) +- [监控工具对比](01.Java/11.软件/03.监控诊断/01.监控工具对比.md) +- [CAT](01.Java/11.软件/03.监控诊断/02.CAT.md) +- [Zipkin](01.Java/11.软件/03.监控诊断/03.Zipkin.md) +- [SkyWalking](01.Java/11.软件/03.监控诊断/04.Skywalking.md) +- [Arthas](01.Java/11.软件/03.监控诊断/05.Arthas.md) ---- +### Java 工具 -## 其他技术栈 +#### Java IO -- [db-tutorial](https://dunwu.github.io/db-tutorial/) - 是对数据库领域开发经验的总结。内容包含:关系型数据库和 Nosql 理论、Mysql、Redis 等。 -- [algorithm-tutorial](https://dunwu.github.io/algorithm-tutorial/) - 是对数据结构和算法的总结。内容包含:一些基本的数据结构、算法。 -- [linux-tutorial](https://github.com/dunwu/linux-tutorial) - 是对 Linux 操作系统的经验总结。内容包含:Linux 常用命令;各种常见软件的 Linux 环境安装配置;运维、部署脚本;Shell、Python 语法教程;Git、Docker 教程。 -- [frontend-tutorial](https://github.com/dunwu/frontend-tutorial) - 前端教程 +- [JSON 序列化](01.Java/12.工具/01.IO/01.JSON序列化.md) - [fastjson](https://github.com/alibaba/fastjson)、[Jackson](https://github.com/FasterXML/jackson)、[Gson](https://github.com/google/gson) +- [二进制序列化](01.Java/12.工具/01.IO/02.二进制序列化.md) - [Protobuf](https://developers.google.com/protocol-buffers)、[Thrift](https://thrift.apache.org/)、[Hessian](http://hessian.caucho.com/)、[Kryo](https://github.com/EsotericSoftware/kryo)、[FST](https://github.com/RuedigerMoeller/fast-serialization) ---- +#### JavaBean 工具 + +- [Lombok](01.Java/12.工具/02.JavaBean/01.Lombok.md) +- [Dozer](01.Java/12.工具/02.JavaBean/02.Dozer.md) + +#### Java 模板引擎 + +- [Freemark](01.Java/12.工具/03.模板引擎/01.Freemark.md) +- [Velocity](01.Java/12.工具/03.模板引擎/02.Thymeleaf.md) +- [Thymeleaf](01.Java/12.工具/03.模板引擎/03.Velocity.md) + +#### Java 测试工具 + +- [Junit](01.Java/12.工具/04.测试/01.Junit.md) +- [Mockito](01.Java/12.工具/04.测试/02.Mockito.md) +- [Jmeter](01.Java/12.工具/04.测试/03.Jmeter.md) +- [JMH](01.Java/12.工具/04.测试/04.JMH.md) + +#### 其他 + +- [Java 日志](01.Java/12.工具/99.其他/01.Java日志.md) +- [Java 工具包](01.Java/12.工具/99.其他/02.Java工具包.md) +- [Reflections](01.Java/12.工具/99.其他/03.Reflections.md) +- [JavaMail](01.Java/12.工具/99.其他/04.JavaMail.md) +- [Jsoup](01.Java/12.工具/99.其他/05.Jsoup.md) +- [Thumbnailator](01.Java/12.工具/99.其他/06.Thumbnailator.md) +- [Zxing](01.Java/12.工具/99.其他/07.Zxing.md) + +### Java 框架 + +#### ORM + +- [Mybatis 快速入门](01.Java/13.框架/11.ORM/01.Mybatis快速入门.md) +- [Mybatis 原理](01.Java/13.框架/11.ORM/02.Mybatis原理.md) -## 学习资源 +#### Spring + +📚 [spring-tutorial](https://dunwu.github.io/spring-tutorial/) 是一个 Spring 实战教程。 + +#### Spring Boot + +📚 [Spring Boot 教程](https://dunwu.github.io/spring-boot-tutorial/) 是一个 Spring Boot 实战教程。 + +#### 安全 + +> Java 领域比较流行的安全框架就是 shiro 和 spring-security。 +> +> shiro 更为简单、轻便,容易理解,能满足大多数基本安全场景下的需要。 +> +> spring-security 功能更丰富,也比 shiro 更复杂。值得一提的是由于 spring-security 是 spring 团队开发,所以集成 spring 和 spring-boot 框架更容易。 + +- [Shiro](01.Java/13.框架/12.安全/01.Shiro.md) +- [SpringSecurity](01.Java/13.框架/12.安全/02.SpringSecurity.md) + +#### IO + +- [Shiro](01.Java/13.框架/13.IO/01.Netty.md) + +### Java 中间件 + +#### 缓存 + +> 缓存可以说是优化系统性能的第一手段,在各种技术中都会有缓存的应用。 +> +> 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://dunwu.github.io/design/distributed/分布式缓存.html),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 + +- [Java 缓存中间件](01.Java/14.中间件/02.缓存/02.Java缓存中间件.md) +- [Ehcache 快速入门](01.Java/14.中间件/02.缓存/04.Ehcache.md) +- [Java 进程内缓存](01.Java/14.中间件/02.缓存/05.Java进程内缓存.md) +- [Http 缓存](01.Java/14.中间件/02.缓存/06.Http缓存.md) + +#### 流量控制 + +- [Hystrix](01.Java/14.中间件/03.流量控制/01.Hystrix.md) + +### [大数据](https://dunwu.github.io/bigdata-tutorial) + +> 大数据技术点以归档在:[bigdata-tutorial](https://dunwu.github.io/bigdata-tutorial) + +- [Hdfs](https://dunwu.github.io/bigdata-tutorial/hdfs) 📚 +- [Hbase](https://dunwu.github.io/bigdata-tutorial/hbase) 📚 +- [Hive](https://dunwu.github.io/bigdata-tutorial/hive) 📚 +- [MapReduce](https://dunwu.github.io/bigdata-tutorial/mapreduce) +- [Yarn](https://dunwu.github.io/bigdata-tutorial/yarn) +- [ZooKeeper](https://dunwu.github.io/bigdata-tutorial/zookeeper) 📚 +- [Kafka](https://dunwu.github.io/bigdata-tutorial/kafka) 📚 +- Spark +- Storm +- [Flink](https://dunwu.github.io/bigdata-tutorial/tree/master/docs/flink) + +## 📚 资料 - Java 经典书籍 - [《Effective Java 中文版》](https://item.jd.com/12507084.html) - 本书介绍了在 Java 编程中 78 条极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。同推荐《重构 : 改善既有代码的设计》、《代码整洁之道》、《代码大全》,有一定的内容重叠。 @@ -104,3 +219,18 @@ footer: CC-BY-SA-4.0 Licensed | Copyright © 2018-Now Dunwu - [《HTTP 权威指南》](https://item.jd.com/11056556.html) - 本书尝试着将 HTTP 中一些互相关联且常被误解的规则梳理清楚,并编写了一系列基于各种主题的章节,对 HTTP 各方面的特性进行了介绍。纵观全书,对 HTTP“为什么”这样做进行了详细的解释,而不仅仅停留在它是“怎么做”的。 - [《TCP/IP 详解 系列》](https://item.jd.com/11966296.html) - 完整而详细的 TCP/IP 协议指南。针对任何希望理解 TCP/IP 协议是如何实现的读者设计。 - [《剑指 Offer:名企面试官精讲典型编程题》](https://item.jd.com/12163054.html) - 剖析了 80 个典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这 5 个面试要点。 + +## 🚪 传送 + +◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ + +> 你可能会感兴趣: + +- [Java 教程](https://github.com/dunwu/java-tutorial) 📚 +- [JavaCore 教程](https://dunwu.github.io/javacore/) 📚 +- [Spring 教程](https://dunwu.github.io/spring-tutorial/) 📚 +- [Spring Boot 教程](https://dunwu.github.io/spring-boot-tutorial/) 📚 +- [数据库教程](https://dunwu.github.io/db-tutorial/) 📚 +- [数据结构和算法教程](https://dunwu.github.io/algorithm-tutorial/) 📚 +- [Linux 教程](https://dunwu.github.io/linux-tutorial/) 📚 +- [Nginx 教程](https://github.com/dunwu/nginx-tutorial/) 📚 \ No newline at end of file diff --git a/docs/javaee/README.md b/docs/javaee/README.md deleted file mode 100644 index 147b5328..00000000 --- a/docs/javaee/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# ☕ JavaEE - -## 知识点 - -- [JavaEE 面经](javaee-interview.md) -- [JavaEE 之 Servlet 指南](javaee-servlet.md) -- [JavaEE 之 Jsp 指南](javaee-jsp.md) -- [JavaEE 之 Filter 和 Listener](javaee-filter-listener.md) -- [JavaEE 之 Cookie 和 Session](javaee-cookie-sesion.md) - -## 学习资料 - -- **书籍** - - [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) - - [Head First Servlets & JSP](https://book.douban.com/subject/1942934/) -- **教程** - - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) - - [Servlet 教程](https://www.runoob.com/servlet/servlet-tutorial.html) - - [博客园孤傲苍狼 JavaWeb 学习总结](https://www.cnblogs.com/xdp-gacl/tag/JavaWeb%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/) - - [JSP 教程](https://www.runoob.com/jsp/jsp-tutorial.html) diff --git a/docs/javatool/README.md b/docs/javatool/README.md deleted file mode 100644 index 75472181..00000000 --- a/docs/javatool/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Java 工具 - -> 本部分内容主要是 Java 开发领域使用的一些 Java 工具,如构建工具、IDE、服务器、日志中心等等。 - -## 📖 内容 - -### [构建工具](build/README.md) - -- [Maven 入门指南](build/maven/maven-quickstart.md) -- [Maven 教程之 pom.xml 详解](build/maven/maven-pom.md) -- [Maven 教程之 settings.xml 详解](build/maven/maven-settings.md) -- [Maven 实战问题和最佳实践](build/maven/maven-action.md) -- [Maven 教程之发布 jar 到私服或中央仓库](build/maven/maven-deploy.md) -- [Maven 插件之代码检查](build/maven/maven-checkstyle-plugin.md) -- [Ant 简易教程](build/ant.md) - -### IDE - -- [IDEA](ide/intellij-idea.md) -- [Eclipse](ide/eclipse.md) - -## 📚 资料 - -- **官网** - - [Maven Github](https://github.com/apache/maven) - - [Maven 官方文档](https://maven.apache.org/ref/current) - - [Ant 官方手册](http://ant.apache.org/manual/index.html) -- **书籍** - - [《Maven 实战》](https://book.douban.com/subject/5345682/) - -## 🚪 传送 - -◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ diff --git a/docs/javatool/build/maven/book.json b/docs/javatool/build/maven/book.json deleted file mode 100644 index ebb671cb..00000000 --- a/docs/javatool/build/maven/book.json +++ /dev/null @@ -1,71 +0,0 @@ -{ - "gitbook": "3.2.2", - "title": "maven-tutorial", - "language": "zh-hans", - "root": "./", - "structure": { - "summary": "sidebar.md" - }, - "links": { - "sidebar": { - "java-tutorial": "https://github.com/dunwu/java-tutorial" - } - }, - "plugins": [ - "-lunr", - "-search", - "advanced-emoji@^0.2.2", - "anchor-navigation-ex@1.0.10", - "anchors@^0.7.1", - "edit-link@^2.0.2", - "expandable-chapters-small@^0.1.7", - "github@^2.0.0", - "search-plus@^0.0.11", - "simple-page-toc@^0.1.1", - "splitter@^0.0.8", - "tbfed-pagefooter@^0.0.1" - ], - "pluginsConfig": { - "anchor-navigation-ex": { - "showLevel": false, - "associatedWithSummary": true, - "multipleH1": true, - "mode": "float", - "isRewritePageTitle": false, - "float": { - "showLevelIcon": false, - "level1Icon": "fa fa-hand-o-right", - "level2Icon": "fa fa-hand-o-right", - "level3Icon": "fa fa-hand-o-right" - }, - "pageTop": { - "showLevelIcon": false, - "level1Icon": "fa fa-hand-o-right", - "level2Icon": "fa fa-hand-o-right", - "level3Icon": "fa fa-hand-o-right" - } - }, - "edit-link": { - "base": "https://github.com/dunwu/java-tutorial/blob/master/docs", - "label": "编辑此页面" - }, - "github": { - "url": "https://github.com/dunwu" - }, - "simple-page-toc": { - "maxDepth": 4, - "skipFirstH1": true - }, - "sharing": { - "weibo": true, - "all": [ - "weibo" - ] - }, - "tbfed-pagefooter": { - "copyright": "Copyright © Dunwu 2017", - "modify_label": "该文件上次修订时间:", - "modify_format": "YYYY-MM-DD HH:mm:ss" - } - } -} diff --git a/docs/javatool/build/maven/cover.jpg b/docs/javatool/build/maven/cover.jpg deleted file mode 100644 index d8591256..00000000 Binary files a/docs/javatool/build/maven/cover.jpg and /dev/null differ diff --git a/docs/javatool/build/maven/package.json b/docs/javatool/build/maven/package.json deleted file mode 100644 index f5e98af8..00000000 --- a/docs/javatool/build/maven/package.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "name": "maven-tutorial", - "author": "Zhang Peng", - "homepage": "http://dunwu.github.io/java-tutorial", - "repository": { - "type": "git", - "url": "git@github.com:dunwu/java-tutorial.git" - }, - "scripts": { - "start": "docsify serve ./ --port 4000", - "clean": "rimraf _book", - "install": "gitbook install", - "serve": "gitbook serve", - "build": "npm run clean & gitbook build", - "pdf": "gitbook pdf" - }, - "dependencies": { - "gitbook-plugin-advanced-emoji": "^0.2.2", - "gitbook-plugin-anchor-navigation-ex": "^1.0.10", - "gitbook-plugin-anchors": "^0.7.1", - "gitbook-plugin-edit-link": "^2.0.2", - "gitbook-plugin-expandable-chapters-small": "^0.1.7", - "gitbook-plugin-github": "^2.0.0", - "gitbook-plugin-search-plus": "0.0.11", - "gitbook-plugin-simple-page-toc": "^0.1.2", - "gitbook-plugin-splitter": "0.0.8", - "gitbook-plugin-tbfed-pagefooter": "0.0.1" - }, - "devDependencies": { - "gh-pages": "^2.1.1", - "rimraf": "^3.0.0" - } -} diff --git a/docs/javatool/build/maven/sidebar.md b/docs/javatool/build/maven/sidebar.md deleted file mode 100644 index 5601cdc0..00000000 --- a/docs/javatool/build/maven/sidebar.md +++ /dev/null @@ -1,6 +0,0 @@ -- [Maven 入门指南](maven-quickstart.md) -- [Maven 教程之 pom.xml 详解](maven-pom.md) -- [Maven 教程之 settings.xml 详解](maven-settings.md) -- [Maven 实战问题和最佳实践](maven-action.md) -- [Maven 教程之发布 jar 到私服或中央仓库](maven-deploy.md) -- [Maven 插件之代码检查](maven-checkstyle-plugin.md) diff --git a/docs/javatool/ide/README.md b/docs/javatool/ide/README.md deleted file mode 100644 index 96e02070..00000000 --- a/docs/javatool/ide/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Java IDE - - -> 自从有了 **IDE**,写代码从此就告别了刀耕火种的蛮荒时代。 -> -> - [Eclipse](eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 -> - 曾经抗拒从转 [Intellij Idea](intellij-idea.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 -> - 你可以在 [vscode](vscode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 - -- [Intellij IDEA 应用指南](intellij-idea.md) -- [Eclipse 应用指南](eclipse.md) -- [Vscode 应用指南](vscode.md) diff --git a/docs/javatool/monitor/README.md b/docs/javatool/monitor/README.md deleted file mode 100644 index 7cc57e21..00000000 --- a/docs/javatool/monitor/README.md +++ /dev/null @@ -1,17 +0,0 @@ -# 微服务调用链监控 - -## 内容 - -- [CAT](cat.md) -- [Zipkin](zipkin.md) -- [SkyWalking](skywalking.md) -- PinPoint -- [Arthas](arthas.md) - -## 资料 - -- [CAT Github](https://github.com/dianping/cat) -- [Zipkin Github](https://github.com/openzipkin/zipkin) -- [SkyWalking Github](https://github.com/apache/skywalking) -- [PinPoint Github](https://github.com/naver/pinpoint) -- [Arthas Github](https://github.com/alibaba/arthas) diff --git a/docs/javatool/monitor/monitor-summary.md b/docs/javatool/monitor/monitor-summary.md deleted file mode 100644 index 1b5b809e..00000000 --- a/docs/javatool/monitor/monitor-summary.md +++ /dev/null @@ -1,27 +0,0 @@ -# 监控工具对比 - -## 监控工具发展史 - -![img](http://dunwu.test.upcdn.net/snap/20200211165813.png) - -## 监控工具比对 - -### 特性对比 - -![img](http://dunwu.test.upcdn.net/snap/20200211171551.png) - -### 生态对比 - -![img](http://dunwu.test.upcdn.net/snap/20200211172631.png) - -## 技术选型 - -- Zipkin 欠缺 APM 报表能力,不推荐。 -- 企业级,推荐 CAT -- 关注和试点 SkyWalking。 - -用好调用链监控,需要订制化、自研能力。 - -## 参考资料 - -[CAT、Zipkin 和 SkyWalking 该如何选型?](https://time.geekbang.org/dailylesson/detail/100028416) diff --git a/docs/package.json b/docs/package.json deleted file mode 100644 index a5efdc14..00000000 --- a/docs/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "java-tutorial", - "version": "1.0.0", - "private": true, - "scripts": { - "clean": "rimraf dist && rimraf .temp", - "build": "npm run clean && vuepress build ./ --temp .temp", - "start": "vuepress dev ./ --temp .temp", - "lint": "markdownlint -r markdownlint-rule-emphasis-style -c ./.markdownlint.json **/*.md -i node_modules", - "lint:fix": "markdownlint -f -r markdownlint-rule-emphasis-style -c ./.markdownlint.json **/*.md -i node_modules", - "show-help": "vuepress --help", - "view-info": "vuepress view-info ./ --temp .temp" - }, - "devDependencies": { - "@vuepress/plugin-active-header-links": "^1.5.2", - "@vuepress/plugin-back-to-top": "^1.5.0", - "@vuepress/plugin-medium-zoom": "^1.5.0", - "@vuepress/plugin-pwa": "^1.5.0", - "@vuepress/theme-vue": "^1.5.0", - "markdownlint-cli": "^0.23.1", - "markdownlint-rule-emphasis-style": "^1.0.1", - "rimraf": "^3.0.1", - "vue-toasted": "^1.1.25", - "vuepress": "^1.5.0", - "vuepress-plugin-flowchart": "^1.4.2" - } -} diff --git a/package.json b/package.json new file mode 100644 index 00000000..93a7223a --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "java-tutorial", + "version": "1.0.0", + "private": true, + "scripts": { + "clean": "rimraf docs/.temp", + "start": "node --max_old_space_size=4096 ./node_modules/vuepress/cli.js dev docs", + "build": "node --max_old_space_size=4096 ./node_modules/vuepress/cli.js build docs", + "deploy": "bash scripts/deploy.sh", + "updateTheme": "yarn remove vuepress-theme-vdoing && rm -rf node_modules && yarn && yarn add vuepress-theme-vdoing -D", + "editFm": "node utils/editFrontmatter.js", + "lint": "markdownlint -r markdownlint-rule-emphasis-style -c docs/.markdownlint.json **/*.md -i node_modules", + "lint:fix": "markdownlint -f -r markdownlint-rule-emphasis-style -c docs/.markdownlint.json **/*.md -i node_modules", + "show-help": "vuepress --help", + "view-info": "vuepress view-info ./ --temp docs/.temp" + }, + "devDependencies": { + "dayjs": "^1.11.7", + "inquirer": "^9.1.4", + "json2yaml": "^1.1.0", + "markdownlint-cli": "^0.33.0", + "markdownlint-rule-emphasis-style": "^1.0.1", + "rimraf": "^4.1.2", + "vue-toasted": "^1.1.25", + "vuepress": "1.9.9", + "vuepress-plugin-baidu-tongji": "^1.0.1", + "vuepress-plugin-comment": "^0.7.3", + "vuepress-plugin-demo-block": "^0.7.2", + "vuepress-plugin-flowchart": "^1.4.2", + "vuepress-plugin-fulltext-search": "^2.2.1", + "vuepress-plugin-one-click-copy": "^1.0.2", + "vuepress-plugin-thirdparty-search": "^1.0.2", + "vuepress-plugin-zooming": "^1.1.7", + "vuepress-theme-vdoing": "^1.12.9", + "yamljs": "^0.3.0", + "markdownlint-cli": "^0.25.0", + "markdownlint-rule-emphasis-style": "^1.0.1", + "rimraf": "^3.0.1", + "vue-toasted": "^1.1.25" + } +} diff --git a/pom.xml b/pom.xml index b2a8a5ef..2c6bc3d8 100644 --- a/pom.xml +++ b/pom.xml @@ -1,19 +1,20 @@ - 4.0.0 + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 - io.github.dunwu.java - java-tutorial - 1.0.0 - pom - JAVA-TUTORIAL + io.github.dunwu.java + java-tutorial + 1.0.0 + pom + JAVA-TUTORIAL - - codes/java-distributed - codes/javaee - codes/javatool - codes/trouble-shooting - codes/deadloop - + + codes/java-distributed + codes/javaee + codes/javatech + codes/javatool + codes/trouble-shooting + codes/deadloop + diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 00000000..c9848e74 --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,10 @@ +/** + * @see https://prettier.io/docs/en/options.html + * @see https://prettier.io/docs/en/configuration.html + */ +module.exports = { + tabWidth: 2, + semi: false, + singleQuote: true, + trailingComma: 'none' +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh index 6324d001..b6ee6ee0 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -2,43 +2,46 @@ # ------------------------------------------------------------------------------ # gh-pages 部署脚本 -# @author Zhang Peng +# @author Zhang Peng # @since 2020/2/10 # ------------------------------------------------------------------------------ # 装载其它库 -ROOT_DIR=$(cd `dirname $0`/..; pwd) +ROOT_DIR=$( + cd $(dirname $0)/.. + pwd +) # 确保脚本抛出遇到的错误 set -e -cd ${ROOT_DIR}/docs - # 生成静态文件 -npm install npm run build # 进入生成的文件夹 -cd dist +cd ${ROOT_DIR}/docs/.temp # 如果是发布到自定义域名 # echo 'www.example.com' > CNAME -git init -git checkout -b gh-pages && git add . -git commit -m 'deploy' - -# 如果发布到 https://.github.io/ -if [[ ${GITHUB_TOKEN} && ${GITEE_TOKEN} ]]; then - echo "使用 token 公钥部署 gh-pages" - # ${GITHUB_TOKEN} 是 Github 私人令牌;${GITEE_TOKEN} 是 Gitee 私人令牌 - # ${GITHUB_TOKEN} 和 ${GITEE_TOKEN} 都是环境变量;travis-ci 构建时会传入变量 - git push --force --quiet "https://dunwu:${GITHUB_TOKEN}@github.com/dunwu/java-tutorial.git" gh-pages - git push --force --quiet "https://turnon:${GITEE_TOKEN}@gitee.com/turnon/java-tutorial.git" gh-pages +#if [[ ${GITHUB_TOKEN} && ${GITEE_TOKEN} ]]; then +if [[ ${GITHUB_TOKEN} ]]; then + msg='自动部署' + GITHUB_URL=https://dunwu:${GITHUB_TOKEN}@github.com/dunwu/java-tutorial.git +# GITEE_URL=https://turnon:${GITEE_TOKEN}@gitee.com/turnon/java-tutorial.git + git config --global user.name "dunwu" + git config --global user.email "forbreak@163.com" else - echo "使用 ssh 公钥部署 gh-pages" - git push -f git@github.com:dunwu/java-tutorial.git gh-pages - git push -f git@gitee.com:turnon/java-tutorial.git gh-pages + msg='手动部署' + GITHUB_URL=git@github.com:dunwu/java-tutorial.git +# GITEE_URL=git@gitee.com:turnon/java-tutorial.git fi - -cd ${ROOT_DIR} +git init +git add -A +git commit -m "${msg}" +# 推送到github gh-pages分支 +git push -f "${GITHUB_URL}" master:gh-pages +#git push -f "${GITEE_URL}" master:gh-pages + +cd - +rm -rf ${ROOT_DIR}/docs/.temp diff --git a/utils/config.yml b/utils/config.yml new file mode 100644 index 00000000..d387646b --- /dev/null +++ b/utils/config.yml @@ -0,0 +1,15 @@ +# 批量添加和修改、删除front matter配置文件 + +# 需要批量处理的路径,docs文件夹内的文件夹 (数组,映射路径:path[0]/path[1]/path[2] ... ) +path: + - docs # 第一个成员必须是docs + +# 要删除的字段 (数组) +delete: + # - tags + + # 要添加、修改front matter的数据 (front matter中没有的数据则添加,已有的数据则覆盖) +data: + # author: + # name: xugaoyi + # link: https://github.com/xugaoyi diff --git a/utils/editFrontmatter.js b/utils/editFrontmatter.js new file mode 100644 index 00000000..0998bf3d --- /dev/null +++ b/utils/editFrontmatter.js @@ -0,0 +1,98 @@ +/** + * 批量添加和修改front matter ,需要配置 ./config.yml 文件。 + */ +const fs = require('fs') // 文件模块 +const path = require('path') // 路径模块 +const matter = require('gray-matter') // front matter解析器 https://github.com/jonschlinkert/gray-matter +const jsonToYaml = require('json2yaml') +const yamlToJs = require('yamljs') +const inquirer = require('inquirer') // 命令行操作 +const chalk = require('chalk') // 命令行打印美化 +const readFileList = require('./modules/readFileList') +const { type, repairDate } = require('./modules/fn') +const log = console.log + +const configPath = path.join(__dirname, 'config.yml') // 配置文件的路径 + +main() + +/** + * 主体函数 + */ +async function main() { + const promptList = [ + { + type: 'confirm', + message: chalk.yellow('批量操作frontmatter有修改数据的风险,确定要继续吗?'), + name: 'edit' + } + ] + let edit = true + + await inquirer.prompt(promptList).then((answers) => { + edit = answers.edit + }) + + if (!edit) { + // 退出操作 + return + } + + const config = yamlToJs.load(configPath) // 解析配置文件的数据转为js对象 + + if (type(config.path) !== 'array') { + log(chalk.red('路径配置有误,path字段应该是一个数组')) + return + } + + if (config.path[0] !== 'docs') { + log(chalk.red("路径配置有误,path数组的第一个成员必须是'docs'")) + return + } + + const filePath = path.join(__dirname, '..', ...config.path) // 要批量修改的文件路径 + const files = readFileList(filePath) // 读取所有md文件数据 + + files.forEach((file) => { + let dataStr = fs.readFileSync(file.filePath, 'utf8') // 读取每个md文件的内容 + const fileMatterObj = matter(dataStr) // 解析md文件的front Matter。 fileMatterObj => {content:'剔除frontmatter后的文件内容字符串', data:{}, ...} + let matterData = fileMatterObj.data // 得到md文件的front Matter + + let mark = false + // 删除操作 + if (config.delete) { + if (type(config.delete) !== 'array') { + log(chalk.yellow('未能完成删除操作,delete字段的值应该是一个数组!')) + } else { + config.delete.forEach((item) => { + if (matterData[item]) { + delete matterData[item] + mark = true + } + }) + } + } + + // 添加、修改操作 + if (type(config.data) === 'object') { + Object.assign(matterData, config.data) // 将配置数据合并到front Matter对象 + mark = true + } + + // 有操作时才继续 + if (mark) { + if (matterData.date && type(matterData.date) === 'date') { + matterData.date = repairDate(matterData.date) // 修复时间格式 + } + const newData = + jsonToYaml + .stringify(matterData) + .replace(/\n\s{2}/g, '\n') + .replace(/"/g, '') + + '---\r\n' + + fileMatterObj.content + fs.writeFileSync(file.filePath, newData) // 写入 + log(chalk.green(`update frontmatter:${file.filePath} `)) + } + }) +} diff --git a/utils/modules/fn.js b/utils/modules/fn.js new file mode 100644 index 00000000..a4654f18 --- /dev/null +++ b/utils/modules/fn.js @@ -0,0 +1,25 @@ +// 类型判断 +exports.type = function (o) { + var s = Object.prototype.toString.call(o) + return s.match(/\[object (.*?)\]/)[1].toLowerCase() +} + +// 修复date时区格式的问题 +exports.repairDate = function (date) { + date = new Date(date) + return `${date.getUTCFullYear()}-${zero(date.getUTCMonth() + 1)}-${zero(date.getUTCDate())} ${zero( + date.getUTCHours() + )}:${zero(date.getUTCMinutes())}:${zero(date.getUTCSeconds())}` +} + +// 日期的格式 +exports.dateFormat = function (date) { + return `${date.getFullYear()}-${zero(date.getMonth() + 1)}-${zero(date.getDate())} ${zero(date.getHours())}:${zero( + date.getMinutes() + )}:${zero(date.getSeconds())}` +} + +// 小于10补0 +function zero(d) { + return d.toString().padStart(2, '0') +} \ No newline at end of file diff --git a/utils/modules/readFileList.js b/utils/modules/readFileList.js new file mode 100644 index 00000000..74a4eb6b --- /dev/null +++ b/utils/modules/readFileList.js @@ -0,0 +1,49 @@ +/** + * 读取所有md文件数据 + */ +const fs = require('fs') // 文件模块 +const path = require('path') // 路径模块 +const docsRoot = path.join(__dirname, '..', '..', 'docs') // docs文件路径 + +function readFileList(dir = docsRoot, filesList = []) { + const files = fs.readdirSync(dir) + files.forEach((item, index) => { + let filePath = path.join(dir, item) + const stat = fs.statSync(filePath) + if (stat.isDirectory() && item !== '.vuepress') { + readFileList(path.join(dir, item), filesList) //递归读取文件 + } else { + if (path.basename(dir) !== 'docs') { + // 过滤docs目录级下的文件 + + const filename = path.basename(filePath) + const fileNameArr = filename.split('.') + const firstDotIndex = filename.indexOf('.') + const lastDotIndex = filename.lastIndexOf('.') + + let name = null, + type = null + if (fileNameArr.length === 2) { + // 没有序号的文件 + name = fileNameArr[0] + type = fileNameArr[1] + } else if (fileNameArr.length >= 3) { + // 有序号的文件(或文件名中间有'.') + name = filename.substring(firstDotIndex + 1, lastDotIndex) + type = filename.substring(lastDotIndex + 1) + } + + if (type === 'md') { + // 过滤非md文件 + filesList.push({ + name, + filePath + }) + } + } + } + }) + return filesList +} + +module.exports = readFileList