diff --git a/.editorconfig b/.editorconfig index 7f575a88..d72a75ea 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,30 +1,27 @@ -# EditorConfig helps developers define and maintain consistent -# coding styles between different editors and IDEs -# http://editorconfig.org -# 所有文件换行以 Unix like 风格(LF),win 格式特定的除外(bat) -# 缩进 java 4 个空格,其他所有文件 2 个空格 +# EditorConfig 用于在 IDE 中检查代码的基本 Code Style +# @see: https://editorconfig.org/ + +# 配置说明: +# 所有文件换行使用 Unix 风格(LF),*.bat 文件使用 Windows 风格(CRLF) +# java / sh 文件缩进 4 个空格,其他所有文件缩进 2 个空格 root = true [*] -# Unix-style newlines with a newline ending every file end_of_line = lf - -# Change these settings to your own preference -indent_style = space indent_size = 2 - -# We recommend you to keep these unchanged +indent_style = space +max_line_length = 120 charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.bat] +[*.{bat, cmd}] end_of_line = crlf -[*.java] -indent_style = space +[*.{java, gradle, groovy, kt, sh}] indent_size = 4 [*.md] +max_line_length = 0 trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes index 91488b54..07962a1f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -56,7 +56,7 @@ *.ico binary *.gif binary -# media +# medias *.mp3 binary *.swf binary diff --git a/.gitignore b/.gitignore index 76e97fd1..83948575 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,43 @@ -################ JAVA ################ -# temp folders +# --------------------------------------------------------------------- +# more gitignore templates see https://github.com/github/gitignore +# --------------------------------------------------------------------- + +# ------------------------------- java ------------------------------- +# compiled folders classes target logs +.mtj.tmp/ -# temp files +# compiled files *.class + +# bluej files +*.ctxt + +# package files # *.jar *.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs +hs_err_pid* + +# maven plugin temp files +.flattened-pom.xml +package-lock.json -################ JAVASCRIPT ################ +# ------------------------------- javascript ------------------------------- # dependencies node_modules # temp folders -build +.temp dist _book _jsdoc @@ -26,13 +48,14 @@ npm-debug.log* yarn-debug.log* yarn-error.log* bundle*.js +book.pdf -################ IDEA ################ +# ------------------------------- intellij ------------------------------- .idea *.iml -################ Eclipse ################ +# ------------------------------- eclipse ------------------------------- .classpath .project diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..7f7498fb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,24 @@ +# 持续集成 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/LICENSE b/LICENSE index 6a8657f1..3b7b82d0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,427 @@ -MIT License - -Copyright (c) 2018 Zhang Peng - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Attribution-ShareAlike 4.0 International + +======================================================================= + +Creative Commons Corporation ("Creative Commons") is not a law firm and +does not provide legal services or legal advice. Distribution of +Creative Commons public licenses does not create a lawyer-client or +other relationship. Creative Commons makes its licenses and related +information available on an "as-is" basis. Creative Commons gives no +warranties regarding its licenses, any material licensed under their +terms and conditions, or any related information. Creative Commons +disclaims all liability for damages resulting from their use to the +fullest extent possible. + +Using Creative Commons Public Licenses + +Creative Commons public licenses provide a standard set of terms and +conditions that creators and other rights holders may use to share +original works of authorship and other material subject to copyright +and certain other rights specified in the public license below. The +following considerations are for informational purposes only, are not +exhaustive, and do not form part of our licenses. + + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + +======================================================================= + +Creative Commons Attribution-ShareAlike 4.0 International Public +License + +By exercising the Licensed Rights (defined below), You accept and agree +to be bound by the terms and conditions of this Creative Commons +Attribution-ShareAlike 4.0 International Public License ("Public +License"). To the extent this Public License may be interpreted as a +contract, You are granted the Licensed Rights in consideration of Your +acceptance of these terms and conditions, and the Licensor grants You +such rights in consideration of benefits the Licensor receives from +making the Licensed Material available under these terms and +conditions. + + +Section 1 -- Definitions. + + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + + c. BY-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution and ShareAlike. + + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + + k. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + + l. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + + m. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + + +Section 2 -- Scope. + + a. License grant. + + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + + a. reproduce and Share the Licensed Material, in whole or + in part; and + + b. produce, reproduce, and Share Adapted Material. + + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + + 3. Term. The term of this Public License is specified in Section + 6(a). + + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + + 5. Downstream recipients. + + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + + b. Other rights. + + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + + 2. Patent and trademark rights are not licensed under this + Public License. + + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + + +Section 3 -- License Conditions. + +Your exercise of the Licensed Rights is expressly made subject to the +following conditions. + + a. Attribution. + + 1. If You Share the Licensed Material (including in modified + form), You must: + + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + + ii. a copyright notice; + + iii. a notice that refers to this Public License; + + iv. a notice that refers to the disclaimer of + warranties; + + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + + b. ShareAlike. + + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-SA Compatible License. + + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + + +Section 4 -- Sui Generis Database Rights. + +Where the Licensed Rights include Sui Generis Database Rights that +apply to Your use of the Licensed Material: + + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + + including for purposes of Section 3(b); and + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + +For the avoidance of doubt, this Section 4 supplements and does not +replace Your obligations under this Public License where the Licensed +Rights include other Copyright and Similar Rights. + + +Section 5 -- Disclaimer of Warranties and Limitation of Liability. + + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + + +Section 6 -- Term and Termination. + + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + + 2. upon express reinstatement by the Licensor. + + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + + +Section 7 -- Other Terms and Conditions. + + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + + +Section 8 -- Interpretation. + + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + + +======================================================================= + +Creative Commons is not a party to its public +licenses. Notwithstanding, Creative Commons may elect to apply one of +its public licenses to material it publishes and in those instances +will be considered the “Licensor.” The text of the Creative Commons +public licenses is dedicated to the public domain under the CC0 Public +Domain Dedication. Except for the limited purpose of indicating that +material is shared under a Creative Commons public license or as +otherwise permitted by the Creative Commons policies published at +creativecommons.org/policies, Creative Commons does not authorize the +use of the trademark "Creative Commons" or any other trademark or logo +of Creative Commons without its prior written consent including, +without limitation, in connection with any unauthorized modifications +to any of its public licenses or any other arrangements, +understandings, or agreements concerning use of licensed material. For +the avoidance of doubt, this paragraph does not form part of the +public licenses. + +Creative Commons may be contacted at creativecommons.org. diff --git a/README.md b/README.md index 0e049d5a..35c9a451 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,169 @@ -# JavaWeb - -> Java Web 开发之路经验总结 - -| :one: | :two: | :three: | :four: | -| --------------------- | ----------------------------- | ------------------------------- | ------------------ | -| [JavaEE](#one-javaee) | [单点式技术](#two-单点式技术) | [分布式技术](#three-分布式技术) | [工具](#four-工具) | - -## :recycle: 架构设计 - -> [架构设计](docs/architecture/) 整理架构设计方面的一些学习总结和心得。 - -- [大型网站架构概述](docs/architecture/大型网站架构概述.md) -- [网站的高性能架构](docs/architecture/网站的高性能架构.md) -- [网站的高可用架构](docs/architecture/网站的高可用架构.md) -- [网站的伸缩性架构](docs/architecture/网站的伸缩性架构.md) - -## :one: JavaEE - -> [JavaEE](docs/javaee/) 技术——Java Web 的基石 - -## :two: 单点式技术 - -> [单点式技术(Standalone)](docs/standalone/),典型的技术如:SSM 框架、SSH 框架。 - -- Platform - - [Spring](https://github.com/dunwu/spring-notes) - JavaSE/JavaEE 一站式开发框架。 -- ORM - - [Mybatis](docs/standalone/orm/mybatis.md) - 一个支持普通 SQL 查询,存储过程和高级映射的优秀持久层框架。 - - Hibernate - 待补充。。。 -- 安全 - - [Shiro](docs/standalone/security/shiro.md) - 安全框架,具有认证、授权、加密、会话管理功能。 - -## :three: 分布式技术 - -> [分布式技术(Distributed)](docs/distributed/),典型的技术如:分布式缓存、分布式消息队列、分布式服务、分布式搜索引擎等。 - -- [分布式技术面试题](docs/distributed/分布式技术面试题.md) - -### [分布式缓存(CACHE)](docs/distributed/cache) - -- [分布式缓存](docs/distributed/cache/分布式缓存.md) -- [Redis](docs/distributed/cache/redis.md) -- Memcached - -### [分布式服务(RPC)](docs/distributed/rpc) - -- [Dubbo](docs/distributed/rpc/dubbo.md) - 基于 Java 开发的高性能 RPC 框架。 -- [ZooKeeper 实战篇](docs/distributed/rpc/zookeeper-basics.md) -- [ZooKeeper 原理篇](docs/distributed/rpc/zookeeper-advanced.md) - -### [分布式消息队列(MQ)](docs/distributed/mq) - -- [分布式消息队列](docs/distributed/mq/分布式消息队列.md) -- [Kafka 实战篇](docs/distributed/mq/kafka-basics.md) -- [Kafka 原理篇](docs/distributed/mq/kafka-advanced.md) -- [RocketMQ 实战篇](docs/distributed/mq/rocketmq-basics.md) -- [RocketMQ 原理篇](docs/distributed/mq/rocketmq-basics.md) -- [ActiveMQ 实战篇](docs/distributed/mq/ActiveMQ.md) -- RabbitMQ - 待补充。。。 - -### 分布式搜索引擎 - -- ElasticSearch - 待补充。。。 - -## :four: 工具 - -> [工具](docs/tools/) 整理了 Java Web 领域常用软件。 - -- [Nginx](https://github.com/dunwu/Nginx) - 轻量级的 Web 服务器、反向代理服务器及电子邮件(IMAP/POP3)代理服务器,支持负载均衡。 -- [Tomcat](docs/tools/tomcat.md) - 轻量级的应用服务器 -- [Jetty](docs/tools/jetty.md) - 比 Tomcat 更轻量级的应用服务器 +

+ + logo + +

+ +

+ license + build +

+ +

JAVATECH

+ +> ☕ **JavaTech** 汇总了 Java 后端开发中常见的主流技术的应用、特性、原理。 +> +> - 🔁 项目同步维护:[Github](https://github.com/dunwu/javatech/) | [Gitee](https://gitee.com/turnon/javatech/) +> - 📖 电子书阅读:[Github Pages](https://dunwu.github.io/javatech/) | [Gitee Pages](http://turnon.gitee.io/javatech/) +> +> 说明: +> +> - 下面的内容清单中,凡是有 📚 标记的技术,都已整理成详细的教程。 +> - 部分技术因为可以应用于不同领域,所以可能会同时出现在不同的类别下。 + +## 📖 内容 + +### [框架](docs/framework) + +- [Spring](https://dunwu.github.io/spring-tutorial/) 📚 +- [Spring Boot](https://dunwu.github.io/spring-boot-tutorial/) 📚 +- [Spring Cloud](https://github.com/dunwu/spring-cloud-tutorial) 📚 +- [MyBatis](docs/framework/mybatis) + - [Mybatis 应用指南](docs/framework/mybatis/Mybatis应用指南.md) + - [Mybatis 原理](docs/framework/mybatis/Mybatis原理.md) +- [Netty](docs/framework/netty.md) + +### [消息队列](docs/mq) + +> 消息队列(Message Queue,简称 MQ)技术是分布式应用间交换信息的一种技术。 +> +> 消息队列主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 +> +> 如果想深入学习各种消息队列产品,建议先了解一下 [消息队列基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/mq.md) ,有助于理解消息队列特性的实现和设计思路。 + +- [消息队列基本原理](docs/mq/消息队列基本原理.md) +- [消息队列面试题](docs/mq/消息队列面试.md) 💯 +- [Kafka](https://dunwu.github.io/bigdata-tutorial/kafka) 📚 +- [RocketMQ](docs/mq/rocketmq.md) +- [ActiveMQ](docs/mq/activemq.md) + +### [缓存](docs/cache) + +> 缓存可以说是优化系统性能的第一手段,在各种技术中都会有缓存的应用。 +> +> 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/cache.md),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200710163555.png) + +- [缓存面试题](docs/cache/cache-interview.md) 💯 +- [缓存基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/cache.md) +- [Java 缓存框架](docs/cache/cache-framework.md) - 关键词:Spring Cache、J2Cache、JetCache +- [Redis 教程](https://dunwu.github.io/db-tutorial/nosql/redis/) 📚 +- [Memcached 应用指南](docs/cache/memcached.md) +- [Java 缓存库](docs/cache/cache-libs.md) - 关键词:ConcurrentHashMap、LRUHashMap、Guava Cache、Caffeine、Ehcache +- [Ehcache 应用指南](docs/cache/ehcache.md) +- [Http 缓存](docs/cache/http-cache.md) + +### [微服务](docs/microservice) + +- [Dubbo](docs/microservice/dubbo.md) +- [**Spring Cloud**](https://github.com/dunwu/spring-cloud-tutorial) 📚 + - Eureka + - Consul + - Nacos + - Zuul + - Gateway +- 通信 + - [Netty](docs/framework/netty.md) + +### 搜索引擎 + +- [ElasticSearch](docs/search/elasticsearch) + - [ElasticSearch 应用指南](docs/search/elasticsearch/elasticsearch-quickstart.md) + - [ElasticSearch API](docs/search/elasticsearch/elasticsearch-api.md) + - [ElasticSearch 运维](docs/search/elasticsearch/elasticsearch-ops.md) +- [Elastic 技术栈](docs/search) + - [Elastic 技术栈快速入门](docs/search/elastic-quickstart.md) + - [Beats 入门指南](docs/search/elastic-beats.md) + - [Beats 运维](docs/search/elastic-beats-ops.md) + - [Kibana 入门指南](docs/search/elastic-kibana.md) + - [Kibana 运维](docs/search/elastic-kibana-ops.md) + - [Logstash 入门指南](docs/search/elastic-logstash.md) + - [Logstash 运维](docs/search/elastic-logstash-ops.md) +- Solr +- Lucene + +### [安全](docs/security) + +> Java 领域比较流行的安全框架就是 shiro 和 spring-security。 +> +> shiro 更为简单、轻便,容易理解,能满足大多数基本安全场景下的需要。 +> +> spring-security 功能更丰富,也比 shiro 更复杂。值得一提的是由于 spring-security 是 spring 团队开发,所以集成 spring 和 spring-boot 框架更容易。 + +- [Shiro](docs/security/shiro.md) +- [Spring Security](docs/security/spring-security.md) + +### [测试](docs/test) + +- [Junit](docs/test/junit.md) +- [Mockito](docs/test/mockito.md) +- [JMH](docs/test/jmh.md) +- [Jmeter](docs/test/jmeter.md) + +### [服务器](docs/server) + +> Tomcat 和 Jetty 都是 Java 比较流行的轻量级服务器。 +> +> Nginx 是目前最流行的反向代理服务器,也常用于负载均衡。 + +- [Tomcat 应用指南](docs/server/Tomcat应用指南.md) +- [Tomcat 连接器](docs/server/Tomcat连接器.md) +- [Tomcat 容器](docs/server/Tomcat容器.md) +- [Tomcat 优化](docs/server/Tomcat优化.md) +- [Jetty](docs/server/jetty.md) +- [Nginx](https://github.com/dunwu/nginx-tutorial) 📚 + +### [大数据](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) + +### [LIB](docs/lib) + +- [日志](docs/lib/javalib-log.md) - log4j2、logback、log4j、Slf4j +- [序列化](docs/lib/serialized/) + - [JSON](docs/lib/serialized/javalib-json.md) - Fastjson、Jackson、Gson + - [二进制](docs/lib/serialized/javalib-binary.md) - Protobuf、Thrift、Hessian、Kryo、FST +- [模板引擎](docs/lib/template) - [Freemark](docs/lib/template/freemark.md)、[Velocity](docs/lib/template/velocity.md)、[Thymeleaf](docs/lib/template/thymeleaf.md) +- JavaBean - [Lombok](docs/lib/bean/lombok.md)、[Dozer](docs/lib/bean/dozer.md) +- 工具包 - Apache Common、Guava、Hutool +- 辅助 - swagger + +## 📚 资料 + +## 🚪 传送 + +◾ 🏠 [JAVATECH 首页](https://github.com/dunwu/javatech) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ + +> 你可能会感兴趣: + +- [Java 教程](https://github.com/dunwu/java-tutorial) 📚 +- [JavaCore 教程](https://dunwu.github.io/javacore/) 📚 +- [JavaTech 教程](https://dunwu.github.io/javatech/) 📚 +- [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/Dubbo.xmind b/assets/Dubbo.xmind new file mode 100644 index 00000000..36b6e597 Binary files /dev/null and b/assets/Dubbo.xmind differ diff --git a/assets/javaweb.eddx b/assets/javaweb.eddx new file mode 100644 index 00000000..e86cfc8b Binary files /dev/null and b/assets/javaweb.eddx differ diff --git a/assets/mq/RocketMQ.xmind b/assets/mq/RocketMQ.xmind new file mode 100644 index 00000000..fdf8105a Binary files /dev/null and b/assets/mq/RocketMQ.xmind differ diff --git a/assets/mybatis.eddx b/assets/mybatis.eddx new file mode 100644 index 00000000..62e6cf8d Binary files /dev/null and b/assets/mybatis.eddx differ diff --git a/assets/mybatis.xmind b/assets/mybatis.xmind new file mode 100644 index 00000000..64ed4a93 Binary files /dev/null and b/assets/mybatis.xmind differ diff --git a/assets/server/tomcat/Lifecycle.uml b/assets/server/tomcat/Lifecycle.uml new file mode 100644 index 00000000..19d6433a --- /dev/null +++ b/assets/server/tomcat/Lifecycle.uml @@ -0,0 +1,27 @@ + + + JAVA + org.apache.catalina.Lifecycle + + org.apache.catalina.Lifecycle + org.apache.catalina.LifecycleState + org.apache.catalina.util.LifecycleBase + + + + + + + + + + + + Methods + Properties + Fields + + All + public + + diff --git a/assets/server/tomcat/ProtocolHandler.uml b/assets/server/tomcat/ProtocolHandler.uml new file mode 100644 index 00000000..8210b959 --- /dev/null +++ b/assets/server/tomcat/ProtocolHandler.uml @@ -0,0 +1,76 @@ + + + JAVA + org.apache.coyote.ajp.AjpAprProtocol + + org.apache.coyote.ajp.AjpNioProtocol + org.apache.coyote.ajp.AbstractAjpProtocol + org.apache.coyote.http11.Http11NioProtocol + org.apache.coyote.ajp.AjpAprProtocol + org.apache.coyote.ProtocolHandler + org.apache.coyote.http11.Http11AprProtocol + org.apache.coyote.http11.AbstractHttp11Protocol + org.apache.coyote.http11.Http11Nio2Protocol + org.apache.coyote.AbstractProtocol + org.apache.coyote.ajp.AjpNio2Protocol + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.apache.coyote.ProtocolHandler + + + All + private + + diff --git "a/assets/server/tomcat/Tomcat\347\224\237\345\221\250\346\234\237\347\256\241\347\220\206\346\200\273\344\275\223\347\261\273\345\233\276.uml" "b/assets/server/tomcat/Tomcat\347\224\237\345\221\250\346\234\237\347\256\241\347\220\206\346\200\273\344\275\223\347\261\273\345\233\276.uml" new file mode 100644 index 00000000..ee728d8c --- /dev/null +++ "b/assets/server/tomcat/Tomcat\347\224\237\345\221\250\346\234\237\347\256\241\347\220\206\346\200\273\344\275\223\347\261\273\345\233\276.uml" @@ -0,0 +1,133 @@ + + + JAVA + org.apache.catalina.Lifecycle + + org.apache.catalina.core.StandardEngine + org.apache.catalina.core.ContainerBase + org.apache.catalina.Container + org.apache.catalina.Lifecycle + org.apache.catalina.core.StandardWrapper + org.apache.catalina.connector.Connector + org.apache.catalina.util.LifecycleBase + org.apache.catalina.core.StandardContext + org.apache.catalina.core.StandardHost + org.apache.catalina.core.StandardServer + org.apache.catalina.core.StandardService + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + All + public + + diff --git a/assets/server/tomcat/jetty.xmind b/assets/server/tomcat/jetty.xmind new file mode 100644 index 00000000..6164e864 Binary files /dev/null and b/assets/server/tomcat/jetty.xmind differ diff --git a/assets/server/tomcat/tomcat.eddx b/assets/server/tomcat/tomcat.eddx new file mode 100644 index 00000000..82b2993b Binary files /dev/null and b/assets/server/tomcat/tomcat.eddx differ diff --git a/assets/server/tomcat/tomcat.xmind b/assets/server/tomcat/tomcat.xmind new file mode 100644 index 00000000..04cf12f8 Binary files /dev/null and b/assets/server/tomcat/tomcat.xmind differ diff --git a/assets/shiro.xmind b/assets/shiro.xmind new file mode 100644 index 00000000..014d8b3e Binary files /dev/null and b/assets/shiro.xmind differ diff --git a/assets/test.eddx b/assets/test.eddx new file mode 100644 index 00000000..a5f0d00c Binary files /dev/null and b/assets/test.eddx differ diff --git a/codes/cache/pom.xml b/codes/cache/pom.xml new file mode 100644 index 00000000..5cf30683 --- /dev/null +++ b/codes/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 + cache + 1.0.0 + jar + Java 缓存 + + + + 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/cache/src/main/java/io/github/dunwu/javatech/SpringBootDataCacheApplication.java b/codes/cache/src/main/java/io/github/dunwu/javatech/SpringBootDataCacheApplication.java new file mode 100644 index 00000000..4a5220d3 --- /dev/null +++ b/codes/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/cache/src/main/java/io/github/dunwu/javatech/cache/CaffeineDemo.java b/codes/cache/src/main/java/io/github/dunwu/javatech/cache/CaffeineDemo.java new file mode 100644 index 00000000..484259a6 --- /dev/null +++ b/codes/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/cache/src/main/java/io/github/dunwu/javatech/cache/GuavaCacheDemo.java b/codes/cache/src/main/java/io/github/dunwu/javatech/cache/GuavaCacheDemo.java new file mode 100644 index 00000000..1cdb585e --- /dev/null +++ b/codes/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/cache/src/main/java/io/github/dunwu/javatech/cache/LRUCache.java b/codes/cache/src/main/java/io/github/dunwu/javatech/cache/LRUCache.java new file mode 100644 index 00000000..3d38eae3 --- /dev/null +++ b/codes/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/cache/src/main/java/io/github/dunwu/javatech/cache/MemcachedDemo.java b/codes/cache/src/main/java/io/github/dunwu/javatech/cache/MemcachedDemo.java new file mode 100644 index 00000000..9399b12d --- /dev/null +++ b/codes/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/cache/src/main/java/io/github/dunwu/javatech/data/User.java b/codes/cache/src/main/java/io/github/dunwu/javatech/data/User.java new file mode 100644 index 00000000..758e3c9a --- /dev/null +++ b/codes/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/cache/src/main/java/io/github/dunwu/javatech/data/UserDao.java b/codes/cache/src/main/java/io/github/dunwu/javatech/data/UserDao.java new file mode 100644 index 00000000..370147cd --- /dev/null +++ b/codes/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/cache/src/main/java/io/github/dunwu/javatech/data/UserDaoImpl.java b/codes/cache/src/main/java/io/github/dunwu/javatech/data/UserDaoImpl.java new file mode 100644 index 00000000..a69ec012 --- /dev/null +++ b/codes/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/cache/src/main/resources/application.properties b/codes/cache/src/main/resources/application.properties new file mode 100644 index 00000000..63306cc8 --- /dev/null +++ b/codes/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/cache/src/main/resources/banner.txt b/codes/cache/src/main/resources/banner.txt new file mode 100644 index 00000000..449413d5 --- /dev/null +++ b/codes/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/cache/src/main/resources/logback.xml b/codes/cache/src/main/resources/logback.xml new file mode 100644 index 00000000..240ee4c6 --- /dev/null +++ b/codes/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/cache/src/main/resources/sql/data.sql b/codes/cache/src/main/resources/sql/data.sql new file mode 100644 index 00000000..694c3472 --- /dev/null +++ b/codes/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/cache/src/main/resources/sql/schema.sql b/codes/cache/src/main/resources/sql/schema.sql new file mode 100644 index 00000000..247bdc1e --- /dev/null +++ b/codes/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/cache/src/test/java/io/github/dunwu/javatech/cache/EhcacheApiTest.java b/codes/cache/src/test/java/io/github/dunwu/javatech/cache/EhcacheApiTest.java new file mode 100644 index 00000000..d8d2a356 --- /dev/null +++ b/codes/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/cache/src/test/java/io/github/dunwu/javatech/cache/LRUCacheTest.java b/codes/cache/src/test/java/io/github/dunwu/javatech/cache/LRUCacheTest.java new file mode 100644 index 00000000..21bfbc81 --- /dev/null +++ b/codes/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/cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCacheDemo.java b/codes/cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCacheDemo.java new file mode 100644 index 00000000..bfc7a895 --- /dev/null +++ b/codes/cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCacheDemo.java @@ -0,0 +1,143 @@ +package io.github.dunwu.javatech.cache.spring; + +import io.github.dunwu.javatech.data.User; +import org.junit.jupiter.api.Test; +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/cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCaffeineCacheTest.java b/codes/cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCaffeineCacheTest.java new file mode 100644 index 00000000..3b1934a9 --- /dev/null +++ b/codes/cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCaffeineCacheTest.java @@ -0,0 +1,73 @@ +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.stereotype.Component; +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/cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringConcurrentHashMapCacheTest.java b/codes/cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringConcurrentHashMapCacheTest.java new file mode 100644 index 00000000..d63d03d1 --- /dev/null +++ b/codes/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/cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringEhcacheCacheTest.java b/codes/cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringEhcacheCacheTest.java new file mode 100644 index 00000000..d39e9d86 --- /dev/null +++ b/codes/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/cache/src/test/java/io/github/dunwu/javatech/cache/spring/UserService.java b/codes/cache/src/test/java/io/github/dunwu/javatech/cache/spring/UserService.java new file mode 100644 index 00000000..588b6d27 --- /dev/null +++ b/codes/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/cache/src/test/java/io/github/dunwu/javatech/cache/spring/package-info.java b/codes/cache/src/test/java/io/github/dunwu/javatech/cache/spring/package-info.java new file mode 100644 index 00000000..21c5686c --- /dev/null +++ b/codes/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/cache/src/test/resources/ehcache/ehcache.xml b/codes/cache/src/test/resources/ehcache/ehcache.xml new file mode 100644 index 00000000..9bf4c661 --- /dev/null +++ b/codes/cache/src/test/resources/ehcache/ehcache.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + diff --git a/codes/cache/src/test/resources/ehcache/ehcache1.xml b/codes/cache/src/test/resources/ehcache/ehcache1.xml new file mode 100644 index 00000000..690eebbd --- /dev/null +++ b/codes/cache/src/test/resources/ehcache/ehcache1.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/codes/cache/src/test/resources/ehcache/ehcache2.xml b/codes/cache/src/test/resources/ehcache/ehcache2.xml new file mode 100644 index 00000000..bb41206a --- /dev/null +++ b/codes/cache/src/test/resources/ehcache/ehcache2.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + diff --git a/codes/cache/src/test/resources/spring/spring-caffeine.xml b/codes/cache/src/test/resources/spring/spring-caffeine.xml new file mode 100644 index 00000000..1570bba2 --- /dev/null +++ b/codes/cache/src/test/resources/spring/spring-caffeine.xml @@ -0,0 +1,19 @@ + + + + 使用 Caffeine 作为 Spring 缓存 + + + + + + + + + diff --git a/codes/cache/src/test/resources/spring/spring-ehcache.xml b/codes/cache/src/test/resources/spring/spring-ehcache.xml new file mode 100644 index 00000000..81af38dd --- /dev/null +++ b/codes/cache/src/test/resources/spring/spring-ehcache.xml @@ -0,0 +1,25 @@ + + + + 使用 EhCache 作为 Spring 缓存 + + + + + + + + + + + + + + + diff --git a/codes/cache/src/test/resources/spring/spring-hashmap.xml b/codes/cache/src/test/resources/spring/spring-hashmap.xml new file mode 100644 index 00000000..1620e0fe --- /dev/null +++ b/codes/cache/src/test/resources/spring/spring-hashmap.xml @@ -0,0 +1,26 @@ + + + + 使用 ConcurrentHashMap 作为 Spring 缓存 + + + + + + + + + + + + + + + + diff --git a/codes/javaee/README.md b/codes/javaee/README.md deleted file mode 100644 index 7587bd07..00000000 --- a/codes/javaee/README.md +++ /dev/null @@ -1 +0,0 @@ -# JavaEE 示例代码 \ No newline at end of file diff --git a/codes/javaee/filter/pom.xml b/codes/javaee/filter/pom.xml deleted file mode 100644 index a748e82f..00000000 --- a/codes/javaee/filter/pom.xml +++ /dev/null @@ -1,130 +0,0 @@ - - 4.0.0 - - - io.github.dunwu - javaee-notes-filter - 1.0.0 - war - - - - - javaee-notes-filter - javaee 学习笔记之 filter - - - - - - UTF-8 - 1.7 - ${java.version} - ${java.version} - - - 9.3.2.v20150730 - - - - - ch.qos.logback - logback-classic - 1.1.3 - - - ch.qos.logback - logback-core - 1.1.3 - - - org.logback-extensions - logback-ext-spring - 0.1.2 - - - org.slf4j - jcl-over-slf4j - 1.7.12 - - - - - - net.coobird - thumbnailator - [0.4, 0.5) - - - - - - commons-fileupload - commons-fileupload - 1.3.1 - - - commons-io - commons-io - 2.5 - - - org.apache.commons - commons-lang3 - 3.4 - - - - - - javax.servlet - javax.servlet-api - 3.1.0 - - - - - - org.eclipse.jetty - jetty-webapp - ${jetty.version} - - - org.eclipse.jetty - jetty-server - ${jetty.version} - - - org.eclipse.jetty - jetty-annotations - ${jetty.version} - - - org.eclipse.jetty - apache-jsp - ${jetty.version} - - - - - - junit - junit - 4.12 - - - org.assertj - assertj-core - 3.4.1 - - - - - - - - - - - diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/CacheFilter.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/CacheFilter.java deleted file mode 100644 index cc79cb30..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/CacheFilter.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.github.dunwu.javaee.filter; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.Reader; -import java.io.Writer; -import java.net.URLEncoder; - -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import io.github.dunwu.javaee.filter.wrapper.CacheResponseWrapper; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class CacheFilter extends MyFilter { - - private ServletContext servletContext; - - // 缓存文件夹,使用Tomcat工作目录 - private File temporalDir; - - // 缓存时间,配置在Filter初始化参数中 - private long cacheTime = Long.MAX_VALUE; - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - super.init(filterConfig); - temporalDir = (File) filterConfig.getServletContext().getAttribute("javax.servlet.context.tempdir"); - servletContext = filterConfig.getServletContext(); - cacheTime = new Long(filterConfig.getInitParameter("cacheTime")); - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - - logger.info("{} 开始做过滤处理", this.getClass().getName()); - - HttpServletRequest httpServletRequest = (HttpServletRequest) request; - HttpServletResponse httpServletResponse = (HttpServletResponse) response; - - // 如果为 POST, 则不经过缓存 - if ("POST".equalsIgnoreCase(httpServletRequest.getMethod())) { - chain.doFilter(httpServletRequest, httpServletResponse); - return; - } - - // 请求的 URI - String uri = httpServletRequest.getRequestURI(); - if (uri == null) - uri = ""; - uri = uri.replace(httpServletRequest.getContextPath() + "/", ""); - uri = uri.trim().length() == 0 ? "index.jsp" : uri; - uri = httpServletRequest.getQueryString() == null ? uri : (uri + "?" + httpServletRequest.getQueryString()); - - // 对应的缓存文件 - File cacheFile = new File(temporalDir, URLEncoder.encode(uri, "UTF-8")); - System.out.println(cacheFile); - - // 如果缓存文件不存在 或者已经超出缓存时间 则请求 Servlet - if (!cacheFile.exists() || cacheFile.length() == 0 - || cacheFile.lastModified() < System.currentTimeMillis() - cacheTime) { - - CacheResponseWrapper cacheResponse = new CacheResponseWrapper(httpServletResponse); - - chain.doFilter(httpServletRequest, cacheResponse); - - // 将内容写入缓存文件 - char[] content = cacheResponse.getCacheWriter().toCharArray(); - - temporalDir.mkdirs(); - cacheFile.createNewFile(); - - Writer writer = new OutputStreamWriter(new FileOutputStream(cacheFile), "UTF-8"); - writer.write(content); - writer.close(); - } - - // 请求的ContentType - String mimeType = servletContext.getMimeType(httpServletRequest.getRequestURI()); - httpServletResponse.setContentType(mimeType); - - // 读取缓存文件的内容,写入客户端浏览器 - Reader ins = new InputStreamReader(new FileInputStream(cacheFile), "UTF-8"); - StringBuffer buffer = new StringBuffer(); - char[] cbuf = new char[1024]; - int len; - while ((len = ins.read(cbuf)) > -1) { - buffer.append(cbuf, 0, len); - } - ins.close(); - // 输出到客户端 - httpServletResponse.getWriter().write(buffer.toString()); - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/CharacterEncodingFilter.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/CharacterEncodingFilter.java deleted file mode 100644 index 5fecbb22..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/CharacterEncodingFilter.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.github.dunwu.javaee.filter; - -import io.github.dunwu.javaee.filter.wrapper.UploadRequestWrapper; -import org.apache.commons.lang3.StringUtils; - -import java.io.IOException; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class CharacterEncodingFilter extends MyFilter { - - private String characterEncoding; - private boolean enabled; - - @Override - public void init(FilterConfig config) throws ServletException { - super.init(config); - - characterEncoding = config.getInitParameter("characterEncoding"); - enabled = "true".equalsIgnoreCase(characterEncoding.trim()) - || "1".equalsIgnoreCase(characterEncoding.trim()); - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, - FilterChain chain) throws IOException, ServletException { - logger.info("{} 开始做过滤处理", this.getClass().getName()); - - if (enabled || StringUtils.isNotBlank(characterEncoding)) { - request.setCharacterEncoding(characterEncoding); - response.setCharacterEncoding(characterEncoding); - } - - logger.info("系统设置HTTP请求和应答的默认编码为 {}", characterEncoding); - chain.doFilter(request, response); - } - - public static class UploadFilter implements Filter { - - public void destroy() { - - } - - public void doFilter(ServletRequest request, ServletResponse response, - FilterChain chain) throws IOException, ServletException { - - UploadRequestWrapper uploadRequest = new UploadRequestWrapper( - (HttpServletRequest) request); - - chain.doFilter(uploadRequest, response); - - } - - public void init(FilterConfig filterConfig) throws ServletException { - - } - - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/ExceptionHandlerFilter.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/ExceptionHandlerFilter.java deleted file mode 100644 index d0c9e166..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/ExceptionHandlerFilter.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.github.dunwu.javaee.filter; - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; - -import io.github.dunwu.javaee.filter.exception.AccountException; -import io.github.dunwu.javaee.filter.exception.BusinessException; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class ExceptionHandlerFilter extends MyFilter { - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - - - try { - chain.doFilter(request, response); - } catch (Exception e) { - logger.info("{} 捕捉到异常", this.getClass().getName()); - Throwable rootCause = e; - - while (rootCause.getCause() != null) { - rootCause = rootCause.getCause(); - } - - String message = rootCause.getMessage(); - - message = message == null ? "异常:" + rootCause.getClass().getName() : message; - - request.setAttribute("message", message); - request.setAttribute("e", e); - - if (rootCause instanceof AccountException) { - request.getRequestDispatcher("/views/jsp/accountException.jsp").forward(request, - response); - } else if (rootCause instanceof BusinessException) { - request.getRequestDispatcher("/views/jsp/businessException.jsp").forward(request, - response); - } else { - request.getRequestDispatcher("/views/jsp/exception.jsp").forward(request, response); - } - } - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/FilterImpl.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/FilterImpl.java deleted file mode 100644 index b47466e2..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/FilterImpl.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.github.dunwu.javaee.filter; - -import java.io.IOException; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class FilterImpl implements Filter { - - private boolean enable; - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // 初始化代码 - enable = "true".equals(filterConfig.getInitParameter("enable")); - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, - FilterChain chain) throws IOException, ServletException { - - System.out.println("befor doFilter(). "); - - chain.doFilter(request, response); - - System.out.println("after doFitler(). "); - - } - - @Override - public void destroy() { - // 资源销毁代码 - } -} - diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/GZipFilter.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/GZipFilter.java deleted file mode 100644 index 8b272274..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/GZipFilter.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.github.dunwu.javaee.filter; - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import io.github.dunwu.javaee.filter.wrapper.GZipResponseWrapper; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class GZipFilter extends MyFilter { - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - - logger.info("{} 开始做过滤处理", this.getClass().getName()); - - HttpServletRequest httpServletRequest = (HttpServletRequest) request; - HttpServletResponse httpServletResponse = (HttpServletResponse) response; - - String acceptEncoding = httpServletRequest.getHeader("Accept-Encoding"); - System.out.println("Accept-Encoding: " + acceptEncoding); - - if (acceptEncoding != null && acceptEncoding.toLowerCase().indexOf("gzip") != -1) { - - // 如果客户浏览器支持 GZIP 格式, 则使用 GZIP 压缩数据 - GZipResponseWrapper gzipResponse = new GZipResponseWrapper(httpServletResponse); - chain.doFilter(httpServletRequest, gzipResponse); - - // 输出压缩数据 - gzipResponse.finishResponse(); - - } else { - // 否则, 不压缩 - chain.doFilter(httpServletRequest, httpServletResponse); - } - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/ImageRedirectFilter.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/ImageRedirectFilter.java deleted file mode 100644 index b71387fa..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/ImageRedirectFilter.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.github.dunwu.javaee.filter; - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class ImageRedirectFilter extends MyFilter { - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - - logger.info("{} 开始做过滤处理", this.getClass().getName()); - - HttpServletRequest httpServletRequest = (HttpServletRequest) request; - HttpServletResponse httpServletResponse = (HttpServletResponse) response; - - // 禁止缓存 - httpServletResponse.setHeader("Cache-Control", "no-store"); - httpServletResponse.setHeader("Pragrma", "no-cache"); - httpServletResponse.setDateHeader("Expires", 0); - - // 链接来源地址 - String referer = httpServletRequest.getHeader("referer"); - - if (referer == null || !referer.contains(httpServletRequest.getServerName())) { - // 如果 链接地址来自其他网站,则返回错误图片 - httpServletRequest.getRequestDispatcher("/views/images/error.gif").forward(httpServletRequest, httpServletResponse); - - } else { - // 图片正常显示 - chain.doFilter(httpServletRequest, httpServletResponse); - } - - } - -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/LogFilter.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/LogFilter.java deleted file mode 100644 index 7f0b2feb..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/LogFilter.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.github.dunwu.javaee.filter; - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class LogFilter extends MyFilter { - @Override - public void doFilter(ServletRequest req, ServletResponse res, - FilterChain chain) throws IOException, ServletException { - - HttpServletRequest request = (HttpServletRequest) req; - HttpServletResponse response = (HttpServletResponse) res; - - long startTime = System.currentTimeMillis(); - String requestURI = request.getRequestURI(); - - requestURI = request.getQueryString() == null ? requestURI - : (requestURI + "?" + request.getQueryString()); - - chain.doFilter(request, response); - - long endTime = System.currentTimeMillis(); - - logger.info("{} 访问了 {},总用时 {} 毫秒", request.getRemoteAddr(), requestURI, - (endTime - startTime)); - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/MyFilter.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/MyFilter.java deleted file mode 100644 index e328b019..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/MyFilter.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * - */ -package io.github.dunwu.javaee.filter; - -import java.io.IOException; - -import javax.servlet.Filter; -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public abstract class MyFilter implements Filter { - protected final Logger logger = LoggerFactory.getLogger(this.getClass()); - - private String filterName; - @Override - public void init(FilterConfig filterConfig) throws ServletException { - // 获取 Filter 的 name,配置在 web.xml 中 - filterName = filterConfig.getFilterName(); - logger.info("启动 Filter: {}", filterName); - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - } - - @Override - public void destroy() { - logger.info("关闭 Filter: {}", filterName); - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/OutputReplaceFilter.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/OutputReplaceFilter.java deleted file mode 100644 index b3a18566..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/OutputReplaceFilter.java +++ /dev/null @@ -1,62 +0,0 @@ -package io.github.dunwu.javaee.filter; - -import java.io.FileInputStream; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.Properties; - -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletResponse; - -import io.github.dunwu.javaee.filter.wrapper.HttpCharacterResponseWrapper; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class OutputReplaceFilter extends MyFilter { - - private Properties properties = new Properties(); - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - super.init(filterConfig); - String file = filterConfig.getInitParameter("file"); - String realPath = filterConfig.getServletContext().getRealPath(file); - try { - properties.load(new FileInputStream(realPath)); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - logger.info("{} 开始做过滤处理", this.getClass().getName()); - - // 自定义的 response - HttpCharacterResponseWrapper wrapper = new HttpCharacterResponseWrapper((HttpServletResponse) response); - - // 提交给 Servlet 或者下一个 Filter - chain.doFilter(request, wrapper); - - // 得到缓存在自定义 response 中的输出内容 - String output = wrapper.getCharArrayWriter().toString(); - - // 修改,替换 - for (Object obj : properties.keySet()) { - String key = (String) obj; - output = output.replace(key, properties.getProperty(key)); - } - - // 输出 - PrintWriter out = response.getWriter(); - out.write(output); - out.println(""); - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/PrivilegeFilter.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/PrivilegeFilter.java deleted file mode 100644 index af1b09cf..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/PrivilegeFilter.java +++ /dev/null @@ -1,83 +0,0 @@ -package io.github.dunwu.javaee.filter; - -import java.io.FileInputStream; -import java.io.IOException; -import java.util.Properties; - -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; - -import io.github.dunwu.javaee.filter.exception.AccountException; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class PrivilegeFilter extends MyFilter { - - private Properties pp = new Properties(); - - @Override - public void init(FilterConfig config) throws ServletException { - - // 从 初始化参数 中获取权 限配置文件 的位置 - String file = config.getInitParameter("file"); - String realPath = config.getServletContext().getRealPath(file); - try { - pp.load(new FileInputStream(realPath)); - } catch (Exception e) { - config.getServletContext().log("读取权限控制文件失败。", e); - } - } - - @Override - public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) - throws IOException, ServletException { - - logger.info("{} 开始做过滤处理", this.getClass().getName()); - - HttpServletRequest request = (HttpServletRequest) req; - - // 获取访问的路径,例如:admin.jsp - String requestURI = request.getRequestURI().replace(request.getContextPath() + "/", ""); - - // 获取 action 参数,例如:add - String action = req.getParameter("action"); - action = action == null ? "" : action; - - // 拼接成 URI。例如:log.do?action=list - String uri = requestURI + "?action=" + action; - - // 从 session 中获取用户权限角色。 - String role = (String) request.getSession(true).getAttribute("role"); - role = role == null ? "guest" : role; - - boolean authentificated = false; - // 开始检查该用户角色是否有权限访问 uri - for (Object obj : pp.keySet()) { - String key = ((String) obj); - // 使用正则表达式验证 需要将 ? . 替换一下,并将通配符 * 处理一下 - if (uri.matches(key.replace("?", "\\?").replace(".", "\\.").replace("*", ".*"))) { - // 如果 role 匹配 - if (role.equals(pp.get(key))) { - authentificated = true; - break; - } - } - } - if (!authentificated) { - throw new RuntimeException(new AccountException("您无权访问该页面。请以合适的身份登陆后查看。")); - } - // 继续运行 - chain.doFilter(req, res); - } - - @Override - public void destroy() { - pp = null; - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/UploadFilter.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/UploadFilter.java deleted file mode 100644 index 715d450c..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/UploadFilter.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.github.dunwu.javaee.filter; - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; - -import io.github.dunwu.javaee.filter.wrapper.UploadRequestWrapper; - -/** - * @author Zhang Peng - * @date 2017/4/4. - */ -public class UploadFilter extends MyFilter { - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - UploadRequestWrapper uploadRequest = new UploadRequestWrapper((HttpServletRequest) request); - chain.doFilter(uploadRequest, response); - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/WaterMarkFilter.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/WaterMarkFilter.java deleted file mode 100644 index 3cbc577e..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/WaterMarkFilter.java +++ /dev/null @@ -1,53 +0,0 @@ -package io.github.dunwu.javaee.filter; - -import java.io.IOException; - -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import io.github.dunwu.javaee.filter.wrapper.WaterMarkResponseWrapper; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class WaterMarkFilter extends MyFilter { - // 水印图片,配置在初始化参数中 - private String waterMarkFile; - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - super.init(filterConfig); - String file = filterConfig.getInitParameter("waterMarkFile"); - waterMarkFile = filterConfig.getServletContext().getRealPath(file); - } - - @Override - public void doFilter(ServletRequest req, ServletResponse res, - FilterChain chain) throws IOException, ServletException { - logger.info("{} 开始做过滤处理", this.getClass().getName()); - - HttpServletRequest request = (HttpServletRequest) req; - HttpServletResponse response = (HttpServletResponse) res; - - String requestURI = request.getRequestURI(); - - String originImageFile = request.getServletContext().getRealPath("/") + requestURI; - - // 自定义的response - WaterMarkResponseWrapper waterMarkRes = new WaterMarkResponseWrapper( - response, originImageFile, waterMarkFile); - - chain.doFilter(request, waterMarkRes); - - // 打水印,输出到客户端浏览器 - waterMarkRes.finishResponse(); - - logger.info("图片 {} 已添加水印图片 {}", originImageFile, waterMarkFile); - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/XSLTFilter.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/XSLTFilter.java deleted file mode 100644 index 4e56cab9..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/XSLTFilter.java +++ /dev/null @@ -1,75 +0,0 @@ -package io.github.dunwu.javaee.filter; - -import java.io.CharArrayWriter; -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.FilterChain; -import javax.servlet.FilterConfig; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.ServletRequest; -import javax.servlet.ServletResponse; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.xml.transform.Source; -import javax.xml.transform.Transformer; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.stream.StreamResult; -import javax.xml.transform.stream.StreamSource; - -/** - * @author Zhang Peng - * @date 2017/3/28. - */ -public class XSLTFilter extends MyFilter { - - private ServletContext servletContext; - - @Override - public void init(FilterConfig filterConfig) throws ServletException { - super.init(filterConfig); - servletContext = filterConfig.getServletContext(); - } - - @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) - throws IOException, ServletException { - - logger.info("{} 开始做过滤处理", this.getClass().getName()); - - HttpServletRequest httpServletRequest = (HttpServletRequest) request; - HttpServletResponse httpServletResponse = (HttpServletResponse) response; - - // 格式样本文件:/book.xsl - Source styleSource = new StreamSource(servletContext.getRealPath("/views/xml/messageLog.xsl")); - - // 请求的 xml 文件 - Source xmlSource = new StreamSource(servletContext - .getRealPath(httpServletRequest.getRequestURI().replace(httpServletRequest.getContextPath() + "", ""))); - try { - - // 转换器工厂 - TransformerFactory transformerFactory = TransformerFactory.newInstance(); - - // 转换器 - Transformer transformer = transformerFactory.newTransformer(styleSource); - - // 将转换的结果保存到该对象中 - CharArrayWriter charArrayWriter = new CharArrayWriter(); - StreamResult result = new StreamResult(charArrayWriter); - - // 转换 - transformer.transform(xmlSource, result); - - // 输出转换后的结果 - httpServletResponse.setContentType("text/html"); - httpServletResponse.setContentLength(charArrayWriter.toString().length()); - PrintWriter out = httpServletResponse.getWriter(); - out.write(charArrayWriter.toString()); - - } catch (Exception e) { - e.printStackTrace(); - } - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/exception/AccountException.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/exception/AccountException.java deleted file mode 100644 index 96a6d42b..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/exception/AccountException.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.dunwu.javaee.filter.exception; - -public class AccountException extends Exception { - - private static final long serialVersionUID = -3040955562136599570L; - - public AccountException(String msg) { - super(msg); - } - -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/exception/BusinessException.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/exception/BusinessException.java deleted file mode 100644 index c3662556..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/exception/BusinessException.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.github.dunwu.javaee.filter.exception; - -public class BusinessException extends Exception { - - private static final long serialVersionUID = -3040955562136599570L; - - public BusinessException(String msg) { - super(msg); - } - -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/test/Download.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/test/Download.java deleted file mode 100644 index e45591ee..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/test/Download.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.github.dunwu.javaee.filter.test; - -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.net.URL; - -public class Download { - - public static String getContent(String url) throws Exception { - - URL r = new URL(url); - - r.openConnection(); - - InputStream ins = r.openStream(); - - Reader reader = new InputStreamReader(ins); - - int len = 0; - char[] tmp = new char[1024]; - - StringBuffer buffer = new StringBuffer(); - - while ((len = reader.read(tmp)) != -1) { - buffer.append(tmp, 0, len); - } - - return buffer.toString(); - } - - public static void main(String[] args) throws Exception { - System.out.println(getContent("http://localhost:8080/filter/book/thinkInJava.xml")); - } - -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/test/GZipTest.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/test/GZipTest.java deleted file mode 100644 index 58ac708f..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/test/GZipTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.github.dunwu.javaee.filter.test; - -import java.net.URL; -import java.net.URLConnection; -import java.text.NumberFormat; - -public class GZipTest { - - public static void test(String url) throws Exception { - - /** 支持 GZIP 的连接 */ - URLConnection connGzip = new URL(url).openConnection(); - connGzip.setRequestProperty("Accept-Encoding", "gzip"); - int lengthGzip = connGzip.getContentLength(); - - /** 不支持 GZIP 的连接 */ - URLConnection connCommon = new URL(url).openConnection(); - int lengthCommon = connCommon.getContentLength(); - - double rate = new Double(lengthGzip) / lengthCommon; - - System.out.println("网址: " + url); - System.out.println("压缩后: " + lengthGzip + " byte, \t压缩前: " - + lengthCommon + " byte, \t比率: " - + NumberFormat.getPercentInstance().format(rate)); - System.out.println(); - } - - public static void main(String[] args) throws Exception { - test("http://localhost:8080/filter/dojo/dojo.js"); - test("http://localhost:8080/filter/image.jsp"); - test("http://localhost:8080/filter/winter.jpg"); - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/CacheResponseWrapper.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/CacheResponseWrapper.java deleted file mode 100644 index 5024bd34..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/CacheResponseWrapper.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.dunwu.javaee.filter.wrapper; - -import java.io.CharArrayWriter; -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class CacheResponseWrapper extends HttpServletResponseWrapper { - - // 缓存字符类输出 - private CharArrayWriter cacheWriter = new CharArrayWriter(); - - public CacheResponseWrapper(HttpServletResponse response) throws IOException { - super(response); - } - - @Override - public PrintWriter getWriter() throws IOException { - return new PrintWriter(cacheWriter); - } - - @Override - public void flushBuffer() throws IOException { - cacheWriter.flush(); - } - - public void finishResponse() throws IOException { - cacheWriter.close(); - } - - public CharArrayWriter getCacheWriter() { - return cacheWriter; - } - - public void setCacheWriter(CharArrayWriter cacheWriter) { - this.cacheWriter = cacheWriter; - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/GZipOutputStream.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/GZipOutputStream.java deleted file mode 100644 index 2a025044..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/GZipOutputStream.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.github.dunwu.javaee.filter.wrapper; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.zip.GZIPOutputStream; - -import javax.servlet.ServletOutputStream; -import javax.servlet.WriteListener; -import javax.servlet.http.HttpServletResponse; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class GZipOutputStream extends ServletOutputStream { - - private HttpServletResponse response; - - // JDK 自带的压缩数据的类 - private GZIPOutputStream gzipOutputStream; - - // 将压缩后的数据存放到 ByteArrayOutputStream 对象中 - private ByteArrayOutputStream byteArrayOutputStream; - - public GZipOutputStream(HttpServletResponse response) throws IOException { - this.response = response; - byteArrayOutputStream = new ByteArrayOutputStream(); - gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream); - } - - public void write(int b) throws IOException { - gzipOutputStream.write(b); - } - - public void close() throws IOException { - - // 压缩完毕 一定要调用该方法 - gzipOutputStream.finish(); - - // 将压缩后的数据输出到客户端 - byte[] content = byteArrayOutputStream.toByteArray(); - - // 设定压缩方式为 GZIP, 客户端浏览器会自动将数据解压 - response.addHeader("Content-Encoding", "gzip"); - response.addHeader("Content-Length", Integer.toString(content.length)); - - // 输出 - ServletOutputStream out = response.getOutputStream(); - out.write(content); - out.close(); - } - - public void flush() throws IOException { - gzipOutputStream.flush(); - } - - public void write(byte[] b, int off, int len) throws IOException { - gzipOutputStream.write(b, off, len); - } - - public void write(byte[] b) throws IOException { - gzipOutputStream.write(b); - } - - @Override - public boolean isReady() { - return false; - } - - @Override - public void setWriteListener(WriteListener writeListener) { - - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/GZipResponseWrapper.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/GZipResponseWrapper.java deleted file mode 100644 index e564c82e..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/GZipResponseWrapper.java +++ /dev/null @@ -1,58 +0,0 @@ -package io.github.dunwu.javaee.filter.wrapper; - -import java.io.IOException; -import java.io.OutputStreamWriter; -import java.io.PrintWriter; - -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class GZipResponseWrapper extends HttpServletResponseWrapper { - - // 默认的 response - private HttpServletResponse response; - - // 自定义的 outputStream, 执行close()的时候对数据压缩,并输出 - private GZipOutputStream gzipOutputStream; - - // 自定义 printWriter,将内容输出到 GZipOutputStream 中 - private PrintWriter writer; - - public GZipResponseWrapper(HttpServletResponse response) throws IOException { - super(response); - this.response = response; - } - - public ServletOutputStream getOutputStream() throws IOException { - if (gzipOutputStream == null) - gzipOutputStream = new GZipOutputStream(response); - return gzipOutputStream; - } - - public PrintWriter getWriter() throws IOException { - if (writer == null) - writer = new PrintWriter(new OutputStreamWriter( - new GZipOutputStream(response), "UTF-8")); - return writer; - } - - // 压缩后数据长度会发生变化 因此将该方法内容置空 - public void setContentLength(int contentLength) { - } - - public void flushBuffer() throws IOException { - gzipOutputStream.flush(); - } - - public void finishResponse() throws IOException { - if (gzipOutputStream != null) - gzipOutputStream.close(); - if (writer != null) - writer.close(); - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/HttpCharacterResponseWrapper.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/HttpCharacterResponseWrapper.java deleted file mode 100644 index 07e1ecdf..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/HttpCharacterResponseWrapper.java +++ /dev/null @@ -1,30 +0,0 @@ -package io.github.dunwu.javaee.filter.wrapper; - -import java.io.CharArrayWriter; -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class HttpCharacterResponseWrapper extends HttpServletResponseWrapper { - - private CharArrayWriter charArrayWriter = new CharArrayWriter(); - - public HttpCharacterResponseWrapper(HttpServletResponse response) { - super(response); - } - - @Override - public PrintWriter getWriter() throws IOException { - return new PrintWriter(charArrayWriter); - } - - public CharArrayWriter getCharArrayWriter() { - return charArrayWriter; - } -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/UploadRequestWrapper.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/UploadRequestWrapper.java deleted file mode 100644 index 654e34fe..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/UploadRequestWrapper.java +++ /dev/null @@ -1,110 +0,0 @@ -package io.github.dunwu.javaee.filter.wrapper; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStream; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletRequestWrapper; - -import org.apache.commons.fileupload.DiskFileUpload; -import org.apache.commons.fileupload.FileItem; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class UploadRequestWrapper extends HttpServletRequestWrapper { - - private static final String MULTIPART_HEADER = "Content-type"; - - // 是否是上传文件 - private boolean multipart; - - // map,保存所有的域 - private Map params = new HashMap(); - - @SuppressWarnings("all") - public UploadRequestWrapper(HttpServletRequest request) { - - super(request); - - // 判断是否为上传文件 - multipart = request.getHeader(MULTIPART_HEADER) != null - && request.getHeader(MULTIPART_HEADER).startsWith("multipart/form-data"); - - if (multipart) { - - try { - // 使用apache的工具解析 - DiskFileUpload upload = new DiskFileUpload(); - upload.setHeaderEncoding("utf8"); - - // 解析,获得所有的文本域与文件域 - List fileItems = upload.parseRequest(request); - - for (Iterator it = fileItems.iterator(); it.hasNext();) { - - // 遍历 - FileItem item = it.next(); - if (item.isFormField()) { - - // 如果是文本域,直接放到map里 - params.put(item.getFieldName(), item.getString("utf8")); - - } else { - - // 否则,为文件,先获取文件名称 - String filename = item.getName().replace("\\", "/"); - filename = filename.substring(filename.lastIndexOf("/") + 1); - - // 保存到系统临时文件夹中 - File file = new File(System.getProperty("java.io.tmpdir"), filename); - - // 保存文件内容 - OutputStream ous = new FileOutputStream(file); - ous.write(item.get()); - ous.close(); - - // 放到map中 - params.put(item.getFieldName(), file); - } - } - - } catch (Exception e) { - e.printStackTrace(); - } - } - } - - @Override - public Object getAttribute(String name) { - - // 如果为上传文件,则从map中取值 - if (multipart && params.containsKey(name)) { - return params.get(name); - } - return super.getAttribute(name); - } - - @Override - public String getParameter(String name) { - - // 如果为上传文件,则从map中取值 - if (multipart && params.containsKey(name)) { - return params.get(name).toString(); - } - return super.getParameter(name); - } - - public static void main(String[] args) { - - System.out.println(System.getProperties().toString().replace(", ", "\r\n")); - - } - -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/WaterMarkOutputStream.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/WaterMarkOutputStream.java deleted file mode 100644 index 50c56bf3..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/WaterMarkOutputStream.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.github.dunwu.javaee.filter.wrapper; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -import javax.servlet.ServletOutputStream; -import javax.servlet.WriteListener; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class WaterMarkOutputStream extends ServletOutputStream { - - // 缓冲图片数据 - private ByteArrayOutputStream byteArrayOutputStream; - - public WaterMarkOutputStream() throws IOException { - byteArrayOutputStream = new ByteArrayOutputStream(); - } - - @Override - public boolean isReady() { - return false; - } - - @Override - public void setWriteListener(WriteListener writeListener) { - - } - - public void write(int b) throws IOException { - byteArrayOutputStream.write(b); - } - - public void close() throws IOException { - byteArrayOutputStream.close(); - } - - public void flush() throws IOException { - byteArrayOutputStream.flush(); - } - - public void write(byte[] b, int off, int len) throws IOException { - byteArrayOutputStream.write(b, off, len); - } - - public void write(byte[] b) throws IOException { - byteArrayOutputStream.write(b); - } - - public ByteArrayOutputStream getByteArrayOutputStream() { - return byteArrayOutputStream; - } - -} diff --git a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/WaterMarkResponseWrapper.java b/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/WaterMarkResponseWrapper.java deleted file mode 100644 index e3f6589b..00000000 --- a/codes/javaee/filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/WaterMarkResponseWrapper.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.github.dunwu.javaee.filter.wrapper; - -import java.awt.image.BufferedImage; -import java.io.FileInputStream; -import java.io.IOException; - -import javax.imageio.ImageIO; -import javax.servlet.ServletOutputStream; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletResponseWrapper; - -import net.coobird.thumbnailator.Thumbnails; -import net.coobird.thumbnailator.geometry.Positions; - -/** - * @author Zhang Peng - * @date 2017/3/27. - */ -public class WaterMarkResponseWrapper extends HttpServletResponseWrapper { - - private String originFile; - // 水印图片位置 - private String waterMarkFile; - - // 原response - private HttpServletResponse response; - - // 自定义servletOutputStream,用于缓冲图像数据 - private WaterMarkOutputStream waterMarkOutputStream; - - public WaterMarkResponseWrapper(HttpServletResponse response, String originFile, String waterMarkFile) - throws IOException { - super(response); - this.response = response; - this.originFile = originFile; - this.waterMarkFile = waterMarkFile; - this.waterMarkOutputStream = new WaterMarkOutputStream(); - } - - // 覆盖getOutputStream(),返回自定义的waterMarkOutputStream - public ServletOutputStream getOutputStream() throws IOException { - return waterMarkOutputStream; - } - - public void flushBuffer() throws IOException { - waterMarkOutputStream.flush(); - } - - // 将图像数据打水印,并输出到客户端浏览器 - public void finishResponse() throws IOException { - FileInputStream fileInputStream = new FileInputStream(waterMarkFile); - BufferedImage wartermarkImage = ImageIO.read(fileInputStream); - Thumbnails.Builder builder = Thumbnails.of(this.originFile); - builder.scale(1.0, 1.0); - builder.watermark(Positions.BOTTOM_RIGHT, wartermarkImage, 0.5f); - - // 打水印后的图片数据 - builder.toOutputStream(response.getOutputStream()); - } -} diff --git a/codes/javaee/filter/src/main/resources/logback.xml b/codes/javaee/filter/src/main/resources/logback.xml deleted file mode 100644 index 25272410..00000000 --- a/codes/javaee/filter/src/main/resources/logback.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - logs/${FILE_NAME}-all.%d{yyyy-MM-dd}.log - 30 - - - - - 30MB - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - - - - - - - - - - diff --git a/codes/javaee/filter/src/main/webapp/WEB-INF/logo.png b/codes/javaee/filter/src/main/webapp/WEB-INF/logo.png deleted file mode 100644 index 39ac67c3..00000000 Binary files a/codes/javaee/filter/src/main/webapp/WEB-INF/logo.png and /dev/null differ diff --git a/codes/javaee/filter/src/main/webapp/WEB-INF/privilege.properties b/codes/javaee/filter/src/main/webapp/WEB-INF/privilege.properties deleted file mode 100644 index 424f61c0..00000000 --- a/codes/javaee/filter/src/main/webapp/WEB-INF/privilege.properties +++ /dev/null @@ -1,8 +0,0 @@ -# Privilege Settings -admin.do?action\=*=administrator -log.do?action\=*=administrator -list.do?action\=add=member -list.do?action\=delete=member -list.do?action\=save=member -list.do?action\=view=guest -list.do?action\=list=guest \ No newline at end of file diff --git a/codes/javaee/filter/src/main/webapp/WEB-INF/sensitive.properties b/codes/javaee/filter/src/main/webapp/WEB-INF/sensitive.properties deleted file mode 100644 index e3879e51..00000000 --- a/codes/javaee/filter/src/main/webapp/WEB-INF/sensitive.properties +++ /dev/null @@ -1,7 +0,0 @@ -# \u81ea\u52a8\u66f4\u6b63 -Chna=China -www.baidu.com.cn=www.baidu.com -# \u81ea\u52a8\u66ff\u6362 -\u8272\u60c5=** -\u60c5\u8272=** -\u8d4c\u535a=** \ No newline at end of file diff --git a/codes/javaee/filter/src/main/webapp/WEB-INF/web.xml b/codes/javaee/filter/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index fc153548..00000000 --- a/codes/javaee/filter/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,182 +0,0 @@ - - - - javaee-filter - - - - index.jsp - /views/jsp/index.jsp - - - index.jsp - / - - - - dispatcherServlet - /views/jsp/dispatcher.jsp - - - dispatcherServlet - *.do - - - - - - CacheFilter - - io.github.dunwu.javaee.filter.CacheFilter - - - cache - true - - - cacheTime - 1000000 - - - - CacheFilter - *.jsp - *.html - *.do - REQUEST - - - - characterEncodingFilter - - io.github.dunwu.javaee.filter.CharacterEncodingFilter - - - characterEncoding - UTF-8 - - - enable - true - - - - characterEncodingFilter - /* - - - - ExceptionHandlerFilter - - io.github.dunwu.javaee.filter.ExceptionHandlerFilter - - - - ExceptionHandlerFilter - /* - - - - GZipFilter - io.github.dunwu.javaee.filter.GZipFilter - - - GZipFilter - /* - - - - ImageRedirectFilter - - io.github.dunwu.javaee.filter.ImageRedirectFilter - - - - ImageRedirectFilter - /views/images/* - - - - LogFilter - io.github.dunwu.javaee.filter.LogFilter - - - LogFilter - /* - - - - OutputReplaceFilter - - io.github.dunwu.javaee.filter.OutputReplaceFilter - - - file - /WEB-INF/sensitive.properties - - - - OutputReplaceFilter - *.jsp - - - - UploadFilter - io.github.dunwu.javaee.filter.UploadFilter - - - UploadFilter - /* - - - - PrivilegeFilter - - io.github.dunwu.javaee.filter.PrivilegeFilter - - - file - /WEB-INF/privilege.properties - - - - PrivilegeFilter - *.do - - - - WaterMarkFilter - - io.github.dunwu.javaee.filter.WaterMarkFilter - - - waterMarkFile - /WEB-INF/logo.png - - - - WaterMarkFilter - *.jpg - *.png - *.bmp - - - - XSLTFilter - io.github.dunwu.javaee.filter.XSLTFilter - - - XSLTFilter - /views/xml/* - - - - - /views/jsp/index.html - /views/jsp/index.htm - /views/jsp/index.jsp - - - diff --git a/codes/javaee/filter/src/main/webapp/views/images/error.gif b/codes/javaee/filter/src/main/webapp/views/images/error.gif deleted file mode 100644 index b6922ac1..00000000 Binary files a/codes/javaee/filter/src/main/webapp/views/images/error.gif and /dev/null differ diff --git a/codes/javaee/filter/src/main/webapp/views/images/mm.jpg b/codes/javaee/filter/src/main/webapp/views/images/mm.jpg deleted file mode 100644 index 4c6de35c..00000000 Binary files a/codes/javaee/filter/src/main/webapp/views/images/mm.jpg and /dev/null differ diff --git a/codes/javaee/filter/src/main/webapp/views/images/sunset.jpg b/codes/javaee/filter/src/main/webapp/views/images/sunset.jpg deleted file mode 100644 index 860f6eec..00000000 Binary files a/codes/javaee/filter/src/main/webapp/views/images/sunset.jpg and /dev/null differ diff --git a/codes/javaee/filter/src/main/webapp/views/images/winter.jpg b/codes/javaee/filter/src/main/webapp/views/images/winter.jpg deleted file mode 100644 index 6db32ca1..00000000 Binary files a/codes/javaee/filter/src/main/webapp/views/images/winter.jpg and /dev/null differ diff --git a/codes/javaee/filter/src/main/webapp/views/js/dojo.js b/codes/javaee/filter/src/main/webapp/views/js/dojo.js deleted file mode 100644 index 4554967b..00000000 --- a/codes/javaee/filter/src/main/webapp/views/js/dojo.js +++ /dev/null @@ -1,6772 +0,0 @@ -/* - Copyright (c) 2004-2006, The Dojo Foundation - All Rights Reserved. - - Licensed under the Academic Free License version 2.1 or above OR the - modified BSD license. For more information on Dojo licensing, see: - - http://dojotoolkit.org/community/licensing.shtml - */ - -/* - This is a compiled version of Dojo, built for deployment and not for - development. To get an editable version, please visit: - - http://dojotoolkit.org - - for documentation and information on getting the source. - */ - -if (typeof dojo == "undefined") { - var dj_global = this; - var dj_currentContext = this; - - function dj_undef(_1, _2) { - return (typeof (_2 || dj_currentContext)[_1] == "undefined"); - } - - if (dj_undef("djConfig", this)) { - var djConfig = {}; - } - if (dj_undef("dojo", this)) { - var dojo = {}; - } - dojo.global = function () { - return dj_currentContext; - }; - dojo.locale = djConfig.locale; - dojo.version = { - major: 0, - minor: 4, - patch: 3, - flag: "-20070622", - revision: Number("$Rev: 8617 $".match(/[0-9]+/)[0]), - toString: function () { - with (dojo.version) { - return major + "." + minor + "." + patch + flag + " (" + revision + ")"; - } - } - }; - dojo.evalProp = function (_3, _4, _5) { - if ((!_4) || (!_3)) { - return undefined; - } - if (!dj_undef(_3, _4)) { - return _4[_3]; - } - return (_5 ? (_4[_3] = {}) : undefined); - }; - dojo.parseObjPath = function (_6, _7, _8) { - var _9 = (_7 || dojo.global()); - var _a = _6.split("."); - var _b = _a.pop(); - for (var i = 0, l = _a.length; i < l && _9; i++) { - _9 = dojo.evalProp(_a[i], _9, _8); - } - return {obj: _9, prop: _b}; - }; - dojo.evalObjPath = function (_e, _f) { - if (typeof _e != "string") { - return dojo.global(); - } - if (_e.indexOf(".") == -1) { - return dojo.evalProp(_e, dojo.global(), _f); - } - var ref = dojo.parseObjPath(_e, dojo.global(), _f); - if (ref) { - return dojo.evalProp(ref.prop, ref.obj, _f); - } - return null; - }; - dojo.errorToString = function (_11) { - if (!dj_undef("message", _11)) { - return _11.message; - } else { - if (!dj_undef("description", _11)) { - return _11.description; - } else { - return _11; - } - } - }; - dojo.raise = function (_12, _13) { - if (_13) { - _12 = _12 + ": " + dojo.errorToString(_13); - } else { - _12 = dojo.errorToString(_12); - } - try { - if (djConfig.isDebug) { - dojo.hostenv.println("FATAL exception raised: " + _12); - } - } - catch (e) { - } - throw _13 || Error(_12); - }; - dojo.debug = function () { - }; - dojo.debugShallow = function (obj) { - }; - dojo.profile = { - start: function () { - }, end: function () { - }, stop: function () { - }, dump: function () { - } - }; - function dj_eval(_15) { - return dj_global.eval ? dj_global.eval(_15) : eval(_15); - } - - dojo.unimplemented = function (_16, _17) { - var _18 = "'" + _16 + "' not implemented"; - if (_17 != null) { - _18 += " " + _17; - } - dojo.raise(_18); - }; - dojo.deprecated = function (_19, _1a, _1b) { - var _1c = "DEPRECATED: " + _19; - if (_1a) { - _1c += " " + _1a; - } - if (_1b) { - _1c += " -- will be removed in version: " + _1b; - } - dojo.debug(_1c); - }; - dojo.render = (function () { - function vscaffold(_1d, _1e) { - var tmp = {capable: false, support: {builtin: false, plugin: false}, prefixes: _1d}; - for (var i = 0; i < _1e.length; i++) { - tmp[_1e[i]] = false; - } - return tmp; - } - - return { - name: "", - ver: dojo.version, - os: {win: false, linux: false, osx: false}, - html: vscaffold(["html"], ["ie", "opera", "khtml", "safari", "moz"]), - svg: vscaffold(["svg"], ["corel", "adobe", "batik"]), - vml: vscaffold(["vml"], ["ie"]), - swf: vscaffold(["Swf", "Flash", "Mm"], ["mm"]), - swt: vscaffold(["Swt"], ["ibm"]) - }; - })(); - dojo.hostenv = (function () { - var _21 = { - isDebug: false, - allowQueryConfig: false, - baseScriptUri: "", - baseRelativePath: "", - libraryScriptUri: "", - iePreventClobber: false, - ieClobberMinimal: true, - preventBackButtonFix: true, - delayMozLoadingFix: false, - searchIds: [], - parseWidgets: true - }; - if (typeof djConfig == "undefined") { - djConfig = _21; - } else { - for (var _22 in _21) { - if (typeof djConfig[_22] == "undefined") { - djConfig[_22] = _21[_22]; - } - } - } - return { - name_: "(unset)", version_: "(unset)", getName: function () { - return this.name_; - }, getVersion: function () { - return this.version_; - }, getText: function (uri) { - dojo.unimplemented("getText", "uri=" + uri); - } - }; - })(); - dojo.hostenv.getBaseScriptUri = function () { - if (djConfig.baseScriptUri.length) { - return djConfig.baseScriptUri; - } - var uri = new String(djConfig.libraryScriptUri || djConfig.baseRelativePath); - if (!uri) { - dojo.raise("Nothing returned by getLibraryScriptUri(): " + uri); - } - var _25 = uri.lastIndexOf("/"); - djConfig.baseScriptUri = djConfig.baseRelativePath; - return djConfig.baseScriptUri; - }; - (function () { - var _26 = { - pkgFileName: "__package__", - loading_modules_: {}, - loaded_modules_: {}, - addedToLoadingCount: [], - removedFromLoadingCount: [], - inFlightCount: 0, - modulePrefixes_: {dojo: {name: "dojo", value: "src"}}, - setModulePrefix: function (_27, _28) { - this.modulePrefixes_[_27] = {name: _27, value: _28}; - }, - moduleHasPrefix: function (_29) { - var mp = this.modulePrefixes_; - return Boolean(mp[_29] && mp[_29].value); - }, - getModulePrefix: function (_2b) { - if (this.moduleHasPrefix(_2b)) { - return this.modulePrefixes_[_2b].value; - } - return _2b; - }, - getTextStack: [], - loadUriStack: [], - loadedUris: [], - post_load_: false, - modulesLoadedListeners: [], - unloadListeners: [], - loadNotifying: false - }; - for (var _2c in _26) { - dojo.hostenv[_2c] = _26[_2c]; - } - })(); - dojo.hostenv.loadPath = function (_2d, _2e, cb) { - var uri; - if (_2d.charAt(0) == "/" || _2d.match(/^\w+:/)) { - uri = _2d; - } else { - uri = this.getBaseScriptUri() + _2d; - } - if (djConfig.cacheBust && dojo.render.html.capable) { - uri += "?" + String(djConfig.cacheBust).replace(/\W+/g, ""); - } - try { - return !_2e ? this.loadUri(uri, cb) : this.loadUriAndCheck(uri, _2e, cb); - } - catch (e) { - dojo.debug(e); - return false; - } - }; - dojo.hostenv.loadUri = function (uri, cb) { - if (this.loadedUris[uri]) { - return true; - } - var _33 = this.getText(uri, null, true); - if (!_33) { - return false; - } - this.loadedUris[uri] = true; - if (cb) { - _33 = "(" + _33 + ")"; - } - var _34 = dj_eval(_33); - if (cb) { - cb(_34); - } - return true; - }; - dojo.hostenv.loadUriAndCheck = function (uri, _36, cb) { - var ok = true; - try { - ok = this.loadUri(uri, cb); - } - catch (e) { - dojo.debug("failed loading ", uri, " with error: ", e); - } - return Boolean(ok && this.findModule(_36, false)); - }; - dojo.loaded = function () { - }; - dojo.unloaded = function () { - }; - dojo.hostenv.loaded = function () { - this.loadNotifying = true; - this.post_load_ = true; - var mll = this.modulesLoadedListeners; - for (var x = 0; x < mll.length; x++) { - mll[x](); - } - this.modulesLoadedListeners = []; - this.loadNotifying = false; - dojo.loaded(); - }; - dojo.hostenv.unloaded = function () { - var mll = this.unloadListeners; - while (mll.length) { - (mll.pop())(); - } - dojo.unloaded(); - }; - dojo.addOnLoad = function (obj, _3d) { - var dh = dojo.hostenv; - if (arguments.length == 1) { - dh.modulesLoadedListeners.push(obj); - } else { - if (arguments.length > 1) { - dh.modulesLoadedListeners.push(function () { - obj[_3d](); - }); - } - } - if (dh.post_load_ && dh.inFlightCount == 0 && !dh.loadNotifying) { - dh.callLoaded(); - } - }; - dojo.addOnUnload = function (obj, _40) { - var dh = dojo.hostenv; - if (arguments.length == 1) { - dh.unloadListeners.push(obj); - } else { - if (arguments.length > 1) { - dh.unloadListeners.push(function () { - obj[_40](); - }); - } - } - }; - dojo.hostenv.modulesLoaded = function () { - if (this.post_load_) { - return; - } - if (this.loadUriStack.length == 0 && this.getTextStack.length == 0) { - if (this.inFlightCount > 0) { - dojo.debug("files still in flight!"); - return; - } - dojo.hostenv.callLoaded(); - } - }; - dojo.hostenv.callLoaded = function () { - if (typeof setTimeout == "object" || (djConfig["useXDomain"] && dojo.render.html.opera)) { - setTimeout("dojo.hostenv.loaded();", 0); - } else { - dojo.hostenv.loaded(); - } - }; - dojo.hostenv.getModuleSymbols = function (_42) { - var _43 = _42.split("."); - for (var i = _43.length; i > 0; i--) { - var _45 = _43.slice(0, i).join("."); - if ((i == 1) && !this.moduleHasPrefix(_45)) { - _43[0] = "../" + _43[0]; - } else { - var _46 = this.getModulePrefix(_45); - if (_46 != _45) { - _43.splice(0, i, _46); - break; - } - } - } - return _43; - }; - dojo.hostenv._global_omit_module_check = false; - dojo.hostenv.loadModule = function (_47, _48, _49) { - if (!_47) { - return; - } - _49 = this._global_omit_module_check || _49; - var _4a = this.findModule(_47, false); - if (_4a) { - return _4a; - } - if (dj_undef(_47, this.loading_modules_)) { - this.addedToLoadingCount.push(_47); - } - this.loading_modules_[_47] = 1; - var _4b = _47.replace(/\./g, "/") + ".js"; - var _4c = _47.split("."); - var _4d = this.getModuleSymbols(_47); - var _4e = ((_4d[0].charAt(0) != "/") && !_4d[0].match(/^\w+:/)); - var _4f = _4d[_4d.length - 1]; - var ok; - if (_4f == "*") { - _47 = _4c.slice(0, -1).join("."); - while (_4d.length) { - _4d.pop(); - _4d.push(this.pkgFileName); - _4b = _4d.join("/") + ".js"; - if (_4e && _4b.charAt(0) == "/") { - _4b = _4b.slice(1); - } - ok = this.loadPath(_4b, !_49 ? _47 : null); - if (ok) { - break; - } - _4d.pop(); - } - } else { - _4b = _4d.join("/") + ".js"; - _47 = _4c.join("."); - var _51 = !_49 ? _47 : null; - ok = this.loadPath(_4b, _51); - if (!ok && !_48) { - _4d.pop(); - while (_4d.length) { - _4b = _4d.join("/") + ".js"; - ok = this.loadPath(_4b, _51); - if (ok) { - break; - } - _4d.pop(); - _4b = _4d.join("/") + "/" + this.pkgFileName + ".js"; - if (_4e && _4b.charAt(0) == "/") { - _4b = _4b.slice(1); - } - ok = this.loadPath(_4b, _51); - if (ok) { - break; - } - } - } - if (!ok && !_49) { - dojo.raise("Could not load '" + _47 + "'; last tried '" + _4b + "'"); - } - } - if (!_49 && !this["isXDomain"]) { - _4a = this.findModule(_47, false); - if (!_4a) { - dojo.raise("symbol '" + _47 + "' is not defined after loading '" + _4b + "'"); - } - } - return _4a; - }; - dojo.hostenv.startPackage = function (_52) { - var _53 = String(_52); - var _54 = _53; - var _55 = _52.split(/\./); - if (_55[_55.length - 1] == "*") { - _55.pop(); - _54 = _55.join("."); - } - var _56 = dojo.evalObjPath(_54, true); - this.loaded_modules_[_53] = _56; - this.loaded_modules_[_54] = _56; - return _56; - }; - dojo.hostenv.findModule = function (_57, _58) { - var lmn = String(_57); - if (this.loaded_modules_[lmn]) { - return this.loaded_modules_[lmn]; - } - if (_58) { - dojo.raise("no loaded module named '" + _57 + "'"); - } - return null; - }; - dojo.kwCompoundRequire = function (_5a) { - var _5b = _5a["common"] || []; - var _5c = _5a[dojo.hostenv.name_] ? _5b.concat(_5a[dojo.hostenv.name_] || []) : _5b.concat(_5a["default"] || []); - for (var x = 0; x < _5c.length; x++) { - var _5e = _5c[x]; - if (_5e.constructor == Array) { - dojo.hostenv.loadModule.apply(dojo.hostenv, _5e); - } else { - dojo.hostenv.loadModule(_5e); - } - } - }; - dojo.require = function (_5f) { - dojo.hostenv.loadModule.apply(dojo.hostenv, arguments); - }; - dojo.requireIf = function (_60, _61) { - var _62 = arguments[0]; - if ((_62 === true) || (_62 == "common") || (_62 && dojo.render[_62].capable)) { - var _63 = []; - for (var i = 1; i < arguments.length; i++) { - _63.push(arguments[i]); - } - dojo.require.apply(dojo, _63); - } - }; - dojo.requireAfterIf = dojo.requireIf; - dojo.provide = function (_65) { - return dojo.hostenv.startPackage.apply(dojo.hostenv, arguments); - }; - dojo.registerModulePath = function (_66, _67) { - return dojo.hostenv.setModulePrefix(_66, _67); - }; - if (djConfig["modulePaths"]) { - for (var param in djConfig["modulePaths"]) { - dojo.registerModulePath(param, djConfig["modulePaths"][param]); - } - } - dojo.setModulePrefix = function (_68, _69) { - dojo.deprecated("dojo.setModulePrefix(\"" + _68 + "\", \"" + _69 + "\")", "replaced by dojo.registerModulePath", "0.5"); - return dojo.registerModulePath(_68, _69); - }; - dojo.exists = function (obj, _6b) { - var p = _6b.split("."); - for (var i = 0; i < p.length; i++) { - if (!obj[p[i]]) { - return false; - } - obj = obj[p[i]]; - } - return true; - }; - dojo.hostenv.normalizeLocale = function (_6e) { - var _6f = _6e ? _6e.toLowerCase() : dojo.locale; - if (_6f == "root") { - _6f = "ROOT"; - } - return _6f; - }; - dojo.hostenv.searchLocalePath = function (_70, _71, _72) { - _70 = dojo.hostenv.normalizeLocale(_70); - var _73 = _70.split("-"); - var _74 = []; - for (var i = _73.length; i > 0; i--) { - _74.push(_73.slice(0, i).join("-")); - } - _74.push(false); - if (_71) { - _74.reverse(); - } - for (var j = _74.length - 1; j >= 0; j--) { - var loc = _74[j] || "ROOT"; - var _78 = _72(loc); - if (_78) { - break; - } - } - }; - dojo.hostenv.localesGenerated; - dojo.hostenv.registerNlsPrefix = function () { - dojo.registerModulePath("nls", "nls"); - }; - dojo.hostenv.preloadLocalizations = function () { - if (dojo.hostenv.localesGenerated) { - dojo.hostenv.registerNlsPrefix(); - function preload(_79) { - _79 = dojo.hostenv.normalizeLocale(_79); - dojo.hostenv.searchLocalePath(_79, true, function (loc) { - for (var i = 0; i < dojo.hostenv.localesGenerated.length; i++) { - if (dojo.hostenv.localesGenerated[i] == loc) { - dojo["require"]("nls.dojo_" + loc); - return true; - } - } - return false; - }); - } - - preload(); - var _7c = djConfig.extraLocale || []; - for (var i = 0; i < _7c.length; i++) { - preload(_7c[i]); - } - } - dojo.hostenv.preloadLocalizations = function () { - }; - }; - dojo.requireLocalization = function (_7e, _7f, _80, _81) { - dojo.hostenv.preloadLocalizations(); - var _82 = dojo.hostenv.normalizeLocale(_80); - var _83 = [_7e, "nls", _7f].join("."); - var _84 = ""; - if (_81) { - var _85 = _81.split(","); - for (var i = 0; i < _85.length; i++) { - if (_82.indexOf(_85[i]) == 0) { - if (_85[i].length > _84.length) { - _84 = _85[i]; - } - } - } - if (!_84) { - _84 = "ROOT"; - } - } - var _87 = _81 ? _84 : _82; - var _88 = dojo.hostenv.findModule(_83); - var _89 = null; - if (_88) { - if (djConfig.localizationComplete && _88._built) { - return; - } - var _8a = _87.replace("-", "_"); - var _8b = _83 + "." + _8a; - _89 = dojo.hostenv.findModule(_8b); - } - if (!_89) { - _88 = dojo.hostenv.startPackage(_83); - var _8c = dojo.hostenv.getModuleSymbols(_7e); - var _8d = _8c.concat("nls").join("/"); - var _8e; - dojo.hostenv.searchLocalePath(_87, _81, function (loc) { - var _90 = loc.replace("-", "_"); - var _91 = _83 + "." + _90; - var _92 = false; - if (!dojo.hostenv.findModule(_91)) { - dojo.hostenv.startPackage(_91); - var _93 = [_8d]; - if (loc != "ROOT") { - _93.push(loc); - } - _93.push(_7f); - var _94 = _93.join("/") + ".js"; - _92 = dojo.hostenv.loadPath(_94, null, function (_95) { - var _96 = function () { - }; - _96.prototype = _8e; - _88[_90] = new _96(); - for (var j in _95) { - _88[_90][j] = _95[j]; - } - }); - } else { - _92 = true; - } - if (_92 && _88[_90]) { - _8e = _88[_90]; - } else { - _88[_90] = _8e; - } - if (_81) { - return true; - } - }); - } - if (_81 && _82 != _84) { - _88[_82.replace("-", "_")] = _88[_84.replace("-", "_")]; - } - }; - (function () { - var _98 = djConfig.extraLocale; - if (_98) { - if (!_98 instanceof Array) { - _98 = [_98]; - } - var req = dojo.requireLocalization; - dojo.requireLocalization = function (m, b, _9c, _9d) { - req(m, b, _9c, _9d); - if (_9c) { - return; - } - for (var i = 0; i < _98.length; i++) { - req(m, b, _98[i], _9d); - } - }; - } - })(); -} -if (typeof window != "undefined") { - (function () { - if (djConfig.allowQueryConfig) { - var _9f = document.location.toString(); - var _a0 = _9f.split("?", 2); - if (_a0.length > 1) { - var _a1 = _a0[1]; - var _a2 = _a1.split("&"); - for (var x in _a2) { - var sp = _a2[x].split("="); - if ((sp[0].length > 9) && (sp[0].substr(0, 9) == "djConfig.")) { - var opt = sp[0].substr(9); - try { - djConfig[opt] = eval(sp[1]); - } - catch (e) { - djConfig[opt] = sp[1]; - } - } - } - } - } - if (((djConfig["baseScriptUri"] == "") || (djConfig["baseRelativePath"] == "")) && (document && document.getElementsByTagName)) { - var _a6 = document.getElementsByTagName("script"); - var _a7 = /(__package__|dojo|bootstrap1)\.js([\?\.]|$)/i; - for (var i = 0; i < _a6.length; i++) { - var src = _a6[i].getAttribute("src"); - if (!src) { - continue; - } - var m = src.match(_a7); - if (m) { - var _ab = src.substring(0, m.index); - if (src.indexOf("bootstrap1") > -1) { - _ab += "../"; - } - if (!this["djConfig"]) { - djConfig = {}; - } - if (djConfig["baseScriptUri"] == "") { - djConfig["baseScriptUri"] = _ab; - } - if (djConfig["baseRelativePath"] == "") { - djConfig["baseRelativePath"] = _ab; - } - break; - } - } - } - var dr = dojo.render; - var drh = dojo.render.html; - var drs = dojo.render.svg; - var dua = (drh.UA = navigator.userAgent); - var dav = (drh.AV = navigator.appVersion); - var t = true; - var f = false; - drh.capable = t; - drh.support.builtin = t; - dr.ver = parseFloat(drh.AV); - dr.os.mac = dav.indexOf("Macintosh") >= 0; - dr.os.win = dav.indexOf("Windows") >= 0; - dr.os.linux = dav.indexOf("X11") >= 0; - drh.opera = dua.indexOf("Opera") >= 0; - drh.khtml = (dav.indexOf("Konqueror") >= 0) || (dav.indexOf("Safari") >= 0); - drh.safari = dav.indexOf("Safari") >= 0; - var _b3 = dua.indexOf("Gecko"); - drh.mozilla = drh.moz = (_b3 >= 0) && (!drh.khtml); - if (drh.mozilla) { - drh.geckoVersion = dua.substring(_b3 + 6, _b3 + 14); - } - drh.ie = (document.all) && (!drh.opera); - drh.ie50 = drh.ie && dav.indexOf("MSIE 5.0") >= 0; - drh.ie55 = drh.ie && dav.indexOf("MSIE 5.5") >= 0; - drh.ie60 = drh.ie && dav.indexOf("MSIE 6.0") >= 0; - drh.ie70 = drh.ie && dav.indexOf("MSIE 7.0") >= 0; - var cm = document["compatMode"]; - drh.quirks = (cm == "BackCompat") || (cm == "QuirksMode") || drh.ie55 || drh.ie50; - dojo.locale = dojo.locale || (drh.ie ? navigator.userLanguage : navigator.language).toLowerCase(); - dr.vml.capable = drh.ie; - drs.capable = f; - drs.support.plugin = f; - drs.support.builtin = f; - var _b5 = window["document"]; - var tdi = _b5["implementation"]; - if (drh.ie && (window.location.protocol == "file:")) { - djConfig.ieForceActiveXXhr = true; - } - if ((tdi) && (tdi["hasFeature"]) && (tdi.hasFeature("org.w3c.dom.svg", "1.0"))) { - drs.capable = t; - drs.support.builtin = t; - drs.support.plugin = f; - } - if (drh.safari) { - var tmp = dua.split("AppleWebKit/")[1]; - var ver = parseFloat(tmp.split(" ")[0]); - if (ver >= 420) { - drs.capable = t; - drs.support.builtin = t; - drs.support.plugin = f; - } - } else { - } - })(); - dojo.hostenv.startPackage("dojo.hostenv"); - dojo.render.name = dojo.hostenv.name_ = "browser"; - dojo.hostenv.searchIds = []; - dojo.hostenv._XMLHTTP_PROGIDS = ["Msxml2.XMLHTTP", "Microsoft.XMLHTTP", "Msxml2.XMLHTTP.4.0"]; - dojo.hostenv.getXmlhttpObject = function () { - var _b9 = null; - var _ba = null; - if (!dojo.render.html.ie || !djConfig.ieForceActiveXXhr) { - try { - _b9 = new XMLHttpRequest(); - } - catch (e) { - } - } - if (!_b9) { - for (var i = 0; i < 3; ++i) { - var _bc = dojo.hostenv._XMLHTTP_PROGIDS[i]; - try { - _b9 = new ActiveXObject(_bc); - } - catch (e) { - _ba = e; - } - if (_b9) { - dojo.hostenv._XMLHTTP_PROGIDS = [_bc]; - break; - } - } - } - if (!_b9) { - return dojo.raise("XMLHTTP not available", _ba); - } - return _b9; - }; - dojo.hostenv._blockAsync = false; - dojo.hostenv.getText = function (uri, _be, _bf) { - if (!_be) { - this._blockAsync = true; - } - var _c0 = this.getXmlhttpObject(); - - function isDocumentOk(_c1) { - var _c2 = _c1["status"]; - return Boolean((!_c2) || ((200 <= _c2) && (300 > _c2)) || (_c2 == 304)); - } - - if (_be) { - var _c3 = this, _c4 = null, gbl = dojo.global(); - var xhr = dojo.evalObjPath("dojo.io.XMLHTTPTransport"); - _c0.onreadystatechange = function () { - if (_c4) { - gbl.clearTimeout(_c4); - _c4 = null; - } - if (_c3._blockAsync || (xhr && xhr._blockAsync)) { - _c4 = gbl.setTimeout(function () { - _c0.onreadystatechange.apply(this); - }, 10); - } else { - if (4 == _c0.readyState) { - if (isDocumentOk(_c0)) { - _be(_c0.responseText); - } - } - } - }; - } - _c0.open("GET", uri, _be ? true : false); - try { - _c0.send(null); - if (_be) { - return null; - } - if (!isDocumentOk(_c0)) { - var err = Error("Unable to load " + uri + " status:" + _c0.status); - err.status = _c0.status; - err.responseText = _c0.responseText; - throw err; - } - } - catch (e) { - this._blockAsync = false; - if ((_bf) && (!_be)) { - return null; - } else { - throw e; - } - } - this._blockAsync = false; - return _c0.responseText; - }; - dojo.hostenv.defaultDebugContainerId = "dojoDebug"; - dojo.hostenv._println_buffer = []; - dojo.hostenv._println_safe = false; - dojo.hostenv.println = function (_c8) { - if (!dojo.hostenv._println_safe) { - dojo.hostenv._println_buffer.push(_c8); - } else { - try { - var _c9 = document.getElementById(djConfig.debugContainerId ? djConfig.debugContainerId : dojo.hostenv.defaultDebugContainerId); - if (!_c9) { - _c9 = dojo.body(); - } - var div = document.createElement("div"); - div.appendChild(document.createTextNode(_c8)); - _c9.appendChild(div); - } - catch (e) { - try { - document.write("

" + _c8 + "
"); - } - catch (e2) { - window.status = _c8; - } - } - } - }; - dojo.addOnLoad(function () { - dojo.hostenv._println_safe = true; - while (dojo.hostenv._println_buffer.length > 0) { - dojo.hostenv.println(dojo.hostenv._println_buffer.shift()); - } - }); - function dj_addNodeEvtHdlr(_cb, _cc, fp) { - var _ce = _cb["on" + _cc] || function () { - }; - _cb["on" + _cc] = function () { - fp.apply(_cb, arguments); - _ce.apply(_cb, arguments); - }; - return true; - } - - dojo.hostenv._djInitFired = false; - function dj_load_init(e) { - dojo.hostenv._djInitFired = true; - var _d0 = (e && e.type) ? e.type.toLowerCase() : "load"; - if (arguments.callee.initialized || (_d0 != "domcontentloaded" && _d0 != "load")) { - return; - } - arguments.callee.initialized = true; - if (typeof (_timer) != "undefined") { - clearInterval(_timer); - delete _timer; - } - var _d1 = function () { - if (dojo.render.html.ie) { - dojo.hostenv.makeWidgets(); - } - }; - if (dojo.hostenv.inFlightCount == 0) { - _d1(); - dojo.hostenv.modulesLoaded(); - } else { - dojo.hostenv.modulesLoadedListeners.unshift(_d1); - } - } - - if (document.addEventListener) { - if (dojo.render.html.opera || (dojo.render.html.moz && (djConfig["enableMozDomContentLoaded"] === true))) { - document.addEventListener("DOMContentLoaded", dj_load_init, null); - } - window.addEventListener("load", dj_load_init, null); - } - if (dojo.render.html.ie && dojo.render.os.win) { - document.attachEvent("onreadystatechange", function (e) { - if (document.readyState == "complete") { - dj_load_init(); - } - }); - } - if (/(WebKit|khtml)/i.test(navigator.userAgent)) { - var _timer = setInterval(function () { - if (/loaded|complete/.test(document.readyState)) { - dj_load_init(); - } - }, 10); - } - if (dojo.render.html.ie) { - dj_addNodeEvtHdlr(window, "beforeunload", function () { - dojo.hostenv._unloading = true; - window.setTimeout(function () { - dojo.hostenv._unloading = false; - }, 0); - }); - } - dj_addNodeEvtHdlr(window, "unload", function () { - dojo.hostenv.unloaded(); - if ((!dojo.render.html.ie) || (dojo.render.html.ie && dojo.hostenv._unloading)) { - dojo.hostenv.unloaded(); - } - }); - dojo.hostenv.makeWidgets = function () { - var _d3 = []; - if (djConfig.searchIds && djConfig.searchIds.length > 0) { - _d3 = _d3.concat(djConfig.searchIds); - } - if (dojo.hostenv.searchIds && dojo.hostenv.searchIds.length > 0) { - _d3 = _d3.concat(dojo.hostenv.searchIds); - } - if ((djConfig.parseWidgets) || (_d3.length > 0)) { - if (dojo.evalObjPath("dojo.widget.Parse")) { - var _d4 = new dojo.xml.Parse(); - if (_d3.length > 0) { - for (var x = 0; x < _d3.length; x++) { - var _d6 = document.getElementById(_d3[x]); - if (!_d6) { - continue; - } - var _d7 = _d4.parseElement(_d6, null, true); - dojo.widget.getParser().createComponents(_d7); - } - } else { - if (djConfig.parseWidgets) { - var _d7 = _d4.parseElement(dojo.body(), null, true); - dojo.widget.getParser().createComponents(_d7); - } - } - } - } - }; - dojo.addOnLoad(function () { - if (!dojo.render.html.ie) { - dojo.hostenv.makeWidgets(); - } - }); - try { - if (dojo.render.html.ie) { - document.namespaces.add("v", "urn:schemas-microsoft-com:vml"); - document.createStyleSheet().addRule("v\\:*", "behavior:url(#default#VML)"); - } - } - catch (e) { - } - dojo.hostenv.writeIncludes = function () { - }; - if (!dj_undef("document", this)) { - dj_currentDocument = this.document; - } - dojo.doc = function () { - return dj_currentDocument; - }; - dojo.body = function () { - return dojo.doc().body || dojo.doc().getElementsByTagName("body")[0]; - }; - dojo.byId = function (id, doc) { - if ((id) && ((typeof id == "string") || (id instanceof String))) { - if (!doc) { - doc = dj_currentDocument; - } - var ele = doc.getElementById(id); - if (ele && (ele.id != id) && doc.all) { - ele = null; - eles = doc.all[id]; - if (eles) { - if (eles.length) { - for (var i = 0; i < eles.length; i++) { - if (eles[i].id == id) { - ele = eles[i]; - break; - } - } - } else { - ele = eles; - } - } - } - return ele; - } - return id; - }; - dojo.setContext = function (_dc, _dd) { - dj_currentContext = _dc; - dj_currentDocument = _dd; - }; - dojo._fireCallback = function (_de, _df, _e0) { - if ((_df) && ((typeof _de == "string") || (_de instanceof String))) { - _de = _df[_de]; - } - return (_df ? _de.apply(_df, _e0 || []) : _de()); - }; - dojo.withGlobal = function (_e1, _e2, _e3, _e4) { - var _e5; - var _e6 = dj_currentContext; - var _e7 = dj_currentDocument; - try { - dojo.setContext(_e1, _e1.document); - _e5 = dojo._fireCallback(_e2, _e3, _e4); - } - finally { - dojo.setContext(_e6, _e7); - } - return _e5; - }; - dojo.withDoc = function (_e8, _e9, _ea, _eb) { - var _ec; - var _ed = dj_currentDocument; - try { - dj_currentDocument = _e8; - _ec = dojo._fireCallback(_e9, _ea, _eb); - } - finally { - dj_currentDocument = _ed; - } - return _ec; - }; -} -dojo.requireIf((djConfig["isDebug"] || djConfig["debugAtAllCosts"]), "dojo.debug"); -dojo.requireIf(djConfig["debugAtAllCosts"] && !window.widget && !djConfig["useXDomain"], "dojo.browser_debug"); -dojo.requireIf(djConfig["debugAtAllCosts"] && !window.widget && djConfig["useXDomain"], "dojo.browser_debug_xd"); -dojo.provide("dojo.string.common"); -dojo.string.trim = function (str, wh) { - if (!str.replace) { - return str; - } - if (!str.length) { - return str; - } - var re = (wh > 0) ? (/^\s+/) : (wh < 0) ? (/\s+$/) : (/^\s+|\s+$/g); - return str.replace(re, ""); -}; -dojo.string.trimStart = function (str) { - return dojo.string.trim(str, 1); -}; -dojo.string.trimEnd = function (str) { - return dojo.string.trim(str, -1); -}; -dojo.string.repeat = function (str, _f4, _f5) { - var out = ""; - for (var i = 0; i < _f4; i++) { - out += str; - if (_f5 && i < _f4 - 1) { - out += _f5; - } - } - return out; -}; -dojo.string.pad = function (str, len, c, dir) { - var out = String(str); - if (!c) { - c = "0"; - } - if (!dir) { - dir = 1; - } - while (out.length < len) { - if (dir > 0) { - out = c + out; - } else { - out += c; - } - } - return out; -}; -dojo.string.padLeft = function (str, len, c) { - return dojo.string.pad(str, len, c, 1); -}; -dojo.string.padRight = function (str, len, c) { - return dojo.string.pad(str, len, c, -1); -}; -dojo.provide("dojo.string"); -dojo.provide("dojo.lang.common"); -dojo.lang.inherits = function (_103, _104) { - if (!dojo.lang.isFunction(_104)) { - dojo.raise("dojo.inherits: superclass argument [" + _104 + "] must be a function (subclass: [" + _103 + "']"); - } - _103.prototype = new _104(); - _103.prototype.constructor = _103; - _103.superclass = _104.prototype; - _103["super"] = _104.prototype; -}; -dojo.lang._mixin = function (obj, _106) { - var tobj = {}; - for (var x in _106) { - if ((typeof tobj[x] == "undefined") || (tobj[x] != _106[x])) { - obj[x] = _106[x]; - } - } - if (dojo.render.html.ie && (typeof (_106["toString"]) == "function") && (_106["toString"] != obj["toString"]) && (_106["toString"] != tobj["toString"])) { - obj.toString = _106.toString; - } - return obj; -}; -dojo.lang.mixin = function (obj, _10a) { - for (var i = 1, l = arguments.length; i < l; i++) { - dojo.lang._mixin(obj, arguments[i]); - } - return obj; -}; -dojo.lang.extend = function (_10d, _10e) { - for (var i = 1, l = arguments.length; i < l; i++) { - dojo.lang._mixin(_10d.prototype, arguments[i]); - } - return _10d; -}; -dojo.inherits = dojo.lang.inherits; -dojo.mixin = dojo.lang.mixin; -dojo.extend = dojo.lang.extend; -dojo.lang.find = function (_111, _112, _113, _114) { - if (!dojo.lang.isArrayLike(_111) && dojo.lang.isArrayLike(_112)) { - dojo.deprecated("dojo.lang.find(value, array)", "use dojo.lang.find(array, value) instead", "0.5"); - var temp = _111; - _111 = _112; - _112 = temp; - } - var _116 = dojo.lang.isString(_111); - if (_116) { - _111 = _111.split(""); - } - if (_114) { - var step = -1; - var i = _111.length - 1; - var end = -1; - } else { - var step = 1; - var i = 0; - var end = _111.length; - } - if (_113) { - while (i != end) { - if (_111[i] === _112) { - return i; - } - i += step; - } - } else { - while (i != end) { - if (_111[i] == _112) { - return i; - } - i += step; - } - } - return -1; -}; -dojo.lang.indexOf = dojo.lang.find; -dojo.lang.findLast = function (_11a, _11b, _11c) { - return dojo.lang.find(_11a, _11b, _11c, true); -}; -dojo.lang.lastIndexOf = dojo.lang.findLast; -dojo.lang.inArray = function (_11d, _11e) { - return dojo.lang.find(_11d, _11e) > -1; -}; -dojo.lang.isObject = function (it) { - if (typeof it == "undefined") { - return false; - } - return (typeof it == "object" || it === null || dojo.lang.isArray(it) || dojo.lang.isFunction(it)); -}; -dojo.lang.isArray = function (it) { - return (it && it instanceof Array || typeof it == "array"); -}; -dojo.lang.isArrayLike = function (it) { - if ((!it) || (dojo.lang.isUndefined(it))) { - return false; - } - if (dojo.lang.isString(it)) { - return false; - } - if (dojo.lang.isFunction(it)) { - return false; - } - if (dojo.lang.isArray(it)) { - return true; - } - if ((it.tagName) && (it.tagName.toLowerCase() == "form")) { - return false; - } - if (dojo.lang.isNumber(it.length) && isFinite(it.length)) { - return true; - } - return false; -}; -dojo.lang.isFunction = function (it) { - return (it instanceof Function || typeof it == "function"); -}; -(function () { - if ((dojo.render.html.capable) && (dojo.render.html["safari"])) { - dojo.lang.isFunction = function (it) { - if ((typeof (it) == "function") && (it == "[object NodeList]")) { - return false; - } - return (it instanceof Function || typeof it == "function"); - }; - } -})(); -dojo.lang.isString = function (it) { - return (typeof it == "string" || it instanceof String); -}; -dojo.lang.isAlien = function (it) { - if (!it) { - return false; - } - return !dojo.lang.isFunction(it) && /\{\s*\[native code\]\s*\}/.test(String(it)); -}; -dojo.lang.isBoolean = function (it) { - return (it instanceof Boolean || typeof it == "boolean"); -}; -dojo.lang.isNumber = function (it) { - return (it instanceof Number || typeof it == "number"); -}; -dojo.lang.isUndefined = function (it) { - return ((typeof (it) == "undefined") && (it == undefined)); -}; -dojo.provide("dojo.lang.extras"); -dojo.lang.setTimeout = function (func, _12a) { - var _12b = window, _12c = 2; - if (!dojo.lang.isFunction(func)) { - _12b = func; - func = _12a; - _12a = arguments[2]; - _12c++; - } - if (dojo.lang.isString(func)) { - func = _12b[func]; - } - var args = []; - for (var i = _12c; i < arguments.length; i++) { - args.push(arguments[i]); - } - return dojo.global().setTimeout(function () { - func.apply(_12b, args); - }, _12a); -}; -dojo.lang.clearTimeout = function (_12f) { - dojo.global().clearTimeout(_12f); -}; -dojo.lang.getNameInObj = function (ns, item) { - if (!ns) { - ns = dj_global; - } - for (var x in ns) { - if (ns[x] === item) { - return new String(x); - } - } - return null; -}; -dojo.lang.shallowCopy = function (obj, deep) { - var i, ret; - if (obj === null) { - return null; - } - if (dojo.lang.isObject(obj)) { - ret = new obj.constructor(); - for (i in obj) { - if (dojo.lang.isUndefined(ret[i])) { - ret[i] = deep ? dojo.lang.shallowCopy(obj[i], deep) : obj[i]; - } - } - } else { - if (dojo.lang.isArray(obj)) { - ret = []; - for (i = 0; i < obj.length; i++) { - ret[i] = deep ? dojo.lang.shallowCopy(obj[i], deep) : obj[i]; - } - } else { - ret = obj; - } - } - return ret; -}; -dojo.lang.firstValued = function () { - for (var i = 0; i < arguments.length; i++) { - if (typeof arguments[i] != "undefined") { - return arguments[i]; - } - } - return undefined; -}; -dojo.lang.getObjPathValue = function (_138, _139, _13a) { - with (dojo.parseObjPath(_138, _139, _13a)) { - return dojo.evalProp(prop, obj, _13a); - } -}; -dojo.lang.setObjPathValue = function (_13b, _13c, _13d, _13e) { - dojo.deprecated("dojo.lang.setObjPathValue", "use dojo.parseObjPath and the '=' operator", "0.6"); - if (arguments.length < 4) { - _13e = true; - } - with (dojo.parseObjPath(_13b, _13d, _13e)) { - if (obj && (_13e || (prop in obj))) { - obj[prop] = _13c; - } - } -}; -dojo.provide("dojo.io.common"); -dojo.io.transports = []; -dojo.io.hdlrFuncNames = ["load", "error", "timeout"]; -dojo.io.Request = function (url, _140, _141, _142) { - if ((arguments.length == 1) && (arguments[0].constructor == Object)) { - this.fromKwArgs(arguments[0]); - } else { - this.url = url; - if (_140) { - this.mimetype = _140; - } - if (_141) { - this.transport = _141; - } - if (arguments.length >= 4) { - this.changeUrl = _142; - } - } -}; -dojo.lang.extend(dojo.io.Request, { - url: "", - mimetype: "text/plain", - method: "GET", - content: undefined, - transport: undefined, - changeUrl: undefined, - formNode: undefined, - sync: false, - bindSuccess: false, - useCache: false, - preventCache: false, - jsonFilter: function (_143) { - if ((this.mimetype == "text/json-comment-filtered") || (this.mimetype == "application/json-comment-filtered")) { - var _144 = _143.indexOf("/*"); - var _145 = _143.lastIndexOf("*/"); - if ((_144 == -1) || (_145 == -1)) { - dojo.debug("your JSON wasn't comment filtered!"); - return ""; - } - return _143.substring(_144 + 2, _145); - } - dojo.debug("please consider using a mimetype of text/json-comment-filtered to avoid potential security issues with JSON endpoints"); - return _143; - }, - load: function (type, data, _148, _149) { - }, - error: function (type, _14b, _14c, _14d) { - }, - timeout: function (type, _14f, _150, _151) { - }, - handle: function (type, data, _154, _155) { - }, - timeoutSeconds: 0, - abort: function () { - }, - fromKwArgs: function (_156) { - if (_156["url"]) { - _156.url = _156.url.toString(); - } - if (_156["formNode"]) { - _156.formNode = dojo.byId(_156.formNode); - } - if (!_156["method"] && _156["formNode"] && _156["formNode"].method) { - _156.method = _156["formNode"].method; - } - if (!_156["handle"] && _156["handler"]) { - _156.handle = _156.handler; - } - if (!_156["load"] && _156["loaded"]) { - _156.load = _156.loaded; - } - if (!_156["changeUrl"] && _156["changeURL"]) { - _156.changeUrl = _156.changeURL; - } - _156.encoding = dojo.lang.firstValued(_156["encoding"], djConfig["bindEncoding"], ""); - _156.sendTransport = dojo.lang.firstValued(_156["sendTransport"], djConfig["ioSendTransport"], false); - var _157 = dojo.lang.isFunction; - for (var x = 0; x < dojo.io.hdlrFuncNames.length; x++) { - var fn = dojo.io.hdlrFuncNames[x]; - if (_156[fn] && _157(_156[fn])) { - continue; - } - if (_156["handle"] && _157(_156["handle"])) { - _156[fn] = _156.handle; - } - } - dojo.lang.mixin(this, _156); - } -}); -dojo.io.Error = function (msg, type, num) { - this.message = msg; - this.type = type || "unknown"; - this.number = num || 0; -}; -dojo.io.transports.addTransport = function (name) { - this.push(name); - this[name] = dojo.io[name]; -}; -dojo.io.bind = function (_15e) { - if (!(_15e instanceof dojo.io.Request)) { - try { - _15e = new dojo.io.Request(_15e); - } - catch (e) { - dojo.debug(e); - } - } - var _15f = ""; - if (_15e["transport"]) { - _15f = _15e["transport"]; - if (!this[_15f]) { - dojo.io.sendBindError(_15e, "No dojo.io.bind() transport with name '" + _15e["transport"] + "'."); - return _15e; - } - if (!this[_15f].canHandle(_15e)) { - dojo.io.sendBindError(_15e, "dojo.io.bind() transport with name '" + _15e["transport"] + "' cannot handle this type of request."); - return _15e; - } - } else { - for (var x = 0; x < dojo.io.transports.length; x++) { - var tmp = dojo.io.transports[x]; - if ((this[tmp]) && (this[tmp].canHandle(_15e))) { - _15f = tmp; - break; - } - } - if (_15f == "") { - dojo.io.sendBindError(_15e, "None of the loaded transports for dojo.io.bind()" + " can handle the request."); - return _15e; - } - } - this[_15f].bind(_15e); - _15e.bindSuccess = true; - return _15e; -}; -dojo.io.sendBindError = function (_162, _163) { - if ((typeof _162.error == "function" || typeof _162.handle == "function") && (typeof setTimeout == "function" || typeof setTimeout == "object")) { - var _164 = new dojo.io.Error(_163); - setTimeout(function () { - _162[(typeof _162.error == "function") ? "error" : "handle"]("error", _164, null, _162); - }, 50); - } else { - dojo.raise(_163); - } -}; -dojo.io.queueBind = function (_165) { - if (!(_165 instanceof dojo.io.Request)) { - try { - _165 = new dojo.io.Request(_165); - } - catch (e) { - dojo.debug(e); - } - } - var _166 = _165.load; - _165.load = function () { - dojo.io._queueBindInFlight = false; - var ret = _166.apply(this, arguments); - dojo.io._dispatchNextQueueBind(); - return ret; - }; - var _168 = _165.error; - _165.error = function () { - dojo.io._queueBindInFlight = false; - var ret = _168.apply(this, arguments); - dojo.io._dispatchNextQueueBind(); - return ret; - }; - dojo.io._bindQueue.push(_165); - dojo.io._dispatchNextQueueBind(); - return _165; -}; -dojo.io._dispatchNextQueueBind = function () { - if (!dojo.io._queueBindInFlight) { - dojo.io._queueBindInFlight = true; - if (dojo.io._bindQueue.length > 0) { - dojo.io.bind(dojo.io._bindQueue.shift()); - } else { - dojo.io._queueBindInFlight = false; - } - } -}; -dojo.io._bindQueue = []; -dojo.io._queueBindInFlight = false; -dojo.io.argsFromMap = function (map, _16b, last) { - var enc = /utf/i.test(_16b || "") ? encodeURIComponent : dojo.string.encodeAscii; - var _16e = []; - var _16f = new Object(); - for (var name in map) { - var _171 = function (elt) { - var val = enc(name) + "=" + enc(elt); - _16e[(last == name) ? "push" : "unshift"](val); - }; - if (!_16f[name]) { - var _174 = map[name]; - if (dojo.lang.isArray(_174)) { - dojo.lang.forEach(_174, _171); - } else { - _171(_174); - } - } - } - return _16e.join("&"); -}; -dojo.io.setIFrameSrc = function (_175, src, _177) { - try { - var r = dojo.render.html; - if (!_177) { - if (r.safari) { - _175.location = src; - } else { - frames[_175.name].location = src; - } - } else { - var idoc; - if (r.ie) { - idoc = _175.contentWindow.document; - } else { - if (r.safari) { - idoc = _175.document; - } else { - idoc = _175.contentWindow; - } - } - if (!idoc) { - _175.location = src; - return; - } else { - idoc.location.replace(src); - } - } - } - catch (e) { - dojo.debug(e); - dojo.debug("setIFrameSrc: " + e); - } -}; -dojo.provide("dojo.lang.array"); -dojo.lang.mixin(dojo.lang, { - has: function (obj, name) { - try { - return typeof obj[name] != "undefined"; - } - catch (e) { - return false; - } - }, isEmpty: function (obj) { - if (dojo.lang.isObject(obj)) { - var tmp = {}; - var _17e = 0; - for (var x in obj) { - if (obj[x] && (!tmp[x])) { - _17e++; - break; - } - } - return _17e == 0; - } else { - if (dojo.lang.isArrayLike(obj) || dojo.lang.isString(obj)) { - return obj.length == 0; - } - } - }, map: function (arr, obj, _182) { - var _183 = dojo.lang.isString(arr); - if (_183) { - arr = arr.split(""); - } - if (dojo.lang.isFunction(obj) && (!_182)) { - _182 = obj; - obj = dj_global; - } else { - if (dojo.lang.isFunction(obj) && _182) { - var _184 = obj; - obj = _182; - _182 = _184; - } - } - if (Array.map) { - var _185 = Array.map(arr, _182, obj); - } else { - var _185 = []; - for (var i = 0; i < arr.length; ++i) { - _185.push(_182.call(obj, arr[i])); - } - } - if (_183) { - return _185.join(""); - } else { - return _185; - } - }, reduce: function (arr, _188, obj, _18a) { - var _18b = _188; - if (arguments.length == 2) { - _18a = _188; - _18b = arr[0]; - arr = arr.slice(1); - } else { - if (arguments.length == 3) { - if (dojo.lang.isFunction(obj)) { - _18a = obj; - obj = null; - } - } else { - if (dojo.lang.isFunction(obj)) { - var tmp = _18a; - _18a = obj; - obj = tmp; - } - } - } - var ob = obj || dj_global; - dojo.lang.map(arr, function (val) { - _18b = _18a.call(ob, _18b, val); - }); - return _18b; - }, forEach: function (_18f, _190, _191) { - if (dojo.lang.isString(_18f)) { - _18f = _18f.split(""); - } - if (Array.forEach) { - Array.forEach(_18f, _190, _191); - } else { - if (!_191) { - _191 = dj_global; - } - for (var i = 0, l = _18f.length; i < l; i++) { - _190.call(_191, _18f[i], i, _18f); - } - } - }, _everyOrSome: function (_194, arr, _196, _197) { - if (dojo.lang.isString(arr)) { - arr = arr.split(""); - } - if (Array.every) { - return Array[_194 ? "every" : "some"](arr, _196, _197); - } else { - if (!_197) { - _197 = dj_global; - } - for (var i = 0, l = arr.length; i < l; i++) { - var _19a = _196.call(_197, arr[i], i, arr); - if (_194 && !_19a) { - return false; - } else { - if ((!_194) && (_19a)) { - return true; - } - } - } - return Boolean(_194); - } - }, every: function (arr, _19c, _19d) { - return this._everyOrSome(true, arr, _19c, _19d); - }, some: function (arr, _19f, _1a0) { - return this._everyOrSome(false, arr, _19f, _1a0); - }, filter: function (arr, _1a2, _1a3) { - var _1a4 = dojo.lang.isString(arr); - if (_1a4) { - arr = arr.split(""); - } - var _1a5; - if (Array.filter) { - _1a5 = Array.filter(arr, _1a2, _1a3); - } else { - if (!_1a3) { - if (arguments.length >= 3) { - dojo.raise("thisObject doesn't exist!"); - } - _1a3 = dj_global; - } - _1a5 = []; - for (var i = 0; i < arr.length; i++) { - if (_1a2.call(_1a3, arr[i], i, arr)) { - _1a5.push(arr[i]); - } - } - } - if (_1a4) { - return _1a5.join(""); - } else { - return _1a5; - } - }, unnest: function () { - var out = []; - for (var i = 0; i < arguments.length; i++) { - if (dojo.lang.isArrayLike(arguments[i])) { - var add = dojo.lang.unnest.apply(this, arguments[i]); - out = out.concat(add); - } else { - out.push(arguments[i]); - } - } - return out; - }, toArray: function (_1aa, _1ab) { - var _1ac = []; - for (var i = _1ab || 0; i < _1aa.length; i++) { - _1ac.push(_1aa[i]); - } - return _1ac; - } -}); -dojo.provide("dojo.lang.func"); -dojo.lang.hitch = function (_1ae, _1af) { - var args = []; - for (var x = 2; x < arguments.length; x++) { - args.push(arguments[x]); - } - var fcn = (dojo.lang.isString(_1af) ? _1ae[_1af] : _1af) || function () { - }; - return function () { - var ta = args.concat([]); - for (var x = 0; x < arguments.length; x++) { - ta.push(arguments[x]); - } - return fcn.apply(_1ae, ta); - }; -}; -dojo.lang.anonCtr = 0; -dojo.lang.anon = {}; -dojo.lang.nameAnonFunc = function (_1b5, _1b6, _1b7) { - var nso = (_1b6 || dojo.lang.anon); - if ((_1b7) || ((dj_global["djConfig"]) && (djConfig["slowAnonFuncLookups"] == true))) { - for (var x in nso) { - try { - if (nso[x] === _1b5) { - return x; - } - } - catch (e) { - } - } - } - var ret = "__" + dojo.lang.anonCtr++; - while (typeof nso[ret] != "undefined") { - ret = "__" + dojo.lang.anonCtr++; - } - nso[ret] = _1b5; - return ret; -}; -dojo.lang.forward = function (_1bb) { - return function () { - return this[_1bb].apply(this, arguments); - }; -}; -dojo.lang.curry = function (_1bc, func) { - var _1be = []; - _1bc = _1bc || dj_global; - if (dojo.lang.isString(func)) { - func = _1bc[func]; - } - for (var x = 2; x < arguments.length; x++) { - _1be.push(arguments[x]); - } - var _1c0 = (func["__preJoinArity"] || func.length) - _1be.length; - - function gather(_1c1, _1c2, _1c3) { - var _1c4 = _1c3; - var _1c5 = _1c2.slice(0); - for (var x = 0; x < _1c1.length; x++) { - _1c5.push(_1c1[x]); - } - _1c3 = _1c3 - _1c1.length; - if (_1c3 <= 0) { - var res = func.apply(_1bc, _1c5); - _1c3 = _1c4; - return res; - } else { - return function () { - return gather(arguments, _1c5, _1c3); - }; - } - } - - return gather([], _1be, _1c0); -}; -dojo.lang.curryArguments = function (_1c8, func, args, _1cb) { - var _1cc = []; - var x = _1cb || 0; - for (x = _1cb; x < args.length; x++) { - _1cc.push(args[x]); - } - return dojo.lang.curry.apply(dojo.lang, [_1c8, func].concat(_1cc)); -}; -dojo.lang.tryThese = function () { - for (var x = 0; x < arguments.length; x++) { - try { - if (typeof arguments[x] == "function") { - var ret = (arguments[x]()); - if (ret) { - return ret; - } - } - } - catch (e) { - dojo.debug(e); - } - } -}; -dojo.lang.delayThese = function (farr, cb, _1d2, _1d3) { - if (!farr.length) { - if (typeof _1d3 == "function") { - _1d3(); - } - return; - } - if ((typeof _1d2 == "undefined") && (typeof cb == "number")) { - _1d2 = cb; - cb = function () { - }; - } else { - if (!cb) { - cb = function () { - }; - if (!_1d2) { - _1d2 = 0; - } - } - } - setTimeout(function () { - (farr.shift())(); - cb(); - dojo.lang.delayThese(farr, cb, _1d2, _1d3); - }, _1d2); -}; -dojo.provide("dojo.string.extras"); -dojo.string.substituteParams = function (_1d4, hash) { - var map = (typeof hash == "object") ? hash : dojo.lang.toArray(arguments, 1); - return _1d4.replace(/\%\{(\w+)\}/g, function (_1d7, key) { - if (typeof (map[key]) != "undefined" && map[key] != null) { - return map[key]; - } - dojo.raise("Substitution not found: " + key); - }); -}; -dojo.string.capitalize = function (str) { - if (!dojo.lang.isString(str)) { - return ""; - } - if (arguments.length == 0) { - str = this; - } - var _1da = str.split(" "); - for (var i = 0; i < _1da.length; i++) { - _1da[i] = _1da[i].charAt(0).toUpperCase() + _1da[i].substring(1); - } - return _1da.join(" "); -}; -dojo.string.isBlank = function (str) { - if (!dojo.lang.isString(str)) { - return true; - } - return (dojo.string.trim(str).length == 0); -}; -dojo.string.encodeAscii = function (str) { - if (!dojo.lang.isString(str)) { - return str; - } - var ret = ""; - var _1df = escape(str); - var _1e0, re = /%u([0-9A-F]{4})/i; - while ((_1e0 = _1df.match(re))) { - var num = Number("0x" + _1e0[1]); - var _1e3 = escape("&#" + num + ";"); - ret += _1df.substring(0, _1e0.index) + _1e3; - _1df = _1df.substring(_1e0.index + _1e0[0].length); - } - ret += _1df.replace(/\+/g, "%2B"); - return ret; -}; -dojo.string.escape = function (type, str) { - var args = dojo.lang.toArray(arguments, 1); - switch (type.toLowerCase()) { - case "xml": - case "html": - case "xhtml": - return dojo.string.escapeXml.apply(this, args); - case "sql": - return dojo.string.escapeSql.apply(this, args); - case "regexp": - case "regex": - return dojo.string.escapeRegExp.apply(this, args); - case "javascript": - case "jscript": - case "js": - return dojo.string.escapeJavaScript.apply(this, args); - case "ascii": - return dojo.string.encodeAscii.apply(this, args); - default: - return str; - } -}; -dojo.string.escapeXml = function (str, _1e8) { - str = str.replace(/&/gm, "&").replace(//gm, ">").replace(/"/gm, """); - if (!_1e8) { - str = str.replace(/'/gm, "'"); - } - return str; -}; -dojo.string.escapeSql = function (str) { - return str.replace(/'/gm, "''"); -}; -dojo.string.escapeRegExp = function (str) { - return str.replace(/\\/gm, "\\\\").replace(/([\f\b\n\t\r[\^$|?*+(){}])/gm, "\\$1"); -}; -dojo.string.escapeJavaScript = function (str) { - return str.replace(/(["'\f\b\n\t\r])/gm, "\\$1"); -}; -dojo.string.escapeString = function (str) { - return ("\"" + str.replace(/(["\\])/g, "\\$1") + "\"").replace(/[\f]/g, "\\f").replace(/[\b]/g, "\\b").replace(/[\n]/g, "\\n").replace(/[\t]/g, "\\t").replace(/[\r]/g, "\\r"); -}; -dojo.string.summary = function (str, len) { - if (!len || str.length <= len) { - return str; - } - return str.substring(0, len).replace(/\.+$/, "") + "..."; -}; -dojo.string.endsWith = function (str, end, _1f1) { - if (_1f1) { - str = str.toLowerCase(); - end = end.toLowerCase(); - } - if ((str.length - end.length) < 0) { - return false; - } - return str.lastIndexOf(end) == str.length - end.length; -}; -dojo.string.endsWithAny = function (str) { - for (var i = 1; i < arguments.length; i++) { - if (dojo.string.endsWith(str, arguments[i])) { - return true; - } - } - return false; -}; -dojo.string.startsWith = function (str, _1f5, _1f6) { - if (_1f6) { - str = str.toLowerCase(); - _1f5 = _1f5.toLowerCase(); - } - return str.indexOf(_1f5) == 0; -}; -dojo.string.startsWithAny = function (str) { - for (var i = 1; i < arguments.length; i++) { - if (dojo.string.startsWith(str, arguments[i])) { - return true; - } - } - return false; -}; -dojo.string.has = function (str) { - for (var i = 1; i < arguments.length; i++) { - if (str.indexOf(arguments[i]) > -1) { - return true; - } - } - return false; -}; -dojo.string.normalizeNewlines = function (text, _1fc) { - if (_1fc == "\n") { - text = text.replace(/\r\n/g, "\n"); - text = text.replace(/\r/g, "\n"); - } else { - if (_1fc == "\r") { - text = text.replace(/\r\n/g, "\r"); - text = text.replace(/\n/g, "\r"); - } else { - text = text.replace(/([^\r])\n/g, "$1\r\n").replace(/\r([^\n])/g, "\r\n$1"); - } - } - return text; -}; -dojo.string.splitEscaped = function (str, _1fe) { - var _1ff = []; - for (var i = 0, _201 = 0; i < str.length; i++) { - if (str.charAt(i) == "\\") { - i++; - continue; - } - if (str.charAt(i) == _1fe) { - _1ff.push(str.substring(_201, i)); - _201 = i + 1; - } - } - _1ff.push(str.substr(_201)); - return _1ff; -}; -dojo.provide("dojo.dom"); -dojo.dom.ELEMENT_NODE = 1; -dojo.dom.ATTRIBUTE_NODE = 2; -dojo.dom.TEXT_NODE = 3; -dojo.dom.CDATA_SECTION_NODE = 4; -dojo.dom.ENTITY_REFERENCE_NODE = 5; -dojo.dom.ENTITY_NODE = 6; -dojo.dom.PROCESSING_INSTRUCTION_NODE = 7; -dojo.dom.COMMENT_NODE = 8; -dojo.dom.DOCUMENT_NODE = 9; -dojo.dom.DOCUMENT_TYPE_NODE = 10; -dojo.dom.DOCUMENT_FRAGMENT_NODE = 11; -dojo.dom.NOTATION_NODE = 12; -dojo.dom.dojoml = "http://www.dojotoolkit.org/2004/dojoml"; -dojo.dom.xmlns = { - svg: "http://www.w3.org/2000/svg", - smil: "http://www.w3.org/2001/SMIL20/", - mml: "http://www.w3.org/1998/Math/MathML", - cml: "http://www.xml-cml.org", - xlink: "http://www.w3.org/1999/xlink", - xhtml: "http://www.w3.org/1999/xhtml", - xul: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", - xbl: "http://www.mozilla.org/xbl", - fo: "http://www.w3.org/1999/XSL/Format", - xsl: "http://www.w3.org/1999/XSL/Transform", - xslt: "http://www.w3.org/1999/XSL/Transform", - xi: "http://www.w3.org/2001/XInclude", - xforms: "http://www.w3.org/2002/01/xforms", - saxon: "http://icl.com/saxon", - xalan: "http://xml.apache.org/xslt", - xsd: "http://www.w3.org/2001/XMLSchema", - dt: "http://www.w3.org/2001/XMLSchema-datatypes", - xsi: "http://www.w3.org/2001/XMLSchema-instance", - rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#", - rdfs: "http://www.w3.org/2000/01/rdf-schema#", - dc: "http://purl.org/dc/elements/1.1/", - dcq: "http://purl.org/dc/qualifiers/1.0", - "soap-env": "http://schemas.xmlsoap.org/soap/envelope/", - wsdl: "http://schemas.xmlsoap.org/wsdl/", - AdobeExtensions: "http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/" -}; -dojo.dom.isNode = function (wh) { - if (typeof Element == "function") { - try { - return wh instanceof Element; - } - catch (e) { - } - } else { - return wh && !isNaN(wh.nodeType); - } -}; -dojo.dom.getUniqueId = function () { - var _203 = dojo.doc(); - do { - var id = "dj_unique_" + (++arguments.callee._idIncrement); - } while (_203.getElementById(id)); - return id; -}; -dojo.dom.getUniqueId._idIncrement = 0; -dojo.dom.firstElement = dojo.dom.getFirstChildElement = function (_205, _206) { - var node = _205.firstChild; - while (node && node.nodeType != dojo.dom.ELEMENT_NODE) { - node = node.nextSibling; - } - if (_206 && node && node.tagName && node.tagName.toLowerCase() != _206.toLowerCase()) { - node = dojo.dom.nextElement(node, _206); - } - return node; -}; -dojo.dom.lastElement = dojo.dom.getLastChildElement = function (_208, _209) { - var node = _208.lastChild; - while (node && node.nodeType != dojo.dom.ELEMENT_NODE) { - node = node.previousSibling; - } - if (_209 && node && node.tagName && node.tagName.toLowerCase() != _209.toLowerCase()) { - node = dojo.dom.prevElement(node, _209); - } - return node; -}; -dojo.dom.nextElement = dojo.dom.getNextSiblingElement = function (node, _20c) { - if (!node) { - return null; - } - do { - node = node.nextSibling; - } while (node && node.nodeType != dojo.dom.ELEMENT_NODE); - if (node && _20c && _20c.toLowerCase() != node.tagName.toLowerCase()) { - return dojo.dom.nextElement(node, _20c); - } - return node; -}; -dojo.dom.prevElement = dojo.dom.getPreviousSiblingElement = function (node, _20e) { - if (!node) { - return null; - } - if (_20e) { - _20e = _20e.toLowerCase(); - } - do { - node = node.previousSibling; - } while (node && node.nodeType != dojo.dom.ELEMENT_NODE); - if (node && _20e && _20e.toLowerCase() != node.tagName.toLowerCase()) { - return dojo.dom.prevElement(node, _20e); - } - return node; -}; -dojo.dom.moveChildren = function (_20f, _210, trim) { - var _212 = 0; - if (trim) { - while (_20f.hasChildNodes() && _20f.firstChild.nodeType == dojo.dom.TEXT_NODE) { - _20f.removeChild(_20f.firstChild); - } - while (_20f.hasChildNodes() && _20f.lastChild.nodeType == dojo.dom.TEXT_NODE) { - _20f.removeChild(_20f.lastChild); - } - } - while (_20f.hasChildNodes()) { - _210.appendChild(_20f.firstChild); - _212++; - } - return _212; -}; -dojo.dom.copyChildren = function (_213, _214, trim) { - var _216 = _213.cloneNode(true); - return this.moveChildren(_216, _214, trim); -}; -dojo.dom.replaceChildren = function (node, _218) { - var _219 = []; - if (dojo.render.html.ie) { - for (var i = 0; i < node.childNodes.length; i++) { - _219.push(node.childNodes[i]); - } - } - dojo.dom.removeChildren(node); - node.appendChild(_218); - for (var i = 0; i < _219.length; i++) { - dojo.dom.destroyNode(_219[i]); - } -}; -dojo.dom.removeChildren = function (node) { - var _21c = node.childNodes.length; - while (node.hasChildNodes()) { - dojo.dom.removeNode(node.firstChild); - } - return _21c; -}; -dojo.dom.replaceNode = function (node, _21e) { - return node.parentNode.replaceChild(_21e, node); -}; -dojo.dom.destroyNode = function (node) { - if (node.parentNode) { - node = dojo.dom.removeNode(node); - } - if (node.nodeType != 3) { - if (dojo.evalObjPath("dojo.event.browser.clean", false)) { - dojo.event.browser.clean(node); - } - if (dojo.render.html.ie) { - node.outerHTML = ""; - } - } -}; -dojo.dom.removeNode = function (node) { - if (node && node.parentNode) { - return node.parentNode.removeChild(node); - } -}; -dojo.dom.getAncestors = function (node, _222, _223) { - var _224 = []; - var _225 = (_222 && (_222 instanceof Function || typeof _222 == "function")); - while (node) { - if (!_225 || _222(node)) { - _224.push(node); - } - if (_223 && _224.length > 0) { - return _224[0]; - } - node = node.parentNode; - } - if (_223) { - return null; - } - return _224; -}; -dojo.dom.getAncestorsByTag = function (node, tag, _228) { - tag = tag.toLowerCase(); - return dojo.dom.getAncestors(node, function (el) { - return ((el.tagName) && (el.tagName.toLowerCase() == tag)); - }, _228); -}; -dojo.dom.getFirstAncestorByTag = function (node, tag) { - return dojo.dom.getAncestorsByTag(node, tag, true); -}; -dojo.dom.isDescendantOf = function (node, _22d, _22e) { - if (_22e && node) { - node = node.parentNode; - } - while (node) { - if (node == _22d) { - return true; - } - node = node.parentNode; - } - return false; -}; -dojo.dom.innerXML = function (node) { - if (node.innerXML) { - return node.innerXML; - } else { - if (node.xml) { - return node.xml; - } else { - if (typeof XMLSerializer != "undefined") { - return (new XMLSerializer()).serializeToString(node); - } - } - } -}; -dojo.dom.createDocument = function () { - var doc = null; - var _231 = dojo.doc(); - if (!dj_undef("ActiveXObject")) { - var _232 = ["MSXML2", "Microsoft", "MSXML", "MSXML3"]; - for (var i = 0; i < _232.length; i++) { - try { - doc = new ActiveXObject(_232[i] + ".XMLDOM"); - } - catch (e) { - } - if (doc) { - break; - } - } - } else { - if ((_231.implementation) && (_231.implementation.createDocument)) { - doc = _231.implementation.createDocument("", "", null); - } - } - return doc; -}; -dojo.dom.createDocumentFromText = function (str, _235) { - if (!_235) { - _235 = "text/xml"; - } - if (!dj_undef("DOMParser")) { - var _236 = new DOMParser(); - return _236.parseFromString(str, _235); - } else { - if (!dj_undef("ActiveXObject")) { - var _237 = dojo.dom.createDocument(); - if (_237) { - _237.async = false; - _237.loadXML(str); - return _237; - } else { - dojo.debug("toXml didn't work?"); - } - } else { - var _238 = dojo.doc(); - if (_238.createElement) { - var tmp = _238.createElement("xml"); - tmp.innerHTML = str; - if (_238.implementation && _238.implementation.createDocument) { - var _23a = _238.implementation.createDocument("foo", "", null); - for (var i = 0; i < tmp.childNodes.length; i++) { - _23a.importNode(tmp.childNodes.item(i), true); - } - return _23a; - } - return ((tmp.document) && (tmp.document.firstChild ? tmp.document.firstChild : tmp)); - } - } - } - return null; -}; -dojo.dom.prependChild = function (node, _23d) { - if (_23d.firstChild) { - _23d.insertBefore(node, _23d.firstChild); - } else { - _23d.appendChild(node); - } - return true; -}; -dojo.dom.insertBefore = function (node, ref, _240) { - if ((_240 != true) && (node === ref || node.nextSibling === ref)) { - return false; - } - var _241 = ref.parentNode; - _241.insertBefore(node, ref); - return true; -}; -dojo.dom.insertAfter = function (node, ref, _244) { - var pn = ref.parentNode; - if (ref == pn.lastChild) { - if ((_244 != true) && (node === ref)) { - return false; - } - pn.appendChild(node); - } else { - return this.insertBefore(node, ref.nextSibling, _244); - } - return true; -}; -dojo.dom.insertAtPosition = function (node, ref, _248) { - if ((!node) || (!ref) || (!_248)) { - return false; - } - switch (_248.toLowerCase()) { - case "before": - return dojo.dom.insertBefore(node, ref); - case "after": - return dojo.dom.insertAfter(node, ref); - case "first": - if (ref.firstChild) { - return dojo.dom.insertBefore(node, ref.firstChild); - } else { - ref.appendChild(node); - return true; - } - break; - default: - ref.appendChild(node); - return true; - } -}; -dojo.dom.insertAtIndex = function (node, _24a, _24b) { - var _24c = _24a.childNodes; - if (!_24c.length || _24c.length == _24b) { - _24a.appendChild(node); - return true; - } - if (_24b == 0) { - return dojo.dom.prependChild(node, _24a); - } - return dojo.dom.insertAfter(node, _24c[_24b - 1]); -}; -dojo.dom.textContent = function (node, text) { - if (arguments.length > 1) { - var _24f = dojo.doc(); - dojo.dom.replaceChildren(node, _24f.createTextNode(text)); - return text; - } else { - if (node.textContent != undefined) { - return node.textContent; - } - var _250 = ""; - if (node == null) { - return _250; - } - for (var i = 0; i < node.childNodes.length; i++) { - switch (node.childNodes[i].nodeType) { - case 1: - case 5: - _250 += dojo.dom.textContent(node.childNodes[i]); - break; - case 3: - case 2: - case 4: - _250 += node.childNodes[i].nodeValue; - break; - default: - break; - } - } - return _250; - } -}; -dojo.dom.hasParent = function (node) { - return Boolean(node && node.parentNode && dojo.dom.isNode(node.parentNode)); -}; -dojo.dom.isTag = function (node) { - if (node && node.tagName) { - for (var i = 1; i < arguments.length; i++) { - if (node.tagName == String(arguments[i])) { - return String(arguments[i]); - } - } - } - return ""; -}; -dojo.dom.setAttributeNS = function (elem, _256, _257, _258) { - if (elem == null || ((elem == undefined) && (typeof elem == "undefined"))) { - dojo.raise("No element given to dojo.dom.setAttributeNS"); - } - if (!((elem.setAttributeNS == undefined) && (typeof elem.setAttributeNS == "undefined"))) { - elem.setAttributeNS(_256, _257, _258); - } else { - var _259 = elem.ownerDocument; - var _25a = _259.createNode(2, _257, _256); - _25a.nodeValue = _258; - elem.setAttributeNode(_25a); - } -}; -dojo.provide("dojo.undo.browser"); -try { - if ((!djConfig["preventBackButtonFix"]) && (!dojo.hostenv.post_load_)) { - document.write(""); - } -} -catch (e) { -} -if (dojo.render.html.opera) { - dojo.debug("Opera is not supported with dojo.undo.browser, so back/forward detection will not work."); -} -dojo.undo.browser = { - initialHref: (!dj_undef("window")) ? window.location.href : "", - initialHash: (!dj_undef("window")) ? window.location.hash : "", - moveForward: false, - historyStack: [], - forwardStack: [], - historyIframe: null, - bookmarkAnchor: null, - locationTimer: null, - setInitialState: function (args) { - this.initialState = this._createState(this.initialHref, args, this.initialHash); - }, - addToHistory: function (args) { - this.forwardStack = []; - var hash = null; - var url = null; - if (!this.historyIframe) { - if (djConfig["useXDomain"] && !djConfig["dojoIframeHistoryUrl"]) { - dojo.debug("dojo.undo.browser: When using cross-domain Dojo builds," + " please save iframe_history.html to your domain and set djConfig.dojoIframeHistoryUrl" + " to the path on your domain to iframe_history.html"); - } - this.historyIframe = window.frames["djhistory"]; - } - if (!this.bookmarkAnchor) { - this.bookmarkAnchor = document.createElement("a"); - dojo.body().appendChild(this.bookmarkAnchor); - this.bookmarkAnchor.style.display = "none"; - } - if (args["changeUrl"]) { - hash = "#" + ((args["changeUrl"] !== true) ? args["changeUrl"] : (new Date()).getTime()); - if (this.historyStack.length == 0 && this.initialState.urlHash == hash) { - this.initialState = this._createState(url, args, hash); - return; - } else { - if (this.historyStack.length > 0 && this.historyStack[this.historyStack.length - 1].urlHash == hash) { - this.historyStack[this.historyStack.length - 1] = this._createState(url, args, hash); - return; - } - } - this.changingUrl = true; - setTimeout("window.location.href = '" + hash + "'; dojo.undo.browser.changingUrl = false;", 1); - this.bookmarkAnchor.href = hash; - if (dojo.render.html.ie) { - url = this._loadIframeHistory(); - var _25f = args["back"] || args["backButton"] || args["handle"]; - var tcb = function (_261) { - if (window.location.hash != "") { - setTimeout("window.location.href = '" + hash + "';", 1); - } - _25f.apply(this, [_261]); - }; - if (args["back"]) { - args.back = tcb; - } else { - if (args["backButton"]) { - args.backButton = tcb; - } else { - if (args["handle"]) { - args.handle = tcb; - } - } - } - var _262 = args["forward"] || args["forwardButton"] || args["handle"]; - var tfw = function (_264) { - if (window.location.hash != "") { - window.location.href = hash; - } - if (_262) { - _262.apply(this, [_264]); - } - }; - if (args["forward"]) { - args.forward = tfw; - } else { - if (args["forwardButton"]) { - args.forwardButton = tfw; - } else { - if (args["handle"]) { - args.handle = tfw; - } - } - } - } else { - if (dojo.render.html.moz) { - if (!this.locationTimer) { - this.locationTimer = setInterval("dojo.undo.browser.checkLocation();", 200); - } - } - } - } else { - url = this._loadIframeHistory(); - } - this.historyStack.push(this._createState(url, args, hash)); - }, - checkLocation: function () { - if (!this.changingUrl) { - var hsl = this.historyStack.length; - if ((window.location.hash == this.initialHash || window.location.href == this.initialHref) && (hsl == 1)) { - this.handleBackButton(); - return; - } - if (this.forwardStack.length > 0) { - if (this.forwardStack[this.forwardStack.length - 1].urlHash == window.location.hash) { - this.handleForwardButton(); - return; - } - } - if ((hsl >= 2) && (this.historyStack[hsl - 2])) { - if (this.historyStack[hsl - 2].urlHash == window.location.hash) { - this.handleBackButton(); - return; - } - } - } - }, - iframeLoaded: function (evt, _267) { - if (!dojo.render.html.opera) { - var _268 = this._getUrlQuery(_267.href); - if (_268 == null) { - if (this.historyStack.length == 1) { - this.handleBackButton(); - } - return; - } - if (this.moveForward) { - this.moveForward = false; - return; - } - if (this.historyStack.length >= 2 && _268 == this._getUrlQuery(this.historyStack[this.historyStack.length - 2].url)) { - this.handleBackButton(); - } else { - if (this.forwardStack.length > 0 && _268 == this._getUrlQuery(this.forwardStack[this.forwardStack.length - 1].url)) { - this.handleForwardButton(); - } - } - } - }, - handleBackButton: function () { - var _269 = this.historyStack.pop(); - if (!_269) { - return; - } - var last = this.historyStack[this.historyStack.length - 1]; - if (!last && this.historyStack.length == 0) { - last = this.initialState; - } - if (last) { - if (last.kwArgs["back"]) { - last.kwArgs["back"](); - } else { - if (last.kwArgs["backButton"]) { - last.kwArgs["backButton"](); - } else { - if (last.kwArgs["handle"]) { - last.kwArgs.handle("back"); - } - } - } - } - this.forwardStack.push(_269); - }, - handleForwardButton: function () { - var last = this.forwardStack.pop(); - if (!last) { - return; - } - if (last.kwArgs["forward"]) { - last.kwArgs.forward(); - } else { - if (last.kwArgs["forwardButton"]) { - last.kwArgs.forwardButton(); - } else { - if (last.kwArgs["handle"]) { - last.kwArgs.handle("forward"); - } - } - } - this.historyStack.push(last); - }, - _createState: function (url, args, hash) { - return {"url": url, "kwArgs": args, "urlHash": hash}; - }, - _getUrlQuery: function (url) { - var _270 = url.split("?"); - if (_270.length < 2) { - return null; - } else { - return _270[1]; - } - }, - _loadIframeHistory: function () { - var url = (djConfig["dojoIframeHistoryUrl"] || dojo.hostenv.getBaseScriptUri() + "iframe_history.html") + "?" + (new Date()).getTime(); - this.moveForward = true; - dojo.io.setIFrameSrc(this.historyIframe, url, false); - return url; - } -}; -dojo.provide("dojo.io.BrowserIO"); -if (!dj_undef("window")) { - dojo.io.checkChildrenForFile = function (node) { - var _273 = false; - var _274 = node.getElementsByTagName("input"); - dojo.lang.forEach(_274, function (_275) { - if (_273) { - return; - } - if (_275.getAttribute("type") == "file") { - _273 = true; - } - }); - return _273; - }; - dojo.io.formHasFile = function (_276) { - return dojo.io.checkChildrenForFile(_276); - }; - dojo.io.updateNode = function (node, _278) { - node = dojo.byId(node); - var args = _278; - if (dojo.lang.isString(_278)) { - args = {url: _278}; - } - args.mimetype = "text/html"; - args.load = function (t, d, e) { - while (node.firstChild) { - dojo.dom.destroyNode(node.firstChild); - } - node.innerHTML = d; - }; - dojo.io.bind(args); - }; - dojo.io.formFilter = function (node) { - var type = (node.type || "").toLowerCase(); - return !node.disabled && node.name && !dojo.lang.inArray(["file", "submit", "image", "reset", "button"], type); - }; - dojo.io.encodeForm = function (_27f, _280, _281) { - if ((!_27f) || (!_27f.tagName) || (!_27f.tagName.toLowerCase() == "form")) { - dojo.raise("Attempted to encode a non-form element."); - } - if (!_281) { - _281 = dojo.io.formFilter; - } - var enc = /utf/i.test(_280 || "") ? encodeURIComponent : dojo.string.encodeAscii; - var _283 = []; - for (var i = 0; i < _27f.elements.length; i++) { - var elm = _27f.elements[i]; - if (!elm || elm.tagName.toLowerCase() == "fieldset" || !_281(elm)) { - continue; - } - var name = enc(elm.name); - var type = elm.type.toLowerCase(); - if (type == "select-multiple") { - for (var j = 0; j < elm.options.length; j++) { - if (elm.options[j].selected) { - _283.push(name + "=" + enc(elm.options[j].value)); - } - } - } else { - if (dojo.lang.inArray(["radio", "checkbox"], type)) { - if (elm.checked) { - _283.push(name + "=" + enc(elm.value)); - } - } else { - _283.push(name + "=" + enc(elm.value)); - } - } - } - var _289 = _27f.getElementsByTagName("input"); - for (var i = 0; i < _289.length; i++) { - var _28a = _289[i]; - if (_28a.type.toLowerCase() == "image" && _28a.form == _27f && _281(_28a)) { - var name = enc(_28a.name); - _283.push(name + "=" + enc(_28a.value)); - _283.push(name + ".x=0"); - _283.push(name + ".y=0"); - } - } - return _283.join("&") + "&"; - }; - dojo.io.FormBind = function (args) { - this.bindArgs = {}; - if (args && args.formNode) { - this.init(args); - } else { - if (args) { - this.init({formNode: args}); - } - } - }; - dojo.lang.extend(dojo.io.FormBind, { - form: null, bindArgs: null, clickedButton: null, init: function (args) { - var form = dojo.byId(args.formNode); - if (!form || !form.tagName || form.tagName.toLowerCase() != "form") { - throw new Error("FormBind: Couldn't apply, invalid form"); - } else { - if (this.form == form) { - return; - } else { - if (this.form) { - throw new Error("FormBind: Already applied to a form"); - } - } - } - dojo.lang.mixin(this.bindArgs, args); - this.form = form; - this.connect(form, "onsubmit", "submit"); - for (var i = 0; i < form.elements.length; i++) { - var node = form.elements[i]; - if (node && node.type && dojo.lang.inArray(["submit", "button"], node.type.toLowerCase())) { - this.connect(node, "onclick", "click"); - } - } - var _290 = form.getElementsByTagName("input"); - for (var i = 0; i < _290.length; i++) { - var _291 = _290[i]; - if (_291.type.toLowerCase() == "image" && _291.form == form) { - this.connect(_291, "onclick", "click"); - } - } - }, onSubmit: function (form) { - return true; - }, submit: function (e) { - e.preventDefault(); - if (this.onSubmit(this.form)) { - dojo.io.bind(dojo.lang.mixin(this.bindArgs, {formFilter: dojo.lang.hitch(this, "formFilter")})); - } - }, click: function (e) { - var node = e.currentTarget; - if (node.disabled) { - return; - } - this.clickedButton = node; - }, formFilter: function (node) { - var type = (node.type || "").toLowerCase(); - var _298 = false; - if (node.disabled || !node.name) { - _298 = false; - } else { - if (dojo.lang.inArray(["submit", "button", "image"], type)) { - if (!this.clickedButton) { - this.clickedButton = node; - } - _298 = node == this.clickedButton; - } else { - _298 = !dojo.lang.inArray(["file", "submit", "reset", "button"], type); - } - } - return _298; - }, connect: function (_299, _29a, _29b) { - if (dojo.evalObjPath("dojo.event.connect")) { - dojo.event.connect(_299, _29a, this, _29b); - } else { - var fcn = dojo.lang.hitch(this, _29b); - _299[_29a] = function (e) { - if (!e) { - e = window.event; - } - if (!e.currentTarget) { - e.currentTarget = e.srcElement; - } - if (!e.preventDefault) { - e.preventDefault = function () { - window.event.returnValue = false; - }; - } - fcn(e); - }; - } - } - }); - dojo.io.XMLHTTPTransport = new function () { - var _29e = this; - var _29f = {}; - this.useCache = false; - this.preventCache = false; - function getCacheKey(url, _2a1, _2a2) { - return url + "|" + _2a1 + "|" + _2a2.toLowerCase(); - } - - function addToCache(url, _2a4, _2a5, http) { - _29f[getCacheKey(url, _2a4, _2a5)] = http; - } - - function getFromCache(url, _2a8, _2a9) { - return _29f[getCacheKey(url, _2a8, _2a9)]; - } - - this.clearCache = function () { - _29f = {}; - }; - function doLoad(_2aa, http, url, _2ad, _2ae) { - if (((http.status >= 200) && (http.status < 300)) || (http.status == 304) || (http.status == 1223) || (location.protocol == "file:" && (http.status == 0 || http.status == undefined)) || (location.protocol == "chrome:" && (http.status == 0 || http.status == undefined))) { - var ret; - if (_2aa.method.toLowerCase() == "head") { - var _2b0 = http.getAllResponseHeaders(); - ret = {}; - ret.toString = function () { - return _2b0; - }; - var _2b1 = _2b0.split(/[\r\n]+/g); - for (var i = 0; i < _2b1.length; i++) { - var pair = _2b1[i].match(/^([^:]+)\s*:\s*(.+)$/i); - if (pair) { - ret[pair[1]] = pair[2]; - } - } - } else { - if (_2aa.mimetype == "text/javascript") { - try { - ret = dj_eval(http.responseText); - } - catch (e) { - dojo.debug(e); - dojo.debug(http.responseText); - ret = null; - } - } else { - if (_2aa.mimetype.substr(0, 9) == "text/json" || _2aa.mimetype.substr(0, 16) == "application/json") { - try { - ret = dj_eval("(" + _2aa.jsonFilter(http.responseText) + ")"); - } - catch (e) { - dojo.debug(e); - dojo.debug(http.responseText); - ret = false; - } - } else { - if ((_2aa.mimetype == "application/xml") || (_2aa.mimetype == "text/xml")) { - ret = http.responseXML; - if (!ret || typeof ret == "string" || !http.getResponseHeader("Content-Type")) { - ret = dojo.dom.createDocumentFromText(http.responseText); - } - } else { - ret = http.responseText; - } - } - } - } - if (_2ae) { - addToCache(url, _2ad, _2aa.method, http); - } - _2aa[(typeof _2aa.load == "function") ? "load" : "handle"]("load", ret, http, _2aa); - } else { - var _2b4 = new dojo.io.Error("XMLHttpTransport Error: " + http.status + " " + http.statusText); - _2aa[(typeof _2aa.error == "function") ? "error" : "handle"]("error", _2b4, http, _2aa); - } - } - - function setHeaders(http, _2b6) { - if (_2b6["headers"]) { - for (var _2b7 in _2b6["headers"]) { - if (_2b7.toLowerCase() == "content-type" && !_2b6["contentType"]) { - _2b6["contentType"] = _2b6["headers"][_2b7]; - } else { - http.setRequestHeader(_2b7, _2b6["headers"][_2b7]); - } - } - } - } - - this.inFlight = []; - this.inFlightTimer = null; - this.startWatchingInFlight = function () { - if (!this.inFlightTimer) { - this.inFlightTimer = setTimeout("dojo.io.XMLHTTPTransport.watchInFlight();", 10); - } - }; - this.watchInFlight = function () { - var now = null; - if (!dojo.hostenv._blockAsync && !_29e._blockAsync) { - for (var x = this.inFlight.length - 1; x >= 0; x--) { - try { - var tif = this.inFlight[x]; - if (!tif || tif.http._aborted || !tif.http.readyState) { - this.inFlight.splice(x, 1); - continue; - } - if (4 == tif.http.readyState) { - this.inFlight.splice(x, 1); - doLoad(tif.req, tif.http, tif.url, tif.query, tif.useCache); - } else { - if (tif.startTime) { - if (!now) { - now = (new Date()).getTime(); - } - if (tif.startTime + (tif.req.timeoutSeconds * 1000) < now) { - if (typeof tif.http.abort == "function") { - tif.http.abort(); - } - this.inFlight.splice(x, 1); - tif.req[(typeof tif.req.timeout == "function") ? "timeout" : "handle"]("timeout", null, tif.http, tif.req); - } - } - } - } - catch (e) { - try { - var _2bb = new dojo.io.Error("XMLHttpTransport.watchInFlight Error: " + e); - tif.req[(typeof tif.req.error == "function") ? "error" : "handle"]("error", _2bb, tif.http, tif.req); - } - catch (e2) { - dojo.debug("XMLHttpTransport error callback failed: " + e2); - } - } - } - } - clearTimeout(this.inFlightTimer); - if (this.inFlight.length == 0) { - this.inFlightTimer = null; - return; - } - this.inFlightTimer = setTimeout("dojo.io.XMLHTTPTransport.watchInFlight();", 10); - }; - var _2bc = dojo.hostenv.getXmlhttpObject() ? true : false; - this.canHandle = function (_2bd) { - var mlc = _2bd["mimetype"].toLowerCase() || ""; - return _2bc && ((dojo.lang.inArray(["text/plain", "text/html", "application/xml", "text/xml", "text/javascript"], mlc)) || (mlc.substr(0, 9) == "text/json" || mlc.substr(0, 16) == "application/json")) && !(_2bd["formNode"] && dojo.io.formHasFile(_2bd["formNode"])); - }; - this.multipartBoundary = "45309FFF-BD65-4d50-99C9-36986896A96F"; - this.bind = function (_2bf) { - if (!_2bf["url"]) { - if (!_2bf["formNode"] && (_2bf["backButton"] || _2bf["back"] || _2bf["changeUrl"] || _2bf["watchForURL"]) && (!djConfig.preventBackButtonFix)) { - dojo.deprecated("Using dojo.io.XMLHTTPTransport.bind() to add to browser history without doing an IO request", "Use dojo.undo.browser.addToHistory() instead.", "0.4"); - dojo.undo.browser.addToHistory(_2bf); - return true; - } - } - var url = _2bf.url; - var _2c1 = ""; - if (_2bf["formNode"]) { - var ta = _2bf.formNode.getAttribute("action"); - if ((ta) && (!_2bf["url"])) { - url = ta; - } - var tp = _2bf.formNode.getAttribute("method"); - if ((tp) && (!_2bf["method"])) { - _2bf.method = tp; - } - _2c1 += dojo.io.encodeForm(_2bf.formNode, _2bf.encoding, _2bf["formFilter"]); - } - if (url.indexOf("#") > -1) { - dojo.debug("Warning: dojo.io.bind: stripping hash values from url:", url); - url = url.split("#")[0]; - } - if (_2bf["file"]) { - _2bf.method = "post"; - } - if (!_2bf["method"]) { - _2bf.method = "get"; - } - if (_2bf.method.toLowerCase() == "get") { - _2bf.multipart = false; - } else { - if (_2bf["file"]) { - _2bf.multipart = true; - } else { - if (!_2bf["multipart"]) { - _2bf.multipart = false; - } - } - } - if (_2bf["backButton"] || _2bf["back"] || _2bf["changeUrl"]) { - dojo.undo.browser.addToHistory(_2bf); - } - var _2c4 = _2bf["content"] || {}; - if (_2bf.sendTransport) { - _2c4["dojo.transport"] = "xmlhttp"; - } - do { - if (_2bf.postContent) { - _2c1 = _2bf.postContent; - break; - } - if (_2c4) { - _2c1 += dojo.io.argsFromMap(_2c4, _2bf.encoding); - } - if (_2bf.method.toLowerCase() == "get" || !_2bf.multipart) { - break; - } - var t = []; - if (_2c1.length) { - var q = _2c1.split("&"); - for (var i = 0; i < q.length; ++i) { - if (q[i].length) { - var p = q[i].split("="); - t.push("--" + this.multipartBoundary, "Content-Disposition: form-data; name=\"" + p[0] + "\"", "", p[1]); - } - } - } - if (_2bf.file) { - if (dojo.lang.isArray(_2bf.file)) { - for (var i = 0; i < _2bf.file.length; ++i) { - var o = _2bf.file[i]; - t.push("--" + this.multipartBoundary, "Content-Disposition: form-data; name=\"" + o.name + "\"; filename=\"" + ("fileName" in o ? o.fileName : o.name) + "\"", "Content-Type: " + ("contentType" in o ? o.contentType : "application/octet-stream"), "", o.content); - } - } else { - var o = _2bf.file; - t.push("--" + this.multipartBoundary, "Content-Disposition: form-data; name=\"" + o.name + "\"; filename=\"" + ("fileName" in o ? o.fileName : o.name) + "\"", "Content-Type: " + ("contentType" in o ? o.contentType : "application/octet-stream"), "", o.content); - } - } - if (t.length) { - t.push("--" + this.multipartBoundary + "--", ""); - _2c1 = t.join("\r\n"); - } - } while (false); - var _2ca = _2bf["sync"] ? false : true; - var _2cb = _2bf["preventCache"] || (this.preventCache == true && _2bf["preventCache"] != false); - var _2cc = _2bf["useCache"] == true || (this.useCache == true && _2bf["useCache"] != false); - if (!_2cb && _2cc) { - var _2cd = getFromCache(url, _2c1, _2bf.method); - if (_2cd) { - doLoad(_2bf, _2cd, url, _2c1, false); - return; - } - } - var http = dojo.hostenv.getXmlhttpObject(_2bf); - var _2cf = false; - if (_2ca) { - var _2d0 = this.inFlight.push({ - "req": _2bf, - "http": http, - "url": url, - "query": _2c1, - "useCache": _2cc, - "startTime": _2bf.timeoutSeconds ? (new Date()).getTime() : 0 - }); - this.startWatchingInFlight(); - } else { - _29e._blockAsync = true; - } - if (_2bf.method.toLowerCase() == "post") { - if (!_2bf.user) { - http.open("POST", url, _2ca); - } else { - http.open("POST", url, _2ca, _2bf.user, _2bf.password); - } - setHeaders(http, _2bf); - http.setRequestHeader("Content-Type", _2bf.multipart ? ("multipart/form-data; boundary=" + this.multipartBoundary) : (_2bf.contentType || "application/x-www-form-urlencoded")); - try { - http.send(_2c1); - } - catch (e) { - if (typeof http.abort == "function") { - http.abort(); - } - doLoad(_2bf, {status: 404}, url, _2c1, _2cc); - } - } else { - var _2d1 = url; - if (_2c1 != "") { - _2d1 += (_2d1.indexOf("?") > -1 ? "&" : "?") + _2c1; - } - if (_2cb) { - _2d1 += (dojo.string.endsWithAny(_2d1, "?", "&") ? "" : (_2d1.indexOf("?") > -1 ? "&" : "?")) + "dojo.preventCache=" + new Date().valueOf(); - } - if (!_2bf.user) { - http.open(_2bf.method.toUpperCase(), _2d1, _2ca); - } else { - http.open(_2bf.method.toUpperCase(), _2d1, _2ca, _2bf.user, _2bf.password); - } - setHeaders(http, _2bf); - try { - http.send(null); - } - catch (e) { - if (typeof http.abort == "function") { - http.abort(); - } - doLoad(_2bf, {status: 404}, url, _2c1, _2cc); - } - } - if (!_2ca) { - doLoad(_2bf, http, url, _2c1, _2cc); - _29e._blockAsync = false; - } - _2bf.abort = function () { - try { - http._aborted = true; - } - catch (e) { - } - return http.abort(); - }; - return; - }; - dojo.io.transports.addTransport("XMLHTTPTransport"); - }; -} -dojo.provide("dojo.io.cookie"); -dojo.io.cookie.setCookie = function (name, _2d3, days, path, _2d6, _2d7) { - var _2d8 = -1; - if ((typeof days == "number") && (days >= 0)) { - var d = new Date(); - d.setTime(d.getTime() + (days * 24 * 60 * 60 * 1000)); - _2d8 = d.toGMTString(); - } - _2d3 = escape(_2d3); - document.cookie = name + "=" + _2d3 + ";" + (_2d8 != -1 ? " expires=" + _2d8 + ";" : "") + (path ? "path=" + path : "") + (_2d6 ? "; domain=" + _2d6 : "") + (_2d7 ? "; secure" : ""); -}; -dojo.io.cookie.set = dojo.io.cookie.setCookie; -dojo.io.cookie.getCookie = function (name) { - var idx = document.cookie.lastIndexOf(name + "="); - if (idx == -1) { - return null; - } - var _2dc = document.cookie.substring(idx + name.length + 1); - var end = _2dc.indexOf(";"); - if (end == -1) { - end = _2dc.length; - } - _2dc = _2dc.substring(0, end); - _2dc = unescape(_2dc); - return _2dc; -}; -dojo.io.cookie.get = dojo.io.cookie.getCookie; -dojo.io.cookie.deleteCookie = function (name) { - dojo.io.cookie.setCookie(name, "-", 0); -}; -dojo.io.cookie.setObjectCookie = function (name, obj, days, path, _2e3, _2e4, _2e5) { - if (arguments.length == 5) { - _2e5 = _2e3; - _2e3 = null; - _2e4 = null; - } - var _2e6 = [], _2e7, _2e8 = ""; - if (!_2e5) { - _2e7 = dojo.io.cookie.getObjectCookie(name); - } - if (days >= 0) { - if (!_2e7) { - _2e7 = {}; - } - for (var prop in obj) { - if (obj[prop] == null) { - delete _2e7[prop]; - } else { - if ((typeof obj[prop] == "string") || (typeof obj[prop] == "number")) { - _2e7[prop] = obj[prop]; - } - } - } - prop = null; - for (var prop in _2e7) { - _2e6.push(escape(prop) + "=" + escape(_2e7[prop])); - } - _2e8 = _2e6.join("&"); - } - dojo.io.cookie.setCookie(name, _2e8, days, path, _2e3, _2e4); -}; -dojo.io.cookie.getObjectCookie = function (name) { - var _2eb = null, _2ec = dojo.io.cookie.getCookie(name); - if (_2ec) { - _2eb = {}; - var _2ed = _2ec.split("&"); - for (var i = 0; i < _2ed.length; i++) { - var pair = _2ed[i].split("="); - var _2f0 = pair[1]; - if (isNaN(_2f0)) { - _2f0 = unescape(pair[1]); - } - _2eb[unescape(pair[0])] = _2f0; - } - } - return _2eb; -}; -dojo.io.cookie.isSupported = function () { - if (typeof navigator.cookieEnabled != "boolean") { - dojo.io.cookie.setCookie("__TestingYourBrowserForCookieSupport__", "CookiesAllowed", 90, null); - var _2f1 = dojo.io.cookie.getCookie("__TestingYourBrowserForCookieSupport__"); - navigator.cookieEnabled = (_2f1 == "CookiesAllowed"); - if (navigator.cookieEnabled) { - this.deleteCookie("__TestingYourBrowserForCookieSupport__"); - } - } - return navigator.cookieEnabled; -}; -if (!dojo.io.cookies) { - dojo.io.cookies = dojo.io.cookie; -} -dojo.kwCompoundRequire({ - common: ["dojo.io.common"], - rhino: ["dojo.io.RhinoIO"], - browser: ["dojo.io.BrowserIO", "dojo.io.cookie"], - dashboard: ["dojo.io.BrowserIO", "dojo.io.cookie"] -}); -dojo.provide("dojo.io.*"); -dojo.provide("dojo.event.common"); -dojo.event = new function () { - this._canTimeout = dojo.lang.isFunction(dj_global["setTimeout"]) || dojo.lang.isAlien(dj_global["setTimeout"]); - function interpolateArgs(args, _2f3) { - var dl = dojo.lang; - var ao = { - srcObj: dj_global, - srcFunc: null, - adviceObj: dj_global, - adviceFunc: null, - aroundObj: null, - aroundFunc: null, - adviceType: (args.length > 2) ? args[0] : "after", - precedence: "last", - once: false, - delay: null, - rate: 0, - adviceMsg: false, - maxCalls: -1 - }; - switch (args.length) { - case 0: - return; - case 1: - return; - case 2: - ao.srcFunc = args[0]; - ao.adviceFunc = args[1]; - break; - case 3: - if ((dl.isObject(args[0])) && (dl.isString(args[1])) && (dl.isString(args[2]))) { - ao.adviceType = "after"; - ao.srcObj = args[0]; - ao.srcFunc = args[1]; - ao.adviceFunc = args[2]; - } else { - if ((dl.isString(args[1])) && (dl.isString(args[2]))) { - ao.srcFunc = args[1]; - ao.adviceFunc = args[2]; - } else { - if ((dl.isObject(args[0])) && (dl.isString(args[1])) && (dl.isFunction(args[2]))) { - ao.adviceType = "after"; - ao.srcObj = args[0]; - ao.srcFunc = args[1]; - var _2f6 = dl.nameAnonFunc(args[2], ao.adviceObj, _2f3); - ao.adviceFunc = _2f6; - } else { - if ((dl.isFunction(args[0])) && (dl.isObject(args[1])) && (dl.isString(args[2]))) { - ao.adviceType = "after"; - ao.srcObj = dj_global; - var _2f6 = dl.nameAnonFunc(args[0], ao.srcObj, _2f3); - ao.srcFunc = _2f6; - ao.adviceObj = args[1]; - ao.adviceFunc = args[2]; - } - } - } - } - break; - case 4: - if ((dl.isObject(args[0])) && (dl.isObject(args[2]))) { - ao.adviceType = "after"; - ao.srcObj = args[0]; - ao.srcFunc = args[1]; - ao.adviceObj = args[2]; - ao.adviceFunc = args[3]; - } else { - if ((dl.isString(args[0])) && (dl.isString(args[1])) && (dl.isObject(args[2]))) { - ao.adviceType = args[0]; - ao.srcObj = dj_global; - ao.srcFunc = args[1]; - ao.adviceObj = args[2]; - ao.adviceFunc = args[3]; - } else { - if ((dl.isString(args[0])) && (dl.isFunction(args[1])) && (dl.isObject(args[2]))) { - ao.adviceType = args[0]; - ao.srcObj = dj_global; - var _2f6 = dl.nameAnonFunc(args[1], dj_global, _2f3); - ao.srcFunc = _2f6; - ao.adviceObj = args[2]; - ao.adviceFunc = args[3]; - } else { - if ((dl.isString(args[0])) && (dl.isObject(args[1])) && (dl.isString(args[2])) && (dl.isFunction(args[3]))) { - ao.srcObj = args[1]; - ao.srcFunc = args[2]; - var _2f6 = dl.nameAnonFunc(args[3], dj_global, _2f3); - ao.adviceObj = dj_global; - ao.adviceFunc = _2f6; - } else { - if (dl.isObject(args[1])) { - ao.srcObj = args[1]; - ao.srcFunc = args[2]; - ao.adviceObj = dj_global; - ao.adviceFunc = args[3]; - } else { - if (dl.isObject(args[2])) { - ao.srcObj = dj_global; - ao.srcFunc = args[1]; - ao.adviceObj = args[2]; - ao.adviceFunc = args[3]; - } else { - ao.srcObj = ao.adviceObj = ao.aroundObj = dj_global; - ao.srcFunc = args[1]; - ao.adviceFunc = args[2]; - ao.aroundFunc = args[3]; - } - } - } - } - } - } - break; - case 6: - ao.srcObj = args[1]; - ao.srcFunc = args[2]; - ao.adviceObj = args[3]; - ao.adviceFunc = args[4]; - ao.aroundFunc = args[5]; - ao.aroundObj = dj_global; - break; - default: - ao.srcObj = args[1]; - ao.srcFunc = args[2]; - ao.adviceObj = args[3]; - ao.adviceFunc = args[4]; - ao.aroundObj = args[5]; - ao.aroundFunc = args[6]; - ao.once = args[7]; - ao.delay = args[8]; - ao.rate = args[9]; - ao.adviceMsg = args[10]; - ao.maxCalls = (!isNaN(parseInt(args[11]))) ? args[11] : -1; - break; - } - if (dl.isFunction(ao.aroundFunc)) { - var _2f6 = dl.nameAnonFunc(ao.aroundFunc, ao.aroundObj, _2f3); - ao.aroundFunc = _2f6; - } - if (dl.isFunction(ao.srcFunc)) { - ao.srcFunc = dl.getNameInObj(ao.srcObj, ao.srcFunc); - } - if (dl.isFunction(ao.adviceFunc)) { - ao.adviceFunc = dl.getNameInObj(ao.adviceObj, ao.adviceFunc); - } - if ((ao.aroundObj) && (dl.isFunction(ao.aroundFunc))) { - ao.aroundFunc = dl.getNameInObj(ao.aroundObj, ao.aroundFunc); - } - if (!ao.srcObj) { - dojo.raise("bad srcObj for srcFunc: " + ao.srcFunc); - } - if (!ao.adviceObj) { - dojo.raise("bad adviceObj for adviceFunc: " + ao.adviceFunc); - } - if (!ao.adviceFunc) { - dojo.debug("bad adviceFunc for srcFunc: " + ao.srcFunc); - dojo.debugShallow(ao); - } - return ao; - } - - this.connect = function () { - if (arguments.length == 1) { - var ao = arguments[0]; - } else { - var ao = interpolateArgs(arguments, true); - } - if (dojo.lang.isString(ao.srcFunc) && (ao.srcFunc.toLowerCase() == "onkey")) { - if (dojo.render.html.ie) { - ao.srcFunc = "onkeydown"; - this.connect(ao); - } - ao.srcFunc = "onkeypress"; - } - if (dojo.lang.isArray(ao.srcObj) && ao.srcObj != "") { - var _2f8 = {}; - for (var x in ao) { - _2f8[x] = ao[x]; - } - var mjps = []; - dojo.lang.forEach(ao.srcObj, function (src) { - if ((dojo.render.html.capable) && (dojo.lang.isString(src))) { - src = dojo.byId(src); - } - _2f8.srcObj = src; - mjps.push(dojo.event.connect.call(dojo.event, _2f8)); - }); - return mjps; - } - var mjp = dojo.event.MethodJoinPoint.getForMethod(ao.srcObj, ao.srcFunc); - if (ao.adviceFunc) { - var mjp2 = dojo.event.MethodJoinPoint.getForMethod(ao.adviceObj, ao.adviceFunc); - } - mjp.kwAddAdvice(ao); - return mjp; - }; - this.log = function (a1, a2) { - var _300; - if ((arguments.length == 1) && (typeof a1 == "object")) { - _300 = a1; - } else { - _300 = {srcObj: a1, srcFunc: a2}; - } - _300.adviceFunc = function () { - var _301 = []; - for (var x = 0; x < arguments.length; x++) { - _301.push(arguments[x]); - } - dojo.debug("(" + _300.srcObj + ")." + _300.srcFunc, ":", _301.join(", ")); - }; - this.kwConnect(_300); - }; - this.connectBefore = function () { - var args = ["before"]; - for (var i = 0; i < arguments.length; i++) { - args.push(arguments[i]); - } - return this.connect.apply(this, args); - }; - this.connectAround = function () { - var args = ["around"]; - for (var i = 0; i < arguments.length; i++) { - args.push(arguments[i]); - } - return this.connect.apply(this, args); - }; - this.connectOnce = function () { - var ao = interpolateArgs(arguments, true); - ao.once = true; - return this.connect(ao); - }; - this.connectRunOnce = function () { - var ao = interpolateArgs(arguments, true); - ao.maxCalls = 1; - return this.connect(ao); - }; - this._kwConnectImpl = function (_309, _30a) { - var fn = (_30a) ? "disconnect" : "connect"; - if (typeof _309["srcFunc"] == "function") { - _309.srcObj = _309["srcObj"] || dj_global; - var _30c = dojo.lang.nameAnonFunc(_309.srcFunc, _309.srcObj, true); - _309.srcFunc = _30c; - } - if (typeof _309["adviceFunc"] == "function") { - _309.adviceObj = _309["adviceObj"] || dj_global; - var _30c = dojo.lang.nameAnonFunc(_309.adviceFunc, _309.adviceObj, true); - _309.adviceFunc = _30c; - } - _309.srcObj = _309["srcObj"] || dj_global; - _309.adviceObj = _309["adviceObj"] || _309["targetObj"] || dj_global; - _309.adviceFunc = _309["adviceFunc"] || _309["targetFunc"]; - return dojo.event[fn](_309); - }; - this.kwConnect = function (_30d) { - return this._kwConnectImpl(_30d, false); - }; - this.disconnect = function () { - if (arguments.length == 1) { - var ao = arguments[0]; - } else { - var ao = interpolateArgs(arguments, true); - } - if (!ao.adviceFunc) { - return; - } - if (dojo.lang.isString(ao.srcFunc) && (ao.srcFunc.toLowerCase() == "onkey")) { - if (dojo.render.html.ie) { - ao.srcFunc = "onkeydown"; - this.disconnect(ao); - } - ao.srcFunc = "onkeypress"; - } - if (!ao.srcObj[ao.srcFunc]) { - return null; - } - var mjp = dojo.event.MethodJoinPoint.getForMethod(ao.srcObj, ao.srcFunc, true); - mjp.removeAdvice(ao.adviceObj, ao.adviceFunc, ao.adviceType, ao.once); - return mjp; - }; - this.kwDisconnect = function (_310) { - return this._kwConnectImpl(_310, true); - }; -}; -dojo.event.MethodInvocation = function (_311, obj, args) { - this.jp_ = _311; - this.object = obj; - this.args = []; - for (var x = 0; x < args.length; x++) { - this.args[x] = args[x]; - } - this.around_index = -1; -}; -dojo.event.MethodInvocation.prototype.proceed = function () { - this.around_index++; - if (this.around_index >= this.jp_.around.length) { - return this.jp_.object[this.jp_.methodname].apply(this.jp_.object, this.args); - } else { - var ti = this.jp_.around[this.around_index]; - var mobj = ti[0] || dj_global; - var meth = ti[1]; - return mobj[meth].call(mobj, this); - } -}; -dojo.event.MethodJoinPoint = function (obj, _319) { - this.object = obj || dj_global; - this.methodname = _319; - this.methodfunc = this.object[_319]; - this.squelch = false; -}; -dojo.event.MethodJoinPoint.getForMethod = function (obj, _31b) { - if (!obj) { - obj = dj_global; - } - var ofn = obj[_31b]; - if (!ofn) { - ofn = obj[_31b] = function () { - }; - if (!obj[_31b]) { - dojo.raise("Cannot set do-nothing method on that object " + _31b); - } - } else { - if ((typeof ofn != "function") && (!dojo.lang.isFunction(ofn)) && (!dojo.lang.isAlien(ofn))) { - return null; - } - } - var _31d = _31b + "$joinpoint"; - var _31e = _31b + "$joinpoint$method"; - var _31f = obj[_31d]; - if (!_31f) { - var _320 = false; - if (dojo.event["browser"]) { - if ((obj["attachEvent"]) || (obj["nodeType"]) || (obj["addEventListener"])) { - _320 = true; - dojo.event.browser.addClobberNodeAttrs(obj, [_31d, _31e, _31b]); - } - } - var _321 = ofn.length; - obj[_31e] = ofn; - _31f = obj[_31d] = new dojo.event.MethodJoinPoint(obj, _31e); - if (!_320) { - obj[_31b] = function () { - return _31f.run.apply(_31f, arguments); - }; - } else { - obj[_31b] = function () { - var args = []; - if (!arguments.length) { - var evt = null; - try { - if (obj.ownerDocument) { - evt = obj.ownerDocument.parentWindow.event; - } else { - if (obj.documentElement) { - evt = obj.documentElement.ownerDocument.parentWindow.event; - } else { - if (obj.event) { - evt = obj.event; - } else { - evt = window.event; - } - } - } - } - catch (e) { - evt = window.event; - } - if (evt) { - args.push(dojo.event.browser.fixEvent(evt, this)); - } - } else { - for (var x = 0; x < arguments.length; x++) { - if ((x == 0) && (dojo.event.browser.isEvent(arguments[x]))) { - args.push(dojo.event.browser.fixEvent(arguments[x], this)); - } else { - args.push(arguments[x]); - } - } - } - return _31f.run.apply(_31f, args); - }; - } - obj[_31b].__preJoinArity = _321; - } - return _31f; -}; -dojo.lang.extend(dojo.event.MethodJoinPoint, { - squelch: false, unintercept: function () { - this.object[this.methodname] = this.methodfunc; - this.before = []; - this.after = []; - this.around = []; - }, disconnect: dojo.lang.forward("unintercept"), run: function () { - var obj = this.object || dj_global; - var args = arguments; - var _327 = []; - for (var x = 0; x < args.length; x++) { - _327[x] = args[x]; - } - var _329 = function (marr) { - if (!marr) { - dojo.debug("Null argument to unrollAdvice()"); - return; - } - var _32b = marr[0] || dj_global; - var _32c = marr[1]; - if (!_32b[_32c]) { - dojo.raise("function \"" + _32c + "\" does not exist on \"" + _32b + "\""); - } - var _32d = marr[2] || dj_global; - var _32e = marr[3]; - var msg = marr[6]; - var _330 = marr[7]; - if (_330 > -1) { - if (_330 == 0) { - return; - } - marr[7]--; - } - var _331; - var to = { - args: [], jp_: this, object: obj, proceed: function () { - return _32b[_32c].apply(_32b, to.args); - } - }; - to.args = _327; - var _333 = parseInt(marr[4]); - var _334 = ((!isNaN(_333)) && (marr[4] !== null) && (typeof marr[4] != "undefined")); - if (marr[5]) { - var rate = parseInt(marr[5]); - var cur = new Date(); - var _337 = false; - if ((marr["last"]) && ((cur - marr.last) <= rate)) { - if (dojo.event._canTimeout) { - if (marr["delayTimer"]) { - clearTimeout(marr.delayTimer); - } - var tod = parseInt(rate * 2); - var mcpy = dojo.lang.shallowCopy(marr); - marr.delayTimer = setTimeout(function () { - mcpy[5] = 0; - _329(mcpy); - }, tod); - } - return; - } else { - marr.last = cur; - } - } - if (_32e) { - _32d[_32e].call(_32d, to); - } else { - if ((_334) && ((dojo.render.html) || (dojo.render.svg))) { - dj_global["setTimeout"](function () { - if (msg) { - _32b[_32c].call(_32b, to); - } else { - _32b[_32c].apply(_32b, args); - } - }, _333); - } else { - if (msg) { - _32b[_32c].call(_32b, to); - } else { - _32b[_32c].apply(_32b, args); - } - } - } - }; - var _33a = function () { - if (this.squelch) { - try { - return _329.apply(this, arguments); - } - catch (e) { - dojo.debug(e); - } - } else { - return _329.apply(this, arguments); - } - }; - if ((this["before"]) && (this.before.length > 0)) { - dojo.lang.forEach(this.before.concat(new Array()), _33a); - } - var _33b; - try { - if ((this["around"]) && (this.around.length > 0)) { - var mi = new dojo.event.MethodInvocation(this, obj, args); - _33b = mi.proceed(); - } else { - if (this.methodfunc) { - _33b = this.object[this.methodname].apply(this.object, args); - } - } - } - catch (e) { - if (!this.squelch) { - dojo.debug(e, "when calling", this.methodname, "on", this.object, "with arguments", args); - dojo.raise(e); - } - } - if ((this["after"]) && (this.after.length > 0)) { - dojo.lang.forEach(this.after.concat(new Array()), _33a); - } - return (this.methodfunc) ? _33b : null; - }, getArr: function (kind) { - var type = "after"; - if ((typeof kind == "string") && (kind.indexOf("before") != -1)) { - type = "before"; - } else { - if (kind == "around") { - type = "around"; - } - } - if (!this[type]) { - this[type] = []; - } - return this[type]; - }, kwAddAdvice: function (args) { - this.addAdvice(args["adviceObj"], args["adviceFunc"], args["aroundObj"], args["aroundFunc"], args["adviceType"], args["precedence"], args["once"], args["delay"], args["rate"], args["adviceMsg"], args["maxCalls"]); - }, addAdvice: function (_340, _341, _342, _343, _344, _345, once, _347, rate, _349, _34a) { - var arr = this.getArr(_344); - if (!arr) { - dojo.raise("bad this: " + this); - } - var ao = [_340, _341, _342, _343, _347, rate, _349, _34a]; - if (once) { - if (this.hasAdvice(_340, _341, _344, arr) >= 0) { - return; - } - } - if (_345 == "first") { - arr.unshift(ao); - } else { - arr.push(ao); - } - }, hasAdvice: function (_34d, _34e, _34f, arr) { - if (!arr) { - arr = this.getArr(_34f); - } - var ind = -1; - for (var x = 0; x < arr.length; x++) { - var aao = (typeof _34e == "object") ? (new String(_34e)).toString() : _34e; - var a1o = (typeof arr[x][1] == "object") ? (new String(arr[x][1])).toString() : arr[x][1]; - if ((arr[x][0] == _34d) && (a1o == aao)) { - ind = x; - } - } - return ind; - }, removeAdvice: function (_355, _356, _357, once) { - var arr = this.getArr(_357); - var ind = this.hasAdvice(_355, _356, _357, arr); - if (ind == -1) { - return false; - } - while (ind != -1) { - arr.splice(ind, 1); - if (once) { - break; - } - ind = this.hasAdvice(_355, _356, _357, arr); - } - return true; - } -}); -dojo.provide("dojo.event.topic"); -dojo.event.topic = new function () { - this.topics = {}; - this.getTopic = function (_35b) { - if (!this.topics[_35b]) { - this.topics[_35b] = new this.TopicImpl(_35b); - } - return this.topics[_35b]; - }; - this.registerPublisher = function (_35c, obj, _35e) { - var _35c = this.getTopic(_35c); - _35c.registerPublisher(obj, _35e); - }; - this.subscribe = function (_35f, obj, _361) { - var _35f = this.getTopic(_35f); - _35f.subscribe(obj, _361); - }; - this.unsubscribe = function (_362, obj, _364) { - var _362 = this.getTopic(_362); - _362.unsubscribe(obj, _364); - }; - this.destroy = function (_365) { - this.getTopic(_365).destroy(); - delete this.topics[_365]; - }; - this.publishApply = function (_366, args) { - var _366 = this.getTopic(_366); - _366.sendMessage.apply(_366, args); - }; - this.publish = function (_368, _369) { - var _368 = this.getTopic(_368); - var args = []; - for (var x = 1; x < arguments.length; x++) { - args.push(arguments[x]); - } - _368.sendMessage.apply(_368, args); - }; -}; -dojo.event.topic.TopicImpl = function (_36c) { - this.topicName = _36c; - this.subscribe = function (_36d, _36e) { - var tf = _36e || _36d; - var to = (!_36e) ? dj_global : _36d; - return dojo.event.kwConnect({srcObj: this, srcFunc: "sendMessage", adviceObj: to, adviceFunc: tf}); - }; - this.unsubscribe = function (_371, _372) { - var tf = (!_372) ? _371 : _372; - var to = (!_372) ? null : _371; - return dojo.event.kwDisconnect({srcObj: this, srcFunc: "sendMessage", adviceObj: to, adviceFunc: tf}); - }; - this._getJoinPoint = function () { - return dojo.event.MethodJoinPoint.getForMethod(this, "sendMessage"); - }; - this.setSquelch = function (_375) { - this._getJoinPoint().squelch = _375; - }; - this.destroy = function () { - this._getJoinPoint().disconnect(); - }; - this.registerPublisher = function (_376, _377) { - dojo.event.connect(_376, _377, this, "sendMessage"); - }; - this.sendMessage = function (_378) { - }; -}; -dojo.provide("dojo.event.browser"); -dojo._ie_clobber = new function () { - this.clobberNodes = []; - function nukeProp(node, prop) { - try { - node[prop] = null; - } - catch (e) { - } - try { - delete node[prop]; - } - catch (e) { - } - try { - node.removeAttribute(prop); - } - catch (e) { - } - } - - this.clobber = function (_37b) { - var na; - var tna; - if (_37b) { - tna = _37b.all || _37b.getElementsByTagName("*"); - na = [_37b]; - for (var x = 0; x < tna.length; x++) { - if (tna[x]["__doClobber__"]) { - na.push(tna[x]); - } - } - } else { - try { - window.onload = null; - } - catch (e) { - } - na = (this.clobberNodes.length) ? this.clobberNodes : document.all; - } - tna = null; - var _37f = {}; - for (var i = na.length - 1; i >= 0; i = i - 1) { - var el = na[i]; - try { - if (el && el["__clobberAttrs__"]) { - for (var j = 0; j < el.__clobberAttrs__.length; j++) { - nukeProp(el, el.__clobberAttrs__[j]); - } - nukeProp(el, "__clobberAttrs__"); - nukeProp(el, "__doClobber__"); - } - } - catch (e) { - } - } - na = null; - }; -}; -if (dojo.render.html.ie) { - dojo.addOnUnload(function () { - dojo._ie_clobber.clobber(); - try { - if ((dojo["widget"]) && (dojo.widget["manager"])) { - dojo.widget.manager.destroyAll(); - } - } - catch (e) { - } - if (dojo.widget) { - for (var name in dojo.widget._templateCache) { - if (dojo.widget._templateCache[name].node) { - dojo.dom.destroyNode(dojo.widget._templateCache[name].node); - dojo.widget._templateCache[name].node = null; - delete dojo.widget._templateCache[name].node; - } - } - } - try { - window.onload = null; - } - catch (e) { - } - try { - window.onunload = null; - } - catch (e) { - } - dojo._ie_clobber.clobberNodes = []; - }); -} -dojo.event.browser = new function () { - var _384 = 0; - this.normalizedEventName = function (_385) { - switch (_385) { - case "CheckboxStateChange": - case "DOMAttrModified": - case "DOMMenuItemActive": - case "DOMMenuItemInactive": - case "DOMMouseScroll": - case "DOMNodeInserted": - case "DOMNodeRemoved": - case "RadioStateChange": - return _385; - break; - default: - var lcn = _385.toLowerCase(); - return (lcn.indexOf("on") == 0) ? lcn.substr(2) : lcn; - break; - } - }; - this.clean = function (node) { - if (dojo.render.html.ie) { - dojo._ie_clobber.clobber(node); - } - }; - this.addClobberNode = function (node) { - if (!dojo.render.html.ie) { - return; - } - if (!node["__doClobber__"]) { - node.__doClobber__ = true; - dojo._ie_clobber.clobberNodes.push(node); - node.__clobberAttrs__ = []; - } - }; - this.addClobberNodeAttrs = function (node, _38a) { - if (!dojo.render.html.ie) { - return; - } - this.addClobberNode(node); - for (var x = 0; x < _38a.length; x++) { - node.__clobberAttrs__.push(_38a[x]); - } - }; - this.removeListener = function (node, _38d, fp, _38f) { - if (!_38f) { - var _38f = false; - } - _38d = dojo.event.browser.normalizedEventName(_38d); - if (_38d == "key") { - if (dojo.render.html.ie) { - this.removeListener(node, "onkeydown", fp, _38f); - } - _38d = "keypress"; - } - if (node.removeEventListener) { - node.removeEventListener(_38d, fp, _38f); - } - }; - this.addListener = function (node, _391, fp, _393, _394) { - if (!node) { - return; - } - if (!_393) { - var _393 = false; - } - _391 = dojo.event.browser.normalizedEventName(_391); - if (_391 == "key") { - if (dojo.render.html.ie) { - this.addListener(node, "onkeydown", fp, _393, _394); - } - _391 = "keypress"; - } - if (!_394) { - var _395 = function (evt) { - if (!evt) { - evt = window.event; - } - var ret = fp(dojo.event.browser.fixEvent(evt, this)); - if (_393) { - dojo.event.browser.stopEvent(evt); - } - return ret; - }; - } else { - _395 = fp; - } - if (node.addEventListener) { - node.addEventListener(_391, _395, _393); - return _395; - } else { - _391 = "on" + _391; - if (typeof node[_391] == "function") { - var _398 = node[_391]; - node[_391] = function (e) { - _398(e); - return _395(e); - }; - } else { - node[_391] = _395; - } - if (dojo.render.html.ie) { - this.addClobberNodeAttrs(node, [_391]); - } - return _395; - } - }; - this.isEvent = function (obj) { - return (typeof obj != "undefined") && (obj) && (typeof Event != "undefined") && (obj.eventPhase); - }; - this.currentEvent = null; - this.callListener = function (_39b, _39c) { - if (typeof _39b != "function") { - dojo.raise("listener not a function: " + _39b); - } - dojo.event.browser.currentEvent.currentTarget = _39c; - return _39b.call(_39c, dojo.event.browser.currentEvent); - }; - this._stopPropagation = function () { - dojo.event.browser.currentEvent.cancelBubble = true; - }; - this._preventDefault = function () { - dojo.event.browser.currentEvent.returnValue = false; - }; - this.keys = { - KEY_BACKSPACE: 8, - KEY_TAB: 9, - KEY_CLEAR: 12, - KEY_ENTER: 13, - KEY_SHIFT: 16, - KEY_CTRL: 17, - KEY_ALT: 18, - KEY_PAUSE: 19, - KEY_CAPS_LOCK: 20, - KEY_ESCAPE: 27, - KEY_SPACE: 32, - KEY_PAGE_UP: 33, - KEY_PAGE_DOWN: 34, - KEY_END: 35, - KEY_HOME: 36, - KEY_LEFT_ARROW: 37, - KEY_UP_ARROW: 38, - KEY_RIGHT_ARROW: 39, - KEY_DOWN_ARROW: 40, - KEY_INSERT: 45, - KEY_DELETE: 46, - KEY_HELP: 47, - KEY_LEFT_WINDOW: 91, - KEY_RIGHT_WINDOW: 92, - KEY_SELECT: 93, - KEY_NUMPAD_0: 96, - KEY_NUMPAD_1: 97, - KEY_NUMPAD_2: 98, - KEY_NUMPAD_3: 99, - KEY_NUMPAD_4: 100, - KEY_NUMPAD_5: 101, - KEY_NUMPAD_6: 102, - KEY_NUMPAD_7: 103, - KEY_NUMPAD_8: 104, - KEY_NUMPAD_9: 105, - KEY_NUMPAD_MULTIPLY: 106, - KEY_NUMPAD_PLUS: 107, - KEY_NUMPAD_ENTER: 108, - KEY_NUMPAD_MINUS: 109, - KEY_NUMPAD_PERIOD: 110, - KEY_NUMPAD_DIVIDE: 111, - KEY_F1: 112, - KEY_F2: 113, - KEY_F3: 114, - KEY_F4: 115, - KEY_F5: 116, - KEY_F6: 117, - KEY_F7: 118, - KEY_F8: 119, - KEY_F9: 120, - KEY_F10: 121, - KEY_F11: 122, - KEY_F12: 123, - KEY_F13: 124, - KEY_F14: 125, - KEY_F15: 126, - KEY_NUM_LOCK: 144, - KEY_SCROLL_LOCK: 145 - }; - this.revKeys = []; - for (var key in this.keys) { - this.revKeys[this.keys[key]] = key; - } - this.fixEvent = function (evt, _39f) { - if (!evt) { - if (window["event"]) { - evt = window.event; - } - } - if ((evt["type"]) && (evt["type"].indexOf("key") == 0)) { - evt.keys = this.revKeys; - for (var key in this.keys) { - evt[key] = this.keys[key]; - } - if (evt["type"] == "keydown" && dojo.render.html.ie) { - switch (evt.keyCode) { - case evt.KEY_SHIFT: - case evt.KEY_CTRL: - case evt.KEY_ALT: - case evt.KEY_CAPS_LOCK: - case evt.KEY_LEFT_WINDOW: - case evt.KEY_RIGHT_WINDOW: - case evt.KEY_SELECT: - case evt.KEY_NUM_LOCK: - case evt.KEY_SCROLL_LOCK: - case evt.KEY_NUMPAD_0: - case evt.KEY_NUMPAD_1: - case evt.KEY_NUMPAD_2: - case evt.KEY_NUMPAD_3: - case evt.KEY_NUMPAD_4: - case evt.KEY_NUMPAD_5: - case evt.KEY_NUMPAD_6: - case evt.KEY_NUMPAD_7: - case evt.KEY_NUMPAD_8: - case evt.KEY_NUMPAD_9: - case evt.KEY_NUMPAD_PERIOD: - break; - case evt.KEY_NUMPAD_MULTIPLY: - case evt.KEY_NUMPAD_PLUS: - case evt.KEY_NUMPAD_ENTER: - case evt.KEY_NUMPAD_MINUS: - case evt.KEY_NUMPAD_DIVIDE: - break; - case evt.KEY_PAUSE: - case evt.KEY_TAB: - case evt.KEY_BACKSPACE: - case evt.KEY_ENTER: - case evt.KEY_ESCAPE: - case evt.KEY_PAGE_UP: - case evt.KEY_PAGE_DOWN: - case evt.KEY_END: - case evt.KEY_HOME: - case evt.KEY_LEFT_ARROW: - case evt.KEY_UP_ARROW: - case evt.KEY_RIGHT_ARROW: - case evt.KEY_DOWN_ARROW: - case evt.KEY_INSERT: - case evt.KEY_DELETE: - case evt.KEY_F1: - case evt.KEY_F2: - case evt.KEY_F3: - case evt.KEY_F4: - case evt.KEY_F5: - case evt.KEY_F6: - case evt.KEY_F7: - case evt.KEY_F8: - case evt.KEY_F9: - case evt.KEY_F10: - case evt.KEY_F11: - case evt.KEY_F12: - case evt.KEY_F12: - case evt.KEY_F13: - case evt.KEY_F14: - case evt.KEY_F15: - case evt.KEY_CLEAR: - case evt.KEY_HELP: - evt.key = evt.keyCode; - break; - default: - if (evt.ctrlKey || evt.altKey) { - var _3a1 = evt.keyCode; - if (_3a1 >= 65 && _3a1 <= 90 && evt.shiftKey == false) { - _3a1 += 32; - } - if (_3a1 >= 1 && _3a1 <= 26 && evt.ctrlKey) { - _3a1 += 96; - } - evt.key = String.fromCharCode(_3a1); - } - } - } else { - if (evt["type"] == "keypress") { - if (dojo.render.html.opera) { - if (evt.which == 0) { - evt.key = evt.keyCode; - } else { - if (evt.which > 0) { - switch (evt.which) { - case evt.KEY_SHIFT: - case evt.KEY_CTRL: - case evt.KEY_ALT: - case evt.KEY_CAPS_LOCK: - case evt.KEY_NUM_LOCK: - case evt.KEY_SCROLL_LOCK: - break; - case evt.KEY_PAUSE: - case evt.KEY_TAB: - case evt.KEY_BACKSPACE: - case evt.KEY_ENTER: - case evt.KEY_ESCAPE: - evt.key = evt.which; - break; - default: - var _3a1 = evt.which; - if ((evt.ctrlKey || evt.altKey || evt.metaKey) && (evt.which >= 65 && evt.which <= 90 && evt.shiftKey == false)) { - _3a1 += 32; - } - evt.key = String.fromCharCode(_3a1); - } - } - } - } else { - if (dojo.render.html.ie) { - if (!evt.ctrlKey && !evt.altKey && evt.keyCode >= evt.KEY_SPACE) { - evt.key = String.fromCharCode(evt.keyCode); - } - } else { - if (dojo.render.html.safari) { - switch (evt.keyCode) { - case 25: - evt.key = evt.KEY_TAB; - evt.shift = true; - break; - case 63232: - evt.key = evt.KEY_UP_ARROW; - break; - case 63233: - evt.key = evt.KEY_DOWN_ARROW; - break; - case 63234: - evt.key = evt.KEY_LEFT_ARROW; - break; - case 63235: - evt.key = evt.KEY_RIGHT_ARROW; - break; - case 63236: - evt.key = evt.KEY_F1; - break; - case 63237: - evt.key = evt.KEY_F2; - break; - case 63238: - evt.key = evt.KEY_F3; - break; - case 63239: - evt.key = evt.KEY_F4; - break; - case 63240: - evt.key = evt.KEY_F5; - break; - case 63241: - evt.key = evt.KEY_F6; - break; - case 63242: - evt.key = evt.KEY_F7; - break; - case 63243: - evt.key = evt.KEY_F8; - break; - case 63244: - evt.key = evt.KEY_F9; - break; - case 63245: - evt.key = evt.KEY_F10; - break; - case 63246: - evt.key = evt.KEY_F11; - break; - case 63247: - evt.key = evt.KEY_F12; - break; - case 63250: - evt.key = evt.KEY_PAUSE; - break; - case 63272: - evt.key = evt.KEY_DELETE; - break; - case 63273: - evt.key = evt.KEY_HOME; - break; - case 63275: - evt.key = evt.KEY_END; - break; - case 63276: - evt.key = evt.KEY_PAGE_UP; - break; - case 63277: - evt.key = evt.KEY_PAGE_DOWN; - break; - case 63302: - evt.key = evt.KEY_INSERT; - break; - case 63248: - case 63249: - case 63289: - break; - default: - evt.key = evt.charCode >= evt.KEY_SPACE ? String.fromCharCode(evt.charCode) : evt.keyCode; - } - } else { - evt.key = evt.charCode > 0 ? String.fromCharCode(evt.charCode) : evt.keyCode; - } - } - } - } - } - } - if (dojo.render.html.ie) { - if (!evt.target) { - evt.target = evt.srcElement; - } - if (!evt.currentTarget) { - evt.currentTarget = (_39f ? _39f : evt.srcElement); - } - if (!evt.layerX) { - evt.layerX = evt.offsetX; - } - if (!evt.layerY) { - evt.layerY = evt.offsetY; - } - var doc = (evt.srcElement && evt.srcElement.ownerDocument) ? evt.srcElement.ownerDocument : document; - var _3a3 = ((dojo.render.html.ie55) || (doc["compatMode"] == "BackCompat")) ? doc.body : doc.documentElement; - if (!evt.pageX) { - evt.pageX = evt.clientX + (_3a3.scrollLeft || 0); - } - if (!evt.pageY) { - evt.pageY = evt.clientY + (_3a3.scrollTop || 0); - } - if (evt.type == "mouseover") { - evt.relatedTarget = evt.fromElement; - } - if (evt.type == "mouseout") { - evt.relatedTarget = evt.toElement; - } - this.currentEvent = evt; - evt.callListener = this.callListener; - evt.stopPropagation = this._stopPropagation; - evt.preventDefault = this._preventDefault; - } - return evt; - }; - this.stopEvent = function (evt) { - if (window.event) { - evt.cancelBubble = true; - evt.returnValue = false; - } else { - evt.preventDefault(); - evt.stopPropagation(); - } - }; -}; -dojo.kwCompoundRequire({ - common: ["dojo.event.common", "dojo.event.topic"], - browser: ["dojo.event.browser"], - dashboard: ["dojo.event.browser"] -}); -dojo.provide("dojo.event.*"); -dojo.provide("dojo.gfx.color"); -dojo.gfx.color.Color = function (r, g, b, a) { - if (dojo.lang.isArray(r)) { - this.r = r[0]; - this.g = r[1]; - this.b = r[2]; - this.a = r[3] || 1; - } else { - if (dojo.lang.isString(r)) { - var rgb = dojo.gfx.color.extractRGB(r); - this.r = rgb[0]; - this.g = rgb[1]; - this.b = rgb[2]; - this.a = g || 1; - } else { - if (r instanceof dojo.gfx.color.Color) { - this.r = r.r; - this.b = r.b; - this.g = r.g; - this.a = r.a; - } else { - this.r = r; - this.g = g; - this.b = b; - this.a = a; - } - } - } -}; -dojo.gfx.color.Color.fromArray = function (arr) { - return new dojo.gfx.color.Color(arr[0], arr[1], arr[2], arr[3]); -}; -dojo.extend(dojo.gfx.color.Color, { - toRgb: function (_3ab) { - if (_3ab) { - return this.toRgba(); - } else { - return [this.r, this.g, this.b]; - } - }, toRgba: function () { - return [this.r, this.g, this.b, this.a]; - }, toHex: function () { - return dojo.gfx.color.rgb2hex(this.toRgb()); - }, toCss: function () { - return "rgb(" + this.toRgb().join() + ")"; - }, toString: function () { - return this.toHex(); - }, blend: function (_3ac, _3ad) { - var rgb = null; - if (dojo.lang.isArray(_3ac)) { - rgb = _3ac; - } else { - if (_3ac instanceof dojo.gfx.color.Color) { - rgb = _3ac.toRgb(); - } else { - rgb = new dojo.gfx.color.Color(_3ac).toRgb(); - } - } - return dojo.gfx.color.blend(this.toRgb(), rgb, _3ad); - } -}); -dojo.gfx.color.named = { - white: [255, 255, 255], - black: [0, 0, 0], - red: [255, 0, 0], - green: [0, 255, 0], - lime: [0, 255, 0], - blue: [0, 0, 255], - navy: [0, 0, 128], - gray: [128, 128, 128], - silver: [192, 192, 192] -}; -dojo.gfx.color.blend = function (a, b, _3b1) { - if (typeof a == "string") { - return dojo.gfx.color.blendHex(a, b, _3b1); - } - if (!_3b1) { - _3b1 = 0; - } - _3b1 = Math.min(Math.max(-1, _3b1), 1); - _3b1 = ((_3b1 + 1) / 2); - var c = []; - for (var x = 0; x < 3; x++) { - c[x] = parseInt(b[x] + ((a[x] - b[x]) * _3b1)); - } - return c; -}; -dojo.gfx.color.blendHex = function (a, b, _3b6) { - return dojo.gfx.color.rgb2hex(dojo.gfx.color.blend(dojo.gfx.color.hex2rgb(a), dojo.gfx.color.hex2rgb(b), _3b6)); -}; -dojo.gfx.color.extractRGB = function (_3b7) { - var hex = "0123456789abcdef"; - _3b7 = _3b7.toLowerCase(); - if (_3b7.indexOf("rgb") == 0) { - var _3b9 = _3b7.match(/rgba*\((\d+), *(\d+), *(\d+)/i); - var ret = _3b9.splice(1, 3); - return ret; - } else { - var _3bb = dojo.gfx.color.hex2rgb(_3b7); - if (_3bb) { - return _3bb; - } else { - return dojo.gfx.color.named[_3b7] || [255, 255, 255]; - } - } -}; -dojo.gfx.color.hex2rgb = function (hex) { - var _3bd = "0123456789ABCDEF"; - var rgb = new Array(3); - if (hex.indexOf("#") == 0) { - hex = hex.substring(1); - } - hex = hex.toUpperCase(); - if (hex.replace(new RegExp("[" + _3bd + "]", "g"), "") != "") { - return null; - } - if (hex.length == 3) { - rgb[0] = hex.charAt(0) + hex.charAt(0); - rgb[1] = hex.charAt(1) + hex.charAt(1); - rgb[2] = hex.charAt(2) + hex.charAt(2); - } else { - rgb[0] = hex.substring(0, 2); - rgb[1] = hex.substring(2, 4); - rgb[2] = hex.substring(4); - } - for (var i = 0; i < rgb.length; i++) { - rgb[i] = _3bd.indexOf(rgb[i].charAt(0)) * 16 + _3bd.indexOf(rgb[i].charAt(1)); - } - return rgb; -}; -dojo.gfx.color.rgb2hex = function (r, g, b) { - if (dojo.lang.isArray(r)) { - g = r[1] || 0; - b = r[2] || 0; - r = r[0] || 0; - } - var ret = dojo.lang.map([r, g, b], function (x) { - x = new Number(x); - var s = x.toString(16); - while (s.length < 2) { - s = "0" + s; - } - return s; - }); - ret.unshift("#"); - return ret.join(""); -}; -dojo.provide("dojo.lfx.Animation"); -dojo.lfx.Line = function (_3c6, end) { - this.start = _3c6; - this.end = end; - if (dojo.lang.isArray(_3c6)) { - var diff = []; - dojo.lang.forEach(this.start, function (s, i) { - diff[i] = this.end[i] - s; - }, this); - this.getValue = function (n) { - var res = []; - dojo.lang.forEach(this.start, function (s, i) { - res[i] = (diff[i] * n) + s; - }, this); - return res; - }; - } else { - var diff = end - _3c6; - this.getValue = function (n) { - return (diff * n) + this.start; - }; - } -}; -if ((dojo.render.html.khtml) && (!dojo.render.html.safari)) { - dojo.lfx.easeDefault = function (n) { - return (parseFloat("0.5") + ((Math.sin((n + parseFloat("1.5")) * Math.PI)) / 2)); - }; -} else { - dojo.lfx.easeDefault = function (n) { - return (0.5 + ((Math.sin((n + 1.5) * Math.PI)) / 2)); - }; -} -dojo.lfx.easeIn = function (n) { - return Math.pow(n, 3); -}; -dojo.lfx.easeOut = function (n) { - return (1 - Math.pow(1 - n, 3)); -}; -dojo.lfx.easeInOut = function (n) { - return ((3 * Math.pow(n, 2)) - (2 * Math.pow(n, 3))); -}; -dojo.lfx.IAnimation = function () { -}; -dojo.lang.extend(dojo.lfx.IAnimation, { - curve: null, - duration: 1000, - easing: null, - repeatCount: 0, - rate: 10, - handler: null, - beforeBegin: null, - onBegin: null, - onAnimate: null, - onEnd: null, - onPlay: null, - onPause: null, - onStop: null, - play: null, - pause: null, - stop: null, - connect: function (evt, _3d6, _3d7) { - if (!_3d7) { - _3d7 = _3d6; - _3d6 = this; - } - _3d7 = dojo.lang.hitch(_3d6, _3d7); - var _3d8 = this[evt] || function () { - }; - this[evt] = function () { - var ret = _3d8.apply(this, arguments); - _3d7.apply(this, arguments); - return ret; - }; - return this; - }, - fire: function (evt, args) { - if (this[evt]) { - this[evt].apply(this, (args || [])); - } - return this; - }, - repeat: function (_3dc) { - this.repeatCount = _3dc; - return this; - }, - _active: false, - _paused: false -}); -dojo.lfx.Animation = function (_3dd, _3de, _3df, _3e0, _3e1, rate) { - dojo.lfx.IAnimation.call(this); - if (dojo.lang.isNumber(_3dd) || (!_3dd && _3de.getValue)) { - rate = _3e1; - _3e1 = _3e0; - _3e0 = _3df; - _3df = _3de; - _3de = _3dd; - _3dd = null; - } else { - if (_3dd.getValue || dojo.lang.isArray(_3dd)) { - rate = _3e0; - _3e1 = _3df; - _3e0 = _3de; - _3df = _3dd; - _3de = null; - _3dd = null; - } - } - if (dojo.lang.isArray(_3df)) { - this.curve = new dojo.lfx.Line(_3df[0], _3df[1]); - } else { - this.curve = _3df; - } - if (_3de != null && _3de > 0) { - this.duration = _3de; - } - if (_3e1) { - this.repeatCount = _3e1; - } - if (rate) { - this.rate = rate; - } - if (_3dd) { - dojo.lang.forEach(["handler", "beforeBegin", "onBegin", "onEnd", "onPlay", "onStop", "onAnimate"], function (item) { - if (_3dd[item]) { - this.connect(item, _3dd[item]); - } - }, this); - } - if (_3e0 && dojo.lang.isFunction(_3e0)) { - this.easing = _3e0; - } -}; -dojo.inherits(dojo.lfx.Animation, dojo.lfx.IAnimation); -dojo.lang.extend(dojo.lfx.Animation, { - _startTime: null, _endTime: null, _timer: null, _percent: 0, _startRepeatCount: 0, play: function (_3e4, _3e5) { - if (_3e5) { - clearTimeout(this._timer); - this._active = false; - this._paused = false; - this._percent = 0; - } else { - if (this._active && !this._paused) { - return this; - } - } - this.fire("handler", ["beforeBegin"]); - this.fire("beforeBegin"); - if (_3e4 > 0) { - setTimeout(dojo.lang.hitch(this, function () { - this.play(null, _3e5); - }), _3e4); - return this; - } - this._startTime = new Date().valueOf(); - if (this._paused) { - this._startTime -= (this.duration * this._percent / 100); - } - this._endTime = this._startTime + this.duration; - this._active = true; - this._paused = false; - var step = this._percent / 100; - var _3e7 = this.curve.getValue(step); - if (this._percent == 0) { - if (!this._startRepeatCount) { - this._startRepeatCount = this.repeatCount; - } - this.fire("handler", ["begin", _3e7]); - this.fire("onBegin", [_3e7]); - } - this.fire("handler", ["play", _3e7]); - this.fire("onPlay", [_3e7]); - this._cycle(); - return this; - }, pause: function () { - clearTimeout(this._timer); - if (!this._active) { - return this; - } - this._paused = true; - var _3e8 = this.curve.getValue(this._percent / 100); - this.fire("handler", ["pause", _3e8]); - this.fire("onPause", [_3e8]); - return this; - }, gotoPercent: function (pct, _3ea) { - clearTimeout(this._timer); - this._active = true; - this._paused = true; - this._percent = pct; - if (_3ea) { - this.play(); - } - return this; - }, stop: function (_3eb) { - clearTimeout(this._timer); - var step = this._percent / 100; - if (_3eb) { - step = 1; - } - var _3ed = this.curve.getValue(step); - this.fire("handler", ["stop", _3ed]); - this.fire("onStop", [_3ed]); - this._active = false; - this._paused = false; - return this; - }, status: function () { - if (this._active) { - return this._paused ? "paused" : "playing"; - } else { - return "stopped"; - } - return this; - }, _cycle: function () { - clearTimeout(this._timer); - if (this._active) { - var curr = new Date().valueOf(); - var step = (curr - this._startTime) / (this._endTime - this._startTime); - if (step >= 1) { - step = 1; - this._percent = 100; - } else { - this._percent = step * 100; - } - if ((this.easing) && (dojo.lang.isFunction(this.easing))) { - step = this.easing(step); - } - var _3f0 = this.curve.getValue(step); - this.fire("handler", ["animate", _3f0]); - this.fire("onAnimate", [_3f0]); - if (step < 1) { - this._timer = setTimeout(dojo.lang.hitch(this, "_cycle"), this.rate); - } else { - this._active = false; - this.fire("handler", ["end"]); - this.fire("onEnd"); - if (this.repeatCount > 0) { - this.repeatCount--; - this.play(null, true); - } else { - if (this.repeatCount == -1) { - this.play(null, true); - } else { - if (this._startRepeatCount) { - this.repeatCount = this._startRepeatCount; - this._startRepeatCount = 0; - } - } - } - } - } - return this; - } -}); -dojo.lfx.Combine = function (_3f1) { - dojo.lfx.IAnimation.call(this); - this._anims = []; - this._animsEnded = 0; - var _3f2 = arguments; - if (_3f2.length == 1 && (dojo.lang.isArray(_3f2[0]) || dojo.lang.isArrayLike(_3f2[0]))) { - _3f2 = _3f2[0]; - } - dojo.lang.forEach(_3f2, function (anim) { - this._anims.push(anim); - anim.connect("onEnd", dojo.lang.hitch(this, "_onAnimsEnded")); - }, this); -}; -dojo.inherits(dojo.lfx.Combine, dojo.lfx.IAnimation); -dojo.lang.extend(dojo.lfx.Combine, { - _animsEnded: 0, play: function (_3f4, _3f5) { - if (!this._anims.length) { - return this; - } - this.fire("beforeBegin"); - if (_3f4 > 0) { - setTimeout(dojo.lang.hitch(this, function () { - this.play(null, _3f5); - }), _3f4); - return this; - } - if (_3f5 || this._anims[0].percent == 0) { - this.fire("onBegin"); - } - this.fire("onPlay"); - this._animsCall("play", null, _3f5); - return this; - }, pause: function () { - this.fire("onPause"); - this._animsCall("pause"); - return this; - }, stop: function (_3f6) { - this.fire("onStop"); - this._animsCall("stop", _3f6); - return this; - }, _onAnimsEnded: function () { - this._animsEnded++; - if (this._animsEnded >= this._anims.length) { - this.fire("onEnd"); - } - return this; - }, _animsCall: function (_3f7) { - var args = []; - if (arguments.length > 1) { - for (var i = 1; i < arguments.length; i++) { - args.push(arguments[i]); - } - } - var _3fa = this; - dojo.lang.forEach(this._anims, function (anim) { - anim[_3f7](args); - }, _3fa); - return this; - } -}); -dojo.lfx.Chain = function (_3fc) { - dojo.lfx.IAnimation.call(this); - this._anims = []; - this._currAnim = -1; - var _3fd = arguments; - if (_3fd.length == 1 && (dojo.lang.isArray(_3fd[0]) || dojo.lang.isArrayLike(_3fd[0]))) { - _3fd = _3fd[0]; - } - var _3fe = this; - dojo.lang.forEach(_3fd, function (anim, i, _401) { - this._anims.push(anim); - if (i < _401.length - 1) { - anim.connect("onEnd", dojo.lang.hitch(this, "_playNext")); - } else { - anim.connect("onEnd", dojo.lang.hitch(this, function () { - this.fire("onEnd"); - })); - } - }, this); -}; -dojo.inherits(dojo.lfx.Chain, dojo.lfx.IAnimation); -dojo.lang.extend(dojo.lfx.Chain, { - _currAnim: -1, play: function (_402, _403) { - if (!this._anims.length) { - return this; - } - if (_403 || !this._anims[this._currAnim]) { - this._currAnim = 0; - } - var _404 = this._anims[this._currAnim]; - this.fire("beforeBegin"); - if (_402 > 0) { - setTimeout(dojo.lang.hitch(this, function () { - this.play(null, _403); - }), _402); - return this; - } - if (_404) { - if (this._currAnim == 0) { - this.fire("handler", ["begin", this._currAnim]); - this.fire("onBegin", [this._currAnim]); - } - this.fire("onPlay", [this._currAnim]); - _404.play(null, _403); - } - return this; - }, pause: function () { - if (this._anims[this._currAnim]) { - this._anims[this._currAnim].pause(); - this.fire("onPause", [this._currAnim]); - } - return this; - }, playPause: function () { - if (this._anims.length == 0) { - return this; - } - if (this._currAnim == -1) { - this._currAnim = 0; - } - var _405 = this._anims[this._currAnim]; - if (_405) { - if (!_405._active || _405._paused) { - this.play(); - } else { - this.pause(); - } - } - return this; - }, stop: function () { - var _406 = this._anims[this._currAnim]; - if (_406) { - _406.stop(); - this.fire("onStop", [this._currAnim]); - } - return _406; - }, _playNext: function () { - if (this._currAnim == -1 || this._anims.length == 0) { - return this; - } - this._currAnim++; - if (this._anims[this._currAnim]) { - this._anims[this._currAnim].play(null, true); - } - return this; - } -}); -dojo.lfx.combine = function (_407) { - var _408 = arguments; - if (dojo.lang.isArray(arguments[0])) { - _408 = arguments[0]; - } - if (_408.length == 1) { - return _408[0]; - } - return new dojo.lfx.Combine(_408); -}; -dojo.lfx.chain = function (_409) { - var _40a = arguments; - if (dojo.lang.isArray(arguments[0])) { - _40a = arguments[0]; - } - if (_40a.length == 1) { - return _40a[0]; - } - return new dojo.lfx.Chain(_40a); -}; -dojo.provide("dojo.html.common"); -dojo.lang.mixin(dojo.html, dojo.dom); -dojo.html.body = function () { - dojo.deprecated("dojo.html.body() moved to dojo.body()", "0.5"); - return dojo.body(); -}; -dojo.html.getEventTarget = function (evt) { - if (!evt) { - evt = dojo.global().event || {}; - } - var t = (evt.srcElement ? evt.srcElement : (evt.target ? evt.target : null)); - while ((t) && (t.nodeType != 1)) { - t = t.parentNode; - } - return t; -}; -dojo.html.getViewport = function () { - var _40d = dojo.global(); - var _40e = dojo.doc(); - var w = 0; - var h = 0; - if (dojo.render.html.mozilla) { - w = _40e.documentElement.clientWidth; - h = _40d.innerHeight; - } else { - if (!dojo.render.html.opera && _40d.innerWidth) { - w = _40d.innerWidth; - h = _40d.innerHeight; - } else { - if (!dojo.render.html.opera && dojo.exists(_40e, "documentElement.clientWidth")) { - var w2 = _40e.documentElement.clientWidth; - if (!w || w2 && w2 < w) { - w = w2; - } - h = _40e.documentElement.clientHeight; - } else { - if (dojo.body().clientWidth) { - w = dojo.body().clientWidth; - h = dojo.body().clientHeight; - } - } - } - } - return {width: w, height: h}; -}; -dojo.html.getScroll = function () { - var _412 = dojo.global(); - var _413 = dojo.doc(); - var top = _412.pageYOffset || _413.documentElement.scrollTop || dojo.body().scrollTop || 0; - var left = _412.pageXOffset || _413.documentElement.scrollLeft || dojo.body().scrollLeft || 0; - return {top: top, left: left, offset: {x: left, y: top}}; -}; -dojo.html.getParentByType = function (node, type) { - var _418 = dojo.doc(); - var _419 = dojo.byId(node); - type = type.toLowerCase(); - while ((_419) && (_419.nodeName.toLowerCase() != type)) { - if (_419 == (_418["body"] || _418["documentElement"])) { - return null; - } - _419 = _419.parentNode; - } - return _419; -}; -dojo.html.getAttribute = function (node, attr) { - node = dojo.byId(node); - if ((!node) || (!node.getAttribute)) { - return null; - } - var ta = typeof attr == "string" ? attr : new String(attr); - var v = node.getAttribute(ta.toUpperCase()); - if ((v) && (typeof v == "string") && (v != "")) { - return v; - } - if (v && v.value) { - return v.value; - } - if ((node.getAttributeNode) && (node.getAttributeNode(ta))) { - return (node.getAttributeNode(ta)).value; - } else { - if (node.getAttribute(ta)) { - return node.getAttribute(ta); - } else { - if (node.getAttribute(ta.toLowerCase())) { - return node.getAttribute(ta.toLowerCase()); - } - } - } - return null; -}; -dojo.html.hasAttribute = function (node, attr) { - return dojo.html.getAttribute(dojo.byId(node), attr) ? true : false; -}; -dojo.html.getCursorPosition = function (e) { - e = e || dojo.global().event; - var _421 = {x: 0, y: 0}; - if (e.pageX || e.pageY) { - _421.x = e.pageX; - _421.y = e.pageY; - } else { - var de = dojo.doc().documentElement; - var db = dojo.body(); - _421.x = e.clientX + ((de || db)["scrollLeft"]) - ((de || db)["clientLeft"]); - _421.y = e.clientY + ((de || db)["scrollTop"]) - ((de || db)["clientTop"]); - } - return _421; -}; -dojo.html.isTag = function (node) { - node = dojo.byId(node); - if (node && node.tagName) { - for (var i = 1; i < arguments.length; i++) { - if (node.tagName.toLowerCase() == String(arguments[i]).toLowerCase()) { - return String(arguments[i]).toLowerCase(); - } - } - } - return ""; -}; -if (dojo.render.html.ie && !dojo.render.html.ie70) { - if (window.location.href.substr(0, 6).toLowerCase() != "https:") { - (function () { - var _426 = dojo.doc().createElement("script"); - _426.src = "javascript:'dojo.html.createExternalElement=function(doc, tag){ return doc.createElement(tag); }'"; - dojo.doc().getElementsByTagName("head")[0].appendChild(_426); - })(); - } -} else { - dojo.html.createExternalElement = function (doc, tag) { - return doc.createElement(tag); - }; -} -dojo.html._callDeprecated = function (_429, _42a, args, _42c, _42d) { - dojo.deprecated("dojo.html." + _429, "replaced by dojo.html." + _42a + "(" + (_42c ? "node, {" + _42c + ": " + _42c + "}" : "") + ")" + (_42d ? "." + _42d : ""), "0.5"); - var _42e = []; - if (_42c) { - var _42f = {}; - _42f[_42c] = args[1]; - _42e.push(args[0]); - _42e.push(_42f); - } else { - _42e = args; - } - var ret = dojo.html[_42a].apply(dojo.html, args); - if (_42d) { - return ret[_42d]; - } else { - return ret; - } -}; -dojo.html.getViewportWidth = function () { - return dojo.html._callDeprecated("getViewportWidth", "getViewport", arguments, null, "width"); -}; -dojo.html.getViewportHeight = function () { - return dojo.html._callDeprecated("getViewportHeight", "getViewport", arguments, null, "height"); -}; -dojo.html.getViewportSize = function () { - return dojo.html._callDeprecated("getViewportSize", "getViewport", arguments); -}; -dojo.html.getScrollTop = function () { - return dojo.html._callDeprecated("getScrollTop", "getScroll", arguments, null, "top"); -}; -dojo.html.getScrollLeft = function () { - return dojo.html._callDeprecated("getScrollLeft", "getScroll", arguments, null, "left"); -}; -dojo.html.getScrollOffset = function () { - return dojo.html._callDeprecated("getScrollOffset", "getScroll", arguments, null, "offset"); -}; -dojo.provide("dojo.uri.Uri"); -dojo.uri = new function () { - this.dojoUri = function (uri) { - return new dojo.uri.Uri(dojo.hostenv.getBaseScriptUri(), uri); - }; - this.moduleUri = function (_432, uri) { - var loc = dojo.hostenv.getModuleSymbols(_432).join("/"); - if (!loc) { - return null; - } - if (loc.lastIndexOf("/") != loc.length - 1) { - loc += "/"; - } - var _435 = loc.indexOf(":"); - var _436 = loc.indexOf("/"); - if (loc.charAt(0) != "/" && (_435 == -1 || _435 > _436)) { - loc = dojo.hostenv.getBaseScriptUri() + loc; - } - return new dojo.uri.Uri(loc, uri); - }; - this.Uri = function () { - var uri = arguments[0]; - for (var i = 1; i < arguments.length; i++) { - if (!arguments[i]) { - continue; - } - var _439 = new dojo.uri.Uri(arguments[i].toString()); - var _43a = new dojo.uri.Uri(uri.toString()); - if ((_439.path == "") && (_439.scheme == null) && (_439.authority == null) && (_439.query == null)) { - if (_439.fragment != null) { - _43a.fragment = _439.fragment; - } - _439 = _43a; - } else { - if (_439.scheme == null) { - _439.scheme = _43a.scheme; - if (_439.authority == null) { - _439.authority = _43a.authority; - if (_439.path.charAt(0) != "/") { - var path = _43a.path.substring(0, _43a.path.lastIndexOf("/") + 1) + _439.path; - var segs = path.split("/"); - for (var j = 0; j < segs.length; j++) { - if (segs[j] == ".") { - if (j == segs.length - 1) { - segs[j] = ""; - } else { - segs.splice(j, 1); - j--; - } - } else { - if (j > 0 && !(j == 1 && segs[0] == "") && segs[j] == ".." && segs[j - 1] != "..") { - if (j == segs.length - 1) { - segs.splice(j, 1); - segs[j - 1] = ""; - } else { - segs.splice(j - 1, 2); - j -= 2; - } - } - } - } - _439.path = segs.join("/"); - } - } - } - } - uri = ""; - if (_439.scheme != null) { - uri += _439.scheme + ":"; - } - if (_439.authority != null) { - uri += "//" + _439.authority; - } - uri += _439.path; - if (_439.query != null) { - uri += "?" + _439.query; - } - if (_439.fragment != null) { - uri += "#" + _439.fragment; - } - } - this.uri = uri.toString(); - var _43e = "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?$"; - var r = this.uri.match(new RegExp(_43e)); - this.scheme = r[2] || (r[1] ? "" : null); - this.authority = r[4] || (r[3] ? "" : null); - this.path = r[5]; - this.query = r[7] || (r[6] ? "" : null); - this.fragment = r[9] || (r[8] ? "" : null); - if (this.authority != null) { - _43e = "^((([^:]+:)?([^@]+))@)?([^:]*)(:([0-9]+))?$"; - r = this.authority.match(new RegExp(_43e)); - this.user = r[3] || null; - this.password = r[4] || null; - this.host = r[5]; - this.port = r[7] || null; - } - this.toString = function () { - return this.uri; - }; - }; -}; -dojo.provide("dojo.html.style"); -dojo.html.getClass = function (node) { - node = dojo.byId(node); - if (!node) { - return ""; - } - var cs = ""; - if (node.className) { - cs = node.className; - } else { - if (dojo.html.hasAttribute(node, "class")) { - cs = dojo.html.getAttribute(node, "class"); - } - } - return cs.replace(/^\s+|\s+$/g, ""); -}; -dojo.html.getClasses = function (node) { - var c = dojo.html.getClass(node); - return (c == "") ? [] : c.split(/\s+/g); -}; -dojo.html.hasClass = function (node, _445) { - return (new RegExp("(^|\\s+)" + _445 + "(\\s+|$)")).test(dojo.html.getClass(node)); -}; -dojo.html.prependClass = function (node, _447) { - _447 += " " + dojo.html.getClass(node); - return dojo.html.setClass(node, _447); -}; -dojo.html.addClass = function (node, _449) { - if (dojo.html.hasClass(node, _449)) { - return false; - } - _449 = (dojo.html.getClass(node) + " " + _449).replace(/^\s+|\s+$/g, ""); - return dojo.html.setClass(node, _449); -}; -dojo.html.setClass = function (node, _44b) { - node = dojo.byId(node); - var cs = new String(_44b); - try { - if (typeof node.className == "string") { - node.className = cs; - } else { - if (node.setAttribute) { - node.setAttribute("class", _44b); - node.className = cs; - } else { - return false; - } - } - } - catch (e) { - dojo.debug("dojo.html.setClass() failed", e); - } - return true; -}; -dojo.html.removeClass = function (node, _44e, _44f) { - try { - if (!_44f) { - var _450 = dojo.html.getClass(node).replace(new RegExp("(^|\\s+)" + _44e + "(\\s+|$)"), "$1$2"); - } else { - var _450 = dojo.html.getClass(node).replace(_44e, ""); - } - dojo.html.setClass(node, _450); - } - catch (e) { - dojo.debug("dojo.html.removeClass() failed", e); - } - return true; -}; -dojo.html.replaceClass = function (node, _452, _453) { - dojo.html.removeClass(node, _453); - dojo.html.addClass(node, _452); -}; -dojo.html.classMatchType = {ContainsAll: 0, ContainsAny: 1, IsOnly: 2}; -dojo.html.getElementsByClass = function (_454, _455, _456, _457, _458) { - _458 = false; - var _459 = dojo.doc(); - _455 = dojo.byId(_455) || _459; - var _45a = _454.split(/\s+/g); - var _45b = []; - if (_457 != 1 && _457 != 2) { - _457 = 0; - } - var _45c = new RegExp("(\\s|^)((" + _45a.join(")|(") + "))(\\s|$)"); - var _45d = _45a.join(" ").length; - var _45e = []; - if (!_458 && _459.evaluate) { - var _45f = ".//" + (_456 || "*") + "[contains("; - if (_457 != dojo.html.classMatchType.ContainsAny) { - _45f += "concat(' ',@class,' '), ' " + _45a.join(" ') and contains(concat(' ',@class,' '), ' ") + " ')"; - if (_457 == 2) { - _45f += " and string-length(@class)=" + _45d + "]"; - } else { - _45f += "]"; - } - } else { - _45f += "concat(' ',@class,' '), ' " + _45a.join(" ') or contains(concat(' ',@class,' '), ' ") + " ')]"; - } - var _460 = _459.evaluate(_45f, _455, null, XPathResult.ANY_TYPE, null); - var _461 = _460.iterateNext(); - while (_461) { - try { - _45e.push(_461); - _461 = _460.iterateNext(); - } - catch (e) { - break; - } - } - return _45e; - } else { - if (!_456) { - _456 = "*"; - } - _45e = _455.getElementsByTagName(_456); - var node, i = 0; - outer: - while (node = _45e[i++]) { - var _464 = dojo.html.getClasses(node); - if (_464.length == 0) { - continue outer; - } - var _465 = 0; - for (var j = 0; j < _464.length; j++) { - if (_45c.test(_464[j])) { - if (_457 == dojo.html.classMatchType.ContainsAny) { - _45b.push(node); - continue outer; - } else { - _465++; - } - } else { - if (_457 == dojo.html.classMatchType.IsOnly) { - continue outer; - } - } - } - if (_465 == _45a.length) { - if ((_457 == dojo.html.classMatchType.IsOnly) && (_465 == _464.length)) { - _45b.push(node); - } else { - if (_457 == dojo.html.classMatchType.ContainsAll) { - _45b.push(node); - } - } - } - } - return _45b; - } -}; -dojo.html.getElementsByClassName = dojo.html.getElementsByClass; -dojo.html.toCamelCase = function (_467) { - var arr = _467.split("-"), cc = arr[0]; - for (var i = 1; i < arr.length; i++) { - cc += arr[i].charAt(0).toUpperCase() + arr[i].substring(1); - } - return cc; -}; -dojo.html.toSelectorCase = function (_46b) { - return _46b.replace(/([A-Z])/g, "-$1").toLowerCase(); -}; -if (dojo.render.html.ie) { - dojo.html.getComputedStyle = function (node, _46d, _46e) { - node = dojo.byId(node); - if (!node || !node.currentStyle) { - return _46e; - } - return node.currentStyle[dojo.html.toCamelCase(_46d)]; - }; - dojo.html.getComputedStyles = function (node) { - return node.currentStyle; - }; -} else { - dojo.html.getComputedStyle = function (node, _471, _472) { - node = dojo.byId(node); - if (!node || !node.style) { - return _472; - } - var s = document.defaultView.getComputedStyle(node, null); - return (s && s[dojo.html.toCamelCase(_471)]) || ""; - }; - dojo.html.getComputedStyles = function (node) { - return document.defaultView.getComputedStyle(node, null); - }; -} -dojo.html.getStyleProperty = function (node, _476) { - node = dojo.byId(node); - return (node && node.style ? node.style[dojo.html.toCamelCase(_476)] : undefined); -}; -dojo.html.getStyle = function (node, _478) { - var _479 = dojo.html.getStyleProperty(node, _478); - return (_479 ? _479 : dojo.html.getComputedStyle(node, _478)); -}; -dojo.html.setStyle = function (node, _47b, _47c) { - node = dojo.byId(node); - if (node && node.style) { - var _47d = dojo.html.toCamelCase(_47b); - node.style[_47d] = _47c; - } -}; -dojo.html.setStyleText = function (_47e, text) { - try { - _47e.style.cssText = text; - } - catch (e) { - _47e.setAttribute("style", text); - } -}; -dojo.html.copyStyle = function (_480, _481) { - if (!_481.style.cssText) { - _480.setAttribute("style", _481.getAttribute("style")); - } else { - _480.style.cssText = _481.style.cssText; - } - dojo.html.addClass(_480, dojo.html.getClass(_481)); -}; -dojo.html.getUnitValue = function (node, _483, _484) { - var s = dojo.html.getComputedStyle(node, _483); - if ((!s) || ((s == "auto") && (_484))) { - return {value: 0, units: "px"}; - } - var _486 = s.match(/(\-?[\d.]+)([a-z%]*)/i); - if (!_486) { - return dojo.html.getUnitValue.bad; - } - return {value: Number(_486[1]), units: _486[2].toLowerCase()}; -}; -dojo.html.getUnitValue.bad = {value: NaN, units: ""}; -if (dojo.render.html.ie) { - dojo.html.toPixelValue = function (_487, _488) { - if (!_488) { - return 0; - } - if (_488.slice(-2) == "px") { - return parseFloat(_488); - } - var _489 = 0; - with (_487) { - var _48a = style.left; - var _48b = runtimeStyle.left; - runtimeStyle.left = currentStyle.left; - try { - style.left = _488 || 0; - _489 = style.pixelLeft; - style.left = _48a; - runtimeStyle.left = _48b; - } - catch (e) { - } - } - return _489; - }; -} else { - dojo.html.toPixelValue = function (_48c, _48d) { - return (_48d && (_48d.slice(-2) == "px") ? parseFloat(_48d) : 0); - }; -} -dojo.html.getPixelValue = function (node, _48f, _490) { - return dojo.html.toPixelValue(node, dojo.html.getComputedStyle(node, _48f)); -}; -dojo.html.setPositivePixelValue = function (node, _492, _493) { - if (isNaN(_493)) { - return false; - } - node.style[_492] = Math.max(0, _493) + "px"; - return true; -}; -dojo.html.styleSheet = null; -dojo.html.insertCssRule = function (_494, _495, _496) { - if (!dojo.html.styleSheet) { - if (document.createStyleSheet) { - dojo.html.styleSheet = document.createStyleSheet(); - } else { - if (document.styleSheets[0]) { - dojo.html.styleSheet = document.styleSheets[0]; - } else { - return null; - } - } - } - if (arguments.length < 3) { - if (dojo.html.styleSheet.cssRules) { - _496 = dojo.html.styleSheet.cssRules.length; - } else { - if (dojo.html.styleSheet.rules) { - _496 = dojo.html.styleSheet.rules.length; - } else { - return null; - } - } - } - if (dojo.html.styleSheet.insertRule) { - var rule = _494 + " { " + _495 + " }"; - return dojo.html.styleSheet.insertRule(rule, _496); - } else { - if (dojo.html.styleSheet.addRule) { - return dojo.html.styleSheet.addRule(_494, _495, _496); - } else { - return null; - } - } -}; -dojo.html.removeCssRule = function (_498) { - if (!dojo.html.styleSheet) { - dojo.debug("no stylesheet defined for removing rules"); - return false; - } - if (dojo.render.html.ie) { - if (!_498) { - _498 = dojo.html.styleSheet.rules.length; - dojo.html.styleSheet.removeRule(_498); - } - } else { - if (document.styleSheets[0]) { - if (!_498) { - _498 = dojo.html.styleSheet.cssRules.length; - } - dojo.html.styleSheet.deleteRule(_498); - } - } - return true; -}; -dojo.html._insertedCssFiles = []; -dojo.html.insertCssFile = function (URI, doc, _49b, _49c) { - if (!URI) { - return; - } - if (!doc) { - doc = document; - } - var _49d = dojo.hostenv.getText(URI, false, _49c); - if (_49d === null) { - return; - } - _49d = dojo.html.fixPathsInCssText(_49d, URI); - if (_49b) { - var idx = -1, node, ent = dojo.html._insertedCssFiles; - for (var i = 0; i < ent.length; i++) { - if ((ent[i].doc == doc) && (ent[i].cssText == _49d)) { - idx = i; - node = ent[i].nodeRef; - break; - } - } - if (node) { - var _4a2 = doc.getElementsByTagName("style"); - for (var i = 0; i < _4a2.length; i++) { - if (_4a2[i] == node) { - return; - } - } - dojo.html._insertedCssFiles.shift(idx, 1); - } - } - var _4a3 = dojo.html.insertCssText(_49d, doc); - dojo.html._insertedCssFiles.push({"doc": doc, "cssText": _49d, "nodeRef": _4a3}); - if (_4a3 && djConfig.isDebug) { - _4a3.setAttribute("dbgHref", URI); - } - return _4a3; -}; -dojo.html.insertCssText = function (_4a4, doc, URI) { - if (!_4a4) { - return; - } - if (!doc) { - doc = document; - } - if (URI) { - _4a4 = dojo.html.fixPathsInCssText(_4a4, URI); - } - var _4a7 = doc.createElement("style"); - _4a7.setAttribute("type", "text/css"); - var head = doc.getElementsByTagName("head")[0]; - if (!head) { - dojo.debug("No head tag in document, aborting styles"); - return; - } else { - head.appendChild(_4a7); - } - if (_4a7.styleSheet) { - var _4a9 = function () { - try { - _4a7.styleSheet.cssText = _4a4; - } - catch (e) { - dojo.debug(e); - } - }; - if (_4a7.styleSheet.disabled) { - setTimeout(_4a9, 10); - } else { - _4a9(); - } - } else { - var _4aa = doc.createTextNode(_4a4); - _4a7.appendChild(_4aa); - } - return _4a7; -}; -dojo.html.fixPathsInCssText = function (_4ab, URI) { - if (!_4ab || !URI) { - return; - } - var _4ad, str = "", url = "", _4b0 = "[\\t\\s\\w\\(\\)\\/\\.\\\\'\"-:#=&?~]+"; - var _4b1 = new RegExp("url\\(\\s*(" + _4b0 + ")\\s*\\)"); - var _4b2 = /(file|https?|ftps?):\/\//; - regexTrim = new RegExp("^[\\s]*(['\"]?)(" + _4b0 + ")\\1[\\s]*?$"); - if (dojo.render.html.ie55 || dojo.render.html.ie60) { - var _4b3 = new RegExp("AlphaImageLoader\\((.*)src=['\"](" + _4b0 + ")['\"]"); - while (_4ad = _4b3.exec(_4ab)) { - url = _4ad[2].replace(regexTrim, "$2"); - if (!_4b2.exec(url)) { - url = (new dojo.uri.Uri(URI, url).toString()); - } - str += _4ab.substring(0, _4ad.index) + "AlphaImageLoader(" + _4ad[1] + "src='" + url + "'"; - _4ab = _4ab.substr(_4ad.index + _4ad[0].length); - } - _4ab = str + _4ab; - str = ""; - } - while (_4ad = _4b1.exec(_4ab)) { - url = _4ad[1].replace(regexTrim, "$2"); - if (!_4b2.exec(url)) { - url = (new dojo.uri.Uri(URI, url).toString()); - } - str += _4ab.substring(0, _4ad.index) + "url(" + url + ")"; - _4ab = _4ab.substr(_4ad.index + _4ad[0].length); - } - return str + _4ab; -}; -dojo.html.setActiveStyleSheet = function (_4b4) { - var i = 0, a, els = dojo.doc().getElementsByTagName("link"); - while (a = els[i++]) { - if (a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title")) { - a.disabled = true; - if (a.getAttribute("title") == _4b4) { - a.disabled = false; - } - } - } -}; -dojo.html.getActiveStyleSheet = function () { - var i = 0, a, els = dojo.doc().getElementsByTagName("link"); - while (a = els[i++]) { - if (a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title") && !a.disabled) { - return a.getAttribute("title"); - } - } - return null; -}; -dojo.html.getPreferredStyleSheet = function () { - var i = 0, a, els = dojo.doc().getElementsByTagName("link"); - while (a = els[i++]) { - if (a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("rel").indexOf("alt") == -1 && a.getAttribute("title")) { - return a.getAttribute("title"); - } - } - return null; -}; -dojo.html.applyBrowserClass = function (node) { - var drh = dojo.render.html; - var _4c0 = { - dj_ie: drh.ie, - dj_ie55: drh.ie55, - dj_ie6: drh.ie60, - dj_ie7: drh.ie70, - dj_iequirks: drh.ie && drh.quirks, - dj_opera: drh.opera, - dj_opera8: drh.opera && (Math.floor(dojo.render.version) == 8), - dj_opera9: drh.opera && (Math.floor(dojo.render.version) == 9), - dj_khtml: drh.khtml, - dj_safari: drh.safari, - dj_gecko: drh.mozilla - }; - for (var p in _4c0) { - if (_4c0[p]) { - dojo.html.addClass(node, p); - } - } -}; -dojo.provide("dojo.html.display"); -dojo.html._toggle = function (node, _4c3, _4c4) { - node = dojo.byId(node); - _4c4(node, !_4c3(node)); - return _4c3(node); -}; -dojo.html.show = function (node) { - node = dojo.byId(node); - if (dojo.html.getStyleProperty(node, "display") == "none") { - dojo.html.setStyle(node, "display", (node.dojoDisplayCache || "")); - node.dojoDisplayCache = undefined; - } -}; -dojo.html.hide = function (node) { - node = dojo.byId(node); - if (typeof node["dojoDisplayCache"] == "undefined") { - var d = dojo.html.getStyleProperty(node, "display"); - if (d != "none") { - node.dojoDisplayCache = d; - } - } - dojo.html.setStyle(node, "display", "none"); -}; -dojo.html.setShowing = function (node, _4c9) { - dojo.html[(_4c9 ? "show" : "hide")](node); -}; -dojo.html.isShowing = function (node) { - return (dojo.html.getStyleProperty(node, "display") != "none"); -}; -dojo.html.toggleShowing = function (node) { - return dojo.html._toggle(node, dojo.html.isShowing, dojo.html.setShowing); -}; -dojo.html.displayMap = {tr: "", td: "", th: "", img: "inline", span: "inline", input: "inline", button: "inline"}; -dojo.html.suggestDisplayByTagName = function (node) { - node = dojo.byId(node); - if (node && node.tagName) { - var tag = node.tagName.toLowerCase(); - return (tag in dojo.html.displayMap ? dojo.html.displayMap[tag] : "block"); - } -}; -dojo.html.setDisplay = function (node, _4cf) { - dojo.html.setStyle(node, "display", ((_4cf instanceof String || typeof _4cf == "string") ? _4cf : (_4cf ? dojo.html.suggestDisplayByTagName(node) : "none"))); -}; -dojo.html.isDisplayed = function (node) { - return (dojo.html.getComputedStyle(node, "display") != "none"); -}; -dojo.html.toggleDisplay = function (node) { - return dojo.html._toggle(node, dojo.html.isDisplayed, dojo.html.setDisplay); -}; -dojo.html.setVisibility = function (node, _4d3) { - dojo.html.setStyle(node, "visibility", ((_4d3 instanceof String || typeof _4d3 == "string") ? _4d3 : (_4d3 ? "visible" : "hidden"))); -}; -dojo.html.isVisible = function (node) { - return (dojo.html.getComputedStyle(node, "visibility") != "hidden"); -}; -dojo.html.toggleVisibility = function (node) { - return dojo.html._toggle(node, dojo.html.isVisible, dojo.html.setVisibility); -}; -dojo.html.setOpacity = function (node, _4d7, _4d8) { - node = dojo.byId(node); - var h = dojo.render.html; - if (!_4d8) { - if (_4d7 >= 1) { - if (h.ie) { - dojo.html.clearOpacity(node); - return; - } else { - _4d7 = 0.999999; - } - } else { - if (_4d7 < 0) { - _4d7 = 0; - } - } - } - if (h.ie) { - if (node.nodeName.toLowerCase() == "tr") { - var tds = node.getElementsByTagName("td"); - for (var x = 0; x < tds.length; x++) { - tds[x].style.filter = "Alpha(Opacity=" + _4d7 * 100 + ")"; - } - } - node.style.filter = "Alpha(Opacity=" + _4d7 * 100 + ")"; - } else { - if (h.moz) { - node.style.opacity = _4d7; - node.style.MozOpacity = _4d7; - } else { - if (h.safari) { - node.style.opacity = _4d7; - node.style.KhtmlOpacity = _4d7; - } else { - node.style.opacity = _4d7; - } - } - } -}; -dojo.html.clearOpacity = function (node) { - node = dojo.byId(node); - var ns = node.style; - var h = dojo.render.html; - if (h.ie) { - try { - if (node.filters && node.filters.alpha) { - ns.filter = ""; - } - } - catch (e) { - } - } else { - if (h.moz) { - ns.opacity = 1; - ns.MozOpacity = 1; - } else { - if (h.safari) { - ns.opacity = 1; - ns.KhtmlOpacity = 1; - } else { - ns.opacity = 1; - } - } - } -}; -dojo.html.getOpacity = function (node) { - node = dojo.byId(node); - var h = dojo.render.html; - if (h.ie) { - var opac = (node.filters && node.filters.alpha && typeof node.filters.alpha.opacity == "number" ? node.filters.alpha.opacity : 100) / 100; - } else { - var opac = node.style.opacity || node.style.MozOpacity || node.style.KhtmlOpacity || 1; - } - return opac >= 0.999999 ? 1 : Number(opac); -}; -dojo.provide("dojo.html.color"); -dojo.html.getBackgroundColor = function (node) { - node = dojo.byId(node); - var _4e3; - do { - _4e3 = dojo.html.getStyle(node, "background-color"); - if (_4e3.toLowerCase() == "rgba(0, 0, 0, 0)") { - _4e3 = "transparent"; - } - if (node == document.getElementsByTagName("body")[0]) { - node = null; - break; - } - node = node.parentNode; - } while (node && dojo.lang.inArray(["transparent", ""], _4e3)); - if (_4e3 == "transparent") { - _4e3 = [255, 255, 255, 0]; - } else { - _4e3 = dojo.gfx.color.extractRGB(_4e3); - } - return _4e3; -}; -dojo.provide("dojo.html.layout"); -dojo.html.sumAncestorProperties = function (node, prop) { - node = dojo.byId(node); - if (!node) { - return 0; - } - var _4e6 = 0; - while (node) { - if (dojo.html.getComputedStyle(node, "position") == "fixed") { - return 0; - } - var val = node[prop]; - if (val) { - _4e6 += val - 0; - if (node == dojo.body()) { - break; - } - } - node = node.parentNode; - } - return _4e6; -}; -dojo.html.setStyleAttributes = function (node, _4e9) { - node = dojo.byId(node); - var _4ea = _4e9.replace(/(;)?\s*$/, "").split(";"); - for (var i = 0; i < _4ea.length; i++) { - var _4ec = _4ea[i].split(":"); - var name = _4ec[0].replace(/\s*$/, "").replace(/^\s*/, "").toLowerCase(); - var _4ee = _4ec[1].replace(/\s*$/, "").replace(/^\s*/, ""); - switch (name) { - case "opacity": - dojo.html.setOpacity(node, _4ee); - break; - case "content-height": - dojo.html.setContentBox(node, {height: _4ee}); - break; - case "content-width": - dojo.html.setContentBox(node, {width: _4ee}); - break; - case "outer-height": - dojo.html.setMarginBox(node, {height: _4ee}); - break; - case "outer-width": - dojo.html.setMarginBox(node, {width: _4ee}); - break; - default: - node.style[dojo.html.toCamelCase(name)] = _4ee; - } - } -}; -dojo.html.boxSizing = { - MARGIN_BOX: "margin-box", - BORDER_BOX: "border-box", - PADDING_BOX: "padding-box", - CONTENT_BOX: "content-box" -}; -dojo.html.getAbsolutePosition = dojo.html.abs = function (node, _4f0, _4f1) { - node = dojo.byId(node, node.ownerDocument); - var ret = {x: 0, y: 0}; - var bs = dojo.html.boxSizing; - if (!_4f1) { - _4f1 = bs.CONTENT_BOX; - } - var _4f4 = 2; - var _4f5; - switch (_4f1) { - case bs.MARGIN_BOX: - _4f5 = 3; - break; - case bs.BORDER_BOX: - _4f5 = 2; - break; - case bs.PADDING_BOX: - default: - _4f5 = 1; - break; - case bs.CONTENT_BOX: - _4f5 = 0; - break; - } - var h = dojo.render.html; - var db = document["body"] || document["documentElement"]; - if (h.ie) { - with (node.getBoundingClientRect()) { - ret.x = left - 2; - ret.y = top - 2; - } - } else { - if (document.getBoxObjectFor) { - _4f4 = 1; - try { - var bo = document.getBoxObjectFor(node); - ret.x = bo.x - dojo.html.sumAncestorProperties(node, "scrollLeft"); - ret.y = bo.y - dojo.html.sumAncestorProperties(node, "scrollTop"); - } - catch (e) { - } - } else { - if (node["offsetParent"]) { - var _4f9; - if ((h.safari) && (node.style.getPropertyValue("position") == "absolute") && (node.parentNode == db)) { - _4f9 = db; - } else { - _4f9 = db.parentNode; - } - if (node.parentNode != db) { - var nd = node; - if (dojo.render.html.opera) { - nd = db; - } - ret.x -= dojo.html.sumAncestorProperties(nd, "scrollLeft"); - ret.y -= dojo.html.sumAncestorProperties(nd, "scrollTop"); - } - var _4fb = node; - do { - var n = _4fb["offsetLeft"]; - if (!h.opera || n > 0) { - ret.x += isNaN(n) ? 0 : n; - } - var m = _4fb["offsetTop"]; - ret.y += isNaN(m) ? 0 : m; - _4fb = _4fb.offsetParent; - } while ((_4fb != _4f9) && (_4fb != null)); - } else { - if (node["x"] && node["y"]) { - ret.x += isNaN(node.x) ? 0 : node.x; - ret.y += isNaN(node.y) ? 0 : node.y; - } - } - } - } - if (_4f0) { - var _4fe = dojo.html.getScroll(); - ret.y += _4fe.top; - ret.x += _4fe.left; - } - var _4ff = [dojo.html.getPaddingExtent, dojo.html.getBorderExtent, dojo.html.getMarginExtent]; - if (_4f4 > _4f5) { - for (var i = _4f5; i < _4f4; ++i) { - ret.y += _4ff[i](node, "top"); - ret.x += _4ff[i](node, "left"); - } - } else { - if (_4f4 < _4f5) { - for (var i = _4f5; i > _4f4; --i) { - ret.y -= _4ff[i - 1](node, "top"); - ret.x -= _4ff[i - 1](node, "left"); - } - } - } - ret.top = ret.y; - ret.left = ret.x; - return ret; -}; -dojo.html.isPositionAbsolute = function (node) { - return (dojo.html.getComputedStyle(node, "position") == "absolute"); -}; -dojo.html._sumPixelValues = function (node, _503, _504) { - var _505 = 0; - for (var x = 0; x < _503.length; x++) { - _505 += dojo.html.getPixelValue(node, _503[x], _504); - } - return _505; -}; -dojo.html.getMargin = function (node) { - return { - width: dojo.html._sumPixelValues(node, ["margin-left", "margin-right"], (dojo.html.getComputedStyle(node, "position") == "absolute")), - height: dojo.html._sumPixelValues(node, ["margin-top", "margin-bottom"], (dojo.html.getComputedStyle(node, "position") == "absolute")) - }; -}; -dojo.html.getBorder = function (node) { - return { - width: dojo.html.getBorderExtent(node, "left") + dojo.html.getBorderExtent(node, "right"), - height: dojo.html.getBorderExtent(node, "top") + dojo.html.getBorderExtent(node, "bottom") - }; -}; -dojo.html.getBorderExtent = function (node, side) { - return (dojo.html.getStyle(node, "border-" + side + "-style") == "none" ? 0 : dojo.html.getPixelValue(node, "border-" + side + "-width")); -}; -dojo.html.getMarginExtent = function (node, side) { - return dojo.html._sumPixelValues(node, ["margin-" + side], dojo.html.isPositionAbsolute(node)); -}; -dojo.html.getPaddingExtent = function (node, side) { - return dojo.html._sumPixelValues(node, ["padding-" + side], true); -}; -dojo.html.getPadding = function (node) { - return { - width: dojo.html._sumPixelValues(node, ["padding-left", "padding-right"], true), - height: dojo.html._sumPixelValues(node, ["padding-top", "padding-bottom"], true) - }; -}; -dojo.html.getPadBorder = function (node) { - var pad = dojo.html.getPadding(node); - var _512 = dojo.html.getBorder(node); - return {width: pad.width + _512.width, height: pad.height + _512.height}; -}; -dojo.html.getBoxSizing = function (node) { - var h = dojo.render.html; - var bs = dojo.html.boxSizing; - if (((h.ie) || (h.opera)) && node.nodeName.toLowerCase() != "img") { - var cm = document["compatMode"]; - if ((cm == "BackCompat") || (cm == "QuirksMode")) { - return bs.BORDER_BOX; - } else { - return bs.CONTENT_BOX; - } - } else { - if (arguments.length == 0) { - node = document.documentElement; - } - var _517; - if (!h.ie) { - _517 = dojo.html.getStyle(node, "-moz-box-sizing"); - if (!_517) { - _517 = dojo.html.getStyle(node, "box-sizing"); - } - } - return (_517 ? _517 : bs.CONTENT_BOX); - } -}; -dojo.html.isBorderBox = function (node) { - return (dojo.html.getBoxSizing(node) == dojo.html.boxSizing.BORDER_BOX); -}; -dojo.html.getBorderBox = function (node) { - node = dojo.byId(node); - return {width: node.offsetWidth, height: node.offsetHeight}; -}; -dojo.html.getPaddingBox = function (node) { - var box = dojo.html.getBorderBox(node); - var _51c = dojo.html.getBorder(node); - return {width: box.width - _51c.width, height: box.height - _51c.height}; -}; -dojo.html.getContentBox = function (node) { - node = dojo.byId(node); - var _51e = dojo.html.getPadBorder(node); - return {width: node.offsetWidth - _51e.width, height: node.offsetHeight - _51e.height}; -}; -dojo.html.setContentBox = function (node, args) { - node = dojo.byId(node); - var _521 = 0; - var _522 = 0; - var isbb = dojo.html.isBorderBox(node); - var _524 = (isbb ? dojo.html.getPadBorder(node) : {width: 0, height: 0}); - var ret = {}; - if (typeof args.width != "undefined") { - _521 = args.width + _524.width; - ret.width = dojo.html.setPositivePixelValue(node, "width", _521); - } - if (typeof args.height != "undefined") { - _522 = args.height + _524.height; - ret.height = dojo.html.setPositivePixelValue(node, "height", _522); - } - return ret; -}; -dojo.html.getMarginBox = function (node) { - var _527 = dojo.html.getBorderBox(node); - var _528 = dojo.html.getMargin(node); - return {width: _527.width + _528.width, height: _527.height + _528.height}; -}; -dojo.html.setMarginBox = function (node, args) { - node = dojo.byId(node); - var _52b = 0; - var _52c = 0; - var isbb = dojo.html.isBorderBox(node); - var _52e = (!isbb ? dojo.html.getPadBorder(node) : {width: 0, height: 0}); - var _52f = dojo.html.getMargin(node); - var ret = {}; - if (typeof args.width != "undefined") { - _52b = args.width - _52e.width; - _52b -= _52f.width; - ret.width = dojo.html.setPositivePixelValue(node, "width", _52b); - } - if (typeof args.height != "undefined") { - _52c = args.height - _52e.height; - _52c -= _52f.height; - ret.height = dojo.html.setPositivePixelValue(node, "height", _52c); - } - return ret; -}; -dojo.html.getElementBox = function (node, type) { - var bs = dojo.html.boxSizing; - switch (type) { - case bs.MARGIN_BOX: - return dojo.html.getMarginBox(node); - case bs.BORDER_BOX: - return dojo.html.getBorderBox(node); - case bs.PADDING_BOX: - return dojo.html.getPaddingBox(node); - case bs.CONTENT_BOX: - default: - return dojo.html.getContentBox(node); - } -}; -dojo.html.toCoordinateObject = dojo.html.toCoordinateArray = function (_534, _535, _536) { - if (_534 instanceof Array || typeof _534 == "array") { - dojo.deprecated("dojo.html.toCoordinateArray", "use dojo.html.toCoordinateObject({left: , top: , width: , height: }) instead", "0.5"); - while (_534.length < 4) { - _534.push(0); - } - while (_534.length > 4) { - _534.pop(); - } - var ret = {left: _534[0], top: _534[1], width: _534[2], height: _534[3]}; - } else { - if (!_534.nodeType && !(_534 instanceof String || typeof _534 == "string") && ("width" in _534 || "height" in _534 || "left" in _534 || "x" in _534 || "top" in _534 || "y" in _534)) { - var ret = { - left: _534.left || _534.x || 0, - top: _534.top || _534.y || 0, - width: _534.width || 0, - height: _534.height || 0 - }; - } else { - var node = dojo.byId(_534); - var pos = dojo.html.abs(node, _535, _536); - var _53a = dojo.html.getMarginBox(node); - var ret = {left: pos.left, top: pos.top, width: _53a.width, height: _53a.height}; - } - } - ret.x = ret.left; - ret.y = ret.top; - return ret; -}; -dojo.html.setMarginBoxWidth = dojo.html.setOuterWidth = function (node, _53c) { - return dojo.html._callDeprecated("setMarginBoxWidth", "setMarginBox", arguments, "width"); -}; -dojo.html.setMarginBoxHeight = dojo.html.setOuterHeight = function () { - return dojo.html._callDeprecated("setMarginBoxHeight", "setMarginBox", arguments, "height"); -}; -dojo.html.getMarginBoxWidth = dojo.html.getOuterWidth = function () { - return dojo.html._callDeprecated("getMarginBoxWidth", "getMarginBox", arguments, null, "width"); -}; -dojo.html.getMarginBoxHeight = dojo.html.getOuterHeight = function () { - return dojo.html._callDeprecated("getMarginBoxHeight", "getMarginBox", arguments, null, "height"); -}; -dojo.html.getTotalOffset = function (node, type, _53f) { - return dojo.html._callDeprecated("getTotalOffset", "getAbsolutePosition", arguments, null, type); -}; -dojo.html.getAbsoluteX = function (node, _541) { - return dojo.html._callDeprecated("getAbsoluteX", "getAbsolutePosition", arguments, null, "x"); -}; -dojo.html.getAbsoluteY = function (node, _543) { - return dojo.html._callDeprecated("getAbsoluteY", "getAbsolutePosition", arguments, null, "y"); -}; -dojo.html.totalOffsetLeft = function (node, _545) { - return dojo.html._callDeprecated("totalOffsetLeft", "getAbsolutePosition", arguments, null, "left"); -}; -dojo.html.totalOffsetTop = function (node, _547) { - return dojo.html._callDeprecated("totalOffsetTop", "getAbsolutePosition", arguments, null, "top"); -}; -dojo.html.getMarginWidth = function (node) { - return dojo.html._callDeprecated("getMarginWidth", "getMargin", arguments, null, "width"); -}; -dojo.html.getMarginHeight = function (node) { - return dojo.html._callDeprecated("getMarginHeight", "getMargin", arguments, null, "height"); -}; -dojo.html.getBorderWidth = function (node) { - return dojo.html._callDeprecated("getBorderWidth", "getBorder", arguments, null, "width"); -}; -dojo.html.getBorderHeight = function (node) { - return dojo.html._callDeprecated("getBorderHeight", "getBorder", arguments, null, "height"); -}; -dojo.html.getPaddingWidth = function (node) { - return dojo.html._callDeprecated("getPaddingWidth", "getPadding", arguments, null, "width"); -}; -dojo.html.getPaddingHeight = function (node) { - return dojo.html._callDeprecated("getPaddingHeight", "getPadding", arguments, null, "height"); -}; -dojo.html.getPadBorderWidth = function (node) { - return dojo.html._callDeprecated("getPadBorderWidth", "getPadBorder", arguments, null, "width"); -}; -dojo.html.getPadBorderHeight = function (node) { - return dojo.html._callDeprecated("getPadBorderHeight", "getPadBorder", arguments, null, "height"); -}; -dojo.html.getBorderBoxWidth = dojo.html.getInnerWidth = function () { - return dojo.html._callDeprecated("getBorderBoxWidth", "getBorderBox", arguments, null, "width"); -}; -dojo.html.getBorderBoxHeight = dojo.html.getInnerHeight = function () { - return dojo.html._callDeprecated("getBorderBoxHeight", "getBorderBox", arguments, null, "height"); -}; -dojo.html.getContentBoxWidth = dojo.html.getContentWidth = function () { - return dojo.html._callDeprecated("getContentBoxWidth", "getContentBox", arguments, null, "width"); -}; -dojo.html.getContentBoxHeight = dojo.html.getContentHeight = function () { - return dojo.html._callDeprecated("getContentBoxHeight", "getContentBox", arguments, null, "height"); -}; -dojo.html.setContentBoxWidth = dojo.html.setContentWidth = function (node, _551) { - return dojo.html._callDeprecated("setContentBoxWidth", "setContentBox", arguments, "width"); -}; -dojo.html.setContentBoxHeight = dojo.html.setContentHeight = function (node, _553) { - return dojo.html._callDeprecated("setContentBoxHeight", "setContentBox", arguments, "height"); -}; -dojo.provide("dojo.lfx.html"); -dojo.lfx.html._byId = function (_554) { - if (!_554) { - return []; - } - if (dojo.lang.isArrayLike(_554)) { - if (!_554.alreadyChecked) { - var n = []; - dojo.lang.forEach(_554, function (node) { - n.push(dojo.byId(node)); - }); - n.alreadyChecked = true; - return n; - } else { - return _554; - } - } else { - var n = []; - n.push(dojo.byId(_554)); - n.alreadyChecked = true; - return n; - } -}; -dojo.lfx.html.propertyAnimation = function (_557, _558, _559, _55a, _55b) { - _557 = dojo.lfx.html._byId(_557); - var _55c = {"propertyMap": _558, "nodes": _557, "duration": _559, "easing": _55a || dojo.lfx.easeDefault}; - var _55d = function (args) { - if (args.nodes.length == 1) { - var pm = args.propertyMap; - if (!dojo.lang.isArray(args.propertyMap)) { - var parr = []; - for (var _561 in pm) { - pm[_561].property = _561; - parr.push(pm[_561]); - } - pm = args.propertyMap = parr; - } - dojo.lang.forEach(pm, function (prop) { - if (dj_undef("start", prop)) { - if (prop.property != "opacity") { - prop.start = parseInt(dojo.html.getComputedStyle(args.nodes[0], prop.property)); - } else { - prop.start = dojo.html.getOpacity(args.nodes[0]); - } - } - }); - } - }; - var _563 = function (_564) { - var _565 = []; - dojo.lang.forEach(_564, function (c) { - _565.push(Math.round(c)); - }); - return _565; - }; - var _567 = function (n, _569) { - n = dojo.byId(n); - if (!n || !n.style) { - return; - } - for (var s in _569) { - try { - if (s == "opacity") { - dojo.html.setOpacity(n, _569[s]); - } else { - n.style[s] = _569[s]; - } - } - catch (e) { - dojo.debug(e); - } - } - }; - var _56b = function (_56c) { - this._properties = _56c; - this.diffs = new Array(_56c.length); - dojo.lang.forEach(_56c, function (prop, i) { - if (dojo.lang.isFunction(prop.start)) { - prop.start = prop.start(prop, i); - } - if (dojo.lang.isFunction(prop.end)) { - prop.end = prop.end(prop, i); - } - if (dojo.lang.isArray(prop.start)) { - this.diffs[i] = null; - } else { - if (prop.start instanceof dojo.gfx.color.Color) { - prop.startRgb = prop.start.toRgb(); - prop.endRgb = prop.end.toRgb(); - } else { - this.diffs[i] = prop.end - prop.start; - } - } - }, this); - this.getValue = function (n) { - var ret = {}; - dojo.lang.forEach(this._properties, function (prop, i) { - var _573 = null; - if (dojo.lang.isArray(prop.start)) { - } else { - if (prop.start instanceof dojo.gfx.color.Color) { - _573 = (prop.units || "rgb") + "("; - for (var j = 0; j < prop.startRgb.length; j++) { - _573 += Math.round(((prop.endRgb[j] - prop.startRgb[j]) * n) + prop.startRgb[j]) + (j < prop.startRgb.length - 1 ? "," : ""); - } - _573 += ")"; - } else { - _573 = ((this.diffs[i]) * n) + prop.start + (prop.property != "opacity" ? prop.units || "px" : ""); - } - } - ret[dojo.html.toCamelCase(prop.property)] = _573; - }, this); - return ret; - }; - }; - var anim = new dojo.lfx.Animation({ - beforeBegin: function () { - _55d(_55c); - anim.curve = new _56b(_55c.propertyMap); - }, onAnimate: function (_576) { - dojo.lang.forEach(_55c.nodes, function (node) { - _567(node, _576); - }); - } - }, _55c.duration, null, _55c.easing); - if (_55b) { - for (var x in _55b) { - if (dojo.lang.isFunction(_55b[x])) { - anim.connect(x, anim, _55b[x]); - } - } - } - return anim; -}; -dojo.lfx.html._makeFadeable = function (_579) { - var _57a = function (node) { - if (dojo.render.html.ie) { - if ((node.style.zoom.length == 0) && (dojo.html.getStyle(node, "zoom") == "normal")) { - node.style.zoom = "1"; - } - if ((node.style.width.length == 0) && (dojo.html.getStyle(node, "width") == "auto")) { - node.style.width = "auto"; - } - } - }; - if (dojo.lang.isArrayLike(_579)) { - dojo.lang.forEach(_579, _57a); - } else { - _57a(_579); - } -}; -dojo.lfx.html.fade = function (_57c, _57d, _57e, _57f, _580) { - _57c = dojo.lfx.html._byId(_57c); - var _581 = {property: "opacity"}; - if (!dj_undef("start", _57d)) { - _581.start = _57d.start; - } else { - _581.start = function () { - return dojo.html.getOpacity(_57c[0]); - }; - } - if (!dj_undef("end", _57d)) { - _581.end = _57d.end; - } else { - dojo.raise("dojo.lfx.html.fade needs an end value"); - } - var anim = dojo.lfx.propertyAnimation(_57c, [_581], _57e, _57f); - anim.connect("beforeBegin", function () { - dojo.lfx.html._makeFadeable(_57c); - }); - if (_580) { - anim.connect("onEnd", function () { - _580(_57c, anim); - }); - } - return anim; -}; -dojo.lfx.html.fadeIn = function (_583, _584, _585, _586) { - return dojo.lfx.html.fade(_583, {end: 1}, _584, _585, _586); -}; -dojo.lfx.html.fadeOut = function (_587, _588, _589, _58a) { - return dojo.lfx.html.fade(_587, {end: 0}, _588, _589, _58a); -}; -dojo.lfx.html.fadeShow = function (_58b, _58c, _58d, _58e) { - _58b = dojo.lfx.html._byId(_58b); - dojo.lang.forEach(_58b, function (node) { - dojo.html.setOpacity(node, 0); - }); - var anim = dojo.lfx.html.fadeIn(_58b, _58c, _58d, _58e); - anim.connect("beforeBegin", function () { - if (dojo.lang.isArrayLike(_58b)) { - dojo.lang.forEach(_58b, dojo.html.show); - } else { - dojo.html.show(_58b); - } - }); - return anim; -}; -dojo.lfx.html.fadeHide = function (_591, _592, _593, _594) { - var anim = dojo.lfx.html.fadeOut(_591, _592, _593, function () { - if (dojo.lang.isArrayLike(_591)) { - dojo.lang.forEach(_591, dojo.html.hide); - } else { - dojo.html.hide(_591); - } - if (_594) { - _594(_591, anim); - } - }); - return anim; -}; -dojo.lfx.html.wipeIn = function (_596, _597, _598, _599) { - _596 = dojo.lfx.html._byId(_596); - var _59a = []; - dojo.lang.forEach(_596, function (node) { - var _59c = {}; - var _59d, _59e, _59f; - with (node.style) { - _59d = top; - _59e = left; - _59f = position; - top = "-9999px"; - left = "-9999px"; - position = "absolute"; - display = ""; - } - var _5a0 = dojo.html.getBorderBox(node).height; - with (node.style) { - top = _59d; - left = _59e; - position = _59f; - display = "none"; - } - var anim = dojo.lfx.propertyAnimation(node, { - "height": { - start: 1, end: function () { - return _5a0; - } - } - }, _597, _598); - anim.connect("beforeBegin", function () { - _59c.overflow = node.style.overflow; - _59c.height = node.style.height; - with (node.style) { - overflow = "hidden"; - height = "1px"; - } - dojo.html.show(node); - }); - anim.connect("onEnd", function () { - with (node.style) { - overflow = _59c.overflow; - height = _59c.height; - } - if (_599) { - _599(node, anim); - } - }); - _59a.push(anim); - }); - return dojo.lfx.combine(_59a); -}; -dojo.lfx.html.wipeOut = function (_5a2, _5a3, _5a4, _5a5) { - _5a2 = dojo.lfx.html._byId(_5a2); - var _5a6 = []; - dojo.lang.forEach(_5a2, function (node) { - var _5a8 = {}; - var anim = dojo.lfx.propertyAnimation(node, { - "height": { - start: function () { - return dojo.html.getContentBox(node).height; - }, end: 1 - } - }, _5a3, _5a4, { - "beforeBegin": function () { - _5a8.overflow = node.style.overflow; - _5a8.height = node.style.height; - with (node.style) { - overflow = "hidden"; - } - dojo.html.show(node); - }, "onEnd": function () { - dojo.html.hide(node); - with (node.style) { - overflow = _5a8.overflow; - height = _5a8.height; - } - if (_5a5) { - _5a5(node, anim); - } - } - }); - _5a6.push(anim); - }); - return dojo.lfx.combine(_5a6); -}; -dojo.lfx.html.slideTo = function (_5aa, _5ab, _5ac, _5ad, _5ae) { - _5aa = dojo.lfx.html._byId(_5aa); - var _5af = []; - var _5b0 = dojo.html.getComputedStyle; - if (dojo.lang.isArray(_5ab)) { - dojo.deprecated("dojo.lfx.html.slideTo(node, array)", "use dojo.lfx.html.slideTo(node, {top: value, left: value});", "0.5"); - _5ab = {top: _5ab[0], left: _5ab[1]}; - } - dojo.lang.forEach(_5aa, function (node) { - var top = null; - var left = null; - var init = (function () { - var _5b5 = node; - return function () { - var pos = _5b0(_5b5, "position"); - top = (pos == "absolute" ? node.offsetTop : parseInt(_5b0(node, "top")) || 0); - left = (pos == "absolute" ? node.offsetLeft : parseInt(_5b0(node, "left")) || 0); - if (!dojo.lang.inArray(["absolute", "relative"], pos)) { - var ret = dojo.html.abs(_5b5, true); - dojo.html.setStyleAttributes(_5b5, "position:absolute;top:" + ret.y + "px;left:" + ret.x + "px;"); - top = ret.y; - left = ret.x; - } - }; - })(); - init(); - var anim = dojo.lfx.propertyAnimation(node, { - "top": {start: top, end: (_5ab.top || 0)}, - "left": {start: left, end: (_5ab.left || 0)} - }, _5ac, _5ad, {"beforeBegin": init}); - if (_5ae) { - anim.connect("onEnd", function () { - _5ae(_5aa, anim); - }); - } - _5af.push(anim); - }); - return dojo.lfx.combine(_5af); -}; -dojo.lfx.html.slideBy = function (_5b9, _5ba, _5bb, _5bc, _5bd) { - _5b9 = dojo.lfx.html._byId(_5b9); - var _5be = []; - var _5bf = dojo.html.getComputedStyle; - if (dojo.lang.isArray(_5ba)) { - dojo.deprecated("dojo.lfx.html.slideBy(node, array)", "use dojo.lfx.html.slideBy(node, {top: value, left: value});", "0.5"); - _5ba = {top: _5ba[0], left: _5ba[1]}; - } - dojo.lang.forEach(_5b9, function (node) { - var top = null; - var left = null; - var init = (function () { - var _5c4 = node; - return function () { - var pos = _5bf(_5c4, "position"); - top = (pos == "absolute" ? node.offsetTop : parseInt(_5bf(node, "top")) || 0); - left = (pos == "absolute" ? node.offsetLeft : parseInt(_5bf(node, "left")) || 0); - if (!dojo.lang.inArray(["absolute", "relative"], pos)) { - var ret = dojo.html.abs(_5c4, true); - dojo.html.setStyleAttributes(_5c4, "position:absolute;top:" + ret.y + "px;left:" + ret.x + "px;"); - top = ret.y; - left = ret.x; - } - }; - })(); - init(); - var anim = dojo.lfx.propertyAnimation(node, { - "top": {start: top, end: top + (_5ba.top || 0)}, - "left": {start: left, end: left + (_5ba.left || 0)} - }, _5bb, _5bc).connect("beforeBegin", init); - if (_5bd) { - anim.connect("onEnd", function () { - _5bd(_5b9, anim); - }); - } - _5be.push(anim); - }); - return dojo.lfx.combine(_5be); -}; -dojo.lfx.html.explode = function (_5c8, _5c9, _5ca, _5cb, _5cc) { - var h = dojo.html; - _5c8 = dojo.byId(_5c8); - _5c9 = dojo.byId(_5c9); - var _5ce = h.toCoordinateObject(_5c8, true); - var _5cf = document.createElement("div"); - h.copyStyle(_5cf, _5c9); - if (_5c9.explodeClassName) { - _5cf.className = _5c9.explodeClassName; - } - with (_5cf.style) { - position = "absolute"; - display = "none"; - var _5d0 = h.getStyle(_5c8, "background-color"); - backgroundColor = _5d0 ? _5d0.toLowerCase() : "transparent"; - backgroundColor = (backgroundColor == "transparent") ? "rgb(221, 221, 221)" : backgroundColor; - } - dojo.body().appendChild(_5cf); - with (_5c9.style) { - visibility = "hidden"; - display = "block"; - } - var _5d1 = h.toCoordinateObject(_5c9, true); - with (_5c9.style) { - display = "none"; - visibility = "visible"; - } - var _5d2 = {opacity: {start: 0.5, end: 1}}; - dojo.lang.forEach(["height", "width", "top", "left"], function (type) { - _5d2[type] = {start: _5ce[type], end: _5d1[type]}; - }); - var anim = new dojo.lfx.propertyAnimation(_5cf, _5d2, _5ca, _5cb, { - "beforeBegin": function () { - h.setDisplay(_5cf, "block"); - }, "onEnd": function () { - h.setDisplay(_5c9, "block"); - _5cf.parentNode.removeChild(_5cf); - } - }); - if (_5cc) { - anim.connect("onEnd", function () { - _5cc(_5c9, anim); - }); - } - return anim; -}; -dojo.lfx.html.implode = function (_5d5, end, _5d7, _5d8, _5d9) { - var h = dojo.html; - _5d5 = dojo.byId(_5d5); - end = dojo.byId(end); - var _5db = dojo.html.toCoordinateObject(_5d5, true); - var _5dc = dojo.html.toCoordinateObject(end, true); - var _5dd = document.createElement("div"); - dojo.html.copyStyle(_5dd, _5d5); - if (_5d5.explodeClassName) { - _5dd.className = _5d5.explodeClassName; - } - dojo.html.setOpacity(_5dd, 0.3); - with (_5dd.style) { - position = "absolute"; - display = "none"; - backgroundColor = h.getStyle(_5d5, "background-color").toLowerCase(); - } - dojo.body().appendChild(_5dd); - var _5de = {opacity: {start: 1, end: 0.5}}; - dojo.lang.forEach(["height", "width", "top", "left"], function (type) { - _5de[type] = {start: _5db[type], end: _5dc[type]}; - }); - var anim = new dojo.lfx.propertyAnimation(_5dd, _5de, _5d7, _5d8, { - "beforeBegin": function () { - dojo.html.hide(_5d5); - dojo.html.show(_5dd); - }, "onEnd": function () { - _5dd.parentNode.removeChild(_5dd); - } - }); - if (_5d9) { - anim.connect("onEnd", function () { - _5d9(_5d5, anim); - }); - } - return anim; -}; -dojo.lfx.html.highlight = function (_5e1, _5e2, _5e3, _5e4, _5e5) { - _5e1 = dojo.lfx.html._byId(_5e1); - var _5e6 = []; - dojo.lang.forEach(_5e1, function (node) { - var _5e8 = dojo.html.getBackgroundColor(node); - var bg = dojo.html.getStyle(node, "background-color").toLowerCase(); - var _5ea = dojo.html.getStyle(node, "background-image"); - var _5eb = (bg == "transparent" || bg == "rgba(0, 0, 0, 0)"); - while (_5e8.length > 3) { - _5e8.pop(); - } - var rgb = new dojo.gfx.color.Color(_5e2); - var _5ed = new dojo.gfx.color.Color(_5e8); - var anim = dojo.lfx.propertyAnimation(node, {"background-color": {start: rgb, end: _5ed}}, _5e3, _5e4, { - "beforeBegin": function () { - if (_5ea) { - node.style.backgroundImage = "none"; - } - node.style.backgroundColor = "rgb(" + rgb.toRgb().join(",") + ")"; - }, "onEnd": function () { - if (_5ea) { - node.style.backgroundImage = _5ea; - } - if (_5eb) { - node.style.backgroundColor = "transparent"; - } - if (_5e5) { - _5e5(node, anim); - } - } - }); - _5e6.push(anim); - }); - return dojo.lfx.combine(_5e6); -}; -dojo.lfx.html.unhighlight = function (_5ef, _5f0, _5f1, _5f2, _5f3) { - _5ef = dojo.lfx.html._byId(_5ef); - var _5f4 = []; - dojo.lang.forEach(_5ef, function (node) { - var _5f6 = new dojo.gfx.color.Color(dojo.html.getBackgroundColor(node)); - var rgb = new dojo.gfx.color.Color(_5f0); - var _5f8 = dojo.html.getStyle(node, "background-image"); - var anim = dojo.lfx.propertyAnimation(node, {"background-color": {start: _5f6, end: rgb}}, _5f1, _5f2, { - "beforeBegin": function () { - if (_5f8) { - node.style.backgroundImage = "none"; - } - node.style.backgroundColor = "rgb(" + _5f6.toRgb().join(",") + ")"; - }, "onEnd": function () { - if (_5f3) { - _5f3(node, anim); - } - } - }); - _5f4.push(anim); - }); - return dojo.lfx.combine(_5f4); -}; -dojo.lang.mixin(dojo.lfx, dojo.lfx.html); -dojo.kwCompoundRequire({browser: ["dojo.lfx.html"], dashboard: ["dojo.lfx.html"]}); -dojo.provide("dojo.lfx.*"); - diff --git a/codes/javaee/filter/src/main/webapp/views/jsp/accountException.jsp b/codes/javaee/filter/src/main/webapp/views/jsp/accountException.jsp deleted file mode 100644 index 5fea13e6..00000000 --- a/codes/javaee/filter/src/main/webapp/views/jsp/accountException.jsp +++ /dev/null @@ -1,44 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> - - - - - accountException - - - - -
- ${ message } -
- -
- - - - - - - - - - - - - -
Account
Password
 
-
- - - \ No newline at end of file diff --git a/codes/javaee/filter/src/main/webapp/views/jsp/businessException.jsp b/codes/javaee/filter/src/main/webapp/views/jsp/businessException.jsp deleted file mode 100644 index c43d7cdb..00000000 --- a/codes/javaee/filter/src/main/webapp/views/jsp/businessException.jsp +++ /dev/null @@ -1,24 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> - - - - -businessException - - - - -
-${ message } 返回 -
- - - \ No newline at end of file diff --git a/codes/javaee/filter/src/main/webapp/views/jsp/dispatcher.jsp b/codes/javaee/filter/src/main/webapp/views/jsp/dispatcher.jsp deleted file mode 100644 index 9bc1bea5..00000000 --- a/codes/javaee/filter/src/main/webapp/views/jsp/dispatcher.jsp +++ /dev/null @@ -1,13 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> - - - - - ${ pageContext.request.requestURI } - - - -你正在访问 ${ pageContext.request.requestURI }?${ pageContext.request.queryString }. - - - \ No newline at end of file diff --git a/codes/javaee/filter/src/main/webapp/views/jsp/exception.jsp b/codes/javaee/filter/src/main/webapp/views/jsp/exception.jsp deleted file mode 100644 index dbc981ef..00000000 --- a/codes/javaee/filter/src/main/webapp/views/jsp/exception.jsp +++ /dev/null @@ -1,24 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> - - - - - exception - - - - -
- ${ message } 返回 -
- - - \ No newline at end of file diff --git a/codes/javaee/filter/src/main/webapp/views/jsp/image.jsp b/codes/javaee/filter/src/main/webapp/views/jsp/image.jsp deleted file mode 100644 index 080a5146..00000000 --- a/codes/javaee/filter/src/main/webapp/views/jsp/image.jsp +++ /dev/null @@ -1,19 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> - - - - - Insert title here - - - - -刷新 -直接访问 -
- -request.getHeader("referer"): ${ header['referer'] } - - - diff --git a/codes/javaee/filter/src/main/webapp/views/jsp/index.jsp b/codes/javaee/filter/src/main/webapp/views/jsp/index.jsp deleted file mode 100644 index 8b2052f2..00000000 --- a/codes/javaee/filter/src/main/webapp/views/jsp/index.jsp +++ /dev/null @@ -1,52 +0,0 @@ -<%@ 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 + "/"; -%> - - - - - javaee-filter 首页 - - - -

javaee-filter 首页

-

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

- - - diff --git a/codes/javaee/filter/src/main/webapp/views/jsp/testCacheFilter.jsp b/codes/javaee/filter/src/main/webapp/views/jsp/testCacheFilter.jsp deleted file mode 100644 index 0ab5e289..00000000 --- a/codes/javaee/filter/src/main/webapp/views/jsp/testCacheFilter.jsp +++ /dev/null @@ -1,70 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> - - - - - Insert title here - - - - - - - - - - - - - - \ No newline at end of file diff --git a/codes/javaee/filter/src/main/webapp/views/jsp/testCharacterEncodingFilter.jsp b/codes/javaee/filter/src/main/webapp/views/jsp/testCharacterEncodingFilter.jsp deleted file mode 100644 index 5c7f2561..00000000 --- a/codes/javaee/filter/src/main/webapp/views/jsp/testCharacterEncodingFilter.jsp +++ /dev/null @@ -1,28 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> - - - - - Insert title here - - - -
-您输入了:
-${ param.text }
-
-

-
- - - \ No newline at end of file diff --git a/codes/javaee/filter/src/main/webapp/views/jsp/testExceptionHandlerFilter.jsp b/codes/javaee/filter/src/main/webapp/views/jsp/testExceptionHandlerFilter.jsp deleted file mode 100644 index ef1f3304..00000000 --- a/codes/javaee/filter/src/main/webapp/views/jsp/testExceptionHandlerFilter.jsp +++ /dev/null @@ -1,29 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> -<%@ page import="io.github.dunwu.javaee.filter.exception.AccountException" %> -<%@ page import="io.github.dunwu.javaee.filter.exception.BusinessException" %> -<% - String action = request.getParameter("action"); - - if ("businessException".equals(action)) { - throw new BusinessException("业务操作失败. "); - } else if ("accountException".equals(action)) { - throw new AccountException("您需要登陆后再进行此项操作. "); - } else if ("exception".equals(action)) { - Integer.parseInt(""); - } - -%> - - - - - exceptionHandler - - - -test BusinessException
-test AccountException
-test Exception
- - - diff --git a/codes/javaee/filter/src/main/webapp/views/jsp/testGZipFilter.jsp b/codes/javaee/filter/src/main/webapp/views/jsp/testGZipFilter.jsp deleted file mode 100644 index c7991dfd..00000000 --- a/codes/javaee/filter/src/main/webapp/views/jsp/testGZipFilter.jsp +++ /dev/null @@ -1,71 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> - -<%@page import="java.net.URL" %> -<%@page import="java.net.URLConnection" %> -<%@page import="java.text.NumberFormat" %> - - - - Insert title here - - - -<%! - public void test(JspWriter out, String url) throws Exception { - - // 模拟支持 GZIP 的浏览器 - URLConnection connGzip = new URL(url).openConnection(); - connGzip.setRequestProperty("Accept-Encoding", "gzip"); - int lengthGzip = connGzip.getContentLength(); - - // 模拟不支持 GZIP 的浏览器 - URLConnection connCommon = new URL(url).openConnection(); - int lengthCommon = connCommon.getContentLength(); - - double rate = new Double(lengthGzip) / lengthCommon; - - out.println(""); - out.println(" "); - out.println(" "); - out.println(" "); - out.println(" "); - out.println(" "); - out.println(" "); - out.println(" "); - out.println(" "); - out.println("
网址: " + url + "
压缩后:" + lengthGzip + " byte压缩前:" + lengthCommon + " byte比率:" + NumberFormat.getPercentInstance().format(rate) + "
"); - } -%> -<% - String[] urls = { - "http://localhost:9899/views/js/dojo.js", - "http://localhost:9899/views/images/image.jsp", - "http://localhost:9899/views/images/winter.jpg", - "http://www.baidu.com", - "http://www.google.cn", - }; - for (String url : urls) { - test(out, url); - } -%> - - - \ No newline at end of file diff --git a/codes/javaee/filter/src/main/webapp/views/jsp/testOutputReplaceFilter.jsp b/codes/javaee/filter/src/main/webapp/views/jsp/testOutputReplaceFilter.jsp deleted file mode 100644 index 2f922496..00000000 --- a/codes/javaee/filter/src/main/webapp/views/jsp/testOutputReplaceFilter.jsp +++ /dev/null @@ -1,19 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> - - - - - Insert title here - - - -Chna
-
-色情
-赌博
-情色
-
-www.baidu.com.cn
- - - \ No newline at end of file diff --git a/codes/javaee/filter/src/main/webapp/views/jsp/testUploadFilter.jsp b/codes/javaee/filter/src/main/webapp/views/jsp/testUploadFilter.jsp deleted file mode 100644 index 372baad6..00000000 --- a/codes/javaee/filter/src/main/webapp/views/jsp/testUploadFilter.jsp +++ /dev/null @@ -1,41 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> - -<%@page import="java.io.File" %> - - - - Insert title here - - - - -header['Content-type'] = ${ header['Content-type'] } - -
- - <%= request.getParameter("text1") %>
- - <% - File file1 = (File) request.getAttribute("file1"); - if (file1 != null) - out.println("
文件: " + file1 + ",
大小: " + file1.length()); - %>

- - <%= request.getParameter("text2") %>
- - <% - File file2 = (File) request.getAttribute("file2"); - if (file2 != null) - out.println("
文件: " + file2 + ",
大小: " + file2.length()); - %>

- - - -
- - - \ No newline at end of file diff --git a/codes/javaee/filter/src/main/webapp/views/jsp/testWaterMarkFilter.jsp b/codes/javaee/filter/src/main/webapp/views/jsp/testWaterMarkFilter.jsp deleted file mode 100644 index e0bb8d34..00000000 --- a/codes/javaee/filter/src/main/webapp/views/jsp/testWaterMarkFilter.jsp +++ /dev/null @@ -1,16 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> - - - - - 图片过滤器示例 - - - - -刷新 -
-${ header } - - - \ No newline at end of file diff --git a/codes/javaee/filter/src/main/webapp/views/xml/book.xml b/codes/javaee/filter/src/main/webapp/views/xml/book.xml deleted file mode 100644 index ab537675..00000000 --- a/codes/javaee/filter/src/main/webapp/views/xml/book.xml +++ /dev/null @@ -1,6 +0,0 @@ - - Bruce - Java 编程思想 - 计算机经典书籍系列丛书 - $100.00 - diff --git a/codes/javaee/filter/src/main/webapp/views/xml/book.xsl b/codes/javaee/filter/src/main/webapp/views/xml/book.xsl deleted file mode 100644 index 30da7f7e..00000000 --- a/codes/javaee/filter/src/main/webapp/views/xml/book.xsl +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - 图书资料: - - - - - - - - - - - - - - - - - -
作者: - -
书名: - -
类别: - -
定价: - -
- - -
-
diff --git a/codes/javaee/filter/src/main/webapp/views/xml/demo.xml b/codes/javaee/filter/src/main/webapp/views/xml/demo.xml deleted file mode 100644 index 0976065f..00000000 --- a/codes/javaee/filter/src/main/webapp/views/xml/demo.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - 你会Ajax吗 - - - - - - - - - 我们用的是dojo框架的 - - - - - - - - - response.setHeader("Content-Disposition", - "attachment;filename=" - + URLEncoder.encode(newFile.getName(), - "UTF-8")); - response.setHeader("Connection", "close"); - response.setContentLength((int)newFile.length()); - resp - - - - - - - - - - 先 out.clear() - - - - - - - - - ok - - - - - - - - - - response.setContentLength((int)newFile.length()); - response.setHeader("Content-Type", - "application/octet-stream"); - - - - - - - - - - application/vnd.ms-excel - - diff --git a/codes/javaee/filter/src/main/webapp/views/xml/messageLog.xsl b/codes/javaee/filter/src/main/webapp/views/xml/messageLog.xsl deleted file mode 100644 index 5b236407..00000000 Binary files a/codes/javaee/filter/src/main/webapp/views/xml/messageLog.xsl and /dev/null differ diff --git a/codes/javaee/filter/src/main/webapp/views/xml/thinkInJava.xml b/codes/javaee/filter/src/main/webapp/views/xml/thinkInJava.xml deleted file mode 100644 index e3cb21c7..00000000 --- a/codes/javaee/filter/src/main/webapp/views/xml/thinkInJava.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - Bruce - Java 编程思想 - 计算机经典书籍系列丛书 - $100.00 - diff --git a/codes/javaee/filter/src/main/webapp/views/xml/xml.xsl b/codes/javaee/filter/src/main/webapp/views/xml/xml.xsl deleted file mode 100644 index edd374d8..00000000 --- a/codes/javaee/filter/src/main/webapp/views/xml/xml.xsl +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/codes/javaee/filter/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java b/codes/javaee/filter/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java deleted file mode 100644 index 6d275e8c..00000000 --- a/codes/javaee/filter/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.github.dunwu.javaee.server; - -import java.util.ArrayList; - -import org.apache.commons.lang3.StringUtils; -import org.assertj.core.util.Lists; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.WebAppClassLoader; -import org.eclipse.jetty.webapp.WebAppContext; - -/** - * JettyFactory 可以工作在 Eclipse 和 Intellij 中,用来启动 jetty 服务。 Intellij - * 并不支持jetty,所以要想类似eclipse一样的使用jetty,需要配置webdefault.xml。 - * - * @author Zhang Peng - */ -@SuppressWarnings("unused") -public class JettyFactory { - private static final int PORT = 8080; - private static final String CONTEXT = "/"; - private static final String RESOURCE_BASE_PATH = "src/main/webapp"; - private static final String WEB_XML_PATH = "/WEB-INF/web.xml"; - private static final String[] TLD_JAR_NAMES = - new String[]{"sitemesh", "spring-webmvc", "shiro-web", "tiles"}; - private static final String WINDOWS_WEBDEFAULT_PATH = "jetty/webdefault.xml"; - - public static final int IDE_ECLIPSE = 0; - public static final int IDE_INTELLIJ = 1; - - - public static Server initServer() { - Profiles.setProfileAsSystemProperty(Profiles.DEVELOPMENT); - WebAppContext webAppContext = new WebAppContext(); - Server server = new Server(PORT); - server.setHandler(webAppContext); - return server; - } - - public static void initWebAppContext(Server server, int type) throws Exception { - System.out.println("[INFO] Application loading"); - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - webAppContext.setContextPath(CONTEXT); - webAppContext.setResourceBase(getAbsolutePath() + RESOURCE_BASE_PATH); - webAppContext.setDescriptor(getAbsolutePath() + RESOURCE_BASE_PATH + WEB_XML_PATH); - - if (IDE_INTELLIJ == type) { - webAppContext.setDefaultsDescriptor(WINDOWS_WEBDEFAULT_PATH); - supportJspAndSetTldJarNames(server, TLD_JAR_NAMES); - } else { - webAppContext.setParentLoaderPriority(true); - } - - System.out.println("[INFO] Application loaded"); - } - - public static void reloadWebAppContext(Server server) throws Exception { - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - System.out.println("[INFO] Application reloading"); - webAppContext.stop(); - WebAppClassLoader classLoader = new WebAppClassLoader(webAppContext); - classLoader.addClassPath(getAbsolutePath() + "target/classes"); - classLoader.addClassPath(getAbsolutePath() + "target/test-classes"); - webAppContext.setClassLoader(classLoader); - webAppContext.start(); - System.out.println("[INFO] Application reloaded"); - } - - - public static void startServer(Server server) throws Exception { - System.out.println("[HINT] Don't forget to set -XX:MaxPermSize=128m"); - server.start(); - System.out.println("Server running at http://localhost:" + PORT + CONTEXT); - System.out.println("[HINT] Hit Enter to reload the application quickly"); - } - - public static String getAbsolutePath() { - String path = null; - String folderPath = JettyFactory.class.getProtectionDomain().getCodeSource().getLocation() - .getPath().substring(1); - if (folderPath.indexOf("target") > 0) { - path = folderPath.substring(0, folderPath.indexOf("target")); - } - return path; - } - - public static void supportJspAndSetTldJarNames(Server server, String... jarNames) { - WebAppContext context = (WebAppContext) server.getHandler(); - // This webapp will use jsps and jstl. We need to enable the AnnotationConfiguration in - // order to correctly set up the jsp container - org.eclipse.jetty.webapp.Configuration.ClassList classlist = - org.eclipse.jetty.webapp.Configuration.ClassList.setServerDefault(server); - classlist.addBefore("org.eclipse.jetty.webapp.JettyWebXmlConfiguration", - "org.eclipse.jetty.annotations.AnnotationConfiguration"); - // Set the ContainerIncludeJarPattern so that jetty examines these container-path jars for - // tlds, web-fragments etc. - // If you omit the jar that contains the jstl .tlds, the jsp engine will scan for them - // instead. - ArrayList jarNameExprssions = Lists.newArrayList(".*/[^/]*servlet-api-[^/]*\\.jar$", - ".*/javax.servlet.jsp.jstl-.*\\.jar$", ".*/[^/]*taglibs.*\\.jar$"); - - for (String jarName : jarNames) { - jarNameExprssions.add(".*/" + jarName + "-[^/]*\\.jar$"); - } - - context.setAttribute("org.eclipse.jetty.io.github.dunwu.javaee.server.webapp.ContainerIncludeJarPattern", - StringUtils.join(jarNameExprssions, '|')); - } -} diff --git a/codes/javaee/filter/src/test/java/io/github/dunwu/javaee/server/Profiles.java b/codes/javaee/filter/src/test/java/io/github/dunwu/javaee/server/Profiles.java deleted file mode 100644 index 3a8ee7c4..00000000 --- a/codes/javaee/filter/src/test/java/io/github/dunwu/javaee/server/Profiles.java +++ /dev/null @@ -1,29 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2005, 2014 springside.github.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - *******************************************************************************/ -package io.github.dunwu.javaee.server; - -/** - * Spring profile 常用方法与profile名称。 - * - * @author calvin - */ -public class Profiles { - - public static final String ACTIVE_PROFILE = "spring.profiles.active"; - public static final String DEFAULT_PROFILE = "spring.profiles.default"; - - public static final String PRODUCTION = "production"; - public static final String DEVELOPMENT = "development"; - public static final String UNIT_TEST = "test"; - public static final String FUNCTIONAL_TEST = "functional"; - - /** - * 在Spring启动前,设置profile的环境变量。 - */ - public static void setProfileAsSystemProperty(String profile) { - System.setProperty(ACTIVE_PROFILE, profile); - } -} diff --git a/codes/javaee/filter/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java b/codes/javaee/filter/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java deleted file mode 100644 index 2936b162..00000000 --- a/codes/javaee/filter/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.github.dunwu.javaee.server; - -import org.eclipse.jetty.server.Server; - -/** - * 快速启动 jetty 服务器,方便测试 - * - * @author Zhang Peng - */ -public class QuickStartServer { - // private static int STARTUP_TYPE = JettyFactory.IDE_ECLIPSE; - private static int STARTUP_TYPE = JettyFactory.IDE_INTELLIJ; - - public static void main(String[] args) throws Exception { - Server server = JettyFactory.initServer(); - JettyFactory.initWebAppContext(server, STARTUP_TYPE); - - try { - JettyFactory.startServer(server); - - // 等待用户输入回车重载应用 - while (true) { - char c = (char) System.in.read(); - if (c == '\n') { - JettyFactory.reloadWebAppContext(server); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.exit(-1); - } - } -} diff --git a/codes/javaee/filter/src/test/resources/jetty/webdefault.xml b/codes/javaee/filter/src/test/resources/jetty/webdefault.xml deleted file mode 100644 index 65ec9cae..00000000 --- a/codes/javaee/filter/src/test/resources/jetty/webdefault.xml +++ /dev/null @@ -1,534 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Default web.xml file. - This file is applied to a Web application before it's own WEB_INF/web.xml file - - - - - - - - org.eclipse.jetty.servlet.listener.ELContextCleaner - - - - - - - - org.eclipse.jetty.servlet.listener.IntrospectorCleaner - - - - - - - - - - - - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - aliases - false - - - acceptRanges - true - - - dirAllowed - true - - - welcomeServlets - false - - - redirectWelcome - false - - - maxCacheSize - 256000000 - - - maxCachedFileSize - 200000000 - - - maxCachedFiles - 2048 - - - gzip - false - - - etags - false - - - useFileMappedBuffer - false - - - - 0 - - - - default - / - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jsp - org.eclipse.jetty.jsp.JettyJspServlet - - logVerbosityLevel - DEBUG - - - fork - false - - - xpoweredBy - false - - - compilerTargetVM - 1.7 - - - compilerSourceVM - 1.7 - - - 0 - - - - jsp - *.jsp - *.jspf - *.jspx - *.xsp - *.JSP - *.JSPF - *.JSPX - *.XSP - - - - - - - - 30 - - - - - - - - - - - - - - - index.html - index.htm - index.jsp - - - - - - - - ar - ISO-8859-6 - - - be - ISO-8859-5 - - - bg - ISO-8859-5 - - - ca - ISO-8859-1 - - - cs - ISO-8859-2 - - - da - ISO-8859-1 - - - de - ISO-8859-1 - - - el - ISO-8859-7 - - - en - ISO-8859-1 - - - es - ISO-8859-1 - - - et - ISO-8859-1 - - - fi - ISO-8859-1 - - - fr - ISO-8859-1 - - - hr - ISO-8859-2 - - - hu - ISO-8859-2 - - - is - ISO-8859-1 - - - it - ISO-8859-1 - - - iw - ISO-8859-8 - - - ja - Shift_JIS - - - ko - EUC-KR - - - lt - ISO-8859-2 - - - lv - ISO-8859-2 - - - mk - ISO-8859-5 - - - nl - ISO-8859-1 - - - no - ISO-8859-1 - - - pl - ISO-8859-2 - - - pt - ISO-8859-1 - - - ro - ISO-8859-2 - - - ru - ISO-8859-5 - - - sh - ISO-8859-5 - - - sk - ISO-8859-2 - - - sl - ISO-8859-2 - - - sq - ISO-8859-2 - - - sr - ISO-8859-5 - - - sv - ISO-8859-1 - - - tr - ISO-8859-9 - - - uk - ISO-8859-5 - - - zh - GB2312 - - - zh_TW - Big5 - - - - - - - - - Disable TRACE - / - TRACE - - - - - - Enable everything but TRACE - / - TRACE - - - - - diff --git a/codes/javaee/jsp/pom.xml b/codes/javaee/jsp/pom.xml deleted file mode 100644 index eb12d448..00000000 --- a/codes/javaee/jsp/pom.xml +++ /dev/null @@ -1,123 +0,0 @@ - - - 4.0.0 - - - io.github.dunwu - javaee-notes-jsp - 1.0.0 - war - - - - - javaee-notes-jsp - javaee 学习笔记之 jsp - - - - - - UTF-8 - 1.7 - ${java.version} - ${java.version} - - - 9.3.2.v20150730 - - - - - ch.qos.logback - logback-classic - 1.1.3 - - - ch.qos.logback - logback-core - 1.1.3 - - - org.logback-extensions - logback-ext-spring - 0.1.2 - - - org.slf4j - jcl-over-slf4j - 1.7.12 - - - - - - commons-fileupload - commons-fileupload - 1.3.1 - - - commons-io - commons-io - 2.5 - - - org.apache.commons - commons-lang3 - 3.4 - - - - - - javax.servlet - javax.servlet-api - 3.1.0 - - - - - - org.eclipse.jetty - jetty-webapp - ${jetty.version} - - - org.eclipse.jetty - jetty-annotations - ${jetty.version} - - - org.eclipse.jetty - apache-jsp - ${jetty.version} - - - org.eclipse.jetty - apache-jstl - ${jetty.version} - - - - - - junit - junit - 4.12 - - - org.assertj - assertj-core - 3.4.1 - - - - - - - - - - - diff --git a/codes/javaee/jsp/src/main/java/com/sun/products/applet/demo/Graph.java b/codes/javaee/jsp/src/main/java/com/sun/products/applet/demo/Graph.java deleted file mode 100644 index e8126fee..00000000 --- a/codes/javaee/jsp/src/main/java/com/sun/products/applet/demo/Graph.java +++ /dev/null @@ -1,452 +0,0 @@ -package com.sun.products.applet.demo; -/* - * @(#)Graph.java 1.9 99/08/04 - * - * Copyright (c) 1997, 1998 Sun Microsystems, Inc. All Rights Reserved. - * - * Sun grants you ("Licensee") a non-exclusive, royalty free, license to use, - * modify and redistribute this software in source and binary code form, - * provided that i) this copyright notice and license appear on all copies of - * the software; and ii) Licensee does not utilize the software in a manner - * which is disparaging to Sun. - * - * This software is provided "AS IS," without a warranty of any kind. ALL - * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING ANY - * IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE OR - * NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN AND ITS LICENSORS SHALL NOT BE - * LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING - * OR DISTRIBUTING THE SOFTWARE OR ITS DERIVATIVES. IN NO EVENT WILL SUN OR ITS - * LICENSORS BE LIABLE FOR ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, - * INDIRECT, SPECIAL, CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER - * CAUSED AND REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF - * OR INABILITY TO USE SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGES. - * - * This software is not designed or intended for use in on-line control of - * aircraft, air traffic, aircraft navigation or aircraft communications; or in - * the design, construction, operation or maintenance of any nuclear - * facility. Licensee represents and warrants that it will not use or - * redistribute the Software for such purposes. - */ - -import java.applet.Applet; -import java.awt.BorderLayout; -import java.awt.Button; -import java.awt.Checkbox; -import java.awt.Color; -import java.awt.Dimension; -import java.awt.FontMetrics; -import java.awt.Graphics; -import java.awt.Image; -import java.awt.Panel; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.ItemEvent; -import java.awt.event.ItemListener; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; -import java.awt.event.MouseMotionListener; -import java.util.StringTokenizer; - - -class Node { - double x; - double y; - - double dx; - double dy; - - boolean fixed; - - String lbl; -} - - -class Edge { - int from; - int to; - - double len; -} - - -class GraphPanel extends Panel - implements Runnable, MouseListener, MouseMotionListener { - /** - * - */ - private static final long serialVersionUID = -6414075534397495418L; - Graph graph; - int nnodes; - Node nodes[] = new Node[100]; - - int nedges; - Edge edges[] = new Edge[200]; - - Thread relaxer; - boolean stress; - boolean random; - - GraphPanel(Graph graph) { - this.graph = graph; - addMouseListener(this); - } - - int findNode(String lbl) { - for (int i = 0 ; i < nnodes ; i++) { - if (nodes[i].lbl.equals(lbl)) { - return i; - } - } - return addNode(lbl); - } - int addNode(String lbl) { - Node n = new Node(); - n.x = 10 + 380*Math.random(); - n.y = 10 + 380*Math.random(); - n.lbl = lbl; - nodes[nnodes] = n; - return nnodes++; - } - void addEdge(String from, String to, int len) { - Edge e = new Edge(); - e.from = findNode(from); - e.to = findNode(to); - e.len = len; - edges[nedges++] = e; - } - - public void run() { - Thread me = Thread.currentThread(); - while (relaxer == me) { - relax(); - if (random && (Math.random() < 0.03)) { - Node n = nodes[(int)(Math.random() * nnodes)]; - if (!n.fixed) { - n.x += 100*Math.random() - 50; - n.y += 100*Math.random() - 50; - } - graph.play(graph.getCodeBase(), "audio/drip.au"); - } - try { - Thread.sleep(100); - } catch (InterruptedException e) { - break; - } - } - } - - synchronized void relax() { - for (int i = 0 ; i < nedges ; i++) { - Edge e = edges[i]; - double vx = nodes[e.to].x - nodes[e.from].x; - double vy = nodes[e.to].y - nodes[e.from].y; - double len = Math.sqrt(vx * vx + vy * vy); - len = (len == 0) ? .0001 : len; - double f = (edges[i].len - len) / (len * 3); - double dx = f * vx; - double dy = f * vy; - - nodes[e.to].dx += dx; - nodes[e.to].dy += dy; - nodes[e.from].dx += -dx; - nodes[e.from].dy += -dy; - } - - for (int i = 0 ; i < nnodes ; i++) { - Node n1 = nodes[i]; - double dx = 0; - double dy = 0; - - for (int j = 0 ; j < nnodes ; j++) { - if (i == j) { - continue; - } - Node n2 = nodes[j]; - double vx = n1.x - n2.x; - double vy = n1.y - n2.y; - double len = vx * vx + vy * vy; - if (len == 0) { - dx += Math.random(); - dy += Math.random(); - } else if (len < 100*100) { - dx += vx / len; - dy += vy / len; - } - } - double dlen = dx * dx + dy * dy; - if (dlen > 0) { - dlen = Math.sqrt(dlen) / 2; - n1.dx += dx / dlen; - n1.dy += dy / dlen; - } - } - - Dimension d = getSize(); - for (int i = 0 ; i < nnodes ; i++) { - Node n = nodes[i]; - if (!n.fixed) { - n.x += Math.max(-5, Math.min(5, n.dx)); - n.y += Math.max(-5, Math.min(5, n.dy)); - } - if (n.x < 0) { - n.x = 0; - } else if (n.x > d.width) { - n.x = d.width; - } - if (n.y < 0) { - n.y = 0; - } else if (n.y > d.height) { - n.y = d.height; - } - n.dx /= 2; - n.dy /= 2; - } - repaint(); - } - - Node pick; - boolean pickfixed; - Image offscreen; - Dimension offscreensize; - Graphics offgraphics; - - final Color fixedColor = Color.red; - final Color selectColor = Color.pink; - final Color edgeColor = Color.black; - final Color nodeColor = new Color(250, 220, 100); - final Color stressColor = Color.darkGray; - final Color arcColor1 = Color.black; - final Color arcColor2 = Color.pink; - final Color arcColor3 = Color.red; - - public void paintNode(Graphics g, Node n, FontMetrics fm) { - int x = (int)n.x; - int y = (int)n.y; - g.setColor((n == pick) ? selectColor : (n.fixed ? fixedColor : nodeColor)); - int w = fm.stringWidth(n.lbl) + 10; - int h = fm.getHeight() + 4; - g.fillRect(x - w/2, y - h / 2, w, h); - g.setColor(Color.black); - g.drawRect(x - w/2, y - h / 2, w-1, h-1); - g.drawString(n.lbl, x - (w-10)/2, (y - (h-4)/2) + fm.getAscent()); - } - - public synchronized void update(Graphics g) { - Dimension d = getSize(); - if ((offscreen == null) || (d.width != offscreensize.width) || (d.height != offscreensize.height)) { - offscreen = createImage(d.width, d.height); - offscreensize = d; - if (offgraphics != null) { - offgraphics.dispose(); - } - offgraphics = offscreen.getGraphics(); - offgraphics.setFont(getFont()); - } - - offgraphics.setColor(getBackground()); - offgraphics.fillRect(0, 0, d.width, d.height); - for (int i = 0 ; i < nedges ; i++) { - Edge e = edges[i]; - int x1 = (int)nodes[e.from].x; - int y1 = (int)nodes[e.from].y; - int x2 = (int)nodes[e.to].x; - int y2 = (int)nodes[e.to].y; - int len = (int)Math.abs(Math.sqrt((x1-x2)*(x1-x2) + (y1-y2)*(y1-y2)) - e.len); - offgraphics.setColor((len < 10) ? arcColor1 : (len < 20 ? arcColor2 : arcColor3)) ; - offgraphics.drawLine(x1, y1, x2, y2); - if (stress) { - String lbl = String.valueOf(len); - offgraphics.setColor(stressColor); - offgraphics.drawString(lbl, x1 + (x2-x1)/2, y1 + (y2-y1)/2); - offgraphics.setColor(edgeColor); - } - } - - FontMetrics fm = offgraphics.getFontMetrics(); - for (int i = 0 ; i < nnodes ; i++) { - paintNode(offgraphics, nodes[i], fm); - } - g.drawImage(offscreen, 0, 0, null); - } - - //1.1 event handling - public void mouseClicked(MouseEvent e) { - } - - public void mousePressed(MouseEvent e) { - addMouseMotionListener(this); - double bestdist = Double.MAX_VALUE; - int x = e.getX(); - int y = e.getY(); - for (int i = 0 ; i < nnodes ; i++) { - Node n = nodes[i]; - double dist = (n.x - x) * (n.x - x) + (n.y - y) * (n.y - y); - if (dist < bestdist) { - pick = n; - bestdist = dist; - } - } - pickfixed = pick.fixed; - pick.fixed = true; - pick.x = x; - pick.y = y; - repaint(); - e.consume(); - } - - public void mouseReleased(MouseEvent e) { - removeMouseMotionListener(this); - if (pick != null) { - pick.x = e.getX(); - pick.y = e.getY(); - pick.fixed = pickfixed; - pick = null; - } - repaint(); - e.consume(); - } - - public void mouseEntered(MouseEvent e) { - } - - public void mouseExited(MouseEvent e) { - } - - public void mouseDragged(MouseEvent e) { - pick.x = e.getX(); - pick.y = e.getY(); - repaint(); - e.consume(); - } - - public void mouseMoved(MouseEvent e) { - } - - public void start() { - relaxer = new Thread(this); - relaxer.start(); - } - - public void stop() { - relaxer = null; - } - -} - - -public class Graph extends Applet implements ActionListener, ItemListener { - - /** - * - */ - private static final long serialVersionUID = 9208137208697128121L; - GraphPanel panel; - Panel controlPanel; - - Button scramble = new Button("Scramble"); - Button shake = new Button("Shake"); - Checkbox stress = new Checkbox("Stress"); - Checkbox random = new Checkbox("Random"); - - public void init() { - setLayout(new BorderLayout()); - - panel = new GraphPanel(this); - add("Center", panel); - controlPanel = new Panel(); - add("South", controlPanel); - - controlPanel.add(scramble); scramble.addActionListener(this); - controlPanel.add(shake); shake.addActionListener(this); - controlPanel.add(stress); stress.addItemListener(this); - controlPanel.add(random); random.addItemListener(this); - - String edges = getParameter("edges"); - for (StringTokenizer t = new StringTokenizer(edges, ",") ; t.hasMoreTokens() ; ) { - String str = t.nextToken(); - int i = str.indexOf('-'); - if (i > 0) { - int len = 50; - int j = str.indexOf('/'); - if (j > 0) { - len = Integer.valueOf(str.substring(j+1)).intValue(); - str = str.substring(0, j); - } - panel.addEdge(str.substring(0,i), str.substring(i+1), len); - } - } - Dimension d = getSize(); - String center = getParameter("center"); - if (center != null){ - Node n = panel.nodes[panel.findNode(center)]; - n.x = d.width / 2; - n.y = d.height / 2; - n.fixed = true; - } - } - - public void destroy() { - remove(panel); - remove(controlPanel); - } - - public void start() { - panel.start(); - } - - public void stop() { - panel.stop(); - } - - public void actionPerformed(ActionEvent e) { - Object src = e.getSource(); - - if (src == scramble) { - play(getCodeBase(), "audio/computer.au"); - Dimension d = getSize(); - for (int i = 0 ; i < panel.nnodes ; i++) { - Node n = panel.nodes[i]; - if (!n.fixed) { - n.x = 10 + (d.width-20)*Math.random(); - n.y = 10 + (d.height-20)*Math.random(); - } - } - return; - } - - if (src == shake) { - play(getCodeBase(), "audio/gong.au"); -// Dimension d = getSize(); - for (int i = 0 ; i < panel.nnodes ; i++) { - Node n = panel.nodes[i]; - if (!n.fixed) { - n.x += 80*Math.random() - 40; - n.y += 80*Math.random() - 40; - } - } - } - - } - - public void itemStateChanged(ItemEvent e) { - Object src = e.getSource(); - boolean on = e.getStateChange() == ItemEvent.SELECTED; - if (src == stress) panel.stress = on; - else if (src == random) panel.random = on; - } - - public String getAppletInfo() { - return "Title: GraphLayout \nAuthor: "; - } - - public String[][] getParameterInfo() { - String[][] info = { - {"edges", "delimited string", "A comma-delimited list of all the edges. It takes the form of 'C-N1,C-N2,C-N3,C-NX,N1-N2/M12,N2-N3/M23,N3-NX/M3X,...' where C is the name of center node (see 'center' parameter) and NX is a node attached to the center node. For the edges connecting nodes to eachother (and not to the center node) you may (optionally) specify a length MXY separated from the edge name by a forward slash."}, - {"center", "string", "The name of the center node."} - }; - return info; - } - -} - diff --git a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Counter.java b/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Counter.java deleted file mode 100644 index fbfe6bf5..00000000 --- a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Counter.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.github.dunwu.javaee.jsp.bean; - -public class Counter { - private int count; - - public int getCount() { - return ++count; - } - - public void setCount(int count) { - this.count = count; - } -} diff --git a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Message.java b/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Message.java deleted file mode 100644 index b7465c53..00000000 --- a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Message.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.github.dunwu.javaee.jsp.bean; - -public class Message { - private String content; - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - -} diff --git a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Person.java b/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Person.java deleted file mode 100644 index 043dc81a..00000000 --- a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Person.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.github.dunwu.javaee.jsp.bean; - -public class Person { - private String name; - private int age; - private String sex; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - public String getSex() { - return sex; - } - - public void setSex(String sex) { - this.sex = sex; - } -} diff --git a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/IpUtil.java b/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/IpUtil.java deleted file mode 100644 index eb35a60c..00000000 --- a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/IpUtil.java +++ /dev/null @@ -1,16 +0,0 @@ -package io.github.dunwu.javaee.jsp.util; - -import io.github.dunwu.javaee.jsp.util.ip.IPSeeker; - -public class IpUtil { - - public static String getIpAddress(String ip) { - try{ - return IPSeeker.getInstance().getAddress(ip); - }catch(Exception e){ - e.printStackTrace(); - } - return "δ֪����"; - } - -} diff --git a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/IPEntry.java b/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/IPEntry.java deleted file mode 100644 index 7b00299b..00000000 --- a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/IPEntry.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.github.dunwu.javaee.jsp.util.ip; - -/* -* LumaQQ - Java QQ Client -* -* Copyright (C) 2004 luma -* -* This program is free software; you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation; either version 2 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program; if not, write to the Free Software -* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -*/ - - - -/** - *
- * һ��IP��Χ��¼�������������Һ�����Ҳ������ʼIP�ͽ���IP
- * 
- * - * @author ����� - */ -public class IPEntry { - public String beginIp; - public String endIp; - public String country; - public String area; - - /** - * ���캯�� - */ - public IPEntry() { - beginIp = endIp = country = area = ""; - } - - public String toString(){ - return this.area+" "+this.country+" IP��Χ:"+this.beginIp+"-"+this.endIp; - } -} - - - diff --git a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/IPSeeker.java b/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/IPSeeker.java deleted file mode 100644 index 523cc25e..00000000 --- a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/IPSeeker.java +++ /dev/null @@ -1,658 +0,0 @@ -package io.github.dunwu.javaee.jsp.util.ip; - -/* -* LumaQQ - Java QQ Client -* -* Copyright (C) 2004 luma -* -* This program is free software; you can redistribute it and/or modify -* it under the terms of the GNU General Public License as published by -* the Free Software Foundation; either version 2 of the License, or -* (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program; if not, write to the Free Software -* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -*/ - - - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.nio.ByteOrder; -import java.nio.MappedByteBuffer; -import java.nio.channels.FileChannel; -import java.util.ArrayList; -import java.util.Hashtable; -import java.util.List; - - - - -/** - *
- * ������ȡQQwry.dat�ļ����Ը���ip��ú���λ�ã�QQwry.dat�ĸ�ʽ��
- * һ. �ļ�ͷ����8�ֽ�
- * 	   1. ��һ����ʼIP�ľ���ƫ�ƣ� 4�ֽ�
- *     2. ���һ����ʼIP�ľ���ƫ�ƣ� 4�ֽ�
- * ��. "������ַ/����/����"��¼��
- *     ���ֽ�ip��ַ�����ÿһ����¼�ֳ���������
- *     1. ���Ҽ�¼
- *     2. ������¼
- *     ���ǵ�����¼�Dz�һ���еġ����ҹ��Ҽ�¼�͵�����¼����������ʽ
- *     1. ��0�������ַ���
- *     2. 4���ֽڣ�һ���ֽڿ���Ϊ0x1��0x2
- * 		  a. Ϊ0x1ʱ����ʾ�ھ���ƫ�ƺ󻹸���һ������ļ�¼��ע���Ǿ���ƫ��֮�󣬶��������ĸ��ֽ�֮��
- *        b. Ϊ0x2ʱ����ʾ�ھ���ƫ�ƺ�û�������¼
- *        ����Ϊ0x1����0x2���������ֽڶ���ʵ�ʹ��������ļ��ھ���ƫ��
- * 		  ����ǵ�����¼��0x1��0x2�ĺ��岻����������������������ֽڣ�Ҳ�϶��Ǹ���3���ֽ�ƫ�ƣ��������
- *        ��Ϊ0��β�ַ���
- * ��. "��ʼ��ַ/������ַƫ��"��¼��
- *     1. ÿ����¼7�ֽڣ�������ʼ��ַ��С��������
- *        a. ��ʼIP��ַ��4�ֽ�
- *        b. ����ip��ַ�ľ���ƫ�ƣ�3�ֽ�
- *
- * ע�⣬����ļ����ip��ַ�����е�ƫ����������little-endian��ʽ����java�Dz���
- * big-endian��ʽ�ģ�Ҫע��ת��
- * 
- * - * @author ����� - */ -public class IPSeeker { - /** - *
-	 * ������װip�����Ϣ��Ŀǰֻ�������ֶΣ�ip���ڵĹ��Һ͵���
-	 * 
- * - * @author ����� - */ - private class IPLocation { - public String country; - public String area; - - public IPLocation() { - country = area = ""; - } - - public IPLocation getCopy() { - IPLocation ret = new IPLocation(); - ret.country = country; - ret.area = area; - return ret; - } - } - - private static final File IP_FILE = new File(IPSeeker.class.getResource("").getFile(), "QQWry.dat"); - - // һЩ�̶������������¼���ȵȵ� - private static final int IP_RECORD_LENGTH = 7; - private static final byte AREA_FOLLOWED = 0x01; - private static final byte NO_AREA = 0x2; - - // ������Ϊcache����ѯһ��ipʱ���Ȳ鿴cache���Լ��ٲ���Ҫ���ظ����� - private Hashtable ipCache; - // ����ļ������� - private RandomAccessFile ipFile; - // �ڴ�ӳ���ļ� - private MappedByteBuffer mbb; - // ��һģʽʵ�� - private static IPSeeker instance = new IPSeeker(); - // ��ʼ�����Ŀ�ʼ�ͽ����ľ���ƫ�� - private long ipBegin, ipEnd; - // Ϊ���Ч�ʶ����õ���ʱ���� - private IPLocation loc; - private byte[] buf; - private byte[] b4; - private byte[] b3; - - /** - * ˽�й��캯�� - */ - private IPSeeker() { - ipCache = new Hashtable(); - loc = new IPLocation(); - buf = new byte[100]; - b4 = new byte[4]; - b3 = new byte[3]; - try { - ipFile = new RandomAccessFile(IP_FILE, "r"); - } catch (FileNotFoundException e) { - System.out.println(IP_FILE.getAbsolutePath()); - System.out.println(IP_FILE); - System.out.println("IP��ַ��Ϣ�ļ�û���ҵ���IP��ʾ���ܽ��޷�ʹ��"); - ipFile = null; - - } - // ������ļ��ɹ�����ȡ�ļ�ͷ��Ϣ - if(ipFile != null) { - try { - ipBegin = readLong4(0); - ipEnd = readLong4(4); - if(ipBegin == -1 || ipEnd == -1) { - ipFile.close(); - ipFile = null; - } - } catch (IOException e) { - System.out.println("IP��ַ��Ϣ�ļ���ʽ�д���IP��ʾ���ܽ��޷�ʹ��"); - ipFile = null; - } - } - } - - /** - * @return ��һʵ�� - */ - public static IPSeeker getInstance() { - return instance; - } - - /** - * ����һ���ص�IJ���ȫ���֣��õ�һϵ�а���s�Ӵ���IP��Χ��¼ - * @param s �ص��Ӵ� - * @return ����IPEntry���͵�List - */ - public List getIPEntriesDebug(String s) { - List ret = new ArrayList(); - long endOffset = ipEnd + 4; - for(long offset = ipBegin + 4; offset <= endOffset; offset += IP_RECORD_LENGTH) { - // ��ȡ����IPƫ�� - long temp = readLong3(offset); - // ���temp������-1����ȡIP�ĵص���Ϣ - if(temp != -1) { - IPLocation loc = getIPLocation(temp); - // �ж��Ƿ�����ص����������s�Ӵ�����������ˣ���������¼��List�У����û�У����� - if(loc.country.indexOf(s) != -1 || loc.area.indexOf(s) != -1) { - IPEntry entry = new IPEntry(); - entry.country = loc.country; - entry.area = loc.area; - // �õ���ʼIP - readIP(offset - 4, b4); - entry.beginIp = Utils.getIpStringFromBytes(b4); - // �õ�����IP - readIP(temp, b4); - entry.endIp = Utils.getIpStringFromBytes(b4); - // ��Ӹü�¼ - ret.add(entry); - } - } - } - return ret; - } - - /** - * ����һ���ص�IJ���ȫ���֣��õ�һϵ�а���s�Ӵ���IP��Χ��¼ - * @param s �ص��Ӵ� - * @return ����IPEntry���͵�List - */ - public List getIPEntries(String s) { - List ret = new ArrayList(); - try { - // ӳ��IP��Ϣ�ļ����ڴ��� - if(mbb == null) { - FileChannel fc = ipFile.getChannel(); - mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, ipFile.length()); - mbb.order(ByteOrder.LITTLE_ENDIAN); - } - - int endOffset = (int)ipEnd; - for(int offset = (int)ipBegin + 4; offset <= endOffset; offset += IP_RECORD_LENGTH) { - int temp = readInt3(offset); - if(temp != -1) { - IPLocation loc = getIPLocation(temp); - // �ж��Ƿ�����ص����������s�Ӵ�����������ˣ���������¼��List�У����û�У����� - if(loc.country.indexOf(s) != -1 || loc.area.indexOf(s) != -1) { - IPEntry entry = new IPEntry(); - entry.country = loc.country; - entry.area = loc.area; - // �õ���ʼIP - readIP(offset - 4, b4); - entry.beginIp = Utils.getIpStringFromBytes(b4); - // �õ�����IP - readIP(temp, b4); - entry.endIp = Utils.getIpStringFromBytes(b4); - // ��Ӹü�¼ - ret.add(entry); - } - } - } - } catch (IOException e) { - System.out.println(e.getMessage()); - } - return ret; - } - - /** - * ���ڴ�ӳ���ļ���offsetλ�ÿ�ʼ��3���ֽڶ�ȡһ��int - * @param offset - * @return - */ - private int readInt3(int offset) { - mbb.position(offset); - return mbb.getInt() & 0x00FFFFFF; - } - - /** - * ���ڴ�ӳ���ļ��ĵ�ǰλ�ÿ�ʼ��3���ֽڶ�ȡһ��int - * @return - */ - private int readInt3() { - return mbb.getInt() & 0x00FFFFFF; - } - - /** - * ����IP�õ������� - * @param ip ip���ֽ�������ʽ - * @return �������ַ��� - */ - public String getCountry(byte[] ip) { - // ���ip��ַ�ļ��Ƿ����� - if(ipFile == null) return "�����IP���ݿ��ļ�"; - // ����ip��ת��ip�ֽ�����Ϊ�ַ�����ʽ - String ipStr = Utils.getIpStringFromBytes(ip); - // �ȼ��cache���Ƿ��Ѿ����������ip�Ľ����û���������ļ� - if(ipCache.containsKey(ipStr)) { - IPLocation loc = (IPLocation)ipCache.get(ipStr); - return loc.country; - } else { - IPLocation loc = getIPLocation(ip); - ipCache.put(ipStr, loc.getCopy()); - return loc.country; - } - } - - /** - * ����IP�õ������� - * @param ip IP���ַ�����ʽ - * @return �������ַ��� - */ - public String getCountry(String ip) { - return getCountry(Utils.getIpByteArrayFromString(ip)); - } - - /** - * ����IP�õ������� - * @param ip ip���ֽ�������ʽ - * @return �������ַ��� - */ - public String getArea(byte[] ip) { - // ���ip��ַ�ļ��Ƿ����� - if(ipFile == null) return "�����IP���ݿ��ļ�"; - // ����ip��ת��ip�ֽ�����Ϊ�ַ�����ʽ - String ipStr = Utils.getIpStringFromBytes(ip); - // �ȼ��cache���Ƿ��Ѿ����������ip�Ľ����û���������ļ� - if(ipCache.containsKey(ipStr)) { - IPLocation loc = (IPLocation)ipCache.get(ipStr); - return loc.area; - } else { - IPLocation loc = getIPLocation(ip); - ipCache.put(ipStr, loc.getCopy()); - return loc.area; - } - } - - /** - * ����IP�õ������� - * @param ip IP���ַ�����ʽ - * @return �������ַ��� - */ - public String getArea(String ip) { - return getArea(Utils.getIpByteArrayFromString(ip)); - } - - /** - * ����ip����ip��Ϣ�ļ����õ�IPLocation�ṹ����������ip���������Աip�еõ� - * @param ip Ҫ��ѯ��IP - * @return IPLocation�ṹ - */ - private IPLocation getIPLocation(byte[] ip) { - IPLocation info = null; - long offset = locateIP(ip); - if(offset != -1) - info = getIPLocation(offset); - if(info == null) { - info = new IPLocation(); - info.country = "δ֪����"; - info.area = "δ֪����"; - } - return info; - } - - /** - * ��offsetλ�ö�ȡ4���ֽ�Ϊһ��long����ΪjavaΪbig-endian��ʽ������û�취 - * ������ôһ����������ת�� - * @param offset - * @return ��ȡ��longֵ������-1��ʾ��ȡ�ļ�ʧ�� - */ - private long readLong4(long offset) { - long ret = 0; - try { - ipFile.seek(offset); - ret |= (ipFile.readByte() & 0xFF); - ret |= ((ipFile.readByte() << 8) & 0xFF00); - ret |= ((ipFile.readByte() << 16) & 0xFF0000); - ret |= ((ipFile.readByte() << 24) & 0xFF000000); - return ret; - } catch (IOException e) { - return -1; - } - } - - /** - * ��offsetλ�ö�ȡ3���ֽ�Ϊһ��long����ΪjavaΪbig-endian��ʽ������û�취 - * ������ôһ����������ת�� - * @param offset - * @return ��ȡ��longֵ������-1��ʾ��ȡ�ļ�ʧ�� - */ - private long readLong3(long offset) { - long ret = 0; - try { - ipFile.seek(offset); - ipFile.readFully(b3); - ret |= (b3[0] & 0xFF); - ret |= ((b3[1] << 8) & 0xFF00); - ret |= ((b3[2] << 16) & 0xFF0000); - return ret; - } catch (IOException e) { - return -1; - } - } - - /** - * �ӵ�ǰλ�ö�ȡ3���ֽ�ת����long - * @return - */ - private long readLong3() { - long ret = 0; - try { - ipFile.readFully(b3); - ret |= (b3[0] & 0xFF); - ret |= ((b3[1] << 8) & 0xFF00); - ret |= ((b3[2] << 16) & 0xFF0000); - return ret; - } catch (IOException e) { - return -1; - } - } - - /** - * ��offsetλ�ö�ȡ�ĸ��ֽڵ�ip��ַ����ip�����У���ȡ���ipΪbig-endian��ʽ������ - * �ļ�����little-endian��ʽ���������ת�� - * @param offset - * @param ip - */ - private void readIP(long offset, byte[] ip) { - try { - ipFile.seek(offset); - ipFile.readFully(ip); - byte temp = ip[0]; - ip[0] = ip[3]; - ip[3] = temp; - temp = ip[1]; - ip[1] = ip[2]; - ip[2] = temp; - } catch (IOException e) { - System.out.println(e.getMessage()); - } - } - - /** - * ��offsetλ�ö�ȡ�ĸ��ֽڵ�ip��ַ����ip�����У���ȡ���ipΪbig-endian��ʽ������ - * �ļ�����little-endian��ʽ���������ת�� - * @param offset - * @param ip - */ - private void readIP(int offset, byte[] ip) { - mbb.position(offset); - mbb.get(ip); - byte temp = ip[0]; - ip[0] = ip[3]; - ip[3] = temp; - temp = ip[1]; - ip[1] = ip[2]; - ip[2] = temp; - } - - /** - * �����Աip��beginIp�Ƚϣ�ע�����beginIp��big-endian�� - * @param ip Ҫ��ѯ��IP - * @param beginIp �ͱ���ѯIP��Ƚϵ�IP - * @return ��ȷ���0��ip����beginIp�򷵻�1��С�ڷ���-1�� - */ - private int compareIP(byte[] ip, byte[] beginIp) { - for(int i = 0; i < 4; i++) { - int r = compareByte(ip[i], beginIp[i]); - if(r != 0) - return r; - } - return 0; - } - - /** - * ������byte�����޷��������бȽ� - * @param b1 - * @param b2 - * @return ��b1����b2�򷵻�1����ȷ���0��С�ڷ���-1 - */ - private int compareByte(byte b1, byte b2) { - if((b1 & 0xFF) > (b2 & 0xFF)) // �Ƚ��Ƿ���� - return 1; - else if((b1 ^ b2) == 0)// �ж��Ƿ���� - return 0; - else - return -1; - } - - /** - * �������������ip�����ݣ���λ���������ip���ҵ����ļ�¼��������һ������ƫ�� - * ����ʹ�ö��ַ����ҡ� - * @param ip Ҫ��ѯ��IP - * @return ����ҵ��ˣ����ؽ���IP��ƫ�ƣ����û���ҵ�������-1 - */ - private long locateIP(byte[] ip) { - long m = 0; - int r; - // �Ƚϵ�һ��ip�� - readIP(ipBegin, b4); - r = compareIP(ip, b4); - if(r == 0) return ipBegin; - else if(r < 0) return -1; - // ��ʼ�������� - for(long i = ipBegin, j = ipEnd; i < j; ) { - m = getMiddleOffset(i, j); - readIP(m, b4); - r = compareIP(ip, b4); - // log.debug(Utils.getIpStringFromBytes(b)); - if(r > 0) - i = m; - else if(r < 0) { - if(m == j) { - j -= IP_RECORD_LENGTH; - m = j; - } else - j = m; - } else - return readLong3(m + 4); - } - // ���ѭ�������ˣ���ôi��j�ض�����ȵģ������¼Ϊ����ܵļ�¼�����Dz��� - // �϶����ǣ���Ҫ���һ�£�����ǣ��ͷ��ؽ�����ַ���ľ���ƫ�� - m = readLong3(m + 4); - readIP(m, b4); - r = compareIP(ip, b4); - if(r <= 0) return m; - else return -1; - } - - /** - * �õ�beginƫ�ƺ�endƫ���м�λ�ü�¼��ƫ�� - * @param begin - * @param end - * @return - */ - private long getMiddleOffset(long begin, long end) { - long records = (end - begin) / IP_RECORD_LENGTH; - records >>= 1; - if(records == 0) records = 1; - return begin + records * IP_RECORD_LENGTH; - } - - /** - * ����һ��ip���ҵ�����¼��ƫ�ƣ�����һ��IPLocation�ṹ - * @param offset - * @return - */ - private IPLocation getIPLocation(long offset) { - try { - // ����4�ֽ�ip - ipFile.seek(offset + 4); - // ��ȡ��һ���ֽ��ж��Ƿ��־�ֽ� - byte b = ipFile.readByte(); - if(b == AREA_FOLLOWED) { - // ��ȡ����ƫ�� - long countryOffset = readLong3(); - // ��ת��ƫ�ƴ� - ipFile.seek(countryOffset); - // �ټ��һ�α�־�ֽڣ���Ϊ���ʱ������ط���Ȼ�����Ǹ��ض��� - b = ipFile.readByte(); - if(b == NO_AREA) { - loc.country = readString(readLong3()); - ipFile.seek(countryOffset + 4); - } else - loc.country = readString(countryOffset); - // ��ȡ������־ - loc.area = readArea(ipFile.getFilePointer()); - } else if(b == NO_AREA) { - loc.country = readString(readLong3()); - loc.area = readArea(offset + 8); - } else { - loc.country = readString(ipFile.getFilePointer() - 1); - loc.area = readArea(ipFile.getFilePointer()); - } - return loc; - } catch (IOException e) { - return null; - } - } - - /** - * @param offset - * @return - */ - private IPLocation getIPLocation(int offset) { - // ����4�ֽ�ip - mbb.position(offset + 4); - // ��ȡ��һ���ֽ��ж��Ƿ��־�ֽ� - byte b = mbb.get(); - if(b == AREA_FOLLOWED) { - // ��ȡ����ƫ�� - int countryOffset = readInt3(); - // ��ת��ƫ�ƴ� - mbb.position(countryOffset); - // �ټ��һ�α�־�ֽڣ���Ϊ���ʱ������ط���Ȼ�����Ǹ��ض��� - b = mbb.get(); - if(b == NO_AREA) { - loc.country = readString(readInt3()); - mbb.position(countryOffset + 4); - } else - loc.country = readString(countryOffset); - // ��ȡ������־ - loc.area = readArea(mbb.position()); - } else if(b == NO_AREA) { - loc.country = readString(readInt3()); - loc.area = readArea(offset + 8); - } else { - loc.country = readString(mbb.position() - 1); - loc.area = readArea(mbb.position()); - } - return loc; - } - - /** - * ��offsetƫ�ƿ�ʼ����������ֽڣ�����һ�������� - * @param offset - * @return �������ַ��� - * @throws IOException - */ - private String readArea(long offset) throws IOException { - ipFile.seek(offset); - byte b = ipFile.readByte(); - if(b == 0x01 || b == 0x02) { - long areaOffset = readLong3(offset + 1); - if(areaOffset == 0) - return "δ֪����"; - else - return readString(areaOffset); - } else - return readString(offset); - } - - /** - * @param offset - * @return - */ - private String readArea(int offset) { - mbb.position(offset); - byte b = mbb.get(); - if(b == 0x01 || b == 0x02) { - int areaOffset = readInt3(); - if(areaOffset == 0) - return "δ֪����"; - else - return readString(areaOffset); - } else - return readString(offset); - } - - /** - * ��offsetƫ�ƴ���ȡһ����0�������ַ��� - * @param offset - * @return ��ȡ���ַ����������ؿ��ַ��� - */ - private String readString(long offset) { - try { - ipFile.seek(offset); - int i; - for(i = 0, buf[i] = ipFile.readByte(); buf[i] != 0; buf[++i] = ipFile.readByte()); - if(i != 0) - return Utils.getString(buf, 0, i, "GBK"); - } catch (IOException e) { - System.out.println(e.getMessage()); - } - return ""; - } - - /** - * ���ڴ�ӳ���ļ���offsetλ�õõ�һ��0��β�ַ��� - * @param offset - * @return - */ - private String readString(int offset) { - try { - mbb.position(offset); - int i; - for(i = 0, buf[i] = mbb.get(); buf[i] != 0; buf[++i] = mbb.get()); - if(i != 0) - return Utils.getString(buf, 0, i, "GBK"); - } catch (IllegalArgumentException e) { - System.out.println(e.getMessage()); - } - return ""; - } - - public String getAddress(String ip){ - String country = getCountry(ip).equals(" CZ88.NET")?"":getCountry(ip); - String area = getArea(ip).equals(" CZ88.NET")?"":getArea(ip); - String address = country+" "+area; - return address.trim(); - } -} - - - - diff --git a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/QQWry.dat b/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/QQWry.dat deleted file mode 100644 index a77baf0a..00000000 Binary files a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/QQWry.dat and /dev/null differ diff --git a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/Test.java b/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/Test.java deleted file mode 100644 index 2b1516f4..00000000 --- a/codes/javaee/jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/Test.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.github.dunwu.javaee.jsp.util.ip; -/** - * @author LJ-silver - */ -import java.util.*; -public class Test { - - public static void main(String[] args) { - - args = new String[]{"ip", "9.128.2.1"}; - - IPSeeker seeker = IPSeeker.getInstance(); - - if(args.length==2){ - if("ip".equals(args[0])){ - System.out.println(args[0]+"�����ڵ�ַ��:"+seeker.getAddress(args[1])); - System.out.println(args[0]+"�����ڵ�ַ������:"+seeker.getCountry(args[1])); - }else if("address".equals(args[0])){ - List a = seeker.getIPEntries(args[1]); -System.out.println(args[0]+":"); - for(int i=0;i - - - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - logs/${FILE_NAME}-all.%d{yyyy-MM-dd}.log - 30 - - - - - 30MB - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - - - - - - - - - - - diff --git a/codes/javaee/jsp/src/main/webapp/META-INF/MANIFEST.MF b/codes/javaee/jsp/src/main/webapp/META-INF/MANIFEST.MF deleted file mode 100644 index 254272e1..00000000 --- a/codes/javaee/jsp/src/main/webapp/META-INF/MANIFEST.MF +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0 -Class-Path: - diff --git a/codes/javaee/jsp/src/main/webapp/WEB-INF/META-INF/MANIFEST.MF b/codes/javaee/jsp/src/main/webapp/WEB-INF/META-INF/MANIFEST.MF deleted file mode 100644 index 254272e1..00000000 --- a/codes/javaee/jsp/src/main/webapp/WEB-INF/META-INF/MANIFEST.MF +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0 -Class-Path: - diff --git a/codes/javaee/jsp/src/main/webapp/WEB-INF/resources/jsp/index.jsp b/codes/javaee/jsp/src/main/webapp/WEB-INF/resources/jsp/index.jsp deleted file mode 100644 index 74319429..00000000 --- a/codes/javaee/jsp/src/main/webapp/WEB-INF/resources/jsp/index.jsp +++ /dev/null @@ -1,16 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> - - - - -My JSP 'index.jsp' starting page - - - - - - - -

This is my JSP page.

- - diff --git a/codes/javaee/jsp/src/main/webapp/WEB-INF/web.xml b/codes/javaee/jsp/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index d65f0eaa..00000000 --- a/codes/javaee/jsp/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - HelloServlet - /examples/configuration.jsp - - message - welcome to jsp - - 1 - - - HelloServlet - /config - /config.jsp - - - - - /WEB-INF/views/jsp/index.jsp - - diff --git a/codes/javaee/jsp/src/main/webapp/examples/02.grammar/break.jsp b/codes/javaee/jsp/src/main/webapp/examples/02.grammar/break.jsp deleted file mode 100644 index d97e9fb9..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/02.grammar/break.jsp +++ /dev/null @@ -1,22 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - -JSP Scriptlets - - -<% - for(int i=0; i<5; i++){ -%> break 所在的循环, i = <%= i %>。
-<% - if(i == 2) break; - } -%> break 循环完毕,
-<% - for(int i=0; i<5; i++){ -%> return 所在的循环, i = <%= i %>。
-<% - if(i == 2) return; - } -%> return 循环完毕,
- - - diff --git a/codes/javaee/jsp/src/main/webapp/examples/02.grammar/for.jsp b/codes/javaee/jsp/src/main/webapp/examples/02.grammar/for.jsp deleted file mode 100644 index 93273521..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/02.grammar/for.jsp +++ /dev/null @@ -1,56 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8" %> - - - 02.JSP语法 - for示例 - - - -
-<% - Object[][] letters = { - {true, "恭喜您注册的信息已经生效", "e_inn@163.com", "forbreak@163.com", "2007-8-8"}, - {true, "JavaEE 7.0 release!", "admin@sun.com", "forbreak@163.com", "2007-6-24"}, - {false, "来信已经收到,下周来面谈", "foo@bar.com", "forbreak@163.com", "2007-5-20"}, - {false, "您有新的邮件", "blog@foo.bar.com", "forbreak@163.com", "2007-3-2"}, - }; - String[] colors = {"#DDDDDD", "#AAAAAA",}; -%> - - - - - - - - - <% - for (int i = 0; i < letters.length; i++) { - Object[] letter = letters[i]; - %> - - - - - - - - <% - } - %> -
 标题 发信人 收信人 时间 
- <% - if (letter[0] == Boolean.TRUE) { - %> - - <% - } else { - out.println(" "); - } - %> - <%= letter[1] %> - <%= letter[2] %> - <%= letter[3] %> - <%= letter[4] %> -
- - diff --git a/codes/javaee/jsp/src/main/webapp/examples/02.grammar/if.jsp b/codes/javaee/jsp/src/main/webapp/examples/02.grammar/if.jsp deleted file mode 100644 index 4b940593..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/02.grammar/if.jsp +++ /dev/null @@ -1,68 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> - - -02.JSP语法 - if...else示例 - - -<% - String param = request.getParameter("param"); - if ("1".equals(param)) { -%> -《破阵子·为孙同甫赋壮语以寄》
-【宋】辛弃疾
-醉里挑灯看剑,梦回吹角连营。
-八百里分麾下炙,五十弦翻塞外声,沙场秋点兵。
-马作的卢飞快,弓如霹雳弦惊。
-了却君王天下事,赢得生前身后名。可怜白发生!
-<% -} else if ("2".equals(param)) { -%> -《青玉案·元夕》
-【宋】辛弃疾
-东风夜放花千树,更吹落,星如雨。
-宝马雕车香满路,凤箫声动,玉壶光转,一夜鱼龙舞。
-蛾儿雪柳黄金缕,笑语盈盈暗香去。
-众里寻他千百度,蓦然回首,那人却在,灯火阑珊处。
-<% -} else if ("3".equals(param)) { -%> -《丑奴儿》
-【宋】辛弃疾
-少年不识愁滋味,爱上层楼,爱上层楼,为赋新词强说愁。
-而今识得愁滋味,欲说还休,欲说还休,却道天凉好个秋。
-<% -} else if ("4".equals(param)) { -%> -《永遇乐》
-【宋】辛弃疾
-千古江山,英雄无觅,孙仲谋处。
-舞榭歌台,风流总被、雨打风吹去。
-斜阳草树,寻常巷陌,人道寄奴曾住。
-想当年,金戈铁马,气吞万里如虎。
-元嘉草草,封狼居胥,赢得仓皇北顾。
-四十三年,望中犹记,烽火扬州路。
-可堪回首,佛狸祠下,一片神鸦社鼓。
-凭谁问:廉颇老矣,尚能饭否?
-<% -} else if ("5".equals(param)) { -%> -《南乡子》
-【宋】辛弃疾
-何处望神州,满眼风光北固楼。
-千古兴亡多少事,悠悠,不尽长江滚滚流。
-年少万兜鍪,坐断东南战未休。
-天下英雄谁敌手?曹刘,生子当如孙仲谋。
-<% -} else { -%> -请使用参数 param=1,2,3,4,5 选择要显示的诗歌
-if.jsp?param=1
-if.jsp?param=2
-if.jsp?param=3
-if.jsp?param=4
-if.jsp?param=5
-<% - } -%> - - diff --git a/codes/javaee/jsp/src/main/webapp/examples/02.grammar/if2.jsp b/codes/javaee/jsp/src/main/webapp/examples/02.grammar/if2.jsp deleted file mode 100644 index 1c574813..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/02.grammar/if2.jsp +++ /dev/null @@ -1,17 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> -<%! int day = 1; %> - - - - - 02.JSP语法 - if...else示例 - - -

if...else示例

-<% if (day == 1 | day == 7) { %> -

今天是周末

-<% } else { %> -

今天不是周末

-<% } %> - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/02.grammar/scriptlet.jsp b/codes/javaee/jsp/src/main/webapp/examples/02.grammar/scriptlet.jsp deleted file mode 100644 index 9764f28d..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/02.grammar/scriptlet.jsp +++ /dev/null @@ -1,16 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - -JSP Scriptlets - - -<% - int num = 10; - int result = 1; - for(int i=1; i<=num; i++){ - result *= i; - } - out.println("
"); - out.println("数字 " + num + " 的阶乘为:" + result); -%> - - diff --git a/codes/javaee/jsp/src/main/webapp/examples/02.grammar/switch.jsp b/codes/javaee/jsp/src/main/webapp/examples/02.grammar/switch.jsp deleted file mode 100644 index 3b077b8a..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/02.grammar/switch.jsp +++ /dev/null @@ -1,37 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%! int day = 3; %> - - - - - 02.JSP语法 - switch...case示例 - - -

Sswitch...case示例

-<% - switch(day) { - case 0: - out.println("星期天"); - break; - case 1: - out.println("星期一"); - break; - case 2: - out.println("星期二"); - break; - case 3: - out.println("星期三"); - break; - case 4: - out.println("星期四"); - break; - case 5: - out.println("星期五"); - break; - default: - out.println("星期六"); - } -%> - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/02.grammar/while.jsp b/codes/javaee/jsp/src/main/webapp/examples/02.grammar/while.jsp deleted file mode 100644 index 59d99c82..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/02.grammar/while.jsp +++ /dev/null @@ -1,23 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8" %> - - - JSP Scriptlets - - -<% - java.util.List list = new java.util.ArrayList(); - - list.add("青青子衿"); - list.add("悠悠我心"); - list.add("但为君故"); - list.add("沉吟至今"); - - java.util.Iterator it = list.iterator(); - - while (it.hasNext()) { -%> <%= it.next() %>
-<% - } -%> - - diff --git a/codes/javaee/jsp/src/main/webapp/examples/03.directive/foot.jsp b/codes/javaee/jsp/src/main/webapp/examples/03.directive/foot.jsp deleted file mode 100644 index d17f0460..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/03.directive/foot.jsp +++ /dev/null @@ -1,13 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - - - - - - -
- Copyright 2017 &Zhang Peng -
- - - diff --git a/codes/javaee/jsp/src/main/webapp/examples/03.directive/head.jsp b/codes/javaee/jsp/src/main/webapp/examples/03.directive/head.jsp deleted file mode 100644 index 3210fdb6..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/03.directive/head.jsp +++ /dev/null @@ -1,19 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - - -JSP 示例 - - - - - - - - - - - - - -
JSP 示例
首页资源文档
\ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/03.directive/include.jsp b/codes/javaee/jsp/src/main/webapp/examples/03.directive/include.jsp deleted file mode 100644 index 5699a431..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/03.directive/include.jsp +++ /dev/null @@ -1,18 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - -<%@ include file="head.jsp" %> - -

念奴娇·赤壁怀古

-

宋·苏轼

-

-

大江东去,浪淘尽,千古风流人物。 -

故垒西边,人道是、三国周郎赤壁。 -

乱石穿空,惊涛拍岸,卷起千堆雪。 -

江山如画,一时多少豪杰。 -

遥想公瑾当年,小乔初嫁了,雄姿英发。 -

羽扇纶巾,谈笑间、强虏灰飞烟灭。 -

故国神游,多情应笑我,早生华发。 -

人生如梦,一樽还酹江月。 -

- -<%@ include file="foot.jsp" %> diff --git a/codes/javaee/jsp/src/main/webapp/examples/03.directive/page.jsp b/codes/javaee/jsp/src/main/webapp/examples/03.directive/page.jsp deleted file mode 100644 index 01b23d7b..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/03.directive/page.jsp +++ /dev/null @@ -1,17 +0,0 @@ -<%@ page language="java" %> -<%@ page contentType="text/html; charset=UTF-8" %> -<%@ page pageEncoding="UTF-8" %> -<%@ page trimDirectiveWhitespaces="false" %> -<%@ page buffer="10kb" %> -<%@ page info="false" %> - - - - 第一个Jsp程序 - - -<% - out.println("你好!"); -%> - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/04.action/date.jsp b/codes/javaee/jsp/src/main/webapp/examples/04.action/date.jsp deleted file mode 100644 index 15c69949..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/04.action/date.jsp +++ /dev/null @@ -1,4 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> -

- 今天的日期是: <%= (new java.util.Date())%> -

\ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/04.action/jspforward.jsp b/codes/javaee/jsp/src/main/webapp/examples/04.action/jspforward.jsp deleted file mode 100644 index aec8a0dd..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/04.action/jspforward.jsp +++ /dev/null @@ -1,23 +0,0 @@ - - -<% - out.clear(); - if("1".equals(request.getParameter("param"))){ -%> - - - - -<% - } -%> - - -闀ㄤ讲鍓ラ弪镡煎禌?/title> -<link rel='stylesheet' type='text/css' href='css/style.css'> -</head> -<body> -i闀ㄥ洷娓规繛锲噭閵娿储鍕鹃柛褉锅挞梹妞ょ箰瀵剟寮?param=1 闀ㄤ礁娼″Λ鍓佹嫚閵夆敛钪妫婚ⅳ顑藉亾? -</body> -</html> - diff --git a/codes/javaee/jsp/src/main/webapp/examples/04.action/jspinclude.jsp b/codes/javaee/jsp/src/main/webapp/examples/04.action/jspinclude.jsp deleted file mode 100644 index caf53e99..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/04.action/jspinclude.jsp +++ /dev/null @@ -1,14 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> -<!DOCTYPE html> -<html> -<head> - <meta charset="utf-8"> - <title>jsp:include 示例 - - - -

jsp:include 示例

- - - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/04.action/jspplugin.jsp b/codes/javaee/jsp/src/main/webapp/examples/04.action/jspplugin.jsp deleted file mode 100644 index 6916eb6e..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/04.action/jspplugin.jsp +++ /dev/null @@ -1,28 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8" %> - - -plugin - - -
- -
- - - - - - - 您的浏览器不支持 Java Applet - - -
- - - - diff --git a/codes/javaee/jsp/src/main/webapp/examples/04.action/jspuseBean.jsp b/codes/javaee/jsp/src/main/webapp/examples/04.action/jspuseBean.jsp deleted file mode 100644 index 48dcdfe8..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/04.action/jspuseBean.jsp +++ /dev/null @@ -1,55 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8" %> - - - Java Bean Actions - - -
- -<%-- 声明 person 类对象 --%> - - -<%-- 设置 person 的所有属性, 属性值从 request 中自动取得,* 表示所有属性 --%> - - -
-
-
- 请填写 person 信息 - - - - - - - - - - - - - - - - - - -
姓名 - <%-- 获取 person 的 name 属性 --%> - -
年龄 - <%-- 获取 person 的 age 属性 --%> - -
性别 - <%-- 获取 person 的 sex 属性 --%> - -
- -
-
-
-
- - - - diff --git a/codes/javaee/jsp/src/main/webapp/examples/04.action/jspuseBean2.jsp b/codes/javaee/jsp/src/main/webapp/examples/04.action/jspuseBean2.jsp deleted file mode 100644 index 56dbc752..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/04.action/jspuseBean2.jsp +++ /dev/null @@ -1,43 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8" %> - - - 计数器 - - -
- -<%-- 定义一个 session 范围内的计数器,记录个人的访问信息 --%> - - -<%-- 定义一个 application 范围内的计数器,记录所有人的访问信息 --%> - - -
-
-
- 计数器 - - - - - - - - - -
您的访问次数: - <%-- 获取各人的访问次数 --%> - - 次 -
总共的访问次数: - <%-- 获取所有人的访问次数 --%> - - 次 -
-
-
-
- - - - diff --git a/codes/javaee/jsp/src/main/webapp/examples/04.action/useBean.html b/codes/javaee/jsp/src/main/webapp/examples/04.action/useBean.html deleted file mode 100644 index 4e5ce1e5..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/04.action/useBean.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - Java Bean Actions - - -
- -
-
-
- 请填写 Person 信息 - - - - - - - - - - - - - - - - - - -
姓名:
年龄:
性别: - Male - Female -
- -
-
-
-
- - - diff --git a/codes/javaee/jsp/src/main/webapp/examples/05.implicit_object/error.jsp b/codes/javaee/jsp/src/main/webapp/examples/05.implicit_object/error.jsp deleted file mode 100644 index cc800f12..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/05.implicit_object/error.jsp +++ /dev/null @@ -1,11 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8" isErrorPage="true" %> - -JSP - - -<% - out.println("程序拋出了一个异常:"+ exception); -%> - - - diff --git a/codes/javaee/jsp/src/main/webapp/examples/05.implicit_object/exception.jsp b/codes/javaee/jsp/src/main/webapp/examples/05.implicit_object/exception.jsp deleted file mode 100644 index 3edbf87e..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/05.implicit_object/exception.jsp +++ /dev/null @@ -1,14 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8" errorPage="error.jsp" %> -<% - out.clear(); - String str = null; - // length() 会抛出 NullPointerException - int length = str.length(); -%> - -JSP - - - - - diff --git a/codes/javaee/jsp/src/main/webapp/examples/cookie/readCookie.jsp b/codes/javaee/jsp/src/main/webapp/examples/cookie/readCookie.jsp deleted file mode 100644 index d44a4435..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/cookie/readCookie.jsp +++ /dev/null @@ -1,31 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ page import="java.net.URLDecoder" %> - - - - - 获取 Cookie - - -<% - Cookie cookie = null; - Cookie[] cookies = null; - // 获取cookies的数据,是一个数组 - cookies = request.getCookies(); - if( cookies != null ){ - out.println("

查找 Cookie 名与值

"); - for (int i = 0; i < cookies.length; i++){ - cookie = cookies[i]; - - out.print("参数名 : " + cookie.getName()); - out.print("
"); - out.print("参数值: " + URLDecoder.decode(cookie.getValue(), "utf-8") +"
"); - out.print("------------------------------------
"); - } - }else{ - out.println("

没有发现 Cookie

"); - } -%> - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/cookie/session.jsp b/codes/javaee/jsp/src/main/webapp/examples/cookie/session.jsp deleted file mode 100644 index f7b8c735..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/cookie/session.jsp +++ /dev/null @@ -1,71 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ page import="java.util.Date" %> -<% - // 获取session创建时间 - Date createTime = new Date(session.getCreationTime()); - // 获取最后访问页面的时间 - Date lastAccessTime = new Date(session.getLastAccessedTime()); - - String note = "再次访问"; - Integer visitCount = 0; - String visitCountKey = "visitCount"; - String userIDKey = "userID"; - String userID = "ABCD"; - - // 检测网页是否由新的访问用户 - if (session.isNew()) { - note = "第一次访问"; - session.setAttribute(userIDKey, userID); - session.setAttribute(visitCountKey, visitCount); - } else { - visitCount = (Integer) session.getAttribute(visitCountKey); - if (null == visitCount) { - visitCount = 0; - } - visitCount += 1; - userID = (String) session.getAttribute(userIDKey); - if (null == userID) { - userID = "ABCD"; - } - session.setAttribute(visitCountKey, visitCount); - } -%> - - - Session 示例 - - - - -<%--提示语--%> -

<% out.print(note); %>

- - - - - - - - - - - - - - - - - - - - - - - - - - -
Session 信息
id<% out.print(session.getId()); %>
创建时间<% out.print(createTime); %>
最后访问时间<% out.print(lastAccessTime); %>
用户 ID<% out.print(userID); %>
访问次数<% out.print(visitCount); %>
- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/cookie/writeCookie.jsp b/codes/javaee/jsp/src/main/webapp/examples/cookie/writeCookie.jsp deleted file mode 100644 index 1c50ae3d..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/cookie/writeCookie.jsp +++ /dev/null @@ -1,41 +0,0 @@ -<%-- -访问本页面的形式如:http://127.0.0.1:8080/runoobDemos/writeCookie.jsp?name=张三&url=www.baidu.com ---%> - -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ page import="java.net.URLEncoder" %> -<% - // 编码,解决中文乱码 - String name = URLEncoder.encode(request.getParameter("name"), "utf-8"); - - // 设置 nameCookie 和 urlCookie cookie - Cookie nameCookie = new Cookie("name", name); - Cookie urlCookie = new Cookie("url", request.getParameter("url")); - - // 设置cookie过期时间为24小时。 - nameCookie.setMaxAge(60 * 60 * 24); - urlCookie.setMaxAge(60 * 60 * 24); - - // 在响应头部添加cookie - response.addCookie(nameCookie); - response.addCookie(urlCookie); -%> - - - 设置 Cookie - - - -

设置 Cookie

- -
    -
  • 网站名: - <%= request.getParameter("name")%> -

  • -
  • 网址: - <%= request.getParameter("url")%> -

  • -
- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/locale/language.jsp b/codes/javaee/jsp/src/main/webapp/examples/locale/language.jsp deleted file mode 100644 index d60eeb99..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/locale/language.jsp +++ /dev/null @@ -1,23 +0,0 @@ -<%@ page import="java.util.Locale" %> -<% - //获取客户端本地化信息 - Locale locale = request.getLocale(); - String language = locale.getLanguage(); - String country = locale.getCountry(); -%> - - - Detecting Locale - - -
-

Detecting Locale

-
-

- <% - out.println("Language : " + language + "
"); - out.println("Country : " + country + "
"); - %> -

- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/locale/language2.jsp b/codes/javaee/jsp/src/main/webapp/examples/locale/language2.jsp deleted file mode 100644 index 13b6eade..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/locale/language2.jsp +++ /dev/null @@ -1,20 +0,0 @@ -<% - // Set response content type - response.setContentType("text/html"); - // Set spanish language code. - response.setHeader("Content-Language", "es"); - String title = "En Espa?ol"; - -%> - - - <% out.print(title); %> - - -

<% out.print(title); %>

-
-

En Espa?ol

-

?Hola Mundo!

-
- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/locale/localeCurrency.jsp b/codes/javaee/jsp/src/main/webapp/examples/locale/localeCurrency.jsp deleted file mode 100644 index 466b6382..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/locale/localeCurrency.jsp +++ /dev/null @@ -1,20 +0,0 @@ -<%@ page import="java.text.NumberFormat,java.util.Locale" %> - -<% - String title = "Locale Specific Currency"; - //Get the client's Locale - Locale locale = request.getLocale(); - NumberFormat nft = NumberFormat.getCurrencyInstance(locale); - String formattedCurr = nft.format(1000000); -%> - - - <% out.print(title); %> - - -

<% out.print(title); %>

-
-

Formatted Currency: <% out.print(formattedCurr); %>

-
- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/locale/localeDate.jsp b/codes/javaee/jsp/src/main/webapp/examples/locale/localeDate.jsp deleted file mode 100644 index 42485237..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/locale/localeDate.jsp +++ /dev/null @@ -1,23 +0,0 @@ -<%@ page import="java.text.DateFormat,java.util.Date" %> -<%@ page import="java.util.Locale " %> - -<% - String title = "Locale Specific Dates"; - //Get the client's Locale - Locale locale = request.getLocale(); - String date = DateFormat.getDateTimeInstance( - DateFormat.FULL, - DateFormat.SHORT, - locale).format(new Date()); -%> - - - <% out.print(title); %> - - -

<% out.print(title); %>

-
-

Local Date: <% out.print(date); %>

-
- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/locale/localePercent.jsp b/codes/javaee/jsp/src/main/webapp/examples/locale/localePercent.jsp deleted file mode 100644 index f635f617..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/locale/localePercent.jsp +++ /dev/null @@ -1,20 +0,0 @@ -<%@ page import="java.text.NumberFormat,java.util.Locale" %> - -<% - String title = "Locale Specific Percentage"; - //Get the client's Locale - Locale locale = request.getLocale(); - NumberFormat nft = NumberFormat.getPercentInstance(locale); - String formattedPerc = nft.format(0.51); -%> - - - <% out.print(title); %> - - -

<% out.print(title); %>

-
-

Formatted Percentage: <% out.print(formattedPerc); %>

-
- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/practice/date.jsp b/codes/javaee/jsp/src/main/webapp/examples/practice/date.jsp deleted file mode 100644 index 4c250187..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/practice/date.jsp +++ /dev/null @@ -1,25 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ page import="java.text.SimpleDateFormat,java.util.Date" %> - - - 显示当前时间与日期 - - - -

显示当前时间与日期

- -<% - Date date = new Date(); - out.print("

" + date.toString() + "

"); -%> - -

使用SimpleDateFormat格式化日期

-<% - Date dNow = new Date(); - SimpleDateFormat ft = - new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - out.print("

" + ft.format(dNow) + "

"); -%> - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/practice/form/checkbox.html b/codes/javaee/jsp/src/main/webapp/examples/practice/form/checkbox.html deleted file mode 100644 index d7efbff7..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/practice/form/checkbox.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Checkbox - - - -
- 谷歌 - 百度 - 淘宝 - -
- - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/practice/form/checkbox.jsp b/codes/javaee/jsp/src/main/webapp/examples/practice/form/checkbox.jsp deleted file mode 100644 index 60966b82..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/practice/form/checkbox.jsp +++ /dev/null @@ -1,23 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> - - - - - Checkbox - - -

从复选框中读取数据

-
    -
  • 谷歌是否选中: - <%= request.getParameter("google")%> -

  • -
  • 百度是否选中: - <%= request.getParameter("baidu")%> -

  • -
  • 淘宝是否选中: - <%= request.getParameter("taobao")%> -

  • -
- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/practice/form/checkbox2.html b/codes/javaee/jsp/src/main/webapp/examples/practice/form/checkbox2.html deleted file mode 100644 index 0c4b6fd0..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/practice/form/checkbox2.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - Checkbox - - - -
- 谷歌 - 百度 - 淘宝 - -
- - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/practice/form/checkbox2.jsp b/codes/javaee/jsp/src/main/webapp/examples/practice/form/checkbox2.jsp deleted file mode 100644 index 2a40276a..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/practice/form/checkbox2.jsp +++ /dev/null @@ -1,29 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ page import="java.util.Enumeration" %> - - - - - Checkbox2 - - -

读取所有表单参数

- - - - - - <% - Enumeration paramNames = request.getParameterNames(); - - while (paramNames.hasMoreElements()) { - String paramName = (String) paramNames.nextElement(); - out.print("\n"); - String paramValue = request.getParameter(paramName); - out.println("\n"); - } - %> -
参数名参数值
" + paramName + " " + paramValue + "
- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/practice/form/formGet.html b/codes/javaee/jsp/src/main/webapp/examples/practice/form/formGet.html deleted file mode 100644 index 1572d4ba..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/practice/form/formGet.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - 表单 - Get操作 - - - -
- 站点名: -
- 网址: - -
- - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/practice/form/formGet.jsp b/codes/javaee/jsp/src/main/webapp/examples/practice/form/formGet.jsp deleted file mode 100644 index 2dc4baa4..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/practice/form/formGet.jsp +++ /dev/null @@ -1,20 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> - - - - - 表单 - Get操作 - - -

使用 GET 方法读取数据

-
    -
  • 站点名: - <%= request.getParameter("name")%> -

  • -
  • 网址: - <%= request.getParameter("url")%> -

  • -
- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/practice/form/formPost.html b/codes/javaee/jsp/src/main/webapp/examples/practice/form/formPost.html deleted file mode 100644 index c624cc0a..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/practice/form/formPost.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - 表单 - Post操作 - - - -
- 站点名: -
- 网址: - -
- - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/practice/form/formPost.jsp b/codes/javaee/jsp/src/main/webapp/examples/practice/form/formPost.jsp deleted file mode 100644 index 8a4ccd7a..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/practice/form/formPost.jsp +++ /dev/null @@ -1,24 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> - - - - - 表单 - Post操作 - - -

使用 POST 方法读取数据

-
    -
  • 站点名: - <% - // 解决中文乱码的问题 - String name = new String((request.getParameter("name")).getBytes("ISO-8859-1"), "UTF-8"); - %> - <%=name%> -

  • -
  • 网址: - <%= request.getParameter("url")%> -

  • -
- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/practice/hitCount.jsp b/codes/javaee/jsp/src/main/webapp/examples/practice/hitCount.jsp deleted file mode 100644 index dcbd6e36..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/practice/hitCount.jsp +++ /dev/null @@ -1,28 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> - - - - 访问量统计 - - -<% - Integer hitsCount = - (Integer) application.getAttribute("hitCounter"); - if (hitsCount == null || hitsCount == 0) { - /* 第一次访问 */ - out.println("欢迎访问!"); - hitsCount = 1; - } else { - /* 返回访问值 */ - out.println("欢迎再次访问!"); - hitsCount += 1; - } - application.setAttribute("hitCounter", hitsCount); -%> - -

页面访问量为: <%= hitsCount%> -

- - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/practice/mail/sendMail.jsp b/codes/javaee/jsp/src/main/webapp/examples/practice/mail/sendMail.jsp deleted file mode 100644 index f07a2c2e..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/practice/mail/sendMail.jsp +++ /dev/null @@ -1,56 +0,0 @@ -<%@ page import="javax.mail.*,javax.mail.internet.*,java.util.Properties" %> -<% - String result; - // 收件人的电子邮件 - String to = "abcd@gmail.com"; - - // 发件人的电子邮件 - String from = "mcmohd@gmail.com"; - - // 假设你是从本地主机发送电子邮件 - String host = "localhost"; - - // 获取系统属性对象 - Properties properties = System.getProperties(); - - // 设置邮件服务器 - properties.setProperty("mail.smtp.host", host); - - // 获取默认的Session对象。 - Session mailSession = Session.getDefaultInstance(properties); - - try { - // 创建一个默认的MimeMessage对象。 - MimeMessage message = new MimeMessage(mailSession); - // 设置 From: 头部的header字段 - message.setFrom(new InternetAddress(from)); - // 设置 To: 头部的header字段 - message.addRecipient(Message.RecipientType.TO, - new InternetAddress(to)); - // 设置 Subject: header字段 - message.setSubject("This is the Subject Line!"); - // 现在设置的实际消息 - message.setText("This is actual message"); - // 发送消息 - Transport.send(message); - result = "Sent message successfully...."; - } catch (MessagingException mex) { - mex.printStackTrace(); - result = "Error: unable to send message...."; - } -%> - - - Send Email using JSP - - -
-

Send Email using JSP

-
-

- <% - out.println("Result: " + result + "\n"); - %> -

- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/practice/redirect.jsp b/codes/javaee/jsp/src/main/webapp/examples/practice/redirect.jsp deleted file mode 100644 index bc4c0165..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/practice/redirect.jsp +++ /dev/null @@ -1,20 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> - - - - 页面重定向 - - - -

页面重定向

- -<% - // 重定向到新地址 - String site = new String("http://www.baidu.com"); - response.setStatus(response.SC_MOVED_TEMPORARILY); - response.setHeader("Location", site); -%> - - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/practice/refresh.jsp b/codes/javaee/jsp/src/main/webapp/examples/practice/refresh.jsp deleted file mode 100644 index 86bf4444..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/practice/refresh.jsp +++ /dev/null @@ -1,29 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ page import="java.util.Calendar,java.util.GregorianCalendar" %> - - - 自动刷新实例 - - - -

每秒刷新时间

-<% - // 设置每秒刷新一次 - response.setIntHeader("Refresh", 1); - // 获取当前时间 - Calendar calendar = new GregorianCalendar(); - String am_pm; - int hour = calendar.get(Calendar.HOUR); - int minute = calendar.get(Calendar.MINUTE); - int second = calendar.get(Calendar.SECOND); - if (calendar.get(Calendar.AM_PM) == 0) - am_pm = "AM"; - else - am_pm = "PM"; - String CT = hour + ":" + minute + ":" + second + " " + am_pm; - out.println("当前时间为: " + CT + "\n"); -%> - - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/uncheck/01.helloWorld.jsp b/codes/javaee/jsp/src/main/webapp/examples/uncheck/01.helloWorld.jsp deleted file mode 100644 index 88b3b2fa..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/uncheck/01.helloWorld.jsp +++ /dev/null @@ -1,10 +0,0 @@ - - - First Jsp Programe - - -<% - out.println("Hello World!"); -%> - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/uncheck/01.helloWorld_zh.jsp b/codes/javaee/jsp/src/main/webapp/examples/uncheck/01.helloWorld_zh.jsp deleted file mode 100644 index 20fc14fa..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/uncheck/01.helloWorld_zh.jsp +++ /dev/null @@ -1,13 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> - - - - 第一个Jsp程序 - - -<% - out.println("你好!"); -%> - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/uncheck/02.life.jsp b/codes/javaee/jsp/src/main/webapp/examples/uncheck/02.life.jsp deleted file mode 100644 index 9a50ba4c..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/uncheck/02.life.jsp +++ /dev/null @@ -1,39 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> - - - life.jsp - - - -<%! - private int initVar=0; - private int serviceVar=0; - private int destroyVar=0; -%> - -<%! - public void jspInit(){ - initVar++; - System.out.println("jspInit(): JSP被初始化了"+initVar+"次"); - } - public void jspDestroy(){ - destroyVar++; - System.out.println("jspDestroy(): JSP被销毁了"+destroyVar+"次"); - } -%> - -<% - serviceVar++; - System.out.println("_jspService(): JSP共响应了"+serviceVar+"次请求"); - - String content1="初始化次数 : "+initVar; - String content2="响应客户请求次数 : "+serviceVar; - String content3="销毁次数 : "+destroyVar; -%> -

菜鸟教程 JSP 测试实例

-

<%=content1 %>

-

<%=content2 %>

-

<%=content3 %>

- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/uncheck/03.yourIp.jsp b/codes/javaee/jsp/src/main/webapp/examples/uncheck/03.yourIp.jsp deleted file mode 100644 index dda307dd..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/uncheck/03.yourIp.jsp +++ /dev/null @@ -1,15 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> - - - - - 菜鸟教程(runoob.com) - - -Hello World!
-<% - out.println("你的 IP 地址 " + request.getRemoteAddr()); -%> - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/examples/uncheck/configuration.jsp b/codes/javaee/jsp/src/main/webapp/examples/uncheck/configuration.jsp deleted file mode 100644 index 0e694b46..00000000 --- a/codes/javaee/jsp/src/main/webapp/examples/uncheck/configuration.jsp +++ /dev/null @@ -1,14 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8" %> - - - JSP config 示例 - - -

- <% - String message = config.getInitParameter("message"); - out.println(message); - %> -

- - diff --git a/codes/javaee/jsp/src/main/webapp/images/bg-btn-blue.gif b/codes/javaee/jsp/src/main/webapp/images/bg-btn-blue.gif deleted file mode 100644 index bc03f1bd..00000000 Binary files a/codes/javaee/jsp/src/main/webapp/images/bg-btn-blue.gif and /dev/null differ diff --git a/codes/javaee/jsp/src/main/webapp/images/mail.gif b/codes/javaee/jsp/src/main/webapp/images/mail.gif deleted file mode 100644 index 0f2b0d76..00000000 Binary files a/codes/javaee/jsp/src/main/webapp/images/mail.gif and /dev/null differ diff --git a/codes/javaee/jsp/src/main/webapp/images/vertical_line.gif b/codes/javaee/jsp/src/main/webapp/images/vertical_line.gif deleted file mode 100644 index 65f8ee78..00000000 Binary files a/codes/javaee/jsp/src/main/webapp/images/vertical_line.gif and /dev/null differ diff --git a/codes/javaee/jsp/src/main/webapp/views/css/style.css b/codes/javaee/jsp/src/main/webapp/views/css/style.css deleted file mode 100644 index c97394dc..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/css/style.css +++ /dev/null @@ -1,18 +0,0 @@ -body, div, td, input {font-size:12px; margin:0px; } -select {height:20px; width:300px; } -.title {font-size: 16px; padding: 10px; margin:10px; width:80%; } -.text {height:20px; width:300px; border:1px solid #AAAAAA; } -.line {margin:2px; } -.leftDiv {width:110px; float:left; height:22px; line-height:22px; font-weight:bold; } -.rightDiv {height:22px; line-height:22px; } -.button { - color:#fff; - font-weight:bold; - font-size: 11px; - text-align:center; - padding:.17em 0 .2em .17em; - border-style:solid; - border-width:1px; - border-color:#9cf #159 #159 #9cf; - background:#69c url(../images/bg-btn-blue.gif) repeat-x; -} \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/action.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/action.jsp deleted file mode 100644 index e2353464..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/action.jsp +++ /dev/null @@ -1,7 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> - - - -这里是正文 - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/action/date.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/action/date.jsp deleted file mode 100644 index 42913eab..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/action/date.jsp +++ /dev/null @@ -1,4 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> -

- 今天的日期是: <%=(new java.util.Date()).toLocaleString()%> -

\ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/action/forward.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/action/forward.jsp deleted file mode 100644 index 3f2f7181..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/action/forward.jsp +++ /dev/null @@ -1,12 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> - - - - -jsp:forward范例 - - -

jsp:forward范例

- - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/action/getProperty.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/action/getProperty.jsp deleted file mode 100644 index ec8c48a4..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/action/getProperty.jsp +++ /dev/null @@ -1,19 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> - - - -jsp:setProperty和jsp:getProperty使用范例 - - - -

jsp:setProperty和jsp:getProperty使用范例

- - - - -

输出信息....

- - - - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/action/include.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/action/include.jsp deleted file mode 100644 index 55db92e9..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/action/include.jsp +++ /dev/null @@ -1,12 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> - - - - -jsp:include范例 - - -

include 动作实例

- - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/action/useBean.html b/codes/javaee/jsp/src/main/webapp/views/jsp/action/useBean.html deleted file mode 100644 index 528dd91d..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/action/useBean.html +++ /dev/null @@ -1,37 +0,0 @@ - - -Java Bean Actions - - - - -
-
-
-
- 请填写 Person 信息 - - - - - - - - - - - - - - - - - -
姓名:
年龄:
性别:Male Female
-
-
-
-
- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/action/useBean.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/action/useBean.jsp deleted file mode 100644 index 0213f300..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/action/useBean.jsp +++ /dev/null @@ -1,50 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - - -Java Bean Actions - - - -
- - <%-- 声明 Person 类对象 person --%> - - - <%-- 设置 person 的所有属性,所有的属性值从 request 中自动取得 --%> - - -
-
-
- 请填写 Person 信息 - - - - - - - - - - - - - - - - - -
姓名: - <%-- 获取 person 的 name 属性 --%> -
年龄: - <%-- 获取 person 的 age 属性 --%> -
性别: - <%-- 获取 person 的 sex 属性 --%> -
-
-
-
-
- - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/application.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/application.jsp deleted file mode 100644 index ab926c25..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/application.jsp +++ /dev/null @@ -1,16 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> -<% - out.getBufferSize(); - - out.println(request); - - - -%> - -JSP Scriptlets - - - - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/break.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/break.jsp deleted file mode 100644 index b7de724b..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/break.jsp +++ /dev/null @@ -1,23 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - -JSP Scriptlets - - -<% - - for(int i=0; i<5; i++){ -%> break 所在的循环, i = <%= i %>.
-<% - if(i == 2) break; - } -%> break 循环完毕.
-<% - for(int i=0; i<5; i++){ -%> return 所在的循环, i = <%= i %>.
-<% - if(i == 2) return; - } -%> return 循环完毕.
- - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/comment.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/comment.jsp deleted file mode 100644 index 6006ae8b..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/comment.jsp +++ /dev/null @@ -1,21 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> -<% - // 这是 Java 行注释 - String path = request.getContextPath(); - - /* - 这是 Java 多行注释 - */ -%> - - - - JSP 注释 - -<%-- - 这是 JSP 注释,可以添加多行注释 ---%> - - This is my JSP page.
- - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/counter.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/counter.jsp deleted file mode 100644 index 6757ece3..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/counter.jsp +++ /dev/null @@ -1,41 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8" %> - - -计数器 - - -
- -<%-- 定义一个 session 范围内的计数器 记录个人访问信息 --%> - - -<%-- 定义一个 application 范围内的计数器 记录所有人的访问信息 --%> - - -
-
-
- 计数器 - - - - - - - - - -
您的访问次数: - <%-- 获取个人的 访问次数 --%> - 次 -
总共的访问次数: - <%-- 获取所有人的 访问次数 --%> - 次 -
-
-
-
- - - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/directive.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/directive.jsp deleted file mode 100644 index 40aa1215..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/directive.jsp +++ /dev/null @@ -1,4 +0,0 @@ -<%@page contentType="image/jpeg"%> - - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/directive/include/foot.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/directive/include/foot.jsp deleted file mode 100644 index 1673b0b2..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/directive/include/foot.jsp +++ /dev/null @@ -1,12 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - - - - - - -
- Copyright 2007-2010 ©Helloweenvsfei
- - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/directive/include/head.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/directive/include/head.jsp deleted file mode 100644 index 30670c4f..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/directive/include/head.jsp +++ /dev/null @@ -1,23 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - - -JSP指令include范例 - - - - - - - - - - - - - - - - - - -
JSP指令include范例
首页资源文档下载关于邮件社区
\ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/directive/include/include.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/directive/include/include.jsp deleted file mode 100644 index 0169c331..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/directive/include/include.jsp +++ /dev/null @@ -1,8 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - -<%@ include file="head.jsp" %> -

-

如今且说林黛玉自在荣府以来,贾母万般怜爱,寝食起居,一如宝玉,迎春,探春, 惜春三个亲孙女倒且靠后, 便是宝玉和黛玉二人之亲密友爱处,亦自较别个不同,日则同行同坐,夜则同息同止,真是言和意顺,略无参商.不想如今忽然来了一个薛宝钗 ,年岁虽大不多,然品格端方,容貌丰美,人多谓黛玉所不及.而且宝钗行为豁达,随分从时, 不比黛玉孤高自许,目无下尘,故比黛玉大得下人之心.便是那些小丫头子们, 亦多喜与宝钗去顽. 因此黛玉心中便有些悒郁不忿之意,宝钗却浑然不觉.那宝玉亦在孩提之间, 况自天性所禀来的一片愚拙偏僻,视姊妹弟兄皆出一意,并无亲疏远近之别.其中因与黛玉同随贾母一处坐卧,故略比别个姊妹熟惯些.既熟惯,则更觉亲密 , 既亲密,则不免一时有求全之毁,不虞之隙.这日不知为何,他二人言语有些不合起来,黛玉又气的独在房中垂泪,宝玉又自悔言语冒撞,前去俯就,那黛玉方渐渐的回转来. 因东边宁府中花园内梅花盛开,贾珍之妻尤氏乃治酒,请贾母,邢夫人,王夫人等赏花. 是日先携了贾蓉之妻,二人来面请.贾母等于早饭后过来,就在会芳园游顽,先茶后酒,不过皆是宁荣二府女眷家宴小集,并无别样新文趣事可记. -

一时宝玉倦怠,欲睡中觉,贾母命人好生哄着,歇一回再来.贾蓉之妻秦氏便忙笑回道:"我们这里有给宝叔收拾下的屋子,老祖宗放心,只管交与我就是了."又向宝玉的奶娘丫鬟等道:"嬷嬷,姐姐们,请宝叔随我这里来."贾母素知秦氏是个极妥当的人 , 生的袅娜纤巧,行事又温柔和平,乃重孙媳中第一个得意之人,见他去安置宝玉,自是安稳的. -

-<%@ include file="foot.jsp" %> \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/directive/page/contentType.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/directive/page/contentType.jsp deleted file mode 100644 index 2e888d21..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/directive/page/contentType.jsp +++ /dev/null @@ -1,12 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> - - - -JSP指令page-属性contentType - - - -

你好啊

-

contentType="text/html; charset=UTF-8"

- - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/directive/page/errorPage.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/directive/page/errorPage.jsp deleted file mode 100644 index 509bfe41..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/directive/page/errorPage.jsp +++ /dev/null @@ -1,13 +0,0 @@ - -<%@ page language="java" errorPage="isErrorPage.jsp" pageEncoding="UTF-8"%> - - -JSP指令page-属性errorPage - - - <% - //这行代码肯定会出错,因为除数是0,一运行就会抛出异常 - int x = 1 / 0; - %> - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/directive/page/isErrorPage.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/directive/page/isErrorPage.jsp deleted file mode 100644 index f4f884aa..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/directive/page/isErrorPage.jsp +++ /dev/null @@ -1,12 +0,0 @@ - -<%@ page language="java" pageEncoding="UTF-8" isErrorPage="true"%> - - -JSP - - - <% - out.println("程序抛出了一个异常:" + exception); - %> - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/el.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/el.jsp deleted file mode 100644 index 9cb21fc4..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/el.jsp +++ /dev/null @@ -1,26 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" %> - -<%! - String ss1 = "ss String"; -%> -<% -%> - - - My JSP 'el.jsp' starting page - - - - - - - - - - - ${not (1==2) } - - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/error.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/error.jsp deleted file mode 100644 index 59893f09..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/error.jsp +++ /dev/null @@ -1,11 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8" isErrorPage="true"%> - - -JSP - - - <% - out.println("程序抛出了一个异常:" + exception); - %> - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/exception.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/exception.jsp deleted file mode 100644 index 4abf68c2..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/exception.jsp +++ /dev/null @@ -1,14 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8" errorPage="/error.jsp" %> -<% - out.clear(); - String str = null; - // length() 操作会抛出 NullPointerException - int length = str.length(); -%> - -JSP - - - - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/grammar/for.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/grammar/for.jsp deleted file mode 100644 index c733f75d..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/grammar/for.jsp +++ /dev/null @@ -1,52 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - -JSP Scriptlets - - - -
-<% - Object[][] letters = { - {true, "恭喜您注册的信息已经生效", "e_inn@163.com", "helloweenvsfei@gmail.com", "2007-8-8"}, - {true, "Java EE 5.0 release!!", "admin@sun.com", "helloweenvsfei@gmail.com", "2007-6-24"}, - {false, "来信已经收到,下周末见面商谈", "foo@bar.com", "helloweenvsfei@gmail.com", "2007-5-20"}, - {false, "您的博客有新的留言", "blog@foo.bar.com", "helloweenvsfei@gmail.com", "2007-3-2"}, - }; - String[] colors = {"#DDDDDD", "#AAAAAA", }; -%> - - - - - - - - -<% - for(int i=0; i - - - - - - - -<% - } -%> -
 标题 发信人 收信人 时间 
- <% - if(letter[0] == Boolean.TRUE){ - %> - - <% - } - else{ - out.println(" "); - } - %> - <%= letter[1] %><%= letter[2] %><%= letter[3] %><%= letter[4] %>
- - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/grammar/if.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/grammar/if.jsp deleted file mode 100644 index 05140e7b..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/grammar/if.jsp +++ /dev/null @@ -1,51 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - -JSP Scriptlets - - -<% - String param = request.getParameter("param"); - - if("1".equals(param)){ -%> -关雎·周南·诗经
-关关雎鸠,在河之洲。窈窕淑女,君子好逑。
-参差荇菜,左右流之。窈窕淑女,寤寐求之。
-求之不得,寤寐思服。悠哉悠哉,辗转反侧。
-参差荇菜,左右采之。窈窕淑女,琴瑟友之。
-参差荇菜,左右芼之。窈窕淑女,钟鼓乐之。
-<% - } - else if("2".equals(param)){ -%> -蒹葭·秦风·诗经
-蒹葭苍苍,白露为霜。所谓伊人,在水一方。
-溯洄从之,道阻且长。溯游从之,宛在水中央。
-蒹葭凄凄,白露未晞。所谓伊人,在水之湄。
-溯洄从之,道阻且跻。溯游从之,宛在水中坻。
-蒹葭采采,白露未已。所谓伊人,在水之涘。
-溯洄从之,道阻且右。溯游从之,宛在水中沚。
-<% - } - else if("3".equals(param)){ -%> -子衿·国风·郑风
-青青子衿,悠悠我心。
-纵我不往,子宁不嗣音?
-青青子佩,悠悠我思。
-纵我不往,子宁不来?
-挑兮达兮,在城阙兮。
-一日不见,如三月兮。
-<% - } - else{ -%> -请使用参数 param=1, 2, 3 选择要显示的诗歌

-if.jsp?param=1
-if.jsp?param=2
-if.jsp?param=3
-<% - } -%> - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/grammar/while.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/grammar/while.jsp deleted file mode 100644 index 5b93d0db..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/grammar/while.jsp +++ /dev/null @@ -1,23 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - -JSP Scriptlets - - -<% - java.util.List list = new java.util.ArrayList(); - - list.add("茕茕白兔"); - list.add("东走西顾"); - list.add("衣不如新"); - list.add("人不如故"); - - java.util.Iterator it = list.iterator(); - - while(it.hasNext()){ -%> <%= it.next() %>
-<% - } - -%> - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/greeting.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/greeting.jsp deleted file mode 100644 index 5703e327..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/greeting.jsp +++ /dev/null @@ -1,42 +0,0 @@ -<%@ page language="java" import="java.util.*" contentType="text/html; charset=utf-8"%> -<% - Locale locale = request.getLocale(); - - Calendar calendar = Calendar.getInstance(locale); - - int hour = calendar.get(Calendar.HOUR_OF_DAY); - - String greeting = ""; - - if (hour <= 6) { - greeting = "凌晨好,您该睡觉了。良好的睡眠是美好一天的开始。"; - } else if (hour <= 9) { - greeting = "早上好。早餐应该注意营养。"; - } else if (hour <= 12) { - greeting = "上午好。工作时注意保护眼睛。"; - } else if (hour <= 18) { - greeting = "下午好。小心工作中打瞌睡。"; - } else if (hour <= 24) { - greeting = "晚上好。放松一下自己,好好休息。睡觉不要太晚啊~~"; - } else { - - } -%> - - - - 欢迎页面 - - - - - - - - - - - -
<%= greeting %>
- - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/index.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/index.jsp deleted file mode 100644 index 7dd27143..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/index.jsp +++ /dev/null @@ -1,25 +0,0 @@ -<%@ page language="java" import="java.util.*" pageEncoding="GB18030" %> -<% - String path = request.getContextPath(); - String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; -%> - - - - - - javaee-jsp ҳ - - - - - - - - - -This is my JSP page.
- - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/life.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/life.jsp deleted file mode 100644 index 6ddc9416..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/life.jsp +++ /dev/null @@ -1,15 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> -<%! - -%> - - - - - -Insert title here - - - - - \ No newline at end of file diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/method.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/method.jsp deleted file mode 100644 index 98d7415c..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/method.jsp +++ /dev/null @@ -1,82 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> -<%@ page import="com.helloweenvsfei.util.ip.IPSeeker" %> -<%! - // 全局变量 - private IPSeeker ipSeeker = IPSeeker.getInstance(); - - // 方法一 - public String getArea(String ip){ - return ipSeeker.getArea(ip); - } - - //方法二 - public String getCountry(String ip){ - return ipSeeker.getCountry(ip); - } - - // 方法三 正则表达式判断是否合法 IP 地址 - public boolean isValidIp(String ip){ - return ip!=null - && ip.trim().matches("^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$"); - } -%> - - -IP 地址查询 - - -
-<% - String ip = request.getParameter("ip"); - String area = ""; - String country = ""; - - // 如果是合法的 IP 地址 - if(isValidIp(ip)){ - // 调用方法一 - country = getCountry(ip); - // 调用方法二 - area = getArea(ip); - } - -%> -
-
-
- IP 地址查询 - - <% - if( isValidIp(ip) ){ - %> - - - - - - - - - - - - - <% - } - %> - - - - - - - - -
IP 地址:<%= ip %>
国家:<%= country %>
地区:<%= area %>
请输入要查询的 IP 地址:
- -
-
-
-
- - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/plugin.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/plugin.jsp deleted file mode 100644 index 6916eb6e..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/plugin.jsp +++ /dev/null @@ -1,28 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8" %> - - -plugin - - -
- -
- - - - - - - 您的浏览器不支持 Java Applet - - -
- - - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/return.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/return.jsp deleted file mode 100644 index 381d415e..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/return.jsp +++ /dev/null @@ -1,27 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - -JSP Scriptlets - - -<% - String param = request.getParameter("param"); -%> -昔我往矣,
-杨柳依依。
-今我来思,
-雨雪霏霏。
-<% - if("return".equals(param)){ - return; - } -%> -青青子衿,
-悠悠我心,
-但为君故,
-沉吟至今!
- -
-<%= request.getRequestURI() %>?param=return - - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/scriptlet.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/scriptlet.jsp deleted file mode 100644 index 749b6006..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/scriptlet.jsp +++ /dev/null @@ -1,17 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=utf-8"%> - -JSP Scriptlets - - -<% - int num = 10; - int result = 1; - for(int i=1; i<=num; i++){ - result *= i; - out.println("第" + i + "步运算:" + result + "
"); - } - out.println("
"); - out.println("数字 " + num + " 的阶乘为: " + result); -%> - - diff --git a/codes/javaee/jsp/src/main/webapp/views/jsp/taglib.jsp b/codes/javaee/jsp/src/main/webapp/views/jsp/taglib.jsp deleted file mode 100644 index 9d05cc47..00000000 --- a/codes/javaee/jsp/src/main/webapp/views/jsp/taglib.jsp +++ /dev/null @@ -1,14 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> - -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - -My JSP 'taglib.jsp' starting page - - - - - - - - diff --git a/codes/javaee/jsp/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java b/codes/javaee/jsp/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java deleted file mode 100644 index 2c85755f..00000000 --- a/codes/javaee/jsp/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.github.dunwu.javaee.server; - -import org.apache.commons.lang3.StringUtils; -import org.assertj.core.util.Lists; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.WebAppClassLoader; -import org.eclipse.jetty.webapp.WebAppContext; - -import java.util.ArrayList; - -/** - * JettyFactory 可以工作在 Eclipse 和 Intellij 中,用来启动 jetty 服务。 Intellij - * 并不支持jetty,所以要想类似eclipse一样的使用jetty,需要配置webdefault.xml。 - * - * @author Zhang Peng - */ -@SuppressWarnings("unused") -public class JettyFactory { - private static final int PORT = 9798; - private static final String CONTEXT = "/"; - private static final String RESOURCE_BASE_PATH = "src/main/webapp"; - private static final String WEB_XML_PATH = "/WEB-INF/web.xml"; - private static final String[] TLD_JAR_NAMES = - new String[]{"sitemesh", "spring-webmvc", "shiro-web", "tiles"}; - private static final String WINDOWS_WEBDEFAULT_PATH = "jetty/webdefault.xml"; - - public static final int IDE_ECLIPSE = 0; - public static final int IDE_INTELLIJ = 1; - - - public static Server initServer() { - Profiles.setProfileAsSystemProperty(Profiles.DEVELOPMENT); - WebAppContext webAppContext = new WebAppContext(); - Server server = new Server(PORT); - server.setHandler(webAppContext); - return server; - } - - public static void initWebAppContext(Server server, int type) throws Exception { - System.out.println("[INFO] Application loading"); - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - webAppContext.setContextPath(CONTEXT); - webAppContext.setResourceBase(getAbsolutePath() + RESOURCE_BASE_PATH); - webAppContext.setDescriptor(getAbsolutePath() + RESOURCE_BASE_PATH + WEB_XML_PATH); - - if (IDE_INTELLIJ == type) { - webAppContext.setDefaultsDescriptor(WINDOWS_WEBDEFAULT_PATH); - supportJspAndSetTldJarNames(server, TLD_JAR_NAMES); - } else { - webAppContext.setParentLoaderPriority(true); - } - - System.out.println("[INFO] Application loaded"); - } - - public static void reloadWebAppContext(Server server) throws Exception { - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - System.out.println("[INFO] Application reloading"); - webAppContext.stop(); - WebAppClassLoader classLoader = new WebAppClassLoader(webAppContext); - classLoader.addClassPath(getAbsolutePath() + "target/classes"); - classLoader.addClassPath(getAbsolutePath() + "target/test-classes"); - webAppContext.setClassLoader(classLoader); - webAppContext.start(); - System.out.println("[INFO] Application reloaded"); - } - - - public static void startServer(Server server) throws Exception { - System.out.println("[HINT] Don't forget to set -XX:MaxPermSize=128m"); - server.start(); - System.out.println("Server running at http://localhost:" + PORT + CONTEXT); - System.out.println("[HINT] Hit Enter to reload the application quickly"); - } - - public static String getAbsolutePath() { - String path = null; - String folderPath = JettyFactory.class.getProtectionDomain().getCodeSource().getLocation() - .getPath().substring(1); - if (folderPath.indexOf("target") > 0) { - path = folderPath.substring(0, folderPath.indexOf("target")); - } - return path; - } - - public static void supportJspAndSetTldJarNames(Server server, String... jarNames) { - WebAppContext context = (WebAppContext) server.getHandler(); - // This webapp will use jsps and jstl. We need to enable the AnnotationConfiguration in - // order to correctly set up the jsp container - org.eclipse.jetty.webapp.Configuration.ClassList classlist = - org.eclipse.jetty.webapp.Configuration.ClassList.setServerDefault(server); - classlist.addBefore("org.eclipse.jetty.webapp.JettyWebXmlConfiguration", - "org.eclipse.jetty.annotations.AnnotationConfiguration"); - // Set the ContainerIncludeJarPattern so that jetty examines these container-path jars for - // tlds, web-fragments etc. - // If you omit the jar that contains the jstl .tlds, the jsp engine will scan for them - // instead. - ArrayList jarNameExprssions = Lists.newArrayList(".*/[^/]*servlet-api-[^/]*\\.jar$", - ".*/javax.servlet.jsp.jstl-.*\\.jar$", ".*/[^/]*taglibs.*\\.jar$"); - - for (String jarName : jarNames) { - jarNameExprssions.add(".*/" + jarName + "-[^/]*\\.jar$"); - } - - context.setAttribute("org.eclipse.jetty.io.github.dunwu.javaee.server.webapp.ContainerIncludeJarPattern", - StringUtils.join(jarNameExprssions, '|')); - } -} diff --git a/codes/javaee/jsp/src/test/java/io/github/dunwu/javaee/server/Profiles.java b/codes/javaee/jsp/src/test/java/io/github/dunwu/javaee/server/Profiles.java deleted file mode 100644 index 3a8ee7c4..00000000 --- a/codes/javaee/jsp/src/test/java/io/github/dunwu/javaee/server/Profiles.java +++ /dev/null @@ -1,29 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2005, 2014 springside.github.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - *******************************************************************************/ -package io.github.dunwu.javaee.server; - -/** - * Spring profile 常用方法与profile名称。 - * - * @author calvin - */ -public class Profiles { - - public static final String ACTIVE_PROFILE = "spring.profiles.active"; - public static final String DEFAULT_PROFILE = "spring.profiles.default"; - - public static final String PRODUCTION = "production"; - public static final String DEVELOPMENT = "development"; - public static final String UNIT_TEST = "test"; - public static final String FUNCTIONAL_TEST = "functional"; - - /** - * 在Spring启动前,设置profile的环境变量。 - */ - public static void setProfileAsSystemProperty(String profile) { - System.setProperty(ACTIVE_PROFILE, profile); - } -} diff --git a/codes/javaee/jsp/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java b/codes/javaee/jsp/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java deleted file mode 100644 index 2936b162..00000000 --- a/codes/javaee/jsp/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.github.dunwu.javaee.server; - -import org.eclipse.jetty.server.Server; - -/** - * 快速启动 jetty 服务器,方便测试 - * - * @author Zhang Peng - */ -public class QuickStartServer { - // private static int STARTUP_TYPE = JettyFactory.IDE_ECLIPSE; - private static int STARTUP_TYPE = JettyFactory.IDE_INTELLIJ; - - public static void main(String[] args) throws Exception { - Server server = JettyFactory.initServer(); - JettyFactory.initWebAppContext(server, STARTUP_TYPE); - - try { - JettyFactory.startServer(server); - - // 等待用户输入回车重载应用 - while (true) { - char c = (char) System.in.read(); - if (c == '\n') { - JettyFactory.reloadWebAppContext(server); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.exit(-1); - } - } -} diff --git a/codes/javaee/jsp/src/test/resources/jetty/webdefault.xml b/codes/javaee/jsp/src/test/resources/jetty/webdefault.xml deleted file mode 100644 index 65ec9cae..00000000 --- a/codes/javaee/jsp/src/test/resources/jetty/webdefault.xml +++ /dev/null @@ -1,534 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Default web.xml file. - This file is applied to a Web application before it's own WEB_INF/web.xml file - - - - - - - - org.eclipse.jetty.servlet.listener.ELContextCleaner - - - - - - - - org.eclipse.jetty.servlet.listener.IntrospectorCleaner - - - - - - - - - - - - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - aliases - false - - - acceptRanges - true - - - dirAllowed - true - - - welcomeServlets - false - - - redirectWelcome - false - - - maxCacheSize - 256000000 - - - maxCachedFileSize - 200000000 - - - maxCachedFiles - 2048 - - - gzip - false - - - etags - false - - - useFileMappedBuffer - false - - - - 0 - - - - default - / - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jsp - org.eclipse.jetty.jsp.JettyJspServlet - - logVerbosityLevel - DEBUG - - - fork - false - - - xpoweredBy - false - - - compilerTargetVM - 1.7 - - - compilerSourceVM - 1.7 - - - 0 - - - - jsp - *.jsp - *.jspf - *.jspx - *.xsp - *.JSP - *.JSPF - *.JSPX - *.XSP - - - - - - - - 30 - - - - - - - - - - - - - - - index.html - index.htm - index.jsp - - - - - - - - ar - ISO-8859-6 - - - be - ISO-8859-5 - - - bg - ISO-8859-5 - - - ca - ISO-8859-1 - - - cs - ISO-8859-2 - - - da - ISO-8859-1 - - - de - ISO-8859-1 - - - el - ISO-8859-7 - - - en - ISO-8859-1 - - - es - ISO-8859-1 - - - et - ISO-8859-1 - - - fi - ISO-8859-1 - - - fr - ISO-8859-1 - - - hr - ISO-8859-2 - - - hu - ISO-8859-2 - - - is - ISO-8859-1 - - - it - ISO-8859-1 - - - iw - ISO-8859-8 - - - ja - Shift_JIS - - - ko - EUC-KR - - - lt - ISO-8859-2 - - - lv - ISO-8859-2 - - - mk - ISO-8859-5 - - - nl - ISO-8859-1 - - - no - ISO-8859-1 - - - pl - ISO-8859-2 - - - pt - ISO-8859-1 - - - ro - ISO-8859-2 - - - ru - ISO-8859-5 - - - sh - ISO-8859-5 - - - sk - ISO-8859-2 - - - sl - ISO-8859-2 - - - sq - ISO-8859-2 - - - sr - ISO-8859-5 - - - sv - ISO-8859-1 - - - tr - ISO-8859-9 - - - uk - ISO-8859-5 - - - zh - GB2312 - - - zh_TW - Big5 - - - - - - - - - Disable TRACE - / - TRACE - - - - - - Enable everything but TRACE - / - TRACE - - - - - diff --git a/codes/javaee/jsp/src/test/resources/logback.xml b/codes/javaee/jsp/src/test/resources/logback.xml deleted file mode 100644 index 69f4fbae..00000000 --- a/codes/javaee/jsp/src/test/resources/logback.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - logs/${FILE_NAME}-all.%d{yyyy-MM-dd}.log - 30 - - - - - 30MB - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - - - - - - - - - - - diff --git a/codes/javaee/jstl/pom.xml b/codes/javaee/jstl/pom.xml deleted file mode 100644 index 72783785..00000000 --- a/codes/javaee/jstl/pom.xml +++ /dev/null @@ -1,184 +0,0 @@ - - - 4.0.0 - - - io.github.dunwu - javaee-notes-jstl - 1.0.0 - war - - - - - javaee-notes-jstl - javaee 学习笔记之 jstl - - - - - - UTF-8 - 1.7 - ${java.version} - ${java.version} - - - 9.3.2.v20150730 - - - - - ch.qos.logback - logback-classic - 1.1.3 - - - ch.qos.logback - logback-core - 1.1.3 - - - org.logback-extensions - logback-ext-spring - 0.1.2 - - - org.slf4j - jcl-over-slf4j - 1.7.12 - - - - - - commons-fileupload - commons-fileupload - 1.3.1 - - - commons-io - commons-io - 2.5 - - - org.apache.commons - commons-lang3 - 3.4 - - - - - - javax.servlet - javax.servlet-api - 3.1.0 - - - javax.servlet.jsp - jsp-api - 2.2 - - - javax.servlet - jstl - 1.2 - - - - - javax.servlet.jsp.jstl - jstl-api - 1.2 - - - javax.servlet - servlet-api - - - javax.servlet.jsp - jsp-api - - - - - - org.glassfish.web - jstl-impl - 1.2 - - - javax.servlet - servlet-api - - - javax.servlet.jsp - jsp-api - - - javax.servlet.jsp.jstl - jstl-api - - - - - - - - org.eclipse.jetty - jetty-webapp - ${jetty.version} - - - org.eclipse.jetty - jetty-annotations - ${jetty.version} - - - org.eclipse.jetty - apache-jsp - ${jetty.version} - - - org.eclipse.jetty - apache-jstl - ${jetty.version} - - - - - - junit - junit - 4.12 - - - org.assertj - assertj-core - 3.4.1 - - - - - - - xalan - xalan - 2.7.2 - - - xerces - xercesImpl - 2.11.0 - - - - - - - - - - - diff --git a/codes/javaee/jstl/src/main/java/io/github/dunwu/javaee/bean/Person.java b/codes/javaee/jstl/src/main/java/io/github/dunwu/javaee/bean/Person.java deleted file mode 100644 index d7b16ba4..00000000 --- a/codes/javaee/jstl/src/main/java/io/github/dunwu/javaee/bean/Person.java +++ /dev/null @@ -1,107 +0,0 @@ -package io.github.dunwu.javaee.bean; - -public class Person { - - private int id; - - private String name; - - private String sex; - - private int age; - - private String telephone; - - private String birthday; - - private String mobile; - - private String address; - - private String city; - - private boolean deleted; - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getSex() { - return sex; - } - - public void setSex(String sex) { - this.sex = sex; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - public boolean isDeleted() { - return deleted; - } - - public void setDeleted(boolean deleted) { - this.deleted = deleted; - } - - public String getTelephone() { - return telephone; - } - - public void setTelephone(String telephone) { - this.telephone = telephone; - } - - public String getBirthday() { - return birthday; - } - - public void setBirthday(String birthday) { - this.birthday = birthday; - } - - public String getMobile() { - return mobile; - } - - public void setMobile(String mobile) { - this.mobile = mobile; - } - - public String getAddress() { - return address; - } - - public void setAddress(String address) { - this.address = address; - } - - public String getCity() { - return city; - } - - public void setCity(String city) { - this.city = city; - } - -} - -// end diff --git a/codes/javaee/jstl/src/main/java/io/github/dunwu/javaee/util/Example.java b/codes/javaee/jstl/src/main/java/io/github/dunwu/javaee/util/Example.java deleted file mode 100644 index 87758b27..00000000 --- a/codes/javaee/jstl/src/main/java/io/github/dunwu/javaee/util/Example.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.dunwu.javaee.util; - -import java.util.ListResourceBundle; - -public class Example extends ListResourceBundle { - public Object[][] getContents() { - return contents; - } - - static final Object[][] contents = - {{"count.one", "一"}, {"count.two", "二"}, {"count.three", "三"},}; -} diff --git a/codes/javaee/jstl/src/main/java/io/github/dunwu/javaee/util/Example_es_ES.java b/codes/javaee/jstl/src/main/java/io/github/dunwu/javaee/util/Example_es_ES.java deleted file mode 100644 index 48f2366e..00000000 --- a/codes/javaee/jstl/src/main/java/io/github/dunwu/javaee/util/Example_es_ES.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.dunwu.javaee.util; - -import java.util.ListResourceBundle; - -public class Example_es_ES extends ListResourceBundle { - public Object[][] getContents() { - return contents; - } - - static final Object[][] contents = - {{"count.one", "one"}, {"count.two", "two"}, {"count.three", "three"},}; -} diff --git a/codes/javaee/jstl/src/main/java/io/github/dunwu/javaee/util/Pagination.java b/codes/javaee/jstl/src/main/java/io/github/dunwu/javaee/util/Pagination.java deleted file mode 100644 index 29561325..00000000 --- a/codes/javaee/jstl/src/main/java/io/github/dunwu/javaee/util/Pagination.java +++ /dev/null @@ -1,209 +0,0 @@ -package io.github.dunwu.javaee.util; - -import java.net.URLEncoder; - -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class Pagination { - private int pageSize = 20; - - private int pageNum = 1; - - private int recordCount; - - private int pageCount; - - private int firstResult; - - private String pageUrl; - - public Pagination(HttpServletRequest request, HttpServletResponse response) { - try { - pageNum = Integer.parseInt(request.getParameter("pageNum")); - } catch (Exception e) {} - - for (Cookie cookie : request.getCookies()) { - if ("pageSize".equals(cookie.getName())) { - try { - pageSize = Integer.parseInt(cookie.getValue()); - } catch (Exception e) {} - } - } - - try { - pageSize = Integer.parseInt(request.getParameter("pageSize")); - } catch (Exception e) {} - - Cookie cookie = new Cookie("pageSize", Integer.toString(pageSize)); - cookie.setMaxAge(Integer.MAX_VALUE); - - response.addCookie(cookie); - - StringBuffer queryString = new StringBuffer(); - - for (Object parameterName : request.getParameterMap().keySet()) { - String name = (String) parameterName; - - if ("pageNum".equals(name) || "pageSize".equals(name)) { - continue; - } - - for (String value : request.getParameterValues(name)) { - if (queryString.length() > 0) { - queryString.append("&"); - } - - try { - queryString.append(name + "=" + URLEncoder.encode(value, "UTF-8")); - } catch (Exception e) { - queryString.append(name + "=" + value); - } - } - } - - pageUrl = request.getRequestURI() + "?" + queryString.toString(); - } - - private void calculate() { - pageCount = (recordCount + pageSize - 1) / pageSize; - - firstResult = (pageNum - 1) * pageSize; - } - - /** - * 生成分页信息 包括第一页,上一页,下一页,最后一页等等。 - */ - public String toString() { - calculate(); - - String url = pageUrl.contains("?") ? pageUrl : pageUrl + "?"; - - StringBuffer buffer = new StringBuffer(); - - buffer.append("每页 "); - - buffer.append(" 条记录 "); - - buffer.append(" 总记录数: " + recordCount); - - buffer.append(" 页数/总页数: " + pageNum + "/" + pageCount + " "); - - buffer.append(" "); - - buffer.append(pageCount == 0 || pageNum == 1 - ? " 第一页 " - : " 第一页 "); - - buffer.append("   "); - - buffer.append(pageCount == 0 || pageNum == 1 - ? " 上一页 " - : " 上一页 "); - - buffer.append("   "); - - buffer.append(pageCount == 0 || pageNum == pageCount - ? " 下一页 " - : " 下一页 "); - - buffer.append("   "); - - buffer.append(pageCount == 0 || pageNum == pageCount - ? " 最后一页 " - : " 最后一页 "); - - buffer.append("   转到第页 "); - - buffer.append(" "); - - buffer.append(""); - - return buffer.toString(); - - } - - public int getPageSize() { - calculate(); - - return pageSize; - } - - public void setPageSize(int pageSize) { - calculate(); - - this.pageSize = pageSize; - } - - public int getRecordCount() { - calculate(); - - return recordCount; - } - - public void setRecordCount(int recordCount) { - calculate(); - - this.recordCount = recordCount; - } - - public int getFirstResult() { - calculate(); - - return firstResult; - } - - public void setFirstResult(int firstResult) { - calculate(); - - this.firstResult = firstResult; - } - - public String getPageUrl() { - return pageUrl + "&pageNum=" + pageNum; - } - - public void setPageUrl(String pageUrl) { - this.pageUrl = pageUrl; - } - -} - -// end diff --git a/codes/javaee/jstl/src/main/resources/init.sql b/codes/javaee/jstl/src/main/resources/init.sql deleted file mode 100644 index 562a8d72..00000000 --- a/codes/javaee/jstl/src/main/resources/init.sql +++ /dev/null @@ -1,45 +0,0 @@ -USE jstl; - -ALTER DATABASE jstl -CHARACTER SET utf8; - -SET NAMES gbk; - -CREATE TABLE tb_corporation ( - id INTEGER AUTO_INCREMENT, - name VARCHAR(255), - description TEXT, - PRIMARY KEY (id) -); - -INSERT INTO tb_corporation (name, description) VALUES ('MicroSoft', '微软'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); -INSERT INTO tb_corporation (name, description) VALUES ('IBM', '国际商用机器'); - diff --git a/codes/javaee/jstl/src/main/resources/logback.xml b/codes/javaee/jstl/src/main/resources/logback.xml deleted file mode 100644 index 2298f01e..00000000 --- a/codes/javaee/jstl/src/main/resources/logback.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - logs/${FILE_NAME}-all.%d{yyyy-MM-dd}.log - 30 - - - - - 30MB - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - - - - - - - - - - - diff --git a/codes/javaee/jstl/src/main/resources/messages.properties b/codes/javaee/jstl/src/main/resources/messages.properties deleted file mode 100644 index 12e36995..00000000 --- a/codes/javaee/jstl/src/main/resources/messages.properties +++ /dev/null @@ -1,2 +0,0 @@ -prompt.hello=Hello, "{0}". -prompt.greeting=Nice to meet you. \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/resources/messages_zh_CN.properties b/codes/javaee/jstl/src/main/resources/messages_zh_CN.properties deleted file mode 100644 index 46054c07..00000000 --- a/codes/javaee/jstl/src/main/resources/messages_zh_CN.properties +++ /dev/null @@ -1,2 +0,0 @@ -prompt.hello=\u4f60\u597d, "{0}". -prompt.greeting=\u5f88\u9ad8\u5174\u89c1\u5230\u4f60. \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/resources/sql/create_employees.sql b/codes/javaee/jstl/src/main/resources/sql/create_employees.sql deleted file mode 100644 index 707cd5cb..00000000 --- a/codes/javaee/jstl/src/main/resources/sql/create_employees.sql +++ /dev/null @@ -1,12 +0,0 @@ -create table Employees -( - id int not null, - age int not null, - first varchar (255), - last varchar (255) -); - -INSERT INTO Employees VALUES (100, 18, 'Zara', 'Ali'); -INSERT INTO Employees VALUES (101, 25, 'Mahnaz', 'Fatma'); -INSERT INTO Employees VALUES (102, 30, 'Zaid', 'Khan'); -INSERT INTO Employees VALUES (103, 28, 'Sumit', 'Mittal'); \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/resources/sql/create_students.sql b/codes/javaee/jstl/src/main/resources/sql/create_students.sql deleted file mode 100644 index a7a434d6..00000000 --- a/codes/javaee/jstl/src/main/resources/sql/create_students.sql +++ /dev/null @@ -1,12 +0,0 @@ -create table Students -( - id int not null, - first varchar (255), - last varchar (255), - dob date -); - -INSERT INTO Students VALUES (100, 'Zara', 'Ali', '2002/05/16'); -INSERT INTO Students VALUES (101, 'Mahnaz', 'Fatma', '1978/11/28'); -INSERT INTO Students VALUES (102, 'Zaid', 'Khan', '1980/10/10'); -INSERT INTO Students VALUES (103, 'Sumit', 'Mittal', '1971/05/08'); \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/WEB-INF/web.xml b/codes/javaee/jstl/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index aa1943d6..00000000 --- a/codes/javaee/jstl/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - HelloServlet - /examples/configuration.jsp - - message - welcome to jsp - - 1 - - - HelloServlet - /config - /config.jsp - - - - - /WEB-INF/views/jsp/index.jsp - - diff --git a/codes/javaee/jstl/src/main/webapp/examples/core/c_catch.jsp b/codes/javaee/jstl/src/main/webapp/examples/core/c_catch.jsp deleted file mode 100644 index 050092ba..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/core/c_catch.jsp +++ /dev/null @@ -1,20 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - c:catch 标签实例 - - - - - <% int x = 5 / 0;%> - - - -

异常为 : ${catchException}
- 发生了异常: ${catchException.message}

-
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/core/c_choose.jsp b/codes/javaee/jstl/src/main/webapp/examples/core/c_choose.jsp deleted file mode 100644 index 392ba3a1..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/core/c_choose.jsp +++ /dev/null @@ -1,23 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - c:choose 标签实例 - - - -

你的工资为 :

- - - 太惨了。 - - - 不错的薪水,还能生活。 - - - 什么都没有。 - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/core/c_forEach.jsp b/codes/javaee/jstl/src/main/webapp/examples/core/c_forEach.jsp deleted file mode 100644 index 8f0df0d3..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/core/c_forEach.jsp +++ /dev/null @@ -1,13 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - c:forEach 标签实例 - - - -Item

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/core/c_forTokens.jsp b/codes/javaee/jstl/src/main/webapp/examples/core/c_forTokens.jsp deleted file mode 100644 index 5c5abb19..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/core/c_forTokens.jsp +++ /dev/null @@ -1,13 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - c:forTokens 标签实例 - - - -

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/core/c_if.jsp b/codes/javaee/jstl/src/main/webapp/examples/core/c_if.jsp deleted file mode 100644 index 60bc9091..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/core/c_if.jsp +++ /dev/null @@ -1,14 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - c:if 标签实例 - - - - -

我的工资为:

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/core/c_import.jsp b/codes/javaee/jstl/src/main/webapp/examples/core/c_import.jsp deleted file mode 100644 index 9abd8c64..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/core/c_import.jsp +++ /dev/null @@ -1,12 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - c:import 标签实例 - - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/core/c_out.jsp b/codes/javaee/jstl/src/main/webapp/examples/core/c_out.jsp deleted file mode 100644 index 4318e366..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/core/c_out.jsp +++ /dev/null @@ -1,20 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - - c:out 标签实例 - - - - - <c:out>实例 - - -

<c:out> 实例

-
-
-使用的表达式结果为null,则输出该默认值
- - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/core/c_param.jsp b/codes/javaee/jstl/src/main/webapp/examples/core/c_param.jsp deleted file mode 100644 index 40661d86..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/core/c_param.jsp +++ /dev/null @@ -1,21 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - c:forTokens 标签实例 - - -

<c:param> 实例

- - - - -<%-- - - - -"> - 使用 <c:param> 为指定URL发送两个参数。--%> - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/core/c_redirect.jsp b/codes/javaee/jstl/src/main/webapp/examples/core/c_redirect.jsp deleted file mode 100644 index addaa6f8..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/core/c_redirect.jsp +++ /dev/null @@ -1,11 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - c:redirect 标签实例 - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/core/c_remove.jsp b/codes/javaee/jstl/src/main/webapp/examples/core/c_remove.jsp deleted file mode 100644 index d1eb3d92..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/core/c_remove.jsp +++ /dev/null @@ -1,14 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - c:remove 标签实例 - - - -

salary 变量值:

- -

删除 salary 变量后的值:

- - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/core/c_set.jsp b/codes/javaee/jstl/src/main/webapp/examples/core/c_set.jsp deleted file mode 100644 index ec1fe9b8..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/core/c_set.jsp +++ /dev/null @@ -1,12 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - c:set 标签实例 - - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/core/c_url.jsp b/codes/javaee/jstl/src/main/webapp/examples/core/c_url.jsp deleted file mode 100644 index 34fee31e..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/core/c_url.jsp +++ /dev/null @@ -1,14 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - c:url 标签实例 - - -

<c:url>实例 Demo

-"> - 这个链接通过 <c:url> 标签生成。 - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_bundle.jsp b/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_bundle.jsp deleted file mode 100644 index 80920b0c..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_bundle.jsp +++ /dev/null @@ -1,18 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> - - - fmt:bundle 标签 - - - - -
-
-
-
- - - diff --git a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_formatDate.jsp b/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_formatDate.jsp deleted file mode 100644 index a78e01af..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_formatDate.jsp +++ /dev/null @@ -1,33 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> - - - - fmt:dateNumber 标签 - - -

日期格式化:

- - -

日期格式化 (1):

-

日期格式化 (2):

-

日期格式化 (3):

-

日期格式化 (4):

-

日期格式化 (5):

-

日期格式化 (6):

-

日期格式化 (7):

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_formatNumber.jsp b/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_formatNumber.jsp deleted file mode 100644 index 55c3101b..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_formatNumber.jsp +++ /dev/null @@ -1,33 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> - - - - fmt:formatNumber 标签 - - -

数字格式化:

- -

格式化数字 (1):

-

格式化数字 (2):

-

格式化数字 (3):

-

格式化数字 (4):

-

格式化数字 (5):

-

格式化数字 (6):

-

格式化数字 (7):

-

格式化数字 (8):

-

美元 : - -

- - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_message.jsp b/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_message.jsp deleted file mode 100644 index 3c7b5f5b..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_message.jsp +++ /dev/null @@ -1,26 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> - - - fmt:message 标签 - - - - -
-
-
-
- - - - -
-
-
-
- - - diff --git a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_parseDate.jsp b/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_parseDate.jsp deleted file mode 100644 index 98192b0c..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_parseDate.jsp +++ /dev/null @@ -1,19 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> - - - - fmt:parseDate 标签 - - -

日期解析:

- - - -

解析后的日期为:

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_parseNumber.jsp b/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_parseNumber.jsp deleted file mode 100644 index 96b84388..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_parseNumber.jsp +++ /dev/null @@ -1,20 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> - - - fmt:parseNumber 标签 - - -

数字解析:

- - - -

数字解析 (1) :

- -

数字解析 (2) :

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_requestEncoding.jsp b/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_requestEncoding.jsp deleted file mode 100644 index 201b7e2f..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_requestEncoding.jsp +++ /dev/null @@ -1,20 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> - - - fmt:message 标签 - - - - - - - -
-
-
- - - diff --git a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_setLocale.jsp b/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_setLocale.jsp deleted file mode 100644 index 832995cc..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_setLocale.jsp +++ /dev/null @@ -1,26 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> - - - fmt:setLocale 标签 - - - - -
-
-
-
- - - - -
-
-
-
- - - diff --git a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_setTimeZone.jsp b/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_setTimeZone.jsp deleted file mode 100644 index 6ee374dc..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_setTimeZone.jsp +++ /dev/null @@ -1,18 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> - - - fmt:setTimeZone 标签 - - - -

当前时区时间:

-

修改为 GMT-8 时区:

- -

Date in Changed Zone:

- - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_timeZone.jsp b/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_timeZone.jsp deleted file mode 100644 index f03a9053..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/fmt/fmt_timeZone.jsp +++ /dev/null @@ -1,42 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> - - - fmt:timeZone 标签 - - - - - - - - - - - - - - - -
-

- - Formatting: - - - -

-
- - - - - -
- - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_contains.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_contains.jsp deleted file mode 100644 index 760abc1a..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_contains.jsp +++ /dev/null @@ -1,22 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - fn:contains 示例 - - - - - - -

找到 china

- - - -

找到 CHINA

- - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_containsIgnoreCase.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_containsIgnoreCase.jsp deleted file mode 100644 index b23f215c..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_containsIgnoreCase.jsp +++ /dev/null @@ -1,22 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - 使用 JSTL 函数 - - - - - - -

找到 china

- - - -

找到 CHINA

- - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_endsWith.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_endsWith.jsp deleted file mode 100644 index 0084cfdf..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_endsWith.jsp +++ /dev/null @@ -1,22 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - 使用 JSTL 函数 - - - - - - -

字符串以 123 结尾

- - - -

字符串以 china 结尾

- - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_escapeXml.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_escapeXml.jsp deleted file mode 100644 index 9faf2408..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_escapeXml.jsp +++ /dev/null @@ -1,23 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - Using JSTL Functions - - - - - - -

使用 escapeXml() 函数:

-

string (1) : ${fn:escapeXml(string1)}

-

string (2) : ${fn:escapeXml(string2)}

- -

不使用 escapeXml() 函数:

-

string (1) : ${string1}

-

string (2) : ${string2}

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_indexOf.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_indexOf.jsp deleted file mode 100644 index 9fc8dbd4..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_indexOf.jsp +++ /dev/null @@ -1,18 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - Using JSTL Functions - - - - - - -

Index (1) : ${fn:indexOf(string1, "first")}

-

Index (2) : ${fn:indexOf(string2, "second")}

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_join.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_join.jsp deleted file mode 100644 index 244362ce..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_join.jsp +++ /dev/null @@ -1,18 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - 使用 JSTL 函数 - - - - - - - -

字符串为 : ${string3}

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_length.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_length.jsp deleted file mode 100644 index 9dcfaf91..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_length.jsp +++ /dev/null @@ -1,19 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - 使用 JSTL 函数 - - - - - - -

字符串长度 (1) : ${fn:length(string1)}

-

字符串长度 (2) : ${fn:length(string2)}

- - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_replace.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_replace.jsp deleted file mode 100644 index 5a3f7b9f..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_replace.jsp +++ /dev/null @@ -1,19 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - 使用 JSTL 函数 - - - - - - -

替换后的字符串 : ${string2}

- - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_split.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_split.jsp deleted file mode 100644 index 832e4fac..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_split.jsp +++ /dev/null @@ -1,23 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - 使用 JSTL 函数 - - - - - - - -

string3 字符串 : ${string3}

- - - - -

string5 字符串: ${string5}

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_startsWith.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_startsWith.jsp deleted file mode 100644 index 27417ed5..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_startsWith.jsp +++ /dev/null @@ -1,21 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - 使用 JSTL 函数 - - - - - -

字符串以 Google 开头

-
-
- -

字符串以 China 开头

-
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_substring.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_substring.jsp deleted file mode 100644 index 410be03a..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_substring.jsp +++ /dev/null @@ -1,17 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - 使用 JSTL 函数 - - - - - - -

生成的子字符串为 : ${string2}

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_substringAfter.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_substringAfter.jsp deleted file mode 100644 index bea1d206..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_substringAfter.jsp +++ /dev/null @@ -1,17 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - 使用 JSTL 函数 - - - - - - -

生成的子字符串 : ${string2}

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_substringBefore.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_substringBefore.jsp deleted file mode 100644 index 03f82ef8..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_substringBefore.jsp +++ /dev/null @@ -1,17 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - 使用 JSTL 函数 - - - - - - -

生成的子字符串 : ${string2}

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_toLowerCase.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_toLowerCase.jsp deleted file mode 100644 index 832e9215..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_toLowerCase.jsp +++ /dev/null @@ -1,17 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - 使用 JSTL 函数 - - - - - - -

字符串为 : ${string2}

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_toUpperCase.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_toUpperCase.jsp deleted file mode 100644 index 656d58c6..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_toUpperCase.jsp +++ /dev/null @@ -1,17 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - 使用 JSTL 函数 - - - - - - -

字符串为 : ${string2}

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/function/fn_trim.jsp b/codes/javaee/jstl/src/main/webapp/examples/function/fn_trim.jsp deleted file mode 100644 index 8302b9fa..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/function/fn_trim.jsp +++ /dev/null @@ -1,19 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> - - - 使用 JSTL 函数 - - - - -

string1 长度 : ${fn:length(string1)}

- - -

string2 长度 : ${fn:length(string2)}

-

字符串为 : ${string2}

- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/sql/sql_dateParam.jsp b/codes/javaee/jstl/src/main/webapp/examples/sql/sql_dateParam.jsp deleted file mode 100644 index 21ee5290..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/sql/sql_dateParam.jsp +++ /dev/null @@ -1,52 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ page import="java.util.Date" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql" %> - -<%--执行本例之前,需要先运行 sql/create_students.sql--%> - - - - sql:dataParam 示例 - - - - - -<% - Date DoB = new Date("2001/12/16"); - int studentId = 100; -%> - - - UPDATE Students SET dob = ? WHERE Id = ? - - - - - - SELECT * from Students; - - - - - - - - - - - - - - - - - -
Emp IDFirst NameLast NameDoB
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/sql/sql_param.jsp b/codes/javaee/jstl/src/main/webapp/examples/sql/sql_param.jsp deleted file mode 100644 index e3e21cca..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/sql/sql_param.jsp +++ /dev/null @@ -1,47 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql" %> - -<%--运行本例之前,先执行sql/create_employees.sql--%> - - - - sql:param 示例 - - - - - - - - - DELETE FROM Employees WHERE Id = ? - - - - - SELECT * from Employees; - - - - - - - - - - - - - - - - - -
Emp IDFirst NameLast NameAge
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/sql/sql_query.jsp b/codes/javaee/jstl/src/main/webapp/examples/sql/sql_query.jsp deleted file mode 100644 index cd113c09..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/sql/sql_query.jsp +++ /dev/null @@ -1,40 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql" %> - -<%--运行本例之前,先执行sql/create_employees.sql--%> - - - - sql:query 示例 - - - - - - - SELECT * from test.Employees; - - - - - - - - - - - - - - - - - -
Emp IDFirst NameLast NameAge
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/sql/sql_setDataSource.jsp b/codes/javaee/jstl/src/main/webapp/examples/sql/sql_setDataSource.jsp deleted file mode 100644 index 3a52911a..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/sql/sql_setDataSource.jsp +++ /dev/null @@ -1,18 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql" %> - - - sql:setDataSource 示例 - - - - - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/sql/sql_transaction.jsp b/codes/javaee/jstl/src/main/webapp/examples/sql/sql_transaction.jsp deleted file mode 100644 index 2b18036b..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/sql/sql_transaction.jsp +++ /dev/null @@ -1,60 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ page import="java.util.Date" %> - -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql" %> - -<%--执行本例之前,需要先运行 sql/create_students.sql--%> - - - - sql:transaction 示例 - - - - - -<% - Date DoB = new Date("2001/12/16"); - int studentId = 100; -%> - - - - UPDATE Students SET last = 'Ali' WHERE Id = 102 - - - UPDATE Students SET last = 'Shah' WHERE Id = 103 - - - INSERT INTO Students - VALUES (104,'Nuha', 'Ali', '2010/05/26'); - - - - - SELECT * from Students; - - - - - - - - - - - - - - - - - -
Emp IDFirst NameLast NameDoB
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/sql/sql_update.jsp b/codes/javaee/jstl/src/main/webapp/examples/sql/sql_update.jsp deleted file mode 100644 index 4cdd9952..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/sql/sql_update.jsp +++ /dev/null @@ -1,44 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql" %> - -<%--运行本例之前,先执行sql/create_employees.sql--%> - - - - sql:update 示例 - - - - - - - INSERT INTO Employees VALUES (104, 2, 'Nuha', 'Ali'); - - - - SELECT * from Employees; - - - - - - - - - - - - - - - - - -
Emp IDFirst NameLast NameAge
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/bundle.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/bundle.jsp deleted file mode 100644 index fc8d78ce..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/bundle.jsp +++ /dev/null @@ -1,41 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> - - - - -Insert title here - - - - - - - - - -
- - - -
- - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/catch.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/catch.jsp deleted file mode 100644 index cf69926c..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/catch.jsp +++ /dev/null @@ -1,25 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> - - - - -Insert title here - - - - - - - - - - 程序抛出了异常 ${ e.class.name },原因: ${ e.message } - - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/choose.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/choose.jsp deleted file mode 100644 index bb09fa4a..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/choose.jsp +++ /dev/null @@ -1,21 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> - - - - -Insert title here - - - - - - when 标签的输出 - - - otherwise 标签的输出 - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/contains.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/contains.jsp deleted file mode 100644 index 1ba3cca8..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/contains.jsp +++ /dev/null @@ -1,35 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - - -header['User-Agent'] = "${ header['User-Agent'] }";

- -您使用 -IE 浏览器 -Firefox 浏览器 -Maxth 浏览器 -MyIE2 浏览器 -Opera 浏览器 -腾讯 Traveler 浏览器 -世界之窗 浏览器 -Kubuntu 浏览器 -, -Windows 操作系统 -Windows 操作系统 -Linux 操作系统 -Linux 操作系统 -Sun 操作系统 -Mac 操作系统 -。 - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/containsIgnoreCase.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/containsIgnoreCase.jsp deleted file mode 100644 index 0cdd7260..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/containsIgnoreCase.jsp +++ /dev/null @@ -1,33 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - - -header['User-Agent'] = "${ header['User-Agent'] }";

- -您使用 -IE 浏览器 -Firefox 浏览器 -Maxth 浏览器 -MyIE2 浏览器 -Opera 浏览器 -腾讯 Traveler 浏览器 -世界之窗 浏览器 -Kubuntu 浏览器 -, -Windows 操作系统 -Linux 操作系统 -Sun 操作系统 -Mac 操作系统 -。 - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/dateParam.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/dateParam.jsp deleted file mode 100644 index 12fd3595..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/dateParam.jsp +++ /dev/null @@ -1,79 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - - - - - - - - create table if not exists tb_person - ( id integer auto_increment, - name varchar(255), - birthday timestamp null, - primary key (id) - ) - - - - insert into tb_person ( name, birthday ) values ( ?, ? ) - - - - - - select * from tb_person where birthday > ( ? - 1 ) - - - - - - - - - - - - - - - - - - -
${ columnName }
${ row[columnName] }
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/endsWith.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/endsWith.jsp deleted file mode 100644 index 8ce959a1..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/endsWith.jsp +++ /dev/null @@ -1,67 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - -<%@page import="java.io.File"%> - - - -Insert title here - - - - -<% - request.setAttribute("files", new File("c:\\").listFiles()); -%> - - - - - - - - - - - - - - -
File NameType
${ file.name } - - - 文件夹 - - JPG 图片 - EXE 应用程序 - GIF 图片 - TXT 文本文件 - WORD 文件 - Excel 文件 - LOG 日志文件 - SQL 数据库脚本文件 - - - -
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/escapeXml.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/escapeXml.jsp deleted file mode 100644 index 3478adef..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/escapeXml.jsp +++ /dev/null @@ -1,21 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - - - - -
-${ fn:escapeXml(source) } -
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/fmt.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/fmt.jsp deleted file mode 100644 index 3a102ae0..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/fmt.jsp +++ /dev/null @@ -1,14 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> - - - - -Insert title here - - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/fn.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/fn.jsp deleted file mode 100644 index 69d7bafb..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/fn.jsp +++ /dev/null @@ -1,14 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/forEach.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/forEach.jsp deleted file mode 100644 index 065c535c..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/forEach.jsp +++ /dev/null @@ -1,73 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> - - - - -Insert title here - - - - - -
${ num }
-
- -
-
-
- - - - - - - - - - - - - -
Header NameHeader Value
${ headerName }${ header[headerName] }
- -
-
-
- - - - - - - - - - - - - -
Header NameHeader Value
${ item.key }${ item.value }
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/forEachWithList.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/forEachWithList.jsp deleted file mode 100644 index f132fc12..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/forEachWithList.jsp +++ /dev/null @@ -1,192 +0,0 @@ -<%@ page language="java" import="java.util.*, com.helloweenvsfei.jstl.bean.*" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<% - List personList = new ArrayList(); - - int i = 1; - - Person person = new Person(); - person.setId(i++); - person.setName("张三"); - person.setAge(20); - person.setSex("男"); - person.setAddress("北京市海淀区上地软件园"); - person.setBirthday("2008-08-08"); - person.setMobile("13820080808"); - person.setTelephone("69653234"); - person.setCity("北京"); - - personList.add(person); - - Person person2 = new Person(); - person2.setId(i++); - person2.setName("李四"); - person2.setAge(20); - person2.setSex("男"); - person2.setAddress("北京市东皇城根锡拉胡同"); - person2.setBirthday("2008-01-01"); - person2.setMobile("13820080808"); - person2.setTelephone("20054879"); - person2.setCity("北京"); - - personList.add(person2); - - Person person3 = new Person(); - person3.setId(i++); - person3.setName("王五"); - person3.setAge(20); - person3.setSex("男"); - person3.setAddress("北京市东皇城根锡拉胡同"); - person3.setBirthday("2008-01-01"); - person3.setMobile("13820080808"); - person3.setTelephone("20054879"); - person3.setCity("北京"); - - personList.add(person3); - - Person person4 = new Person(); - person4.setId(i++); - person4.setName("王二麻子"); - person4.setAge(20); - person4.setSex("男"); - person4.setAddress("北京市东皇城根锡拉胡同"); - person4.setBirthday("2008-01-01"); - person4.setMobile("13820080808"); - person4.setTelephone("20054879"); - person4.setCity("北京"); - - personList.add(person4); - - Person person5 = new Person(); - person5.setId(i++); - person5.setName("王二麻子"); - person5.setAge(20); - person5.setSex("男"); - person5.setAddress("北京市东皇城根锡拉胡同"); - person5.setBirthday("2008-01-01"); - person5.setMobile("13820080808"); - person5.setTelephone("20054879"); - person5.setCity("北京"); - - personList.add(person5); - - Person person6 = new Person(); - person6.setId(i++); - person6.setName("王二麻子"); - person6.setAge(20); - person6.setSex("男"); - person6.setAddress("北京市东皇城根锡拉胡同"); - person6.setBirthday("2008-01-01"); - person6.setMobile("13820080808"); - person6.setTelephone("20054879"); - person6.setCity("北京"); - - personList.add(person6); - - Person person7 = new Person(); - person7.setId(i++); - person7.setName("王二麻子"); - person7.setAge(20); - person7.setSex("男"); - person7.setAddress("北京市东皇城根锡拉胡同"); - person7.setBirthday("2008-01-01"); - person7.setMobile("13820080808"); - person7.setTelephone("20054879"); - person7.setCity("北京"); - - personList.add(person7); - - request.setAttribute("personList", personList); - -%> - - - - -Insert title here - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
编号姓名年龄性别城市地址生日手机电话
${ person.id }${ person.name }${ person.age }${ person.sex }${ person.city }${ person.address }${ person.birthday }${ person.mobile }${ person.telephone }
- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
编号姓名年龄性别城市地址生日手机电话
${ varStatus.current.id }${ varStatus.current.name }${ varStatus.current.age }${ varStatus.current.sex }${ varStatus.current.city }${ varStatus.current.address }${ varStatus.current.birthday }${ varStatus.current.mobile }${ varStatus.current.telephone }
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/forTokens.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/forTokens.jsp deleted file mode 100644 index 9de4ecd8..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/forTokens.jsp +++ /dev/null @@ -1,38 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> - - - - -Insert title here - - - - - - - - - - - - - - - -
varStatus.indexname
${ varStatus.index }${ item }
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/formatDate.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/formatDate.jsp deleted file mode 100644 index 993ce5c4..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/formatDate.jsp +++ /dev/null @@ -1,75 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> - -<%@page import="java.util.Locale"%> -<%@page import="java.lang.reflect.Field"%> -<%@page import="java.util.ArrayList"%> -<%@page import="java.util.List"%> -<%@page import="java.util.Date"%> - - - -Insert title here - - - - -<% - Field[] field = Locale.class.getFields(); - - List localeList = new ArrayList(); - - for(int i=0; i - - - - - - - - - - - - - - - - - - - - - - -
LocaleDate and TimeNumbercurrency
${ locale }
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/formatNumber.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/formatNumber.jsp deleted file mode 100644 index ab1cab2e..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/formatNumber.jsp +++ /dev/null @@ -1,87 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> - -<%@page import="java.util.Locale"%> -<%@page import="java.lang.reflect.Field"%> -<%@page import="java.util.ArrayList"%> -<%@page import="java.util.List"%> -<%@page import="java.util.Date"%> - - - -Insert title here - - - - -<% - Field[] field = Locale.class.getFields(); - - List localeList = new ArrayList(); - - for(int i=0; i - - - -当前格式: - -  - - - ${ locale }  - - - - - - - - - - - - - - - - - - - -
数字原值数字格式货币格式百分数格式
${ number }
- - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/if.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/if.jsp deleted file mode 100644 index c43ef5ef..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/if.jsp +++ /dev/null @@ -1,51 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> - - - - - - - - - -
- - - 添加操作 - - - - - - - - - -
帐号
真实姓名
-
- - 修改操作 - - - - - - - - - -
帐号
真实姓名
-
- -
- - -
- - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/import.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/import.jsp deleted file mode 100644 index 70936ec2..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/import.jsp +++ /dev/null @@ -1,25 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> - - - - -Insert title here - - - - - - - -Baidu 的源代码为: - - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/index.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/index.jsp deleted file mode 100644 index 3d9e4430..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/index.jsp +++ /dev/null @@ -1,16 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> - - - - -Insert title here - - - -index.jsp - -Test - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/indexOf.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/indexOf.jsp deleted file mode 100644 index d670c482..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/indexOf.jsp +++ /dev/null @@ -1,16 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - -fn:indexOf('filename.txt', '.') = ${ fn:indexOf('filename.txt', '.') } - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/join.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/join.jsp deleted file mode 100644 index a9444c3d..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/join.jsp +++ /dev/null @@ -1,20 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - -<% - request.setAttribute("array", new String[]{"John", "Tom", "Tommi", "Kurt", }); -%> - -${ fn:join(array, '; ') } - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/length.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/length.jsp deleted file mode 100644 index c0472d25..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/length.jsp +++ /dev/null @@ -1,18 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - -"${ pageContext.request.requestURL }" 的长度:${ fn:length(pageContext.request.requestURI) }
-Cookie[] 的长度:${ fn:length(pageContext.request.cookies) }
-Map header 的长度: ${ fn:length(header) }
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/out.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/out.jsp deleted file mode 100644 index 632d93e3..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/out.jsp +++ /dev/null @@ -1,14 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> - - - - - Insert title here - - - -action 参数为: - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/param.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/param.jsp deleted file mode 100644 index 13a18a56..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/param.jsp +++ /dev/null @@ -1,17 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=GBK"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> - - - - -Insert title here - - - - - - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/parse.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/parse.jsp deleted file mode 100644 index 04dd7b53..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/parse.jsp +++ /dev/null @@ -1,50 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%> - - - - -Insert title here - - - - - - -
- -
- - - -新浪 RSS
-版本:
-标题:
-来源:
-版权:
-出版时间:
-链接地址:
- - - - Helloween - 20 - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/parseDate.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/parseDate.jsp deleted file mode 100644 index 07282468..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/parseDate.jsp +++ /dev/null @@ -1,16 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> - - - - -Insert title here - - - - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/parseNumber.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/parseNumber.jsp deleted file mode 100644 index 20bd9c71..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/parseNumber.jsp +++ /dev/null @@ -1,16 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> - - - - -Insert title here - - - -
-
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/query.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/query.jsp deleted file mode 100644 index f6bd23e9..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/query.jsp +++ /dev/null @@ -1,57 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - - - - - - - - - - - - - - - - - - - - - - -
IDNameDescription
${ row['id'] }${ row['name'] }${ row['description'] }
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/queryPagination.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/queryPagination.jsp deleted file mode 100644 index 63ed4d80..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/queryPagination.jsp +++ /dev/null @@ -1,76 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - -<%@page import="com.helloweenvsfei.util.Pagination"%> - - - -Insert title here - - - - -<% - request.setAttribute("pagination", new Pagination(request, response)); -%> - - - - - SELECT count(*) count FROM help_topic - - - - - - - - SELECT * FROM help_topic - - - - - - - - - - - - - - - - - -
Help_IDNameDescription
${ row['help_topic_id'] }${ row['name'] }${ row['description'] }
-
-${ pagination } - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/queryReflect.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/queryReflect.jsp deleted file mode 100644 index c904c5c1..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/queryReflect.jsp +++ /dev/null @@ -1,74 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - - -
-
- -
- - - - - - - ${ param.sql } - - - - - - - - - - - - - - - - - -
${ columnName }
${ row[columnName] }
- -
- - -
Exception: ${ e.message }
-
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/redirect.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/redirect.jsp deleted file mode 100644 index 4b9f6030..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/redirect.jsp +++ /dev/null @@ -1,14 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> - - - - -Insert title here - - - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/remove.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/remove.jsp deleted file mode 100644 index 91c17b30..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/remove.jsp +++ /dev/null @@ -1,21 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> - -<%@page import="java.util.HashMap"%> - - - -Insert title here - - - -<% - request.setAttribute("somemap", new HashMap()); -%> - - - -${ somemap == null ? 'somemap 已经被删除' : 'somemap 没有被删除' } - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/requestEncoding.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/requestEncoding.jsp deleted file mode 100644 index 0c19c8ad..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/requestEncoding.jsp +++ /dev/null @@ -1,24 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> - - - - -Insert title here - - - - - - -
- 关键字: -

- -
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/set.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/set.jsp deleted file mode 100644 index be63603f..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/set.jsp +++ /dev/null @@ -1,34 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> - - - - -Insert title here - - - - - -本网站总访问人次:${ totalCount }
-其中您的访问次数:${ count }
- - -by body - -
-
-
-
-<% - request.setAttribute("person", new com.helloweenvsfei.jstl.bean.Person()); - request.setAttribute("map", new java.util.HashMap()); -%> - -${ person.name } - - -${ map.name } - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/setBundle.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/setBundle.jsp deleted file mode 100644 index 390ba5f3..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/setBundle.jsp +++ /dev/null @@ -1,23 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> - - - - -Insert title here - - - - - - - - - Helloween -
- - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/setDataSource.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/setDataSource.jsp deleted file mode 100644 index ed4aeb1e..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/setDataSource.jsp +++ /dev/null @@ -1,27 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - - - - - -数据源:${ dataSource.class.name } - -${ serverDataSource } - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/setLocale.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/setLocale.jsp deleted file mode 100644 index ff66b766..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/setLocale.jsp +++ /dev/null @@ -1,62 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> - -<%@page import="java.util.Locale"%> - - - -Insert title here - - - - -<% - request.setAttribute("localeList", Locale.getAvailableLocales()); -%> - - - - - - - - - - - - - - - - - - - - - - - - -
LocaleLanguageDate and TimeNumbercurrency
${ locale.displayName }${ locale.displayLanguage }
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/setTimeZone.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/setTimeZone.jsp deleted file mode 100644 index b0650ce2..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/setTimeZone.jsp +++ /dev/null @@ -1,40 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> - - - - -Insert title here - - - - - - - - -现在时刻:北京时间 -
- - -
- GMT${ i>=12 ? '+' : '' }${ i-12 } : - - - -
-
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/sina.xml b/codes/javaee/jstl/src/main/webapp/examples/uncheck/sina.xml deleted file mode 100644 index 2bc06c38..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/sina.xml +++ /dev/null @@ -1,300 +0,0 @@ - - - - - - - <![CDATA[新闻中心-新闻要闻]]> - - - - <![CDATA[新闻中心]]> - - http://news.sina.com.cn - http://www.sinaimg.cn/dy/sina_news626.gif - - - - - http://news.sina.com.cn/iframe/o/allnews/input/index.htm - zh-cn - WWW.SINA.COM.CN - 5 - - - - Fri, 30 Nov 2007 05:25:01 GMT - - - - - - <![CDATA[视频-联盟杯拜仁紫百合惊险战平 克洛斯笑纳空门]]> - - http://video.sina.com.cn/sports/g/bn/2007-11-30/13236492.shtml - WWW.SINA.COM.CN - http://video.sina.com.cn/sports/g/bn/2007-11-30/13236492.shtml - - - - Fri, 30 Nov 2007 05:23:53 GMT - - - - - - - - <![CDATA[18律师声援遭诱捕浏阳商人]]> - - http://finance.sina.com.cn/column/complain/20071130/13234236791.shtml - WWW.SINA.COM.CN - http://finance.sina.com.cn/column/complain/20071130/13234236791.shtml - - - - Fri, 30 Nov 2007 05:23:08 GMT - - - - - - - - <![CDATA[视频-马竞必杀定位球降服弱旅 幸运之神眷顾纽伦堡]]> - - http://video.sina.com.cn/sports/g/bn/2007-11-30/13206491.shtml - WWW.SINA.COM.CN - http://video.sina.com.cn/sports/g/bn/2007-11-30/13206491.shtml - - - - Fri, 30 Nov 2007 05:20:23 GMT - - - - - - - - <![CDATA[[泰国橡胶]USS3橡胶现货价格下跌,因基本面疲弱]]> - - http://finance.sina.com.cn/money/future/20071130/13204236790.shtml - WWW.SINA.COM.CN - http://finance.sina.com.cn/money/future/20071130/13204236790.shtml - - - - Fri, 30 Nov 2007 05:20:17 GMT - - - - - - - - <![CDATA[[马来棕榈油]2007年11月30日午马来西亚棕榈油现货行情]]> - - http://finance.sina.com.cn/money/future/20071130/13204236789.shtml - WWW.SINA.COM.CN - http://finance.sina.com.cn/money/future/20071130/13204236789.shtml - - - - Fri, 30 Nov 2007 05:20:13 GMT - - - - - - - - <![CDATA[马来西亚BMD毛棕榈油期货午盘下滑,市场等待出口预估数据]]> - - http://finance.sina.com.cn/money/future/20071130/13204236788.shtml - WWW.SINA.COM.CN - http://finance.sina.com.cn/money/future/20071130/13204236788.shtml - - - - Fri, 30 Nov 2007 05:20:04 GMT - - - - - - - - <![CDATA[[机构看盘]集成利期货:郑糖探低回升中幅收跌,短线压力依旧较大]]> - - http://finance.sina.com.cn/money/future/20071130/13194236787.shtml - WWW.SINA.COM.CN - http://finance.sina.com.cn/money/future/20071130/13194236787.shtml - - - - Fri, 30 Nov 2007 05:19:56 GMT - - - - - - - - <![CDATA[[现货行情]11月30日广西糖网食糖批发市场收市行情]]> - - http://finance.sina.com.cn/money/future/20071130/13194236786.shtml - WWW.SINA.COM.CN - http://finance.sina.com.cn/money/future/20071130/13194236786.shtml - - - - Fri, 30 Nov 2007 05:19:52 GMT - - - - - - - - <![CDATA[[现货行情]11月30日昆明商品中心批发市场收市行情]]> - - http://finance.sina.com.cn/money/future/20071130/13194236785.shtml - WWW.SINA.COM.CN - http://finance.sina.com.cn/money/future/20071130/13194236785.shtml - - - - Fri, 30 Nov 2007 05:19:48 GMT - - - - - - - - <![CDATA[长沙百亿企业添新]]> - - http://finance.sina.com.cn/china/dfjj/20071130/13194236784.shtml - WWW.SINA.COM.CN - http://finance.sina.com.cn/china/dfjj/20071130/13194236784.shtml - - - - Fri, 30 Nov 2007 05:19:41 GMT - - - - - - - - <![CDATA[新代码启用 垃圾短信难隐形]]> - - http://tech.sina.com.cn/t/2007-11-30/13191884840.shtml - WWW.SINA.COM.CN - http://tech.sina.com.cn/t/2007-11-30/13191884840.shtml - - - - Fri, 30 Nov 2007 05:19:36 GMT - - - - - - - - <![CDATA[至酷腕表型MP3 松下MP130V仅400元甩卖]]> - - http://tech.sina.com.cn/digi/mp3/2007-11-30/13191884839.shtml - WWW.SINA.COM.CN - http://tech.sina.com.cn/digi/mp3/2007-11-30/13191884839.shtml - - - - Fri, 30 Nov 2007 05:19:12 GMT - - - - - - - - <![CDATA[稳定成品油市场供应加强价格监管]]> - - http://finance.sina.com.cn/g/20071130/13184236783.shtml - WWW.SINA.COM.CN - http://finance.sina.com.cn/g/20071130/13184236783.shtml - - - - Fri, 30 Nov 2007 05:18:42 GMT - - - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/split.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/split.jsp deleted file mode 100644 index f728f7a8..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/split.jsp +++ /dev/null @@ -1,21 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - -${ header['accept'] } - - -header['accept']:

- - ${ name }
-
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/sql.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/sql.jsp deleted file mode 100644 index 9025ef1a..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/sql.jsp +++ /dev/null @@ -1,17 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/substring.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/substring.jsp deleted file mode 100644 index f83ae4e6..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/substring.jsp +++ /dev/null @@ -1,21 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - -liujhua@cn.ibm.com -${ fn:substring(email, 0, fn:indexOf(email, '@') ) } - -${ fn:substringBefore(email, '@') } - -${ fn:substringAfter(email, '@') } - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/timeZone.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/timeZone.jsp deleted file mode 100644 index 659de4fe..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/timeZone.jsp +++ /dev/null @@ -1,75 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> - - - -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> - - - - -Insert title here - - - - -<% - Map hashMap = new HashMap(); - - for(String ID : TimeZone.getAvailableIDs()){ - hashMap.put(ID, TimeZone.getTimeZone(ID)); - } - - request.setAttribute("timeZoneIds", TimeZone.getAvailableIDs()); - request.setAttribute("timeZone", hashMap); -%> - - - - - -现在时刻:<%= TimeZone.getDefault().getDisplayName() %> -
- - - - - - - - - - - - - - - - - -
时区ID时区现在时间时差
${ ID }${ timeZone[ID].displayName } - - - - ${ timeZone[ID].rawOffset / 60 / 60 / 1000 }
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/transaction.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/transaction.jsp deleted file mode 100644 index 7667b2d3..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/transaction.jsp +++ /dev/null @@ -1,87 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - - - - - - - - - - - insert into tb_corporation ( name, description ) values ('事务测试', '事务测试') - - - 已插入一条。
-
- - - - insert into tb_corporation ( id, name, description ) values (1, '事务测试', '事务测试') - - -
- -
- - -
操作异常,原因:${ e.message }。事务已经回滚。
-
- - - select * from tb_corporation - - - - - - - - - - - - - - - - - -
${ columnName }
${ row[columnName] }
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/update.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/update.jsp deleted file mode 100644 index 242afb79..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/update.jsp +++ /dev/null @@ -1,65 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%> - - - - -Insert title here - - - - - - - - drop table if exists tb_corporation - - -DROP TABLE, 影响到的数据条数:${ result }
- - -create table tb_corporation ( - id integer auto_increment, - name varchar(255), - description text, - primary key(id) -) - - -CREATE TABLE, 影响到的数据条数:${ result }
- - - insert into tb_corporation ( name, description ) values ('MicroSoft', '微软') - - -INSERT, 影响到的数据条数:${ result }
- - - insert into tb_corporation ( name, description ) values ('IBM', '国际商用机器') - - -INSERT, 影响到的数据条数:${ result }
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/url.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/url.jsp deleted file mode 100644 index 3ef0f5e8..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/url.jsp +++ /dev/null @@ -1,15 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> - - - - -Insert title here - - - - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_choose.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_choose.jsp deleted file mode 100644 index b412a755..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_choose.jsp +++ /dev/null @@ -1,43 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%> - - - - -Insert title here - - - - - - -
- - - - - - 使用了 JNDI 属性。 - - - 没有使用 JNDI 属性。 - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_forEach.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_forEach.jsp deleted file mode 100644 index e01aecb6..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_forEach.jsp +++ /dev/null @@ -1,43 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%> - - - - -Insert title here - - - - - - -
- -
- - - - - ${ status.count }. - " target="_blank">  -
-

- -
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_if.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_if.jsp deleted file mode 100644 index 0c383fff..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_if.jsp +++ /dev/null @@ -1,42 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%> - - - - -Insert title here - - - - - - -
- - - - - 属性 Driver Class Name 存在: .
-
- - - 属性 Driver Class Name 不存在。 - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_out.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_out.jsp deleted file mode 100644 index a902d1de..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_out.jsp +++ /dev/null @@ -1,25 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x" %> - - - - - Insert title here - - - - - - Helloween - 20 - - - -content: ${ content }
- -输出属性 description:
-输出元素 name:
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_set.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_set.jsp deleted file mode 100644 index b7ffdda5..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_set.jsp +++ /dev/null @@ -1,38 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%> - - - - -Insert title here - - - - - - -
- - - - - -Driver Class Name: - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_transform.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_transform.jsp deleted file mode 100644 index 3419077e..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/x_transform.jsp +++ /dev/null @@ -1,103 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%> - - - - -Insert title here - - - - - - - - - - 1 - 张三 - zhangsan@host.com - Software Engine - - - 2 - 李四 - lisi@somehost.com - Sales - - - 3 - 王五 - wangwu@someweb.com - Manager - - - - - - - - - - - - - - - - - - - - - - - - -
编号姓名电子邮件描述
-
-
- -
- -
- -
- - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/uncheck/xml.jsp b/codes/javaee/jstl/src/main/webapp/examples/uncheck/xml.jsp deleted file mode 100644 index ecc8590a..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/uncheck/xml.jsp +++ /dev/null @@ -1,26 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%> - - - - -Insert title here - - - - - - Helloween - 20 - - - -Description:
-Name:
-age:
- - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/xml/x_choose.jsp b/codes/javaee/jstl/src/main/webapp/examples/xml/x_choose.jsp deleted file mode 100644 index 0d2ac381..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/xml/x_choose.jsp +++ /dev/null @@ -1,42 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml" %> - - - - x:choose 标签 - - -

Books Info:

- - - - - Padam History - ZARA - 100 - - - Great Mistry - NUHA - 2000 - - - - - - - - Book is written by ZARA - - - Book is written by NUHA - - - Unknown author. - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/xml/x_forEach.jsp b/codes/javaee/jstl/src/main/webapp/examples/xml/x_forEach.jsp deleted file mode 100644 index ea0bba13..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/xml/x_forEach.jsp +++ /dev/null @@ -1,36 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml" %> - - - - x:forEach 标签 - - -

Books Info:

- - - - - Padam History - ZARA - 100 - - - Great Mistry - NUHA - 2000 - - - - - -
    - -
  • Book Name:
  • -
    -
- - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/xml/x_if.jsp b/codes/javaee/jstl/src/main/webapp/examples/xml/x_if.jsp deleted file mode 100644 index 204cfdce..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/xml/x_if.jsp +++ /dev/null @@ -1,40 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml" %> - - - - x:if 标签 - - -

Books Info:

- - - - - Padam History - ZARA - 100 - - - Great Mistry - NUHA - 2000 - - - - - - - -Document has at least one - element. - -
- -Book prices are very high - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/xml/x_out.jsp b/codes/javaee/jstl/src/main/webapp/examples/xml/x_out.jsp deleted file mode 100644 index 24d7e7ba..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/xml/x_out.jsp +++ /dev/null @@ -1,35 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml" %> - - - - x:out 标签 - - -

Books Info:

- - - - - Padam History - ZARA - 100 - - - Great Mistry - NUHA - 2000 - - - - - -The title of the first book is: - -
-The price of the second book: - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/xml/x_param.jsp b/codes/javaee/jstl/src/main/webapp/examples/xml/x_param.jsp deleted file mode 100644 index c5000bbc..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/xml/x_param.jsp +++ /dev/null @@ -1,33 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml" %> - - - - x:param 标签 - - -

Books Info:

- - - - Padam History - ZARA - 100 - - - Great Mistry - NUHA - 2000 - - - - - - - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/xml/x_parse.jsp b/codes/javaee/jstl/src/main/webapp/examples/xml/x_parse.jsp deleted file mode 100644 index 0df6f613..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/xml/x_parse.jsp +++ /dev/null @@ -1,22 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8" %> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml" %> - - - - x:parse 标签 - - -

Books Info:

- - - -The title of the first book is: - -
-The price of the second book: - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/xml/x_set.jsp b/codes/javaee/jstl/src/main/webapp/examples/xml/x_set.jsp deleted file mode 100644 index 65df38cf..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/xml/x_set.jsp +++ /dev/null @@ -1,31 +0,0 @@ -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml" %> - - - - x:set 标签 - - -

Books Info:

- - - - - Padam History - ZARA - 100 - - - Great Mistry - NUHA - 2000 - - - - - - -The price of the second book: - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/examples/xml/x_transform.jsp b/codes/javaee/jstl/src/main/webapp/examples/xml/x_transform.jsp deleted file mode 100644 index 158b1fcd..00000000 --- a/codes/javaee/jstl/src/main/webapp/examples/xml/x_transform.jsp +++ /dev/null @@ -1,31 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -<%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml" %> - - - - x:transform 标签 - - -

Books Info:

- - - - Padam History - ZARA - 100 - - - Great Mistry - NUHA - 2000 - - - - - - - - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/xml/books.xml b/codes/javaee/jstl/src/main/webapp/xml/books.xml deleted file mode 100644 index 4969ebae..00000000 --- a/codes/javaee/jstl/src/main/webapp/xml/books.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - Padam History - ZARA - 100 - - - Great Mistry - NUHA - 2000 - - \ No newline at end of file diff --git a/codes/javaee/jstl/src/main/webapp/xml/style.xsl b/codes/javaee/jstl/src/main/webapp/xml/style.xsl deleted file mode 100644 index f7ef2ad9..00000000 --- a/codes/javaee/jstl/src/main/webapp/xml/style.xsl +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - -
-
\ No newline at end of file diff --git a/codes/javaee/jstl/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java b/codes/javaee/jstl/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java deleted file mode 100644 index f20d87b3..00000000 --- a/codes/javaee/jstl/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.github.dunwu.javaee.server; - -import java.util.ArrayList; - -import org.apache.commons.lang3.StringUtils; -import org.assertj.core.util.Lists; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.WebAppClassLoader; -import org.eclipse.jetty.webapp.WebAppContext; - -/** - * JettyFactory 可以工作在 Eclipse 和 Intellij 中,用来启动 jetty 服务。 Intellij - * 并不支持jetty,所以要想类似eclipse一样的使用jetty,需要配置webdefault.xml。 - * - * @author Zhang Peng - */ -@SuppressWarnings("unused") -public class JettyFactory { - private static final int PORT = 9798; - private static final String CONTEXT = "/"; - private static final String RESOURCE_BASE_PATH = "src/main/webapp"; - private static final String WEB_XML_PATH = "/WEB-INF/web.xml"; - private static final String[] TLD_JAR_NAMES = - new String[]{"sitemesh", "spring-webmvc", "shiro-web", "tiles"}; - private static final String WINDOWS_WEBDEFAULT_PATH = "jetty/webdefault.xml"; - - public static final int IDE_ECLIPSE = 0; - public static final int IDE_INTELLIJ = 1; - - - public static Server initServer() { - Profiles.setProfileAsSystemProperty(Profiles.DEVELOPMENT); - WebAppContext webAppContext = new WebAppContext(); - Server server = new Server(PORT); - server.setHandler(webAppContext); - return server; - } - - public static void initWebAppContext(Server server, int type) throws Exception { - System.out.println("[INFO] Application loading"); - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - webAppContext.setContextPath(CONTEXT); - webAppContext.setResourceBase(getAbsolutePath() + RESOURCE_BASE_PATH); - webAppContext.setDescriptor(getAbsolutePath() + RESOURCE_BASE_PATH + WEB_XML_PATH); - - if (IDE_INTELLIJ == type) { - webAppContext.setDefaultsDescriptor(WINDOWS_WEBDEFAULT_PATH); - supportJspAndSetTldJarNames(server, TLD_JAR_NAMES); - } else { - webAppContext.setParentLoaderPriority(true); - } - - System.out.println("[INFO] Application loaded"); - } - - public static void reloadWebAppContext(Server server) throws Exception { - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - System.out.println("[INFO] Application reloading"); - webAppContext.stop(); - WebAppClassLoader classLoader = new WebAppClassLoader(webAppContext); - classLoader.addClassPath(getAbsolutePath() + "target/classes"); - classLoader.addClassPath(getAbsolutePath() + "target/test-classes"); - webAppContext.setClassLoader(classLoader); - webAppContext.start(); - System.out.println("[INFO] Application reloaded"); - } - - - public static void startServer(Server server) throws Exception { - System.out.println("[HINT] Don't forget to set -XX:MaxPermSize=128m"); - server.start(); - System.out.println("Server running at http://localhost:" + PORT + CONTEXT); - System.out.println("[HINT] Hit Enter to reload the application quickly"); - } - - public static String getAbsolutePath() { - String path = null; - String folderPath = JettyFactory.class.getProtectionDomain().getCodeSource().getLocation() - .getPath().substring(1); - if (folderPath.indexOf("target") > 0) { - path = folderPath.substring(0, folderPath.indexOf("target")); - } - return path; - } - - public static void supportJspAndSetTldJarNames(Server server, String... jarNames) { - WebAppContext context = (WebAppContext) server.getHandler(); - // This webapp will use jsps and jstl. We need to enable the AnnotationConfiguration in - // order to correctly set up the jsp container - org.eclipse.jetty.webapp.Configuration.ClassList classlist = - org.eclipse.jetty.webapp.Configuration.ClassList.setServerDefault(server); - classlist.addBefore("org.eclipse.jetty.webapp.JettyWebXmlConfiguration", - "org.eclipse.jetty.annotations.AnnotationConfiguration"); - // Set the ContainerIncludeJarPattern so that jetty examines these container-path jars for - // tlds, web-fragments etc. - // If you omit the jar that contains the jstl .tlds, the jsp engine will scan for them - // instead. - ArrayList jarNameExprssions = Lists.newArrayList(".*/[^/]*servlet-api-[^/]*\\.jar$", - ".*/javax.servlet.jsp.jstl-.*\\.jar$", ".*/[^/]*taglibs.*\\.jar$"); - - for (String jarName : jarNames) { - jarNameExprssions.add(".*/" + jarName + "-[^/]*\\.jar$"); - } - - context.setAttribute("org.eclipse.jetty.io.github.dunwu.javaee.server.webapp.ContainerIncludeJarPattern", - StringUtils.join(jarNameExprssions, '|')); - } -} diff --git a/codes/javaee/jstl/src/test/java/io/github/dunwu/javaee/server/Profiles.java b/codes/javaee/jstl/src/test/java/io/github/dunwu/javaee/server/Profiles.java deleted file mode 100644 index 3a8ee7c4..00000000 --- a/codes/javaee/jstl/src/test/java/io/github/dunwu/javaee/server/Profiles.java +++ /dev/null @@ -1,29 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2005, 2014 springside.github.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - *******************************************************************************/ -package io.github.dunwu.javaee.server; - -/** - * Spring profile 常用方法与profile名称。 - * - * @author calvin - */ -public class Profiles { - - public static final String ACTIVE_PROFILE = "spring.profiles.active"; - public static final String DEFAULT_PROFILE = "spring.profiles.default"; - - public static final String PRODUCTION = "production"; - public static final String DEVELOPMENT = "development"; - public static final String UNIT_TEST = "test"; - public static final String FUNCTIONAL_TEST = "functional"; - - /** - * 在Spring启动前,设置profile的环境变量。 - */ - public static void setProfileAsSystemProperty(String profile) { - System.setProperty(ACTIVE_PROFILE, profile); - } -} diff --git a/codes/javaee/jstl/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java b/codes/javaee/jstl/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java deleted file mode 100644 index 2936b162..00000000 --- a/codes/javaee/jstl/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.github.dunwu.javaee.server; - -import org.eclipse.jetty.server.Server; - -/** - * 快速启动 jetty 服务器,方便测试 - * - * @author Zhang Peng - */ -public class QuickStartServer { - // private static int STARTUP_TYPE = JettyFactory.IDE_ECLIPSE; - private static int STARTUP_TYPE = JettyFactory.IDE_INTELLIJ; - - public static void main(String[] args) throws Exception { - Server server = JettyFactory.initServer(); - JettyFactory.initWebAppContext(server, STARTUP_TYPE); - - try { - JettyFactory.startServer(server); - - // 等待用户输入回车重载应用 - while (true) { - char c = (char) System.in.read(); - if (c == '\n') { - JettyFactory.reloadWebAppContext(server); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.exit(-1); - } - } -} diff --git a/codes/javaee/jstl/src/test/resources/jetty/webdefault.xml b/codes/javaee/jstl/src/test/resources/jetty/webdefault.xml deleted file mode 100644 index 65ec9cae..00000000 --- a/codes/javaee/jstl/src/test/resources/jetty/webdefault.xml +++ /dev/null @@ -1,534 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Default web.xml file. - This file is applied to a Web application before it's own WEB_INF/web.xml file - - - - - - - - org.eclipse.jetty.servlet.listener.ELContextCleaner - - - - - - - - org.eclipse.jetty.servlet.listener.IntrospectorCleaner - - - - - - - - - - - - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - aliases - false - - - acceptRanges - true - - - dirAllowed - true - - - welcomeServlets - false - - - redirectWelcome - false - - - maxCacheSize - 256000000 - - - maxCachedFileSize - 200000000 - - - maxCachedFiles - 2048 - - - gzip - false - - - etags - false - - - useFileMappedBuffer - false - - - - 0 - - - - default - / - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jsp - org.eclipse.jetty.jsp.JettyJspServlet - - logVerbosityLevel - DEBUG - - - fork - false - - - xpoweredBy - false - - - compilerTargetVM - 1.7 - - - compilerSourceVM - 1.7 - - - 0 - - - - jsp - *.jsp - *.jspf - *.jspx - *.xsp - *.JSP - *.JSPF - *.JSPX - *.XSP - - - - - - - - 30 - - - - - - - - - - - - - - - index.html - index.htm - index.jsp - - - - - - - - ar - ISO-8859-6 - - - be - ISO-8859-5 - - - bg - ISO-8859-5 - - - ca - ISO-8859-1 - - - cs - ISO-8859-2 - - - da - ISO-8859-1 - - - de - ISO-8859-1 - - - el - ISO-8859-7 - - - en - ISO-8859-1 - - - es - ISO-8859-1 - - - et - ISO-8859-1 - - - fi - ISO-8859-1 - - - fr - ISO-8859-1 - - - hr - ISO-8859-2 - - - hu - ISO-8859-2 - - - is - ISO-8859-1 - - - it - ISO-8859-1 - - - iw - ISO-8859-8 - - - ja - Shift_JIS - - - ko - EUC-KR - - - lt - ISO-8859-2 - - - lv - ISO-8859-2 - - - mk - ISO-8859-5 - - - nl - ISO-8859-1 - - - no - ISO-8859-1 - - - pl - ISO-8859-2 - - - pt - ISO-8859-1 - - - ro - ISO-8859-2 - - - ru - ISO-8859-5 - - - sh - ISO-8859-5 - - - sk - ISO-8859-2 - - - sl - ISO-8859-2 - - - sq - ISO-8859-2 - - - sr - ISO-8859-5 - - - sv - ISO-8859-1 - - - tr - ISO-8859-9 - - - uk - ISO-8859-5 - - - zh - GB2312 - - - zh_TW - Big5 - - - - - - - - - Disable TRACE - / - TRACE - - - - - - Enable everything but TRACE - / - TRACE - - - - - diff --git a/codes/javaee/listener/pom.xml b/codes/javaee/listener/pom.xml deleted file mode 100644 index bf2c5117..00000000 --- a/codes/javaee/listener/pom.xml +++ /dev/null @@ -1,183 +0,0 @@ - - 4.0.0 - - - io.github.dunwu - javaee-notes-listener - 1.0.0 - war - - - - - javaee-notes-listener - javaee 学习笔记之 listener - - - - - - UTF-8 - 1.7 - ${java.version} - ${java.version} - - - 9.3.2.v20150730 - - - - - ch.qos.logback - logback-classic - 1.1.3 - - - ch.qos.logback - logback-core - 1.1.3 - - - org.logback-extensions - logback-ext-spring - 0.1.2 - - - org.slf4j - jcl-over-slf4j - 1.7.12 - - - - - - commons-fileupload - commons-fileupload - 1.3.1 - - - commons-io - commons-io - 2.5 - - - org.apache.commons - commons-lang3 - 3.4 - - - - - - javax.servlet - javax.servlet-api - 3.1.0 - - - javax.servlet.jsp - jsp-api - 2.2 - - - javax.servlet - jstl - 1.2 - - - - - javax.servlet.jsp.jstl - jstl-api - 1.2 - - - javax.servlet - servlet-api - - - javax.servlet.jsp - jsp-api - - - - - - org.glassfish.web - jstl-impl - 1.2 - - - javax.servlet - servlet-api - - - javax.servlet.jsp - jsp-api - - - javax.servlet.jsp.jstl - jstl-api - - - - - - - - org.eclipse.jetty - jetty-webapp - ${jetty.version} - - - org.eclipse.jetty - jetty-annotations - ${jetty.version} - - - org.eclipse.jetty - apache-jsp - ${jetty.version} - - - org.eclipse.jetty - apache-jstl - ${jetty.version} - - - - - - junit - junit - 4.12 - - - org.assertj - assertj-core - 3.4.1 - - - - - - - xalan - xalan - 2.7.2 - - - xerces - xercesImpl - 2.11.0 - - - - - - - - - - - diff --git a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/LoginSessionListener.java b/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/LoginSessionListener.java deleted file mode 100644 index 9d0ee026..00000000 --- a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/LoginSessionListener.java +++ /dev/null @@ -1,91 +0,0 @@ -package io.github.dunwu.javaee.listener; - -import javax.servlet.http.HttpSession; -import javax.servlet.http.HttpSessionAttributeListener; -import javax.servlet.http.HttpSessionBindingEvent; - -import io.github.dunwu.javaee.listener.bean.PersonInfo; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.Map; - -/** - * 单点登录处理类 - * - * @author Zhang Peng - * @date 2017/4/4. - */ -public class LoginSessionListener implements HttpSessionAttributeListener { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - Map map = new HashMap(); - - @Override - public void attributeAdded(HttpSessionBindingEvent event) { - String name = event.getName(); - - // 登录 - if (name.equals("personInfo")) { - - PersonInfo personInfo = (PersonInfo) event.getValue(); - - if (map.get(personInfo.getAccount()) != null) { - - // map 中有记录,表明该帐号在其他机器上登录过,将以前的登录失效 - HttpSession session = map.get(personInfo.getAccount()); - PersonInfo oldPersonInfo = (PersonInfo) session.getAttribute("personInfo"); - - logger.debug("帐号" + oldPersonInfo.getAccount() + "在" + oldPersonInfo.getIp() - + "已经登录,该登录将被迫下线。"); - - session.removeAttribute("personInfo"); - session.setAttribute("msg", "您的帐号已经在其他机器上登录,您被迫下线。"); - } - - // 将session以用户名为索引,放入map中 - map.put(personInfo.getAccount(), event.getSession()); - logger.debug("帐号" + personInfo.getAccount() + "在" + personInfo.getIp() + "登录。"); - } - } - - @Override - public void attributeRemoved(HttpSessionBindingEvent event) { - String name = event.getName(); - - // 注销 - if (name.equals("personInfo")) { - // 将该session从map中移除 - PersonInfo personInfo = (PersonInfo) event.getValue(); - map.remove(personInfo.getAccount()); - logger.debug("帐号" + personInfo.getAccount() + "注销。"); - } - } - - @Override - public void attributeReplaced(HttpSessionBindingEvent event) { - String name = event.getName(); - - // 没有注销的情况下,用另一个帐号登录 - if (name.equals("personInfo")) { - - // 移除旧的的登录信息 - PersonInfo oldPersonInfo = (PersonInfo) event.getValue(); - map.remove(oldPersonInfo.getAccount()); - - // 新的登录信息 - PersonInfo personInfo = (PersonInfo) event.getSession().getAttribute("personInfo"); - - // 也要检查新登录的帐号是否在别的机器上登录过 - if (map.get(personInfo.getAccount()) != null) { - // map 中有记录,表明该帐号在其他机器上登录过,将以前的登录失效 - HttpSession session = map.get(personInfo.getAccount()); - session.removeAttribute("personInfo"); - session.setAttribute("msg", "您的帐号已经在其他机器上登录,您被迫下线。"); - } - map.put("personInfo", event.getSession()); - } - } - -} diff --git a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionActivationListener.java b/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionActivationListener.java deleted file mode 100644 index bbd38402..00000000 --- a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionActivationListener.java +++ /dev/null @@ -1,47 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.listener; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.servlet.http.HttpSessionActivationListener; -import javax.servlet.http.HttpSessionEvent; -import java.io.Serializable; - -/** - * @author Zhang Peng - * @date 2017/4/4. - */ -public class MyHttpSessionActivationListener implements HttpSessionActivationListener, Serializable { - - - private static final long serialVersionUID = 1L; - private final Logger logger = LoggerFactory.getLogger(getClass()); - private String name; - - @Override - public void sessionWillPassivate(HttpSessionEvent se) { - - logger.debug(name + "和session一起被序列化(钝化)到硬盘了,session的id是:" + se.getSession().getId()); - } - - @Override - public void sessionDidActivate(HttpSessionEvent se) { - logger.debug(name + "和session一起从硬盘反序列化(活化)回到内存了,session的id是:" + se.getSession().getId()); - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public MyHttpSessionActivationListener(String name) { - this.name = name; - } -} diff --git a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionAttributeListener.java b/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionAttributeListener.java deleted file mode 100644 index 250095f6..00000000 --- a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionAttributeListener.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.github.dunwu.javaee.listener; - -import javax.servlet.http.HttpSession; -import javax.servlet.http.HttpSessionAttributeListener; -import javax.servlet.http.HttpSessionBindingEvent; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MyHttpSessionAttributeListener implements HttpSessionAttributeListener { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - /** - * 添加属性 - */ - @Override - public void attributeAdded(HttpSessionBindingEvent se) { - String name = se.getName(); - logger.debug("新建session属性:key={}, value={}", name, se.getValue()); - } - - /** - * 删除属性 - */ - @Override - public void attributeRemoved(HttpSessionBindingEvent se) { - String name = se.getName(); - logger.debug("新建session属性:key={}, value={}", name, se.getValue()); - } - - /** - * 修改属性 - */ - @Override - public void attributeReplaced(HttpSessionBindingEvent se) { - HttpSession session = se.getSession(); - String name = se.getName(); - logger.debug("新建session属性:key={}, 原value={}, 现value={}", name, se.getValue(), - session.getAttribute(name)); - } -} diff --git a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionBindingListener.java b/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionBindingListener.java deleted file mode 100644 index e61c85d7..00000000 --- a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionBindingListener.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.github.dunwu.javaee.listener; - -import javax.servlet.http.HttpSessionBindingEvent; -import javax.servlet.http.HttpSessionBindingListener; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class MyHttpSessionBindingListener implements HttpSessionBindingListener { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Override - public void valueBound(HttpSessionBindingEvent event) { - logger.debug("HttpSessionBinding valueBound"); - } - - @Override - public void valueUnbound(HttpSessionBindingEvent event) { - logger.debug("HttpSessionBinding valueUnbound"); - } - -} diff --git a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionListener.java b/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionListener.java deleted file mode 100644 index 37817bec..00000000 --- a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionListener.java +++ /dev/null @@ -1,49 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.listener; - -import java.util.Date; - -import javax.servlet.http.HttpSession; -import javax.servlet.http.HttpSessionEvent; -import javax.servlet.http.HttpSessionListener; - -import io.github.dunwu.javaee.listener.util.ApplicationConstants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * @author Zhang Peng - * @date 2017/4/4. - */ -public class MyHttpSessionListener implements HttpSessionListener { - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Override - public void sessionCreated(HttpSessionEvent se) { - HttpSession session = se.getSession(); - - // 将 session 放入 map - ApplicationConstants.SESSION_MAP.put(session.getId(), session); - // 总访问人数++ - ApplicationConstants.TOTAL_HISTORY_COUNT++; - - // 如果当前在线人数超过历史记录,则更新最大在线人数,并记录时间 - if (ApplicationConstants.SESSION_MAP.size() > ApplicationConstants.MAX_ONLINE_COUNT) { - ApplicationConstants.MAX_ONLINE_COUNT = ApplicationConstants.SESSION_MAP.size(); - ApplicationConstants.MAX_ONLINE_COUNT_DATE = new Date(); - } - - logger.debug("创建了一个session: {}", session); - } - - @Override - public void sessionDestroyed(HttpSessionEvent se) { - HttpSession session = se.getSession(); - // 将session从map中移除 - ApplicationConstants.SESSION_MAP.remove(session.getId()); - - logger.debug("销毁了一个session: {}", session); - } -} diff --git a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyServletContextAttributeListener.java b/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyServletContextAttributeListener.java deleted file mode 100644 index 8a35d16f..00000000 --- a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyServletContextAttributeListener.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.listener; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.text.MessageFormat; - -import javax.servlet.ServletContextAttributeEvent; -import javax.servlet.ServletContextAttributeListener; - -/** - * @author Zhang Peng - * @date 2017/4/4. - */ -public class MyServletContextAttributeListener implements ServletContextAttributeListener { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Override - public void attributeAdded(ServletContextAttributeEvent scab) { - logger.debug("ServletContext域对象中添加了属性:{},属性值是:{}", scab.getName(), scab.getValue()); - } - - @Override - public void attributeRemoved(ServletContextAttributeEvent scab) { - logger.debug("ServletContext域对象中删除了属性:{},属性值是:{}", scab.getName(), scab.getValue()); - } - - @Override - public void attributeReplaced(ServletContextAttributeEvent scab) { - logger.debug("ServletContext域对象中替换了属性:{},原值是:{}, 现值是:{}", - scab.getName(), scab.getSource(), scab.getValue()); - } -} diff --git a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyServletContextListener.java b/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyServletContextListener.java deleted file mode 100644 index 2ed2324b..00000000 --- a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyServletContextListener.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.listener; - -import java.util.Date; - -import javax.servlet.ServletContext; -import javax.servlet.ServletContextEvent; -import javax.servlet.ServletContextListener; - -import io.github.dunwu.javaee.listener.util.ApplicationConstants; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * ServletContextListener接口用于监听ServletContext对象的创建和销毁事件。 - * - * @author Zhang Peng - * @date 2017/4/4. - */ -public class MyServletContextListener implements ServletContextListener { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Override - public void contextInitialized(ServletContextEvent event) { - // 启动时,记录服务器启动时间 - ApplicationConstants.START_DATE = new Date(); - ServletContext servletContext = event.getServletContext(); - logger.info("即将启动 {}", servletContext.getContextPath()); - } - - @Override - public void contextDestroyed(ServletContextEvent event) { - // 关闭时,将结果清除。也可以将结果保存到硬盘上。 - ApplicationConstants.START_DATE = null; - ApplicationConstants.MAX_ONLINE_COUNT_DATE = null; - ServletContext servletContext = event.getServletContext(); - logger.info("即将关闭 {}", servletContext.getContextPath()); - } -} diff --git a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyServletRequestAttributeListener.java b/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyServletRequestAttributeListener.java deleted file mode 100644 index 28816c9a..00000000 --- a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyServletRequestAttributeListener.java +++ /dev/null @@ -1,37 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.listener; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.text.MessageFormat; - -import javax.servlet.ServletRequestAttributeEvent; -import javax.servlet.ServletRequestAttributeListener; - -/** - * @author Zhang Peng - * @date 2017/4/4. - */ -public class MyServletRequestAttributeListener implements ServletRequestAttributeListener { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Override - public void attributeAdded(ServletRequestAttributeEvent srae) { - logger.debug("ServletRequest域对象中添加了属性:{},属性值是:{}", srae.getName(), srae.getValue()); - } - - @Override - public void attributeRemoved(ServletRequestAttributeEvent srae) { - logger.debug("ServletRequest域对象中删除了属性:{},属性值是:{}", srae.getName(), srae.getValue()); - } - - @Override - public void attributeReplaced(ServletRequestAttributeEvent srae) { - logger.debug("ServletRequest域对象中替换了属性:{},原值是:{}, 现值是:{}", - srae.getName(), srae.getSource(), srae.getValue()); - } -} diff --git a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyServletRequestListener.java b/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyServletRequestListener.java deleted file mode 100644 index 8bdd9655..00000000 --- a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/MyServletRequestListener.java +++ /dev/null @@ -1,61 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.listener; - -import javax.servlet.ServletRequestEvent; -import javax.servlet.ServletRequestListener; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * @author Zhang Peng - * @date 2017/4/4. - */ -public class MyServletRequestListener implements ServletRequestListener { - - private final Logger logger = LoggerFactory.getLogger(getClass()); - - @Override - public void requestInitialized(ServletRequestEvent event) { - - HttpServletRequest request = (HttpServletRequest) event.getServletRequest(); - - HttpSession session = request.getSession(true); - - // 记录IP地址 - session.setAttribute("ip", request.getRemoteAddr()); - - // 记录访问次数,只记录访问 .html, .do, .jsp, .action 的累计次数 - String uri = request.getRequestURI(); - uri = request.getQueryString() == null ? uri : (uri + "?" + request.getQueryString()); - request.setAttribute("dateCreated", System.currentTimeMillis()); - String[] suffix = {".html", ".do", ".jsp", ".action"}; - for (int i = 0; i < suffix.length; i++) { - if (uri.endsWith(suffix[i])) { - break; - } - if (i == suffix.length - 1) return; - } - - Integer activeTimes = (Integer) session.getAttribute("activeTimes"); - - if (activeTimes == null) { - activeTimes = 0; - } - - session.setAttribute("activeTimes", activeTimes + 1); - logger.debug("IP: {} 请求 {}", request.getRemoteAddr(), uri); - } - - @Override - public void requestDestroyed(ServletRequestEvent event) { - HttpServletRequest request = (HttpServletRequest) event.getServletRequest(); - long time = System.currentTimeMillis() - (Long) request.getAttribute("dateCreated"); - logger.debug("{} 请求处理结束, 用时 {} 毫秒", request.getRemoteAddr(), time); - } -} - diff --git a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/PersonInfoListener.java b/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/PersonInfoListener.java deleted file mode 100644 index 458f8a02..00000000 --- a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/PersonInfoListener.java +++ /dev/null @@ -1,78 +0,0 @@ -package io.github.dunwu.javaee.listener; - -import java.io.Serializable; -import java.util.Date; - -import javax.servlet.http.HttpSession; -import javax.servlet.http.HttpSessionActivationListener; -import javax.servlet.http.HttpSessionBindingEvent; -import javax.servlet.http.HttpSessionBindingListener; -import javax.servlet.http.HttpSessionEvent; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class PersonInfoListener implements HttpSessionActivationListener, - HttpSessionBindingListener, Serializable { - - private static final long serialVersionUID = -4780592776386225973L; - - Logger log = LoggerFactory.getLogger(getClass()); - - private String name; - - private Date dateCreated; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Date getDateCreated() { - return dateCreated; - } - - public void setDateCreated(Date dateCreated) { - this.dateCreated = dateCreated; - } - - // 从硬盘加载后 - public void sessionDidActivate(HttpSessionEvent se) { - HttpSession session = se.getSession(); - log.info(this + "已经成功从硬盘中加载。sessionId: " + session.getId()); - } - - // 即将被钝化到硬盘时 - public void sessionWillPassivate(HttpSessionEvent se) { - HttpSession session = se.getSession(); - log.info(this + "即将保存到硬盘。sessionId: " + session.getId()); - } - - // 被放进session前 - public void valueBound(HttpSessionBindingEvent event) { - HttpSession session = event.getSession(); - String name = event.getName(); - log.info(this + "被绑定到session \"" + session.getId() + "\"的" + name - + "属性上"); - - // 记录放到session中的时间 - this.setDateCreated(new Date()); - } - - // 从session中移除后 - public void valueUnbound(HttpSessionBindingEvent event) { - HttpSession session = event.getSession(); - String name = event.getName(); - log.info(this + "被从session \"" + session.getId() + "\"的" + name - + "属性上移除"); - } - - @Override - public String toString() { - return "PersonInfoListener(" + name + ")"; - } - -} diff --git a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/SessionAttributeListenerTest.java b/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/SessionAttributeListenerTest.java deleted file mode 100644 index 1d2a4cb2..00000000 --- a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/SessionAttributeListenerTest.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.github.dunwu.javaee.listener; - -import javax.servlet.http.HttpSession; -import javax.servlet.http.HttpSessionAttributeListener; -import javax.servlet.http.HttpSessionBindingEvent; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class SessionAttributeListenerTest implements HttpSessionAttributeListener { - - Logger logger = LoggerFactory.getLogger(getClass()); - - // 添加属性 - public void attributeAdded(HttpSessionBindingEvent se) { - HttpSession session = se.getSession(); - String name = se.getName(); - logger.info("新建session属性:" + name + ", 值为:" + se.getValue()); - } - - // 删除属性 - public void attributeRemoved(HttpSessionBindingEvent se) { - HttpSession session = se.getSession(); - String name = se.getName(); - logger.info("删除session属性:" + name + ", 值为:" + se.getValue()); - } - - // 修改属性 - public void attributeReplaced(HttpSessionBindingEvent se) { - HttpSession session = se.getSession(); - String name = se.getName(); - Object oldValue = se.getValue(); - logger.info("修改session属性:" + name + ", 原值:" + oldValue + ", 新值:" + session.getAttribute(name)); - } -} diff --git a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/bean/PersonInfo.java b/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/bean/PersonInfo.java deleted file mode 100644 index 73ffb230..00000000 --- a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/bean/PersonInfo.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.github.dunwu.javaee.listener.bean; - -import java.io.Serializable; -import java.util.Date; - -public class PersonInfo implements Serializable { - - private static final long serialVersionUID = 4063725584941336123L; - - // 帐号 - private String account; - - // 登录IP地址 - private String ip; - - // 登录时间 - private Date loginDate; - - public String getAccount() { - return account; - } - - public void setAccount(String account) { - this.account = account; - } - - public String getIp() { - return ip; - } - - public void setIp(String ip) { - this.ip = ip; - } - - public Date getLoginDate() { - return loginDate; - } - - public void setLoginDate(Date loginDate) { - this.loginDate = loginDate; - } - - @Override - public boolean equals(Object obj) { - if (obj == null || !(obj instanceof PersonInfo)) { - return false; - } - return account.equalsIgnoreCase(((PersonInfo) obj).getAccount()); - } -} diff --git a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/util/ApplicationConstants.java b/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/util/ApplicationConstants.java deleted file mode 100644 index bbdbceaa..00000000 --- a/codes/javaee/listener/src/main/java/io/github/dunwu/javaee/listener/util/ApplicationConstants.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.dunwu.javaee.listener.util; - -import java.util.Date; -import java.util.HashMap; -import java.util.Map; - -import javax.servlet.http.HttpSession; - -public class ApplicationConstants { - - // 所有的 Session - public static Map SESSION_MAP = new HashMap(); - - // 当前登录的用户总数 - public static int CURRENT_LOGIN_COUNT = 0; - - // 历史访客总数 - public static int TOTAL_HISTORY_COUNT = 0; - - // 服务器启动时间 - public static Date START_DATE = new Date(); - - // 最高在线时间 - public static Date MAX_ONLINE_COUNT_DATE = new Date(); - - // 最高在线人数 - public static int MAX_ONLINE_COUNT = 0; -} diff --git a/codes/javaee/listener/src/main/resources/logback.xml b/codes/javaee/listener/src/main/resources/logback.xml deleted file mode 100644 index c137b1c8..00000000 --- a/codes/javaee/listener/src/main/resources/logback.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - logs/${FILE_NAME}-all.%d{yyyy-MM-dd}.log - 30 - - - - - 30MB - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - - - - - - - - - - - diff --git a/codes/javaee/listener/src/main/webapp/META-INF/MANIFEST.MF b/codes/javaee/listener/src/main/webapp/META-INF/MANIFEST.MF deleted file mode 100644 index 254272e1..00000000 --- a/codes/javaee/listener/src/main/webapp/META-INF/MANIFEST.MF +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0 -Class-Path: - diff --git a/codes/javaee/listener/src/main/webapp/META-INF/context.xml b/codes/javaee/listener/src/main/webapp/META-INF/context.xml deleted file mode 100644 index c57698e0..00000000 --- a/codes/javaee/listener/src/main/webapp/META-INF/context.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/codes/javaee/listener/src/main/webapp/WEB-INF/resources/jsp/index.jsp b/codes/javaee/listener/src/main/webapp/WEB-INF/resources/jsp/index.jsp deleted file mode 100644 index 74319429..00000000 --- a/codes/javaee/listener/src/main/webapp/WEB-INF/resources/jsp/index.jsp +++ /dev/null @@ -1,16 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> - - - - -My JSP 'index.jsp' starting page - - - - - - - -

This is my JSP page.

- - diff --git a/codes/javaee/listener/src/main/webapp/WEB-INF/web.xml b/codes/javaee/listener/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 98a6b99b..00000000 --- a/codes/javaee/listener/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,69 +0,0 @@ - - - - - contextPath - value. sdf - - - - - - - io.github.dunwu.javaee.listener.MyHttpSessionListener - - - - - io.github.dunwu.javaee.listener.MyServletContextListener - - - - - io.github.dunwu.javaee.listener.MyServletRequestListener - - - - - - - io.github.dunwu.javaee.listener.MyHttpSessionAttributeListener - - - - - io.github.dunwu.javaee.listener.MyServletContextAttributeListener - - - - - io.github.dunwu.javaee.listener.MyServletRequestAttributeListener - - - - - - - io.github.dunwu.javaee.listener.MyHttpSessionBindingListener - - - - - - io.github.dunwu.javaee.listener.LoginSessionListener - - - - - - io.github.dunwu.javaee.listener.PersonInfoListener - - - - - - index.jsp - - diff --git a/codes/javaee/listener/src/main/webapp/a.gif b/codes/javaee/listener/src/main/webapp/a.gif deleted file mode 100644 index 6f1cba44..00000000 Binary files a/codes/javaee/listener/src/main/webapp/a.gif and /dev/null differ diff --git a/codes/javaee/listener/src/main/webapp/aa.html b/codes/javaee/listener/src/main/webapp/aa.html deleted file mode 100644 index 4153961f..00000000 --- a/codes/javaee/listener/src/main/webapp/aa.html +++ /dev/null @@ -1,17 +0,0 @@ -

sdfsd

- -

sadfsdf

- -
    -
  1. sadf
  2. -
  3. asdfsadf
  4. -
  5. sdf
  6. -
  7. sdfsadfasdf
  8. -
- - - - - - - diff --git a/codes/javaee/listener/src/main/webapp/active.jsp b/codes/javaee/listener/src/main/webapp/active.jsp deleted file mode 100644 index e7fe89e3..00000000 --- a/codes/javaee/listener/src/main/webapp/active.jsp +++ /dev/null @@ -1,24 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> - -<%@ page import="io.github.dunwu.javaee.listener.bean.PersonInfo" %> - - - - Insert title here - - - -<% - PersonInfo personInfo = (PersonInfo) session.getAttribute("personInfo"); - if (personInfo == null) { - personInfo = new PersonInfo(); - personInfo.setAccount("Zhang Peng"); - session.setAttribute("personInfo", personInfo); - out.println("PersonInfo 对象不存在。已经成功新建。sessionId: " + session.getId()); - } else { - out.println("PersonInfo 对象存在。无需新建。sessionId: " + session.getId()); - } -%> - - - diff --git a/codes/javaee/listener/src/main/webapp/b.gif b/codes/javaee/listener/src/main/webapp/b.gif deleted file mode 100644 index 2996ad0e..00000000 Binary files a/codes/javaee/listener/src/main/webapp/b.gif and /dev/null differ diff --git a/codes/javaee/listener/src/main/webapp/index.jsp b/codes/javaee/listener/src/main/webapp/index.jsp deleted file mode 100644 index b45578ba..00000000 --- a/codes/javaee/listener/src/main/webapp/index.jsp +++ /dev/null @@ -1,22 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> -<% - String path = request.getContextPath(); - String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path + "/"; -%> - - - - - - javaee-listener 首页 - - - -This is my JSP page.
-Listener
-active
-登录
-在线用户统计
-Listener
- - diff --git a/codes/javaee/listener/src/main/webapp/listener.jsp b/codes/javaee/listener/src/main/webapp/listener.jsp deleted file mode 100644 index fb7dbd3c..00000000 --- a/codes/javaee/listener/src/main/webapp/listener.jsp +++ /dev/null @@ -1,84 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> - - - -<% - String action = request.getParameter("action"); - String name = request.getParameter("name"); - String value = request.getParameter("value"); - - session.getId(); - - if ("addRequestAttribute".equals(action)) { - request.setAttribute(name, value); - } else if ("removeRequestAttribute".equals(action)) { - request.removeAttribute(name); - } else if ("addSessionAttribute".equals(action)) { - session.setAttribute(name, value); - } else if ("removeSessionAttribute".equals(action)) { - session.removeAttribute(name); - } else if ("logout".equals(action)) { - session.invalidate(); - out.println("返回"); - return; - } -%> - - - - - - -
-
-
-
删除 Session
-
- -
-
-
- -服务器启动时间:<%=DateFormat.getDateTimeInstance().format(ApplicationConstants.START_DATE)%>, -累计共接待过 <%= ApplicationConstants.TOTAL_HISTORY_COUNT %> 访客。
-同时在线人数最多为 <%= ApplicationConstants.MAX_ONLINE_COUNT %> 人, -发生在 <%=DateFormat.getDateTimeInstance().format(ApplicationConstants.MAX_ONLINE_COUNT_DATE)%>。
- -目前在线总数:<%= ApplicationConstants.SESSION_MAP.size() %>,登录用户:<%=ApplicationConstants.CURRENT_LOGIN_COUNT%>。
- - - - - - - - - - - <% - for (String id : ApplicationConstants.SESSION_MAP.keySet()) { - HttpSession sess = ApplicationConstants.SESSION_MAP.get(id); - %> - - - - - - - - - - <% - } - %> -
jsessionidaccountcreationTimelastAccessedTimenewactiveTimesip
<%=id%><%=sess.getAttribute("account")%><%=DateFormat.getDateTimeInstance().format( - new Date(sess.getCreationTime()))%><%=DateFormat.getDateTimeInstance().format( - new Date(sess.getLastAccessedTime()))%><%=sess.isNew()%><%=sess.getAttribute("activeTimes")%><%=sess.getAttribute("ip")%>
- - diff --git a/codes/javaee/listener/src/main/webapp/online.jsp b/codes/javaee/listener/src/main/webapp/online.jsp deleted file mode 100644 index 2a03d70b..00000000 --- a/codes/javaee/listener/src/main/webapp/online.jsp +++ /dev/null @@ -1,47 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> - - - - - - -服务器启动时间:<%=DateFormat.getDateTimeInstance().format(ApplicationConstants.START_DATE)%>, -累计共接待过 <%= ApplicationConstants.TOTAL_HISTORY_COUNT %> 访客。
-同时在线人数最多为 <%= ApplicationConstants.MAX_ONLINE_COUNT %> 人, -发生在 <%=DateFormat.getDateTimeInstance().format(ApplicationConstants.MAX_ONLINE_COUNT_DATE)%>。
- -目前在线总数:<%= ApplicationConstants.SESSION_MAP.size() %>,登录用户:<%=ApplicationConstants.CURRENT_LOGIN_COUNT%>。
- - - - - - - - - - - <% - for (String id : ApplicationConstants.SESSION_MAP.keySet()) { - HttpSession sess = ApplicationConstants.SESSION_MAP.get(id); - PersonInfo personInfo = (PersonInfo)sess.getAttribute("personInfo"); - %> - > - - - - - - - - - <% - } - %> -
jsessionidaccountcreationTimelastAccessedTimenewactiveTimesip
<%=id%><%=personInfo==null ? " " : personInfo.getAccount()%><%=DateFormat.getDateTimeInstance().format(sess.getCreationTime())%><%=DateFormat.getDateTimeInstance().format( - new Date(sess.getLastAccessedTime()))%><%=sess.isNew()%><%=sess.getAttribute("activeTimes")%><%=sess.getAttribute("ip") %>
- - diff --git a/codes/javaee/listener/src/main/webapp/testLoginSessionListener.jsp b/codes/javaee/listener/src/main/webapp/testLoginSessionListener.jsp deleted file mode 100644 index 1e60c1a9..00000000 --- a/codes/javaee/listener/src/main/webapp/testLoginSessionListener.jsp +++ /dev/null @@ -1,71 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> -<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt" %> - -<% - String action = request.getParameter("action"); - String account = request.getParameter("account"); - - if("login".equals(action) && account.trim().length() > 0){ - - // 登录,将personInfo放入session - PersonInfo personInfo = new PersonInfo(); - personInfo.setAccount(account.trim().toLowerCase()); - personInfo.setIp(request.getRemoteAddr()); - personInfo.setLoginDate(new java.util.Date()); - - session.setAttribute("personInfo", personInfo); - - response.sendRedirect(response.encodeRedirectURL(request.getRequestURI())); - return; - } - else if("logout".equals(action)){ - - // 注销,将personInfo从session中移除 - session.removeAttribute("personInfo"); - - response.sendRedirect(response.encodeRedirectURL(request.getRequestURI())); - return; - } -%> - - - - - Insert title here - - - - - - - - - 欢迎您,${ personInfo.account }。
- 您的登录IP为${ personInfo.ip },
- 登录时间为。 - 退出 - - - -
- - - - ${ msg } - -
- 帐号: - - -
-
- -
- - - diff --git a/codes/javaee/listener/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java b/codes/javaee/listener/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java deleted file mode 100644 index 6d275e8c..00000000 --- a/codes/javaee/listener/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.github.dunwu.javaee.server; - -import java.util.ArrayList; - -import org.apache.commons.lang3.StringUtils; -import org.assertj.core.util.Lists; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.WebAppClassLoader; -import org.eclipse.jetty.webapp.WebAppContext; - -/** - * JettyFactory 可以工作在 Eclipse 和 Intellij 中,用来启动 jetty 服务。 Intellij - * 并不支持jetty,所以要想类似eclipse一样的使用jetty,需要配置webdefault.xml。 - * - * @author Zhang Peng - */ -@SuppressWarnings("unused") -public class JettyFactory { - private static final int PORT = 8080; - private static final String CONTEXT = "/"; - private static final String RESOURCE_BASE_PATH = "src/main/webapp"; - private static final String WEB_XML_PATH = "/WEB-INF/web.xml"; - private static final String[] TLD_JAR_NAMES = - new String[]{"sitemesh", "spring-webmvc", "shiro-web", "tiles"}; - private static final String WINDOWS_WEBDEFAULT_PATH = "jetty/webdefault.xml"; - - public static final int IDE_ECLIPSE = 0; - public static final int IDE_INTELLIJ = 1; - - - public static Server initServer() { - Profiles.setProfileAsSystemProperty(Profiles.DEVELOPMENT); - WebAppContext webAppContext = new WebAppContext(); - Server server = new Server(PORT); - server.setHandler(webAppContext); - return server; - } - - public static void initWebAppContext(Server server, int type) throws Exception { - System.out.println("[INFO] Application loading"); - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - webAppContext.setContextPath(CONTEXT); - webAppContext.setResourceBase(getAbsolutePath() + RESOURCE_BASE_PATH); - webAppContext.setDescriptor(getAbsolutePath() + RESOURCE_BASE_PATH + WEB_XML_PATH); - - if (IDE_INTELLIJ == type) { - webAppContext.setDefaultsDescriptor(WINDOWS_WEBDEFAULT_PATH); - supportJspAndSetTldJarNames(server, TLD_JAR_NAMES); - } else { - webAppContext.setParentLoaderPriority(true); - } - - System.out.println("[INFO] Application loaded"); - } - - public static void reloadWebAppContext(Server server) throws Exception { - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - System.out.println("[INFO] Application reloading"); - webAppContext.stop(); - WebAppClassLoader classLoader = new WebAppClassLoader(webAppContext); - classLoader.addClassPath(getAbsolutePath() + "target/classes"); - classLoader.addClassPath(getAbsolutePath() + "target/test-classes"); - webAppContext.setClassLoader(classLoader); - webAppContext.start(); - System.out.println("[INFO] Application reloaded"); - } - - - public static void startServer(Server server) throws Exception { - System.out.println("[HINT] Don't forget to set -XX:MaxPermSize=128m"); - server.start(); - System.out.println("Server running at http://localhost:" + PORT + CONTEXT); - System.out.println("[HINT] Hit Enter to reload the application quickly"); - } - - public static String getAbsolutePath() { - String path = null; - String folderPath = JettyFactory.class.getProtectionDomain().getCodeSource().getLocation() - .getPath().substring(1); - if (folderPath.indexOf("target") > 0) { - path = folderPath.substring(0, folderPath.indexOf("target")); - } - return path; - } - - public static void supportJspAndSetTldJarNames(Server server, String... jarNames) { - WebAppContext context = (WebAppContext) server.getHandler(); - // This webapp will use jsps and jstl. We need to enable the AnnotationConfiguration in - // order to correctly set up the jsp container - org.eclipse.jetty.webapp.Configuration.ClassList classlist = - org.eclipse.jetty.webapp.Configuration.ClassList.setServerDefault(server); - classlist.addBefore("org.eclipse.jetty.webapp.JettyWebXmlConfiguration", - "org.eclipse.jetty.annotations.AnnotationConfiguration"); - // Set the ContainerIncludeJarPattern so that jetty examines these container-path jars for - // tlds, web-fragments etc. - // If you omit the jar that contains the jstl .tlds, the jsp engine will scan for them - // instead. - ArrayList jarNameExprssions = Lists.newArrayList(".*/[^/]*servlet-api-[^/]*\\.jar$", - ".*/javax.servlet.jsp.jstl-.*\\.jar$", ".*/[^/]*taglibs.*\\.jar$"); - - for (String jarName : jarNames) { - jarNameExprssions.add(".*/" + jarName + "-[^/]*\\.jar$"); - } - - context.setAttribute("org.eclipse.jetty.io.github.dunwu.javaee.server.webapp.ContainerIncludeJarPattern", - StringUtils.join(jarNameExprssions, '|')); - } -} diff --git a/codes/javaee/listener/src/test/java/io/github/dunwu/javaee/server/Profiles.java b/codes/javaee/listener/src/test/java/io/github/dunwu/javaee/server/Profiles.java deleted file mode 100644 index 3a8ee7c4..00000000 --- a/codes/javaee/listener/src/test/java/io/github/dunwu/javaee/server/Profiles.java +++ /dev/null @@ -1,29 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2005, 2014 springside.github.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - *******************************************************************************/ -package io.github.dunwu.javaee.server; - -/** - * Spring profile 常用方法与profile名称。 - * - * @author calvin - */ -public class Profiles { - - public static final String ACTIVE_PROFILE = "spring.profiles.active"; - public static final String DEFAULT_PROFILE = "spring.profiles.default"; - - public static final String PRODUCTION = "production"; - public static final String DEVELOPMENT = "development"; - public static final String UNIT_TEST = "test"; - public static final String FUNCTIONAL_TEST = "functional"; - - /** - * 在Spring启动前,设置profile的环境变量。 - */ - public static void setProfileAsSystemProperty(String profile) { - System.setProperty(ACTIVE_PROFILE, profile); - } -} diff --git a/codes/javaee/listener/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java b/codes/javaee/listener/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java deleted file mode 100644 index 2936b162..00000000 --- a/codes/javaee/listener/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.github.dunwu.javaee.server; - -import org.eclipse.jetty.server.Server; - -/** - * 快速启动 jetty 服务器,方便测试 - * - * @author Zhang Peng - */ -public class QuickStartServer { - // private static int STARTUP_TYPE = JettyFactory.IDE_ECLIPSE; - private static int STARTUP_TYPE = JettyFactory.IDE_INTELLIJ; - - public static void main(String[] args) throws Exception { - Server server = JettyFactory.initServer(); - JettyFactory.initWebAppContext(server, STARTUP_TYPE); - - try { - JettyFactory.startServer(server); - - // 等待用户输入回车重载应用 - while (true) { - char c = (char) System.in.read(); - if (c == '\n') { - JettyFactory.reloadWebAppContext(server); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.exit(-1); - } - } -} diff --git a/codes/javaee/listener/src/test/resources/jetty/webdefault.xml b/codes/javaee/listener/src/test/resources/jetty/webdefault.xml deleted file mode 100644 index 65ec9cae..00000000 --- a/codes/javaee/listener/src/test/resources/jetty/webdefault.xml +++ /dev/null @@ -1,534 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Default web.xml file. - This file is applied to a Web application before it's own WEB_INF/web.xml file - - - - - - - - org.eclipse.jetty.servlet.listener.ELContextCleaner - - - - - - - - org.eclipse.jetty.servlet.listener.IntrospectorCleaner - - - - - - - - - - - - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - aliases - false - - - acceptRanges - true - - - dirAllowed - true - - - welcomeServlets - false - - - redirectWelcome - false - - - maxCacheSize - 256000000 - - - maxCachedFileSize - 200000000 - - - maxCachedFiles - 2048 - - - gzip - false - - - etags - false - - - useFileMappedBuffer - false - - - - 0 - - - - default - / - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jsp - org.eclipse.jetty.jsp.JettyJspServlet - - logVerbosityLevel - DEBUG - - - fork - false - - - xpoweredBy - false - - - compilerTargetVM - 1.7 - - - compilerSourceVM - 1.7 - - - 0 - - - - jsp - *.jsp - *.jspf - *.jspx - *.xsp - *.JSP - *.JSPF - *.JSPX - *.XSP - - - - - - - - 30 - - - - - - - - - - - - - - - index.html - index.htm - index.jsp - - - - - - - - ar - ISO-8859-6 - - - be - ISO-8859-5 - - - bg - ISO-8859-5 - - - ca - ISO-8859-1 - - - cs - ISO-8859-2 - - - da - ISO-8859-1 - - - de - ISO-8859-1 - - - el - ISO-8859-7 - - - en - ISO-8859-1 - - - es - ISO-8859-1 - - - et - ISO-8859-1 - - - fi - ISO-8859-1 - - - fr - ISO-8859-1 - - - hr - ISO-8859-2 - - - hu - ISO-8859-2 - - - is - ISO-8859-1 - - - it - ISO-8859-1 - - - iw - ISO-8859-8 - - - ja - Shift_JIS - - - ko - EUC-KR - - - lt - ISO-8859-2 - - - lv - ISO-8859-2 - - - mk - ISO-8859-5 - - - nl - ISO-8859-1 - - - no - ISO-8859-1 - - - pl - ISO-8859-2 - - - pt - ISO-8859-1 - - - ro - ISO-8859-2 - - - ru - ISO-8859-5 - - - sh - ISO-8859-5 - - - sk - ISO-8859-2 - - - sl - ISO-8859-2 - - - sq - ISO-8859-2 - - - sr - ISO-8859-5 - - - sv - ISO-8859-1 - - - tr - ISO-8859-9 - - - uk - ISO-8859-5 - - - zh - GB2312 - - - zh_TW - Big5 - - - - - - - - - Disable TRACE - / - TRACE - - - - - - Enable everything but TRACE - / - TRACE - - - - - diff --git a/codes/javaee/oss/pom.xml b/codes/javaee/oss/pom.xml deleted file mode 100644 index 4278f78a..00000000 --- a/codes/javaee/oss/pom.xml +++ /dev/null @@ -1,191 +0,0 @@ - - - 4.0.0 - - - io.github.dunwu - javaee-notes-oss - 1.0.0 - jar - - - - - javaee-notes-oss - javaee 学习笔记之 OSS(Open Source Software) - - - - - - UTF-8 - 1.7 - ${java.version} - ${java.version} - - - 9.3.2.v20150730 - - - - - ch.qos.logback - logback-classic - 1.1.3 - - - ch.qos.logback - logback-core - 1.1.3 - - - org.logback-extensions - logback-ext-spring - 0.1.2 - - - org.slf4j - jcl-over-slf4j - 1.7.12 - - - commons-logging - commons-logging - 1.2 - - - log4j - log4j - 1.2.17 - - - - - - commons-codec - commons-codec - 1.10 - - - org.bouncycastle - bcprov-jdk15on - 1.54 - - - javax.validation - validation-api - 1.1.0.Final - - - - - - org.jsoup - jsoup - 1.9.2 - - - - - - com.google.zxing - core - 3.3.0 - - - com.google.zxing - javase - 3.3.0 - - - net.coobird - thumbnailator - [0.4, 0.5) - - - net.sf.jmimemagic - jmimemagic - 0.1.4 - - - - - - org.apache.activemq - activemq-all - 5.14.1 - - - - - - com.alibaba - fastjson - 1.2.8 - - - com.fasterxml.jackson.core - jackson-core - 2.7.4 - - - com.fasterxml.jackson.core - jackson-databind - 2.5.1 - - - - - - javax.mail - mail - 1.4.7 - - - - - - org.apache.velocity - velocity - 1.7 - - - - - - junit - junit - 4.12 - test - - - org.assertj - assertj-core - 3.5.2 - test - - - - - - org.apache.commons - commons-lang3 - 3.4 - - - commons-collections - commons-collections - 3.2.2 - - - - - - - - - - - diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/DsaCoder.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/DsaCoder.java deleted file mode 100644 index b7d6627e..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/DsaCoder.java +++ /dev/null @@ -1,84 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.digest; - -import org.apache.commons.codec.binary.Base64; - -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.Signature; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; - -/** - * @Title DsaCoder - * @Description 数字签名算法(Digital Signature Algorithm, DSA)工具类。 DSA是一种数字签名算法。 - * DSA仅支持SHA系列算法,而JDK仅支持SHA1withDSA。 - * @Author victor zhang - * @Date 2016年7月21日 - */ -public class DsaCoder { - public static final String KEY_ALGORITHM = "DSA"; - public static final String SIGN_ALGORITHM = "SHA1withDSA"; - - /** - * DSA密钥长度默认1024位。 密钥长度必须是64的整数倍,范围在512~1024之间 - */ - private static final int KEY_SIZE = 1024; - - private KeyPair keyPair; - - public DsaCoder() throws Exception { - keyPair = initKey(); - } - - public byte[] signature(byte[] data, byte[] privateKey) throws Exception { - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); - PrivateKey key = keyFactory.generatePrivate(keySpec); - - Signature signature = Signature.getInstance(SIGN_ALGORITHM); - signature.initSign(key); - signature.update(data); - return signature.sign(); - } - - public boolean verify(byte[] data, byte[] publicKey, byte[] sign) throws Exception { - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); - PublicKey key = keyFactory.generatePublic(keySpec); - - Signature signature = Signature.getInstance(SIGN_ALGORITHM); - signature.initVerify(key); - signature.update(data); - return signature.verify(sign); - } - - private KeyPair initKey() throws Exception { - // 初始化密钥对生成器 - KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM); - // 实例化密钥对生成器 - keyPairGen.initialize(KEY_SIZE); - // 实例化密钥对 - return keyPairGen.genKeyPair(); - } - - public byte[] getPublicKey() { - return keyPair.getPublic().getEncoded(); - } - - public byte[] getPrivateKey() { - return keyPair.getPrivate().getEncoded(); - } - - public static void main(String[] args) throws Exception { - String msg = "Hello World"; - DsaCoder dsa = new DsaCoder(); - byte[] sign = dsa.signature(msg.getBytes(), dsa.getPrivateKey()); - boolean flag = dsa.verify(msg.getBytes(), dsa.getPublicKey(), sign); - String result = flag ? "数字签名匹配" : "数字签名不匹配"; - System.out.println("数字签名:" + Base64.encodeBase64URLSafeString(sign)); - System.out.println("验证结果:" + result); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/HmacCoder.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/HmacCoder.java deleted file mode 100644 index a706d355..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/HmacCoder.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.digest; - -import org.apache.commons.codec.binary.Base64; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -/** - * @Title HmacCoder - * @Description 消息认证码算法(Message Autherntication Code, MAC)是基于密码的消息摘要算法。 它兼容了MD/SHA的特性,并以此为基础加入了密钥。 - * @Author victor zhang - * @Date 2016年7月21日 - */ -public class HmacCoder { - /** - * JDK支持HmacMD5, HmacSHA1, HmacSHA256, HmacSHA384, HmacSHA512 - */ - public enum HmacTypeEn { - HmacMD5, HmacSHA1, HmacSHA256, HmacSHA384, HmacSHA512; - } - - public static byte[] encode(byte[] plaintext, byte[] secretKey, HmacTypeEn type) throws Exception { - SecretKeySpec keySpec = new SecretKeySpec(secretKey, type.name()); - Mac mac = Mac.getInstance(keySpec.getAlgorithm()); - mac.init(keySpec); - return mac.doFinal(plaintext); - } - - public static void main(String[] args) throws Exception { - String msg = "Hello World!"; - byte[] secretKey = "Secret_Key".getBytes("UTF8"); - byte[] digest = HmacCoder.encode(msg.getBytes(), secretKey, HmacTypeEn.HmacSHA256); - System.out.println("原文: " + msg); - System.out.println("摘要: " + Base64.encodeBase64URLSafeString(digest)); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/MdCoder.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/MdCoder.java deleted file mode 100644 index 5cf5efce..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/MdCoder.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.digest; - -import org.apache.commons.codec.binary.Base64; - -import java.security.MessageDigest; - -/** - * @Title MdCoder - * @Description 消息摘要算法(Message Digest, MD)是消息摘要算法。 - * @Author victor zhang - * @Date 2016年7月21日 - */ -public class MdCoder { - /** - * JDK支持MD2和MD5两种MD算法 - */ - public enum MdTypeEn { - MD2, MD5 - } - - public static byte[] encode(byte[] input, MdTypeEn type) throws Exception { - // 根据类型,初始化消息摘要对象 - MessageDigest md5Digest = MessageDigest.getInstance(type.name()); - - // 更新要计算的内容 - md5Digest.update(input); - - // 完成哈希计算,返回摘要 - return md5Digest.digest(); - } - - public static byte[] encodeWithBase64(byte[] input, MdTypeEn type) throws Exception { - return Base64.encodeBase64URLSafe(encode(input, type)); - } - - public static void main(String[] args) throws Exception { - String msg = "Hello World!"; - byte[] encodeWithBase64 = MdCoder.encodeWithBase64(msg.getBytes(), MdTypeEn.MD5); - - String result = String.format("%s摘要:%s", MdTypeEn.MD5.name(), new String(encodeWithBase64)); - System.out.println("原文: " + msg); - System.out.println(result); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/ShaCoder.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/ShaCoder.java deleted file mode 100644 index 69b3028c..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/ShaCoder.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.digest; - -import org.apache.commons.codec.binary.Base64; - -import java.security.MessageDigest; - -/** - * @Title MsgDigestDemo - * @Description 安全散列算法(Secure Hash Algorithm, SHA)是消息摘要算法 - * @Author victor zhang - * @Date 2016年7月21日 - */ -public class ShaCoder { - /** - * JDK支持SHA1、SHA256、SHA384和SHA512几种SHA算法 - */ - public enum ShaTypeEn { - SHA1("SHA1"), SHA256("SHA-256"), SHA384("SHA-384"), SHA512("SHA-512"); - - private String name; - - ShaTypeEn(String name) { - this.name = name; - } - - public String getName() { - return this.name; - } - } - - public static byte[] encode(byte[] input, ShaTypeEn type) throws Exception { - // 根据类型,初始化消息摘要对象 - MessageDigest md5Digest = MessageDigest.getInstance(type.getName()); - - // 更新要计算的内容 - md5Digest.update(input); - - // 完成哈希计算,返回摘要 - return md5Digest.digest(); - } - - public static byte[] encodeWithBase64(byte[] input, ShaTypeEn type) throws Exception { - return Base64.encodeBase64URLSafe(encode(input, type)); - } - - public static void main(String[] args) throws Exception { - String msg = "Hello World!"; - byte[] encodeWithBase64 = ShaCoder.encodeWithBase64(msg.getBytes(), ShaTypeEn.SHA384); - - String result = String.format("%s摘要:%s", ShaTypeEn.SHA384.getName(), new String(encodeWithBase64)); - System.out.println("原文: " + msg); - System.out.println(result); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/AESCoder.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/AESCoder.java deleted file mode 100644 index 20207c6a..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/AESCoder.java +++ /dev/null @@ -1,128 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.encrypt; - -import org.bouncycastle.util.encoders.Base64; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.KeyGenerator; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.IvParameterSpec; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; - -/** - * @Title AESCoder - * @Description AES安全编码:对称加密算法。DES的替代方案。 - * @Author victor zhang - * @Date 2016年7月14日 - */ -public class AESCoder { - public static final String KEY_ALGORITHM_AES = "AES"; - public static final String CIPHER_AES_DEFAULT = "AES"; - public static final String CIPHER_AES_ECB_PKCS5PADDING = "AES/ECB/PKCS5Padding"; // 算法/模式/补码方式 - public static final String CIPHER_AES_CBC_PKCS5PADDING = "AES/CBC/PKCS5Padding"; - public static final String CIPHER_AES_CBC_NOPADDING = "AES/CBC/NoPadding"; - - private static final String SEED = "%%%today is nice***"; // 用于生成随机数的种子 - private Key key; - private Cipher cipher; - private String transformation; - - public AESCoder() throws NoSuchAlgorithmException, NoSuchPaddingException { - this.key = initKey(); - this.cipher = Cipher.getInstance(CIPHER_AES_DEFAULT); - this.transformation = CIPHER_AES_DEFAULT; - } - - public AESCoder(String transformation) throws NoSuchAlgorithmException, NoSuchPaddingException { - this.key = initKey(); - this.cipher = Cipher.getInstance(transformation); - this.transformation = transformation; - } - - /** - * @Title decrypt - * @Description 解密 - * @Author victor zhang - * @Date 2016年7月20日 - * @param input 密文 - * @return byte[] 明文 - * @throws InvalidKeyException - * @throws IllegalBlockSizeException - * @throws BadPaddingException - * @throws InvalidAlgorithmParameterException - */ - public byte[] decrypt(byte[] input) throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException, - InvalidAlgorithmParameterException { - if (transformation.equals(CIPHER_AES_CBC_PKCS5PADDING) || transformation.equals(CIPHER_AES_CBC_NOPADDING)) { - cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(getIV())); - } else { - cipher.init(Cipher.DECRYPT_MODE, key); - } - return cipher.doFinal(input); - } - - /** - * @Title encrypt - * @Description 加密 - * @Author victor zhang - * @Date 2016年7月20日 - * @param input 明文 - * @return byte[] 密文 - * @throws InvalidKeyException - * @throws IllegalBlockSizeException - * @throws BadPaddingException - * @throws InvalidAlgorithmParameterException - */ - public byte[] encrypt(byte[] input) throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException, - InvalidAlgorithmParameterException { - if (transformation.equals(CIPHER_AES_CBC_PKCS5PADDING) || transformation.equals(CIPHER_AES_CBC_NOPADDING)) { - cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(getIV())); - } else { - cipher.init(Cipher.ENCRYPT_MODE, key); - } - return cipher.doFinal(input); - } - - /** - * @Title initKey - * @Description 根据随机数种子生成一个密钥 - * @Author victor zhang - * @Date 2016年7月14日 - * @return Key - * @throws NoSuchAlgorithmException - */ - private Key initKey() throws NoSuchAlgorithmException { - // 根据种子生成一个安全的随机数 - SecureRandom secureRandom = null; - secureRandom = new SecureRandom(SEED.getBytes()); - - KeyGenerator keyGen = KeyGenerator.getInstance(KEY_ALGORITHM_AES); - keyGen.init(secureRandom); - return keyGen.generateKey(); - } - - private byte[] getIV() { - String iv = "0123456789ABCDEF"; // IV length: must be 16 bytes long - return iv.getBytes(); - } - - public static void main(String[] args) throws Exception { - AESCoder aes = new AESCoder(CIPHER_AES_CBC_PKCS5PADDING); - - String msg = "Hello World!"; - System.out.println("[AES加密、解密]"); - System.out.println("message: " + msg); - byte[] encoded = aes.encrypt(msg.getBytes("UTF8")); - String encodedBase64 = Base64.toBase64String(encoded); - System.out.println("encoded: " + encodedBase64); - - byte[] decodedBase64 = Base64.decode(encodedBase64); - byte[] decoded = aes.decrypt(decodedBase64); - System.out.println("decoded: " + new String(decoded)); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/Base64Demo.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/Base64Demo.java deleted file mode 100644 index efd3a121..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/Base64Demo.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.encrypt; - -import org.apache.commons.codec.binary.Base64; - -import java.io.UnsupportedEncodingException; - -/** - * @Title Base64Demo - * @Description Base64编码、解码范例 - * @Author victor zhang - * @Date 2016年7月21日 - */ -public class Base64Demo { - public static void main(String[] args) throws UnsupportedEncodingException { - String url = - "https://www.baidu.com/s?wd=Base64&rsv_spt=1&rsv_iqid=0xa9188d560005131f&issp=1&f=3&rsv_bp=0&rsv_idx=2&ie=utf-8&tn=baiduhome_pg&rsv_enter=1&rsv_sug3=1&rsv_sug1=1&rsv_sug7=001&rsv_sug2=1&rsp=0&rsv_sug9=es_2_1&rsv_sug4=2153&rsv_sug=9"; - // byte[] encoded = Base64.encodeBase64(url.getBytes("UTF8")); // 标准的Base64编码 - byte[] encoded = Base64.encodeBase64URLSafe(url.getBytes("UTF8")); // URL安全的Base64编码 - byte[] decoded = Base64.decodeBase64(encoded); - System.out.println("url:" + url); - System.out.println("encoded:" + new String(encoded)); - System.out.println("decoded:" + new String(decoded)); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/DESCoder.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/DESCoder.java deleted file mode 100644 index 5c905447..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/DESCoder.java +++ /dev/null @@ -1,130 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.encrypt; - -import org.bouncycastle.util.encoders.Base64; - -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.KeyGenerator; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.IvParameterSpec; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.Key; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.SecureRandom; - -/** - * @Title DESCoder - * @Description DES安全编码:是经典的对称加密算法。密钥仅56位,且迭代次数偏少。已被视为并不安全的加密算法。 - * @Author victor zhang - * @Date 2016年7月14日 - */ -public class DESCoder { - public static final String KEY_ALGORITHM_DES = "DES"; - public static final String CIPHER_DES_DEFAULT = "DES"; - public static final String CIPHER_DES_ECB_PKCS5PADDING = "DES/ECB/PKCS5Padding"; // 算法/模式/补码方式 - public static final String CIPHER_DES_CBC_PKCS5PADDING = "DES/CBC/PKCS5Padding"; - public static final String CIPHER_DES_CBC_NOPADDING = "DES/CBC/NoPadding"; - private static final String SEED = "%%%today is nice***"; // 用于生成随机数的种子 - - private Key key; - private Cipher cipher; - private String transformation; - - public DESCoder() throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException { - this.key = initKey(); - this.cipher = Cipher.getInstance(CIPHER_DES_DEFAULT); - this.transformation = CIPHER_DES_DEFAULT; - } - - public DESCoder(String transformation) - throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException { - this.key = initKey(); - this.cipher = Cipher.getInstance(transformation); - this.transformation = transformation; - } - - /** - * @Title decrypt - * @Description 解密 - * @Author victor zhang - * @Date 2016年7月20日 - * @param input 密文 - * @return byte[] 明文 - * @throws InvalidKeyException - * @throws IllegalBlockSizeException - * @throws BadPaddingException - * @throws InvalidAlgorithmParameterException - */ - public byte[] decrypt(byte[] input) throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException, - InvalidAlgorithmParameterException { - if (transformation.equals(CIPHER_DES_CBC_PKCS5PADDING) || transformation.equals(CIPHER_DES_CBC_NOPADDING)) { - cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(getIV())); - } else { - cipher.init(Cipher.DECRYPT_MODE, key); - } - return cipher.doFinal(input); - } - - /** - * @Title encrypt - * @Description 加密 - * @Author victor zhang - * @Date 2016年7月20日 - * @param input 明文 - * @return byte[] 密文 - * @throws InvalidKeyException - * @throws IllegalBlockSizeException - * @throws BadPaddingException - * @throws InvalidAlgorithmParameterException - */ - public byte[] encrypt(byte[] input) throws InvalidKeyException, IllegalBlockSizeException, BadPaddingException, - InvalidAlgorithmParameterException { - if (transformation.equals(CIPHER_DES_CBC_PKCS5PADDING) || transformation.equals(CIPHER_DES_CBC_NOPADDING)) { - cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(getIV())); - } else { - cipher.init(Cipher.ENCRYPT_MODE, key); - } - return cipher.doFinal(input); - } - - /** - * @Title initKey - * @Description 根据随机数种子生成一个密钥 - * @Author victor zhang - * @Date 2016年7月14日 - * @Return Key - * @throws NoSuchAlgorithmException - * @throws NoSuchProviderException - */ - private Key initKey() throws NoSuchAlgorithmException, NoSuchProviderException { - // 根据种子生成一个安全的随机数 - SecureRandom secureRandom = null; - secureRandom = new SecureRandom(SEED.getBytes()); - - KeyGenerator keyGen = KeyGenerator.getInstance(KEY_ALGORITHM_DES); - keyGen.init(secureRandom); - return keyGen.generateKey(); - } - - private byte[] getIV() { - String iv = "01234567"; // IV length: must be 8 bytes long - return iv.getBytes(); - } - - public static void main(String[] args) throws Exception { - DESCoder aes = new DESCoder(CIPHER_DES_CBC_PKCS5PADDING); - - String msg = "Hello World!"; - System.out.println("原文: " + msg); - byte[] encoded = aes.encrypt(msg.getBytes("UTF8")); - String encodedBase64 = Base64.toBase64String(encoded); - System.out.println("密文: " + encodedBase64); - - byte[] decodedBase64 = Base64.decode(encodedBase64); - byte[] decoded = aes.decrypt(decodedBase64); - System.out.println("明文: " + new String(decoded)); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/DESedeCoder.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/DESedeCoder.java deleted file mode 100644 index 7c21f497..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/DESedeCoder.java +++ /dev/null @@ -1,72 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.encrypt; - -import org.apache.commons.codec.binary.Base64; -import org.bouncycastle.jce.provider.BouncyCastleProvider; - -import javax.crypto.Cipher; -import javax.crypto.KeyGenerator; -import java.security.Key; -import java.security.NoSuchAlgorithmException; -import java.security.NoSuchProviderException; -import java.security.Security; - -/** - * @Title DESedeCoder - * @Description DESede安全编码,DES的升级版,支持更长的密钥,基本算法不变。 - * @Author victor zhang - * @Date 2016年7月20日 - */ -public class DESedeCoder { - /** - * 加密算法 - */ - public static final String KEY_ALGORITHM = "DESede"; - - /** - * 算法名称/加密模式/填充方式 - */ - public static final String CIPHER_ALGORITHM = "DESede/ECB/PKCS5Padding"; - - /** - * 密钥 - */ - private Key key; - - public DESedeCoder() throws NoSuchAlgorithmException, NoSuchProviderException { - this.key = initKey(); - } - - public byte[] encrypt(byte[] plaintext) throws Exception { - Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); - cipher.init(Cipher.ENCRYPT_MODE, key); - return cipher.doFinal(plaintext); - } - - public byte[] decrypt(byte[] ciphertext) throws Exception { - Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); - cipher.init(Cipher.DECRYPT_MODE, key); - return cipher.doFinal(ciphertext); - } - - private Key initKey() throws NoSuchAlgorithmException, NoSuchProviderException { - // 标准的密钥生成 - // KeyGenerator keyGen = KeyGenerator.getInstance(KEY_ALGORITHM); - // keyGen.init(112); - - // 标准的密钥生成不支持128位。如果要使用,需引入Bouncy Castle的加密算法,方法如下 - Security.addProvider(new BouncyCastleProvider()); - KeyGenerator keyGen = KeyGenerator.getInstance(KEY_ALGORITHM, "BC"); - keyGen.init(128); - return keyGen.generateKey(); - } - - public static void main(String[] args) throws Exception { - DESedeCoder desedeCoder = new DESedeCoder(); - String message = "Hello World!"; - byte[] ciphertext = desedeCoder.encrypt(message.getBytes()); - System.out.println(Base64.encodeBase64String(ciphertext)); - - byte[] plaintext = desedeCoder.decrypt(ciphertext); - System.out.println(new String(plaintext)); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/PBECoder.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/PBECoder.java deleted file mode 100644 index 2d654ef8..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/PBECoder.java +++ /dev/null @@ -1,92 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.encrypt; - -import org.apache.commons.codec.binary.Base64; - -import javax.crypto.Cipher; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.PBEParameterSpec; -import java.security.Key; -import java.security.SecureRandom; - -/** - * @Title PBECode - * @Description 基于口令加密(Password Based Encryption, PBE),是一种对称加密算法。 - * 其特点是:口令由用户自己掌管,采用随机数(这里叫做盐)杂凑多重加密等方法保证数据的安全性。 - * PBE没有密钥概念,密钥在其他对称加密算法中是经过计算得出的,PBE则使用口令替代了密钥。 - * @Author victor zhang - * @Date 2016年7月20日 - */ -public class PBECoder { - public static final String KEY_ALGORITHM = "PBEWITHMD5andDES"; - - public static final int ITERATION_COUNT = 100; - - private Key key; - private byte[] salt; - - public PBECoder(String password) throws Exception { - this.salt = initSalt(); - this.key = initKey(password); - } - - public byte[] encrypt(byte[] plaintext) throws Exception { - PBEParameterSpec paramSpec = new PBEParameterSpec(salt, ITERATION_COUNT); - Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); - cipher.init(Cipher.ENCRYPT_MODE, key, paramSpec); - return cipher.doFinal(plaintext); - } - - public byte[] decrypt(byte[] ciphertext) throws Exception { - PBEParameterSpec paramSpec = new PBEParameterSpec(salt, ITERATION_COUNT); - Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); - cipher.init(Cipher.DECRYPT_MODE, key, paramSpec); - return cipher.doFinal(ciphertext); - } - - private byte[] initSalt() throws Exception { - SecureRandom secureRandom = new SecureRandom(); - return secureRandom.generateSeed(8); // 盐长度必须为8字节 - } - - private Key initKey(String password) throws Exception { - PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray()); - SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM); - return keyFactory.generateSecret(keySpec); - } - - public static void main(String[] args) throws Exception { - PBECoder encode = new PBECoder("123456"); - String message = "Hello World!"; - byte[] ciphertext = encode.encrypt(message.getBytes()); - byte[] plaintext = encode.decrypt(ciphertext); - - System.out.println("原文:" + message); - System.out.println("密文:" + Base64.encodeBase64String(ciphertext)); - System.out.println("明文:" + new String(plaintext)); - } - - public static void test1() throws Exception { - - // 产生盐 - SecureRandom secureRandom = new SecureRandom(); - byte[] salt = secureRandom.generateSeed(8); // 盐长度必须为8字节 - - // 产生Key - String password = "123456"; - PBEKeySpec keySpec = new PBEKeySpec(password.toCharArray()); - SecretKeyFactory keyFactory = SecretKeyFactory.getInstance(KEY_ALGORITHM); - SecretKey secretKey = keyFactory.generateSecret(keySpec); - - - PBEParameterSpec paramSpec = new PBEParameterSpec(salt, ITERATION_COUNT); - Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec); - - byte[] plaintext = "Hello World".getBytes(); - byte[] ciphertext = cipher.doFinal(plaintext); - new String (ciphertext); - } - -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/RSACoder.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/RSACoder.java deleted file mode 100644 index 6e682619..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/RSACoder.java +++ /dev/null @@ -1,139 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.encrypt; - -import org.apache.commons.codec.binary.Base64; - -import javax.crypto.Cipher; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.Signature; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; - -/** - * @Title RSACoder - * @Description RSA安全编码:非对称加密算法。它既可以用来加密、解密,也可以用来做数字签名 - * @Author victor zhang - * @Date 2016年7月20日 - */ -public class RSACoder { - public final static String KEY_ALGORITHM = "RSA"; - public final static String SIGN_ALGORITHM = "MD5WithRSA"; - public enum RsaSignTypeEn { - MD2WithRSA, MD5WithRSA, SHA1WithRSA - } - private KeyPair keyPair; - - public RSACoder() throws Exception { - this.keyPair = initKeyPair(); - } - - public byte[] encryptByPublicKey(byte[] plaintext, byte[] key) throws Exception { - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(key); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); - PublicKey publicKey = keyFactory.generatePublic(keySpec); - Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); - cipher.init(Cipher.ENCRYPT_MODE, publicKey); - return cipher.doFinal(plaintext); - } - - public byte[] encryptByPrivateKey(byte[] plaintext, byte[] key) throws Exception { - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); - PrivateKey privateKey = keyFactory.generatePrivate(keySpec); - Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); - cipher.init(Cipher.ENCRYPT_MODE, privateKey); - return cipher.doFinal(plaintext); - } - - public byte[] decryptByPublicKey(byte[] ciphertext, byte[] key) throws Exception { - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(key); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); - PublicKey publicKey = keyFactory.generatePublic(keySpec); - Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); - cipher.init(Cipher.DECRYPT_MODE, publicKey); - return cipher.doFinal(ciphertext); - } - - public byte[] decryptByPrivateKey(byte[] ciphertext, byte[] key) throws Exception { - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(key); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); - PrivateKey privateKey = keyFactory.generatePrivate(keySpec); - Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); - cipher.init(Cipher.DECRYPT_MODE, privateKey); - return cipher.doFinal(ciphertext); - } - - public byte[] signature(byte[] data, byte[] privateKey, RsaSignTypeEn type) throws Exception { - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKey); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); - PrivateKey key = keyFactory.generatePrivate(keySpec); - - Signature signature = Signature.getInstance(type.name()); - signature.initSign(key); - signature.update(data); - return signature.sign(); - } - - public boolean verify(byte[] data, byte[] publicKey, byte[] sign, RsaSignTypeEn type) throws Exception { - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); - PublicKey key = keyFactory.generatePublic(keySpec); - - Signature signature = Signature.getInstance(type.name()); - signature.initVerify(key); - signature.update(data); - return signature.verify(sign); - } - - public byte[] getPublicKey() { - return keyPair.getPublic().getEncoded(); - } - - public byte[] getPrivateKey() { - return keyPair.getPrivate().getEncoded(); - } - - private KeyPair initKeyPair() throws Exception { - // KeyPairGenerator类用于生成公钥和私钥对,基于RSA算法生成对象 - KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM); - // 初始化密钥对生成器,密钥大小为1024位 - keyPairGen.initialize(1024); - // 生成一个密钥对 - return keyPairGen.genKeyPair(); - } - - public static void main(String[] args) throws Exception { - String msg = "Hello World!"; - RSACoder coder = new RSACoder(); - // 私钥加密,公钥解密 - byte[] ciphertext = coder.encryptByPrivateKey(msg.getBytes("UTF8"), coder.keyPair.getPrivate().getEncoded()); - byte[] plaintext = coder.decryptByPublicKey(ciphertext, coder.keyPair.getPublic().getEncoded()); - - // 公钥加密,私钥解密 - byte[] ciphertext2 = coder.encryptByPublicKey(msg.getBytes(), coder.keyPair.getPublic().getEncoded()); - byte[] plaintext2 = coder.decryptByPrivateKey(ciphertext2, coder.keyPair.getPrivate().getEncoded()); - - byte[] sign = coder.signature(msg.getBytes(), coder.getPrivateKey(), RsaSignTypeEn.SHA1WithRSA); - boolean flag = coder.verify(msg.getBytes(), coder.getPublicKey(), sign, RsaSignTypeEn.SHA1WithRSA); - String result = flag ? "数字签名匹配" : "数字签名不匹配"; - - System.out.println("原文:" + msg); - System.out.println("公钥:" + Base64.encodeBase64URLSafeString(coder.keyPair.getPublic().getEncoded())); - System.out.println("私钥:" + Base64.encodeBase64URLSafeString(coder.keyPair.getPrivate().getEncoded())); - - System.out.println("============== 私钥加密,公钥解密 =============="); - System.out.println("密文:" + Base64.encodeBase64URLSafeString(ciphertext)); - System.out.println("明文:" + new String(plaintext)); - - System.out.println("============== 公钥加密,私钥解密 =============="); - System.out.println("密文:" + Base64.encodeBase64URLSafeString(ciphertext2)); - System.out.println("明文:" + new String(plaintext2)); - - System.out.println("============== 数字签名 =============="); - System.out.println("数字签名:" + Base64.encodeBase64URLSafeString(sign)); - System.out.println("验证结果:" + result); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/DownloadPolicy.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/DownloadPolicy.java deleted file mode 100644 index 6a8eb1ca..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/DownloadPolicy.java +++ /dev/null @@ -1,112 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.sample; - -import org.apache.commons.lang3.StringUtils; - -import java.io.Serializable; - -/** - * Created by victor zhang on 2016/7/22. - */ -public class DownloadPolicy implements Serializable { - /** - * - */ - private static final long serialVersionUID = 1159078308838844309L; - - /** - * 文件ID - */ - private Long fileId; - - /** - * 下载文件所属空间 - */ - private String namespace; - - /** - * 令牌有效的截止时间。用Unix时间表示。单位秒 - */ - private Long deadline; - - /** - * 允许下载文件类型 - */ - private String fType = UploadConstant.SUPPORT_FILE_TYPE; - - /** - * 操作类型 - */ - private final String operate = UploadConstant.TOKEN_DOWNLOAD; - - /** - * @param policy - * @return boolean - * @title isValid - * @description 判断数据是否有效 - * @author victor zhang - * @date 2016年7月22日 - */ - public static boolean isValid(DownloadPolicy policy) { - // 检查必要项是否为空 - if (StringUtils.isBlank(policy.namespace) || null == policy.deadline) { - return false; - } - - // 令牌截止时间不能是已过期时间 - long life = policy.deadline - System.currentTimeMillis() / 1000; - if (life <= 0) { - return false; - } - - // 检查文件类型 - if (StringUtils.isBlank(policy.fType)) { - return false; - } - String[] requestTypes = policy.fType.split("\\|"); - for (String item : requestTypes) { - if (!UploadConstant.SUPPORT_FILE_TYPE_SET.contains(item)) { - return false; - } - } - - return true; - } - - - public Long getFileId() { - return fileId; - } - - public void setFileId(Long fileId) { - this.fileId = fileId; - } - - public String getNamespace() { - return namespace; - } - - public void setNamespace(String namespace) { - this.namespace = namespace; - } - - public Long getDeadline() { - return deadline; - } - - public void setDeadline(Long e) { - this.deadline = e; - } - - public String getfType() { - return fType; - } - - public void setfType(String fType) { - this.fType = fType; - } - - public String getOperate() { - return operate; - } - -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/ModifyPolicy.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/ModifyPolicy.java deleted file mode 100644 index 7783fc56..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/ModifyPolicy.java +++ /dev/null @@ -1,95 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.sample; - -import org.apache.commons.lang3.StringUtils; - -import java.io.Serializable; - -/** - * Created by victor zhang on 2016/7/22. - */ -public class ModifyPolicy implements Serializable { - private static final long serialVersionUID = -547821705196510884L; - - /** - * 下载文件所属空间 - */ - private String namespace; - - /** - * 令牌有效的截止时间。用Unix时间表示。单位秒 - */ - private Long deadline; - - /** - * 允许下载文件类型 - */ - private String fType = UploadConstant.SUPPORT_FILE_TYPE; - - /** - * 操作类型 - */ - private final String operate = UploadConstant.TOKEN_MODIFY; - - /** - * @param policy - * @return boolean - * @title isValid - * @description 判断数据是否有效 - * @author victor zhang - * @date 2016年7月22日 - */ - public static boolean isValid(ModifyPolicy policy) { - // 检查必要项是否为空 - if (StringUtils.isBlank(policy.namespace) || null == policy.deadline) { - return false; - } - - // 令牌截止时间不能是已过期时间 - long life = policy.deadline - System.currentTimeMillis() / 1000; - if (life <= 0) { - return false; - } - - // 检查文件类型 - if (StringUtils.isBlank(policy.fType)) { - return false; - } - String[] requestTypes = policy.fType.split("\\|"); - for (String item : requestTypes) { - if (!UploadConstant.SUPPORT_FILE_TYPE_SET.contains(item)) { - return false; - } - } - - return true; - } - - public String getNamespace() { - return namespace; - } - - public void setNamespace(String namespace) { - this.namespace = namespace; - } - - public Long getDeadline() { - return deadline; - } - - public void setDeadline(Long deadline) { - this.deadline = deadline; - } - - public String getfType() { - return fType; - } - - public void setfType(String fType) { - this.fType = fType; - } - - public String getOperate() { - return operate; - } - -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/TokenUtil.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/TokenUtil.java deleted file mode 100644 index 1e408358..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/TokenUtil.java +++ /dev/null @@ -1,157 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.sample; - -import io.github.dunwu.javaee.oss.encode.digest.HmacCoder; -import org.apache.commons.codec.binary.Base64; - -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONObject; - -/** - * 令牌工具类 token = accessKey + digest + policy digest = HmacSHA256(policy, secretKey) - */ -public final class TokenUtil { - private static final String SEPARATOR = "|||"; - private static final String ACCESS_KEY = "ACCESS_KEY"; - private static final String SECRET_KEY = "SECRET_KEY"; - - public enum TokenTypeEN { - UPLOAD, - DOWNLOAD, - MODIFY - } - - public static String getToken(byte[] policy, byte[] accessKey, byte[] secretKey, String tokenType) - throws Exception { - JSONObject policyJson = JSONObject.parseObject(new String(policy)); - - // 检查令牌是否符合系统规格 - if (TokenTypeEN.UPLOAD.name().equalsIgnoreCase(tokenType)) { - UploadPolicy uploadPolicy = JSONObject.toJavaObject(policyJson, UploadPolicy.class); - if (!UploadPolicy.isValid(uploadPolicy)) { - throw new Exception("The policy is not conform to the specifications of the system."); - } - } else if (TokenTypeEN.DOWNLOAD.name().equalsIgnoreCase(tokenType)) { - DownloadPolicy downloadPolicy = JSONObject.toJavaObject(policyJson, DownloadPolicy.class); - if (!DownloadPolicy.isValid(downloadPolicy)) { - throw new Exception("The policy is not conform to the specifications of the system."); - } - } else if (TokenTypeEN.MODIFY.name().equalsIgnoreCase(tokenType)) { - ModifyPolicy modifyPolicy = JSONObject.toJavaObject(policyJson, ModifyPolicy.class); - if (!ModifyPolicy.isValid(modifyPolicy)) { - throw new Exception("The policy is not conform to the specifications of the system."); - } - } else { - throw new Exception("Required token is not supported."); - } - - // 根据secretKey和policy生成消息摘要(使用基于口令编码的HmacSHA256算法) - byte[] digest = HmacCoder.encode(policy, secretKey, HmacCoder.HmacTypeEn.HmacSHA512); - - // Token = AccessKey::Digest::Policy。数据拼接之前都要做URL安全的Base64编码 - String token = Base64.encodeBase64URLSafeString(accessKey) + SEPARATOR - + Base64.encodeBase64URLSafeString(digest) + SEPARATOR + Base64.encodeBase64URLSafeString(policy); - - return token; - } - - public static Object getPolicy(String policyBase64, String digestBase64, byte[] secretKey, String tokenType) - throws Exception { - String policy = new String(Base64.decodeBase64(policyBase64)); - - // 根据secretKey和policy验证摘要是否被篡改 - byte[] checkDigest = HmacCoder.encode(policy.getBytes(), secretKey, HmacCoder.HmacTypeEn.HmacSHA512); - String checkDigestBase64 = Base64.encodeBase64URLSafeString(checkDigest); - if (!checkDigestBase64.equals(digestBase64)) { - throw new SecurityException("The policy is not match to the digest."); - } - - JSONObject json = JSONObject.parseObject(policy); - if (TokenTypeEN.UPLOAD.name().equalsIgnoreCase(tokenType)) { - return JSONObject.toJavaObject(json, UploadPolicy.class); - } else if (TokenTypeEN.DOWNLOAD.name().equalsIgnoreCase(tokenType)) { - return JSONObject.toJavaObject(json, DownloadPolicy.class); - } else if (TokenTypeEN.MODIFY.name().equalsIgnoreCase(tokenType)) { - return JSONObject.toJavaObject(json, ModifyPolicy.class); - } - - return null; - } - - private static String initUploadPolicy() { - long deadline = System.currentTimeMillis() / 1000 + 3600 * 7; - UploadPolicy policy = new UploadPolicy(); - policy.setNamespace("namespace"); - policy.setDeadline(deadline); - policy.setfType("pdf"); - return JSON.toJSONString(policy); - } - - private static String initDownladPolicy() { - long deadline = System.currentTimeMillis() / 1000 + 3600 * 7; - DownloadPolicy policy = new DownloadPolicy(); - policy.setFileId(5748527L); - policy.setNamespace("namespace"); - policy.setDeadline(deadline); - policy.setfType("pdf"); - return JSON.toJSONString(policy); - } - - private static String initModifyPolicy() { - long deadline = System.currentTimeMillis() / 1000 + 3600 * 7; - ModifyPolicy policy = new ModifyPolicy(); - policy.setNamespace("namespace"); - policy.setDeadline(deadline); - policy.setfType("png"); - return JSON.toJSONString(policy); - } - - public static String testGetToken(String tokenType) throws Exception { - String policy = null; - if (TokenTypeEN.UPLOAD.name().equals(tokenType)) { - policy = initUploadPolicy(); - } else if (TokenTypeEN.DOWNLOAD.name().equals(tokenType)) { - policy = initDownladPolicy(); - } else if (TokenTypeEN.MODIFY.name().equals(tokenType)) { - policy = initModifyPolicy(); - } - String policyBase64 = Base64.encodeBase64URLSafeString(policy.getBytes()); - String accessKeyBase64 = Base64.encodeBase64URLSafeString(ACCESS_KEY.getBytes()); - - System.out.println(String.format("============== %s ==============", tokenType)); - System.out.println("policy:" + policy); - System.out.println("policyBase64:" + policyBase64); - System.out.println("accessKeyBase64:" + accessKeyBase64); - - String token = getToken(policy.getBytes(), ACCESS_KEY.getBytes(), SECRET_KEY.getBytes(), tokenType); - System.out.println("Token:" + token); - return token; - } - - public static Object testGetPolicy(String token, String tokenType) throws Exception { - String[] params = token.split(SEPARATOR); - // byte[] accessKey = Base64.decodeBase64(params[0]); - String digestBase64 = params[1]; - String policyBase64 = params[2]; - - if (TokenTypeEN.UPLOAD.name().equals(tokenType)) { - UploadPolicy policy = (UploadPolicy) getPolicy(policyBase64, digestBase64, SECRET_KEY.getBytes(), - tokenType); - return policy; - } else if (TokenTypeEN.DOWNLOAD.name().equals(tokenType)) { - DownloadPolicy policy = (DownloadPolicy) getPolicy(policyBase64, digestBase64, SECRET_KEY.getBytes(), - tokenType); - return policy; - } else if (TokenTypeEN.MODIFY.name().equals(tokenType)) { - ModifyPolicy policy = (ModifyPolicy) getPolicy(policyBase64, digestBase64, SECRET_KEY.getBytes(), - tokenType); - return policy; - } else { - return null; - } - - } - - public static void main(String[] args) throws Exception { - testGetToken(TokenTypeEN.DOWNLOAD.name()); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/UploadConstant.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/UploadConstant.java deleted file mode 100644 index c317737e..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/UploadConstant.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.sample; - -import org.apache.commons.collections.CollectionUtils; - -import java.util.HashSet; -import java.util.Set; - -/** - * Created by victor zhang on 2016/7/26. - */ -public class UploadConstant { - - public static final long FSIZE_MIN = 1024L; // 1KB - public static final long FSIZE_MAX = 5 * 1024 * 1024L; // 5MB - public static final long FSIZE_MIN_DEFAULT = 1024L; // 1KB - public static final long FSIZE_MAX_DEFAULT = 2 * 1024 * 1024L; // 2MB - - public static final String TOKEN_UPLOAD = "UPLOAD"; - public static final String TOKEN_DOWNLOAD = "DOWNLOAD"; - public static final String TOKEN_MODIFY = "MODIFY"; - - public static final String SUPPORT_FILE_TYPE = "pdf|doc|docx|png|jpg|jpeg|gif"; - public static final Set SUPPORT_FILE_TYPE_SET; - static { - SUPPORT_FILE_TYPE_SET = new HashSet(); - String[] supportedTypes = UploadConstant.SUPPORT_FILE_TYPE.split("\\|"); - CollectionUtils.addAll(SUPPORT_FILE_TYPE_SET, supportedTypes); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/UploadPolicy.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/UploadPolicy.java deleted file mode 100644 index c1ba2d7e..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/UploadPolicy.java +++ /dev/null @@ -1,153 +0,0 @@ -package io.github.dunwu.javaee.oss.encode.sample; - -import org.apache.commons.lang3.StringUtils; - -import java.io.Serializable; - -/** - * Created by victor zhang on 2016/7/22. - */ -public class UploadPolicy implements Serializable { - private static final long serialVersionUID = 8289239747395166646L; - - /** - * 上传文件所属空间 - */ - private String namespace; - - /** - * 令牌有效的截止时间。用Unix时间表示。单位秒 - */ - private Long deadline; - - /** - * 文件上传后保留时间。单位天。默认值-1,表示永久保留 - */ - private Integer deleteAfterDays = -1; - - /** - * 上传文件大小上限。单位Byte。 - */ - private Long fsizeMin = UploadConstant.FSIZE_MIN_DEFAULT; - - /** - * 上传文件大小上限。单位Byte。 - */ - private Long fsizeMax = UploadConstant.FSIZE_MAX_DEFAULT; - - /** - * 允许上传文件类型 - */ - private String fType = UploadConstant.SUPPORT_FILE_TYPE; - - /** - * 是否认证令牌。0表示认证,1表示不认证。默认为0 - */ - private Integer verifyToken = 0; - - /** - * 操作类型 - */ - private final String operate = UploadConstant.TOKEN_UPLOAD; - - /** - * @param policy - * @return boolean - * @title isValid - * @description 判断数据是否有效 - * @author victor zhang - * @date 2016年7月22日 - */ - public static boolean isValid(UploadPolicy policy) { - // 检查必要项是否为空 - if (StringUtils.isBlank(policy.namespace) || null == policy.deadline) { - return false; - } - - // 令牌截止时间不能是已过期时间 - long life = policy.deadline - System.currentTimeMillis() / 1000; - if (life <= 0) { - return false; - } - - // 判断文件大小的上限、下限是否符合系统规格 - if (policy.fsizeMin > policy.fsizeMax || policy.fsizeMin < UploadConstant.FSIZE_MIN - || policy.fsizeMax > UploadConstant.FSIZE_MAX) { - return false; - } - - // 检查文件类型 - if (StringUtils.isBlank(policy.fType)) { - return false; - } - String[] requestTypes = policy.fType.split("\\|"); - for (String item : requestTypes) { - if (!UploadConstant.SUPPORT_FILE_TYPE_SET.contains(item)) { - return false; - } - } - - return true; - } - - public String getNamespace() { - return namespace; - } - - public void setNamespace(String namespace) { - this.namespace = namespace; - } - - public Long getDeadline() { - return deadline; - } - - public void setDeadline(Long deadline) { - this.deadline = deadline; - } - - public Integer getDeleteAfterDays() { - return deleteAfterDays; - } - - public void setDeleteAfterDays(Integer deleteAfterDays) { - this.deleteAfterDays = deleteAfterDays; - } - - public Long getFsizeMin() { - return fsizeMin; - } - - public void setFsizeMin(Long fsizeMin) { - this.fsizeMin = fsizeMin; - } - - public Long getFsizeMax() { - return fsizeMax; - } - - public void setFsizeMax(Long fsizeMax) { - this.fsizeMax = fsizeMax; - } - - public String getfType() { - return fType; - } - - public void setfType(String fType) { - this.fType = fType; - } - - public Integer getVerifyToken() { - return verifyToken; - } - - public void setVerifyToken(Integer verifyToken) { - this.verifyToken = verifyToken; - } - - public String getOperate() { - return operate; - } - -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/html/CnblogParser.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/html/CnblogParser.java deleted file mode 100644 index 27d1ff44..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/html/CnblogParser.java +++ /dev/null @@ -1,52 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.html; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import java.io.IOException; - -/** - * 博客园博文抓取工具 - * - * @author Victor Zhang - * @date 2016/11/8. - */ -public class CnblogParser { - private static final String BLOG_URL = "http://www.cnblogs.com/jingmoxukong/"; - - public static void main(String[] args) throws Exception { - int total = 0; - for (int page = 0; page <= 16; page++) { - total += printAllTitleInPage(BLOG_URL, page); - } - System.out.println("总文章数:" + total); - } - - /** - * 获取指定页HTML 文档指定的body - * - * @throws IOException - */ - private static int printAllTitleInPage(String blogUrl, int page) throws IOException { - int count = 0; - Document doc = Jsoup.connect(blogUrl + "default.html?page=" + page).get(); - Elements postTitles = doc.body().getElementsByClass("postTitle"); - for (Element postTitle : postTitles) { - Elements links = postTitle.getElementsByTag("a"); - for (Element link : links) { - if (link.hasText()) { - System.out.println(link.text()); - System.out.println(link.attr("href")); - count++; - } - } - } - return count; - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/html/XiamiParser.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/html/XiamiParser.java deleted file mode 100644 index b6b1109d..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/html/XiamiParser.java +++ /dev/null @@ -1,104 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.html; - -import org.apache.commons.collections.CollectionUtils; -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; - -import java.io.IOException; -import java.util.HashSet; -import java.util.Set; - -/** - * 获取虾米网我的音乐中所有曲目 - * - * @author Victor Zhang - * @date 2016/11/8. - */ -public class XiamiParser { - private static final String BLOG_URL = "http://www.xiami.com/space/lib-song/u/5524914/page"; - - public static void main(String[] args) throws Exception { - XiamiParser parser = new XiamiParser(); - Set allSongInfos = new HashSet<>(); - for (int page = 0; page <= 65; page++) { - Set curPageSongs = parser.getSongInfoSet(BLOG_URL, page); - CollectionUtils.addAll(allSongInfos, curPageSongs.iterator()); - } - System.out.println("总歌曲数目:" + allSongInfos.size()); - parser.printAllSongInfo(allSongInfos); - } - - /** - * 获取指定页HTML 文档指定的body - * - * @throws IOException - */ - private Set getSongInfoSet(String blogUrl, int page) throws IOException { - Set songList = new HashSet(); - Document doc = Jsoup.connect(blogUrl + "/" + page).get(); - Elements postTitles = doc.body().getElementsByClass("track_list"); - for (Element postTitle : postTitles) { - Elements songs = postTitle.getElementsByTag("tr"); - for (Element song : songs) { - Elements name = song.getElementsByClass("song_name"); - for (Element link : name) { - SongInfo songinfo = new SongInfo(); - songinfo.setName(link.child(0).text()); - Elements artistName = link.getElementsByClass("artist_name"); - songinfo.setArtist(artistName.get(0).text()); - songList.add(songinfo); - } - } - } - return songList; - } - - private void printAllSongInfo(Set songs) { - for (SongInfo song : songs) { - System.out.println(song.getName() + "\t" + song.getArtist()); - } - } - - public class SongInfo { - private String name; - private String artist; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getArtist() { - return artist; - } - - public void setArtist(String artist) { - this.artist = artist; - } - - @Override - public boolean equals(Object obj) { - if (obj.getClass() != SongInfo.class) - return false; - SongInfo external = (SongInfo) obj; - if (external.getName().equals(this.getName()) && external.getArtist().equals(this.getArtist())) - return true; - else - return false; - } - - @Override - public int hashCode() { - return 1; - } - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/image/ImageUtil.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/image/ImageUtil.java deleted file mode 100644 index d9557a4d..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/image/ImageUtil.java +++ /dev/null @@ -1,155 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.image; - -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import javax.imageio.ImageIO; - -import io.github.dunwu.javaee.oss.image.dto.ImageParamDTO; -import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import net.coobird.thumbnailator.Thumbnails; -import net.coobird.thumbnailator.geometry.Positions; -import net.sf.jmimemagic.Magic; -import net.sf.jmimemagic.MagicException; -import net.sf.jmimemagic.MagicMatch; -import net.sf.jmimemagic.MagicMatchNotFoundException; -import net.sf.jmimemagic.MagicParseException; - -/** - * 图片工具类 - * - * @author Victor Zhang - * @date 2017/1/16. - */ -public class ImageUtil { - private static final Logger logger = LoggerFactory.getLogger(ImageUtil.class); - - public static void toFile(String oldFile, String newFile, ImageParamDTO params) throws IOException { - if (StringUtils.isBlank(oldFile) || StringUtils.isBlank(newFile)) { - logger.error("原文件名或目标文件名为空"); - return; - } - Thumbnails.Builder builder = Thumbnails.of(oldFile); - fillBuilderWithParams(builder, params); - if (null == builder) { - return; - } - builder.toFile(newFile); - } - - public static BufferedImage toBufferedImage(String oldFile, ImageParamDTO params) throws IOException { - if (StringUtils.isBlank(oldFile)) { - logger.error("原文件名或目标文件名为空"); - return null; - } - Thumbnails.Builder builder = Thumbnails.of(oldFile); - fillBuilderWithParams(builder, params); - if (null == builder) { - return null; - } - return builder.asBufferedImage(); - } - - public static OutputStream toOutputStream(InputStream input, OutputStream output, ImageParamDTO params) - throws IOException { - Thumbnails.Builder builder = Thumbnails.of(input); - if (null == builder) { - return null; - } - - try { - fillBuilderWithParams(builder, params); - builder.toOutputStream(output); - } catch (IOException e) { - logger.error("图片处理失败\n" + e.getMessage()); - throw e; - } - - return output; - } - - private static void fillBuilderWithParams(Thumbnails.Builder builder, ImageParamDTO params) throws IOException { - if (null == params) { - throw new IOException("图片格式化参数为空"); - } - - // 按照一定规则改变原图尺寸 - if (null != params.getWidth() && null != params.getHeight()) { - builder.size(params.getWidth(), params.getHeight()); - } else if (null != params.getXscale() && null != params.getYscale()) { - builder.scale(params.getXscale(), params.getYscale()); - } else if (null != params.getScale()) { - builder.scale(params.getScale(), params.getScale()); - } else { - builder.scale(1.0); // 如果没有设置尺寸参数,默认大小为原图大小 - } - - // 设置图片旋转角度 - if (null != params.getRotate()) { - builder.rotate(params.getRotate()); - } - - // 设置图片压缩质量 - if (null != params.getQuality()) { - builder.outputQuality(params.getQuality()); - } - - // 设置图片格式 - if (StringUtils.isNotBlank(params.getFormat())) { - builder.outputFormat(params.getFormat()); - } - - // 设置水印 - ImageParamDTO.WaterMark waterMark = params.getWaterMark(); - if (null != waterMark) { - Positions pos = ImageParamDTO.getPostionsByCode(waterMark.getPosition()); - if (null == pos) { - throw new IOException("请检查水印图片的位置类型,有效范围在[1,9]"); - } - BufferedImage bufferedImage = ImageIO.read(new FileInputStream(waterMark.getImage())); - builder.watermark(pos, bufferedImage, waterMark.getOpacity()); - } - } - - /** - * 获取文件的 ContentType - * - * @param content - * @return - * @throws MagicParseException - * @throws MagicException - * @throws MagicMatchNotFoundException - */ - public static String getContentType(byte[] content) - throws MagicParseException, MagicException, MagicMatchNotFoundException { - MagicMatch match = Magic.getMagicMatch(content); - return match.getMimeType(); - } - - public static final InputStream bytes2InputStream(byte[] buf) { - return new ByteArrayInputStream(buf); - } - - public static final byte[] inputStream2bytes(InputStream inStream) throws IOException { - ByteArrayOutputStream swapStream = new ByteArrayOutputStream(); - byte[] buff = new byte[100]; - int rc = 0; - while ((rc = inStream.read(buff, 0, 100)) > 0) { - swapStream.write(buff, 0, rc); - } - byte[] in2b = swapStream.toByteArray(); - return in2b; - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/image/QRCodeUtil.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/image/QRCodeUtil.java deleted file mode 100644 index 8ca45307..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/image/QRCodeUtil.java +++ /dev/null @@ -1,67 +0,0 @@ -package io.github.dunwu.javaee.oss.image; - -import java.awt.image.BufferedImage; -import java.io.FileInputStream; -import java.io.IOException; -import java.nio.file.FileSystems; -import java.nio.file.Path; - -import javax.imageio.ImageIO; - -import com.google.zxing.Binarizer; -import com.google.zxing.BinaryBitmap; -import com.google.zxing.LuminanceSource; -import com.google.zxing.MultiFormatReader; -import com.google.zxing.MultiFormatWriter; -import com.google.zxing.Result; -import com.google.zxing.WriterException; -import com.google.zxing.client.j2se.BufferedImageLuminanceSource; -import com.google.zxing.client.j2se.MatrixToImageWriter; -import com.google.zxing.common.BitMatrix; -import com.google.zxing.common.HybridBinarizer; - -import io.github.dunwu.javaee.oss.image.dto.BarcodeParamDTO; - -/** - * 二维码工具类 - * - * @author Victor Zhang - * @date 2017/1/16. - */ -public class QRCodeUtil { - /** - * 创建一个qrcode图片 - * - * @param content 加密信息,建议使用json格式 - * @param paramDTO qrcode 参数 - * @throws WriterException - * @throws IOException - */ - public static void encode(String content, BarcodeParamDTO paramDTO) throws WriterException, IOException { - // 生成矩阵 - BitMatrix bitMatrix = new MultiFormatWriter().encode(content, paramDTO.getBarcodeFormat(), paramDTO.getWidth(), - paramDTO.getHeight(), paramDTO.getEncodeHints()); - Path path = FileSystems.getDefault().getPath(paramDTO.getFilepath()); - MatrixToImageWriter.writeToPath(bitMatrix, paramDTO.getImageFormat(), path);// 输出图像 - } - - /** - * 解析 qrcode 图片 - * - * @param paramDTO qrcode 参数 - * @return - */ - public static String decode(BarcodeParamDTO paramDTO) { - try { - BufferedImage bufferedImage = ImageIO.read(new FileInputStream(paramDTO.getFilepath())); - LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage); - Binarizer binarizer = new HybridBinarizer(source); - BinaryBitmap bitmap = new BinaryBitmap(binarizer); - Result result = new MultiFormatReader().decode(bitmap, paramDTO.getDecodeHints()); - return result.getText(); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/image/dto/BarcodeParamDTO.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/image/dto/BarcodeParamDTO.java deleted file mode 100644 index ee532eba..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/image/dto/BarcodeParamDTO.java +++ /dev/null @@ -1,81 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.image.dto; - -import com.google.zxing.BarcodeFormat; -import com.google.zxing.DecodeHintType; -import com.google.zxing.EncodeHintType; - -import java.util.Map; - -/** - * @author Victor Zhang - * @date 2017/1/17. - */ -public class BarcodeParamDTO { - private Integer width; // 图像宽度 - private Integer height; // 图像高度 - private String filepath; // 图片路径 - private String imageFormat; // 图片文件格式 - private BarcodeFormat barcodeFormat; // 二维码形式 - private Map encodeHints; // 二维码的编码参数 - private Map decodeHints; // 二维码的解码参数 - - public Integer getWidth() { - return width; - } - - public void setWidth(Integer width) { - this.width = width; - } - - public Integer getHeight() { - return height; - } - - public void setHeight(Integer height) { - this.height = height; - } - - public String getFilepath() { - return filepath; - } - - public void setFilepath(String filepath) { - this.filepath = filepath; - } - - public String getImageFormat() { - return imageFormat; - } - - public void setImageFormat(String imageFormat) { - this.imageFormat = imageFormat; - } - - public BarcodeFormat getBarcodeFormat() { - return barcodeFormat; - } - - public void setBarcodeFormat(BarcodeFormat barcodeFormat) { - this.barcodeFormat = barcodeFormat; - } - - public Map getEncodeHints() { - return encodeHints; - } - - public void setEncodeHints(Map encodeHints) { - this.encodeHints = encodeHints; - } - - public Map getDecodeHints() { - return decodeHints; - } - - public void setDecodeHints(Map decodeHints) { - this.decodeHints = decodeHints; - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/image/dto/ImageParamDTO.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/image/dto/ImageParamDTO.java deleted file mode 100644 index aa0a53ce..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/image/dto/ImageParamDTO.java +++ /dev/null @@ -1,169 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.image.dto; - -import net.coobird.thumbnailator.geometry.Positions; - -import java.io.Serializable; - -/** - * @author Victor Zhang - * @date 2017/1/16. - */ -public class ImageParamDTO implements Serializable { - public static String[] IMAGE_TYPES = {"png", "jpg", "jpeg", "bmp", "gif"}; - - private Integer width; // 宽度 - private Integer height; // 高度 - private Double xscale; // 宽度比例 - private Double yscale; // 高度比例 - private Double scale; // 总比例,相当于将xscale和yscale都设为同比例 - private Double rotate; // 旋转角度,范围为[0.0, 360.0] - private Double quality; // 压缩质量,范围为[0.0, 1.0] - private String format; // 图片格式,支持jpg,jpeg,png,bmp,gif - private WaterMark waterMark; // 水印信息 - - public Integer getWidth() { - return width; - } - - public void setWidth(Integer width) { - this.width = width; - } - - public Integer getHeight() { - return height; - } - - public void setHeight(Integer height) { - this.height = height; - } - - public Double getXscale() { - return xscale; - } - - public void setXscale(Double xscale) { - this.xscale = xscale; - } - - public Double getYscale() { - return yscale; - } - - public void setYscale(Double yscale) { - this.yscale = yscale; - } - - public Double getScale() { - return scale; - } - - public void setScale(Double scale) { - this.scale = scale; - } - - public Double getRotate() { - return rotate; - } - - public void setRotate(Double rotate) { - this.rotate = rotate; - } - - public Double getQuality() { - return quality; - } - - public void setQuality(Double quality) { - this.quality = quality; - } - - public String getFormat() { - return format; - } - - public void setFormat(String format) { - this.format = format; - } - - public WaterMark getWaterMark() { - return waterMark; - } - - public void setWaterMark(WaterMark waterMark) { - this.waterMark = waterMark; - } - - /** - * 将位置类型码转换为 thumbnailator 可以识别的位置类型 - * - * @param code - * @return - */ - public static Positions getPostionsByCode(Integer code) { - switch (code) { - case 1: - return Positions.TOP_LEFT; - case 2: - return Positions.TOP_CENTER; - case 3: - return Positions.TOP_RIGHT; - case 4: - return Positions.CENTER_LEFT; - case 5: - return Positions.CENTER; - case 6: - return Positions.CENTER_RIGHT; - case 7: - return Positions.BOTTOM_LEFT; - case 8: - return Positions.BOTTOM_CENTER; - case 9: - return Positions.BOTTOM_RIGHT; - default: - return null; - } - } - - public static class WaterMark { - private Integer position; - private String image; - private Float opacity; - - public WaterMark() { - } - - public WaterMark(Integer position, String image, Float opacity) { - this.position = position; - this.image = image; - this.opacity = opacity; - } - - public Integer getPosition() { - return position; - } - - public void setPosition(Integer position) { - this.position = position; - } - - public String getImage() { - return image; - } - - public void setImage(String image) { - this.image = image; - } - - public Float getOpacity() { - return opacity; - } - - public void setOpacity(Float opacity) { - this.opacity = opacity; - } - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSHelloWorld.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSHelloWorld.java deleted file mode 100644 index 0a12bf56..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSHelloWorld.java +++ /dev/null @@ -1,145 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.jms; - -import org.apache.activemq.ActiveMQConnectionFactory; - -import javax.jms.Connection; -import javax.jms.DeliveryMode; -import javax.jms.Destination; -import javax.jms.ExceptionListener; -import javax.jms.JMSException; -import javax.jms.Message; -import javax.jms.MessageConsumer; -import javax.jms.MessageProducer; -import javax.jms.Session; -import javax.jms.TextMessage; - -/** - * @author Victor Zhang - * @date 2016/11/29. - */ -public class JMSHelloWorld { - public static void main(String[] args) throws Exception { - thread(new HelloWorldProducer(), false); - thread(new HelloWorldProducer(), false); - thread(new HelloWorldConsumer(), false); - Thread.sleep(1000); - thread(new HelloWorldConsumer(), false); - thread(new HelloWorldProducer(), false); - thread(new HelloWorldConsumer(), false); - thread(new HelloWorldProducer(), false); - Thread.sleep(1000); - thread(new HelloWorldConsumer(), false); - thread(new HelloWorldProducer(), false); - thread(new HelloWorldConsumer(), false); - thread(new HelloWorldConsumer(), false); - thread(new HelloWorldProducer(), false); - thread(new HelloWorldProducer(), false); - Thread.sleep(1000); - thread(new HelloWorldProducer(), false); - thread(new HelloWorldConsumer(), false); - thread(new HelloWorldConsumer(), false); - thread(new HelloWorldProducer(), false); - thread(new HelloWorldConsumer(), false); - thread(new HelloWorldProducer(), false); - thread(new HelloWorldConsumer(), false); - thread(new HelloWorldProducer(), false); - thread(new HelloWorldConsumer(), false); - thread(new HelloWorldConsumer(), false); - thread(new HelloWorldProducer(), false); - } - - public static void thread(Runnable runnable, boolean daemon) { - Thread brokerThread = new Thread(runnable); - brokerThread.setDaemon(daemon); - brokerThread.start(); - } - - public static class HelloWorldProducer implements Runnable { - public void run() { - try { - // Create a ConnectionFactory - ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("vm://localhost"); - - // Create a Connection - Connection connection = connectionFactory.createConnection(); - connection.start(); - - // Create a Session - Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); - - // Create the destination (Topic or Queue) - Destination destination = session.createQueue("TEST.FOO"); - - // Create a MessageProducer from the Session to the Topic or Queue - MessageProducer producer = session.createProducer(destination); - producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); - - // Create a messages - String text = "Hello world! From: " + Thread.currentThread().getName() + " : " + this.hashCode(); - TextMessage message = session.createTextMessage(text); - - // Tell the producer to send the message - System.out.println("Sent message: " + message.hashCode() + " : " + Thread.currentThread().getName()); - producer.send(message); - - // Clean up - session.close(); - connection.close(); - } catch (Exception e) { - System.out.println("Caught: " + e); - e.printStackTrace(); - } - } - } - - public static class HelloWorldConsumer implements Runnable, ExceptionListener { - public void run() { - try { - - // Create a ConnectionFactory - ActiveMQConnectionFactory connectionFactory = new ActiveMQConnectionFactory("vm://localhost"); - - // Create a Connection - Connection connection = connectionFactory.createConnection(); - connection.start(); - - connection.setExceptionListener(this); - - // Create a Session - Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE); - - // Create the destination (Topic or Queue) - Destination destination = session.createQueue("TEST.FOO"); - - // Create a MessageConsumer from the Session to the Topic or Queue - MessageConsumer consumer = session.createConsumer(destination); - - // Wait for a message - Message message = consumer.receive(1000); - - if (message instanceof TextMessage) { - TextMessage textMessage = (TextMessage) message; - String text = textMessage.getText(); - System.out.println("Received: " + text); - } else { - System.out.println("Received: " + message); - } - - consumer.close(); - session.close(); - connection.close(); - } catch (Exception e) { - System.out.println("Caught: " + e); - e.printStackTrace(); - } - } - - public synchronized void onException(JMSException ex) { - System.out.println("JMS Exception occured. Shutting down client."); - } - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSReceiver.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSReceiver.java deleted file mode 100644 index ecc0a98c..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSReceiver.java +++ /dev/null @@ -1,68 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.jms; - -import org.apache.activemq.ActiveMQConnection; -import org.apache.activemq.ActiveMQConnectionFactory; - -import javax.jms.Connection; -import javax.jms.ConnectionFactory; -import javax.jms.Destination; -import javax.jms.MessageConsumer; -import javax.jms.Session; -import javax.jms.TextMessage; - -/** - * 消息的消费者 - * - * @author Victor Zhang - * @date 2016/11/28. - */ -public class JMSReceiver { - public static void main(String[] args) { - // ConnectionFactory :连接工厂,JMS 用它创建连接 - ConnectionFactory connectionFactory; - // Connection :JMS 客户端到JMS Provider 的连接 - Connection connection = null; - // Session: 一个发送或接收消息的线程 - Session session; - // Destination :消息的目的地;消息发送给谁. - Destination destination; - // 消费者,消息接收者 - MessageConsumer consumer; - connectionFactory = new ActiveMQConnectionFactory( - ActiveMQConnection.DEFAULT_USER, - ActiveMQConnection.DEFAULT_PASSWORD, - ActiveMQConnection.DEFAULT_BROKER_URL); - try { - // 构造从工厂得到连接对象 - connection = connectionFactory.createConnection(); - // 启动 - connection.start(); - // 获取操作连接 - session = connection.createSession(Boolean.FALSE, Session.AUTO_ACKNOWLEDGE); - // 获取session注意参数值xingbo.xu-queue是一个服务器的queue,须在在ActiveMq的console配置 - destination = session.createQueue("FirstQueue"); - consumer = session.createConsumer(destination); - while (true) { - //设置接收者接收消息的时间,为了便于测试,这里谁定为100s - TextMessage message = (TextMessage) consumer.receive(100000); - if (null != message) { - System.out.println("收到消息" + message.getText()); - } else { - break; - } - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - try { - if (null != connection) - connection.close(); - } catch (Throwable ignore) { - } - } - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSSender.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSSender.java deleted file mode 100644 index 24296ac0..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSSender.java +++ /dev/null @@ -1,82 +0,0 @@ -package io.github.dunwu.javaee.oss.jms; - -import javax.jms.Connection; -import javax.jms.ConnectionFactory; -import javax.jms.DeliveryMode; -import javax.jms.Destination; -import javax.jms.MessageProducer; -import javax.jms.Session; -import javax.jms.TextMessage; - -import org.apache.activemq.ActiveMQConnection; -import org.apache.activemq.ActiveMQConnectionFactory; - -/** - * 消息的生产者 - * - * @author Victor Zhang - * @date 2016/11/28. - */ -public class JMSSender { - private static final int SEND_NUMBER = 4; - - public static void main(String[] args) { - // ConnectionFactory :连接工厂,JMS 用它创建连接 - ConnectionFactory connectionFactory; - // Connection :JMS 客户端到JMS Provider 的连接 - Connection connection = null; - // Session: 一个发送或接收消息的线程 - Session session; - // Destination :消息的目的地 - Destination destination; - // MessageProducer:消息发送者 - MessageProducer producer; - // TextMessage message; - // 构造ConnectionFactory实例对象,此处采用ActiveMq的实现jar - connectionFactory = new ActiveMQConnectionFactory(ActiveMQConnection.DEFAULT_USER, - ActiveMQConnection.DEFAULT_PASSWORD, ActiveMQConnection.DEFAULT_BROKER_URL); - try { - // 构造从工厂得到连接对象 - connection = connectionFactory.createConnection(); - // 启动 - connection.start(); - // 获取操作连接 - session = connection.createSession(Boolean.TRUE, Session.AUTO_ACKNOWLEDGE); - // 获取session注意参数值xingbo.xu-queue是一个服务器的queue,须在在ActiveMq的console配置 - destination = session.createQueue("FirstQueue"); - // 得到消息生成者【发送者】 - producer = session.createProducer(destination); - // 设置不持久化,此处学习,实际根据项目决定 - producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); - // 构造消息,此处写死,项目就是参数,或者方法获取 - sendMessage(session, producer); - session.commit(); - } catch (Exception e) { - e.printStackTrace(); - } finally { - try { - if (null != connection) - connection.close(); - } catch (Throwable ignore) { - } - } - } - - /** - * 发送消息 - * - * @param session - * @param messageProducer 消息生产者 - * @throws Exception - */ - public static void sendMessage(Session session, MessageProducer messageProducer) throws Exception { - for (int i = 0; i < SEND_NUMBER; i++) { - // 创建一条文本消息 - TextMessage message = session.createTextMessage("ActiveMQ 发送消息" + i); - System.out.println("发送消息:Activemq 发送消息" + i); - // 通过消息生产者发出消息 - messageProducer.send(message); - } - - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/logging/JclDemo.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/logging/JclDemo.java deleted file mode 100644 index a34e66c9..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/logging/JclDemo.java +++ /dev/null @@ -1,28 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.logging; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -/** - * 测试 common-logging + log4j 输出日志 - * - * @author Victor Zhang - * @date 2016/11/18. - */ -public class JclDemo { - private static final Log log = LogFactory.getLog(JclDemo.class); - - public static void main(String[] args) { - String msg = "print logging, 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"); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/logging/Slf4jDemo.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/logging/Slf4jDemo.java deleted file mode 100644 index 3c87626e..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/logging/Slf4jDemo.java +++ /dev/null @@ -1,27 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.logging; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * 测试 slf4j + logback 输出日志 - * - * @author Victor Zhang - * @date 2016/11/18. - */ -public class Slf4jDemo { - private static final Logger log = LoggerFactory.getLogger(Slf4jDemo.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"); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/ForwardMail.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/ForwardMail.java deleted file mode 100644 index 08e5141f..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/ForwardMail.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.github.dunwu.javaee.oss.mail; /** - * The Apache License 2.0 - * Copyright (c) 2016 victor zhang - */ - -import java.util.Date; -import java.util.Properties; - -import javax.mail.Folder; -import javax.mail.Message; -import javax.mail.Multipart; -import javax.mail.Session; -import javax.mail.Store; -import javax.mail.Transport; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeBodyPart; -import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeMultipart; - -/** - * @author victor zhang - * @date 2017/4/5. - */ -public class ForwardMail { - private static final String MAIL_SERVER_SMTP = "smtp.163.com"; - private static final String MAIL_SERVER_POP3 = "pop3.163.com"; - private static final String USER = "xxxxxx"; - private static final String PASSWORD = "******"; - private static final String MAIL_FROM = "xxxxxx@163.com"; - private static final String MAIL_TO = "xxxxxx@163.com"; - private static final String MAIL_CC = "xxxxxx@163.com"; - private static final String MAIL_BCC = "xxxxxx@163.com"; - - 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...."); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailConfigDTO.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailConfigDTO.java deleted file mode 100644 index 4f5444a6..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailConfigDTO.java +++ /dev/null @@ -1,66 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.mail; - -/** - * @author Victor Zhang - * @date 2016/12/23. - */ -public class MailConfigDTO { - private String smtpHost; - private String pop3Host; - private String mailDomain; - private String mailAccount; - private String mailPassword; - private String mailFromHost; - - public String getSmtpHost() { - return smtpHost; - } - - public void setSmtpHost(String smtpHost) { - this.smtpHost = smtpHost; - } - - public String getPop3Host() { - return pop3Host; - } - - public void setPop3Host(String pop3Host) { - this.pop3Host = pop3Host; - } - - public String getMailDomain() { - return mailDomain; - } - - public void setMailDomain(String mailDomain) { - this.mailDomain = mailDomain; - } - - public String getMailAccount() { - return mailAccount; - } - - public void setMailAccount(String mailAccount) { - this.mailAccount = mailAccount; - } - - public String getMailPassword() { - return mailPassword; - } - - public void setMailPassword(String mailPassword) { - this.mailPassword = mailPassword; - } - - public String getMailFromHost() { - return mailFromHost; - } - - public void setMailFromHost(String mailFromHost) { - this.mailFromHost = mailFromHost; - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailDTO.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailDTO.java deleted file mode 100644 index cb5d4de0..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailDTO.java +++ /dev/null @@ -1,95 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.mail; - -import javax.mail.internet.MimeMultipart; - -/** - * @author Victor Zhang - * @date 2016/12/22. - */ -public class MailDTO { - private String from; - private String to; // 邮件的收件人 - private String cc; // 邮件的抄送人 - private String bcc; // 邮件的密送人 - private String subject; // 邮件主题 - private String type; // text或html两种类型 - private String text; // 邮件文本内容 - private String charset; // 邮件编码类型(如UTF-8、GBK等) - private MimeMultipart content; - - public String getFrom() { - return from; - } - - public void setFrom(String from) { - this.from = from; - } - - public String getTo() { - return to; - } - - public void setTo(String to) { - this.to = to; - } - - public String getCc() { - return cc; - } - - public void setCc(String cc) { - this.cc = cc; - } - - public String getBcc() { - return bcc; - } - - public void setBcc(String bcc) { - this.bcc = bcc; - } - - public String getSubject() { - return subject; - } - - public void setSubject(String subject) { - this.subject = subject; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - - public String getCharset() { - return charset; - } - - public void setCharset(String charset) { - this.charset = charset; - } - - public MimeMultipart getContent() { - return content; - } - - public void setContent(MimeMultipart content) { - this.content = content; - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailUtil.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailUtil.java deleted file mode 100644 index 6f467d6a..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailUtil.java +++ /dev/null @@ -1,204 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.mail; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -import javax.mail.*; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeUtility; - -import org.apache.commons.lang3.StringUtils; - -/** - * @author Victor Zhang - * @date 2016/12/22. - */ -public class MailUtil { - private MailConfigDTO configDTO; - private static final String TYPE_TEXT = "text"; - private static final String TYPE_HTML = "html"; - - /** - * 以默认配置初始化邮件收发工具 - */ - public MailUtil() { - this.configDTO = initEmailConfig(); - } - - /** - * 以自定义配置初始化邮件收发工具 - */ - public MailUtil(MailConfigDTO configDTO) { - this.configDTO = configDTO; - } - - private MailConfigDTO initEmailConfig() { - MailConfigDTO configDTO = new MailConfigDTO(); - Properties p = new Properties(); - try { - p.load(MailUtil.class.getResourceAsStream("/mail/mail.properties")); - } catch (IOException e) { - e.printStackTrace(); - } - - configDTO.setSmtpHost(p.getProperty("smtp.host")); - configDTO.setPop3Host(p.getProperty("pop3.host")); - configDTO.setMailDomain(p.getProperty("mail.host")); - configDTO.setMailAccount(p.getProperty("mail.account")); - configDTO.setMailPassword(p.getProperty("mail.password")); - configDTO.setMailFromHost(configDTO.getMailAccount() + configDTO.getMailDomain()); - return configDTO; - } - - /** - * 发送邮件 - * - * @param info - * @throws MessagingException - */ - public void sendEmail(MailDTO info) throws MessagingException { - Properties props = new Properties(); - props.setProperty("mail.debug", "true"); - props.setProperty("mail.transport.protocol", "smtp"); - props.setProperty("mail.host", configDTO.getSmtpHost()); - props.setProperty("mail.smtp.auth", "true"); - - // 1、创建session - Session session = Session.getInstance(props); - - // 2、通过session得到transport对象 - Transport ts = session.getTransport(); - - // 3、连上邮件服务器 - ts.connect(configDTO.getSmtpHost(), configDTO.getMailAccount(), configDTO.getMailPassword()); - - // 4、创建邮件 - MimeMessage message = new MimeMessage(session); - if (!fillEmail(message, info)) { - return; - } - - // 5、发送邮件 - ts.sendMessage(message, message.getAllRecipients()); - ts.close(); - } - - private boolean fillEmail(MimeMessage message, MailDTO info) throws MessagingException { - return fillEmailHeader(message, info) && fillEmailBody(message, info); - } - - /** - * 填充邮件头 - */ - private boolean fillEmailHeader(MimeMessage message, MailDTO info) throws MessagingException { - // 邮件的发件人 - if (StringUtils.isNotBlank(configDTO.getMailFromHost())) { - message.setFrom(new InternetAddress(configDTO.getMailFromHost())); - } else { - System.out.println("发件人不能为空"); - return false; - } - - // 邮件的收件人 - if (StringUtils.isNotBlank(info.getTo())) { - message.setRecipient(Message.RecipientType.TO, new InternetAddress(info.getTo())); - } else { - System.out.println("收件人不能为空"); - return false; - } - - // 邮件的抄送人 - if (StringUtils.isNotBlank(info.getCc())) { - message.setRecipient(Message.RecipientType.CC, new InternetAddress(info.getCc())); - } - - // 邮件的密送人 - if (StringUtils.isNotBlank(info.getBcc())) { - message.setRecipient(Message.RecipientType.BCC, new InternetAddress(info.getBcc())); - } - - // 邮件的标题 - if (StringUtils.isNotBlank(info.getCharset())) { - message.setSubject(info.getSubject(), info.getCharset()); - } else { - message.setSubject(info.getSubject()); - } - - - return true; - } - - /** - * 填充邮件内容 - */ - private boolean fillEmailBody(MimeMessage message, MailDTO info) throws MessagingException { - if (StringUtils.isBlank(info.getType()) - || StringUtils.isBlank(info.getText())) { - return false; - } - - if (StringUtils.equals(info.getType(), TYPE_TEXT)) { - if (StringUtils.isNotBlank(info.getCharset())) { - message.setText(info.getText(), info.getCharset()); - } else { - message.setText(info.getText()); - } - - } else if (StringUtils.equals(info.getType(), TYPE_HTML)) { - String type = "text/html"; - if (StringUtils.isNotBlank(info.getCharset())) { - type += ";charset=" + info.getCharset(); - } - message.setContent(info.getText(), type); - } - - return true; - } - - public List receiveEmail() throws MessagingException, IOException { - // 创建一个有具体连接信息的Properties对象 - Properties prop = new Properties(); - prop.setProperty("mail.debug", "false"); - prop.setProperty("mail.store.protocol", "pop3"); - prop.setProperty("mail.pop3.host", configDTO.getPop3Host()); - - // 1、创建session - Session session = Session.getInstance(prop); - - // 2、通过session得到Store对象 - Store store = session.getStore(); - - // 3、连上邮件服务器 - store.connect(configDTO.getPop3Host(), configDTO.getMailAccount(), configDTO.getMailPassword()); - - // 4、获得邮箱内的邮件夹 - Folder folder = store.getFolder("inbox"); - folder.open(Folder.READ_ONLY); - - // 获得邮件夹Folder内的所有邮件Message对象 - Message[] messages = folder.getMessages(); - - List results = new ArrayList(); - for (int i = 0; i < messages.length; i++) { - MailDTO dto = new MailDTO(); - dto.setFrom(MimeUtility.decodeText(messages[i].getFrom()[0].toString())); - dto.setSubject(messages[i].getSubject()); - dto.setText(messages[i].getContent().toString()); - results.add(dto); - System.out.println("第 " + (i + 1) + "封邮件的主题:" + dto.getSubject()); - System.out.println("第 " + (i + 1) + "封邮件的发件人地址:" + dto.getFrom()); - // System.out.println("第 " + (i + 1) + "封邮件的内容:\n" + messages[i].getContent().toString()); - } - - // 5、关闭 - folder.close(false); - store.close(); - return results; - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendAttachmentMail.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendAttachmentMail.java deleted file mode 100644 index bdf90c26..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendAttachmentMail.java +++ /dev/null @@ -1,79 +0,0 @@ -package io.github.dunwu.javaee.oss.mail; /** - * The Apache License 2.0 - * Copyright (c) 2016 victor zhang - */ - -import javax.mail.Message; -import javax.mail.Session; -import javax.mail.Transport; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeBodyPart; -import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeMultipart; -import java.util.Properties; - -/** - * @author victor zhang - * @date 2017/4/5. - */ -public class SendAttachmentMail { - private static final String MAIL_SERVER_HOST = "smtp.163.com"; - private static final String USER = "xxxxxx"; - private static final String PASSWORD = "******"; - private static final String MAIL_FROM = "xxxxxx@163.com"; - private static final String MAIL_TO = "xxxxxx@163.com"; - private static final String MAIL_CC = "xxxxxx@163.com"; - private static final String MAIL_BCC = "xxxxxx@163.com"; - - 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(); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendTemplateMail.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendTemplateMail.java deleted file mode 100644 index 8ea8dc48..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendTemplateMail.java +++ /dev/null @@ -1,79 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.mail; - -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -import javax.mail.MessagingException; - -import io.github.dunwu.javaee.oss.template.VelocityUtil; -import org.apache.velocity.VelocityContext; - -/** - * @author Victor Zhang - * @date 2016/12/23. - * @see org.zp.javaee.tools.mail.MailUtil 注意:如果想要成功发送邮件,需要修改JavaParty项目 - * src\javaee\tools\src\main\resources\mail\mail.properties 中的 参数,请根据实际邮箱来配置。 - */ -public class SendTemplateMail { - private static final String DEFAULT_TO = "xxxxxx@163.com"; - - public static class Hyperlink { - private String link; - private String name; - - Hyperlink(String name, String link) { - this.name = name; - this.link = link; - } - - public String getLink() { - return link; - } - - public void setLink(String link) { - this.link = link; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - public static void main(String[] args) throws MessagingException { - VelocityContext context = new VelocityContext(); - context.put("name", "Victor Zhang"); - context.put("hint", "欢迎使用Velocity邮件模板:"); - - // 直接传入一个对象 - context.put("date", new Date()); - - // 传入一个Vector - Hyperlink item1 = new Hyperlink("百度首页", "https://www.baidu.com"); - Hyperlink item2 = new Hyperlink("网易首页", "http://www.163.com/"); - List list = new ArrayList<>(); - list.add(item1); - list.add(item2); - context.put("links", list); - context.put("logo", - "http://images.cnblogs.com/cnblogs_com/jingmoxukong/709053/o_%e6%94%bb%e5%9f%8e%e7%8b%ae2.png"); - - MailDTO info = new MailDTO(); - info.setTo(DEFAULT_TO); // 收件人邮箱 - info.setSubject("测试html邮件"); // 邮件主题 - info.setType("html"); - info.setCharset("utf-8"); - - info.setText(VelocityUtil.getMergeOutput(context, "template/mail.vm")); - MailUtil mailUtil = new MailUtil(); - mailUtil.sendEmail(info); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendTextMail.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendTextMail.java deleted file mode 100644 index 59c55125..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendTextMail.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.github.dunwu.javaee.oss.mail; /** - * The Apache License 2.0 - * Copyright (c) 2016 victor zhang - */ - -import javax.mail.Message; -import javax.mail.Session; -import javax.mail.Transport; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeMessage; -import java.util.Properties; - -/** - * @author victor zhang - * @date 2017/4/5. - */ -public class SendTextMail { - private static final String MAIL_SERVER_HOST = "smtp.163.com"; - private static final String USER = "xxxxxx"; - private static final String PASSWORD = "******"; - private static final String MAIL_FROM = "xxxxxx@163.com"; - private static final String MAIL_TO = "xxxxxx@163.com"; - private static final String MAIL_CC = "xxxxxx@163.com"; - private static final String MAIL_BCC = "xxxxxx@163.com"; - - 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(); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/StoreMail.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/StoreMail.java deleted file mode 100644 index e39cbcfb..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/StoreMail.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.github.dunwu.javaee.oss.mail; /** - * The Apache License 2.0 - * Copyright (c) 2016 victor zhang - */ - -import javax.mail.Folder; -import javax.mail.Message; -import javax.mail.Session; -import javax.mail.Store; -import java.util.Properties; - -/** - * @author victor zhang - * @date 2017/4/5. - */ -public class StoreMail { - - private static final String MAIL_SERVER_HOST = "pop3.163.com"; - private static final String USER = "xxxxxx"; - private static final String PASSWORD = "******"; - private static final String MAIL_FROM = "xxxxxx@163.com"; - private static final String MAIL_TO = "xxxxxx@163.com"; - private static final String MAIL_CC = "xxxxxx@163.com"; - private static final String MAIL_BCC = "xxxxxx@163.com"; - - -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(); -} -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/sendHtmlMail.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/sendHtmlMail.java deleted file mode 100644 index 74eae4ec..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/mail/sendHtmlMail.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.github.dunwu.javaee.oss.mail; /** - * The Apache License 2.0 - * Copyright (c) 2016 victor zhang - */ - -import javax.activation.DataHandler; -import javax.activation.FileDataSource; -import javax.mail.Message; -import javax.mail.Session; -import javax.mail.Transport; -import javax.mail.internet.InternetAddress; -import javax.mail.internet.MimeBodyPart; -import javax.mail.internet.MimeMessage; -import javax.mail.internet.MimeMultipart; -import java.util.Properties; - -/** - * @author victor zhang - * @date 2017/4/5. - */ -public class sendHtmlMail { - private static final String MAIL_SERVER_HOST = "smtp.163.com"; - private static final String USER = "xxxxxx"; - private static final String PASSWORD = "******"; - private static final String MAIL_FROM = "xxxxxx@163.com"; - private static final String MAIL_TO = "xxxxxx@163.com"; - private static final String MAIL_CC = "xxxxxx@163.com"; - private static final String MAIL_BCC = "xxxxxx@163.com"; - - 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(); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/template/LoadVelocityDemo.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/template/LoadVelocityDemo.java deleted file mode 100644 index cc2fa11a..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/template/LoadVelocityDemo.java +++ /dev/null @@ -1,94 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.template; - -import java.io.IOException; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Properties; - -import org.apache.velocity.Template; -import org.apache.velocity.VelocityContext; -import org.apache.velocity.app.VelocityEngine; - -/** - * @author Victor Zhang - * @date 2016/12/22. - */ -public class LoadVelocityDemo { - public static void main(String[] args) throws IOException { - loadByClasspath(); - loadByFilepath(); - loadByConfig(); - } - - /** - * 加载classpath目录下的vm文件 - */ - public static void loadByClasspath() { - System.out.println("========== loadByClasspath =========="); - - Properties p = new Properties(); - p.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); - VelocityEngine ve = new VelocityEngine(); - ve.init(p); - Template t = ve.getTemplate("template/hello.vm"); - - System.out.println(fillTemplate(t)); - } - - /** - * 根据绝对路径加载,vm文件置于硬盘某分区中 - */ - public static void loadByFilepath() { - System.out.println("========== loadByFilepath =========="); - - Properties p = new Properties(); - p.put(VelocityEngine.FILE_RESOURCE_LOADER_PATH, - "D:\\01_Workspace\\Project\\zp\\javaparty\\src\\toolbox\\template\\src\\main\\resources"); - VelocityEngine ve = new VelocityEngine(); - ve.init(p); - Template t = ve.getTemplate("hello.vm"); - - System.out.println(fillTemplate(t)); - } - - /** - * 根据资源路径加载 - */ - public static void loadByConfig() throws IOException { - System.out.println("========== loadByConfig =========="); - - Properties p = new Properties(); - p.load(LoadVelocityDemo.class.getResourceAsStream("/template/velocity.properties")); - VelocityEngine ve = new VelocityEngine(); - ve.init(p); - Template t = ve.getTemplate("template/hello.vm"); - - System.out.println(fillTemplate(t)); - } - - /** - * 使用文本文件,使用文本文件,如:velocity.properties - */ - private static String fillTemplate(Template t) { - // 初始化VelocityContext - VelocityContext ctx = new VelocityContext(); - ctx.put("name", "victor"); - ctx.put("date", (new Date()).toString()); - List temp = new ArrayList(); - temp.add("1"); - temp.add("2"); - ctx.put("list", temp); - - // 初始化Writer - StringWriter sw = new StringWriter(); - - t.merge(ctx, sw); - return sw.toString(); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityHelloWorld.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityHelloWorld.java deleted file mode 100644 index fc6a95e9..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityHelloWorld.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.github.dunwu.javaee.oss.template; /** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ - -import org.apache.velocity.Template; -import org.apache.velocity.VelocityContext; -import org.apache.velocity.app.VelocityEngine; - -import java.io.StringWriter; - -/** - * Velocity 的 HelloWorld 示例 - * - * @author Victor Zhang - * @date 2016/12/22. - */ -public class VelocityHelloWorld { - public static void main(String args[]) { - /* 1.初始化 Velocity */ - VelocityEngine velocityEngine = new VelocityEngine(); - velocityEngine.setProperty(VelocityEngine.RESOURCE_LOADER, "file"); - velocityEngine.setProperty(VelocityEngine.FILE_RESOURCE_LOADER_PATH, "D:/01_Workspace/Project/zp/javaparty/src/toolbox/template/src/main/resources"); - velocityEngine.init(); - - /* 2.创建一个上下文对象 */ - VelocityContext context = new VelocityContext(); - - /* 3.添加你的数据对象到上下文 */ - context.put("name", "Victor Zhang"); - context.put("project", "Velocity"); - - /* 4.选择一个模板 */ - Template template = velocityEngine.getTemplate("template/hello.vm"); - - /* 5.将你的数据与模板合并,产生输出内容 */ - StringWriter sw = new StringWriter(); - template.merge(context, sw); - System.out.println("final output:\n" + sw); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityHelloWorld2.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityHelloWorld2.java deleted file mode 100644 index 7c3c0709..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityHelloWorld2.java +++ /dev/null @@ -1,47 +0,0 @@ -package io.github.dunwu.javaee.oss.template; /** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ - -import org.apache.velocity.Template; -import org.apache.velocity.VelocityContext; -import org.apache.velocity.app.VelocityEngine; - -import java.io.IOException; -import java.io.StringWriter; -import java.util.Properties; - -/** - * Velocity 的 HelloWorld 示例 - * - * @author Victor Zhang - * @date 2016/12/22. - */ -public class VelocityHelloWorld2 { - public static void main(String args[]) { - /* 1.初始化 Velocity */ - Properties p = new Properties(); - try { - p.load(VelocityUtil.class.getResourceAsStream("/template/velocity.properties")); - } catch (IOException e) { - e.printStackTrace(); - } - VelocityEngine velocityEngine = new VelocityEngine(); - velocityEngine.init(p); - - /* 2.创建一个上下文对象 */ - VelocityContext context = new VelocityContext(); - - /* 3.添加你的数据对象到上下文 */ - context.put("name", "Victor Zhang"); - context.put("project", "Velocity"); - - /* 4.选择一个模板 */ - Template template = velocityEngine.getTemplate("template/hello.vm"); - - /* 5.将你的数据与模板合并,产生输出内容 */ - StringWriter sw = new StringWriter(); - template.merge(context, sw); - System.out.println("final output:\n" + sw); - } -} diff --git a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityUtil.java b/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityUtil.java deleted file mode 100644 index 833c8b3b..00000000 --- a/codes/javaee/oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityUtil.java +++ /dev/null @@ -1,46 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.template; - -import org.apache.velocity.Template; -import org.apache.velocity.VelocityContext; -import org.apache.velocity.app.VelocityEngine; - -import java.io.IOException; -import java.io.StringWriter; -import java.util.Properties; - -/** - * @author Victor Zhang - * @date 2016/12/23. - */ -public class VelocityUtil { - private static VelocityEngine velocityEngine; - - static { - Properties props = new Properties(); - try { - props.load(VelocityUtil.class.getResourceAsStream("/template/velocity.properties")); - } catch (IOException e) { - e.printStackTrace(); - } - velocityEngine = new VelocityEngine(); - velocityEngine.init(props); - } - - public static String getMergeOutput(VelocityContext context, String templateName) { - Template template = velocityEngine.getTemplate(templateName); - - StringWriter sw = new StringWriter(); - template.merge(context, sw); - String output = sw.toString(); - try { - sw.close(); - } catch (IOException e) { - e.printStackTrace(); - } - return output; - } -} diff --git a/codes/javaee/oss/src/main/resources/html/example.html b/codes/javaee/oss/src/main/resources/html/example.html deleted file mode 100644 index f38346b2..00000000 --- a/codes/javaee/oss/src/main/resources/html/example.html +++ /dev/null @@ -1,8 +0,0 @@ - - - 测试html页面 - - -

牛刀小试

- - \ No newline at end of file diff --git a/codes/javaee/oss/src/main/resources/images/lion.jpg b/codes/javaee/oss/src/main/resources/images/lion.jpg deleted file mode 100644 index d10bd212..00000000 Binary files a/codes/javaee/oss/src/main/resources/images/lion.jpg and /dev/null differ diff --git a/codes/javaee/oss/src/main/resources/images/lion2.jpg b/codes/javaee/oss/src/main/resources/images/lion2.jpg deleted file mode 100644 index 6794cb34..00000000 Binary files a/codes/javaee/oss/src/main/resources/images/lion2.jpg and /dev/null differ diff --git a/codes/javaee/oss/src/main/resources/log4j.xml b/codes/javaee/oss/src/main/resources/log4j.xml deleted file mode 100644 index d4c6b712..00000000 --- a/codes/javaee/oss/src/main/resources/log4j.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/codes/javaee/oss/src/main/resources/logback.xml b/codes/javaee/oss/src/main/resources/logback.xml deleted file mode 100644 index 5fffa5ed..00000000 --- a/codes/javaee/oss/src/main/resources/logback.xml +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - - - - - %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 - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/codes/javaee/oss/src/main/resources/mail/mail.properties b/codes/javaee/oss/src/main/resources/mail/mail.properties deleted file mode 100644 index e26a9bdf..00000000 --- a/codes/javaee/oss/src/main/resources/mail/mail.properties +++ /dev/null @@ -1,5 +0,0 @@ -smtp.host = smtp.163.com -pop3.host = pop3.163.com -mail.host = @163.com -mail.account = xxxxxx -mail.password = xxxxxx \ No newline at end of file diff --git a/codes/javaee/oss/src/main/resources/template/footer.vm b/codes/javaee/oss/src/main/resources/template/footer.vm deleted file mode 100644 index 5ee4d9da..00000000 --- a/codes/javaee/oss/src/main/resources/template/footer.vm +++ /dev/null @@ -1,15 +0,0 @@ -#set ($company = "Apache") -#set ($year = "2016") - - - - - - - - -
- Copyright $company $year. 保留所有权利。 -
- - \ No newline at end of file diff --git a/codes/javaee/oss/src/main/resources/template/header.vm b/codes/javaee/oss/src/main/resources/template/header.vm deleted file mode 100644 index 4d382929..00000000 --- a/codes/javaee/oss/src/main/resources/template/header.vm +++ /dev/null @@ -1,5 +0,0 @@ - - - Velocity 邮件模板 - - \ No newline at end of file diff --git a/codes/javaee/oss/src/main/resources/template/hello.vm b/codes/javaee/oss/src/main/resources/template/hello.vm deleted file mode 100644 index b43003ab..00000000 --- a/codes/javaee/oss/src/main/resources/template/hello.vm +++ /dev/null @@ -1,3 +0,0 @@ -Hello World! The first velocity demo. -Name is $name. -Project is $project \ No newline at end of file diff --git a/codes/javaee/oss/src/main/resources/template/mail.vm b/codes/javaee/oss/src/main/resources/template/mail.vm deleted file mode 100644 index b5c6c594..00000000 --- a/codes/javaee/oss/src/main/resources/template/mail.vm +++ /dev/null @@ -1,135 +0,0 @@ - - - - - - - - - - - - - ## 页首 - #parse("template/header.vm") - - - - - - ## 页脚 - #parse("template/footer.vm") - -
- - - - ## 收件人名 - - - - - ## 提示 - - - - - ## 邮件正文 - - - - - ## 日期 - - - - - ## 系统提示 - - - - - -
- - 亲爱的 $name: -
-
- -

$hint

-
-
-

将超链接、超链接名存入对象,将多个这样的对象放入列表:

- #foreach( $item in $links ) - $item.name
- #end -

传图片url,显示logo

- logo -
-
- $date -
-
-
- 提示:此邮件由系统自动发送,请勿直接回复 -
-
-
- - - - - \ No newline at end of file diff --git a/codes/javaee/oss/src/main/resources/template/velocity.properties b/codes/javaee/oss/src/main/resources/template/velocity.properties deleted file mode 100644 index 0b0fdde7..00000000 --- a/codes/javaee/oss/src/main/resources/template/velocity.properties +++ /dev/null @@ -1,4 +0,0 @@ -resource.loader=file -file.resource.loader.path=D:/01_Workspace/Project/zp/java/javaee-notes/codes/oss/src/main/resources -input.encoding=utf-8 -output.encoding=utf-8 diff --git a/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/html/JsoupTest.java b/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/html/JsoupTest.java deleted file mode 100644 index d5b374cc..00000000 --- a/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/html/JsoupTest.java +++ /dev/null @@ -1,103 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.html; - -import java.io.File; -import java.io.IOException; - -import org.jsoup.Jsoup; -import org.jsoup.nodes.Document; -import org.jsoup.nodes.Element; -import org.jsoup.select.Elements; -import org.junit.Before; -import org.junit.Test; - -/** - * @author Victor Zhang - * @date 2016/11/24. - */ -public class JsoupTest { - final String filePath = System.getProperty("user.dir") + "\\src\\test\\resources\\html\\jsoup-cookbook.html"; - private Document docFromStr; - private Document docFromUrl; - private Document docFromFile; - - /** - * Jsoup 有三种方式加载文档(Document): HTML字符串、URL地址、html文件 - * - * @throws IOException - */ - @Before - public void before() throws IOException { - // 从一个html字符串加载Document对象 - final String html = "First parse" - + "

Parsed HTML into a doc.

"; - docFromStr = Jsoup.parse(html); - - // 从一个URL加载Document对象 - docFromUrl = Jsoup.connect("https://www.baidu.com/").get(); - - // 从一个文件加载Document对象 - File input = new File(filePath); - docFromFile = Jsoup.parse(input, "UTF-8"); - - String htmlFragment = "

Lorem ipsum.

"; - Document doc = Jsoup.parse(htmlFragment); - } - - /** - * 获取html的title、head、body - */ - @Test - public void testGetHeadAndBody() { - System.out.println("title内容:\n" + docFromStr.title()); - System.out.println("head内容:\n" + docFromStr.head()); - System.out.println("body内容:\n" + docFromStr.body()); - } - - /** - * 使用DOM方法来遍历一个文档 - */ - @Test - public void test01() { - // 遍历一个Document对象中所有的链接 - Element content = docFromFile.body(); - Elements links = content.getElementsByTag("a"); - for (Element link : links) { - System.out.println("linkHref: " + link.attr("href")); - System.out.println("linkText: " + link.text()); - } - } - - /** - * 使用选择器语法来查找元素 - */ - @Test - public void testSelect() { - // 带有href属性的a元素 - Elements hrefs = docFromUrl.select("a[href]"); - System.out.println("[hrefs]\n" + hrefs.toString()); - // 扩展名为.png的图片 - Elements pngs = docFromUrl.select("img[src$=.png]"); - System.out.println("[pngs]\n" + pngs.toString()); - // class等于masthead的div标签 - Element head_wrappers = docFromUrl.select("div.head_wrapper").first(); - System.out.println("[head_wrapper:]\n" + head_wrappers.toString()); - // 在h3元素之后的a元素 - Elements resultLinks = docFromUrl.select("div.head_wrapper > a"); - System.out.println("[resultLinks]\n" + resultLinks.toString()); - } - - @Test - public void test() { - // 从元素集合抽取属性、文本和html内容 - Element link = docFromUrl.select("a").first();// 查找第一个a元素 - System.out.println("outerHtml: " + link.outerHtml()); - System.out.println("html: " + link.html()); // 取得链接内的html内容 - System.out.println("href: " + link.attr("href")); // 取得字符串中的文本 - System.out.println("text: " + link.text()); // 取得链接地址中的文本 - - } -} diff --git a/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/image/ImageUtilTest.java b/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/image/ImageUtilTest.java deleted file mode 100644 index 4901badc..00000000 --- a/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/image/ImageUtilTest.java +++ /dev/null @@ -1,73 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.image; - -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; - -import io.github.dunwu.javaee.oss.image.dto.ImageParamDTO; -import org.junit.Assert; -import org.junit.Test; - -import net.sf.jmimemagic.MagicException; -import net.sf.jmimemagic.MagicMatchNotFoundException; -import net.sf.jmimemagic.MagicParseException; - -/** - * @author Victor Zhang - * @date 2017/1/16. - */ -public class ImageUtilTest { - @Test - public void testToFile() throws IOException { - final String oldFile = System.getProperty("user.dir") + "/src/test/resources/images/lion2.jpg"; - final String newFile = System.getProperty("user.dir") + "/src/test/resources/images/lion2_watermark"; - final String warterFile = System.getProperty("user.dir") + "/src/test/resources/images/wartermark.png"; - - ImageParamDTO params = new ImageParamDTO(); - params.setFormat("png"); - params.setWaterMark(new ImageParamDTO.WaterMark(9, warterFile, 0.6f)); - ImageUtil.toFile(oldFile, newFile, params); - } - - @Test - public void testToBufferedImage() throws IOException { - final String oldFile = System.getProperty("user.dir") + "/src/test/resources/images/lion2.jpg"; - - ImageParamDTO params = new ImageParamDTO(); - params.setWidth(64); - params.setHeight(64); - - BufferedImage bufferedImage = ImageUtil.toBufferedImage(oldFile, params); - Assert.assertNotNull(bufferedImage); - } - - @Test - public void testGetContentType() - throws MagicParseException, MagicException, MagicMatchNotFoundException, IOException { - final String oldFile = System.getProperty("user.dir") + "/src/test/resources/images/lion2.jpg"; - byte[] bytes = toBytes(new File(oldFile)); - String type = ImageUtil.getContentType(bytes); - Assert.assertEquals("image/jpeg", type); - } - - private static byte[] toBytes(File file) throws IOException { - InputStream input = new FileInputStream(file); - ByteArrayOutputStream output = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length = 0; - while ((length = input.read(buffer)) != -1) { - output.write(buffer, 0, length); - } - byte[] data = output.toByteArray(); - output.close(); - input.close(); - return data; - } -} diff --git a/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/image/QRCodeUtilTest.java b/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/image/QRCodeUtilTest.java deleted file mode 100644 index 6542ec2c..00000000 --- a/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/image/QRCodeUtilTest.java +++ /dev/null @@ -1,97 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.image; - -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import io.github.dunwu.javaee.oss.image.dto.BarcodeParamDTO; -import org.junit.Assert; -import org.junit.Before; -import org.junit.FixMethodOrder; -import org.junit.Test; -import org.junit.runners.MethodSorters; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.zxing.BarcodeFormat; -import com.google.zxing.DecodeHintType; -import com.google.zxing.EncodeHintType; -import com.google.zxing.WriterException; - -/** - * 测试qrcode工具类 - * - * @author Victor Zhang - * @date 2017/1/16. - */ -@FixMethodOrder(MethodSorters.NAME_ASCENDING) -public class QRCodeUtilTest { - private static final String qrcodeFile = "d:\\qrcode.png"; - private String jsonContent = null; - private BarcodeParamDTO paramDTO = null; - - @Before - public void before() throws JsonProcessingException { - jsonContent = initTestJson(); - paramDTO = initBarcodeParam(); - } - - private String initTestJson() throws JsonProcessingException { - Map userData = new HashMap(); - Map fullname = new HashMap(); - fullname.put("first", "Peng"); - fullname.put("last", "Zhang"); - userData.put("name", fullname); - userData.put("gender", "MALE"); - userData.put("email", "aaa@163.com"); - - ObjectMapper mapper = new ObjectMapper(); // can reuse, share globally - return mapper.writeValueAsString(userData); - } - - private BarcodeParamDTO initBarcodeParam() { - BarcodeParamDTO paramDTO = new BarcodeParamDTO(); - paramDTO.setWidth(200); - paramDTO.setHeight(200); - paramDTO.setFilepath(qrcodeFile); - paramDTO.setImageFormat("png"); - paramDTO.setBarcodeFormat(BarcodeFormat.QR_CODE); - - // 编码参数 - Map encodeHints = new HashMap(); - encodeHints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); - paramDTO.setEncodeHints(encodeHints); - - // 解码参数 - HashMap decodeHints = new HashMap(); - decodeHints.put(DecodeHintType.CHARACTER_SET, "UTF-8"); - paramDTO.setDecodeHints(decodeHints); - - return paramDTO; - } - - /** - * 测试创建qrcode图片 - */ - @Test - public void test01() throws IOException, WriterException { - QRCodeUtil.encode(jsonContent, paramDTO); - File f = new File(qrcodeFile); - Assert.assertTrue(f.exists()); - } - - /** - * 测试解析qrcode图片 - */ - @Test - public void test02() { - String expect = "{\"gender\":\"MALE\",\"name\":{\"last\":\"Zhang\",\"first\":\"Peng\"},\"email\":\"aaa@163.com\"}"; - String content = QRCodeUtil.decode(paramDTO); - Assert.assertEquals(expect, content); - } -} diff --git a/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/image/ThumbnailatorTest.java b/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/image/ThumbnailatorTest.java deleted file mode 100644 index 946006f2..00000000 --- a/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/image/ThumbnailatorTest.java +++ /dev/null @@ -1,181 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.image; - -import net.coobird.thumbnailator.Thumbnails; -import net.coobird.thumbnailator.geometry.Positions; -import net.coobird.thumbnailator.name.Rename; -import org.junit.Assert; -import org.junit.Test; - -import javax.imageio.ImageIO; -import javax.imageio.stream.FileImageOutputStream; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.URL; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * @author Victor Zhang - * @date 2017/1/17. - */ -public class ThumbnailatorTest { - /** - * 测试输入单个对象 - * 执行后会在D:\下生成几张不同尺寸的图片 - */ - @Test - public void testInput01() throws IOException { - final String oldFile = System.getProperty("user.dir") + "/src/test/resources/images/lion.jpg"; - - // 输入形式:文件名 - Thumbnails.of(oldFile) - .size(16, 16) - .toFile("d:\\test_input_16_16.png"); - Assert.assertTrue(new File("d:\\test_input_16_16.png").exists()); - - // 输入形式:File 对象 - Thumbnails.of(new File(oldFile)) - .size(32, 32) - .toFile(new File("d:\\test_input_32_32.png")); - Assert.assertTrue(new File("d:\\test_input_32_32.png").exists()); - - // 输入形式:URL 对象 - URL url = new URL("https://raw.githubusercontent.com/dunwu/JavaParty/master/toolbox/image/src/test/resources/images/lion.jpg"); - Thumbnails.of(url) - .size(64, 64) - .toFile(new File("d:\\test_input_64_64.png")); - Assert.assertTrue(new File("d:\\test_input_64_64.png").exists()); - - // 输入形式:BufferedImage 对象 - BufferedImage originalImage = ImageIO.read(new File(oldFile)); - Thumbnails.of(originalImage) - .size(80, 80) - .toFile(new File("d:\\test_input_80_80.png")); - Assert.assertTrue(new File("d:\\test_input_80_80.png").exists()); - - // 输入形式:InputStream 对象 - InputStream fis = new FileInputStream(oldFile); - Thumbnails.of(fis) - .size(96, 96) - .toFile(new File("d:\\test_input_96_96.png")); - Assert.assertTrue(new File("d:\\test_input_96_96.png").exists()); - } - - /** - * 测试输入多个对象 - * 执行后会在D:\下生成几个含有图片的文件夹 - * 注:如果输入是多个对象,则输出也必须选用输出多个对象的方式 - * - * @throws IOException - */ - @Test - public void testInput02() throws IOException { - final String oldFile = System.getProperty("user.dir") + "/src/test/resources/images/lion.jpg"; - final String oldFile2 = System.getProperty("user.dir") + "/src/test/resources/images/lion2.jpg"; - - createFolderIfNotExist("D:\\fromFilenames"); - Set filenames = new HashSet(); - filenames.add(oldFile); - filenames.add(oldFile2); - Thumbnails.fromFilenames(filenames) - .size(50, 50) - .toFiles(new File("D:\\fromFilenames"), Rename.PREFIX_DOT_THUMBNAIL); - - createFolderIfNotExist("D:\\fromFiles"); - List files = new ArrayList(); - files.add(new File(oldFile)); - files.add(new File(oldFile2)); - Thumbnails.fromFiles(files) - .size(50, 50) - .toFiles(new File("D:\\fromFiles"), Rename.PREFIX_HYPHEN_THUMBNAIL); - } - - @Test - public void testOutput() throws IOException { - final String oldFile = System.getProperty("user.dir") + "/src/test/resources/images/lion2.jpg"; - File file = new File(oldFile); - BufferedImage bufferedImage = ImageIO.read(file); - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - // 输入形式:文件名 - Thumbnails.of(bufferedImage) - .scale(1.0) - .outputFormat("png") - .toOutputStream(outputStream); - byte[] bytes = outputStream.toByteArray(); - FileImageOutputStream imageOutput = new FileImageOutputStream(new File("d:\\lion_output.png")); - imageOutput.write(bytes, 0, bytes.length); - imageOutput.close(); - } - - @Test - public void testResize() throws IOException { - final String oldFile = System.getProperty("user.dir") + "/src/test/resources/images/lion2.jpg"; - Thumbnails.of(oldFile) - .size(16, 16) - .toFile("d:\\lion_16_16.png"); - - Thumbnails.of(oldFile) - .scale(2.0) - .toFile("d:\\lion_scale_2.0.png"); - - Thumbnails.of(oldFile) - .scale(1.0, 0.5) - .toFile("d:\\lion_scale_1.0_0.5.png"); - } - - @Test - public void testRotate() throws IOException { - final String oldFile = System.getProperty("user.dir") + "/src/test/resources/images/lion2.jpg"; - Thumbnails.of(oldFile) - .scale(0.8) - .rotate(90) - .toFile("d:\\lion2_rotate_90.png"); - - Thumbnails.of(oldFile) - .scale(0.8) - .rotate(180) - .toFile("d:\\lion2_rotate_180.png"); - } - - @Test - public void testWatermark() throws IOException { - final String oldFile = System.getProperty("user.dir") + "/src/test/resources/images/lion2.jpg"; - final String wartermarkFile = System.getProperty("user.dir") + "/src/test/resources/images/wartermark.png"; - BufferedImage watermarkImage = ImageIO.read(new File(wartermarkFile)); - Thumbnails.of(oldFile) - .scale(0.8) - .watermark(Positions.BOTTOM_LEFT, watermarkImage, 0.5f) - .toFile("d:\\lion2_watermark.png"); - } - - @Test - public void testBatchChange() throws IOException { - final String oldFile = System.getProperty("user.dir") + "/src/test/resources/images/lion.jpg"; - final String oldFile2 = System.getProperty("user.dir") + "/src/test/resources/images/lion2.jpg"; - final String wartermarkFile = System.getProperty("user.dir") + "/src/test/resources/images/wartermark.png"; - BufferedImage watermarkImage = ImageIO.read(new File(wartermarkFile)); - createFolderIfNotExist("D:\\watermark"); - - Thumbnails.of(oldFile, oldFile2) - .scale(0.8) - .watermark(Positions.BOTTOM_LEFT, watermarkImage, 0.5f) - .toFiles(new File("D:\\watermark"), Rename.PREFIX_DOT_THUMBNAIL); - } - - private void createFolderIfNotExist(String folderPath) throws IOException { - File f = new File(folderPath); - if (!(f.exists() && f.isDirectory())) { - f.mkdirs(); - } - } -} diff --git a/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/template/VelocityUtilTest.java b/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/template/VelocityUtilTest.java deleted file mode 100644 index ce9767f0..00000000 --- a/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/template/VelocityUtilTest.java +++ /dev/null @@ -1,22 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ -package io.github.dunwu.javaee.oss.template; - -import org.apache.velocity.VelocityContext; -import org.junit.Test; - -/** - * @author Victor Zhang - * @date 2016/12/23. - */ -public class VelocityUtilTest { - @Test - public void test() { - VelocityContext context = new VelocityContext(); - context.put("name", "Victor Zhang"); - context.put("project", "Velocity"); - System.out.println(VelocityUtil.getMergeOutput(context, "template/hello.vm")); - } -} diff --git a/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/test/JUnitExecTest.java b/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/test/JUnitExecTest.java deleted file mode 100644 index 5b248b0c..00000000 --- a/codes/javaee/oss/src/test/java/io/github/dunwu/javaee/oss/test/JUnitExecTest.java +++ /dev/null @@ -1,95 +0,0 @@ -package io.github.dunwu.javaee.oss.test; /** - * The Apache License 2.0 - * Copyright (c) 2016 Victor Zhang - */ - -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Ignore; -import org.junit.Test; - -import static org.junit.Assert.fail; - -/** - * JUnit4开始支持注解,本例展示一个单元测试执行过程中,各个注解的调用顺序 - * - * @author Victor Zhang - * @date 2016/11/18. - */ -public class JUnitExecTest { - private static JUnitExecTest instance = new JUnitExecTest(); - - @BeforeClass - public static void beforeClass1() { - System.out.println("@beforeClass1"); - } - - @BeforeClass - public static void beforeClass2() { - System.out.println("@beforeClass2"); - } - - @Before - public void before1() throws Exception { - System.out.println("@before1"); - } - - @Before - public void before2() throws Exception { - System.out.println("@before2"); - } - - @Test - public void testAdd() { - System.out.println(1); - } - - @Test - public void testSubstract() { - System.out.println(2); - } - - @Ignore("Multiply() Not yet implemented") - @Test - public void testMultiply() { - System.out.println(3); - fail("Not yet implemented"); - } - - @Test - public void testDivide() { - System.out.println(4); - } - - @Test(timeout = 1000) - public void testSquareRoot() { - System.out.println(5); - } - - @Test - public void divideByZero() { - System.out.println(6); - } - - @After - public void after1() { - System.out.println("@after1"); - } - - @After - public void after2() { - System.out.println("@after2"); - } - - @AfterClass - public static void afterClass1() { - System.out.println("@afterClass1"); - } - - @AfterClass - public static void afterClass2() { - System.out.println("@afterClass2"); - } -} diff --git a/codes/javaee/oss/src/test/resources/html/jsoup-cookbook.html b/codes/javaee/oss/src/test/resources/html/jsoup-cookbook.html deleted file mode 100644 index cc16bf8a..00000000 --- a/codes/javaee/oss/src/test/resources/html/jsoup-cookbook.html +++ /dev/null @@ -1,90 +0,0 @@ - - - - Cookbook: jsoup Java HTML parser - - - - - - - - - - - - - \ No newline at end of file diff --git a/codes/javaee/oss/src/test/resources/images/lion.jpg b/codes/javaee/oss/src/test/resources/images/lion.jpg deleted file mode 100644 index d10bd212..00000000 Binary files a/codes/javaee/oss/src/test/resources/images/lion.jpg and /dev/null differ diff --git a/codes/javaee/oss/src/test/resources/images/lion2.jpg b/codes/javaee/oss/src/test/resources/images/lion2.jpg deleted file mode 100644 index 6794cb34..00000000 Binary files a/codes/javaee/oss/src/test/resources/images/lion2.jpg and /dev/null differ diff --git a/codes/javaee/oss/src/test/resources/images/lion2_watermark.png b/codes/javaee/oss/src/test/resources/images/lion2_watermark.png deleted file mode 100644 index 2de3bb8a..00000000 Binary files a/codes/javaee/oss/src/test/resources/images/lion2_watermark.png and /dev/null differ diff --git a/codes/javaee/oss/src/test/resources/images/wartermark.png b/codes/javaee/oss/src/test/resources/images/wartermark.png deleted file mode 100644 index 675f9a39..00000000 Binary files a/codes/javaee/oss/src/test/resources/images/wartermark.png and /dev/null differ diff --git a/codes/javaee/oss/src/test/resources/logback.xml b/codes/javaee/oss/src/test/resources/logback.xml deleted file mode 100644 index d8cd7f71..00000000 --- a/codes/javaee/oss/src/test/resources/logback.xml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - %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}/spring.%d{yyyy-MM-dd}.log - - 30 - - - - - 10MB - - - - %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/codes/javaee/parent/pom.xml b/codes/javaee/parent/pom.xml deleted file mode 100644 index c675bebe..00000000 --- a/codes/javaee/parent/pom.xml +++ /dev/null @@ -1,409 +0,0 @@ - - - 4.0.0 - - - - - - io.github.dunwu - javaee-notes-parent - 1.0.0 - pom - - - - - - - - org.mongodb - mongo-java-driver - 3.3.0 - - - org.mongodb - mongodb-driver-async - 3.3.0 - - - redis.clients - jedis - 2.7.3 - - - com.alibaba - druid - 1.0.19 - - - mysql - mysql-connector-java - 5.1.39 - - - com.h2database - h2 - 1.4.196 - - - - - - org.jsoup - jsoup - 1.9.2 - - - - - - javax.servlet - javax.servlet-api - 3.1.0 - provided - - - javax.servlet.jsp - jsp-api - 2.2 - provided - - - javax.servlet - jstl - 1.2 - provided - - - javax.websocket - javax.websocket-api - 1.1 - provided - - - - - - org.eclipse.jetty - jetty-webapp - ${jetty.version} - test - - - org.eclipse.jetty - jetty-annotations - ${jetty.version} - test - - - org.eclipse.jetty - apache-jsp - ${jetty.version} - test - - - org.eclipse.jetty - apache-jstl - ${jetty.version} - test - - - org.eclipse.jetty.websocket - javax-websocket-server-impl - ${jetty.version} - - - - - - com.fasterxml.jackson.core - jackson-core - ${fasterxml.jackson} - - - com.fasterxml.jackson.core - jackson-databind - ${fasterxml.jackson} - - - org.codehaus.jackson - jackson-mapper-asl - 1.9.13 - - - com.alibaba - fastjson - 1.2.7 - - - com.google.code.gson - gson - 2.3.1 - - - - - - commons-logging - commons-logging - 1.2 - - - log4j - log4j - 1.2.17 - - - ch.qos.logback - logback-classic - 1.1.3 - - - ch.qos.logback - logback-core - 1.1.3 - - - org.logback-extensions - logback-ext-spring - 0.1.2 - - - org.slf4j - jcl-over-slf4j - 1.7.12 - - - - - - net.sf.dozer - dozer - 5.4.0 - - - org.slf4j - jcl-over-slf4j - - - org.slf4j - slf4j-log4j12 - - - - - - - - org.mybatis - mybatis - 3.4.2 - - - org.mybatis - mybatis-spring - 1.3.0 - - - org.mybatis.generator - mybatis-generator-core - 1.3.5 - - - - - - org.apache.shiro - shiro-core - ${shiro.version} - - - org.apache.shiro - shiro-web - ${shiro.version} - - - org.apache.shiro - shiro-ehcache - ${shiro.version} - - - org.apache.shiro - shiro-quartz - ${shiro.version} - - - org.apache.shiro - shiro-spring - ${shiro.version} - - - - - - org.apache.velocity - velocity - 1.7 - - - - - - junit - junit - 4.12 - test - - - org.assertj - assertj-core - 3.4.1 - test - - - - - - commons-codec - commons-codec - 1.9 - - - commons-collections - commons-collections - 3.2.1 - - - commons-dbcp - commons-dbcp - 1.2.2 - - - commons-fileupload - commons-fileupload - 1.3.1 - - - commons-io - commons-io - 2.4 - - - org.apache.commons - commons-lang3 - 3.4 - - - com.google.guava - guava - 19.0 - - - - - - dom4j - dom4j - 1.6 - - - - - - - - - UTF-8 - - - 9.3.2.v20150730 - 4.1.4.RELEASE - 1.4.0 - 4.12 - - 2.9.0 - - - - - - - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 2.12 - - - org.apache.tomcat.maven - tomcat7-maven-plugin - 2.2 - - - org.eclipse.jetty - jetty-maven-plugin - ${jetty.version} - - - org.codehaus.mojo - aspectj-maven-plugin - - 1.2 - - - - org.aspectj - aspectjrt - 1.8.1 - - - org.aspectj - aspectjtools - 1.8.1 - - - - - - compile - test-compile - - - - - true - ${java.version} - ${java.version} - - - - - org.mybatis.generator - mybatis-generator-maven-plugin - 1.3.5 - - - org.mybatis.generator - mybatis-generator-core - 1.3.5 - - - - - - - - - - - javaee-notes-parent - javaee-notes 项目的父POM,管理所有子项目的依赖版本 - - - - diff --git a/codes/javaee/pom.xml b/codes/javaee/pom.xml deleted file mode 100644 index f8ef2b5b..00000000 --- a/codes/javaee/pom.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - 4.0.0 - - - - - - io.github.dunwu - javaee-notes - 1.0.0 - pom - - - - - parent - servlet - jsp - session - filter - listener - jstl - taglib - oss - websocket - - - - - - UTF-8 - - - - - - - - - - - - javaee-notes - javaee 学习笔记 - https://github.com/dunwu/javaee-notes - 2016-2017 - - - Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0.txt - repo - A business-friendly OSS license - - - - - Zhang Peng - forbreak@163.com - +8 - - - - - - - - Github - https://github.com/dunwu/javaee-notes/issues - - - https://github.com/dunwu/javaee-notes - scm:git:git://github.com/dunwu/javaee-notes.git - scm:git:ssh://git@github.com:dunwu/javaee-notes.git - - - - - diff --git a/codes/javaee/servlet/pom.xml b/codes/javaee/servlet/pom.xml deleted file mode 100644 index e76194d3..00000000 --- a/codes/javaee/servlet/pom.xml +++ /dev/null @@ -1,102 +0,0 @@ - - - 4.0.0 - - - io.github.dunwu - javaee-notes-servlet - 1.0.0 - war - - - - - javaee-notes-servlet - javaee 学习笔记之 servlet - - - - - - UTF-8 - 1.7 - ${java.version} - ${java.version} - - - 9.3.2.v20150730 - - - - - org.eclipse.jetty - jetty-webapp - ${jetty.version} - test - - - org.eclipse.jetty - jetty-annotations - ${jetty.version} - test - - - org.eclipse.jetty - apache-jsp - ${jetty.version} - test - - - org.eclipse.jetty - apache-jstl - ${jetty.version} - test - - - - - ch.qos.logback - logback-classic - 1.1.3 - - - - - javax.servlet - javax.servlet-api - 3.1.0 - - - - - junit - junit - 4.12 - - - org.assertj - assertj-core - 3.5.2 - - - - - org.apache.commons - commons-lang3 - 3.4 - - - commons-fileupload - commons-fileupload - 1.3.1 - - - - - - - - - - diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/AnnotationServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/AnnotationServlet.java deleted file mode 100644 index 4c925c22..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/AnnotationServlet.java +++ /dev/null @@ -1,88 +0,0 @@ -package io.github.dunwu.javaee.servlet; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class AnnotationServlet extends HttpServlet { - - /** - * - */ - private static final long serialVersionUID = 7516608289980410869L; - - /** - * Constructor of the object. - */ - public AnnotationServlet() { - this.log("AnnotationServlet()"); - } - - public void log(String str){ - System.out.println(str); - } - - /** - * Destruction of the servlet.
- */ - public void destroy() { - this.log("destroy()"); - } - - /** - * The doGet method of the servlet.
- * - * This method is called when a form has its tag value method equals to get. - * - * @param request the request send by the client to the server - * @param response the response send by the server to the client - * @throws ServletException if an error occurred - * @throws IOException if an error occurred - */ - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - this.log("doGet()"); - - response.setContentType("text/html"); - PrintWriter out = response.getWriter(); - out.println(""); - out.println(""); - out.println(" A Servlet"); - out.println(" "); - out.print(" This is "); - out.print(this.getClass()); - out.println(", using the GET method"); - out.println(" "); - out.println(""); - out.flush(); - out.close(); - } - - /** - * Initialization of the servlet.
- * - * @throws ServletException if an error occure - */ - public void init() throws ServletException { - this.log("init()"); - } - - @PostConstruct - public void postConstruct(){ - this.log("postConstruct()"); - } - - - - public @PreDestroy void preDestroy(){ - this.log("preDestroy()"); - } - -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/ContextParamServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/ContextParamServlet.java deleted file mode 100644 index fbacad87..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/ContextParamServlet.java +++ /dev/null @@ -1,68 +0,0 @@ -package io.github.dunwu.javaee.servlet; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * 读取web.xml中的 - * 配置在中,只能让对应的servlet使用; - * 配置在全局中,可以让所有的servlet使用。 - */ -public class ContextParamServlet extends HttpServlet { - - private static final long serialVersionUID = 3194071196406358461L; - - private final Logger logger = LoggerFactory.getLogger(this.getClass()); - - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - logger.info("ContextParamServlet 读取 web.xml 中的"); - - response.setCharacterEncoding("UTF-8"); - response.setContentType("text/html"); - PrintWriter out = response.getWriter(); - out.println(""); - out.println(""); - out.println(" 读取文档参数"); - out.println(" "); - out.println(" "); - out.println("

"); - out.println("
所有的文档参数
"); - - ServletContext servletContext = this.getServletConfig().getServletContext(); - String uploadFolder = servletContext.getInitParameter("upload folder"); - String allowedFileType = servletContext.getInitParameter("allowed file type"); - - out.println("
"); - out.println("
上传文件夹
"); - out.println("
" + uploadFolder + "
"); - out.println("
"); - - out.println("
"); - out.println("
实际磁盘路径
"); - out.println("
" + servletContext.getRealPath(uploadFolder) + "
"); - out.println("
"); - - out.println("
"); - out.println("
允许上传的类型
"); - out.println("
" + allowedFileType + "
"); - out.println("
"); - - out.println("
"); - - out.println(" "); - out.println(""); - out.flush(); - out.close(); - } -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/FirstServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/FirstServlet.java deleted file mode 100644 index d3a4826d..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/FirstServlet.java +++ /dev/null @@ -1,97 +0,0 @@ -package io.github.dunwu.javaee.servlet; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * 第一个Servlet - */ -public class FirstServlet extends HttpServlet { - private static final long serialVersionUID = 2386052823761867369L; - - /** - * 以 GET 方式访问页面时执行该函数。 - * 执行 doGet 前会先执行 getLastModified,如果浏览器发现 getLastModified 返回的数值 - * 与上次访问时返回的数值相同,则认为该文档没有更新,浏览器采用缓存而不执行 doGet。 - * 如果 getLastModified 返回 -1,则认为是时刻更新的,总是执行该函数。 - */ - @Override - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - // 调用 HttpServlet 自带的日志函数输出信息到控制台 - this.log("执行 doGet 方法... "); - - // 处理 doGet - this.execute(request, response); - } - - /** - * 以 POST 方式访问页面时执行该函数。 - */ - @Override - public void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - // 调用 HttpServlet 自带的日志函数输出信息到控制台 - this.log("执行 doPost 方法... "); - - // 处理 doPost - this.execute(request, response); - } - - /** - * 返回该 Servlet 生成的文档的更新时间。对 Get 方式访问有效。 - * 返回的时间为相对于 1970年1月1日08:00:00 的毫秒数。 - * 如果为 -1 则认为是实时更新。默认为 -1。 - */ - @Override - public long getLastModified(HttpServletRequest request) { - - // 调用 HttpServlet 自带的日志函数输出信息到控制台 - this.log("执行 getLastModified 方法... "); - - return 0; - } - - private void execute(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException{ - - response.setCharacterEncoding("UTF-8"); - request.setCharacterEncoding("UTF-8"); - - // 访问该 Servlet 的 URI - String requestURI = request.getRequestURI(); - // 访问该 Servlet 的方式,是 GET 还是 POST - String method = request.getMethod(); - // 客户端提交的参数 param 值 - String param = request.getParameter("param"); - - response.setContentType("text/html"); - PrintWriter out = response.getWriter(); - out.println(""); - out.println(""); - out.println(" A Servlet"); - out.println(" "); - out.println(" 以 " + method + " 方式访问该页面。取到的 param 参数为:" + param + "
"); - - out.println("
"); - out.println("
"); - - // 由客户端浏览器读取该文档的更新时间 - out.println(" "); - out.println(" "); - - out.println(" "); - out.println(""); - out.flush(); - out.close(); - - } - -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/ForwardServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/ForwardServlet.java deleted file mode 100644 index b6300feb..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/ForwardServlet.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.github.dunwu.javaee.servlet; - -import java.io.IOException; -import java.util.Date; - -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class ForwardServlet extends HttpServlet { - - private static final long serialVersionUID = -291840563095094360L; - - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - String destination = request.getParameter("destination"); - - // 跳转到 /WEB-INF/web.xml。通过地址栏输入网址是不能访问到该文件的,但是 forward 可以 - if("file".equals(destination)){ - RequestDispatcher dispatcher = request.getRequestDispatcher("/WEB-INF/web.xml"); - dispatcher.forward(request, response); - } - // 跳转到 /forward.jsp - else if("jsp".equals(destination)){ - // 通过 setAttribute 方法传递一个 Date 对象给 JSP 页面 - Date date = new Date(); - request.setAttribute("date", date); - RequestDispatcher dispatcher = request.getRequestDispatcher("/forward.jsp"); - dispatcher.forward(request, response); - } - // 跳转到另一个 Servlet - else if("servlet".equals(destination)){ - RequestDispatcher dispatcher = request.getRequestDispatcher("/servlet/LifeCycleServlet"); - dispatcher.forward(request, response); - } - else{ - response.setCharacterEncoding("UTF-8"); - response.setContentType("text/html; charset=UTF-8"); - response.getWriter().println("缺少参数。用法:" + request.getRequestURL() + "?destination=jsp 或者 file 或者 servlet "); - } - - } - public void doPut(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - this.doGet(request, response); - } - -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/HelloServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/HelloServlet.java deleted file mode 100644 index b653b59e..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/HelloServlet.java +++ /dev/null @@ -1,109 +0,0 @@ -/** - * The Apache License 2.0 - * Copyright (c) 2016 Zhang Peng - */ -package io.github.dunwu.javaee.servlet; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.io.PrintWriter; - -/** - * @author Zhang Peng - * @date 2016/12/23. - */ -public class HelloServlet extends HttpServlet { - private static final long serialVersionUID = 1L; - - /** - * Constructor of the object. - */ - public HelloServlet() { - super(); - } - - /** - * Destruction of the servlet.
- */ - public void destroy() { - super.destroy(); // Just puts "destroy" string in log - // Put your code here - } - - /** - * The doGet method of the servlet.
- * - * This method is called when a form has its tag value method equals to get. - * - * @param request the request send by the client to the server - * @param response the response send by the server to the client - * @throws ServletException if an error occurred - * @throws IOException if an error occurred - */ - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - // 设置 request,response 编码方式 - response.setCharacterEncoding("UTF-8"); - request.setCharacterEncoding("UTF-8"); - - // 设置文档类型 - response.setContentType("text/html"); - PrintWriter out = response.getWriter(); - - // 输出到客户端浏览器 - out.println(""); - out.println(""); - out.println(""); - out.println("A Servlet"); - out.println(" "); - - String requestURI = request.getRequestURI(); - out.println("
"); - out.println("请输入您的名字:"); - out.println(""); - out.println("
"); - out.println(""); - - // 取浏览器提交的 name 参数 - String name = request.getParameter("name"); - - // 如果 name 不为空且长度大于 0 - if(name != null && name.trim().length() > 0){ - out.println("您好, " + name + ". 欢迎来到 Java Web 世界. "); - } - - out.println(" "); - out.println(""); - out.flush(); - out.close(); - } - - /** - * The doPost method of the servlet.
- * - * This method is called when a form has its tag value method equals to post. - * - * @param request the request send by the client to the server - * @param response the response send by the server to the client - * @throws ServletException if an error occurred - * @throws IOException if an error occurred - */ - public void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - this.doGet(request, response); - } - - /** - * Initialization of the servlet.
- * - * @throws ServletException if an error occure - */ - public void init() throws ServletException { - // Put your code here - } -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/ImageServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/ImageServlet.java deleted file mode 100644 index 4d127d1d..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/ImageServlet.java +++ /dev/null @@ -1,92 +0,0 @@ -package io.github.dunwu.javaee.servlet; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class ImageServlet extends HttpServlet { - - private static final long serialVersionUID = -5446593186536558309L; - - public ImageServlet() { - System.out.println("正在加载 " + this.getClass().getName() + " ... "); - } - - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - String referer = request.getHeader("referer"); - - // 如果直接输入的网址,或者是从别的网站打开的,显示错误信息。 - if(referer == null - || !referer.toLowerCase().startsWith("http://" + request.getServerName().toLowerCase())){ - // 打开图片 error.gif - request.getRequestDispatcher("/error.gif").forward(request, response); - return; - } - - String requestURI = request.getRequestURI(); - String fileName = requestURI.substring(requestURI.lastIndexOf("/") + 1); - - // 请求的文件位置 - File file = new File(this.getServletContext().getRealPath("upload"), fileName); - this.log("请求文件 " + file.getAbsolutePath()); - - // 如果文件不存在,显示错误信息 - if(!file.exists()){ - response.getWriter().println("File " + requestURI + " doesn't exist. "); - return; - } - - // 设置打开方式为 inline,浏览器中打开 - response.setHeader("Content-Disposition", "inline;filename=" + file.getName()); - response.setHeader("Connection", "close"); - - if(fileName.toLowerCase().endsWith(".jpg")) - // .jpg 图片格式 - response.setHeader("Content-Type", "image/jpeg"); - else if(fileName.toLowerCase().endsWith(".gif")) - // .gif 图片格式 - response.setHeader("Content-Type", "image/gif"); - else if(fileName.toLowerCase().endsWith(".doc")) - // word 格式 - response.setHeader("Content-Type", "application/msword"); - else - // 其他格式 - response.setHeader("Content-Type", "application/octet-stream"); - - // 通过 ins 读取文件 - InputStream ins = new FileInputStream(file); - // 通过 ous 发送给客户端 - OutputStream ous = response.getOutputStream(); - - try{ - // 缓存 - byte[] buffer = new byte[1024]; - int len = 0; - - // 读取文件内容并将它发送给客户端浏览器 - while((len=ins.read(buffer)) > -1){ - ous.write(buffer, 0, len); - } - }finally{ - if(ous != null) ous.close(); - if(ins != null) ins.close(); - } - - } - - public void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - this.doGet(request, response); - } - -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/InitParamServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/InitParamServlet.java deleted file mode 100644 index 67cef195..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/InitParamServlet.java +++ /dev/null @@ -1,87 +0,0 @@ -package io.github.dunwu.javaee.servlet; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.Enumeration; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * 初始化参数示例 - * 读取web.xml中的 - * 配置在中,只能让对应的servlet使用; - * 配置在全局中,可以让所有的servlet使用。 - */ -public class InitParamServlet extends HttpServlet { - private static final long serialVersionUID = 7298032096933866458L; - private final Logger logger = LoggerFactory.getLogger(this.getClass()); - - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - logger.info("InitParamServlet 初始化用户名、密码参数"); - request.setCharacterEncoding("UTF-8"); - response.setCharacterEncoding("UTF-8"); - response.setContentType("text/html"); - - PrintWriter out = response.getWriter(); - out.println(""); - out.println(""); - out.println(" 请登录查看 Notice 文件"); - out.println(""); - out.println(" "); - out.println("
"); - out.println("帐号:
"); - out.println("密码:

"); - out.println(""); - out.println("
"); - - if(true){ - out.println("






用户名、密码为:
"); - Enumeration params = this.getInitParameterNames(); - while(params.hasMoreElements()){ - String username = (String)params.nextElement(); - String password = this.getInitParameter(username); - out.println("[" + username + ", " + password + "], "); - logger.info("用户名={}、密码={}", username, password); - } - } - - out.println(" "); - out.println(""); - out.flush(); - out.close(); - } - - public void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - // 提交的 username 参数 - String username = request.getParameter("username"); - // 提交的 password 参数 - String password = request.getParameter("password"); - // 取所有的初始化参数名称 - Enumeration params = this.getInitParameterNames(); - while(params.hasMoreElements()){ - String usernameParam = (String)params.nextElement(); - // 取参数值 - String passnameParam = this.getInitParameter(usernameParam); - // 如果 username 匹配且 password 匹配. username 大小写不敏感,password大小写敏感 - if(usernameParam.equalsIgnoreCase(username) - && passnameParam.equals(password)){ - // 显示文件。/WEB-INF 下的文件不能通过浏览器访问到,因此是安全的 - request.getRequestDispatcher("/WEB-INF/notice.html").forward(request, response); - return; - } - } - // username,password 不匹配,显示登录页面 - logger.warn("用户名={}、密码={}不匹配。", username, password); - this.doGet(request, response); - } - -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/InjectionServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/InjectionServlet.java deleted file mode 100644 index 059f1726..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/InjectionServlet.java +++ /dev/null @@ -1,53 +0,0 @@ -package io.github.dunwu.javaee.servlet; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.annotation.Resource; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class InjectionServlet extends HttpServlet { - - private static final long serialVersionUID = -8526907492073769090L; - - // 注入的 字符串 - private @Resource(name="hello") String hello; - // 注入的 整数 - private @Resource(name="i") int i; - - // 注入更常见的写法 - @Resource(name="persons") - private String persons; - - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - response.setCharacterEncoding("UTF-8"); - request.setCharacterEncoding("UTF-8"); - - response.setContentType("text/html"); - PrintWriter out = response.getWriter(); - out.println(""); - out.println(""); - out.println(" 资源注入"); - out.println(""); - - out.println("注入的字符串
  - " + hello + "
"); - out.println("注入的整数
  - " + i + "
"); - out.println("注入的字符串数组
"); - - for(String person : persons.split(",")){ - out.println("  - " + person + "
"); - } - - out.println(" "); - out.println(" "); - out.println(""); - out.flush(); - out.close(); - } - -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/LifeCycleServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/LifeCycleServlet.java deleted file mode 100644 index 40b476ae..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/LifeCycleServlet.java +++ /dev/null @@ -1,121 +0,0 @@ -package io.github.dunwu.javaee.servlet; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.ServletConfig; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class LifeCycleServlet extends HttpServlet { - - private static final long serialVersionUID = -7197419401412129310L; - - private static double startPoint = 0; - - @Override - public void init() throws ServletException { - this.log("执行 init() 方法 ... "); - ServletConfig conf = this.getServletConfig(); - startPoint = Double.parseDouble(conf.getInitParameter("startPoint")); - } - - - @Override - protected void service(HttpServletRequest arg0, HttpServletResponse arg1) throws ServletException, IOException { - this.log("执行 service() 方法 ... "); - super.service(arg0, arg1); - } - - @Override - public void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - this.log("执行 doPost() 方法 ... "); - response.setCharacterEncoding("UTF-8"); - response.setContentType("text/html"); - PrintWriter out = response.getWriter(); - out.println(""); - out.println("个人所得税计算"); - out.println(""); - out.println(""); - - out.println("

个税计算器
"); - - try{ - // 从参数中获取的工资数目 - double income = new Double(request.getParameter("income")); - // 应纳税部分 - double charge = income - startPoint; - // 缴税 - double tax = 0; - - if (charge<=0) {tax=0;} - if (charge>0&&charge<=500) {tax=charge*0.05;} - if (charge>500&&charge<=2000) {tax=charge*0.1-25;} - if (charge>2000&&charge<=5000) {tax=charge*0.15-125;} - if (charge>5000&&charge<=20000) {tax=charge*0.2-375;} - if (charge>20000&&charge<=40000) {tax=charge*0.25-1375;} - if (charge>40000&&charge<=60000) {tax=charge*0.30-3375;} - if (charge>60000&&charge<=80000) {tax=charge*0.35-6375;} - if (charge>80000&&charge<=100000) {tax=charge*0.4-10375;} - if (charge>100000) {tax=charge*0.45-15375;} - - out.println("
"); - out.println("
您的工资为
" + income + " 元
"); - out.println("
"); - - out.println("
"); - out.println("
您应纳税
" + tax + " 元
"); - out.println("

"); - - out.println(""); - - }catch(Exception e){ - out.println("请输入数值类型数据。"); - } - out.println(""); - out.println(""); - out.flush(); - out.close(); - } - - @Override - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - this.log("执行 doGet() 方法 ... "); - response.setCharacterEncoding("UTF-8"); - response.setContentType("text/html"); - PrintWriter out = response.getWriter(); - out.println(""); - out.println(""); - out.println("个人所得税计算"); - - out.println("

个税计算器
"); - out.println("
"); - - out.println("
"); - out.println("
您的工资为
单位:元
"); - out.println("

"); - - out.println("
"); - out.println("
"); - out.println("
"); - - out.println("
"); - - out.println(""); - out.println(""); - out.println(""); - out.flush(); - out.close(); - } - - @Override - public void destroy() { - this.log("执行 destroy() 方法 ... "); - startPoint = 0; - } - -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/PostServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/PostServlet.java deleted file mode 100644 index 2b0ea507..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/PostServlet.java +++ /dev/null @@ -1,159 +0,0 @@ -package io.github.dunwu.javaee.servlet; - -import java.io.IOException; -import java.io.PrintWriter; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class PostServlet extends HttpServlet { - - private static final long serialVersionUID = 2112378505872783022L; - - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - response.setCharacterEncoding("UTF-8"); - response.getWriter().println("请使用 POST 方式提交数据。"); - } - - public void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - response.setCharacterEncoding("UTF-8"); - request.setCharacterEncoding("UTF-8"); - - // 从 文本框 text 中取姓名 - String name = request.getParameter("name"); - // 从 密码域 password 中取密码 - String password = request.getParameter("password"); - // 从 单选框 checkbox 中取性别 - String sex = request.getParameter("sex"); - - int age = 0; - try { - // 取 年龄. 需要把 字符串 转换为 int. - // 如果格式不对会抛出 NumberFormattingException - age = Integer.parseInt(request.getParameter("age")); - } catch (Exception e) { - } - - Date birthday = null; - try { - // 取 生日. 需要把 字符串 转化为 Date. - // 如果格式不对会抛出 ParseException - DateFormat format = new SimpleDateFormat("yyyy-MM-dd"); - birthday = format.parse(request.getParameter("birthday")); - } catch (Exception e) { - } - - // 从 多选框 checkbox 中取多个值 - String[] interesting = request.getParameterValues("interesting"); - // 从 下拉框 select 中取值 - String area = request.getParameter("area"); - // 从 文本域 textarea 中取值 - String description = request.getParameter("description"); - - // 取 提交按钮 的键值 - String btn = request.getParameter("btn"); - - response.setContentType("text/html"); - PrintWriter out = response.getWriter(); - - out - .println(""); - out.println(""); - out.println("感谢您提交信息"); - out.println(""); - out.println(""); - - out.println("

"); - out.println("
填写用户信息
"); - - out.println("
"); - out.println("
您的姓名:
"); - out.println("
" + name + "
"); - out.println("
"); - - out.println("
"); - out.println("
您的密码:
"); - out.println("
" + password - + "
"); - out.println("
"); - - out.println("
"); - out.println("
您的性别:
"); - out.println("
" + sex + "
"); - out.println("
"); - - out.println("
"); - out.println("
您的年龄:
"); - out.println("
" + age + "
"); - out.println("
"); - - out.println("
"); - out.println("
您的生日:
"); - out.println("
"); - out.println(new SimpleDateFormat("yyyy年MM月dd日").format(birthday)); - out.println("
"); - out.println("
"); - - out.println("
"); - out.println("
您的兴趣:
"); - out.println("
"); - - if (interesting != null) - for (String str : interesting) { - out.println(str + ", "); - } - - out.println("
"); - out.println("
"); - - out.println("
"); - out.println("
自我描述:
"); - out.println("
" + description - + "
"); - out.println("
"); - - out.println("
"); - out.println("
按钮键值:
"); - out.println("
" + btn + "
"); - out.println("
"); - - out.println("
"); - out.println("
"); - out.println("
"); - out - .println("

"); - out.println("
"); - out.println("
"); - - out.println(""); - out.println(""); - out.println(""); - out.flush(); - out.close(); - } -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/RedirectServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/RedirectServlet.java deleted file mode 100644 index df34d10b..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/RedirectServlet.java +++ /dev/null @@ -1,81 +0,0 @@ -package io.github.dunwu.javaee.servlet; - -import java.io.IOException; -import java.io.PrintWriter; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class RedirectServlet extends HttpServlet { - - private static final long serialVersionUID = 4442189888545647793L; - - Map map = new HashMap(); - - @Override - public void init() throws ServletException { - map.put("/download/setup.exe", 0); - map.put("/download/application.zip", 0); - map.put("/download/01.mp3", 0); - } - - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - String filename = request.getParameter("filename"); - - if(filename != null){ - // 取下载次数 - int hit = map.get(filename); - // 下载次数 + 1 后保存 - map.put(filename, ++ hit); - // 重定向到文件 - response.sendRedirect(request.getContextPath() + filename); - } - else{ - response.setCharacterEncoding("UTF-8"); - response.setContentType("text/html"); - PrintWriter out = response.getWriter(); - out.println(""); - out.println(""); - out.println(" 文件下载"); - out.println(" "); - out.println("
"); - - out.println("
文件下载"); - out.println(""); - out.println(" "); - out.println(" "); - out.println(" "); - out.println(" "); - out.println(" "); - - for(Entry entry : map.entrySet()){ - out.println(""); - out.println(" "); - out.println(" "); - out.println(" "); - out.println(""); - } - - out.println("
文件名下载次数下载
" + entry.getKey() + "" + entry.getValue() + "下载
"); - out.println(" "); - out.println(" "); - out.println(""); - out.flush(); - out.close(); - } - - } - - @Override - public void destroy() { - map = null; - } - -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/RequestServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/RequestServlet.java deleted file mode 100644 index 8ee47db8..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/RequestServlet.java +++ /dev/null @@ -1,168 +0,0 @@ -package io.github.dunwu.javaee.servlet; - -import java.io.IOException; -import java.io.PrintWriter; -import java.security.Principal; -import java.util.Locale; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * 获取HttpServletRequest信息示例 - */ -public class RequestServlet extends HttpServlet { - private static final long serialVersionUID = -7936817351382556277L; - - private final Logger logger = LoggerFactory.getLogger(this.getClass()); - - /** - * @param accept - * @return 客户端浏览器接受的文件类型 - */ - private String getAccept(String accept) { - StringBuffer buffer = new StringBuffer(); - if (accept.contains("image/gif")) - buffer.append("GIF 文件, "); - if (accept.contains("image/x-xbitmap")) - buffer.append("BMP 文件, "); - if (accept.contains("image/jpeg")) - buffer.append("JPG 文件, "); - if (accept.contains("application/vnd.ms-excel")) - buffer.append("Excel 文件, "); - if (accept.contains("application/vnd.ms-powerpoint")) - buffer.append("PPT 文件, "); - if (accept.contains("application/msword")) - buffer.append("Word 文件, "); - return buffer.toString().replaceAll(", $", ""); - } - - /** - * @param locale - * @return 语言环境名称 - */ - private String getLocale(Locale locale) { - if (Locale.SIMPLIFIED_CHINESE.equals(locale)) - return "简体中文"; - if (Locale.TRADITIONAL_CHINESE.equals(locale)) - return "繁体中文"; - if (Locale.ENGLISH.equals(locale)) - return "英文"; - if (Locale.JAPANESE.equals(locale)) - return "日文"; - return "未知语言环境"; - } - - /** - * @param userAgent - * @return 客户端浏览器信息 - */ - private String getNavigator(String userAgent) { - if (userAgent.indexOf("TencentTraveler") > 0) - return "腾讯浏览器"; - if (userAgent.indexOf("Maxthon") > 0) - return "Maxthon浏览器"; - if (userAgent.indexOf("MyIE2") > 0) - return "MyIE2浏览器"; - if (userAgent.indexOf("Firefox") > 0) - return "Firefox浏览器"; - if (userAgent.indexOf("MSIE") > 0) - return "IE 浏览器"; - return "未知浏览器"; - } - - /** - * @param userAgent - * @return 客户端操作系统 - */ - private String getOS(String userAgent) { - if (userAgent.indexOf("Windows NT 5.1") > 0) - return "Windows XP"; - if (userAgent.indexOf("Windows 98") > 0) - return "Windows 98"; - if (userAgent.indexOf("Windows NT 5.0") > 0) - return "Windows 2000"; - if (userAgent.indexOf("Linux") > 0) - return "Linux"; - if (userAgent.indexOf("Unix") > 0) - return "Unix"; - return "未知"; - } - - @Override - public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - logger.info("访问 doGet"); - - request.setCharacterEncoding("UTF-8"); - response.setCharacterEncoding("UTF-8"); - - response.setContentType("text/html"); - - String authType = request.getAuthType(); - String localAddr = request.getLocalAddr(); - Locale locale = request.getLocale(); - String localName = request.getLocalName(); - String contextPath = request.getContextPath(); - int localPort = request.getLocalPort(); - String method = request.getMethod(); - String pathInfo = request.getPathInfo(); - String pathTranslated = request.getPathTranslated(); - String protocol = request.getProtocol(); - String queryString = request.getQueryString(); - String remoteAddr = request.getRemoteAddr(); - int port = request.getRemotePort(); - String remoteUser = request.getRemoteUser(); - String requestedSessionId = request.getRequestedSessionId(); - String requestURI = request.getRequestURI(); - StringBuffer requestURL = request.getRequestURL(); - String scheme = request.getScheme(); - String serverName = request.getServerName(); - int serverPort = request.getServerPort(); - String servletPath = request.getServletPath(); - Principal userPrincipal = request.getUserPrincipal(); - - String accept = request.getHeader("accept"); - String referer = request.getHeader("referer"); - String userAgent = request.getHeader("user-agent"); - - String serverInfo = this.getServletContext().getServerInfo(); - - PrintWriter out = response.getWriter(); - out.println(""); - out.println(""); - - // 这里之间的信息在浏览器中显示为标题 - out.println(" Request Servlet"); - out.println(" "); - out.println(" "); - - out.println("您的IP为 " + remoteAddr + ";您使用 " + getOS(userAgent) + " 操作系统," - + getNavigator(userAgent) + " 。您使用 " + getLocale(locale) + "。
"); - out.println("服务器IP为 " + localAddr + localAddr + ";服务器使用 " + serverPort + " 端口,您的浏览器使用了 " - + port + " 端口访问本网页。
"); - out.println("服务器软件为:" + serverInfo + "。服务器名称为 " + localName + "。
"); - out.println("您的浏览器接受 " + getAccept(accept) + "。
"); - out.println("您从 " + referer + " 访问到该页面。
"); - out.println("使用的协议为 " + protocol + "。URL协议头 " + scheme + ",服务器名称 " + serverName - + ",您访问的URI为 " + requestURI + "。
"); - out.println("该 Servlet 路径为 " + servletPath + ",该 Servlet 类名为 " + this.getClass().getName() - + "。
"); - out.println("本应用程序在硬盘的根目录为 " + this.getServletContext().getRealPath("") + ",网络相对路径为 " - + contextPath + "。
"); - - out.println("
"); - - out.println("

点击刷新本页面 "); - - out.println(" "); - out.println(""); - out.flush(); - out.close(); - } - -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/ThreadSafetyServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/ThreadSafetyServlet.java deleted file mode 100644 index b498dbaa..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/ThreadSafetyServlet.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.github.dunwu.javaee.servlet; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -public class ThreadSafetyServlet extends HttpServlet { - - private static final long serialVersionUID = 2957055449370562943L; - - private String name; - - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - name = request.getParameter("name"); - - try { - Thread.sleep(5000); - } catch (InterruptedException e) { - } - - response.getWriter().println("您好, " + name + ". 您使用了 GET 方式提交数据"); - } - - public void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - name = request.getParameter("name"); - - response.getWriter().println("您好, " + name + ". 您使用了 POST 方式提交数据"); - } -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/UploadServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/UploadServlet.java deleted file mode 100644 index 2e7421fe..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/UploadServlet.java +++ /dev/null @@ -1,179 +0,0 @@ -package io.github.dunwu.javaee.servlet; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.net.URLEncoder; -import java.util.List; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.fileupload.DiskFileUpload; -import org.apache.commons.fileupload.FileItem; -import org.apache.commons.fileupload.FileUploadException; - -public class UploadServlet extends HttpServlet { - - private static final long serialVersionUID = 7523024737218332088L; - - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - response.setCharacterEncoding("UTF-8"); - response.getWriter().println("请以 POST 方式上传文件"); - } - - @SuppressWarnings("unchecked") - public void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - File file1 = null, file2 = null; - String description1 = null, description2 = null; - - response.setCharacterEncoding("UTF-8"); - response.setContentType("text/html"); - PrintWriter out = response.getWriter(); - out.println(""); - out.println(""); - out.println(" A Servlet"); - out.println(" "); - out.println(" "); - - out.println("

"); - out.println("
上传文件
"); - - out.println("
"); - out.println("
上传日志:
"); - out.println("
"); - - // 使用 DiskFileUpload 对象解析 request - DiskFileUpload diskFileUpload = new DiskFileUpload(); - try { - // 将 解析的结果 放置在 List 中 - List list = diskFileUpload.parseRequest(request); - out.println("遍历所有的 FileItem ...
"); - // 遍历 list 中所有的 FileItem - for(FileItem fileItem : list){ - if(fileItem.isFormField()){ - // 如果是 文本域 - if("description1".equals(fileItem.getFieldName())){ - // 如果该 FileItem 名称为 description1 - out.println("遍历到 description1 ...
"); - description1 = new String(fileItem.getString().getBytes(), "UTF-8"); - } - if("description2".equals(fileItem.getFieldName())){ - // 如果该 FileItem 名称为 description2 - out.println("遍历到 description2 ...
"); - description2 = new String(fileItem.getString().getBytes(), "UTF-8"); - } - } - else{ - // 否则,为文件域 - if("file1".equals(fileItem.getFieldName())){ - // 客户端文件路径构建的 File 对象 - File remoteFile = new File(new String(fileItem.getName().getBytes(), "UTF-8")); - out.println("遍历到 file1 ...
"); - out.println("客户端文件位置: " + remoteFile.getAbsolutePath() + "
"); - // 服务器端文件,放在 upload 文件夹下 - file1 = new File(this.getServletContext().getRealPath("attachment"), remoteFile.getName()); - file1.getParentFile().mkdirs(); - file1.createNewFile(); - - // 写文件,将 FileItem 的文件内容写到文件中 - InputStream ins = fileItem.getInputStream(); - OutputStream ous = new FileOutputStream(file1); - - try{ - byte[] buffer = new byte[1024]; - int len = 0; - while((len=ins.read(buffer)) > -1) - ous.write(buffer, 0, len); - out.println("已保存文件" + file1.getAbsolutePath() + "
"); - }finally{ - ous.close(); - ins.close(); - } - } - if("file2".equals(fileItem.getFieldName())){ - // 客户端文件路径构建的 File 对象 - File remoteFile = new File(new String(fileItem.getName().getBytes(), "UTF-8")); - out.println("遍历到 file2 ...
"); - out.println("客户端文件位置: " + remoteFile.getAbsolutePath() + "
"); - // 服务器端文件,放在 upload 文件夹下 - file2 = new File(this.getServletContext().getRealPath("attachment"), remoteFile.getName()); - file2.getParentFile().mkdirs(); - file2.createNewFile(); - - // 写文件,将 FileItem 的文件内容写到文件中 - InputStream ins = fileItem.getInputStream(); - OutputStream ous = new FileOutputStream(file2); - - try{ - byte[] buffer = new byte[1024]; - int len = 0; - while((len=ins.read(buffer)) > -1) - ous.write(buffer, 0, len); - out.println("已保存文件" + file2.getAbsolutePath() + "
"); - }finally{ - ous.close(); - ins.close(); - } - } - } - } - out.println("Request 解析完毕"); - } catch (FileUploadException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - out.println("
"); - out.println("
"); - - if(file1 != null){ - out.println("
"); - out.println("
file1:
"); - out.println("
"); - out.println(" " + file1.getName() + "" ); - out.println("
"); - out.println("
"); - } - - if(file2 != null){ - out.println("
"); - out.println("
file2:
"); - out.println("
"); - out.println(" " + file2.getName() + "" ); - out.println("
"); - out.println("
"); - } - - - out.println("
"); - out.println("
description1:
"); - out.println("
"); - out.println(description1); - out.println("
"); - out.println("
"); - - out.println("
"); - out.println("
description2:
"); - out.println("
"); - out.println(description2); - out.println("
"); - out.println("
"); - - out.println("
"); - - out.println(" "); - out.println(""); - out.flush(); - out.close(); - } - -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/ProgressUploadServlet.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/ProgressUploadServlet.java deleted file mode 100644 index a71abaab..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/ProgressUploadServlet.java +++ /dev/null @@ -1,131 +0,0 @@ -package io.github.dunwu.javaee.servlet.upload; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Iterator; -import java.util.List; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.commons.fileupload.FileItem; -import org.apache.commons.fileupload.disk.DiskFileItemFactory; -import org.apache.commons.fileupload.servlet.ServletFileUpload; - -public class ProgressUploadServlet extends HttpServlet { - - private static final long serialVersionUID = -4935921396709035718L; - - public void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - // 上传状态 - UploadStatus status = new UploadStatus(); - - // 监听器 - UploadListener listener = new UploadListener(status); - - // 把 UploadStatus 放到 session 里 - request.getSession(true).setAttribute("uploadStatus", status); - - // Apache 上传工具 - ServletFileUpload upload = new ServletFileUpload( - new DiskFileItemFactory()); - - // 设置 listener - upload.setProgressListener(listener); - - try { - List itemList = upload.parseRequest(request); - - for (Iterator it = itemList.iterator(); it.hasNext();) { - FileItem item = (FileItem) it.next(); - if (item.isFormField()) { - System.out.println("FormField: " + item.getFieldName() - + " = " + item.getString()); - } else { - System.out.println("File: " + item.getName()); - - // 统一 Linux 与 windows 的路径分隔符 - String fileName = item.getName().replace("/", "\\"); - fileName = fileName.substring(fileName.lastIndexOf("\\")); - - File saved = new File("C:\\upload_test", fileName); - saved.getParentFile().mkdirs(); - - InputStream ins = item.getInputStream(); - OutputStream ous = new FileOutputStream(saved); - - byte[] tmp = new byte[1024]; - int len = -1; - - while ((len = ins.read(tmp)) != -1) { - ous.write(tmp, 0, len); - } - - ous.close(); - ins.close(); - - response.getWriter().println("已保存文件:" + saved); - } - } - } catch (Exception e) { - e.printStackTrace(); - response.getWriter().println("上传发生错误:" + e.getMessage()); - } - } - - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - response.setHeader("Cache-Control", "no-store"); - response.setHeader("Pragrma", "no-cache"); - response.setDateHeader("Expires", 0); - - UploadStatus status = (UploadStatus) request.getSession(true) - .getAttribute("uploadStatus"); - - if (status == null) { - response.getWriter().println("没有上传信息"); - return; - } - - long startTime = status.getStartTime(); - long currentTime = System.currentTimeMillis(); - - // 已传输的时间 单位:s - long time = (currentTime - startTime) / 1000 + 1; - - // 传输速度 单位:byte/s - double velocity = ((double) status.getBytesRead()) / (double) time; - - // 估计总时间 单位:s - double totalTime = status.getContentLength() / velocity; - - // 估计剩余时间 单位:s - double timeLeft = totalTime - time; - - // 已完成的百分比 - int percent = (int) (100 * (double) status.getBytesRead() / (double) status - .getContentLength()); - - // 已完成数 单位:M - double length = ((double) status.getBytesRead()) / 1024 / 1024; - - // 总长度 单位:M - double totalLength = ((double) status.getContentLength()) / 1024 / 1024; - - // 格式:百分比||已完成数(M)||文件总长度(M)||传输速率(K)||已用时间(s)||估计总时间(s)||估计剩余时间(s)||正在上传第几个文件 - String value = percent + "||" + length + "||" + totalLength + "||" - + velocity + "||" + time + "||" + totalTime + "||" + timeLeft - + "||" + status.getItems(); - - response.getWriter().println(value); - } - -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/UploadListener.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/UploadListener.java deleted file mode 100644 index 94708317..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/UploadListener.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.dunwu.javaee.servlet.upload; - -import org.apache.commons.fileupload.ProgressListener; - -public class UploadListener implements ProgressListener { - - private UploadStatus status; - - public UploadListener(UploadStatus status) { - this.status = status; - } - - public void update(long bytesRead, long contentLength, int items) { - status.setBytesRead(bytesRead); - status.setContentLength(contentLength); - status.setItems(items); - } -} diff --git a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/UploadStatus.java b/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/UploadStatus.java deleted file mode 100644 index 0483f070..00000000 --- a/codes/javaee/servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/UploadStatus.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.github.dunwu.javaee.servlet.upload; - -public class UploadStatus { - - private long bytesRead; - - private long contentLength; - - private int items; - - private long startTime = System.currentTimeMillis(); - - public long getBytesRead() { - return bytesRead; - } - - public void setBytesRead(long bytesRead) { - this.bytesRead = bytesRead; - } - - public long getContentLength() { - return contentLength; - } - - public void setContentLength(long contentLength) { - this.contentLength = contentLength; - } - - public int getItems() { - return items; - } - - public void setItems(int items) { - this.items = items; - } - - public long getStartTime() { - return startTime; - } - - public void setStartTime(long startTime) { - this.startTime = startTime; - } - -} diff --git a/codes/javaee/servlet/src/main/resources/logback.xml b/codes/javaee/servlet/src/main/resources/logback.xml deleted file mode 100644 index c7c8cd2d..00000000 --- a/codes/javaee/servlet/src/main/resources/logback.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - - %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 - - - - - - - - - - - - - - - - - diff --git a/codes/javaee/servlet/src/main/webapp/META-INF/MANIFEST.MF b/codes/javaee/servlet/src/main/webapp/META-INF/MANIFEST.MF deleted file mode 100644 index 254272e1..00000000 --- a/codes/javaee/servlet/src/main/webapp/META-INF/MANIFEST.MF +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0 -Class-Path: - diff --git a/codes/javaee/servlet/src/main/webapp/WEB-INF/notice.html b/codes/javaee/servlet/src/main/webapp/WEB-INF/notice.html deleted file mode 100644 index c6af966b..00000000 --- a/codes/javaee/servlet/src/main/webapp/WEB-INF/notice.html +++ /dev/null @@ -1,79 +0,0 @@ - - - - -Eclipse.org Software User Agreement - - - -

Eclipse Foundation Software User Agreement

-

March 17, 2005

- -

Usage Of Content

- -

THE ECLIPSE FOUNDATION MAKES AVAILABLE SOFTWARE, DOCUMENTATION, INFORMATION AND/OR OTHER MATERIALS FOR OPEN SOURCE PROJECTS - (COLLECTIVELY "CONTENT"). USE OF THE CONTENT IS GOVERNED BY THE TERMS AND CONDITIONS OF THIS AGREEMENT AND/OR THE TERMS AND - CONDITIONS OF LICENSE AGREEMENTS OR NOTICES INDICATED OR REFERENCED BELOW. BY USING THE CONTENT, YOU AGREE THAT YOUR USE - OF THE CONTENT IS GOVERNED BY THIS AGREEMENT AND/OR THE TERMS AND CONDITIONS OF ANY APPLICABLE LICENSE AGREEMENTS OR - NOTICES INDICATED OR REFERENCED BELOW. IF YOU DO NOT AGREE TO THE TERMS AND CONDITIONS OF THIS AGREEMENT AND THE TERMS AND - CONDITIONS OF ANY APPLICABLE LICENSE AGREEMENTS OR NOTICES INDICATED OR REFERENCED BELOW, THEN YOU MAY NOT USE THE CONTENT.

- -

Applicable Licenses

- -

Unless otherwise indicated, all Content made available by the Eclipse Foundation is provided to you under the terms and conditions of the Eclipse Public License Version 1.0 - ("EPL"). A copy of the EPL is provided with this Content and is also available at http://www.eclipse.org/legal/epl-v10.html. - For purposes of the EPL, "Program" will mean the Content.

- -

Content includes, but is not limited to, source code, object code, documentation and other files maintained in the Eclipse.org CVS repository ("Repository") in CVS - modules ("Modules") and made available as downloadable archives ("Downloads").

- -
    -
  • Content may be structured and packaged into modules to facilitate delivering, extending, and upgrading the Content. Typical modules may include plug-ins ("Plug-ins"), plug-in fragments ("Fragments"), and features ("Features").
  • -
  • Each Plug-in or Fragment may be packaged as a sub-directory or JAR (Java™ ARchive) in a directory named "plugins".
  • -
  • A Feature is a bundle of one or more Plug-ins and/or Fragments and associated material. Each Feature may be packaged as a sub-directory in a directory named "features". Within a Feature, files named "feature.xml" may contain a list of the names and version numbers of the Plug-ins - and/or Fragments associated with that Feature.
  • -
  • Features may also include other Features ("Included Features"). Within a Feature, files named "feature.xml" may contain a list of the names and version numbers of Included Features.
  • -
- -

The terms and conditions governing Plug-ins and Fragments should be contained in files named "about.html" ("Abouts"). The terms and conditions governing Features and -Included Features should be contained in files named "license.html" ("Feature Licenses"). Abouts and Feature Licenses may be located in any directory of a Download or Module -including, but not limited to the following locations:

- -
    -
  • The top-level (root) directory
  • -
  • Plug-in and Fragment directories
  • -
  • Inside Plug-ins and Fragments packaged as JARs
  • -
  • Sub-directories of the directory named "src" of certain Plug-ins
  • -
  • Feature directories
  • -
- -

Note: if a Feature made available by the Eclipse Foundation is installed using the Eclipse Update Manager, you must agree to a license ("Feature Update License") during the -installation process. If the Feature contains Included Features, the Feature Update License should either provide you with the terms and conditions governing the Included Features or -inform you where you can locate them. Feature Update Licenses may be found in the "license" property of files named "feature.properties" found within a Feature. -Such Abouts, Feature Licenses, and Feature Update Licenses contain the terms and conditions (or references to such terms and conditions) that govern your use of the associated Content in -that directory.

- -

THE ABOUTS, FEATURE LICENSES, AND FEATURE UPDATE LICENSES MAY REFER TO THE EPL OR OTHER LICENSE AGREEMENTS, NOTICES OR TERMS AND CONDITIONS. SOME OF THESE -OTHER LICENSE AGREEMENTS MAY INCLUDE (BUT ARE NOT LIMITED TO):

- - - -

IT IS YOUR OBLIGATION TO READ AND ACCEPT ALL SUCH TERMS AND CONDITIONS PRIOR TO USE OF THE CONTENT. If no About, Feature License, or Feature Update License is provided, please -contact the Eclipse Foundation to determine what terms and conditions govern that particular Content.

- -

Cryptography

- -

Content may contain encryption software. The country in which you are currently may have restrictions on the import, possession, and use, and/or re-export to - another country, of encryption software. BEFORE using any encryption software, please check the country's laws, regulations and policies concerning the import, - possession, or use, and re-export of encryption software, to see if this is permitted.

- -Java and all Java-based trademarks are trademarks of Sun Microsystems, Inc. in the United States, other countries, or both. - - diff --git a/codes/javaee/servlet/src/main/webapp/WEB-INF/web.xml b/codes/javaee/servlet/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 4ca585b8..00000000 --- a/codes/javaee/servlet/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,216 +0,0 @@ - - - - - FirstServlet - - io.github.dunwu.javaee.servlet.FirstServlet - - - message - welcome to FirstServlet - - - encoding - utf-8 - - 1 - - - FirstServlet - /servlet/FirstServlet - /servlet/FirstServlet.asp - /servlet/FirstServlet.jsp - /servlet/FirstServlet.php - /servlet/FirstServlet.aspx - - - - RequestServlet - - io.github.dunwu.javaee.servlet.RequestServlet - - - - RequestServlet - /servlet/RequestServlet - - - - InitParamServlet - - io.github.dunwu.javaee.servlet.InitParamServlet - - - root - root - - - admin - admin - - - - InitParamServlet - /servlet/InitParamServlet - - - - - upload folder - attachment - - - allowed file type - .gif,.jpg,.bmp - - - ContextParamServlet - - io.github.dunwu.javaee.servlet.ContextParamServlet - - - - ContextParamServlet - /servlet/ContextParamServlet - - - - InjectionServlet - - io.github.dunwu.javaee.servlet.InjectionServlet - - - - InjectionServlet - /servlet/InjectionServlet - - - - PostServlet - - io.github.dunwu.javaee.servlet.PostServlet - - - - PostServlet - /servlet/PostServlet - - - - ImageServlet - - io.github.dunwu.javaee.servlet.ImageServlet - - 1 - - - UploadServlet - - io.github.dunwu.javaee.servlet.UploadServlet - - - - LifeCycleServlet - - io.github.dunwu.javaee.servlet.LifeCycleServlet - - - startPoint - 2000 - - - - AnnotationServlet - - io.github.dunwu.javaee.servlet.AnnotationServlet - - - - ForwardServlet - - io.github.dunwu.javaee.servlet.ForwardServlet - - - - RedirectServlet - - io.github.dunwu.javaee.servlet.RedirectServlet - - - - ProgressUploadServlet - - io.github.dunwu.javaee.servlet.upload.ProgressUploadServlet - - - - ThreadSafetyServlet - - io.github.dunwu.javaee.servlet.ThreadSafetyServlet - - - - - ImageServlet - /upload/* - - - UploadServlet - /servlet/UploadServlet - - - LifeCycleServlet - /servlet/LifeCycleServlet - - - AnnotationServlet - /servlet/AnnotationServlet - - - ForwardServlet - /servlet/ForwardServlet - - - RedirectServlet - /servlet/RedirectServlet - - - ProgressUploadServlet - /servlet/ProgressUploadServlet - - - ThreadSafetyServlet - /servlet/ThreadSafetyServlet - - - - index.jsp - - - - hello - java.lang.String - - Hello, Welcome to the JavaEE Resource Injection. - - - - - i - java.lang.Integer - 30 - - - - persons - java.lang.String - - Helloween, Cobain, Roses, Axl, - - - diff --git a/codes/javaee/servlet/src/main/webapp/index.jsp b/codes/javaee/servlet/src/main/webapp/index.jsp deleted file mode 100644 index 21f6a540..00000000 --- a/codes/javaee/servlet/src/main/webapp/index.jsp +++ /dev/null @@ -1,33 +0,0 @@ -<%@ page contentType="text/html;charset=UTF-8" language="java" %> - - - javaee-web - - -

首页

-

欢迎访问

-

测试链接

- - - diff --git a/codes/javaee/servlet/src/main/webapp/views/jsp/postPersonalInformation.html b/codes/javaee/servlet/src/main/webapp/views/jsp/postPersonalInformation.html deleted file mode 100644 index b643fcda..00000000 --- a/codes/javaee/servlet/src/main/webapp/views/jsp/postPersonalInformation.html +++ /dev/null @@ -1,120 +0,0 @@ - - - -提交用户信息 - - - - -
-
-
-
- 填写用户信息 -
-
-
请填写您的姓名:
-
- -
-
-
-
请填写您的密码:
-
- -
-
-
-
请再次输入密码:
-
- -
-
-
-
请选择性别:
-
- - - - -
-
-
-
请输入年龄:
-
- -
-
-
-
请输入生日:
-
- -
格式:"yyyy-mm-dd" -
-
-
-
请选择您的爱好
-
- - - - - - -
-
-
-
请选择省市:
-
- -
-
-
-
自我描述:
-
- -
-
-
-
-
-

-
-
-
-
-
- - diff --git a/codes/javaee/servlet/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java b/codes/javaee/servlet/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java deleted file mode 100644 index 373e9e8c..00000000 --- a/codes/javaee/servlet/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.github.dunwu.javaee.server; - -import org.apache.commons.lang3.StringUtils; -import org.assertj.core.util.Lists; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.WebAppClassLoader; -import org.eclipse.jetty.webapp.WebAppContext; - -import java.util.ArrayList; - -/** - * JettyFactory 可以工作在 Eclipse 和 Intellij 中,用来启动 jetty 服务。 Intellij - * 并不支持jetty,所以要想类似eclipse一样的使用jetty,需要配置webdefault.xml。 - * - * @author Zhang Peng - */ -@SuppressWarnings("unused") -public class JettyFactory { - private static final int PORT = 8080; - private static final String CONTEXT = "/"; - private static final String RESOURCE_BASE_PATH = "src/main/webapp"; - private static final String WEB_XML_PATH = "/WEB-INF/web.xml"; - private static final String[] TLD_JAR_NAMES = - new String[]{"sitemesh", "spring-webmvc", "shiro-web", "tiles"}; - private static final String WINDOWS_WEBDEFAULT_PATH = "jetty/webdefault.xml"; - - public static final int IDE_ECLIPSE = 0; - public static final int IDE_INTELLIJ = 1; - - - public static Server initServer() { - Profiles.setProfileAsSystemProperty(Profiles.DEVELOPMENT); - WebAppContext webAppContext = new WebAppContext(); - Server server = new Server(PORT); - server.setHandler(webAppContext); - return server; - } - - public static void initWebAppContext(Server server, int type) throws Exception { - System.out.println("[INFO] Application loading"); - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - webAppContext.setContextPath(CONTEXT); - webAppContext.setResourceBase(getAbsolutePath() + RESOURCE_BASE_PATH); - webAppContext.setDescriptor(getAbsolutePath() + RESOURCE_BASE_PATH + WEB_XML_PATH); - - if (IDE_INTELLIJ == type) { - webAppContext.setDefaultsDescriptor(WINDOWS_WEBDEFAULT_PATH); - supportJspAndSetTldJarNames(server, TLD_JAR_NAMES); - } else { - webAppContext.setParentLoaderPriority(true); - } - - System.out.println("[INFO] Application loaded"); - } - - public static void reloadWebAppContext(Server server) throws Exception { - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - System.out.println("[INFO] Application reloading"); - webAppContext.stop(); - WebAppClassLoader classLoader = new WebAppClassLoader(webAppContext); - classLoader.addClassPath(getAbsolutePath() + "target/classes"); - classLoader.addClassPath(getAbsolutePath() + "target/test-classes"); - webAppContext.setClassLoader(classLoader); - webAppContext.start(); - System.out.println("[INFO] Application reloaded"); - } - - - public static void startServer(Server server) throws Exception { - System.out.println("[HINT] Don't forget to set -XX:MaxPermSize=128m"); - server.start(); - System.out.println("Server running at http://localhost:" + PORT + CONTEXT); - System.out.println("[HINT] Hit Enter to reload the application quickly"); - } - - public static String getAbsolutePath() { - String path = null; - String folderPath = JettyFactory.class.getProtectionDomain().getCodeSource().getLocation() - .getPath().substring(1); - if (folderPath.indexOf("target") > 0) { - path = folderPath.substring(0, folderPath.indexOf("target")); - } - return path; - } - - public static void supportJspAndSetTldJarNames(Server server, String... jarNames) { - WebAppContext context = (WebAppContext) server.getHandler(); - // This webapp will use jsps and jstl. We need to enable the AnnotationConfiguration in - // order to correctly set up the jsp container - org.eclipse.jetty.webapp.Configuration.ClassList classlist = - org.eclipse.jetty.webapp.Configuration.ClassList.setServerDefault(server); - classlist.addBefore("org.eclipse.jetty.webapp.JettyWebXmlConfiguration", - "org.eclipse.jetty.annotations.AnnotationConfiguration"); - // Set the ContainerIncludeJarPattern so that jetty examines these container-path jars for - // tlds, web-fragments etc. - // If you omit the jar that contains the jstl .tlds, the jsp engine will scan for them - // instead. - ArrayList jarNameExprssions = Lists.newArrayList(".*/[^/]*servlet-api-[^/]*\\.jar$", - ".*/javax.servlet.jsp.jstl-.*\\.jar$", ".*/[^/]*taglibs.*\\.jar$"); - - for (String jarName : jarNames) { - jarNameExprssions.add(".*/" + jarName + "-[^/]*\\.jar$"); - } - - context.setAttribute("org.eclipse.jetty.io.github.dunwu.javaee.server.webapp.ContainerIncludeJarPattern", - StringUtils.join(jarNameExprssions, '|')); - } -} diff --git a/codes/javaee/servlet/src/test/java/io/github/dunwu/javaee/server/Profiles.java b/codes/javaee/servlet/src/test/java/io/github/dunwu/javaee/server/Profiles.java deleted file mode 100644 index 3a8ee7c4..00000000 --- a/codes/javaee/servlet/src/test/java/io/github/dunwu/javaee/server/Profiles.java +++ /dev/null @@ -1,29 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2005, 2014 springside.github.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - *******************************************************************************/ -package io.github.dunwu.javaee.server; - -/** - * Spring profile 常用方法与profile名称。 - * - * @author calvin - */ -public class Profiles { - - public static final String ACTIVE_PROFILE = "spring.profiles.active"; - public static final String DEFAULT_PROFILE = "spring.profiles.default"; - - public static final String PRODUCTION = "production"; - public static final String DEVELOPMENT = "development"; - public static final String UNIT_TEST = "test"; - public static final String FUNCTIONAL_TEST = "functional"; - - /** - * 在Spring启动前,设置profile的环境变量。 - */ - public static void setProfileAsSystemProperty(String profile) { - System.setProperty(ACTIVE_PROFILE, profile); - } -} diff --git a/codes/javaee/servlet/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java b/codes/javaee/servlet/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java deleted file mode 100644 index 2936b162..00000000 --- a/codes/javaee/servlet/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.github.dunwu.javaee.server; - -import org.eclipse.jetty.server.Server; - -/** - * 快速启动 jetty 服务器,方便测试 - * - * @author Zhang Peng - */ -public class QuickStartServer { - // private static int STARTUP_TYPE = JettyFactory.IDE_ECLIPSE; - private static int STARTUP_TYPE = JettyFactory.IDE_INTELLIJ; - - public static void main(String[] args) throws Exception { - Server server = JettyFactory.initServer(); - JettyFactory.initWebAppContext(server, STARTUP_TYPE); - - try { - JettyFactory.startServer(server); - - // 等待用户输入回车重载应用 - while (true) { - char c = (char) System.in.read(); - if (c == '\n') { - JettyFactory.reloadWebAppContext(server); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.exit(-1); - } - } -} diff --git a/codes/javaee/servlet/src/test/resources/jetty/webdefault.xml b/codes/javaee/servlet/src/test/resources/jetty/webdefault.xml deleted file mode 100644 index 65ec9cae..00000000 --- a/codes/javaee/servlet/src/test/resources/jetty/webdefault.xml +++ /dev/null @@ -1,534 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Default web.xml file. - This file is applied to a Web application before it's own WEB_INF/web.xml file - - - - - - - - org.eclipse.jetty.servlet.listener.ELContextCleaner - - - - - - - - org.eclipse.jetty.servlet.listener.IntrospectorCleaner - - - - - - - - - - - - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - aliases - false - - - acceptRanges - true - - - dirAllowed - true - - - welcomeServlets - false - - - redirectWelcome - false - - - maxCacheSize - 256000000 - - - maxCachedFileSize - 200000000 - - - maxCachedFiles - 2048 - - - gzip - false - - - etags - false - - - useFileMappedBuffer - false - - - - 0 - - - - default - / - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jsp - org.eclipse.jetty.jsp.JettyJspServlet - - logVerbosityLevel - DEBUG - - - fork - false - - - xpoweredBy - false - - - compilerTargetVM - 1.7 - - - compilerSourceVM - 1.7 - - - 0 - - - - jsp - *.jsp - *.jspf - *.jspx - *.xsp - *.JSP - *.JSPF - *.JSPX - *.XSP - - - - - - - - 30 - - - - - - - - - - - - - - - index.html - index.htm - index.jsp - - - - - - - - ar - ISO-8859-6 - - - be - ISO-8859-5 - - - bg - ISO-8859-5 - - - ca - ISO-8859-1 - - - cs - ISO-8859-2 - - - da - ISO-8859-1 - - - de - ISO-8859-1 - - - el - ISO-8859-7 - - - en - ISO-8859-1 - - - es - ISO-8859-1 - - - et - ISO-8859-1 - - - fi - ISO-8859-1 - - - fr - ISO-8859-1 - - - hr - ISO-8859-2 - - - hu - ISO-8859-2 - - - is - ISO-8859-1 - - - it - ISO-8859-1 - - - iw - ISO-8859-8 - - - ja - Shift_JIS - - - ko - EUC-KR - - - lt - ISO-8859-2 - - - lv - ISO-8859-2 - - - mk - ISO-8859-5 - - - nl - ISO-8859-1 - - - no - ISO-8859-1 - - - pl - ISO-8859-2 - - - pt - ISO-8859-1 - - - ro - ISO-8859-2 - - - ru - ISO-8859-5 - - - sh - ISO-8859-5 - - - sk - ISO-8859-2 - - - sl - ISO-8859-2 - - - sq - ISO-8859-2 - - - sr - ISO-8859-5 - - - sv - ISO-8859-1 - - - tr - ISO-8859-9 - - - uk - ISO-8859-5 - - - zh - GB2312 - - - zh_TW - Big5 - - - - - - - - - Disable TRACE - / - TRACE - - - - - - Enable everything but TRACE - / - TRACE - - - - - diff --git a/codes/javaee/servlet/src/test/resources/logback.xml b/codes/javaee/servlet/src/test/resources/logback.xml deleted file mode 100644 index 69f4fbae..00000000 --- a/codes/javaee/servlet/src/test/resources/logback.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - logs/${FILE_NAME}-all.%d{yyyy-MM-dd}.log - 30 - - - - - 30MB - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - - - - - - - - - - - diff --git a/codes/javaee/session/pom.xml b/codes/javaee/session/pom.xml deleted file mode 100644 index 4f7d81e6..00000000 --- a/codes/javaee/session/pom.xml +++ /dev/null @@ -1,122 +0,0 @@ - - 4.0.0 - - - io.github.dunwu - javaee-notes-session - 1.0.0 - war - - - - - javaee-notes-session - javaee 学习笔记之 session - - - - - - UTF-8 - 1.7 - ${java.version} - ${java.version} - - - 9.3.2.v20150730 - - - - - ch.qos.logback - logback-classic - 1.1.3 - - - ch.qos.logback - logback-core - 1.1.3 - - - org.logback-extensions - logback-ext-spring - 0.1.2 - - - org.slf4j - jcl-over-slf4j - 1.7.12 - - - - - - commons-fileupload - commons-fileupload - 1.3.1 - - - commons-io - commons-io - 2.5 - - - org.apache.commons - commons-lang3 - 3.4 - - - - - - javax.servlet - javax.servlet-api - 3.1.0 - - - - - - org.eclipse.jetty - jetty-webapp - ${jetty.version} - - - org.eclipse.jetty - jetty-server - ${jetty.version} - - - org.eclipse.jetty - jetty-annotations - ${jetty.version} - - - org.eclipse.jetty - apache-jsp - ${jetty.version} - - - - - - junit - junit - 4.12 - - - org.assertj - assertj-core - 3.4.1 - - - - - - - - - - - diff --git a/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/AddCookies.java b/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/AddCookies.java deleted file mode 100644 index b41be2cd..00000000 --- a/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/AddCookies.java +++ /dev/null @@ -1,70 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.cookie; - -import java.io.IOException; -import java.io.PrintWriter; -import java.net.URLEncoder; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * @author Zhang Peng - * @date 2017/3/26. - */ -@WebServlet("/servlet/AddCookies") -public class AddCookies extends HttpServlet { - private static final long serialVersionUID = 1L; - - /** - * @see HttpServlet#HttpServlet() - */ - public AddCookies() { - super(); - } - - /** - * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) - */ - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - // 为名字和姓氏创建 Cookie - Cookie name = new Cookie("name", URLEncoder.encode(request.getParameter("name"), "UTF-8")); // 中文转码 - Cookie url = new Cookie("url", request.getParameter("url")); - - // 为两个 Cookie 设置过期日期为 24 小时后 - name.setMaxAge(60 * 60 * 24); - url.setMaxAge(60 * 60 * 24); - - // 在响应头中添加两个 Cookie - response.addCookie(name); - response.addCookie(url); - - // 设置响应内容类型 - response.setContentType("text/html;charset=UTF-8"); - - PrintWriter out = response.getWriter(); - String title = "设置 Cookie 实例"; - String docType = "\n"; - out.println(docType + "\n" + "" + title + "\n" - + "\n" + "

" + title - + "

\n" + "
    \n" + "
  • 站点名::" + request.getParameter("name") - + "\n
  • " + "
  • 站点 URL::" + request.getParameter("url") - + "\n
  • " + "
\n" + ""); - } - - /** - * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) - */ - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - doGet(request, response); - } - -} diff --git a/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/DeleteCookies.java b/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/DeleteCookies.java deleted file mode 100644 index 8d5d7338..00000000 --- a/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/DeleteCookies.java +++ /dev/null @@ -1,76 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.cookie; - -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * @author Zhang Peng - * @date 2017/3/26. - */ -@WebServlet("/servlet/DeleteCookies") -public class DeleteCookies extends HttpServlet { - private static final long serialVersionUID = 1L; - - /** - * @see HttpServlet#HttpServlet() - */ - public DeleteCookies() { - super(); - } - - /** - * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) - */ - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - Cookie cookie = null; - Cookie[] cookies = null; - // 获取与该域相关的 Cookie 的数组 - cookies = request.getCookies(); - - // 设置响应内容类型 - response.setContentType("text/html;charset=UTF-8"); - - PrintWriter out = response.getWriter(); - String title = "删除 Cookie 实例"; - String docType = "\n"; - out.println(docType + "\n" + "" + title + "\n" - + "\n"); - if (cookies != null) { - out.println("

Cookie 名称和值

"); - for (int i = 0; i < cookies.length; i++) { - cookie = cookies[i]; - if ((cookie.getName()).compareTo("url") == 0) { - cookie.setMaxAge(0); - response.addCookie(cookie); - out.print("已删除的 cookie:" + cookie.getName() + "
"); - } - out.print("名称:" + cookie.getName() + ","); - out.print("值:" + cookie.getValue() + "
"); - } - } else { - out.println("

No Cookie founds

"); - } - out.println(""); - out.println(""); - } - - /** - * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) - */ - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - doGet(request, response); - } - -} diff --git a/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/ReadCookies.java b/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/ReadCookies.java deleted file mode 100644 index df12236c..00000000 --- a/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/ReadCookies.java +++ /dev/null @@ -1,77 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.cookie; - -import java.io.IOException; -import java.io.PrintWriter; -import java.net.URLDecoder; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * @author Zhang Peng - * @date 2017/3/26. - */ -@WebServlet("/servlet/ReadCookies") -public class ReadCookies extends HttpServlet { - private static final long serialVersionUID = 1L; - - /** - * @see HttpServlet#HttpServlet() - */ - public ReadCookies() { - super(); - } - - /** - * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) - */ - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - Cookie cookie = null; - Cookie[] cookies = null; - // 获取与该域相关的 Cookie 的数组 - cookies = request.getCookies(); - - // 设置响应内容类型 - response.setContentType("text/html;charset=UTF-8"); - - PrintWriter out = response.getWriter(); - String title = "Delete Cookie Example"; - String docType = "\n"; - out.println(docType + "\n" + "" + title + "\n" - + "\n"); - if (cookies != null) { - out.println("

Cookie 名称和值

"); - for (int i = 0; i < cookies.length; i++) { - cookie = cookies[i]; - if ((cookie.getName()).compareTo("name") == 0) { - cookie.setMaxAge(0); - response.addCookie(cookie); - out.print("已删除的 cookie:" + cookie.getName() + "
"); - } - out.print("名称:" + cookie.getName() + ","); - out.print("值:" + URLDecoder.decode(cookie.getValue(), "utf-8") + "
"); - } - } else { - out.println("

No Cookie founds

"); - } - out.println(""); - out.println(""); - } - - /** - * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) - */ - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - doGet(request, response); - } - -} diff --git a/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/bean/Person.java b/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/bean/Person.java deleted file mode 100644 index fdaee7ed..00000000 --- a/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/bean/Person.java +++ /dev/null @@ -1,65 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.cookie.bean; - -import java.io.Serializable; -import java.util.Date; - -/** - * @author Zhang Peng - * @date 2017/3/26. - */ -public class Person implements Serializable { - - private static final long serialVersionUID = -827111150707830908L; - - private String name; - - private String password; - - private int age; - - private Date birthday; - - public Person(String name, String password, int age, Date birthday) { -// super(); - this.name = name; - this.password = password; - this.age = age; - this.birthday = birthday; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - public Date getBirthday() { - return birthday; - } - - public void setBirthday(Date birthday) { - this.birthday = birthday; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getPassword() { - return password; - } - - public void setPassword(String password) { - this.password = password; - } -} - diff --git a/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/bean/Topic.java b/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/bean/Topic.java deleted file mode 100644 index 8867ba75..00000000 --- a/codes/javaee/session/src/main/java/io/github/dunwu/javaee/cookie/bean/Topic.java +++ /dev/null @@ -1,42 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.cookie.bean; - -/** - * @author Zhang Peng - * @date 2017/3/26. - */ -public class Topic { - - private int id; - - private String title; - - private String content; - - public String getContent() { - return content; - } - - public void setContent(String content) { - this.content = content; - } - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - -} diff --git a/codes/javaee/session/src/main/java/io/github/dunwu/javaee/session/SessionTrackServlet.java b/codes/javaee/session/src/main/java/io/github/dunwu/javaee/session/SessionTrackServlet.java deleted file mode 100644 index b341ff93..00000000 --- a/codes/javaee/session/src/main/java/io/github/dunwu/javaee/session/SessionTrackServlet.java +++ /dev/null @@ -1,71 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.session; - -import java.io.IOException; -import java.io.PrintWriter; -import java.text.SimpleDateFormat; -import java.util.Date; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -/** - * @author Zhang Peng - * @date 2017/3/26. - */ -@WebServlet("/servlet/SessionTrackServlet") -public class SessionTrackServlet extends HttpServlet { - private static final long serialVersionUID = 1L; - - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - // 如果不存在 session 会话,则创建一个 session 对象 - HttpSession session = request.getSession(true); - // 获取 session 创建时间 - Date createTime = new Date(session.getCreationTime()); - // 获取该网页的最后一次访问时间 - Date lastAccessTime = new Date(session.getLastAccessedTime()); - - // 设置日期输出的格式 - SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - - String title = "Servlet Session 实例"; - Integer visitCount = new Integer(0); - String visitCountKey = new String("visitCount"); - String userIDKey = new String("userID"); - String userID = new String("admin"); - - // 检查网页上是否有新的访问者 - if (session.isNew()) { - session.setAttribute(userIDKey, userID); - } else { - visitCount = (Integer) session.getAttribute(visitCountKey); - visitCount = visitCount + 1; - userID = (String) session.getAttribute(userIDKey); - } - session.setAttribute(visitCountKey, visitCount); - - // 设置响应内容类型 - response.setContentType("text/html;charset=UTF-8"); - PrintWriter out = response.getWriter(); - - String docType = "\n"; - out.println(docType + "\n" + "" + title + "\n" - + "\n" + "

" + title - + "

\n" + "

Session 信息

\n" - + "\n" + "\n" - + " \n" + "\n" + " \n" - + " \n" + "\n" - + " \n" + " \n" - + "\n" + " \n" + " \n" + "\n" + " \n" + " \n" + "\n" + " \n" + " \n" + "
Session 信息
id" + session.getId() + "
创建时间" + df.format(createTime) + "
最后访问时间" + df.format(lastAccessTime) - + "
用户 ID" + userID - + "
访问统计:" + visitCount - + "
\n" + ""); - } -} diff --git a/codes/javaee/session/src/main/resources/logback.xml b/codes/javaee/session/src/main/resources/logback.xml deleted file mode 100644 index 4a10fe94..00000000 --- a/codes/javaee/session/src/main/resources/logback.xml +++ /dev/null @@ -1,45 +0,0 @@ - - - - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - logs/${FILE_NAME}-all.%d{yyyy-MM-dd}.log - 30 - - - - - 30MB - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - - - - - - - - - - diff --git a/codes/javaee/session/src/main/webapp/META-INF/MANIFEST.MF b/codes/javaee/session/src/main/webapp/META-INF/MANIFEST.MF deleted file mode 100644 index 254272e1..00000000 --- a/codes/javaee/session/src/main/webapp/META-INF/MANIFEST.MF +++ /dev/null @@ -1,3 +0,0 @@ -Manifest-Version: 1.0 -Class-Path: - diff --git a/codes/javaee/session/src/main/webapp/META-INF/context.xml b/codes/javaee/session/src/main/webapp/META-INF/context.xml deleted file mode 100644 index 8f05edc7..00000000 --- a/codes/javaee/session/src/main/webapp/META-INF/context.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/codes/javaee/session/src/main/webapp/WEB-INF/web.xml b/codes/javaee/session/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 25bb24d6..00000000 --- a/codes/javaee/session/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,49 +0,0 @@ - - - - - AddCookies - io.github.dunwu.javaee.servlet.cookie.AddCookies - - - AddCookies - /servlet/AddCookies - - - - ReadCookies - io.github.dunwu.javaee.servlet.cookie.ReadCookies - - - ReadCookies - /servlet/ReadCookies - - - - DeleteCookies - io.github.dunwu.javaee.servlet.cookie.DeleteCookies - - - DeleteCookies - /servlet/DeleteCookies - - - - SessionTrackServlet - io.github.dunwu.javaee.session.SessionTrackServlet - - - SessionTrackServlet - /servlet/SessionTrackServlet - - - - 20 - - - - diff --git a/codes/javaee/session/src/main/webapp/encodeURL.jsp b/codes/javaee/session/src/main/webapp/encodeURL.jsp deleted file mode 100644 index 9efa8d28..00000000 --- a/codes/javaee/session/src/main/webapp/encodeURL.jsp +++ /dev/null @@ -1,16 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" %> - - - -Cookie Encoding - - - ">Homepage - - <%= response.encodeURL("index.jsp") %>?c=1&wd=Java - - - - - - diff --git a/codes/javaee/session/src/main/webapp/index.jsp b/codes/javaee/session/src/main/webapp/index.jsp deleted file mode 100644 index 19ce2916..00000000 --- a/codes/javaee/session/src/main/webapp/index.jsp +++ /dev/null @@ -1,26 +0,0 @@ -<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> -<% -String path = request.getContextPath(); -String basePath = request.getScheme()+"://"+request.getServerName()+":"+request.getServerPort()+path+"/"; -%> - - - - - - - My JSP 'index.jsp' starting page - - - - - - - - - - This is my JSP page.
- - diff --git a/codes/javaee/session/src/main/webapp/views/css/style.css b/codes/javaee/session/src/main/webapp/views/css/style.css deleted file mode 100644 index 8b9fae5a..00000000 --- a/codes/javaee/session/src/main/webapp/views/css/style.css +++ /dev/null @@ -1,51 +0,0 @@ -body, div, td, input { - font-size: 12px; - margin: 0px; -} - -select { - height: 20px; - width: 300px; -} - -.title { - font-size: 16px; - padding: 10px; - margin: 10px; - width: 80%; -} - -.text { - height: 20px; - width: 300px; - border: 1px solid #AAAAAA; -} - -.line { - margin: 2px; -} - -.leftDiv { - width: 110px; - float: left; - height: 22px; - line-height: 22px; - font-weight: bold; -} - -.rightDiv { - height: 22px; - line-height: 22px; -} - -.button { - color: #fff; - font-weight: bold; - font-size: 11px; - text-align: center; - padding: .17em 0 .2em .17em; - border-style: solid; - border-width: 1px; - border-color: #9cf #159 #159 #9cf; - background: #69c url(../images/bg-btn-blue.gif) repeat-x; -} \ No newline at end of file diff --git a/codes/javaee/session/src/main/webapp/views/images/bg-btn-blue.gif b/codes/javaee/session/src/main/webapp/views/images/bg-btn-blue.gif deleted file mode 100644 index bc03f1bd..00000000 Binary files a/codes/javaee/session/src/main/webapp/views/images/bg-btn-blue.gif and /dev/null differ diff --git a/codes/javaee/session/src/main/webapp/views/images/cookie.gif b/codes/javaee/session/src/main/webapp/views/images/cookie.gif deleted file mode 100644 index 6c6bd580..00000000 Binary files a/codes/javaee/session/src/main/webapp/views/images/cookie.gif and /dev/null differ diff --git a/codes/javaee/session/src/main/webapp/views/images/errorstate.gif b/codes/javaee/session/src/main/webapp/views/images/errorstate.gif deleted file mode 100644 index a3b621b5..00000000 Binary files a/codes/javaee/session/src/main/webapp/views/images/errorstate.gif and /dev/null differ diff --git a/codes/javaee/session/src/main/webapp/views/images/mail.gif b/codes/javaee/session/src/main/webapp/views/images/mail.gif deleted file mode 100644 index 0f2b0d76..00000000 Binary files a/codes/javaee/session/src/main/webapp/views/images/mail.gif and /dev/null differ diff --git a/codes/javaee/session/src/main/webapp/views/images/vertical_line.gif b/codes/javaee/session/src/main/webapp/views/images/vertical_line.gif deleted file mode 100644 index 65f8ee78..00000000 Binary files a/codes/javaee/session/src/main/webapp/views/images/vertical_line.gif and /dev/null differ diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/addCookies.jsp b/codes/javaee/session/src/main/webapp/views/jsp/cookie/addCookies.jsp deleted file mode 100644 index 787c14e5..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/cookie/addCookies.jsp +++ /dev/null @@ -1,16 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" %> - - - - - 添加Cookie - - -
- 站点名 : -
- 站点 URL:
- -
- - \ No newline at end of file diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/base64.jsp b/codes/javaee/session/src/main/webapp/views/jsp/cookie/base64.jsp deleted file mode 100644 index 2501eca4..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/cookie/base64.jsp +++ /dev/null @@ -1,35 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" %> - - - -<% - File file = new File(this.getServletContext().getRealPath("../../images/cookie.gif")); - - // 二进制数组 - byte[] binary = new byte[(int) file.length()]; - - // 从图片文件读取二进制数据. - InputStream ins = this.getServletContext().getResourceAsStream(file.getName()); - ins.read(binary); - ins.close(); - - // BASE64 编码 - String content = BASE64Encoder.class.newInstance().encode(binary); - - // 包含二进制数据的 Cookie - Cookie cookie = new Cookie("file", content); - - // 将 Cookie 发送到客户端 - response.addCookie(cookie); -%> - - - - Cookie Encoding - - -从 Cookie 中获取到的二进制图片:
- - - - diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/base64_decode.jsp b/codes/javaee/session/src/main/webapp/views/jsp/cookie/base64_decode.jsp deleted file mode 100644 index 2f4f933e..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/cookie/base64_decode.jsp +++ /dev/null @@ -1,32 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" %> - - -<% - // 清除输出 - out.clear(); - - for (Cookie cookie : request.getCookies()) { - - if (cookie.getName().equals("file")) { - - // 从 Cookie 中取二进制数据 - byte[] binary = BASE64Decoder.class.newInstance().decodeBuffer(cookie.getValue().replace(" ", "")); - - // 设置内容类型为 gif 图片 - response.setHeader("Content-Type", "image/gif"); - response.setHeader("Content-Disposition", "inline;filename=cookie.gif"); - response.setHeader("Connection", "close"); - - // 设置长度 - response.setContentLength(binary.length); - - // 输出到客户端 - response.getOutputStream().write(binary); - response.getOutputStream().flush(); - response.getOutputStream().close(); - - return; - } - } - -%> \ No newline at end of file diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/cookie.gif b/codes/javaee/session/src/main/webapp/views/jsp/cookie/cookie.gif deleted file mode 100644 index 6c6bd580..00000000 Binary files a/codes/javaee/session/src/main/webapp/views/jsp/cookie/cookie.gif and /dev/null differ diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/cookie.jsp b/codes/javaee/session/src/main/webapp/views/jsp/cookie/cookie.jsp deleted file mode 100644 index 4af95e27..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/cookie/cookie.jsp +++ /dev/null @@ -1,81 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" errorPage="login.jsp" %> -<% - request.setCharacterEncoding("UTF-8"); - - String username = ""; - int visitTimes = 0; - - // 所有的 cookie - Cookie[] cookies = request.getCookies(); - - // 遍历所有的 Cookie 寻找 用户帐号信息与登录次数信息 - for (int i = 0; cookies != null && i < cookies.length; i++) { - Cookie cookie = cookies[i]; - if ("username".equals(cookie.getName())) { - username = cookie.getValue(); - } else if ("visitTimes".equals(cookie.getName())) { - visitTimes = Integer.parseInt(cookie.getValue()); - cookie.setValue("" + ++visitTimes); - } - } - - // 如果没有找到 Cookie 中保存的用户名,则转到登录界面 - if (username == null || username.trim().equals("")) { - throw new Exception("您还没有登录。请先登录"); - } - - // 修改 Cookie,更新用户的访问次数 - Cookie visitTimesCookie = new Cookie("visitTimes", Integer.toString(++visitTimes)); - response.addCookie(visitTimesCookie); - -%> - - - - - Cookie - - - - - - - - -
-
- 登录信息 -
- - - - - - - - - - - - - -
- 您的帐号: - - <%= username %> -
- 登录次数: - - <%= visitTimes %> -
- - -
-
-
-
- - - diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/cookieAttribute.jsp b/codes/javaee/session/src/main/webapp/views/jsp/cookie/cookieAttribute.jsp deleted file mode 100644 index 1772d74d..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/cookie/cookieAttribute.jsp +++ /dev/null @@ -1,31 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" errorPage="login.jsp" %> -<% - - Cookie[] cc = request.getCookies(); - - out.println(cc); - - Cookie cookie = new Cookie("password1", "babyface009988"); - - cookie.setPath(request.getContextPath()); - - - response.addCookie(cookie); - -%> - - - - - Cookie - - - - - - - - - - - diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/cookieDomain.jsp b/codes/javaee/session/src/main/webapp/views/jsp/cookie/cookieDomain.jsp deleted file mode 100644 index 45da571a..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/cookie/cookieDomain.jsp +++ /dev/null @@ -1,27 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" errorPage="login.jsp" %> -<% - - Cookie cookie = new Cookie("time", "20080808"); - cookie.setDomain(".h_google.com"); - cookie.setPath("/"); - cookie.setMaxAge(Integer.MAX_VALUE); - - response.addCookie(cookie); - -%> - - - - - Cookie - - - - - - - - - - - diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/encoding.jsp b/codes/javaee/session/src/main/webapp/views/jsp/cookie/encoding.jsp deleted file mode 100644 index 9d6c2c0c..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/cookie/encoding.jsp +++ /dev/null @@ -1,35 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" %> - - -<% - // 使用中文的 Cookie. name 与 value 都使用 UTF-8 编码. - Cookie cookie = new Cookie( - URLEncoder.encode("姓名", "UTF-8"), - URLEncoder.encode("张三", "UTF-8")); - - // 发送到客户端 - response.addCookie(cookie); -%> - - - - Cookie Encoding - - -<% - Cookie[] cookies = request.getCookies(); - if (null != cookies) { - for (int i = 0; i < cookies.length; i++) { - - String cookieName = URLDecoder.decode(cookies[i].getName(), "UTF-8"); - String cookieValue = URLDecoder.decode(cookies[i].getValue(), "UTF-8"); - - out.println(cookieName + "="); - out.println(cookieValue + ";
"); - } - } else { - out.println("Cookie 已经写入客户端. 请刷新页面. "); - } -%> - - diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/history.js b/codes/javaee/session/src/main/webapp/views/jsp/cookie/history.js deleted file mode 100644 index 441a0ff5..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/cookie/history.js +++ /dev/null @@ -1,24 +0,0 @@ -function getCookie(name) { - var str = document.cookie; - if (!str || str.indexOf(name + "=") < 0) return; - var cookies = str.split("; "); - for (var i = 0; i < cookies.length; i++) { - var cookie = cookies[i]; - if (cookie.indexOf(name + "=") == 0) { - var value = cookie.substring(name.length + 1); - } - } -} -function setCookie(name, value) { - var expires = (arguments.length > 2) ? arguments[2] : null; - var path = (arguments.length > 3) ? arguments[3] : null; - var domain = (arguments.length > 4) ? arguments[4] : null; - var secure = (arguments.length > 5) ? arguments[5] : false; - document.cookie = name + "=" + encodeURI(value) - + ((expires == null) ? "" : ("; expires=" + expires.toGMTString())) - + ((path == null) ? "" : ("; path=" + path)) - + ((domain == null) ? "" : ("; domain=" + domain)) - + ((secure == true) ? "; secure" : ""); -} - -var ss = getCookie('history'); diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/history.jsp b/codes/javaee/session/src/main/webapp/views/jsp/cookie/history.jsp deleted file mode 100644 index 37883463..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/cookie/history.jsp +++ /dev/null @@ -1,64 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" isErrorPage="false" %> -<%! - -%> -<% - - -%> - - - - . - - - -
-
- 当前有效的 Cookie - -
-
- 历史记录 -
- - - - - - - - - - - - - - - - - -
- 帐号: - - -
- 密码: - - -
- 有效期: - - 关闭浏览器即失效
- 30天内有效
- 永久有效
-
- - -
-
-
-
- - - diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/javascript.jsp b/codes/javaee/session/src/main/webapp/views/jsp/cookie/javascript.jsp deleted file mode 100644 index f74530bd..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/cookie/javascript.jsp +++ /dev/null @@ -1,107 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" %> - - - - 欢迎您 - - - - -
-
- 当前有效的 Cookie -
- -
-
- - 欢迎您 - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- 读取 Cookie: - - -
-   - -
- 设置 Cookie: - -
- Name 属性: - - -
- Value 属性: - - -
- - -
-
-
- - diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/login.jsp b/codes/javaee/session/src/main/webapp/views/jsp/cookie/login.jsp deleted file mode 100644 index 3ab9b5cf..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/cookie/login.jsp +++ /dev/null @@ -1,68 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" isErrorPage="true" %> -<% - request.setCharacterEncoding("UTF-8"); - response.setCharacterEncoding("UTF-8"); - - if ("POST".equals(request.getMethod())) { - - Cookie usernameCookie = new Cookie("username", request.getParameter("username")); - Cookie visittimesCookie = new Cookie("visitTimes", "0"); - - response.addCookie(usernameCookie); - response.addCookie(visittimesCookie); - - response.sendRedirect(request.getContextPath() + "/cookie.jsp"); - - return; - } -%> - - - - 请先登录 - - - -
-
- 登录 -
- - - - - - - - - - - - - - - - - -
- - - <%= exception.getMessage() %> -
- 帐号: - - -
- 密码: - - -
- - -
-
-
-
- - - diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/loginCookie.jsp b/codes/javaee/session/src/main/webapp/views/jsp/cookie/loginCookie.jsp deleted file mode 100644 index 17cb0ff5..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/cookie/loginCookie.jsp +++ /dev/null @@ -1,163 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" isErrorPage="false" %> - -<%! - // 密钥 - private static final String KEY = ":cookie@helloweenvsfei.com"; - - // MD5 加密算法 - public final static String calcMD5(String ss) { - - String s = ss == null ? "" : ss; - - char hexDigits[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; - try { - byte[] strTemp = s.getBytes(); - MessageDigest mdTemp = MessageDigest.getInstance("MD5"); - mdTemp.update(strTemp); - byte[] md = mdTemp.digest(); - int j = md.length; - char str[] = new char[j * 2]; - int k = 0; - for (int i = 0; i < j; i++) { - byte byte0 = md[i]; - str[k++] = hexDigits[byte0 >>> 4 & 0xf]; - str[k++] = hexDigits[byte0 & 0xf]; - } - return new String(str); - } catch (Exception e) { - return null; - } - } - -%> -<% - request.setCharacterEncoding("UTF-8"); - response.setCharacterEncoding("UTF-8"); - - String action = request.getParameter("action"); - - if ("login".equals(action)) { - - String account = request.getParameter("account"); - String password = request.getParameter("password"); - int timeout = new Integer(request.getParameter("timeout")); - - // 把帐号连同密钥使用MD5后加密后保存 - String ssid = calcMD5(account + KEY); - - // 把帐号保存到Cookie中 并控制有效期 - Cookie accountCookie = new Cookie("account", account); - accountCookie.setMaxAge(timeout); - - // 把加密结果保存到Cookie中 并控制有效期 - Cookie ssidCookie = new Cookie("ssid", ssid); - ssidCookie.setMaxAge(timeout); - - response.addCookie(accountCookie); - response.addCookie(ssidCookie); - - // 重新请求本页面 - response.sendRedirect(request.getRequestURI() + "?" + System.currentTimeMillis()); - return; - } else if ("logout".equals(action)) { - - // 删除Cookie中的帐号 - Cookie accountCookie = new Cookie("account", ""); - accountCookie.setMaxAge(0); - - // 删除Cookie中的加密结果 - Cookie ssidCookie = new Cookie("ssid", ""); - ssidCookie.setMaxAge(0); - - response.addCookie(accountCookie); - response.addCookie(ssidCookie); - - // 重新请求本页面 - response.sendRedirect(request.getRequestURI() + "?" + System.currentTimeMillis()); - return; - } - - boolean loggin = false; - - String account = null; - String ssid = null; - - // 获取Cookie中的account与ssid - if (request.getCookies() != null) { - for (Cookie cookie : request.getCookies()) { - if (cookie.getName().equals("account")) - account = cookie.getValue(); - if (cookie.getName().equals("ssid")) - ssid = cookie.getValue(); - } - } - - if (account != null && ssid != null) { - // 如果加密规则正确, 则视为已经登录 - loggin = ssid.equals(calcMD5(account + KEY)); - } - -%> - - - - <%= loggin ? "欢迎您回来" : "请先登录" %> - - - - -
-
- 当前有效的 Cookie - -
-
- <%= loggin ? "欢迎您回来" : "请先登录" %> - - <% if (loggin) { %> - 欢迎您, ${ cookie.account.value }.    - 注销 - <% } else { %> -
- - - - - - - - - - - - - - - - - -
- 帐号: - - -
- 密码: - - -
- 有效期: - - 关闭浏览器即失效
- 30天内有效
- 永久有效
-
- - -
-
- <% } %> -
-
- - - diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/maxAge.jsp b/codes/javaee/session/src/main/webapp/views/jsp/cookie/maxAge.jsp deleted file mode 100644 index 650475a5..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/cookie/maxAge.jsp +++ /dev/null @@ -1,26 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" %> -<% - Cookie cookie = new Cookie("username", "helloweenvsfei"); - cookie.setMaxAge(0); - - // 必须执行这一句 - response.addCookie(cookie); - -%> - - - - - Cookie maxAge - - - - - - - - - -This is my JSP page.
- - diff --git a/codes/javaee/session/src/main/webapp/views/jsp/cookie/setCookie.jsp b/codes/javaee/session/src/main/webapp/views/jsp/cookie/setCookie.jsp deleted file mode 100644 index 54c559f3..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/cookie/setCookie.jsp +++ /dev/null @@ -1,101 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" %> - -<%! - boolean isNull(String str) { - return str == null || str.trim().length() == 0; - } -%> -<% - request.setCharacterEncoding("UTF-8"); - - if ("POST".equals(request.getMethod())) { - - String name = request.getParameter("name"); - String value = request.getParameter("value"); - String maxAge = request.getParameter("maxAge"); - String domain = request.getParameter("domain"); - String path = request.getParameter("path"); - String comment = request.getParameter("comment"); - String secure = request.getParameter("secure"); - - if (!isNull(name)) { - - Cookie cookie = new Cookie( - URLEncoder.encode(name, "UTF-8"), - URLEncoder.encode(value, "UTF-8")); - - if (!isNull(maxAge)) cookie.setMaxAge(Integer.parseInt(maxAge)); - if (!isNull(domain)) cookie.setDomain(domain); - if (!isNull(path)) cookie.setPath(path); - if (!isNull(comment)) cookie.setComment(comment); - if (!isNull(secure)) cookie.setSecure("true".equalsIgnoreCase(secure)); - - response.addCookie(cookie); - } - } -%> - - - - Cookie - - - - - - - - -
-
- 当前有效的 Cookie - -
-
- 设置新 Cookie -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
name:
value:
maxAge:
domain:
path:
comment:
secure:
- - -
-
-
-
- - - diff --git a/codes/javaee/session/src/main/webapp/views/jsp/session/session.jsp b/codes/javaee/session/src/main/webapp/views/jsp/session/session.jsp deleted file mode 100644 index 19e2ec3d..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/session/session.jsp +++ /dev/null @@ -1,95 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" %> - - - -<%@ page import="java.util.Date" %> -<%! - DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); -%> -<% - response.setCharacterEncoding("UTF-8"); - - Person[] persons = { - new Person("Liu Jinghua", "password1", 34, dateFormat.parse("1982-01-01")), - new Person("Hello Kitty", "hellokitty", 23, dateFormat.parse("1984-02-25")), - new Person("Garfield", "garfield_pass", 23, dateFormat.parse("1994-09-12")), - }; - - String message = ""; - - if (request.getMethod().equals("POST")) { - - for (int i = 0; i < persons.length; i++) { - // 如果 用户名正确 且 密码正确 - if (persons[i].getName().equalsIgnoreCase(request.getParameter("username")) - && persons[i].getPassword().equals(request.getParameter("password"))) { - - // 登录成功, 设置将用户的信息以及登录时间保存到 Session - session.setAttribute("person", persons[i]); - session.setAttribute("loginTime", new Date()); - - response.sendRedirect(request.getContextPath() + "welcome.jsp"); - return; - - } - } - - // 登录失败 - message = "用户名密码不匹配,登录失败。"; - } - -%> - - - - 请先登录 - - - -
-
- 登录 -
- - <% if (!message.equals("")) { %> - - - - - <% } %> - - - - - - - - - - - - -
- - - <%= message %> -
- 帐号: - - -
- 密码: - - -
- - -
-
-
-
- -Hello Kitty, hellokitty - - - diff --git a/codes/javaee/session/src/main/webapp/views/jsp/session/vote.jsp b/codes/javaee/session/src/main/webapp/views/jsp/session/vote.jsp deleted file mode 100644 index 7af342ab..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/session/vote.jsp +++ /dev/null @@ -1,52 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" %> - - - - 欢迎您 - - - -
-
- 投票 - - - - - - - - - - - - - - - - - - - - - -
- 您的姓名: - -
- -
- 您的年龄: - -
- 您的生日: - -
- - -
-
-
- - - diff --git a/codes/javaee/session/src/main/webapp/views/jsp/session/welcome.jsp b/codes/javaee/session/src/main/webapp/views/jsp/session/welcome.jsp deleted file mode 100644 index f27b994e..00000000 --- a/codes/javaee/session/src/main/webapp/views/jsp/session/welcome.jsp +++ /dev/null @@ -1,63 +0,0 @@ -<%@ page language="java" pageEncoding="UTF-8" %> -<% - if (session.getAttribute("person") == null) { - response.sendRedirect("session.jsp"); - return; - } -%> - - - - 欢迎您, ${ person.name } - - - -
-
- 欢迎您${ person.name } - - - - - - - - - - - - - - - - - - - - - -
- 您的姓名: - - ${ person.name } -
- 登录时间: - - ${ loginTime } -
- 您的年龄: - - ${ person.age } -
- 您的生日: - - ${ person.birthday } -
- - -
-
-
- - - diff --git a/codes/javaee/session/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java b/codes/javaee/session/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java deleted file mode 100644 index ba3db0e5..00000000 --- a/codes/javaee/session/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.github.dunwu.javaee.server; - -import java.util.ArrayList; - -import org.apache.commons.lang3.StringUtils; -import org.assertj.core.util.Lists; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.WebAppClassLoader; -import org.eclipse.jetty.webapp.WebAppContext; - -/** - * JettyFactory 可以工作在 Eclipse 和 Intellij 中,用来启动 jetty 服务。 Intellij - * 并不支持jetty,所以要想类似eclipse一样的使用jetty,需要配置webdefault.xml。 - * - * @author Zhang Peng - */ -@SuppressWarnings("unused") -public class JettyFactory { - private static final int PORT = 9899; - private static final String CONTEXT = "/"; - private static final String RESOURCE_BASE_PATH = "src/main/webapp"; - private static final String WEB_XML_PATH = "/WEB-INF/web.xml"; - private static final String[] TLD_JAR_NAMES = - new String[]{"sitemesh", "spring-webmvc", "shiro-web", "tiles"}; - private static final String WINDOWS_WEBDEFAULT_PATH = "jetty/webdefault.xml"; - - public static final int IDE_ECLIPSE = 0; - public static final int IDE_INTELLIJ = 1; - - - public static Server initServer() { - Profiles.setProfileAsSystemProperty(Profiles.DEVELOPMENT); - WebAppContext webAppContext = new WebAppContext(); - Server server = new Server(PORT); - server.setHandler(webAppContext); - return server; - } - - public static void initWebAppContext(Server server, int type) throws Exception { - System.out.println("[INFO] Application loading"); - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - webAppContext.setContextPath(CONTEXT); - webAppContext.setResourceBase(getAbsolutePath() + RESOURCE_BASE_PATH); - webAppContext.setDescriptor(getAbsolutePath() + RESOURCE_BASE_PATH + WEB_XML_PATH); - - if (IDE_INTELLIJ == type) { - webAppContext.setDefaultsDescriptor(WINDOWS_WEBDEFAULT_PATH); - supportJspAndSetTldJarNames(server, TLD_JAR_NAMES); - } else { - webAppContext.setParentLoaderPriority(true); - } - - System.out.println("[INFO] Application loaded"); - } - - public static void reloadWebAppContext(Server server) throws Exception { - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - System.out.println("[INFO] Application reloading"); - webAppContext.stop(); - WebAppClassLoader classLoader = new WebAppClassLoader(webAppContext); - classLoader.addClassPath(getAbsolutePath() + "target/classes"); - classLoader.addClassPath(getAbsolutePath() + "target/test-classes"); - webAppContext.setClassLoader(classLoader); - webAppContext.start(); - System.out.println("[INFO] Application reloaded"); - } - - - public static void startServer(Server server) throws Exception { - System.out.println("[HINT] Don't forget to set -XX:MaxPermSize=128m"); - server.start(); - System.out.println("Server running at http://localhost:" + PORT + CONTEXT); - System.out.println("[HINT] Hit Enter to reload the application quickly"); - } - - public static String getAbsolutePath() { - String path = null; - String folderPath = JettyFactory.class.getProtectionDomain().getCodeSource().getLocation() - .getPath().substring(1); - if (folderPath.indexOf("target") > 0) { - path = folderPath.substring(0, folderPath.indexOf("target")); - } - return path; - } - - public static void supportJspAndSetTldJarNames(Server server, String... jarNames) { - WebAppContext context = (WebAppContext) server.getHandler(); - // This webapp will use jsps and jstl. We need to enable the AnnotationConfiguration in - // order to correctly set up the jsp container - org.eclipse.jetty.webapp.Configuration.ClassList classlist = - org.eclipse.jetty.webapp.Configuration.ClassList.setServerDefault(server); - classlist.addBefore("org.eclipse.jetty.webapp.JettyWebXmlConfiguration", - "org.eclipse.jetty.annotations.AnnotationConfiguration"); - // Set the ContainerIncludeJarPattern so that jetty examines these container-path jars for - // tlds, web-fragments etc. - // If you omit the jar that contains the jstl .tlds, the jsp engine will scan for them - // instead. - ArrayList jarNameExprssions = Lists.newArrayList(".*/[^/]*servlet-api-[^/]*\\.jar$", - ".*/javax.servlet.jsp.jstl-.*\\.jar$", ".*/[^/]*taglibs.*\\.jar$"); - - for (String jarName : jarNames) { - jarNameExprssions.add(".*/" + jarName + "-[^/]*\\.jar$"); - } - - context.setAttribute("org.eclipse.jetty.io.github.dunwu.javaee.server.webapp.ContainerIncludeJarPattern", - StringUtils.join(jarNameExprssions, '|')); - } -} diff --git a/codes/javaee/session/src/test/java/io/github/dunwu/javaee/server/Profiles.java b/codes/javaee/session/src/test/java/io/github/dunwu/javaee/server/Profiles.java deleted file mode 100644 index 3a8ee7c4..00000000 --- a/codes/javaee/session/src/test/java/io/github/dunwu/javaee/server/Profiles.java +++ /dev/null @@ -1,29 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2005, 2014 springside.github.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - *******************************************************************************/ -package io.github.dunwu.javaee.server; - -/** - * Spring profile 常用方法与profile名称。 - * - * @author calvin - */ -public class Profiles { - - public static final String ACTIVE_PROFILE = "spring.profiles.active"; - public static final String DEFAULT_PROFILE = "spring.profiles.default"; - - public static final String PRODUCTION = "production"; - public static final String DEVELOPMENT = "development"; - public static final String UNIT_TEST = "test"; - public static final String FUNCTIONAL_TEST = "functional"; - - /** - * 在Spring启动前,设置profile的环境变量。 - */ - public static void setProfileAsSystemProperty(String profile) { - System.setProperty(ACTIVE_PROFILE, profile); - } -} diff --git a/codes/javaee/session/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java b/codes/javaee/session/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java deleted file mode 100644 index 2936b162..00000000 --- a/codes/javaee/session/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.github.dunwu.javaee.server; - -import org.eclipse.jetty.server.Server; - -/** - * 快速启动 jetty 服务器,方便测试 - * - * @author Zhang Peng - */ -public class QuickStartServer { - // private static int STARTUP_TYPE = JettyFactory.IDE_ECLIPSE; - private static int STARTUP_TYPE = JettyFactory.IDE_INTELLIJ; - - public static void main(String[] args) throws Exception { - Server server = JettyFactory.initServer(); - JettyFactory.initWebAppContext(server, STARTUP_TYPE); - - try { - JettyFactory.startServer(server); - - // 等待用户输入回车重载应用 - while (true) { - char c = (char) System.in.read(); - if (c == '\n') { - JettyFactory.reloadWebAppContext(server); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.exit(-1); - } - } -} diff --git a/codes/javaee/session/src/test/resources/jetty/webdefault.xml b/codes/javaee/session/src/test/resources/jetty/webdefault.xml deleted file mode 100644 index 65ec9cae..00000000 --- a/codes/javaee/session/src/test/resources/jetty/webdefault.xml +++ /dev/null @@ -1,534 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Default web.xml file. - This file is applied to a Web application before it's own WEB_INF/web.xml file - - - - - - - - org.eclipse.jetty.servlet.listener.ELContextCleaner - - - - - - - - org.eclipse.jetty.servlet.listener.IntrospectorCleaner - - - - - - - - - - - - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - aliases - false - - - acceptRanges - true - - - dirAllowed - true - - - welcomeServlets - false - - - redirectWelcome - false - - - maxCacheSize - 256000000 - - - maxCachedFileSize - 200000000 - - - maxCachedFiles - 2048 - - - gzip - false - - - etags - false - - - useFileMappedBuffer - false - - - - 0 - - - - default - / - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jsp - org.eclipse.jetty.jsp.JettyJspServlet - - logVerbosityLevel - DEBUG - - - fork - false - - - xpoweredBy - false - - - compilerTargetVM - 1.7 - - - compilerSourceVM - 1.7 - - - 0 - - - - jsp - *.jsp - *.jspf - *.jspx - *.xsp - *.JSP - *.JSPF - *.JSPX - *.XSP - - - - - - - - 30 - - - - - - - - - - - - - - - index.html - index.htm - index.jsp - - - - - - - - ar - ISO-8859-6 - - - be - ISO-8859-5 - - - bg - ISO-8859-5 - - - ca - ISO-8859-1 - - - cs - ISO-8859-2 - - - da - ISO-8859-1 - - - de - ISO-8859-1 - - - el - ISO-8859-7 - - - en - ISO-8859-1 - - - es - ISO-8859-1 - - - et - ISO-8859-1 - - - fi - ISO-8859-1 - - - fr - ISO-8859-1 - - - hr - ISO-8859-2 - - - hu - ISO-8859-2 - - - is - ISO-8859-1 - - - it - ISO-8859-1 - - - iw - ISO-8859-8 - - - ja - Shift_JIS - - - ko - EUC-KR - - - lt - ISO-8859-2 - - - lv - ISO-8859-2 - - - mk - ISO-8859-5 - - - nl - ISO-8859-1 - - - no - ISO-8859-1 - - - pl - ISO-8859-2 - - - pt - ISO-8859-1 - - - ro - ISO-8859-2 - - - ru - ISO-8859-5 - - - sh - ISO-8859-5 - - - sk - ISO-8859-2 - - - sl - ISO-8859-2 - - - sq - ISO-8859-2 - - - sr - ISO-8859-5 - - - sv - ISO-8859-1 - - - tr - ISO-8859-9 - - - uk - ISO-8859-5 - - - zh - GB2312 - - - zh_TW - Big5 - - - - - - - - - Disable TRACE - / - TRACE - - - - - - Enable everything but TRACE - / - TRACE - - - - - diff --git a/codes/javaee/taglib/pom.xml b/codes/javaee/taglib/pom.xml deleted file mode 100644 index ac265d5e..00000000 --- a/codes/javaee/taglib/pom.xml +++ /dev/null @@ -1,181 +0,0 @@ - - - 4.0.0 - - - io.github.dunwu - javaee-notes-taglib - 1.0.0 - war - - - - - javaee-notes-taglib - javaee 学习笔记之 taglib - - - - - - UTF-8 - 1.7 - ${java.version} - ${java.version} - - - 9.3.2.v20150730 - - - - - ch.qos.logback - logback-classic - 1.1.3 - - - ch.qos.logback - logback-core - 1.1.3 - - - org.logback-extensions - logback-ext-spring - 0.1.2 - - - org.slf4j - jcl-over-slf4j - 1.7.12 - - - - - - commons-fileupload - commons-fileupload - 1.3.1 - - - commons-io - commons-io - 2.5 - - - org.apache.commons - commons-lang3 - 3.4 - - - - - - javax.servlet - javax.servlet-api - 3.1.0 - - - javax.servlet.jsp - jsp-api - 2.2 - - - javax.servlet - jstl - 1.2 - - - javax.servlet.jsp.jstl - jstl-api - 1.2 - - - javax.servlet - servlet-api - - - javax.servlet.jsp - jsp-api - - - - - org.glassfish.web - jstl-impl - 1.2 - - - javax.servlet - servlet-api - - - javax.servlet.jsp - jsp-api - - - javax.servlet.jsp.jstl - jstl-api - - - - - - - - org.eclipse.jetty - jetty-webapp - ${jetty.version} - - - org.eclipse.jetty - jetty-annotations - ${jetty.version} - - - org.eclipse.jetty - apache-jsp - ${jetty.version} - - - org.eclipse.jetty - apache-jstl - ${jetty.version} - - - - - - junit - junit - 4.12 - - - org.assertj - assertj-core - 3.4.1 - - - - - - - xalan - xalan - 2.7.2 - - - xerces - xercesImpl - 2.11.0 - - - - - - - - - - - diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/Copyright.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/Copyright.java deleted file mode 100644 index 9574545a..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/Copyright.java +++ /dev/null @@ -1,62 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.taglib; - -import java.io.IOException; -import java.util.ResourceBundle; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.PageContext; -import javax.servlet.jsp.tagext.Tag; - -/** - * @author Zhang Peng - * @date 2017/4/3. - */ -public class Copyright implements Tag { - private Tag parent; - - private PageContext pageContext; - - @Override - public int doEndTag() throws JspException { - JspWriter out = pageContext.getOut(); - - try { - out.println("
"); - out.println(ResourceBundle.getBundle("copyright").getString("copyright")); - out.println("
"); - } catch (IOException e) { - throw new JspException(e); - } - - return EVAL_PAGE; - } - - @Override - public int doStartTag() throws JspException { - return SKIP_BODY; - } - - @Override - public Tag getParent() { - return this.parent; - } - - @Override - public void release() {} - - @Override - public void setPageContext(PageContext pageContext) { - this.pageContext = pageContext; - } - - @Override - public void setParent(Tag parent) { - this.parent = parent; - } - -} - diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag.java deleted file mode 100644 index a60cf4e4..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.taglib; - -/** - * @author Zhang Peng - * @date 2017/4/3. - */ - -import java.io.IOException; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.tagext.SimpleTagSupport; - -public class HelloTag extends SimpleTagSupport { - @Override - public void doTag() throws JspException, IOException { - JspWriter out = getJspContext().getOut(); - out.println("Hello Custom Tag!"); - } -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag2.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag2.java deleted file mode 100644 index 9355e4d9..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag2.java +++ /dev/null @@ -1,23 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.taglib; - -import java.io.IOException; -import java.io.StringWriter; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.SimpleTagSupport; - -/** - * @author Zhang Peng - * @date 2017/4/3. - */ -public class HelloTag2 extends SimpleTagSupport { - StringWriter sw = new StringWriter(); - - public void doTag() throws JspException, IOException { - getJspBody().invoke(sw); - getJspContext().getOut().println(sw.toString()); - } -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag3.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag3.java deleted file mode 100644 index 85516fd7..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag3.java +++ /dev/null @@ -1,39 +0,0 @@ -/** - * The Apache License 2.0 Copyright (c) 2017 Zhang Peng - */ -package io.github.dunwu.javaee.taglib; - -import java.io.IOException; -import java.io.StringWriter; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.tagext.SimpleTagSupport; - -/** - * @author Zhang Peng - * @date 2017/4/3. - */ -public class HelloTag3 extends SimpleTagSupport { - - private String message; - - public void setMessage(String msg) { - this.message = msg; - } - - StringWriter sw = new StringWriter(); - - public void doTag() throws JspException, IOException { - if (message != null) { - /* 从属性中使用消息 */ - JspWriter out = getJspContext().getOut(); - out.println(message); - } else { - /* 从内容体中使用消息 */ - getJspBody().invoke(sw); - getJspContext().getOut().println(sw.toString()); - } - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/bean/Person.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/bean/Person.java deleted file mode 100644 index 5ac24fcb..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/bean/Person.java +++ /dev/null @@ -1,105 +0,0 @@ -package io.github.dunwu.javaee.taglib.bean; - -public class Person { - - private int id; - - private String name; - - private String sex; - - private int age; - - private String telephone; - - private String birthday; - - private String mobile; - - private String address; - - private String city; - - private boolean deleted; - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getSex() { - return sex; - } - - public void setSex(String sex) { - this.sex = sex; - } - - public int getAge() { - return age; - } - - public void setAge(int age) { - this.age = age; - } - - public boolean isDeleted() { - return deleted; - } - - public void setDeleted(boolean deleted) { - this.deleted = deleted; - } - - public String getTelephone() { - return telephone; - } - - public void setTelephone(String telephone) { - this.telephone = telephone; - } - - public String getBirthday() { - return birthday; - } - - public void setBirthday(String birthday) { - this.birthday = birthday; - } - - public String getMobile() { - return mobile; - } - - public void setMobile(String mobile) { - this.mobile = mobile; - } - - public String getAddress() { - return address; - } - - public void setAddress(String address) { - this.address = address; - } - - public String getCity() { - return city; - } - - public void setCity(String city) { - this.city = city; - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/function/Function.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/function/Function.java deleted file mode 100644 index 34c64a55..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/function/Function.java +++ /dev/null @@ -1,59 +0,0 @@ -package io.github.dunwu.javaee.taglib.function; - -import java.util.Collection; - -public class Function { - - /** - * 返回字节长度 - * - * @param obj - * @return - */ - @SuppressWarnings("unchecked") - public static int length(Object obj) { - - if (obj == null) return 0; - - if (obj instanceof StringBuffer) { - return length(((StringBuffer) obj).toString()); - } - - if (obj instanceof String) { - return ((String) obj).getBytes().length; - } - - if (obj instanceof Collection) { - return ((Collection) obj).size(); - } - - return 0; - } - - public static String substring(String str, int byteLength) { - - if (str == null) return ""; - - StringBuffer buffer = new StringBuffer(); - - for (int i = 0; i < str.length(); i++) { - char ch = str.charAt(i); - if (length(buffer.toString() + ch) > byteLength) { - break; - } else { - buffer.append(ch); - } - } - - return buffer.toString(); - } - - public static void main(String[] args) { - - System.out.println(length("中文测试")); - - System.out.println(substring("中文测试", 5)); - - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/AddTag.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/AddTag.java deleted file mode 100644 index 60576f7d..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/AddTag.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.github.dunwu.javaee.taglib.tags; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.TagSupport; - -public class AddTag extends TagSupport { - - private static final long serialVersionUID = -579746915908972833L; - - private int num1; - - private int num2; - - public void setNum1(int num1) { - this.num1 = num1; - } - - public void setNum2(int num2) { - this.num2 = num2; - } - - @Override - public int doEndTag() throws JspException { - try { - this.pageContext.getOut().println(num1 + " + " + num2 + " = " + (num1 + num2)); - } catch (Exception e) { - e.printStackTrace(); - } - return EVAL_PAGE; - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/Copyright.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/Copyright.java deleted file mode 100644 index 4b238b6e..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/Copyright.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.github.dunwu.javaee.taglib.tags; - -import java.io.IOException; -import java.util.ResourceBundle; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.PageContext; -import javax.servlet.jsp.tagext.Tag; - -public class Copyright implements Tag -{ - private Tag parent; - - private PageContext pageContext; - - @Override - public int doEndTag() - throws JspException - { - JspWriter out = pageContext.getOut(); - - try - { - out.println("
"); - out.println(ResourceBundle.getBundle("copyright").getString("copyright")); - out.println("
"); - } - catch (IOException e) - { - throw new JspException(e); - } - - return EVAL_PAGE; - } - - @Override - public int doStartTag() - throws JspException - { - return SKIP_BODY; - } - - @Override - public Tag getParent() - { - return this.parent; - } - - @Override - public void release() - { - } - - @Override - public void setPageContext(PageContext pageContext) - { - this.pageContext = pageContext; - } - - @Override - public void setParent(Tag parent) - { - this.parent = parent; - } - -} - -// end diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/Copyright2.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/Copyright2.java deleted file mode 100644 index 3f4bbd25..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/Copyright2.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.github.dunwu.javaee.taglib.tags; - -import java.io.IOException; -import java.util.ResourceBundle; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.tagext.TagSupport; - -public class Copyright2 extends TagSupport { - - private static final long serialVersionUID = -2936770589554413334L; - - @Override - public int doEndTag() throws JspException { - JspWriter out = pageContext.getOut(); - - try { - out.println("
"); - out.println(ResourceBundle.getBundle("copyright").getString("copyright")); - out.println("
"); - } catch (IOException e) { - throw new JspException(e); - } - - return EVAL_PAGE; - } - - @Override - public int doStartTag() throws JspException { - return super.doStartTag(); - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/DynamicAttributeTag.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/DynamicAttributeTag.java deleted file mode 100644 index 62402fb2..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/DynamicAttributeTag.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.github.dunwu.javaee.taglib.tags; - -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.tagext.DynamicAttributes; -import javax.servlet.jsp.tagext.TagSupport; - -public class DynamicAttributeTag extends TagSupport implements - DynamicAttributes { - - private static final long serialVersionUID = -1477571708507488373L; - - private Map map = new HashMap(); - - @Override - public int doEndTag() throws JspException { - - JspWriter out = pageContext.getOut(); - - double min = 0, max = 0; - - for (Double d : map.values()) { - min = Math.min(d, min); - max = Math.max(d, max); - } - - StringBuffer buffer = new StringBuffer(); - - buffer.append(""); - - for (Entry entry : map.entrySet()) { - buffer.append(""); - buffer.append(""); - buffer.append(""); - buffer.append(""); - } - - buffer.append("
" + entry.getKey() + " " + entry.getValue() + "
"); - - try { - out.write(buffer.toString()); - } catch (Exception e) { - } - - return super.doEndTag(); - } - - @Override - public void setDynamicAttribute(String uri, String key, Object value) - throws JspException { - - map.put(key, Double.parseDouble((String) value)); - } -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/HelloTag.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/HelloTag.java deleted file mode 100644 index 68fe5624..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/HelloTag.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.github.dunwu.javaee.taglib.tags; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.TagSupport; - -public class HelloTag extends TagSupport { - - private static final long serialVersionUID = -8828591126748246256L; - - private String name; - - @Override - public int doEndTag() throws JspException { - - try { - this.pageContext.getOut().println("Hello, " + name); - } catch (Exception e) { - throw new JspException(e); - } - - return EVAL_PAGE; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/IteratorTag.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/IteratorTag.java deleted file mode 100644 index 4126287a..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/IteratorTag.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.github.dunwu.javaee.taglib.tags; - -import java.util.Collection; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.TagSupport; - -public class IteratorTag extends TagSupport { - - private static final long serialVersionUID = -8828591126748246256L; - - private Collection connection; - - @Override - public int doEndTag() throws JspException { - - try { - for (Object obj : connection) { - this.pageContext.getOut().println(obj + ",
"); - } - } catch (Exception e) { - throw new JspException(e); - } - - return EVAL_PAGE; - } - - public void setConnection(Collection connection) { - this.connection = connection; - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/LoopTag.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/LoopTag.java deleted file mode 100644 index 961ac4dd..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/LoopTag.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.github.dunwu.javaee.taglib.tags; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.BodyTagSupport; - -public class LoopTag extends BodyTagSupport { - - private static final long serialVersionUID = 5882067091737658241L; - - private int times; - - @Override - public int doStartTag() throws JspException { - times = 5; - return super.doStartTag(); - } - - @Override - public int doAfterBody() throws JspException { - - if (times-- > 0) { - - /** 只要 times > 0 就继续循环,同时 times 自减 */ - try { - this.getPreviousOut() - .println(this.getBodyContent().getString()); - - } catch (Exception e) { - } - - return EVAL_BODY_AGAIN; - - } else { - - /** 结束运行,同时复原 times */ - - times = 5; - - return SKIP_BODY; - } - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/ToLowerCaseTag.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/ToLowerCaseTag.java deleted file mode 100644 index 60e372b3..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/ToLowerCaseTag.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.github.dunwu.javaee.taglib.tags; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.BodyTagSupport; - -public class ToLowerCaseTag extends BodyTagSupport { - - private static final long serialVersionUID = -2529343271020971948L; - - @Override - public int doEndTag() throws JspException { - - String content = this.getBodyContent().getString(); - - try { - this.pageContext.getOut().print(content.toLowerCase()); - } catch (Exception e) { - } - - return EVAL_PAGE; - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/table/Column.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/table/Column.java deleted file mode 100644 index 0992333c..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/table/Column.java +++ /dev/null @@ -1,65 +0,0 @@ -package io.github.dunwu.javaee.taglib.tags.table; - -import java.util.HashMap; -import java.util.Map; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.TagSupport; - -public class Column extends TagSupport { - private static final long serialVersionUID = 5119493903438602864L; - - private String property; - - private String label; - - private String type; - - public int doStartTag() throws JspException { - if (!(this.getParent() instanceof Table)) { - throw new JspException("Column must be inside Table. "); - } - - - Map column = new HashMap(); - - column.put("label", label); - column.put("property", property); - column.put("type", type); - - Table table = (Table) this.getParent(); - - table.getColumns().add(column); - - return SKIP_BODY; - } - - public int doEndTag() throws JspException { - return EVAL_PAGE; - } - - public String getProperty() { - return property; - } - - public void setProperty(String property) { - this.property = property; - } - - public String getLabel() { - return label; - } - - public void setLabel(String label) { - this.label = label; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/table/Table.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/table/Table.java deleted file mode 100644 index a0942848..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/table/Table.java +++ /dev/null @@ -1,200 +0,0 @@ -package io.github.dunwu.javaee.taglib.tags.table; - -import java.io.IOException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.tagext.BodyContent; -import javax.servlet.jsp.tagext.BodyTagSupport; - -public class Table extends BodyTagSupport { - private static final long serialVersionUID = 3358444196409845360L; - - /** 存储列信息 */ - private List> columns = new ArrayList>(); - - /** 存储数据,可能为 集合类型的或者数组类型的 */ - private Object items; - - /** 取排序数据的 URL */ - private String url; - - @Override - public int doStartTag() throws JspException { - columns.clear(); - - return super.doStartTag(); - } - - @Override - @SuppressWarnings("unchecked") - public int doAfterBody() throws JspException { - try { - BodyContent bc = getBodyContent(); - JspWriter out = bc.getEnclosingWriter(); - - HttpServletRequest request = (HttpServletRequest) pageContext.getRequest(); - - /** 按哪一列排序 */ - String orderName = request.getParameter("orderName"); - /** 按升序还是降序排序 */ - String orderType = request.getParameter("orderType"); - - orderType = "desc".equals(orderType) ? "desc" : "asc"; - - out.println(""); - out.println(" "); - out.println(" "); - - for (int i = 0; i < columns.size(); i++) { - /** 获取列信息 */ - Map column = columns.get(i); - - /** 列头 */ - String label = column.get("label"); - /** 该列对应的 Java Bean 的属性 */ - String property = column.get("property"); - - label = label == null ? property : label; - - out.println(""); - out.println(""); - } - - out.println(" "); - - if (items != null) { - - /** 遍历所有的数据 */ - for (Object obj : (Iterable) items) { - - out.println(" "); - - for (int i = 0; i < columns.size(); i++) { - - Map column = columns.get(i); - - String property = column.get("property"); - - String getterStyle = toGetterStyle(property); - - try { - String getter = "get" + getterStyle; - String is = "is" + getterStyle; - - Method method = null; - - try { - /** 获取 getXxx() 形式的方法 */ - method = obj.getClass().getMethod(getter); - } catch (Exception e) {} - - if (method == null) { - /** 如果没有,获取 isXxx() 形式的方法 */ - method = obj.getClass().getMethod(is); - } - - method.setAccessible(true); - - /** 获取属性值 */ - Object value = method.invoke(obj); - out.println(""); - - } catch (Exception e) { - throw new JspException(e); - } - } - out.println(" "); - } - } - - out.println("
"); - out.println(""); - - out.println(""); - - out.println(label); - - if (property.equals(orderName)) { - - out.println(""); - } - - out.println(""); - - out.println("
" + value - + "
"); - - out.println(""); - - } catch (IOException ioe) { - throw new JspException("Error: " + ioe.getMessage()); - } - - return SKIP_BODY; - } - - public Object getItems() { - return items; - } - - public void setItems(Object items) { - this.items = items; - } - - /** - * 首字母大写 - * - * @param column - * @return - */ - public String toGetterStyle(String column) { - if (column.length() == 1) return column.toUpperCase(); - - char ch = column.charAt(0); - - return Character.toUpperCase(ch) + column.substring(1); - - } - - public List> getColumns() { - return columns; - } - - public void setColumns(List> columns) { - this.columns = columns; - } - - public String getUrl() { - return url; - } - - public void setUrl(String url) { - this.url = url; - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/MultiAttributeTag.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/MultiAttributeTag.java deleted file mode 100644 index 4488a295..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/MultiAttributeTag.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.github.dunwu.javaee.taglib.tags2; - -import java.io.IOException; -import java.io.StringWriter; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.JspFragment; -import javax.servlet.jsp.tagext.SimpleTagSupport; - -public class MultiAttributeTag extends SimpleTagSupport { - - private JspFragment body1; - - private JspFragment body2; - - public void setBody1(JspFragment body1) { - this.body1 = body1; - } - - public void setBody2(JspFragment body2) { - this.body2 = body2; - } - - @Override - public void doTag() throws JspException, IOException { - - StringWriter writer1 = new StringWriter(); - StringWriter writer2 = new StringWriter(); - - for (int i = 0; i < 5; i++) { - // body1 调用 5 次 - body1.invoke(writer1); - } - - for (int i = 0; i < 3; i++) { - // body2 调用 3 次 - body2.invoke(writer2); - } - - this.getJspContext().getOut() - .print("3 次调用 body2 后的结果:" + writer2.getBuffer().toString() + "

"); - - this.getJspContext().getOut() - .print("5 次调用 body1 后的结果:" + writer1.getBuffer().toString() + "

"); - - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/MultiTag.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/MultiTag.java deleted file mode 100644 index fa92c801..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/MultiTag.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.github.dunwu.javaee.taglib.tags2; - -import java.io.IOException; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.SimpleTagSupport; - -public class MultiTag extends SimpleTagSupport { - - private int num1; - - private int num2; - - @Override - public void doTag() throws JspException, IOException { - this.getJspContext().getOut().write("" + num1 + " * " + num2 + " = " + (num1 * num2)); - } - - public int getNum1() { - return num1; - } - - public void setNum1(int num1) { - this.num1 = num1; - } - - public int getNum2() { - return num2; - } - - public void setNum2(int num2) { - this.num2 = num2; - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/RepeatTag.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/RepeatTag.java deleted file mode 100644 index 9d0ab423..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/RepeatTag.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.dunwu.javaee.taglib.tags2; - -import javax.servlet.jsp.tagext.SimpleTagSupport; - -public class RepeatTag extends SimpleTagSupport { - - private int times; - - public int getTimes() { - return times; - } - - public void setTimes(int times) { - this.times = times; - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/ToUpperCaseTag.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/ToUpperCaseTag.java deleted file mode 100644 index 14a08b1d..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/ToUpperCaseTag.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.github.dunwu.javaee.taglib.tags2; - -import java.io.IOException; -import java.io.StringWriter; - -import javax.servlet.jsp.JspException; -import javax.servlet.jsp.tagext.JspFragment; -import javax.servlet.jsp.tagext.SimpleTagSupport; - -public class ToUpperCaseTag extends SimpleTagSupport { - - @Override - public void doTag() throws JspException, IOException { - - // 将 标签体内容读入该 writer - StringWriter writer = new StringWriter(); - - // 标签体 为 JspFragment 的形式 - JspFragment jspFragment = this.getJspBody(); - - // 通过 invoke 输出到指定的 writer 中。 - // 如果参数为 null,将输出到默认的 writer 中,即 getJspContext().getOut() 输出到HTML中 - jspFragment.invoke(writer); - - String content = writer.getBuffer().toString(); - this.getJspContext().getOut().print(content.toUpperCase()); - } - -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/test/Messages.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/test/Messages.java deleted file mode 100644 index 32f5baff..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/test/Messages.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.github.dunwu.javaee.taglib.test; - -import java.text.MessageFormat; -import java.util.MissingResourceException; -import java.util.ResourceBundle; - -public class Messages { - private static final String BUNDLE_NAME = "com.helloweenvsfei.test.messages"; //$NON-NLS-1$ - - private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME); - - private Messages() {} - - public static String getString(String key) { - try { - return RESOURCE_BUNDLE.getString(key); - } catch (MissingResourceException e) { - return '!' + key + '!'; - } - } - - public static String getString(String key, Object... params) { - try { - String value = RESOURCE_BUNDLE.getString(key); - - return MessageFormat.format(value, params); - - } catch (MissingResourceException e) { - return '!' + key + '!'; - } - } -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/test/TestMessage.java b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/test/TestMessage.java deleted file mode 100644 index 1fa39ff8..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/test/TestMessage.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.github.dunwu.javaee.taglib.test; - -public class TestMessage { - public static void main(String[] args) { - System.out.println(Messages.getString("TestMessage.0", "A", "B")); //$NON-NLS-1$ - } -} diff --git a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/test/messages.properties b/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/test/messages.properties deleted file mode 100644 index c99d9bfc..00000000 --- a/codes/javaee/taglib/src/main/java/io/github/dunwu/javaee/taglib/test/messages.properties +++ /dev/null @@ -1 +0,0 @@ -TestMessage.0=test {0}, {1} diff --git a/codes/javaee/taglib/src/main/resources/copyright.properties b/codes/javaee/taglib/src/main/resources/copyright.properties deleted file mode 100644 index 2a835e8b..00000000 --- a/codes/javaee/taglib/src/main/resources/copyright.properties +++ /dev/null @@ -1 +0,0 @@ -copyright=©2016-2017, Zhang Peng diff --git a/codes/javaee/taglib/src/main/resources/logback.xml b/codes/javaee/taglib/src/main/resources/logback.xml deleted file mode 100644 index abe2ed7a..00000000 --- a/codes/javaee/taglib/src/main/resources/logback.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - logs/${FILE_NAME}-all.%d{yyyy-MM-dd}.log - 30 - - - - - 30MB - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c.%M - %m%n - - - - - - - - - - - - - - - - - - diff --git a/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/function.tld b/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/function.tld deleted file mode 100644 index 78530770..00000000 --- a/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/function.tld +++ /dev/null @@ -1,44 +0,0 @@ - - - - - custom functions library - custom functions - 1.1 - function - http://www.victorzhang.com/function - - - - - length - - io.github.dunwu.javaee.taglib.function.Function - - - int length(java.lang.Object) - - - ${fn:length(string)} - - - - - - - substring - - io.github.dunwu.javaee.taglib.function.Function - - - java.lang.String substring(java.lang.String, int) - - - ${fn:length(string, 3)} - - - - diff --git a/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/hello.tld b/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/hello.tld deleted file mode 100644 index 2dca9237..00000000 --- a/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/hello.tld +++ /dev/null @@ -1,10 +0,0 @@ - - 1.0 - 2.0 - Example TLD - - Hello - io.github.dunwu.javaee.taglib.HelloTag - empty - - diff --git a/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/hello2.tld b/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/hello2.tld deleted file mode 100644 index 44728ef9..00000000 --- a/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/hello2.tld +++ /dev/null @@ -1,10 +0,0 @@ - - 1.0 - 2.0 - Example TLD with Body - - Hello - io.github.dunwu.javaee.taglib.HelloTag2 - scriptless - - diff --git a/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/hello3.tld b/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/hello3.tld deleted file mode 100644 index 2a7c20bb..00000000 --- a/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/hello3.tld +++ /dev/null @@ -1,13 +0,0 @@ - - 1.0 - 2.0 - Example TLD with Body - - Hello - io.github.dunwu.javaee.taglib.HelloTag3 - scriptless - - message - - - diff --git a/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/taglib.tld b/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/taglib.tld deleted file mode 100644 index d1a10666..00000000 --- a/codes/javaee/taglib/src/main/webapp/WEB-INF/tld/taglib.tld +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - 1.0 - 1.1 - taglib - http://www.victorzhang.com/tags - A simple tab library for the examples - - - copyright - io.github.dunwu.javaee.taglib.Copyright - JSP - Copyright tag. - - - - hello - io.github.dunwu.javaee.taglib.tags.HelloTag - empty - Hello tag with parameters. - - name - true - true - - - - - add - io.github.dunwu.javaee.taglib.tags.AddTag - empty - Add tag with parameters. - - num1 - true - true - - - num2 - true - true - - - - - toLowerCase - io.github.dunwu.javaee.taglib.tags.ToLowerCaseTag - JSP - Tag with body. - - - - loop - io.github.dunwu.javaee.taglib.tags.LoopTag - JSP - Tag with body. - - - - dynamicAttribute - io.github.dunwu.javaee.taglib.tags.DynamicAttributeTag - empty - true - Tag with dynamic attribute. - - - - table - io.github.dunwu.javaee.taglib.tags.table.Table - JSP - Table tag. - - items - true - true - - - url - false - true - - - - - column - io.github.dunwu.javaee.taglib.tags.table.Column - empty - Column tag. - - property - true - true - - - label - false - true - - - - - multi - io.github.dunwu.javaee.taglib.tags2.MultiTag - empty - multi tag with parameters. - - num1 - true - true - - - num2 - true - true - - - - - toUpperCase - io.github.dunwu.javaee.taglib.tags2.ToUpperCaseTag - tagdependent - body tag - - - - multiAttribute - io.github.dunwu.javaee.taglib.tags2.MultiAttributeTag - tagdependent - multi attribute tag with parameters. - - body1 - false - true - - - body2 - false - true - - - - diff --git a/codes/javaee/taglib/src/main/webapp/WEB-INF/web.xml b/codes/javaee/taglib/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index aa1943d6..00000000 --- a/codes/javaee/taglib/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - HelloServlet - /examples/configuration.jsp - - message - welcome to jsp - - 1 - - - HelloServlet - /config - /config.jsp - - - - - /WEB-INF/views/jsp/index.jsp - - diff --git a/codes/javaee/taglib/src/main/webapp/dynamic.jsp b/codes/javaee/taglib/src/main/webapp/dynamic.jsp deleted file mode 100644 index 62a3ae09..00000000 --- a/codes/javaee/taglib/src/main/webapp/dynamic.jsp +++ /dev/null @@ -1,18 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> -<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib" %> - - - - Insert title here - - - - - - - - \ No newline at end of file diff --git a/codes/javaee/taglib/src/main/webapp/function.jsp b/codes/javaee/taglib/src/main/webapp/function.jsp deleted file mode 100644 index 8e42df47..00000000 --- a/codes/javaee/taglib/src/main/webapp/function.jsp +++ /dev/null @@ -1,46 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> -<%@ taglib uri="http://www.victorzhang.com/function" prefix="fn" %> - - - - Insert title here - - - - -<% - request.setAttribute("string", "字符串测试"); -%> - - - - - - - - - - - - - - -
字符串变量${ string }
字符串长度(按字节计)${ fn:length(string) }
截取 7 个字节${ fn:substring(string, 7) }
- - - \ No newline at end of file diff --git a/codes/javaee/taglib/src/main/webapp/hello.jsp b/codes/javaee/taglib/src/main/webapp/hello.jsp deleted file mode 100644 index fc34f1f2..00000000 --- a/codes/javaee/taglib/src/main/webapp/hello.jsp +++ /dev/null @@ -1,15 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> -<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib" %> - - - - Insert title here - - - -
-
-
- - - \ No newline at end of file diff --git a/codes/javaee/taglib/src/main/webapp/images/I.png b/codes/javaee/taglib/src/main/webapp/images/I.png deleted file mode 100644 index e8512fb9..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/I.png and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/L.png b/codes/javaee/taglib/src/main/webapp/images/L.png deleted file mode 100644 index eb334eda..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/L.png and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/Lminus.png b/codes/javaee/taglib/src/main/webapp/images/Lminus.png deleted file mode 100644 index f7c43c0a..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/Lminus.png and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/Lplus.png b/codes/javaee/taglib/src/main/webapp/images/Lplus.png deleted file mode 100644 index 848ec2fc..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/Lplus.png and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/T.png b/codes/javaee/taglib/src/main/webapp/images/T.png deleted file mode 100644 index 30173254..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/T.png and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/Thumbs.db b/codes/javaee/taglib/src/main/webapp/images/Thumbs.db deleted file mode 100644 index afada40f..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/Thumbs.db and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/Tminus.png b/codes/javaee/taglib/src/main/webapp/images/Tminus.png deleted file mode 100644 index 2260e424..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/Tminus.png and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/Tplus.png b/codes/javaee/taglib/src/main/webapp/images/Tplus.png deleted file mode 100644 index 2c8d8f4f..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/Tplus.png and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/asc.gif b/codes/javaee/taglib/src/main/webapp/images/asc.gif deleted file mode 100644 index ca472c09..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/asc.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/bg-btn-blue.gif b/codes/javaee/taglib/src/main/webapp/images/bg-btn-blue.gif deleted file mode 100644 index bc03f1bd..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/bg-btn-blue.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/bg_4.jpg b/codes/javaee/taglib/src/main/webapp/images/bg_4.jpg deleted file mode 100644 index 076cdece..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/bg_4.jpg and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/blank.png b/codes/javaee/taglib/src/main/webapp/images/blank.png deleted file mode 100644 index cee9cd37..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/blank.png and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/bottom-left.gif b/codes/javaee/taglib/src/main/webapp/images/bottom-left.gif deleted file mode 100644 index 8ff0da62..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/bottom-left.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/bottom-right.gif b/codes/javaee/taglib/src/main/webapp/images/bottom-right.gif deleted file mode 100644 index 03a51b0a..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/bottom-right.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/btn-go-dark.gif b/codes/javaee/taglib/src/main/webapp/images/btn-go-dark.gif deleted file mode 100644 index 206207a7..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/btn-go-dark.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/delete.gif b/codes/javaee/taglib/src/main/webapp/images/delete.gif deleted file mode 100644 index c610608f..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/delete.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/delimiter.gif b/codes/javaee/taglib/src/main/webapp/images/delimiter.gif deleted file mode 100644 index 5bfd67a2..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/delimiter.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/desc.gif b/codes/javaee/taglib/src/main/webapp/images/desc.gif deleted file mode 100644 index dfab21c1..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/desc.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/edit.gif b/codes/javaee/taglib/src/main/webapp/images/edit.gif deleted file mode 100644 index 25583265..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/edit.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/element.gif b/codes/javaee/taglib/src/main/webapp/images/element.gif deleted file mode 100644 index b07b87b1..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/element.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/file.png b/codes/javaee/taglib/src/main/webapp/images/file.png deleted file mode 100644 index a20c6fa0..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/file.png and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/foldericon.png b/codes/javaee/taglib/src/main/webapp/images/foldericon.png deleted file mode 100644 index 2684748b..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/foldericon.png and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/ibm-tab-background.gif b/codes/javaee/taglib/src/main/webapp/images/ibm-tab-background.gif deleted file mode 100644 index d36dc073..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/ibm-tab-background.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/ibm_logo.gif b/codes/javaee/taglib/src/main/webapp/images/ibm_logo.gif deleted file mode 100644 index 5d1d0477..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/ibm_logo.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/left-nav-corner.gif b/codes/javaee/taglib/src/main/webapp/images/left-nav-corner.gif deleted file mode 100644 index ae395ea6..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/left-nav-corner.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/leftnav-overview-highlight.gif b/codes/javaee/taglib/src/main/webapp/images/leftnav-overview-highlight.gif deleted file mode 100644 index 2996ad0e..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/leftnav-overview-highlight.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/line01.gif b/codes/javaee/taglib/src/main/webapp/images/line01.gif deleted file mode 100644 index 6b9a6e3f..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/line01.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/masthead-links-gradient.gif b/codes/javaee/taglib/src/main/webapp/images/masthead-links-gradient.gif deleted file mode 100644 index b20772a4..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/masthead-links-gradient.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/memo.gif b/codes/javaee/taglib/src/main/webapp/images/memo.gif deleted file mode 100644 index 2387e310..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/memo.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/new.png b/codes/javaee/taglib/src/main/webapp/images/new.png deleted file mode 100644 index a20c6fa0..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/new.png and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/next.gif b/codes/javaee/taglib/src/main/webapp/images/next.gif deleted file mode 100644 index d0d7d2fe..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/next.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/openfoldericon.png b/codes/javaee/taglib/src/main/webapp/images/openfoldericon.png deleted file mode 100644 index 15fcd567..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/openfoldericon.png and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/pagetools-gradient.gif b/codes/javaee/taglib/src/main/webapp/images/pagetools-gradient.gif deleted file mode 100644 index 7beb3cb9..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/pagetools-gradient.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/pagetools_gradient_a.gif b/codes/javaee/taglib/src/main/webapp/images/pagetools_gradient_a.gif deleted file mode 100644 index 50b38ed0..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/pagetools_gradient_a.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/password.gif b/codes/javaee/taglib/src/main/webapp/images/password.gif deleted file mode 100644 index 8e3d48a6..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/password.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/prev.gif b/codes/javaee/taglib/src/main/webapp/images/prev.gif deleted file mode 100644 index e7d51fd7..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/prev.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/project.png b/codes/javaee/taglib/src/main/webapp/images/project.png deleted file mode 100644 index 4a773bc3..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/project.png and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/pspbrwse.jbf b/codes/javaee/taglib/src/main/webapp/images/pspbrwse.jbf deleted file mode 100644 index 1485c076..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/pspbrwse.jbf and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/rl-bullet.gif b/codes/javaee/taglib/src/main/webapp/images/rl-bullet.gif deleted file mode 100644 index 60046e74..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/rl-bullet.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/role.gif b/codes/javaee/taglib/src/main/webapp/images/role.gif deleted file mode 100644 index d28c326d..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/role.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/team.gif b/codes/javaee/taglib/src/main/webapp/images/team.gif deleted file mode 100644 index 81fb7b4a..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/team.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/template-gradient.gif b/codes/javaee/taglib/src/main/webapp/images/template-gradient.gif deleted file mode 100644 index 1bd540ea..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/template-gradient.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/top-content-shadow.gif b/codes/javaee/taglib/src/main/webapp/images/top-content-shadow.gif deleted file mode 100644 index 34035a8a..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/top-content-shadow.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/top-left.gif b/codes/javaee/taglib/src/main/webapp/images/top-left.gif deleted file mode 100644 index 00b8ab41..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/top-left.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/top-right.gif b/codes/javaee/taglib/src/main/webapp/images/top-right.gif deleted file mode 100644 index ed23cf8e..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/top-right.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/images/trans.gif b/codes/javaee/taglib/src/main/webapp/images/trans.gif deleted file mode 100644 index 6964168b..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/images/trans.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/main/webapp/index.jsp b/codes/javaee/taglib/src/main/webapp/index.jsp deleted file mode 100644 index 3a2872cf..00000000 --- a/codes/javaee/taglib/src/main/webapp/index.jsp +++ /dev/null @@ -1,82 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=gb2312" %> -<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib" %> - - - - Insert title here - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/codes/javaee/taglib/src/main/webapp/loop.jsp b/codes/javaee/taglib/src/main/webapp/loop.jsp deleted file mode 100644 index 20c7afea..00000000 --- a/codes/javaee/taglib/src/main/webapp/loop.jsp +++ /dev/null @@ -1,15 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib"%> - - - -Insert title here - - - -
-Loop. 
-
- - - \ No newline at end of file diff --git a/codes/javaee/taglib/src/main/webapp/multi.jsp b/codes/javaee/taglib/src/main/webapp/multi.jsp deleted file mode 100644 index a9d2d65b..00000000 --- a/codes/javaee/taglib/src/main/webapp/multi.jsp +++ /dev/null @@ -1,13 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib"%> - - - -Insert title here - - - - - - - \ No newline at end of file diff --git a/codes/javaee/taglib/src/main/webapp/multiAttribute.jsp b/codes/javaee/taglib/src/main/webapp/multiAttribute.jsp deleted file mode 100644 index 1450c5bd..00000000 --- a/codes/javaee/taglib/src/main/webapp/multiAttribute.jsp +++ /dev/null @@ -1,21 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib"%> - - - -Insert title here - - - - - - 标签体一, - 标签体二, - - - - \ No newline at end of file diff --git a/codes/javaee/taglib/src/main/webapp/style/style.css b/codes/javaee/taglib/src/main/webapp/style/style.css deleted file mode 100644 index d6233e67..00000000 --- a/codes/javaee/taglib/src/main/webapp/style/style.css +++ /dev/null @@ -1,66 +0,0 @@ -body, td, div, input, textarea, span { - font-size: 12px; - font-family: Arial; -} - -.list_table { - border-collapse: collapse; -} - -.list_table .tr_title { - -} - -.list_table .tr_title td { - border: 1px solid #DDDDDD; - background: #EEEEEE; - background: url("../images/ibm-tab-background.gif") no-repeat 0px 0px; - border-top: 1px solid #7E9AB0; - color: #FFFFFF; - text-align: center; - padding-top: 4px; - padding-bottom: 4px; - white-space: nowrap; - font-weight: bold; -} - -.list_table .tr_title td span { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.list_table .tr_title img { - padding-top: 1px; - padding-bottom: 1px; -} - -.list_table .tr_data { - padding: 2px; - text-align: center; -} - -.list_table .tr_data td { - border: 1px solid #DDDDDD; - padding: 2px; -} - -.list_table .tr_data td span { - width: 100%; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.button { - color: #fff; - font-weight: bold; - font-size: 11px; - font-family: verdana; - text-align: center; - padding: .17em 0 .2em .17em; - border-style: solid; - border-width: 1px; - border-color: #9cf #159 #159 #9cf; - background: #69c url(../images/bg-btn-blue.gif) repeat-x; -} diff --git a/codes/javaee/taglib/src/main/webapp/table.jsp b/codes/javaee/taglib/src/main/webapp/table.jsp deleted file mode 100644 index 25f735e4..00000000 --- a/codes/javaee/taglib/src/main/webapp/table.jsp +++ /dev/null @@ -1,186 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=gb2312"%> -<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib"%> -<%@page import="java.util.ArrayList"%> -<%@page import="io.github.dunwu.javaee.taglib.bean.Person"%> -<%@page import="java.util.List"%> -<% - List personList = new ArrayList(); - - int i = 1; - - Person person = new Person(); - person.setId(i++); - person.setName(""); - person.setAge(20); - person.setSex(""); - person.setAddress("кϵ԰"); - person.setBirthday("2008-08-08"); - person.setMobile("13820080808"); - person.setTelephone("69653234"); - person.setCity(""); - - personList.add(person); - - Person person2 = new Person(); - person2.setId(i++); - person2.setName(""); - person2.setAge(20); - person2.setSex(""); - person2.setAddress("жʳǸͬ"); - person2.setBirthday("2008-01-01"); - person2.setMobile("13820080808"); - person2.setTelephone("20054879"); - person2.setCity(""); - - personList.add(person2); - - Person person3 = new Person(); - person3.setId(i++); - person3.setName(""); - person3.setAge(20); - person3.setSex(""); - person3.setAddress("жʳǸͬ"); - person3.setBirthday("2008-01-01"); - person3.setMobile("13820080808"); - person3.setTelephone("20054879"); - person3.setCity(""); - - personList.add(person3); - - Person person4 = new Person(); - person4.setId(i++); - person4.setName(""); - person4.setAge(20); - person4.setSex("Ů"); - person4.setAddress("жʳǸͬ"); - person4.setBirthday("2008-01-01"); - person4.setMobile("13820080808"); - person4.setTelephone("20054879"); - person4.setCity(""); - - personList.add(person4); - - Person person5 = new Person(); - person5.setId(i++); - person5.setName(""); - person5.setAge(20); - person5.setSex(""); - person5.setAddress("жʳǸͬ"); - person5.setBirthday("2008-01-01"); - person5.setMobile("13820080808"); - person5.setTelephone("20054879"); - person5.setCity(""); - - personList.add(person5); - - Person person6 = new Person(); - person6.setId(i++); - person6.setName(""); - person6.setAge(20); - person6.setSex("Ů"); - person6.setAddress("жʳǸͬ"); - person6.setBirthday("2008-01-01"); - person6.setMobile("13820080808"); - person6.setTelephone("20054879"); - person6.setCity(""); - - personList.add(person6); - - Person person7 = new Person(); - person7.setId(i++); - person7.setName(""); - person7.setAge(20); - person7.setSex(""); - person7.setAddress("кּ36A"); - person7.setBirthday("2008-01-01"); - person7.setMobile("13820080808"); - person7.setTelephone("20054879"); - person7.setCity(""); - - personList.add(person7); - - request.setAttribute("personList", personList); -%> - - - -Insert title here - - - - - - - - - - - - - - - - - - - - - - - diff --git a/codes/javaee/taglib/src/main/webapp/taglib/copyright.jsp b/codes/javaee/taglib/src/main/webapp/taglib/copyright.jsp deleted file mode 100644 index 6f0b5dde..00000000 --- a/codes/javaee/taglib/src/main/webapp/taglib/copyright.jsp +++ /dev/null @@ -1,13 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib"%> - - - -Insert title here - - - -Hello, taglib - - - \ No newline at end of file diff --git a/codes/javaee/taglib/src/main/webapp/taglib/helloTag.jsp b/codes/javaee/taglib/src/main/webapp/taglib/helloTag.jsp deleted file mode 100644 index bba11051..00000000 --- a/codes/javaee/taglib/src/main/webapp/taglib/helloTag.jsp +++ /dev/null @@ -1,9 +0,0 @@ -<%@ taglib prefix="ex" uri="/WEB-INF/tld/hello.tld" %> - - - A sample custom tag - - - - - \ No newline at end of file diff --git a/codes/javaee/taglib/src/main/webapp/taglib/helloTag2.jsp b/codes/javaee/taglib/src/main/webapp/taglib/helloTag2.jsp deleted file mode 100644 index e6fbe55c..00000000 --- a/codes/javaee/taglib/src/main/webapp/taglib/helloTag2.jsp +++ /dev/null @@ -1,11 +0,0 @@ -<%@ taglib prefix="ex" uri="/WEB-INF/tld/hello2.tld"%> - - - A sample custom tag - - - - This is message body - - - \ No newline at end of file diff --git a/codes/javaee/taglib/src/main/webapp/taglib/helloTag3.jsp b/codes/javaee/taglib/src/main/webapp/taglib/helloTag3.jsp deleted file mode 100644 index 1bff0b9b..00000000 --- a/codes/javaee/taglib/src/main/webapp/taglib/helloTag3.jsp +++ /dev/null @@ -1,9 +0,0 @@ -<%@ taglib prefix="ex" uri="/WEB-INF/tld/hello3.tld" %> - - - A sample custom tag - - - - - \ No newline at end of file diff --git a/codes/javaee/taglib/src/main/webapp/test.jsp b/codes/javaee/taglib/src/main/webapp/test.jsp deleted file mode 100644 index 42d01f2d..00000000 --- a/codes/javaee/taglib/src/main/webapp/test.jsp +++ /dev/null @@ -1,41 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8"%> -<%@page import="java.util.List"%> -<%@page import="java.util.ArrayList"%> -<%@page import="io.github.dunwu.javaee.taglib.bean.Person"%> -<% - if (true) - { - List personList = new ArrayList(); - - Person person = new Person(); - person.setId(1); - person.setName("张三"); - person.setAge(20); - person.setSex("男"); - person.setAddress("北京市海淀区上地软件园"); - person.setBirthday("2008-08-08"); - person.setMobile("13820080808"); - person.setTelephone("69653234"); - person.setCity("北京"); - - personList.add(person); - Person person2 = new Person(); - person2.setId(2); - person2.setName("李四"); - person2.setAge(20); - person2.setSex("女"); - person2.setAddress("北京市东皇城根锡拉胡同"); - person2.setBirthday("2008-01-01"); - person2.setMobile("13820080808"); - person2.setTelephone("20054879"); - person2.setCity("北京"); - - personList.add(person2); - - request.setAttribute("personList", personList); - - request.getRequestDispatcher("/index.jsp").forward(request, - response); - - } -%> diff --git a/codes/javaee/taglib/src/main/webapp/toLowerCase.jsp b/codes/javaee/taglib/src/main/webapp/toLowerCase.jsp deleted file mode 100644 index 79b6180b..00000000 --- a/codes/javaee/taglib/src/main/webapp/toLowerCase.jsp +++ /dev/null @@ -1,13 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> -<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib" %> - - - - Insert title here - - - -Hello, To Lower Case Tag with Body. - - - \ No newline at end of file diff --git a/codes/javaee/taglib/src/main/webapp/toUpperCase.jsp b/codes/javaee/taglib/src/main/webapp/toUpperCase.jsp deleted file mode 100644 index 098e3ea4..00000000 --- a/codes/javaee/taglib/src/main/webapp/toUpperCase.jsp +++ /dev/null @@ -1,15 +0,0 @@ -<%@ page language="java" contentType="text/html; charset=UTF-8" %> -<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib" %> - - - - Insert title here - - - - - This is a to upper case tag. - - - - \ No newline at end of file diff --git a/codes/javaee/taglib/src/main/webapp/vote.gif b/codes/javaee/taglib/src/main/webapp/vote.gif deleted file mode 100644 index 26a5a3e5..00000000 Binary files a/codes/javaee/taglib/src/main/webapp/vote.gif and /dev/null differ diff --git a/codes/javaee/taglib/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java b/codes/javaee/taglib/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java deleted file mode 100644 index f20d87b3..00000000 --- a/codes/javaee/taglib/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.github.dunwu.javaee.server; - -import java.util.ArrayList; - -import org.apache.commons.lang3.StringUtils; -import org.assertj.core.util.Lists; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.WebAppClassLoader; -import org.eclipse.jetty.webapp.WebAppContext; - -/** - * JettyFactory 可以工作在 Eclipse 和 Intellij 中,用来启动 jetty 服务。 Intellij - * 并不支持jetty,所以要想类似eclipse一样的使用jetty,需要配置webdefault.xml。 - * - * @author Zhang Peng - */ -@SuppressWarnings("unused") -public class JettyFactory { - private static final int PORT = 9798; - private static final String CONTEXT = "/"; - private static final String RESOURCE_BASE_PATH = "src/main/webapp"; - private static final String WEB_XML_PATH = "/WEB-INF/web.xml"; - private static final String[] TLD_JAR_NAMES = - new String[]{"sitemesh", "spring-webmvc", "shiro-web", "tiles"}; - private static final String WINDOWS_WEBDEFAULT_PATH = "jetty/webdefault.xml"; - - public static final int IDE_ECLIPSE = 0; - public static final int IDE_INTELLIJ = 1; - - - public static Server initServer() { - Profiles.setProfileAsSystemProperty(Profiles.DEVELOPMENT); - WebAppContext webAppContext = new WebAppContext(); - Server server = new Server(PORT); - server.setHandler(webAppContext); - return server; - } - - public static void initWebAppContext(Server server, int type) throws Exception { - System.out.println("[INFO] Application loading"); - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - webAppContext.setContextPath(CONTEXT); - webAppContext.setResourceBase(getAbsolutePath() + RESOURCE_BASE_PATH); - webAppContext.setDescriptor(getAbsolutePath() + RESOURCE_BASE_PATH + WEB_XML_PATH); - - if (IDE_INTELLIJ == type) { - webAppContext.setDefaultsDescriptor(WINDOWS_WEBDEFAULT_PATH); - supportJspAndSetTldJarNames(server, TLD_JAR_NAMES); - } else { - webAppContext.setParentLoaderPriority(true); - } - - System.out.println("[INFO] Application loaded"); - } - - public static void reloadWebAppContext(Server server) throws Exception { - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - System.out.println("[INFO] Application reloading"); - webAppContext.stop(); - WebAppClassLoader classLoader = new WebAppClassLoader(webAppContext); - classLoader.addClassPath(getAbsolutePath() + "target/classes"); - classLoader.addClassPath(getAbsolutePath() + "target/test-classes"); - webAppContext.setClassLoader(classLoader); - webAppContext.start(); - System.out.println("[INFO] Application reloaded"); - } - - - public static void startServer(Server server) throws Exception { - System.out.println("[HINT] Don't forget to set -XX:MaxPermSize=128m"); - server.start(); - System.out.println("Server running at http://localhost:" + PORT + CONTEXT); - System.out.println("[HINT] Hit Enter to reload the application quickly"); - } - - public static String getAbsolutePath() { - String path = null; - String folderPath = JettyFactory.class.getProtectionDomain().getCodeSource().getLocation() - .getPath().substring(1); - if (folderPath.indexOf("target") > 0) { - path = folderPath.substring(0, folderPath.indexOf("target")); - } - return path; - } - - public static void supportJspAndSetTldJarNames(Server server, String... jarNames) { - WebAppContext context = (WebAppContext) server.getHandler(); - // This webapp will use jsps and jstl. We need to enable the AnnotationConfiguration in - // order to correctly set up the jsp container - org.eclipse.jetty.webapp.Configuration.ClassList classlist = - org.eclipse.jetty.webapp.Configuration.ClassList.setServerDefault(server); - classlist.addBefore("org.eclipse.jetty.webapp.JettyWebXmlConfiguration", - "org.eclipse.jetty.annotations.AnnotationConfiguration"); - // Set the ContainerIncludeJarPattern so that jetty examines these container-path jars for - // tlds, web-fragments etc. - // If you omit the jar that contains the jstl .tlds, the jsp engine will scan for them - // instead. - ArrayList jarNameExprssions = Lists.newArrayList(".*/[^/]*servlet-api-[^/]*\\.jar$", - ".*/javax.servlet.jsp.jstl-.*\\.jar$", ".*/[^/]*taglibs.*\\.jar$"); - - for (String jarName : jarNames) { - jarNameExprssions.add(".*/" + jarName + "-[^/]*\\.jar$"); - } - - context.setAttribute("org.eclipse.jetty.io.github.dunwu.javaee.server.webapp.ContainerIncludeJarPattern", - StringUtils.join(jarNameExprssions, '|')); - } -} diff --git a/codes/javaee/taglib/src/test/java/io/github/dunwu/javaee/server/Profiles.java b/codes/javaee/taglib/src/test/java/io/github/dunwu/javaee/server/Profiles.java deleted file mode 100644 index 3a8ee7c4..00000000 --- a/codes/javaee/taglib/src/test/java/io/github/dunwu/javaee/server/Profiles.java +++ /dev/null @@ -1,29 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2005, 2014 springside.github.io - * - * Licensed under the Apache License, Version 2.0 (the "License"); - *******************************************************************************/ -package io.github.dunwu.javaee.server; - -/** - * Spring profile 常用方法与profile名称。 - * - * @author calvin - */ -public class Profiles { - - public static final String ACTIVE_PROFILE = "spring.profiles.active"; - public static final String DEFAULT_PROFILE = "spring.profiles.default"; - - public static final String PRODUCTION = "production"; - public static final String DEVELOPMENT = "development"; - public static final String UNIT_TEST = "test"; - public static final String FUNCTIONAL_TEST = "functional"; - - /** - * 在Spring启动前,设置profile的环境变量。 - */ - public static void setProfileAsSystemProperty(String profile) { - System.setProperty(ACTIVE_PROFILE, profile); - } -} diff --git a/codes/javaee/taglib/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java b/codes/javaee/taglib/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java deleted file mode 100644 index 2936b162..00000000 --- a/codes/javaee/taglib/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.github.dunwu.javaee.server; - -import org.eclipse.jetty.server.Server; - -/** - * 快速启动 jetty 服务器,方便测试 - * - * @author Zhang Peng - */ -public class QuickStartServer { - // private static int STARTUP_TYPE = JettyFactory.IDE_ECLIPSE; - private static int STARTUP_TYPE = JettyFactory.IDE_INTELLIJ; - - public static void main(String[] args) throws Exception { - Server server = JettyFactory.initServer(); - JettyFactory.initWebAppContext(server, STARTUP_TYPE); - - try { - JettyFactory.startServer(server); - - // 等待用户输入回车重载应用 - while (true) { - char c = (char) System.in.read(); - if (c == '\n') { - JettyFactory.reloadWebAppContext(server); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.exit(-1); - } - } -} diff --git a/codes/javaee/taglib/src/test/resources/jetty/webdefault.xml b/codes/javaee/taglib/src/test/resources/jetty/webdefault.xml deleted file mode 100644 index 65ec9cae..00000000 --- a/codes/javaee/taglib/src/test/resources/jetty/webdefault.xml +++ /dev/null @@ -1,534 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Default web.xml file. - This file is applied to a Web application before it's own WEB_INF/web.xml file - - - - - - - - org.eclipse.jetty.servlet.listener.ELContextCleaner - - - - - - - - org.eclipse.jetty.servlet.listener.IntrospectorCleaner - - - - - - - - - - - - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - aliases - false - - - acceptRanges - true - - - dirAllowed - true - - - welcomeServlets - false - - - redirectWelcome - false - - - maxCacheSize - 256000000 - - - maxCachedFileSize - 200000000 - - - maxCachedFiles - 2048 - - - gzip - false - - - etags - false - - - useFileMappedBuffer - false - - - - 0 - - - - default - / - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jsp - org.eclipse.jetty.jsp.JettyJspServlet - - logVerbosityLevel - DEBUG - - - fork - false - - - xpoweredBy - false - - - compilerTargetVM - 1.7 - - - compilerSourceVM - 1.7 - - - 0 - - - - jsp - *.jsp - *.jspf - *.jspx - *.xsp - *.JSP - *.JSPF - *.JSPX - *.XSP - - - - - - - - 30 - - - - - - - - - - - - - - - index.html - index.htm - index.jsp - - - - - - - - ar - ISO-8859-6 - - - be - ISO-8859-5 - - - bg - ISO-8859-5 - - - ca - ISO-8859-1 - - - cs - ISO-8859-2 - - - da - ISO-8859-1 - - - de - ISO-8859-1 - - - el - ISO-8859-7 - - - en - ISO-8859-1 - - - es - ISO-8859-1 - - - et - ISO-8859-1 - - - fi - ISO-8859-1 - - - fr - ISO-8859-1 - - - hr - ISO-8859-2 - - - hu - ISO-8859-2 - - - is - ISO-8859-1 - - - it - ISO-8859-1 - - - iw - ISO-8859-8 - - - ja - Shift_JIS - - - ko - EUC-KR - - - lt - ISO-8859-2 - - - lv - ISO-8859-2 - - - mk - ISO-8859-5 - - - nl - ISO-8859-1 - - - no - ISO-8859-1 - - - pl - ISO-8859-2 - - - pt - ISO-8859-1 - - - ro - ISO-8859-2 - - - ru - ISO-8859-5 - - - sh - ISO-8859-5 - - - sk - ISO-8859-2 - - - sl - ISO-8859-2 - - - sq - ISO-8859-2 - - - sr - ISO-8859-5 - - - sv - ISO-8859-1 - - - tr - ISO-8859-9 - - - uk - ISO-8859-5 - - - zh - GB2312 - - - zh_TW - Big5 - - - - - - - - - Disable TRACE - / - TRACE - - - - - - Enable everything but TRACE - / - TRACE - - - - - diff --git a/codes/javaee/websocket/pom.xml b/codes/javaee/websocket/pom.xml deleted file mode 100644 index cb21a2ae..00000000 --- a/codes/javaee/websocket/pom.xml +++ /dev/null @@ -1,136 +0,0 @@ - - - 4.0.0 - - - - - - io.github.dunwu - javaee-notes-websocket - 1.0.0 - war - - - - - io.github.dunwu - javaee-notes-parent - 1.0.0 - ../parent - - - - - ch.qos.logback - logback-classic - - - org.slf4j - jcl-over-slf4j - - - - - - javax.servlet - javax.servlet-api - provided - - - javax.servlet.jsp - jsp-api - provided - - - javax.websocket - javax.websocket-api - provided - - - - - - org.eclipse.jetty - jetty-webapp - - - org.eclipse.jetty - jetty-annotations - - - org.eclipse.jetty - apache-jsp - - - org.eclipse.jetty - apache-jstl - - - org.eclipse.jetty.websocket - javax-websocket-server-impl - - - - - - org.apache.commons - commons-lang3 - - - com.google.guava - guava - - - - - - - - UTF-8 - 1.7 - ${java.version} - ${java.version} - - - - - - - - - - - org.apache.tomcat.maven - tomcat7-maven-plugin - - 8089 - /${artifactId} - UTF-8 - - - - org.eclipse.jetty - jetty-maven-plugin - - - 8089 - - - /${artifactId} - - - - - - - - - - javaee-notes-websocket - javaee 学习笔记之 websocket - - - - diff --git a/codes/javaee/websocket/src/main/java/io/github/dunwu/javaee/servlet/SocketServlet.java b/codes/javaee/websocket/src/main/java/io/github/dunwu/javaee/servlet/SocketServlet.java deleted file mode 100644 index 93423d5e..00000000 --- a/codes/javaee/websocket/src/main/java/io/github/dunwu/javaee/servlet/SocketServlet.java +++ /dev/null @@ -1,60 +0,0 @@ -package io.github.dunwu.javaee.servlet; - -import java.io.IOException; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import io.github.dunwu.javaee.websocket.WebSocketServer; - -/** - * SocketServlet信息示例 - */ -public class SocketServlet extends HttpServlet { - private static final long serialVersionUID = -7936817351382556277L; - - private final Logger logger = LoggerFactory.getLogger(this.getClass()); - - /** - * Constructor of the object. - */ - public SocketServlet() { - super(); - } - - public void destroy() { - super.destroy(); // Just puts "destroy" string in log - // Put your code here - } - - public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - - // 设置 request,response 编码方式 - response.setCharacterEncoding("UTF-8"); - request.setCharacterEncoding("UTF-8"); - - // 取浏览器提交的 name 参数 - String id = request.getParameter("id"); - String text = request.getParameter("text"); - - if (id != null && id.length() > 0) { - WebSocketServer.send(id, text); - } else { - WebSocketServer.sendAll(text); - } - } - - public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - this.doGet(request, response); - } - - public void init() throws ServletException { - // Put your code here - } - -} diff --git a/codes/javaee/websocket/src/main/java/io/github/dunwu/javaee/websocket/WebSocketServer.java b/codes/javaee/websocket/src/main/java/io/github/dunwu/javaee/websocket/WebSocketServer.java deleted file mode 100644 index e54635c2..00000000 --- a/codes/javaee/websocket/src/main/java/io/github/dunwu/javaee/websocket/WebSocketServer.java +++ /dev/null @@ -1,124 +0,0 @@ -package io.github.dunwu.javaee.websocket; - -import java.io.IOException; -import java.util.HashSet; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -import javax.websocket.CloseReason; -import javax.websocket.EncodeException; -import javax.websocket.EndpointConfig; -import javax.websocket.OnClose; -import javax.websocket.OnError; -import javax.websocket.OnMessage; -import javax.websocket.OnOpen; -import javax.websocket.Session; -import javax.websocket.server.PathParam; -import javax.websocket.server.ServerEndpoint; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Websocket 消息处理中心 - * @see https://github.com/jetty-project/embedded-jetty-websocket-examples - * @author Zhang Peng - */ -@ServerEndpoint(value = "/auth/user/{id}", configurator = WebSocketServerConfigurator.class) -public class WebSocketServer { - - private static final Logger logger = LoggerFactory.getLogger(WebSocketServer.class); - - static Map> userSessionMap = new ConcurrentHashMap<>(); - // private static Map userSessionMap = new ConcurrentHashMap>(); - - @OnMessage - public void onMessage(String message, Session session) throws IOException, InterruptedException { - logger.debug("收到一条客户端消息。session:{}, msg:{}", session.getId(), message); - } - - @OnOpen - public void onOpen(Session session, EndpointConfig config, @PathParam("id") String id) { - logger.info("Session {} connected.", session.getId()); - - // 如果是新 Session,记录进 Map - boolean isNewUser = true; - Iterator i = userSessionMap.entrySet().iterator(); - while (i.hasNext()) { - Map.Entry entry = (Map.Entry) i.next(); - String key = (String) entry.getKey(); - if (key.equals(id)) { - userSessionMap.get(key).add(session); - isNewUser = false; - break; - } - } - if (isNewUser) { - Set sessions = new HashSet<>(); - sessions.add(session); - userSessionMap.put(id, sessions); - } - logger.info("当前在线用户数: {}", userSessionMap.values().size()); - } - - @OnClose - public void onClose(Session session, CloseReason closeReason) { - logger.info("Session {} disconnected. Because of {}", session.getId(), closeReason); - for (Set item : userSessionMap.values()) { - if (item.contains(session)) { - // 删除连接 session - item.remove(session); - // 如果 userId 对应的 session 数为 0 ,删除该 userId 对应的记录 - if (0 == item.size()) { - userSessionMap.values().remove(item); - } - break; - } - } - logger.info("当前在线用户数: {}", userSessionMap.values().size()); - } - - @OnError - public void onError(Throwable error) { - logger.error("Websocket error: {}", error.getMessage()); - } - - /** - * 发送广播消息 - * @param message - */ - public static void sendAll(String message) { - logger.info("SendAll: {}", message); - - for (Set groups : userSessionMap.values()) { - for (Session session : groups) { - try { - session.getBasicRemote().sendObject(message); - } catch (IOException e) { - logger.error(e.toString()); - } catch (EncodeException e) { - logger.error(e.toString()); - } - } - } - } - - public static void send(String userId, String message) { - for (String id : userSessionMap.keySet()) { - if (id.equals(userId)) { - for (Session session : userSessionMap.get(userId)) { - try { - session.getBasicRemote().sendObject(message); - logger.info("SendAll: {}", message); - } catch (Exception e) { - logger.error(e.toString()); - } - } - } - } - - } - -} diff --git a/codes/javaee/websocket/src/main/java/io/github/dunwu/javaee/websocket/WebSocketServerConfigurator.java b/codes/javaee/websocket/src/main/java/io/github/dunwu/javaee/websocket/WebSocketServerConfigurator.java deleted file mode 100644 index 6b421f59..00000000 --- a/codes/javaee/websocket/src/main/java/io/github/dunwu/javaee/websocket/WebSocketServerConfigurator.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.github.dunwu.javaee.websocket; - -import javax.servlet.http.HttpSession; -import javax.websocket.HandshakeResponse; -import javax.websocket.server.HandshakeRequest; -import javax.websocket.server.ServerEndpointConfig; - -/** - * @author Zhang Peng - */ -public class WebSocketServerConfigurator extends ServerEndpointConfig.Configurator { - @Override - public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { - HttpSession httpSession = (HttpSession) request.getHttpSession(); - sec.getUserProperties().put(HttpSession.class.getName(), httpSession); - } -} diff --git a/codes/javaee/websocket/src/main/resources/logback.xml b/codes/javaee/websocket/src/main/resources/logback.xml deleted file mode 100644 index 2b38c24b..00000000 --- a/codes/javaee/websocket/src/main/resources/logback.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - %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 - - - - - - - - - - - - - - - - diff --git a/codes/javaee/websocket/src/main/webapp/WEB-INF/web.xml b/codes/javaee/websocket/src/main/webapp/WEB-INF/web.xml deleted file mode 100644 index 32dca619..00000000 --- a/codes/javaee/websocket/src/main/webapp/WEB-INF/web.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - SocketServlet - - io.github.dunwu.javaee.servlet.SocketServlet - - - - SocketServlet - /sock/* - - - - index.jsp - - - diff --git a/codes/javaee/websocket/src/main/webapp/websocketA.html b/codes/javaee/websocket/src/main/webapp/websocketA.html deleted file mode 100644 index 68b310d4..00000000 --- a/codes/javaee/websocket/src/main/webapp/websocketA.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - webscocket test - - - - - - - - -
-
- - - diff --git a/codes/javaee/websocket/src/main/webapp/websocketB.html b/codes/javaee/websocket/src/main/webapp/websocketB.html deleted file mode 100644 index b0c88eaa..00000000 --- a/codes/javaee/websocket/src/main/webapp/websocketB.html +++ /dev/null @@ -1,62 +0,0 @@ - - - - - webscocket test - - - - - - - - -
-
- - - diff --git a/codes/javaee/websocket/src/test/java/io/github/dunwu/javaee/server/DevServer.java b/codes/javaee/websocket/src/test/java/io/github/dunwu/javaee/server/DevServer.java deleted file mode 100644 index 6e6408a6..00000000 --- a/codes/javaee/websocket/src/test/java/io/github/dunwu/javaee/server/DevServer.java +++ /dev/null @@ -1,125 +0,0 @@ -package io.github.dunwu.javaee.server; - -import java.util.ArrayList; - -import org.apache.commons.lang3.StringUtils; -import org.eclipse.jetty.server.Server; -import org.eclipse.jetty.webapp.WebAppClassLoader; -import org.eclipse.jetty.webapp.WebAppContext; -import org.eclipse.jetty.websocket.jsr356.server.ServerContainer; -import org.eclipse.jetty.websocket.jsr356.server.deploy.WebSocketServerContainerInitializer; - -import com.google.common.collect.Lists; - -import io.github.dunwu.javaee.websocket.WebSocketServer; - -/** - * DevServer 可以工作在 Eclipse 和 Intellij 中,用来启动 jetty 服务。 Intellij 并不支持jetty,所以要想类似eclipse一样的使用jetty,需要配置webdefault.xml。 - * - * @author Zhang Peng - */ -@SuppressWarnings("unused") -public class DevServer { - private static final int PORT = 8089; - private static final String CONTEXT = "/"; - private static final String RESOURCE_BASE_PATH = "src/main/webapp"; - private static final String WEB_XML_PATH = "/WEB-INF/web.xml"; - private static final String[] TLD_JAR_NAMES = new String[] { "sitemesh", "spring-webmvc", "shiro-web", "tiles" }; - private static final String WINDOWS_WEBDEFAULT_PATH = "jetty/webdefault.xml"; - - public static final int IDE_ECLIPSE = 0; - public static final int IDE_INTELLIJ = 1; - - public static final String PRODUCTION = "production"; - public static final String DEVELOPMENT = "development"; - public static final String UNIT_TEST = "test"; - public static final String FUNCTIONAL_TEST = "functional"; - - private static void reloadWebAppContext(Server server) throws Exception { - WebAppContext webAppContext = (WebAppContext) server.getHandler(); - System.out.println("[INFO] Application reloading"); - webAppContext.stop(); - WebAppClassLoader classLoader = new WebAppClassLoader(webAppContext); - classLoader.addClassPath(getAbsolutePath() + "target/classes"); - classLoader.addClassPath(getAbsolutePath() + "target/test-classes"); - webAppContext.setClassLoader(classLoader); - webAppContext.start(); - System.out.println("[INFO] Application reloaded"); - } - - private static String getAbsolutePath() { - String path = null; - String folderPath = DevServer.class.getProtectionDomain().getCodeSource().getLocation().getPath().substring(1); - if (folderPath.indexOf("target") > 0) { - path = folderPath.substring(0, folderPath.indexOf("target")); - } - return path; - } - - private static void supportJspAndSetTldJarNames(Server server, String... jarNames) { - WebAppContext context = (WebAppContext) server.getHandler(); - // This webapp will use jsps and jstl. We need to enable the AnnotationConfiguration in - // order to correctly set up the jsp container - org.eclipse.jetty.webapp.Configuration.ClassList classlist = org.eclipse.jetty.webapp.Configuration.ClassList - .setServerDefault(server); - classlist.addBefore("org.eclipse.jetty.webapp.JettyWebXmlConfiguration", - "org.eclipse.jetty.annotations.AnnotationConfiguration"); - // Set the ContainerIncludeJarPattern so that jetty examines these container-path jars for - // tlds, web-fragments etc. - // If you omit the jar that contains the jstl .tlds, the jsp engine will scan for them - // instead. - ArrayList jarNameExprssions = Lists.newArrayList(".*/[^/]*servlet-api-[^/]*\\.jar$", - ".*/javax.servlet.jsp.jstl-.*\\.jar$", ".*/[^/]*taglibs.*\\.jar$"); - - for (String jarName : jarNames) { - jarNameExprssions.add(".*/" + jarName + "-[^/]*\\.jar$"); - } - - context.setAttribute("org.eclipse.jetty.io.github.dunwu.javaee.server.webapp.ContainerIncludeJarPattern", - StringUtils.join(jarNameExprssions, '|')); - } - - public static void main(String[] args) throws Exception { - - // 初始化 WebAppContext - System.out.println("[INFO] Application loading"); - WebAppContext webAppContext = new WebAppContext(); - webAppContext.setContextPath(CONTEXT); - webAppContext.setResourceBase(getAbsolutePath() + RESOURCE_BASE_PATH); - webAppContext.setDescriptor(getAbsolutePath() + RESOURCE_BASE_PATH + WEB_XML_PATH); - webAppContext.setDefaultsDescriptor(WINDOWS_WEBDEFAULT_PATH); - - // 初始化 server - Server server = new Server(PORT); - server.setHandler(webAppContext); - supportJspAndSetTldJarNames(server, TLD_JAR_NAMES); - - System.out.println("[INFO] Application loaded"); - - try { - // Initialize javax.websocket layer - ServerContainer wscontainer = WebSocketServerContainerInitializer.configureContext(webAppContext); - - // Add WebSocket endpoint to javax.websocket layer - wscontainer.addEndpoint(WebSocketServer.class); - - System.out.println("[HINT] Don't forget to set -XX:MaxPermSize=128m"); - System.out.println("Server running at http://localhost:" + PORT + CONTEXT); - System.out.println("[HINT] Hit Enter to reload the application quickly"); - - server.start(); - // server.join(); - - // 等待用户输入回车重载应用 - while (true) { - char c = (char) System.in.read(); - if (c == '\n') { - DevServer.reloadWebAppContext(server); - } - } - } catch (Exception e) { - e.printStackTrace(); - System.exit(-1); - } - } -} diff --git a/codes/javaee/websocket/src/test/resources/jetty/webdefault.xml b/codes/javaee/websocket/src/test/resources/jetty/webdefault.xml deleted file mode 100644 index 65ec9cae..00000000 --- a/codes/javaee/websocket/src/test/resources/jetty/webdefault.xml +++ /dev/null @@ -1,534 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Default web.xml file. - This file is applied to a Web application before it's own WEB_INF/web.xml file - - - - - - - - org.eclipse.jetty.servlet.listener.ELContextCleaner - - - - - - - - org.eclipse.jetty.servlet.listener.IntrospectorCleaner - - - - - - - - - - - - - - - - - default - org.eclipse.jetty.servlet.DefaultServlet - - aliases - false - - - acceptRanges - true - - - dirAllowed - true - - - welcomeServlets - false - - - redirectWelcome - false - - - maxCacheSize - 256000000 - - - maxCachedFileSize - 200000000 - - - maxCachedFiles - 2048 - - - gzip - false - - - etags - false - - - useFileMappedBuffer - false - - - - 0 - - - - default - / - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - jsp - org.eclipse.jetty.jsp.JettyJspServlet - - logVerbosityLevel - DEBUG - - - fork - false - - - xpoweredBy - false - - - compilerTargetVM - 1.7 - - - compilerSourceVM - 1.7 - - - 0 - - - - jsp - *.jsp - *.jspf - *.jspx - *.xsp - *.JSP - *.JSPF - *.JSPX - *.XSP - - - - - - - - 30 - - - - - - - - - - - - - - - index.html - index.htm - index.jsp - - - - - - - - ar - ISO-8859-6 - - - be - ISO-8859-5 - - - bg - ISO-8859-5 - - - ca - ISO-8859-1 - - - cs - ISO-8859-2 - - - da - ISO-8859-1 - - - de - ISO-8859-1 - - - el - ISO-8859-7 - - - en - ISO-8859-1 - - - es - ISO-8859-1 - - - et - ISO-8859-1 - - - fi - ISO-8859-1 - - - fr - ISO-8859-1 - - - hr - ISO-8859-2 - - - hu - ISO-8859-2 - - - is - ISO-8859-1 - - - it - ISO-8859-1 - - - iw - ISO-8859-8 - - - ja - Shift_JIS - - - ko - EUC-KR - - - lt - ISO-8859-2 - - - lv - ISO-8859-2 - - - mk - ISO-8859-5 - - - nl - ISO-8859-1 - - - no - ISO-8859-1 - - - pl - ISO-8859-2 - - - pt - ISO-8859-1 - - - ro - ISO-8859-2 - - - ru - ISO-8859-5 - - - sh - ISO-8859-5 - - - sk - ISO-8859-2 - - - sl - ISO-8859-2 - - - sq - ISO-8859-2 - - - sr - ISO-8859-5 - - - sv - ISO-8859-1 - - - tr - ISO-8859-9 - - - uk - ISO-8859-5 - - - zh - GB2312 - - - zh_TW - Big5 - - - - - - - - - Disable TRACE - / - TRACE - - - - - - Enable everything but TRACE - / - TRACE - - - - - diff --git a/codes/javaee/websocket/src/test/resources/logback.xml b/codes/javaee/websocket/src/test/resources/logback.xml deleted file mode 100644 index 1784e3bf..00000000 --- a/codes/javaee/websocket/src/test/resources/logback.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n - - - - - - ${user.dir}/logs/${DIR_NAME}/test.%d{yyyy-MM-dd}.log - 30 - - - - - 30MB - - - - %d{HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n - - - - - - - - - - - - - - - - diff --git a/codes/javatech-file/pom.xml b/codes/javatech-file/pom.xml new file mode 100644 index 00000000..dfedd3b0 --- /dev/null +++ b/codes/javatech-file/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-file + 1.0.0 + jar + Java 处理文件 + + + 4.0.0 + + + + + + org.apache.poi + poi + ${poi.version} + + + org.apache.poi + poi-ooxml + ${poi.version} + + + org.apache.poi + poi-scratchpad + ${poi.version} + + + + + io.github.biezhi + TinyPinyin + 2.0.3.RELEASE + + + + junit + junit + test + + + diff --git a/codes/javatech-file/src/main/java/io/github/dunwu/javatech/office/PinyinDemo.java b/codes/javatech-file/src/main/java/io/github/dunwu/javatech/office/PinyinDemo.java new file mode 100644 index 00000000..b3742bdd --- /dev/null +++ b/codes/javatech-file/src/main/java/io/github/dunwu/javatech/office/PinyinDemo.java @@ -0,0 +1,15 @@ +package io.github.dunwu.javatech.office; + +import com.github.promeg.pinyinhelper.Pinyin; + +/** + * @author Zhang Peng + * @since 2019-12-09 + */ +public class PinyinDemo { + + public static void main(String[] args) { + System.out.println("args = " + Pinyin.toPinyin('中')); + } + +} diff --git a/codes/javatech-file/src/main/java/io/github/dunwu/javatech/office/WordUtil.java b/codes/javatech-file/src/main/java/io/github/dunwu/javatech/office/WordUtil.java new file mode 100644 index 00000000..64cae3f1 --- /dev/null +++ b/codes/javatech-file/src/main/java/io/github/dunwu/javatech/office/WordUtil.java @@ -0,0 +1,263 @@ +package io.github.dunwu.javatech.office; + +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-file/src/test/java/io/github/dunwu/javatech/office/WordUtilTest.java b/codes/javatech-file/src/test/java/io/github/dunwu/javatech/office/WordUtilTest.java new file mode 100644 index 00000000..1566e002 --- /dev/null +++ b/codes/javatech-file/src/test/java/io/github/dunwu/javatech/office/WordUtilTest.java @@ -0,0 +1,111 @@ +package io.github.dunwu.javatech.office; + +import org.junit.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-io/javatech-bean/README.md b/codes/javatech-io/javatech-bean/README.md new file mode 100644 index 00000000..3b9709fa --- /dev/null +++ b/codes/javatech-io/javatech-bean/README.md @@ -0,0 +1,373 @@ +# Lombok 应用指南 + + + +- [1. Lombok 简介](#1-lombok-简介) +- [2. Lombok 安装](#2-lombok-安装) +- [3. Lombok 使用](#3-lombok-使用) + - [3.1. @Getter and @Setter](#31-getter-and-setter) + - [3.2. @NonNull](#32-nonnull) + - [3.3. @ToString](#33-tostring) + - [3.4. @EqualsAndHashCode](#34-equalsandhashcode) + - [3.5. @Data](#35-data) + - [3.6. @Cleanup](#36-cleanup) + - [3.7. @Synchronized](#37-synchronized) + - [3.8. @SneakyThrows](#38-sneakythrows) + - [3.9. 示例源码](#39-示例源码) +- [4. 参考资料](#4-参考资料) + + + +## 1. Lombok 简介 + +Lombok 是一种 Java 实用工具,可用来帮助开发人员消除 Java 的冗长,尤其是对于简单的 Java 对象(POJO)。它通过注释实现这一目的。通过在开发环境中实现 Lombok,开发人员可以节省构建诸如 `hashCode()` 和 `equals()` 、`getter / setter` 这样的方法以及以往用来分类各种 accessor 和 mutator 的大量时间。 + +## 2. Lombok 安装 + +使 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 + +``` + +## 3. Lombok 使用 + +Lombok 提供注解 API 来修饰指定的类: + +### 3.1. @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; +} +``` + +### 3.2. @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; +} +``` + +### 3.3. @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 + ")"; + } +} +``` + +### 3.4. @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; + } +} +``` + +### 3.5. @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 + ")"; + } +} +``` + +### 3.6. @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(); + } +} +``` + +### 3.7. @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); + } +} +``` + +### 3.8. @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); + } +} +``` + +### 3.9. 示例源码 + +> 示例源码:[javalib-bean](https://github.com/dunwu/java-tutorial/tree/master/javalib-bean) + +## 4. 参考资料 + +- [Lombok 官网](https://projectlombok.org/) +- [Lombok Github](https://github.com/rzwitserloot/lombok) +- [IntelliJ IDEA - Lombok Plugin](http://plugins.jetbrains.com/plugin/6317-lombok-plugin) diff --git a/codes/javatech-io/javatech-bean/pom.xml b/codes/javatech-io/javatech-bean/pom.xml new file mode 100644 index 00000000..d8006c2b --- /dev/null +++ b/codes/javatech-io/javatech-bean/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-bean + 1.0.0 + jar + Java IO Bean 操作示例 + + + + org.projectlombok + lombok + + + io.github.dunwu + dunwu-tool-core + + + com.fasterxml.jackson.core + jackson-databind + + + 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-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/BuilderDemo01.java b/codes/javatech-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/BuilderDemo01.java new file mode 100644 index 00000000..4ab3ecf2 --- /dev/null +++ b/codes/javatech-io/javatech-bean/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 peng.zhang + * @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-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/BuilderDemo02.java b/codes/javatech-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/BuilderDemo02.java new file mode 100644 index 00000000..eaa6ca05 --- /dev/null +++ b/codes/javatech-io/javatech-bean/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 peng.zhang + * @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-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/DataDemo.java b/codes/javatech-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/DataDemo.java new file mode 100644 index 00000000..a35811c0 --- /dev/null +++ b/codes/javatech-io/javatech-bean/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-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/EqualsAndHashCodeDemo.java b/codes/javatech-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/EqualsAndHashCodeDemo.java new file mode 100644 index 00000000..c342ab4d --- /dev/null +++ b/codes/javatech-io/javatech-bean/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-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/GetterAndSetterDemo.java b/codes/javatech-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/GetterAndSetterDemo.java new file mode 100644 index 00000000..e4365b6e --- /dev/null +++ b/codes/javatech-io/javatech-bean/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-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/NonNullDemo.java b/codes/javatech-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/NonNullDemo.java new file mode 100644 index 00000000..266e3ad8 --- /dev/null +++ b/codes/javatech-io/javatech-bean/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-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/Person.java b/codes/javatech-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/Person.java new file mode 100644 index 00000000..30f1519c --- /dev/null +++ b/codes/javatech-io/javatech-bean/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-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/ToStringDemo.java b/codes/javatech-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/ToStringDemo.java new file mode 100644 index 00000000..bd083e91 --- /dev/null +++ b/codes/javatech-io/javatech-bean/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-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/User.java b/codes/javatech-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/User.java new file mode 100644 index 00000000..093b010a --- /dev/null +++ b/codes/javatech-io/javatech-bean/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-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/package-info.java b/codes/javatech-io/javatech-bean/src/main/java/io/github/dunwu/javatech/bean/lombok/package-info.java new file mode 100644 index 00000000..c0f2a966 --- /dev/null +++ b/codes/javatech-io/javatech-bean/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-io/javatech-bean/src/test/java/io/github/dunwu/javatech/bean/BeanConvertTest.java b/codes/javatech-io/javatech-bean/src/test/java/io/github/dunwu/javatech/bean/BeanConvertTest.java new file mode 100644 index 00000000..e1bc3fd7 --- /dev/null +++ b/codes/javatech-io/javatech-bean/src/test/java/io/github/dunwu/javatech/bean/BeanConvertTest.java @@ -0,0 +1,103 @@ +package io.github.dunwu.javatech.bean; + +import cn.hutool.core.bean.BeanUtil; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * @author Zhang Peng + * @since 2020-05-15 + */ +public class BeanConvertTest { + + @Test + 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-io/javatech-bean/src/test/java/io/github/dunwu/javatech/bean/lombok/LombokTest.java b/codes/javatech-io/javatech-bean/src/test/java/io/github/dunwu/javatech/bean/lombok/LombokTest.java new file mode 100644 index 00000000..f0988e7f --- /dev/null +++ b/codes/javatech-io/javatech-bean/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-io/javatech-excel/pom.xml b/codes/javatech-io/javatech-excel/pom.xml new file mode 100644 index 00000000..bfe564ea --- /dev/null +++ b/codes/javatech-io/javatech-excel/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-excel + 1.0.0 + jar + Java IO EXCEL 示例 + + + + org.projectlombok + lombok + + + io.github.dunwu + dunwu-tool-core + + + com.alibaba + easyexcel + 2.2.6 + + + com.alibaba + fastjson + 1.2.66 + + + org.hibernate.validator + hibernate-validator + 6.0.12.Final + + + + + junit + junit + test + + + org.assertj + assertj-core + test + + + + diff --git a/codes/javatech-io/javatech-excel/src/test/java/io/github/dunwu/javatech/ExcelTest.java b/codes/javatech-io/javatech-excel/src/test/java/io/github/dunwu/javatech/ExcelTest.java new file mode 100644 index 00000000..cd6dd66b --- /dev/null +++ b/codes/javatech-io/javatech-excel/src/test/java/io/github/dunwu/javatech/ExcelTest.java @@ -0,0 +1,130 @@ +package io.github.dunwu.javatech; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.annotation.ExcelProperty; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import lombok.Data; +import lombok.experimental.Accessors; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.validation.constraints.NotBlank; + +/** + * @author Zhang Peng + * @since 2020-07-01 + */ +public class ExcelTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(ExcelTest.class); + + public static final String OUT_FILE = "d:\\temp-" + System.currentTimeMillis() + ".xlsx"; + public static final String IN_FILE = "d:\\属性列表模板.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; + @NotBlank + @ExcelProperty("衍生函数实例参数") + private String e; + @ExcelProperty("参数配置示例") + private String f; + + } + + @Test + public void syncRead() throws FileNotFoundException { + File file = new File(IN_FILE); + InputStream inputStream = new FileInputStream(file); + List eventAttrDefExcelDTOS = ExcelUtil.readSync(inputStream, BeanDemo.class); + System.out.println(eventAttrDefExcelDTOS); + } + + @Test + public void syncRead2() throws FileNotFoundException { + File file = new File("d:\\temp-1595405363178.xlsx"); + InputStream inputStream = new FileInputStream(file); + List list = ExcelUtil.readSync(inputStream, BeanDemo.class); + System.out.println(list); + } + + @Test + public void asyncReadByCustom() throws FileNotFoundException { + File file = new File("d:\\temp-1595405363178.xlsx"); + InputStream inputStream = new FileInputStream(file); + List list = ExcelUtil.readSync(inputStream, BeanDemo.class); + System.out.println(list); + } +} diff --git a/codes/javatech-io/javatech-excel/src/test/java/io/github/dunwu/javatech/ExcelUtil.java b/codes/javatech-io/javatech-excel/src/test/java/io/github/dunwu/javatech/ExcelUtil.java new file mode 100644 index 00000000..7d1450c6 --- /dev/null +++ b/codes/javatech-io/javatech-excel/src/test/java/io/github/dunwu/javatech/ExcelUtil.java @@ -0,0 +1,101 @@ +package io.github.dunwu.javatech; + +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-io/javatech-json/pom.xml b/codes/javatech-io/javatech-json/pom.xml new file mode 100644 index 00000000..38842ae4 --- /dev/null +++ b/codes/javatech-io/javatech-json/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-json + 1.0.0 + jar + Java IO JSON 示例 + + + + com.alibaba + fastjson + + + com.fasterxml.jackson.core + jackson-databind + + + com.google.code.gson + gson + + + + org.projectlombok + lombok + + + io.github.dunwu + dunwu-tool-core + + + + + junit + junit + test + + + org.assertj + assertj-core + test + + + + diff --git a/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/Group.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/Group.java new file mode 100644 index 00000000..aeb26674 --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/Group.java @@ -0,0 +1,42 @@ +package io.github.dunwu.javatech.json.bean; + +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) { + users.add(user); + } + +} diff --git a/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/Person.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/Person.java new file mode 100644 index 00000000..28a8bc01 --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/Person.java @@ -0,0 +1,42 @@ +package io.github.dunwu.javatech.json.bean; + +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-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/TestBean.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/TestBean.java new file mode 100644 index 00000000..b6dfd46a --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/TestBean.java @@ -0,0 +1,56 @@ +package io.github.dunwu.javatech.json.bean; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.Accessors; + +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 { + + 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-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/TestBean2.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/TestBean2.java new file mode 100644 index 00000000..85c221fc --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/TestBean2.java @@ -0,0 +1,50 @@ +package io.github.dunwu.javatech.json.bean; + +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-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/User.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/User.java new file mode 100644 index 00000000..4e595afd --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/bean/User.java @@ -0,0 +1,25 @@ +package io.github.dunwu.javatech.json.bean; + +public class User { + + private Long id; + + private String name; + + 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; + } + +} diff --git a/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/fastjson/FastjsonAnnotationBean.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/fastjson/FastjsonAnnotationBean.java new file mode 100644 index 00000000..ba398659 --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/fastjson/FastjsonAnnotationBean.java @@ -0,0 +1,174 @@ +package io.github.dunwu.javatech.json.fastjson; + +import com.alibaba.fastjson.annotation.JSONField; +import com.fasterxml.jackson.annotation.JsonIgnore; + +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 { + + 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-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/fastjson/FastjsonCaseTests.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/fastjson/FastjsonCaseTests.java new file mode 100644 index 00000000..110ffbab --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/fastjson/FastjsonCaseTests.java @@ -0,0 +1,60 @@ +package io.github.dunwu.javatech.json.fastjson; + +import com.alibaba.fastjson.JSON; +import io.github.dunwu.javatech.json.bean.Group; +import io.github.dunwu.javatech.json.util.BeanUtils; +import io.github.dunwu.tool.util.DateUtil; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; + +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 = BeanUtils.initGroupBean(); + 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-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/fastjson/FastjsonPerformanceTests.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/fastjson/FastjsonPerformanceTests.java new file mode 100644 index 00000000..cf7a5186 --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/fastjson/FastjsonPerformanceTests.java @@ -0,0 +1,50 @@ +package io.github.dunwu.javatech.json.fastjson; + +import com.alibaba.fastjson.JSON; +import io.github.dunwu.javatech.json.bean.TestBean2; +import io.github.dunwu.javatech.json.util.BeanUtils; +import org.junit.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-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/gson/GsonAnnotationBean.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/gson/GsonAnnotationBean.java new file mode 100644 index 00000000..f8b81504 --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/gson/GsonAnnotationBean.java @@ -0,0 +1,52 @@ +package io.github.dunwu.javatech.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-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/gson/GsonCaseTests.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/gson/GsonCaseTests.java new file mode 100644 index 00000000..ab6c1494 --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/gson/GsonCaseTests.java @@ -0,0 +1,80 @@ +package io.github.dunwu.javatech.json.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.junit.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-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/gson/GsonPerformanceTests.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/gson/GsonPerformanceTests.java new file mode 100644 index 00000000..8e5dc1e8 --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/gson/GsonPerformanceTests.java @@ -0,0 +1,53 @@ +package io.github.dunwu.javatech.json.gson; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.github.dunwu.javatech.json.bean.TestBean2; +import io.github.dunwu.javatech.json.util.BeanUtils; +import org.junit.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-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/gson/VersionedClass.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/gson/VersionedClass.java new file mode 100644 index 00000000..14e79f1d --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/gson/VersionedClass.java @@ -0,0 +1,21 @@ +package io.github.dunwu.javatech.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-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/jackson/JacksonAnnotationBean.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/jackson/JacksonAnnotationBean.java new file mode 100644 index 00000000..bf5f6f0f --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/jackson/JacksonAnnotationBean.java @@ -0,0 +1,55 @@ +package io.github.dunwu.javatech.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-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/jackson/JacksonPerformanceTests.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/jackson/JacksonPerformanceTests.java new file mode 100644 index 00000000..95d873dc --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/jackson/JacksonPerformanceTests.java @@ -0,0 +1,53 @@ +package io.github.dunwu.javatech.json.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.dunwu.javatech.json.bean.TestBean2; +import io.github.dunwu.javatech.json.util.BeanUtils; +import org.junit.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-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/jackson/JacksonTests.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/jackson/JacksonTests.java new file mode 100644 index 00000000..52534975 --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/jackson/JacksonTests.java @@ -0,0 +1,101 @@ +package io.github.dunwu.javatech.json.jackson; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.dunwu.javatech.json.bean.Person; +import org.junit.Assert; +import org.junit.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(); + } + Assert.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(); + } + Assert.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(); + } + + Assert.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(); + } + Assert.assertNotNull(json); + System.out.println("json = [" + json + "]"); + } + +} diff --git a/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/util/BeanUtils.java b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/util/BeanUtils.java new file mode 100644 index 00000000..4e2372ca --- /dev/null +++ b/codes/javatech-io/javatech-json/src/test/java/io/github/dunwu/javatech/json/util/BeanUtils.java @@ -0,0 +1,74 @@ +package io.github.dunwu.javatech.json.util; + +import io.github.dunwu.javatech.json.bean.Group; +import io.github.dunwu.javatech.json.bean.TestBean; +import io.github.dunwu.javatech.json.bean.TestBean2; +import io.github.dunwu.javatech.json.bean.User; + +import java.sql.Date; +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(); + Date date = 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(); + Date date = 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-io/javatech-serialize/pom.xml b/codes/javatech-io/javatech-serialize/pom.xml new file mode 100644 index 00000000..28c8bc01 --- /dev/null +++ b/codes/javatech-io/javatech-serialize/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-serialize + 1.0.0 + jar + Java IO 序列化示例 + + + + + + + + de.ruedigermoeller + fst + 2.56 + + + com.esotericsoftware + kryo + 5.0.0-RC4 + + + + io.github.dunwu + dunwu-tool-core + + + org.projectlombok + lombok + + + + + junit + junit + test + + + org.assertj + assertj-core + test + + + + diff --git a/codes/javatech-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/FstDemo.java b/codes/javatech-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/FstDemo.java new file mode 100644 index 00000000..0ea4cc52 --- /dev/null +++ b/codes/javatech-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/FstDemo.java @@ -0,0 +1,73 @@ +package io.github.dunwu.javatech.io; + +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-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/JdkSerializeDemo.java b/codes/javatech-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/JdkSerializeDemo.java new file mode 100644 index 00000000..7c86ae32 --- /dev/null +++ b/codes/javatech-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/JdkSerializeDemo.java @@ -0,0 +1,78 @@ +package io.github.dunwu.javatech.io; + +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-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/KryoDemo.java b/codes/javatech-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/KryoDemo.java new file mode 100644 index 00000000..bf333215 --- /dev/null +++ b/codes/javatech-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/KryoDemo.java @@ -0,0 +1,114 @@ +package io.github.dunwu.javatech.io; + +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-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/bean/BeanUtils.java b/codes/javatech-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/bean/BeanUtils.java new file mode 100644 index 00000000..9b00a817 --- /dev/null +++ b/codes/javatech-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/bean/BeanUtils.java @@ -0,0 +1,33 @@ +package io.github.dunwu.javatech.io.bean; + +import io.github.dunwu.tool.util.DateUtil; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +/** + * @author Zhang Peng + * @since 2019-11-22 + */ +public class BeanUtils { + + public static TestBean initJdk8Bean() { + String[] strArray = { "a", "b", "c" }; + Integer[] intArray = { 1, 2, 3, 4, 5 }; + List intList = new ArrayList<>(Arrays.asList(intArray)); + Map map = new HashMap<>(); + map.put("name", "jack"); + map.put("age", 18); + map.put("length", 175.3f); + TestBean bean = new TestBean(); + LocalDateTime localDateTime = LocalDateTime.of(2000, 1, 1, 12, 0, 0); + Date date = DateUtil.toDate(localDateTime); + 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; + } + +} diff --git a/codes/javatech-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/bean/TestBean.java b/codes/javatech-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/bean/TestBean.java new file mode 100644 index 00000000..805becbb --- /dev/null +++ b/codes/javatech-io/javatech-serialize/src/main/java/io/github/dunwu/javatech/io/bean/TestBean.java @@ -0,0 +1,59 @@ +package io.github.dunwu.javatech.io.bean; + +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 enum Color { + RED, + YELLOW, + BLUE + } + +} diff --git a/codes/javatech-io/javatech-serialize/src/test/java/io/github/dunwu/javatech/io/SerializePerformanceTest.java b/codes/javatech-io/javatech-serialize/src/test/java/io/github/dunwu/javatech/io/SerializePerformanceTest.java new file mode 100644 index 00000000..a37257c7 --- /dev/null +++ b/codes/javatech-io/javatech-serialize/src/test/java/io/github/dunwu/javatech/io/SerializePerformanceTest.java @@ -0,0 +1,63 @@ +package io.github.dunwu.javatech.io; + +import io.github.dunwu.javatech.io.bean.BeanUtils; +import io.github.dunwu.javatech.io.bean.TestBean; +import org.junit.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * 序列化、反序列化性能测试 + * + * @author Zhang Peng + * @since 2019-11-22 + */ +public class SerializePerformanceTest { + + 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-io/pom.xml b/codes/javatech-io/pom.xml new file mode 100644 index 00000000..b66c689a --- /dev/null +++ b/codes/javatech-io/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + io.github.dunwu.javatech + javatech-io + 1.0.0 + pom + Java IO + + + javatech-bean + javatech-json + javatech-serialize + javatech-excel + + diff --git a/codes/javatech-log/javatech-log4j/pom.xml b/codes/javatech-log/javatech-log4j/pom.xml new file mode 100644 index 00000000..32e98653 --- /dev/null +++ b/codes/javatech-log/javatech-log4j/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-log4j + 1.0.0 + jar + Java 日志 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-log/javatech-log4j/src/main/java/io/github/dunwu/javatech/log/Log4jDemo.java b/codes/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-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-log/javatech-log4j/src/main/resources/log4j.xml b/codes/javatech-log/javatech-log4j/src/main/resources/log4j.xml new file mode 100644 index 00000000..e86941cd --- /dev/null +++ b/codes/javatech-log/javatech-log4j/src/main/resources/log4j.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/codes/javatech-log/javatech-log4j2/pom.xml b/codes/javatech-log/javatech-log4j2/pom.xml new file mode 100644 index 00000000..3baacd4d --- /dev/null +++ b/codes/javatech-log/javatech-log4j2/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-log4j2 + 1.0.0 + jar + Java 日志 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-log/javatech-log4j2/src/main/java/io/github/dunwu/javatech/log/Log4j2Demo.java b/codes/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-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-log/javatech-log4j2/src/main/resources/log4j2.xml b/codes/javatech-log/javatech-log4j2/src/main/resources/log4j2.xml new file mode 100644 index 00000000..12650afa --- /dev/null +++ b/codes/javatech-log/javatech-log4j2/src/main/resources/log4j2.xml @@ -0,0 +1,45 @@ + + + + ???? + + + + + + + + + + + + + + + ${PATTERN} + + + + + + + + + + + + + + + + + + + + + + diff --git a/codes/javatech-log/javatech-logback/pom.xml b/codes/javatech-log/javatech-logback/pom.xml new file mode 100644 index 00000000..7b2076f0 --- /dev/null +++ b/codes/javatech-log/javatech-logback/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-logback + 1.0.0 + jar + Java 日志 Logback + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + + ch.qos.logback + logback-classic + + + + + + + + + junit + junit + test + + + + diff --git a/codes/javatech-log/javatech-logback/src/main/java/io/github/dunwu/javatech/log/LogbackDemo.java b/codes/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-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-log/javatech-logback/src/main/resources/logback.xml b/codes/javatech-log/javatech-logback/src/main/resources/logback.xml new file mode 100644 index 00000000..8d3ec5a8 --- /dev/null +++ b/codes/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-log/javatech-logstash/pom.xml b/codes/javatech-log/javatech-logstash/pom.xml new file mode 100644 index 00000000..0b3b7fc1 --- /dev/null +++ b/codes/javatech-log/javatech-logstash/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-logstash + 1.0.0 + jar + Java 日志 Logstash + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + log4j + log4j + 1.2.17 + + + ch.qos.logback + logback-classic + + + + diff --git a/codes/javatech-log/javatech-logstash/src/main/java/io/github/dunwu/javatech/ElasticDemo.java b/codes/javatech-log/javatech-logstash/src/main/java/io/github/dunwu/javatech/ElasticDemo.java new file mode 100644 index 00000000..256e3146 --- /dev/null +++ b/codes/javatech-log/javatech-logstash/src/main/java/io/github/dunwu/javatech/ElasticDemo.java @@ -0,0 +1,47 @@ +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 final org.apache.log4j.Logger log4jLog = org.apache.log4j.Logger.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); + } + }); + } + } + + private static void sendLog4jLog() { + for (int i = 0; i < 100; i++) { + log4jLog.info(String.format("这是第 %d 条日志", ++index)); + } + } + +} diff --git a/codes/javatech-log/javatech-logstash/src/main/resources/log4j.xml b/codes/javatech-log/javatech-logstash/src/main/resources/log4j.xml new file mode 100644 index 00000000..68949a9b --- /dev/null +++ b/codes/javatech-log/javatech-logstash/src/main/resources/log4j.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/codes/javatech-log/javatech-logstash/src/main/resources/logback.xml b/codes/javatech-log/javatech-logstash/src/main/resources/logback.xml new file mode 100644 index 00000000..c917bd86 --- /dev/null +++ b/codes/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-log/pom.xml b/codes/javatech-log/pom.xml new file mode 100644 index 00000000..08345937 --- /dev/null +++ b/codes/javatech-log/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + io.github.dunwu.javatech + javatech-log + 1.0.0 + pom + Java 日志 + + + javatech-log4j + javatech-log4j2 + javatech-logback + javatech-logstash + + diff --git a/codes/javatech-mq/javatech-kafka-springboot/pom.xml b/codes/javatech-mq/javatech-kafka-springboot/pom.xml new file mode 100644 index 00000000..e43a39df --- /dev/null +++ b/codes/javatech-mq/javatech-kafka-springboot/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-starter-parent + 0.5.6 + + + io.github.dunwu.javatech + javatech-kafka-springboot + 1.0.0 + jar + Java 消息队列 Kafka + Spring Boot + + + 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-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/KafkaConsumer.java b/codes/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-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-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/KafkaProducer.java b/codes/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-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-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/KafkaProducerController.java b/codes/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-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-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/MsgKafkaApplication.java b/codes/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-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-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/config/KafkaConsumerConfig.java b/codes/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/config/KafkaConsumerConfig.java new file mode 100644 index 00000000..31ae3462 --- /dev/null +++ b/codes/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/config/KafkaConsumerConfig.java @@ -0,0 +1,61 @@ +package io.github.dunwu.javatech.config; + +import java.util.HashMap; +import java.util.Map; +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; + +@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-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/config/KafkaProducerConfig.java b/codes/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/config/KafkaProducerConfig.java new file mode 100644 index 00000000..06ab586b --- /dev/null +++ b/codes/javatech-mq/javatech-kafka-springboot/src/main/java/io/github/dunwu/javatech/config/KafkaProducerConfig.java @@ -0,0 +1,60 @@ +package io.github.dunwu.javatech.config; + +import java.util.HashMap; +import java.util.Map; +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; + +@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-mq/javatech-kafka-springboot/src/main/resources/application.properties b/codes/javatech-mq/javatech-kafka-springboot/src/main/resources/application.properties new file mode 100644 index 00000000..e25cabf2 --- /dev/null +++ b/codes/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-mq/javatech-kafka-springboot/src/main/resources/logback.xml b/codes/javatech-mq/javatech-kafka-springboot/src/main/resources/logback.xml new file mode 100644 index 00000000..1946a29c --- /dev/null +++ b/codes/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-mq/javatech-kafka-springboot/src/test/java/io/github/dunwu/javatech/KafkaProducerTest.java b/codes/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-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-mq/javatech-kafka/pom.xml b/codes/javatech-mq/javatech-kafka/pom.xml new file mode 100644 index 00000000..22a51f46 --- /dev/null +++ b/codes/javatech-mq/javatech-kafka/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-kafka + 1.0.0 + jar + Java 消息队列 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-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerAOC.java b/codes/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerAOC.java new file mode 100644 index 00000000..52cffae0 --- /dev/null +++ b/codes/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerAOC.java @@ -0,0 +1,43 @@ +package io.github.dunwu.javatech.kafka; + +import java.util.Arrays; +import java.util.Properties; +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; + +/** + * 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-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerManual.java b/codes/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerManual.java new file mode 100644 index 00000000..43c7b091 --- /dev/null +++ b/codes/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerManual.java @@ -0,0 +1,47 @@ +package io.github.dunwu.javatech.kafka; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +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; + +/** + * @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-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerManualPartition.java b/codes/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerManualPartition.java new file mode 100644 index 00000000..1c1f8665 --- /dev/null +++ b/codes/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ConsumerManualPartition.java @@ -0,0 +1,48 @@ +package io.github.dunwu.javatech.kafka; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Properties; +import org.apache.kafka.clients.consumer.*; +import org.apache.kafka.common.TopicPartition; + +/** + * @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-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ProducerDemo.java b/codes/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ProducerDemo.java new file mode 100644 index 00000000..7c1d2434 --- /dev/null +++ b/codes/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ProducerDemo.java @@ -0,0 +1,48 @@ +package io.github.dunwu.javatech.kafka; + +import java.util.Properties; +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; + +/** + * 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-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ProducerInTransaction.java b/codes/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ProducerInTransaction.java new file mode 100644 index 00000000..cff270ca --- /dev/null +++ b/codes/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/ProducerInTransaction.java @@ -0,0 +1,142 @@ +package io.github.dunwu.javatech.kafka; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +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; + +/** + * @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-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/StreamDemo.java b/codes/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/StreamDemo.java new file mode 100644 index 00000000..2d9c701b --- /dev/null +++ b/codes/javatech-mq/javatech-kafka/src/main/java/io/github/dunwu/javatech/kafka/StreamDemo.java @@ -0,0 +1,45 @@ +package io.github.dunwu.javatech.kafka; + +import java.util.Arrays; +import java.util.Properties; +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; + +/** + * 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-mq/javatech-kafka/src/main/resources/logback.xml b/codes/javatech-mq/javatech-kafka/src/main/resources/logback.xml new file mode 100644 index 00000000..72b2ddb5 --- /dev/null +++ b/codes/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-mq/javatech-rocketmq/pom.xml b/codes/javatech-mq/javatech-rocketmq/pom.xml new file mode 100644 index 00000000..69a1e2d6 --- /dev/null +++ b/codes/javatech-mq/javatech-rocketmq/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-rocketmq + 1.0.0 + jar + Java 消息队列 Rocketmq + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + org.apache.rocketmq + rocketmq-client + 4.2.0 + + + ch.qos.logback + logback-classic + + + + diff --git a/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/AsyncProducer.java b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/AsyncProducer.java new file mode 100644 index 00000000..75ee2eb6 --- /dev/null +++ b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/AsyncProducer.java @@ -0,0 +1,45 @@ +package io.github.dunwu.javatech.rocketmq; + +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; + +/** + * 发送可靠的异步消息示例 异步传输通常用于响应时间敏感的业务场景。 + * + * @author Zhang Peng + */ +public class AsyncProducer { + + public static void main(String[] args) throws Exception { + // Instantiate with a producer group name. + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); + producer.setNamesrvAddr(RocketConfig.HOST); + // Launch the instance. + producer.start(); + producer.setRetryTimesWhenSendAsyncFailed(0); + for (int i = 0; i < 100; i++) { + final int index = i; + // Create a message instance, specifying topic, tag and message body. + Message msg = new Message("TopicTest", "TagA", "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + producer.send(msg, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId()); + } + + @Override + public void onException(Throwable e) { + System.out.printf("%-10d Exception %s %n", index, e); + e.printStackTrace(); + } + }); + } + // Shut down once the producer instance is not longer in use. + producer.shutdown(); + } + +} diff --git a/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/BatchProducer.java b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/BatchProducer.java new file mode 100644 index 00000000..5d0095d1 --- /dev/null +++ b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/BatchProducer.java @@ -0,0 +1,93 @@ +package io.github.dunwu.javatech.rocketmq; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.message.Message; + +/** + * 批量发送消息 + * + * @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(RocketConfig.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(); + } + + public static 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-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/BroadcastConsumer.java b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/BroadcastConsumer.java new file mode 100644 index 00000000..9a8c6acb --- /dev/null +++ b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/BroadcastConsumer.java @@ -0,0 +1,43 @@ +package io.github.dunwu.javatech.rocketmq; + +import java.util.List; +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; + +/** + * 接收广播消息示例 + * + * @author Zhang Peng + */ +public class BroadcastConsumer { + + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); + consumer.setNamesrvAddr(RocketConfig.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-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/BroadcastProducer.java b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/BroadcastProducer.java new file mode 100644 index 00000000..b35b37eb --- /dev/null +++ b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/BroadcastProducer.java @@ -0,0 +1,29 @@ +package io.github.dunwu.javatech.rocketmq; + +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(RocketConfig.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-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/OnewayProducer.java b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/OnewayProducer.java new file mode 100644 index 00000000..b231aa5e --- /dev/null +++ b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/OnewayProducer.java @@ -0,0 +1,32 @@ +package io.github.dunwu.javatech.rocketmq; + +import org.apache.rocketmq.client.producer.DefaultMQProducer; +import org.apache.rocketmq.common.message.Message; +import org.apache.rocketmq.remoting.common.RemotingHelper; + +/** + * 单向传输示例 单向传输用于需要中等可靠性的情况,例如日志收集。 + * + * @author Zhang Peng + */ +public class OnewayProducer { + + public static void main(String[] args) throws Exception { + // Instantiate with a producer group name. + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); + producer.setNamesrvAddr(RocketConfig.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); + } + // Shut down once the producer instance is not longer in use. + producer.shutdown(); + } + +} diff --git a/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/OrderedConsumer.java b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/OrderedConsumer.java new file mode 100644 index 00000000..f7abeca9 --- /dev/null +++ b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/OrderedConsumer.java @@ -0,0 +1,55 @@ +package io.github.dunwu.javatech.rocketmq; + +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +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; + +/** + * 接收全局和分区排序的消息。 + * + * @author Zhang Peng + */ +public class OrderedConsumer { + + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); + consumer.setNamesrvAddr(RocketConfig.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/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/OrderedProducer.java b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/OrderedProducer.java similarity index 82% rename from codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/OrderedProducer.java rename to codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/OrderedProducer.java index 8e2f36b2..e736448a 100644 --- a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/OrderedProducer.java +++ b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/OrderedProducer.java @@ -1,6 +1,5 @@ -package io.github.dunwu.javaweb.rocketmq; +package io.github.dunwu.javatech.rocketmq; -import java.util.List; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.MQProducer; import org.apache.rocketmq.client.producer.MessageQueueSelector; @@ -9,21 +8,25 @@ 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. + // Instantiate with a producer group name. MQProducer producer = new DefaultMQProducer("example_group_name"); ((DefaultMQProducer) producer).setNamesrvAddr(RocketConfig.HOST); - //Launch the instance. + // Launch the instance. producer.start(); - String[] tags = new String[]{"TagA", "TagB", "TagC", "TagD", "TagE"}; + 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. + // Create a message instance, specifying topic, tag and message body. Message msg = new Message("TopicTestjjj", tags[i % tags.length], "KEY" + i, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); SendResult sendResult = producer.send(msg, new MessageQueueSelector() { @@ -37,7 +40,8 @@ public MessageQueue select(List mqs, Message msg, Object arg) { System.out.printf("%s%n", sendResult); } - //server shutdown + // server shutdown producer.shutdown(); } + } diff --git a/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/RocketConfig.java b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/RocketConfig.java new file mode 100644 index 00000000..a9e00062 --- /dev/null +++ b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/RocketConfig.java @@ -0,0 +1,10 @@ +package io.github.dunwu.javatech.rocketmq; + +/** + * @author Zhang Peng + */ +public class RocketConfig { + + public static final String HOST = "192.168.28.32:9876"; + +} diff --git a/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/ScheduledMessageConsumer.java b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/ScheduledMessageConsumer.java new file mode 100644 index 00000000..e89183e4 --- /dev/null +++ b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/ScheduledMessageConsumer.java @@ -0,0 +1,39 @@ +package io.github.dunwu.javatech.rocketmq; + +import java.util.List; +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; + +/** + * 接收定时消息 + * + * @author Zhang Peng + */ +public class ScheduledMessageConsumer { + + public static void main(String[] args) throws Exception { + // Instantiate message consumer + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer"); + // 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-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/ScheduledMessageProducer.java b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/ScheduledMessageProducer.java new file mode 100644 index 00000000..9096412f --- /dev/null +++ b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/ScheduledMessageProducer.java @@ -0,0 +1,31 @@ +package io.github.dunwu.javatech.rocketmq; + +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"); + // 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-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/SyncProducer.java b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/SyncProducer.java new file mode 100644 index 00000000..6d60e61f --- /dev/null +++ b/codes/javatech-mq/javatech-rocketmq/src/main/java/io/github/dunwu/javatech/rocketmq/SyncProducer.java @@ -0,0 +1,34 @@ +package io.github.dunwu.javatech.rocketmq; + +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 SyncProducer { + + 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(RocketConfig.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-mq/javatech-rocketmq/src/main/resources/logback.xml b/codes/javatech-mq/javatech-rocketmq/src/main/resources/logback.xml new file mode 100644 index 00000000..3be789cb --- /dev/null +++ b/codes/javatech-mq/javatech-rocketmq/src/main/resources/logback.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + 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 + + + + + + + + + + + + diff --git a/codes/javatech-mq/pom.xml b/codes/javatech-mq/pom.xml new file mode 100644 index 00000000..27658b98 --- /dev/null +++ b/codes/javatech-mq/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + io.github.dunwu.javatech + javatech-mq + 1.0.0 + pom + Java 消息队列 + + + javatech-kafka + javatech-kafka-springboot + javatech-rocketmq + + diff --git a/codes/javatech-others/javatech-cli/pom.xml b/codes/javatech-others/javatech-cli/pom.xml new file mode 100644 index 00000000..2d6356ec --- /dev/null +++ b/codes/javatech-others/javatech-cli/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-cli + 1.0.0 + jar + Java 其他 - 命令行 + + + 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-others/javatech-cli/src/main/java/io/github/dunwu/javatech/AnsiSystem.java b/codes/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-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-others/javatech-cli/src/main/java/io/github/dunwu/javatech/CliDemo.java b/codes/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-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-others/javatech-cli/src/main/java/io/github/dunwu/javatech/CliUtil.java b/codes/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-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-others/javatech-cli/src/main/java/io/github/dunwu/javatech/SystemInfoUtil.java b/codes/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-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-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiBgColor.java b/codes/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-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-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiColor.java b/codes/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-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-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiElement.java b/codes/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-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-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/AnsiSgr.java b/codes/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-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-others/javatech-cli/src/main/java/io/github/dunwu/javatech/constant/Color.java b/codes/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-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-others/javatech-cli/src/test/java/io/github/dunwu/javatech/CliUtilTests.java b/codes/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-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-others/javatech-cli/src/test/java/io/github/dunwu/javatech/SystemInfoTest.java b/codes/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-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-others/javatech-ruleengine/README.md b/codes/javatech-others/javatech-ruleengine/README.md new file mode 100644 index 00000000..92bab0d2 --- /dev/null +++ b/codes/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-others/javatech-ruleengine/pom.xml b/codes/javatech-others/javatech-ruleengine/pom.xml new file mode 100644 index 00000000..c36e4907 --- /dev/null +++ b/codes/javatech-others/javatech-ruleengine/pom.xml @@ -0,0 +1,71 @@ + + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-ruleengine + 1.0.0 + jar + Java 其他 - 规则引擎 + + + 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} + + + + io.github.dunwu + dunwu-tool-core + + + 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-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/eazyrules/Launcher.java b/codes/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-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-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/eazyrules/WeatherRule.java b/codes/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-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-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/BasicRule.java b/codes/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-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-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/DefaultRuleEngine.java b/codes/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-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-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/MvelRule.java b/codes/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-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-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/MvelRuleSet.java b/codes/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-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-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/Rule.java b/codes/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-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-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleConstant.java b/codes/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-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-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleContext.java b/codes/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-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-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleEngine.java b/codes/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-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-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleEngineBuilder.java b/codes/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-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-others/javatech-ruleengine/src/main/java/io/github/dunwu/javatech/rule/mvel/RuleEngineParams.java b/codes/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-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-others/javatech-ruleengine/src/main/resources/logback.xml b/codes/javatech-others/javatech-ruleengine/src/main/resources/logback.xml new file mode 100644 index 00000000..8d3ec5a8 --- /dev/null +++ b/codes/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-others/javatech-ruleengine/src/main/resources/weather-rule.yml b/codes/javatech-others/javatech-ruleengine/src/main/resources/weather-rule.yml new file mode 100644 index 00000000..77dddbb5 --- /dev/null +++ b/codes/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-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/eazyrules/EazyRulesTest.java b/codes/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/eazyrules/EazyRulesTest.java new file mode 100644 index 00000000..ac257bf3 --- /dev/null +++ b/codes/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/eazyrules/EazyRulesTest.java @@ -0,0 +1,86 @@ +package io.github.dunwu.javatech.rule.eazyrules; + +import io.github.dunwu.javatech.rule.eazyrules.WeatherRule; +import io.github.dunwu.tool.io.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-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/mvel/ClassTests.java b/codes/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-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-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/mvel/SalaryRuleTest.java b/codes/javatech-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/mvel/SalaryRuleTest.java new file mode 100644 index 00000000..951ac359 --- /dev/null +++ b/codes/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 com.alibaba.fastjson.JSON; +import io.github.dunwu.tool.io.FileUtil; +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-others/javatech-ruleengine/src/test/java/io/github/dunwu/javatech/rule/mvel/demo.mvel b/codes/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-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-others/javatech-ruleengine/src/test/resources/SalaryRule.json b/codes/javatech-others/javatech-ruleengine/src/test/resources/SalaryRule.json new file mode 100644 index 00000000..6a43150e --- /dev/null +++ b/codes/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-others/javatech-zookeeper/pom.xml b/codes/javatech-others/javatech-zookeeper/pom.xml new file mode 100644 index 00000000..87c60b33 --- /dev/null +++ b/codes/javatech-others/javatech-zookeeper/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-zookeeper + 1.0.0 + jar + Java 其他 - Zookeeper + + + + org.apache.zookeeper + zookeeper + 3.5.6 + + + org.apache.curator + curator-recipes + 4.3.0 + + + io.github.dunwu + dunwu-tool-core + + + junit + junit + test + + + diff --git a/codes/javatech-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/Callback.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/DLockTemplate.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/DistributedLock.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/TimeoutHandler.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/ZkDLockTemplate.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/ZkReentrantLockCleanerTask.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/dlock/ZookeeperReentrantDistributedLock.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperConnection.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperCreate.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperDelete.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperExists.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperGetChildren.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperGetData.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/ZooKeeperSetData.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/example/package-info.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/sequence/DistributedSequence.java b/codes/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-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-others/javatech-zookeeper/src/main/java/io/github/dunwu/javatech/zk/sequence/ZkDistributedSequence.java b/codes/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-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-others/javatech-zookeeper/src/test/java/io/github/dunwu/javatech/zk/dlock/ZkReentrantLockTemplateTest.java b/codes/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-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-others/javatech-zookeeper/src/test/resources/logback.xml b/codes/javatech-others/javatech-zookeeper/src/test/resources/logback.xml new file mode 100644 index 00000000..e94627b4 --- /dev/null +++ b/codes/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-others/pom.xml b/codes/javatech-others/pom.xml new file mode 100644 index 00000000..d5aab917 --- /dev/null +++ b/codes/javatech-others/pom.xml @@ -0,0 +1,18 @@ + + + 4.0.0 + + io.github.dunwu.javatech + javatech-others + 1.0.0 + pom + Java 其他 + + + javatech-cli + javatech-ruleengine + javatech-zookeeper + + diff --git a/codes/javatech-server/README.md b/codes/javatech-server/README.md new file mode 100644 index 00000000..3f574661 --- /dev/null +++ b/codes/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-server/pom.xml b/codes/javatech-server/pom.xml new file mode 100644 index 00000000..6afe7704 --- /dev/null +++ b/codes/javatech-server/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-server + 1.0.0 + war + Java 服务器 + + + [8.5.40,) + + + + + + ch.qos.logback + logback-classic + + + org.logback-extensions + logback-ext-spring + 0.1.2 + + + 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 + + + + + diff --git a/codes/javatech-server/src/main/java/io/github/dunwu/javatech/controller/HelloController.java b/codes/javatech-server/src/main/java/io/github/dunwu/javatech/controller/HelloController.java new file mode 100644 index 00000000..9588d2b0 --- /dev/null +++ b/codes/javatech-server/src/main/java/io/github/dunwu/javatech/controller/HelloController.java @@ -0,0 +1,56 @@ +package io.github.dunwu.javatech.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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()); + + /** + *

+ * 在本例中,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("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-server/src/main/java/io/github/dunwu/javatech/controller/IndexController.java b/codes/javatech-server/src/main/java/io/github/dunwu/javatech/controller/IndexController.java new file mode 100644 index 00000000..65549bb7 --- /dev/null +++ b/codes/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-server/src/main/java/io/github/dunwu/javatech/server/SimpleTomcatServer.java b/codes/javatech-server/src/main/java/io/github/dunwu/javatech/server/SimpleTomcatServer.java new file mode 100644 index 00000000..5741fc81 --- /dev/null +++ b/codes/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-server/src/main/java/io/github/dunwu/javatech/server/TomcatServer.java b/codes/javatech-server/src/main/java/io/github/dunwu/javatech/server/TomcatServer.java new file mode 100644 index 00000000..a18bb192 --- /dev/null +++ b/codes/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-server/src/main/resources/logback.xml b/codes/javatech-server/src/main/resources/logback.xml new file mode 100644 index 00000000..9884e7be --- /dev/null +++ b/codes/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-server/src/main/resources/properties/application-develop.properties b/codes/javatech-server/src/main/resources/properties/application-develop.properties new file mode 100644 index 00000000..25f09a35 --- /dev/null +++ b/codes/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-server/src/main/resources/properties/application-test.properties b/codes/javatech-server/src/main/resources/properties/application-test.properties new file mode 100644 index 00000000..25f09a35 --- /dev/null +++ b/codes/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-server/src/main/resources/spring/spring-servlet.xml b/codes/javatech-server/src/main/resources/spring/spring-servlet.xml new file mode 100644 index 00000000..6c05fe85 --- /dev/null +++ b/codes/javatech-server/src/main/resources/spring/spring-servlet.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/codes/javatech-server/src/main/resources/tomcat/conf/server.xml b/codes/javatech-server/src/main/resources/tomcat/conf/server.xml new file mode 100644 index 00000000..77316698 --- /dev/null +++ b/codes/javatech-server/src/main/resources/tomcat/conf/server.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/codes/javatech-server/src/main/resources/tomcat/conf/web.xml b/codes/javatech-server/src/main/resources/tomcat/conf/web.xml new file mode 100644 index 00000000..7dc916a5 --- /dev/null +++ b/codes/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/javaee/filter/src/main/webapp/META-INF/MANIFEST.MF b/codes/javatech-server/src/main/webapp/META-INF/MANIFEST.MF similarity index 100% rename from codes/javaee/filter/src/main/webapp/META-INF/MANIFEST.MF rename to codes/javatech-server/src/main/webapp/META-INF/MANIFEST.MF diff --git a/codes/javatech-server/src/main/webapp/WEB-INF/web.xml b/codes/javatech-server/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..974a088b --- /dev/null +++ b/codes/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-server/src/main/webapp/views/jsp/hello.jsp b/codes/javatech-server/src/main/webapp/views/jsp/hello.jsp new file mode 100644 index 00000000..774fc949 --- /dev/null +++ b/codes/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-server/src/main/webapp/views/jsp/index.jsp b/codes/javatech-server/src/main/webapp/views/jsp/index.jsp new file mode 100644 index 00000000..763fee86 --- /dev/null +++ b/codes/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-test/pom.xml b/codes/javatech-test/pom.xml new file mode 100644 index 00000000..9830b8da --- /dev/null +++ b/codes/javatech-test/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.7 + + + io.github.dunwu.javatech + javatech-test + 1.0.0 + jar + Java 测试 + + + 1.22 + + + + + ch.qos.logback + logback-classic + + + + + org.junit.jupiter + junit-jupiter + test + + + + + junit + junit + test + + + + + org.assertj + assertj-core + test + + + org.mockito + mockito-core + test + + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + provided + + + + diff --git a/codes/javatech-test/src/main/java/io/github/dunwu/javatech/jmh/JMHSample_34_SafeLooping.java b/codes/javatech-test/src/main/java/io/github/dunwu/javatech/jmh/JMHSample_34_SafeLooping.java new file mode 100644 index 00000000..979b08be --- /dev/null +++ b/codes/javatech-test/src/main/java/io/github/dunwu/javatech/jmh/JMHSample_34_SafeLooping.java @@ -0,0 +1,173 @@ +package io.github.dunwu.javatech.jmh; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.CompilerControl; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +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-test/src/main/java/io/github/dunwu/javatech/jmh/JmhQuickStart.java b/codes/javatech-test/src/main/java/io/github/dunwu/javatech/jmh/JmhQuickStart.java new file mode 100644 index 00000000..c00183d7 --- /dev/null +++ b/codes/javatech-test/src/main/java/io/github/dunwu/javatech/jmh/JmhQuickStart.java @@ -0,0 +1,43 @@ +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-test/src/main/java/io/github/dunwu/javatech/jmh/StringBuilderBenchmark.java b/codes/javatech-test/src/main/java/io/github/dunwu/javatech/jmh/StringBuilderBenchmark.java new file mode 100644 index 00000000..b72f35a2 --- /dev/null +++ b/codes/javatech-test/src/main/java/io/github/dunwu/javatech/jmh/StringBuilderBenchmark.java @@ -0,0 +1,52 @@ +package io.github.dunwu.javatech.jmh; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Threads; +import org.openjdk.jmh.annotations.Warmup; +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-test/src/main/java/io/github/dunwu/javatech/lombok/Calculator.java b/codes/javatech-test/src/main/java/io/github/dunwu/javatech/lombok/Calculator.java new file mode 100644 index 00000000..26c99915 --- /dev/null +++ b/codes/javatech-test/src/main/java/io/github/dunwu/javatech/lombok/Calculator.java @@ -0,0 +1,9 @@ +package io.github.dunwu.javatech.bean.lombok; + +public class Calculator { + + public int add(int a, int b) { + return a + b; + } + +} diff --git a/codes/javatech-test/src/main/java/io/github/dunwu/javatech/lombok/Person.java b/codes/javatech-test/src/main/java/io/github/dunwu/javatech/lombok/Person.java new file mode 100644 index 00000000..6ae9aa14 --- /dev/null +++ b/codes/javatech-test/src/main/java/io/github/dunwu/javatech/lombok/Person.java @@ -0,0 +1,34 @@ +package io.github.dunwu.javatech.bean.lombok; + +/** + * @author Zhang Peng + * @since 2018-11-29 + */ +public class Person { + + private String firstName; + + private String lastName; + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + +} diff --git a/codes/javatech-test/src/main/resources/logback.xml b/codes/javatech-test/src/main/resources/logback.xml new file mode 100644 index 00000000..240ee4c6 --- /dev/null +++ b/codes/javatech-test/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-test/src/test/java/io/github/dunwu/javatech/junit4/JUnitTest.java b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit4/JUnitTest.java new file mode 100644 index 00000000..7f914913 --- /dev/null +++ b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit4/JUnitTest.java @@ -0,0 +1,71 @@ +package io.github.dunwu.javatech.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-test/src/test/java/io/github/dunwu/javatech/junit5/AssertionsTests.java b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/AssertionsTests.java new file mode 100644 index 00000000..3ad5d90a --- /dev/null +++ b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/AssertionsTests.java @@ -0,0 +1,151 @@ +package io.github.dunwu.javatech.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-test/src/test/java/io/github/dunwu/javatech/junit5/AssumptionsTests.java b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/AssumptionsTests.java new file mode 100644 index 00000000..023120c6 --- /dev/null +++ b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/AssumptionsTests.java @@ -0,0 +1,43 @@ +package io.github.dunwu.javatech.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-test/src/test/java/io/github/dunwu/javatech/junit5/DisplayNameTests.java b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/DisplayNameTests.java new file mode 100644 index 00000000..d6308357 --- /dev/null +++ b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/DisplayNameTests.java @@ -0,0 +1,32 @@ +package io.github.dunwu.javatech.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-test/src/test/java/io/github/dunwu/javatech/junit5/DynamicTests.java b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/DynamicTests.java new file mode 100644 index 00000000..ce447000 --- /dev/null +++ b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/DynamicTests.java @@ -0,0 +1,117 @@ +package io.github.dunwu.javatech.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, () -> assertTrue(n % 2 == 0))); + } + + @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-test/src/test/java/io/github/dunwu/javatech/junit5/NestedTests.java b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/NestedTests.java new file mode 100644 index 00000000..d8da2a48 --- /dev/null +++ b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/NestedTests.java @@ -0,0 +1,84 @@ +package io.github.dunwu.javatech.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-test/src/test/java/io/github/dunwu/javatech/junit5/ParameterizedTests.java b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/ParameterizedTests.java new file mode 100644 index 00000000..43c2c278 --- /dev/null +++ b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/ParameterizedTests.java @@ -0,0 +1,32 @@ +package io.github.dunwu.javatech.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-test/src/test/java/io/github/dunwu/javatech/junit5/RepeatedTests.java b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/RepeatedTests.java new file mode 100644 index 00000000..d83fc813 --- /dev/null +++ b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/RepeatedTests.java @@ -0,0 +1,46 @@ +package io.github.dunwu.javatech.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.println(String.format("About to execute repetition %d of %d for %s", // + 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-test/src/test/java/io/github/dunwu/javatech/junit5/StandardTests.java b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/StandardTests.java new file mode 100644 index 00000000..e342eea6 --- /dev/null +++ b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/junit5/StandardTests.java @@ -0,0 +1,52 @@ +package io.github.dunwu.javatech.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-test/src/test/java/io/github/dunwu/javatech/mockito/MockitoTest.java b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/mockito/MockitoTest.java new file mode 100644 index 00000000..6211d1a7 --- /dev/null +++ b/codes/javatech-test/src/test/java/io/github/dunwu/javatech/mockito/MockitoTest.java @@ -0,0 +1,225 @@ +package io.github.dunwu.javatech.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/javaweb/mq/kafka/pom.xml b/codes/javaweb/mq/kafka/pom.xml deleted file mode 100644 index 1fcbff96..00000000 --- a/codes/javaweb/mq/kafka/pom.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - 4.0.0 - io.github.dunwu - javaweb-mq-Kafka - 1.0.0 - jar - Javaweb :: MQ :: Kafka - - - UTF-8 - 1.8 - ${java.version} - ${java.version} - - - - - org.apache.kafka - kafka-clients - 1.1.0 - - - org.apache.kafka - kafka-streams - 1.1.0 - - - ch.qos.logback - logback-classic - 1.1.2 - - - - - ${project.artifactId} - - - diff --git a/codes/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ConsumerAOC.java b/codes/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ConsumerAOC.java deleted file mode 100644 index 9843aa14..00000000 --- a/codes/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ConsumerAOC.java +++ /dev/null @@ -1,42 +0,0 @@ -package io.github.dunwu.javaweb.kafka; - -import java.util.Arrays; -import java.util.Properties; -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; - -/** - * 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/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ConsumerManual.java b/codes/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ConsumerManual.java deleted file mode 100644 index 4310dad5..00000000 --- a/codes/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ConsumerManual.java +++ /dev/null @@ -1,45 +0,0 @@ -package io.github.dunwu.javaweb.kafka; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Properties; -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; - -/** - * @author Zhang Peng - * @date 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/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ConsumerManualPartition.java b/codes/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ConsumerManualPartition.java deleted file mode 100644 index c97890d9..00000000 --- a/codes/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ConsumerManualPartition.java +++ /dev/null @@ -1,56 +0,0 @@ -package io.github.dunwu.javaweb.kafka; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Properties; -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 org.apache.kafka.clients.consumer.OffsetAndMetadata; -import org.apache.kafka.common.TopicPartition; - -/** - * @author Zhang Peng - * @date 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/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ProducerDemo.java b/codes/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ProducerDemo.java deleted file mode 100644 index 133def52..00000000 --- a/codes/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ProducerDemo.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.github.dunwu.javaweb.kafka; - -import java.util.Properties; -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; - -/** - * 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/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ProducerInTransaction.java b/codes/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ProducerInTransaction.java deleted file mode 100644 index 54c31ba0..00000000 --- a/codes/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/ProducerInTransaction.java +++ /dev/null @@ -1,146 +0,0 @@ -package io.github.dunwu.javaweb.kafka; - -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.Properties; -import org.apache.kafka.clients.consumer.Consumer; -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 org.apache.kafka.clients.consumer.OffsetAndMetadata; -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; - -/** - * @author Zhang Peng - */ -public class ProducerInTransaction { - private static final String HOST = "192.168.28.32:9092"; - - 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; - } - - /** - * 在一个事务只有生产消息操作 - */ - 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 void main(String[] args) { - onlyProduceInTransaction(); - consumeTransferProduce(); - } -} diff --git a/codes/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/StreamDemo.java b/codes/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/StreamDemo.java deleted file mode 100644 index 0dc14e5b..00000000 --- a/codes/javaweb/mq/kafka/src/main/java/io/github/dunwu/javaweb/kafka/StreamDemo.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.github.dunwu.javaweb.kafka; - -import java.util.Arrays; -import java.util.Properties; -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; - -/** - * 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/javaweb/mq/kafka/src/main/resources/logback.xml b/codes/javaweb/mq/kafka/src/main/resources/logback.xml deleted file mode 100644 index 45fa970a..00000000 --- a/codes/javaweb/mq/kafka/src/main/resources/logback.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - 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/javaweb/mq/rocketmq/pom.xml b/codes/javaweb/mq/rocketmq/pom.xml deleted file mode 100644 index 81edd3ba..00000000 --- a/codes/javaweb/mq/rocketmq/pom.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - 4.0.0 - io.github.dunwu - javaweb-mq-rocketmq - 1.0.0 - jar - Javaweb :: MQ :: RocketMQ - - - UTF-8 - 1.8 - ${java.version} - ${java.version} - - - - - org.apache.rocketmq - rocketmq-client - 4.2.0 - - - ch.qos.logback - logback-classic - 1.1.2 - - - - - ${project.artifactId} - - - diff --git a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/AsyncProducer.java b/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/AsyncProducer.java deleted file mode 100644 index d2324131..00000000 --- a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/AsyncProducer.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.github.dunwu.javaweb.rocketmq; - -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; - -/** - * 发送可靠的异步消息示例 - * 异步传输通常用于响应时间敏感的业务场景。 - * @author Zhang Peng - */ -public class AsyncProducer { - public static void main(String[] args) throws Exception { - //Instantiate with a producer group name. - DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); - producer.setNamesrvAddr(RocketConfig.HOST); - //Launch the instance. - producer.start(); - producer.setRetryTimesWhenSendAsyncFailed(0); - for (int i = 0; i < 100; i++) { - final int index = i; - //Create a message instance, specifying topic, tag and message body. - Message msg = new Message("TopicTest", - "TagA", - "OrderID188", - "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); - producer.send(msg, new SendCallback() { - @Override - public void onSuccess(SendResult sendResult) { - System.out.printf("%-10d OK %s %n", index, - sendResult.getMsgId()); - } - - @Override - public void onException(Throwable e) { - System.out.printf("%-10d Exception %s %n", index, e); - e.printStackTrace(); - } - }); - } - //Shut down once the producer instance is not longer in use. - producer.shutdown(); - } -} diff --git a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/BatchProducer.java b/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/BatchProducer.java deleted file mode 100644 index cc16d015..00000000 --- a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/BatchProducer.java +++ /dev/null @@ -1,86 +0,0 @@ -package io.github.dunwu.javaweb.rocketmq; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import org.apache.rocketmq.client.producer.DefaultMQProducer; -import org.apache.rocketmq.common.message.Message; - -/** - * 批量发送消息 - * @author Zhang Peng - */ -public class BatchProducer { - public static 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; - } - } - - public static void main(String[] args) throws Exception { - //Instantiate with a producer group name. - DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); - producer.setNamesrvAddr(RocketConfig.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/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/BroadcastConsumer.java b/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/BroadcastConsumer.java deleted file mode 100644 index 2370a3f8..00000000 --- a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/BroadcastConsumer.java +++ /dev/null @@ -1,41 +0,0 @@ -package io.github.dunwu.javaweb.rocketmq; - -import java.util.List; -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; - -/** - * 接收广播消息示例 - * @author Zhang Peng - */ -public class BroadcastConsumer { - public static void main(String[] args) throws Exception { - DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); - consumer.setNamesrvAddr(RocketConfig.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/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/BroadcastProducer.java b/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/BroadcastProducer.java deleted file mode 100644 index 7f2b70ac..00000000 --- a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/BroadcastProducer.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.dunwu.javaweb.rocketmq; - -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(RocketConfig.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/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/OnewayProducer.java b/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/OnewayProducer.java deleted file mode 100644 index a5293466..00000000 --- a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/OnewayProducer.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.github.dunwu.javaweb.rocketmq; - -import org.apache.rocketmq.client.producer.DefaultMQProducer; -import org.apache.rocketmq.common.message.Message; -import org.apache.rocketmq.remoting.common.RemotingHelper; - -/** - * 单向传输示例 - * 单向传输用于需要中等可靠性的情况,例如日志收集。 - * @author Zhang Peng - */ -public class OnewayProducer { - public static void main(String[] args) throws Exception { - //Instantiate with a producer group name. - DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); - producer.setNamesrvAddr(RocketConfig.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); - - } - //Shut down once the producer instance is not longer in use. - producer.shutdown(); - } -} diff --git a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/OrderedConsumer.java b/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/OrderedConsumer.java deleted file mode 100644 index 24938e73..00000000 --- a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/OrderedConsumer.java +++ /dev/null @@ -1,54 +0,0 @@ -package io.github.dunwu.javaweb.rocketmq; - -import java.util.List; -import java.util.concurrent.atomic.AtomicLong; -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; - -/** - * 接收全局和分区排序的消息。 - * @author Zhang Peng - */ -public class OrderedConsumer { - public static void main(String[] args) throws Exception { - DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); - consumer.setNamesrvAddr(RocketConfig.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/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/RocketConfig.java b/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/RocketConfig.java deleted file mode 100644 index ab58601b..00000000 --- a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/RocketConfig.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.github.dunwu.javaweb.rocketmq; - -/** - * @author Zhang Peng - */ -public class RocketConfig { - public static final String HOST = "192.168.28.32:9876"; -} diff --git a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/ScheduledMessageConsumer.java b/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/ScheduledMessageConsumer.java deleted file mode 100644 index 00f3bc45..00000000 --- a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/ScheduledMessageConsumer.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.github.dunwu.javaweb.rocketmq; - -import java.util.List; -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; - -/** - * 接收定时消息 - * @author Zhang Peng - */ -public class ScheduledMessageConsumer { - public static void main(String[] args) throws Exception { - // Instantiate message consumer - DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("ExampleConsumer"); - // 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/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/ScheduledMessageProducer.java b/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/ScheduledMessageProducer.java deleted file mode 100644 index a8426342..00000000 --- a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/ScheduledMessageProducer.java +++ /dev/null @@ -1,28 +0,0 @@ -package io.github.dunwu.javaweb.rocketmq; - -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"); - // 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/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/SyncProducer.java b/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/SyncProducer.java deleted file mode 100644 index daf18f20..00000000 --- a/codes/javaweb/mq/rocketmq/src/main/java/io/github/dunwu/javaweb/rocketmq/SyncProducer.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.github.dunwu.javaweb.rocketmq; - -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 SyncProducer { - 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(RocketConfig.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/javaweb/mq/rocketmq/src/main/resources/logback.xml b/codes/javaweb/mq/rocketmq/src/main/resources/logback.xml deleted file mode 100644 index 28bbd739..00000000 --- a/codes/javaweb/mq/rocketmq/src/main/resources/logback.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - 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 - - - - - - - - - - - - diff --git a/codes/javaweb/rpc/zookeeper/pom.xml b/codes/javaweb/rpc/zookeeper/pom.xml deleted file mode 100644 index 525dfdbd..00000000 --- a/codes/javaweb/rpc/zookeeper/pom.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - 4.0.0 - io.github.dunwu - javaweb-rpc-zookeeper - 1.0.0 - jar - Javaweb :: Rpc :: Zookeeper - - UTF-8 - 1.8 - ${java.version} - ${java.version} - - - - org.apache.zookeeper - zookeeper - 3.4.12 - - - - - ${project.artifactId} - - diff --git a/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKConnection.java b/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKConnection.java deleted file mode 100644 index 26a2bae0..00000000 --- a/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKConnection.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.github.dunwu.javaweb.zookeeper; - -import java.io.IOException; -import java.util.concurrent.CountDownLatch; -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; - -/** - * ZooKeeper 连接示例 - */ -public class ZKConnection { - - // declare zookeeper instance to access ZooKeeper ensemble - private ZooKeeper zoo; - private static final String HOST = "localhost"; - final CountDownLatch connectedSignal = new CountDownLatch(1); - - // Method to connect zookeeper ensemble. - public ZooKeeper connect(String host) throws IOException, InterruptedException { - - zoo = new ZooKeeper(host, 5000, new Watcher() { - 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(); - } - - public static void main(String[] args) throws IOException, InterruptedException { - ZKConnection zkConnection = new ZKConnection(); - ZooKeeper zk = zkConnection.connect(HOST); - States state = zk.getState(); - System.out.println("ZooKeeper isAlive:" + state.isAlive()); - zk.close(); - } -} diff --git a/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKCreate.java b/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKCreate.java deleted file mode 100644 index 55a74d77..00000000 --- a/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKCreate.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.github.dunwu.javaweb.zookeeper; - -import org.apache.zookeeper.CreateMode; -import org.apache.zookeeper.KeeperException; -import org.apache.zookeeper.ZooDefs; -import org.apache.zookeeper.ZooKeeper; - -/** - * ZooKeeper 添加 Znode 示例 - */ -public class ZKCreate { - // create static instance for zookeeper class. - private static ZooKeeper zk; - - // create static instance for ZooKeeperConnection class. - private static ZKConnection conn; - - private static final String HOST = "localhost"; - - // 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); - } - - 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 ZKConnection(); - 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(); - } - } - } -} diff --git a/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKDelete.java b/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKDelete.java deleted file mode 100644 index cce6e62e..00000000 --- a/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKDelete.java +++ /dev/null @@ -1,31 +0,0 @@ -package io.github.dunwu.javaweb.zookeeper; - -import org.apache.zookeeper.KeeperException; -import org.apache.zookeeper.ZooKeeper; - -public class ZKDelete { - private static ZooKeeper zk; - private static ZKConnection conn; - private static final String HOST = "localhost"; - - // 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()); - } - - public static void main(String[] args) throws InterruptedException, KeeperException { - String path = "/MyFirstZnode"; //Assign path to the znode - - try { - conn = new ZKConnection(); - 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(); - } - } - } -} diff --git a/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKExists.java b/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKExists.java deleted file mode 100644 index d0afba0b..00000000 --- a/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKExists.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.github.dunwu.javaweb.zookeeper; - -import org.apache.zookeeper.KeeperException; -import org.apache.zookeeper.ZooKeeper; -import org.apache.zookeeper.data.Stat; - -public class ZKExists { - private static ZooKeeper zk; - private static ZKConnection conn; - private static final String HOST = "localhost"; - - // 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); - } - - public static void main(String[] args) throws InterruptedException, KeeperException { - String path = "/MyFirstZnode"; // Assign znode to the specified path - - try { - conn = new ZKConnection(); - zk = conn.connect(HOST); - Stat stat = znode_exists(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 - } - } -} diff --git a/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKGetChildren.java b/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKGetChildren.java deleted file mode 100644 index 34f57393..00000000 --- a/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKGetChildren.java +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.dunwu.javaweb.zookeeper; - -import java.util.List; -import org.apache.zookeeper.KeeperException; -import org.apache.zookeeper.ZooKeeper; -import org.apache.zookeeper.data.Stat; - -public class ZKGetChildren { - private static ZooKeeper zk; - private static ZKConnection conn; - private static final String HOST = "localhost"; - - // 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); - } - - public static void main(String[] args) throws InterruptedException, KeeperException { - String path = "/MyFirstZnode"; // Assign path to the znode - - try { - conn = new ZKConnection(); - 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(); - } - } - } -} diff --git a/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKGetData.java b/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKGetData.java deleted file mode 100644 index cee2780a..00000000 --- a/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKGetData.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.github.dunwu.javaweb.zookeeper; - -import java.util.concurrent.CountDownLatch; -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; - -public class ZKGetData { - - private static ZooKeeper zk; - private static ZKConnection conn; - private static final String HOST = "localhost"; - - public static Stat existsZnode(String path) throws - KeeperException, InterruptedException { - return zk.exists(path, true); - } - - public static void main(String[] args) throws InterruptedException { - String path = "/MyFirstZnode"; - final CountDownLatch connectedSignal = new CountDownLatch(1); - - try { - conn = new ZKConnection(); - 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(); - } - } - } -} diff --git a/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKSetData.java b/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKSetData.java deleted file mode 100644 index d9c81aff..00000000 --- a/codes/javaweb/rpc/zookeeper/src/main/java/io/github/dunwu/javaweb/zookeeper/ZKSetData.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.github.dunwu.javaweb.zookeeper; - -import org.apache.zookeeper.KeeperException; -import org.apache.zookeeper.ZooKeeper; - -public class ZKSetData { - private static ZooKeeper zk; - private static ZKConnection conn; - - private static final String HOST = "localhost"; - - // 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()); - } - - 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 ZKConnection(); - 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(); - } - } - } -} diff --git a/docs/.markdownlint.json b/docs/.markdownlint.json new file mode 100644 index 00000000..1ab9a8fa --- /dev/null +++ b/docs/.markdownlint.json @@ -0,0 +1,18 @@ +{ + "default": true, + "MD002": false, + "MD004": { "style": "dash" }, + "ul-indent": { "indent": 2 }, + "MD013": { "line_length": 600 }, + "MD024": false, + "MD025": false, + "MD026": false, + "MD029": { "style": "ordered" }, + "MD033": false, + "MD034": false, + "MD036": false, + "fenced-code-language": false, + "no-hard-tabs": false, + "whitespace": false, + "emphasis-style": { "style": "consistent" } +} diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js new file mode 100644 index 00000000..e31bc8c7 --- /dev/null +++ b/docs/.vuepress/config.js @@ -0,0 +1,158 @@ +/** + * @see https://vuepress.vuejs.org/zh/ + */ +module.exports = { + port: '4000', + dest: 'dist', + base: '/javatech/', + title: 'JAVATECH', + description: 'Java 技术生态', + head: [['link', { rel: 'icon', href: `/favicon.ico` }]], + markdown: { + externalLinks: { + target: '_blank', + rel: 'noopener noreferrer', + }, + }, + themeConfig: { + logo: 'https://raw.githubusercontent.com/dunwu/images/dev/common/dunwu-logo-200.png', + repo: 'dunwu/javatech', + repoLabel: 'Github', + docsDir: 'docs', + docsBranch: 'master', + editLinks: true, + smoothScroll: true, + locales: { + '/': { + label: '简体中文', + selectText: 'Languages', + editLinkText: '帮助我们改善此页面!', + lastUpdated: '上次更新', + nav: [ + { + text: '框架', + link: '/framework/', + }, + { + text: '缓存', + link: '/cache/', + }, + { + text: '消息队列', + link: '/mq/', + }, + { + text: 'LIB库', + link: '/lib/', + }, + { + text: '微服务', + link: '/microservice/', + }, + { + text: '安全', + link: '/security/', + }, + { + text: '测试', + link: '/test/', + }, + { + text: '服务器', + link: '/server/', + }, + { + 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, + }, + }, + }, + plugins: [ + [ + '@vuepress/active-header-links', + { + sidebarLinkSelector: '.sidebar-link', + headerAnchorSelector: '.header-anchor', + }, + ], + ['@vuepress/back-to-top', true], + [ + '@vuepress/pwa', + { + serviceWorker: true, + updatePopup: true, + }, + ], + [ + '@vuepress/last-updated', + { + transformer: (timestamp, lang) => { + // 不要忘了安装 moment + const moment = require('moment') + moment.locale(lang) + return moment(timestamp).fromNow() + }, + }, + ], + ['@vuepress/medium-zoom', true], + [ + 'container', + { + type: 'vue', + before: '
',
+        after: '
', + }, + ], + [ + 'container', + { + type: 'upgrade', + before: (info) => ``, + after: '', + }, + ], + ['flowchart'], + ], +} diff --git a/docs/.vuepress/enhanceApp.js b/docs/.vuepress/enhanceApp.js new file mode 100644 index 00000000..7b3605fc --- /dev/null +++ b/docs/.vuepress/enhanceApp.js @@ -0,0 +1,7 @@ +export default ({ Vue, isServer }) => { + if (!isServer) { + import('vue-toasted' /* webpackChunkName: "notification" */).then(module => { + Vue.use(module.default) + }) + } +} diff --git a/docs/.vuepress/public/favicon.ico b/docs/.vuepress/public/favicon.ico new file mode 100644 index 00000000..51e9bfa0 Binary files /dev/null and b/docs/.vuepress/public/favicon.ico differ diff --git a/docs/.vuepress/public/images/dunwu-logo-100.png b/docs/.vuepress/public/images/dunwu-logo-100.png new file mode 100644 index 00000000..12d81778 Binary files /dev/null and b/docs/.vuepress/public/images/dunwu-logo-100.png differ diff --git a/docs/.vuepress/public/images/dunwu-logo-200.png b/docs/.vuepress/public/images/dunwu-logo-200.png new file mode 100644 index 00000000..ea0a019c Binary files /dev/null and b/docs/.vuepress/public/images/dunwu-logo-200.png differ diff --git a/docs/.vuepress/public/images/dunwu-logo-50.png b/docs/.vuepress/public/images/dunwu-logo-50.png new file mode 100644 index 00000000..90a19762 Binary files /dev/null and b/docs/.vuepress/public/images/dunwu-logo-50.png differ diff --git a/docs/.vuepress/public/images/dunwu-logo.png b/docs/.vuepress/public/images/dunwu-logo.png new file mode 100644 index 00000000..61570e2a Binary files /dev/null and b/docs/.vuepress/public/images/dunwu-logo.png differ diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..f7ad577b --- /dev/null +++ b/docs/README.md @@ -0,0 +1,168 @@ +--- +home: true +heroImage: https://raw.githubusercontent.com/dunwu/images/dev/common/dunwu-logo-200.png +heroText: JAVATECH +tagline: ☕ javatech 汇总了 Java 开发中常见的主流技术的应用、特性、原理。 +actionLink: / +footer: CC-BY-SA-4.0 Licensed | Copyright © 2018-Now Dunwu +--- + +![license](https://badgen.net/github/license/dunwu/javatech) +![build](https://travis-ci.com/dunwu/javatech.svg?branch=master) + +> ☕ **JavaTech** 汇总了 Java 开发中常见的主流技术的应用、特性、原理。 +> +> - 🔁 项目同步维护:[Github](https://github.com/dunwu/javatech/) | [Gitee](https://gitee.com/turnon/javatech/) +> - 📖 电子书阅读:[Github Pages](https://dunwu.github.io/javatech/) | [Gitee Pages](http://turnon.gitee.io/javatech/) +> +> 说明: +> +> - 下面的内容清单中,凡是有 📚 标记的技术,都已整理成详细的教程。 +> - 部分技术因为可以应用于不同领域,所以可能会同时出现在不同的类别下。 + +## 📖 内容 + +### [框架](framework) + +- [Spring](https://dunwu.github.io/spring-tutorial/) 📚 +- [Spring Boot](https://dunwu.github.io/spring-boot-tutorial/) 📚 +- [Spring Cloud](https://github.com/dunwu/spring-cloud-tutorial) 📚 +- [MyBatis](framework/mybatis) + - [Mybatis 应用指南](framework/mybatis/Mybatis应用指南.md) + - [Mybatis 原理](framework/mybatis/Mybatis原理.md) +- [Netty](framework/netty.md) + +### [消息队列](mq) + +> 消息队列(Message Queue,简称 MQ)技术是分布式应用间交换信息的一种技术。 +> +> 消息队列主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 +> +> 如果想深入学习各种消息队列产品,建议先了解一下 [消息队列基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/mq.md) ,有助于理解消息队列特性的实现和设计思路。 + +- [消息队列基本原理](mq/消息队列基本原理.md) +- [消息队列面试题](mq/消息队列面试.md) 💯 +- [Kafka](https://dunwu.github.io/bigdata-tutorial/kafka) 📚 +- [RocketMQ](mq/rocketmq.md) +- [ActiveMQ](mq/activemq.md) + +### [缓存](cache) + +> 缓存可以说是优化系统性能的第一手段,在各种技术中都会有缓存的应用。 +> +> 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/cache.md),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200710163555.png) + +- [缓存面试题](cache/cache-interview.md) 💯 +- [缓存基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/cache.md) +- [Java 缓存框架](cache/cache-framework.md) - 关键词:Spring Cache、J2Cache、JetCache +- [Redis 教程](https://dunwu.github.io/db-tutorial/nosql/redis/) 📚 +- [Memcached 应用指南](cache/memcached.md) +- [Java 缓存库](cache/cache-libs.md) - 关键词:ConcurrentHashMap、LRUHashMap、Guava Cache、Caffeine、Ehcache +- [Ehcache 应用指南](cache/ehcache.md) +- [Http 缓存](cache/http-cache.md) + +### [微服务](microservice) + +- [Dubbo](microservice/dubbo.md) +- [**Spring Cloud**](https://github.com/dunwu/spring-cloud-tutorial) 📚 + - Eureka + - Consul + - Nacos + - Zuul + - Gateway +- 通信 + - [Netty](framework/netty.md) + +### 搜索引擎 + +- [ElasticSearch](search/elasticsearch) + - [ElasticSearch 应用指南](search/elasticsearch/elasticsearch-quickstart.md) + - [ElasticSearch API](search/elasticsearch/elasticsearch-api.md) + - [ElasticSearch 运维](search/elasticsearch/elasticsearch-ops.md) +- [Elastic 技术栈](search) + - [Elastic 技术栈快速入门](search/elastic-quickstart.md) + - [Beats 入门指南](search/elastic-beats.md) + - [Beats 运维](search/elastic-beats-ops.md) + - [Kibana 入门指南](search/elastic-kibana.md) + - [Kibana 运维](search/elastic-kibana-ops.md) + - [Logstash 入门指南](search/elastic-logstash.md) + - [Logstash 运维](search/elastic-logstash-ops.md) +- Solr +- Lucene + +### [安全](security) + +> Java 领域比较流行的安全框架就是 shiro 和 spring-security。 +> +> shiro 更为简单、轻便,容易理解,能满足大多数基本安全场景下的需要。 +> +> spring-security 功能更丰富,也比 shiro 更复杂。值得一提的是由于 spring-security 是 spring 团队开发,所以集成 spring 和 spring-boot 框架更容易。 + +- [Shiro](security/shiro.md) +- [Spring Security](security/spring-security.md) + +### [测试](test) + +- [Junit](test/junit.md) +- [Mockito](test/mockito.md) +- [JMH](test/jmh.md) +- [Jmeter](test/jmeter.md) + +### [服务器](server) + +> Tomcat 和 Jetty 都是 Java 比较流行的轻量级服务器。 +> +> Nginx 是目前最流行的反向代理服务器,也常用于负载均衡。 + +- [Tomcat 应用指南](server/Tomcat应用指南.md) +- [Tomcat 连接器](server/Tomcat连接器.md) +- [Tomcat 容器](server/Tomcat容器.md) +- [Tomcat 优化](server/Tomcat优化.md) +- [Jetty](server/jetty.md) +- [Nginx](https://github.com/dunwu/nginx-tutorial) 📚 + +### [大数据](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) + +### [LIB](lib) + +- [日志](lib/javalib-log.md) - log4j2、logback、log4j、Slf4j +- [序列化](lib/serialized/) + - [JSON](lib/serialized/javalib-json.md) - Fastjson、Jackson、Gson + - [二进制](lib/serialized/javalib-binary.md) - Protobuf、Thrift、Hessian、Kryo、FST +- [模板引擎](lib/template) - [Freemark](lib/template/freemark.md)、[Velocity](lib/template/velocity.md)、[Thymeleaf](lib/template/thymeleaf.md) +- JavaBean - [Lombok](lib/bean/lombok.md)、[Dozer](lib/bean/dozer.md) +- 工具包 - Apache Common、Guava、Hutool +- 辅助 - swagger + +## 📚 资料 + +## 🚪 传送 + +◾ 🏠 [JAVATECH 首页](https://github.com/dunwu/javatech) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ + +> 你可能会感兴趣: + +- [Java 教程](https://github.com/dunwu/java-tutorial) 📚 +- [JavaCore 教程](https://dunwu.github.io/javacore/) 📚 +- [JavaTech 教程](https://dunwu.github.io/javatech/) 📚 +- [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/docs/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204.md" "b/docs/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204.md" deleted file mode 100644 index 53cc6454..00000000 --- "a/docs/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204.md" +++ /dev/null @@ -1,145 +0,0 @@ ---- -title: 分布式架构 -date: 2018/06/12 -categories: -- 分布式 -tags: -- 分布式 ---- - -# 分布式架构 - - - -- [分布式架构的演进](#分布式架构的演进) - - [初始阶段架构](#初始阶段架构) - - [应用服务和数据服务分离](#应用服务和数据服务分离) - - [使用缓存改善性能](#使用缓存改善性能) - - [使用应用服务器集群](#使用应用服务器集群) - - [数据库读写分离](#数据库读写分离) - - [反向代理和 CDN 加速](#反向代理和-cdn-加速) - - [分布式文件系统和分布式数据库](#分布式文件系统和分布式数据库) - - [使用 NoSQL 和搜索引擎](#使用-nosql-和搜索引擎) - - [业务拆分](#业务拆分) - - [分布式服务](#分布式服务) -- [分布式架构的问题](#分布式架构的问题) -- [分布式架构的关键技术](#分布式架构的关键技术) - - [消息队列](#消息队列) - - [服务化](#服务化) - - [服务总线](#服务总线) -- [分布式架构的通信模式](#分布式架构的通信模式) - - [request/response 模式(同步模式)](#requestresponse-模式同步模式) - - [Callback(异步模式)](#callback异步模式) - - [Future 模式](#future-模式) - - [Oneway 模式](#oneway-模式) - - [Reliable 模式](#reliable-模式) -- [资料](#资料) - - - -## 分布式架构的演进 - - - -## 分布式架构的问题 - -- 当服务越来越多时,服务 URL 配置管理变得非常困难,F5 硬件负载均衡器的单点压力也越来越大。 -- 当进一步发展,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。 -- 接着,服务的调用量越来越大,服务的容量问题就暴露出来,这个服务需要多少机器支撑?什么时候该加机器? -- 服务多了,沟通成本也开始上升,调某个服务失败该找谁?服务的参数都有什么约定? -- 一个服务有多个业务消费者,如何确保服务质量? -- 随着服务的不停升级,总有些意想不到的事发生,比如 cache 写错了导致内存溢出,故障不可避免,每次核心服务一挂,影响一大片,人心慌慌,如何控制故障的影响面?服务是否可以功能降级?或者资源劣化? - -## 分布式架构的关键技术 - -### 消息队列 - -消息队列通过消息对象分解系统耦合性,不同子系统处理同一个消息。 - -#### 消息队列框架 - -
- -
- -#### 消息队列原理 - -
- -
- -### 服务化 - -服务框架通过接口分解系统耦合性,不同子系统通过相同的接口描述进行服务启用。 - -服务框架是一个点对点模型。 - -服务框架面向同构系统。 - -适合:移动应用、互联网应用、外部系统。 - -#### 服务化框架 - -
- -
- -#### 服务化原理 - -
- -
- -#### 服务治理 - -服务治理是服务框架/服务总线的核心功能。所谓服务治理,是指服务的提供方和消费方达成一致的约定,保证服务的高质量。服务治理功能可以解决将某些特定流量引入某一批机器,以及限制某些非法消费者的恶意访问,并在提供者处理量达到一定程度是,拒绝接受新的访问。 - -当前比较流行的服务治理框架:Dubbo。 - -### 服务总线 - -服务总线同服务框架一样,均是通过接口分解系统耦合性,不同子系统通过相同的接口描述进行服务启用。 - -服务总线是一个总线式的模型。 - -服务总线面向同构、异构系统。 - -适合:内部系统。 - -#### 服务总线框架 - -
- -
- -#### 服务总线原理 - -
- -
- -## 分布式架构的通信模式 - -### request/response 模式(同步模式) - -客户端发起请求一直阻塞到服务端返回请求为止。 - -### Callback(异步模式) - -客户端发送一个 RPC 请求给服务器,服务端处理后再发送一个消息给消息发送端提供的 callback 端点,此类情况非常合适以下场景:A 组件发送 RPC 请求给 B,B 处理完成后,需要通知 A 组件做后续处理。 - -### Future 模式 - -客户端发送完请求后,继续做自己的事情,返回一个包含消息结果的 Future 对象。客户端需要使用返回结果时,使用 Future 对象的.get(),如果此时没有结果返回的话,会一直阻塞到有结果返回为止。 - -### Oneway 模式 - -客户端调用完继续执行,不管接收端是否成功。 - -### Reliable 模式 - -为保证通信可靠,将借助于消息中心来实现消息的可靠送达,请求将做持久化存储,在接收方在线时做送达,并由消息中心保证异常重试。 - -## 资料 - -- https://www.zhihu.com/question/22764869/answer/31277656 diff --git "a/docs/architecture/\345\244\247\345\236\213\345\210\206\345\270\203\345\274\217\347\275\221\347\253\231\346\236\266\346\236\204.md" "b/docs/architecture/\345\244\247\345\236\213\345\210\206\345\270\203\345\274\217\347\275\221\347\253\231\346\236\266\346\236\204.md" deleted file mode 100644 index c29c4375..00000000 --- "a/docs/architecture/\345\244\247\345\236\213\345\210\206\345\270\203\345\274\217\347\275\221\347\253\231\346\236\266\346\236\204.md" +++ /dev/null @@ -1,384 +0,0 @@ ---- -title: 大型分布式网站架构 -date: 2018/07/05 -categories: -- 分布式 -tags: -- 分布式 ---- - -# 大型分布式网站架构 - - - -- [1. 大型分布式网站架构概述](#1-大型分布式网站架构概述) - - [1.1. 大型网站的特点](#11-大型网站的特点) - - [1.2. 大型网站架构目标](#12-大型网站架构目标) - - [1.3. 大型网站架构模式](#13-大型网站架构模式) - - [1.4. 高性能架构](#14-高性能架构) - - [1.5. 高可用架构](#15-高可用架构) - - [1.6. 可伸缩架构](#16-可伸缩架构) - - [1.7. 可扩展架构](#17-可扩展架构) - - [1.8. 安全架构](#18-安全架构) - - [1.9. 敏捷性](#19-敏捷性) - - [1.10. 大型架构举例](#110-大型架构举例) -- [2. 电商网站架构案例](#2-电商网站架构案例) - - [2.1. 网站初级架构](#21-网站初级架构) - - [2.2. 系统容量预估](#22-系统容量预估) - - [2.3. 网站架构分析](#23-网站架构分析) - - [2.4. 网站架构优化](#24-网站架构优化) - - [2.5. 架构总结](#25-架构总结) -- [3. 资料](#3-资料) - - - -## 1. 大型分布式网站架构概述 - -### 1.1. 大型网站的特点 - -- 用户多,分布广泛 -- 大流量,高并发 -- 海量数据,服务高可用 -- 安全环境恶劣,易受网络攻击 -- 功能多,变更快,频繁发布 -- 从小到大,渐进发展 -- 以用户为中心 -- 免费服务,付费体验 - -### 1.2. 大型网站架构目标 - -- 高性能:提供快速的访问体验。 -- 高可用:网站服务一直可以正常访问。 -- 可伸缩:通过硬件增加/减少,提高/降低处理能力。 -- 安全性:提供网站安全访问和数据加密,安全存储等策略。 -- 扩展性:方便的通过新增/移除方式,增加/减少新的功能/模块。 -- 敏捷性:随需应变,快速响应; - -### 1.3. 大型网站架构模式 - -- 分层:一般可分为,应用层,服务层,数据层,管理层,分析层; -- 分割:一般按照业务/模块/功能特点进行划分,比如应用层分为首页,用户中心。 -- 分布式:将应用分开部署(比如多台物理机),通过远程调用协同工作。 -- 集群:一个应用/模块/功能部署多份(如:多台物理机),通过负载均衡共同提供对外访问。 -- 缓存:将数据放在距离应用或用户最近的位置,加快访问速度。 -- 异步:将同步的操作异步化。客户端发出请求,不等待服务端响应,等服务端处理完毕后,使用通知或轮询的方式告知请求方。一般指:请求——响应——通知 模式。 -- 冗余:增加副本,提高可用性,安全性,性能。 -- 安全:对已知问题有有效的解决方案,对未知/潜在问题建立发现和防御机制。 -- 自动化:将重复的,不需要人工参与的事情,通过工具的方式,使用机器完成。 -- 敏捷性:积极接受需求变更,快速响应业务发展需求。 - -### 1.4. 高性能架构 - -以用户为中心,提供快速的网页访问体验。主要参数有较短的响应时间,较大的并发处理能力,较高的吞吐量,稳定的性能参数。 - -可分为前端优化,应用层优化,代码层优化,存储层优化。 - -前端优化:网站业务逻辑之前的部分; - -浏览器优化:减少 Http 请求数,使用浏览器缓存,启用压缩,Css Js 位置,Js 异步,减少 Cookie 传输; - -CDN 加速,反向代理; - -应用层优化:处理网站业务的服务器。使用缓存,异步,集群 - -代码优化:合理的架构,多线程,资源复用(对象池,线程池等),良好的数据结构,JVM 调优,单例,Cache 等; - -存储优化:缓存,固态硬盘,光纤传输,优化读写,磁盘冗余,分布式存储(HDFS),NOSQL 等; - -### 1.5. 高可用架构 - -大型网站应该在任何时候都可以正常访问。正常提供对外服务。因为大型网站的复杂性,分布式,廉价服务器,开源数据库,操作系统等特点。要保证高可用是很困难的,也就是说网站的故障是不可避免的。 - -如何提高可用性,就是需要迫切解决的问题。首先,需要从架构级别,在规划的时候,就考虑可用性。行业内一般用几个 9 表示可用性指标。比如四个 9(99.99),一年内允许的不可用时间是 53 分钟。 - -不同层级使用的策略不同,一般采用冗余备份和失效转移解决高可用问题。 - -应用层:一般设计为无状态的,对于每次请求,使用哪一台服务器处理是没有影响的。一般使用负载均衡技术(需要解决 Session 同步问题),实现高可用。 - -服务层:负载均衡,分级管理,快速失败(超时设置),异步调用,服务降级,幂等设计等。 - -数据层:冗余备份(冷,热备[同步,异步],温备),失效转移(确认,转移,恢复)。数据高可用方面著名的理论基础是 CAP 理论(持久性,可用性,数据一致性[强一致,用户一致,最终一致]) - -### 1.6. 可伸缩架构 - -伸缩性是指在不改变原有架构设计的基础上,通过添加/减少硬件(服务器)的方式,提高/降低系统的处理能力。 - -应用层:对应用进行垂直或水平切分。然后针对单一功能进行负载均衡(DNS,HTTP[反向代理],IP,链路层)。 - -服务层:与应用层类似; - -数据层:分库,分表,NOSQL 等;常用算法 Hash,一致性 Hash。 - -### 1.7. 可扩展架构 - -可以方便的进行功能模块的新增/移除,提供代码/模块级别良好的可扩展性。 - -模块化,组件化:高内聚,内耦合,提高复用性,扩展性。 - -稳定接口:定义稳定的接口,在接口不变的情况下,内部结构可以“随意”变化。 - -设计模式:应用面向对象思想,原则,使用设计模式,进行代码层面的设计。 - -消息队列:模块化的系统,通过消息队列进行交互,使模块之间的依赖解耦。 - -分布式服务:公用模块服务化,提供其他系统使用,提高可重用性,扩展性。 - -### 1.8. 安全架构 - -对已知问题有有效的解决方案,对未知/潜在问题建立发现和防御机制。对于安全问题,首先要提高安全意识,建立一个安全的有效机制,从政策层面,组织层面进行保障。比如服务器密码不能泄露,密码每月更新,并且三次内不能重复;每周安全扫描等。以制度化的方式,加强安全体系的建设。同时,需要注意与安全有关的各个环节。安全问题不容忽视。包括基础设施安全,应用系统安全,数据保密安全等。 - -基础设施安全:硬件采购,操作系统,网络环境方面的安全。一般采用,正规渠道购买高质量的产品,选择安全的操作系统,及时修补漏洞,安装杀毒软件防火墙。防范病毒,后门。设置防火墙策略,建立 DDOS 防御系统,使用攻击检测系统,进行 子网隔离等手段。 - -​ 应用系统安全:在程序开发时,对已知常用问题,使用正确的方式,在代码层面解决掉。防止跨站脚本攻击(XSS),注入攻击,跨站请求伪造(CSRF),错误信息,HTML 注释,文件上传,路径遍历等。还可以使用 Web 应用防火墙(比如:ModSecurity),进行安全漏洞扫描等措施,加强应用级别的安全。 - -​ 数据保密安全:存储安全(存在在可靠的设备,实时,定时备份),保存安全(重要的信息加密保存,选择合适的人员复杂保存和检测等),传输安全(防止数据窃取和数据篡改); - -​ 常用的加解密算法(单项散列加密[MD5,SHA],对称加密[DES,3DES,RC]),非对称加密[RSA]等。 - -### 1.9. 敏捷性 - -网站的架构设计,运维管理要适应变化,提供高伸缩性,高扩展性。方便的应对快速的业务发展,突增高流量访问等要求。 - -除上面介绍的架构要素外,还需要引入敏捷管理,敏捷开发的思想。使业务,产品,技术,运维统一起来,随需应变,快速响应。 - -### 1.10. 大型架构举例 - -![img](https://images2015.cnblogs.com/blog/820332/201511/820332-20151116062211858-1998556963.png) - -以上采用七层逻辑架构,第一层客户层,第二层前端优化层,第三层应用层,第四层服务层,第五层数据存储层,第六层大数据存储层,第七层大数据处理层。 - -客户层:支持 PC 浏览器和手机 APP。差别是手机 APP 可以直接访问通过 IP 访问,反向代理服务器。 - -前端层:使用 DNS 负载均衡,CDN 本地加速以及反向代理服务; - -应用层:网站应用集群;按照业务进行垂直拆分,比如商品应用,会员中心等; - -服务层:提供公用服务,比如用户服务,订单服务,支付服务等; - -数据层:支持关系型数据库集群(支持读写分离),NOSQL 集群,分布式文件系统集群;以及分布式 Cache; - -大数据存储层:支持应用层和服务层的日志数据收集,关系数据库和 NOSQL 数据库的结构化和半结构化数据收集; - -大数据处理层:通过 Mapreduce 进行离线数据分析或 Storm 实时数据分析,并将处理后的数据存入关系型数据库。(实际使用中,离线数据和实时数据会按照业务要求进行分类处理,并存入不同的数据库中,供应用层或服务层使用)。 - -## 2. 电商网站架构案例 - -### 2.1. 网站初级架构 - -一般网站,刚开始的做法,是三台服务器,一台部署应用,一台部署数据库,一台部署 NFS 文件系统。 - -这是前几年比较传统的做法,之前见到一个网站 10 万多会员,垂直服装设计门户,N 多图片。使用了一台服务器部署了应用,数据库以及图片存储。出现了很多性能问题。 - -如下图: - -![img](https://images2015.cnblogs.com/blog/820332/201511/820332-20151130071801546-1010554271.png) - -但是,目前主流的网站架构已经发生了翻天覆地的变化。一般都会采用集群的方式,进行高可用设计。至少是下面这个样子。 - -![img](https://images2015.cnblogs.com/blog/820332/201511/820332-20151130071816093-197013108.png) - -(1) 使用集群对应用服务器进行冗余,实现高可用;(负载均衡设备可与应用一块部署) - -使用数据库主备模式,实现数据备份和高可用; - -### 2.2. 系统容量预估 - -预估步骤: - -(1) 注册用户数-日均 UV 量-每日的 PV 量-每天的并发量; - -(2) 峰值预估:平常量的 2~3 倍; - -(3) 根据并发量(并发,事务数),存储容量计算系统容量。 - -客户需求:3~5 年用户数达到 1000 万注册用户; - -每秒并发数预估: - -(1) 每天的 UV 为 200 万(二八原则); - -(2) 每日每天点击浏览 30 次; - -(3) PV 量:200\*30=6000 万; - -(4) 集中访问量:24*0.2=4.8 小时会有 6000 万*0.8=4800 万(二八原则); - -(5) 每分并发量:4.8\*60=288 分钟,每分钟访问 4800/288=16.7 万(约等于); - -(6) 每秒并发量:16.7 万/60=2780(约等于); - -(7) 假设:高峰期为平常值的三倍,则每秒的并发数可以达到 8340 次。 - -(8) 1 毫秒=1.3 次访问; - -没好好学数学后悔了吧?!(不知道以上算是否有错误,呵呵~~) - -服务器预估:(以 tomcat 服务器举例) - -(1) 按一台 web 服务器,支持每秒 300 个并发计算。平常需要 10 台服务器(约等于);[tomcat 默认配置是 150] - -(2) 高峰期:需要 30 台服务器; - -容量预估:70/90 原则 - -系统 CPU 一般维持在 70%左右的水平,高峰期达到 90%的水平,是不浪费资源,并比较稳定的。内存,IO 类似。 - -以上预估仅供参考,因为服务器配置,业务逻辑复杂度等都有影响。在此 CPU,硬盘,网络等不再进行评估。 - -### 2.3. 网站架构分析 - -根据以上预估,有几个问题: - -- 需要部署大量的服务器,高峰期计算,可能要部署 30 台 Web 服务器。并且这三十台服务器,只有秒杀,活动时才会用到,存在大量的浪费。 -- 所有的应用部署在同一台服务器,应用之间耦合严重。需要进行垂直切分和水平切分。 -- 大量应用存在冗余代码 -- 服务器 SESSION 同步耗费大量内存和网络带宽 -- 数据需要频繁访问数据库,数据库访问压力巨大。 - -大型网站一般需要做以下架构优化(优化是架构设计时,就要考虑的,一般从架构/代码级别解决,调优主要是简单参数的调整,比如 JVM 调优;如果调优涉及大量代码改造,就不是调优了,属于重构): - -- 业务拆分 -- 应用集群部署(分布式部署,集群部署和负载均衡) -- 多级缓存 -- 单点登录(分布式 Session) -- 数据库集群(读写分离,分库分表) -- 服务化 -- 消息队列 -- 其他技术 - -### 2.4. 网站架构优化 - -#### 业务拆分 - -根据业务属性进行垂直切分,划分为产品子系统,购物子系统,支付子系统,评论子系统,客服子系统,接口子系统(对接如进销存,短信等外部系统)。 - -根据业务子系统进行等级定义,可分为核心系统和非核心系统。核心系统:产品子系统,购物子系统,支付子系统;非核心:评论子系统,客服子系统,接口子系统。 - -业务拆分作用:提升为子系统可由专门的团队和部门负责,专业的人做专业的事,解决模块之间耦合以及扩展性问题;每个子系统单独部署,避免集中部署导致一个应用挂了,全部应用不可用的问题。 - -等级定义作用:用于流量突发时,对关键应用进行保护,实现优雅降级;保护关键应用不受到影响。 - -拆分后的架构图: - -![img](https://images2015.cnblogs.com/blog/820332/201511/820332-20151130073032968-1952416935.png) - -参考部署方案 2 -![img](https://images2015.cnblogs.com/blog/820332/201511/820332-20151130073047530-940494801.png) - -(1) 如上图每个应用单独部署 - -(2) 核心系统和非核心系统组合部署 - -#### 应用集群部署(分布式,集群,负载均衡) - -​ 分布式部署:将业务拆分后的应用单独部署,应用直接通过 RPC 进行远程通信; - -​ 集群部署:电商网站的高可用要求,每个应用至少部署两台服务器进行集群部署; - -​ 负载均衡:是高可用系统必须的,一般应用通过负载均衡实现高可用,分布式服务通过内置的负载均衡实现高可用,关系型数据库通过主备方式实现高可用。 - -集群部署后架构图: - -![img](https://images2015.cnblogs.com/blog/820332/201511/820332-20151130073058983-1408763899.png) - -#### 多级缓存 - -缓存按照存放的位置一般可分为两类:本地缓存和分布式缓存。本案例采用二级缓存的方式,进行缓存的设计。一级缓存为本地缓存,二级缓存为分布式缓存。(还有页面缓存,片段缓存等,那是更细粒度的划分) - -一级缓存,缓存数据字典,和常用热点数据等基本不可变/有规则变化的信息,二级缓存缓存需要的所有缓存。当一级缓存过期或不可用时,访问二级缓存的数据。如果二级缓存也没有,则访问数据库。 - -缓存的比例,一般 1:4,即可考虑使用缓存。(理论上是 1:2 即可)。 - -![img](https://images2015.cnblogs.com/blog/820332/201511/820332-20151130073113483-523713370.png) - -​ 根据业务特性可使用以下缓存过期策略: - -(1) 缓存自动过期; - -(2) 缓存触发过期; - -#### 单点登录(分布式 Session) - -系统分割为多个子系统,独立部署后,不可避免的会遇到会话管理的问题。一般可采用 Session 同步,Cookies,分布式 Session 方式。电商网站一般采用分布式 Session 实现。 - -再进一步可以根据分布式 Session,建立完善的单点登录或账户管理系统。 - -![img](https://images2015.cnblogs.com/blog/820332/201511/820332-20151130073125015-707498377.png) - -​ 流程说明 - -(1) 用户第一次登录时,将会话信息(用户 Id 和用户信息),比如以用户 Id 为 Key,写入分布式 Session; - -(2) 用户再次登录时,获取分布式 Session,是否有会话信息,如果没有则调到登录页; - -(3) 一般采用 Cache 中间件实现,建议使用 Redis,因为它有持久化功能,方便分布式 Session 宕机后,可以从持久化存储中加载会话信息; - -(4) 存入会话时,可以设置会话保持的时间,比如 15 分钟,超过后自动超时; - -结合 Cache 中间件,实现的分布式 Session,可以很好的模拟 Session 会话。 - -#### 数据库集群(读写分离,分库分表) - -大型网站需要存储海量的数据,为达到海量数据存储,高可用,高性能一般采用冗余的方式进行系统设计。一般有两种方式读写分离和分库分表。 - -读写分离:一般解决读比例远大于写比例的场景,可采用一主一备,一主多备或多主多备方式。 - -本案例在业务拆分的基础上,结合分库分表和读写分离。如下图: - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151201062903515-1864482914.png) - -(1) 业务拆分后:每个子系统需要单独的库; - -(2) 如果单独的库太大,可以根据业务特性,进行再次分库,比如商品分类库,产品库; - -(3) 分库后,如果表中有数据量很大的,则进行分表,一般可以按照 Id,时间等进行分表;(高级的用法是一致性 Hash) - -(4) 在分库,分表的基础上,进行读写分离; - -相关中间件可参考 Cobar(阿里,目前已不在维护),TDDL(阿里),Atlas(奇虎 360),MyCat(在 Cobar 基础上,国内很多牛人,号称国内第一开源项目)。 - -分库分表后序列的问题,JOIN,事务的问题,会在分库分表主题分享中,介绍。 - -#### 服务化 - -​将多个子系统公用的功能/模块,进行抽取,作为公用服务使用。比如本案例的会员子系统就可以抽取为公用的服务。 - -​ ![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151201062916952-1233076938.png) - -#### 消息队列 - -​ 消息队列可以解决子系统/模块之间的耦合,实现异步,高可用,高性能的系统。是分布式系统的标准配置。本案例中,消息队列主要应用在购物,配送环节。 - -(1) 用户下单后,写入消息队列,后直接返回客户端; - -(2) 库存子系统:读取消息队列信息,完成减库存; - -(3) 配送子系统:读取消息队列信息,进行配送; - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151201062938155-1893082140.png) - -目前使用较多的 MQ 有 Active MQ,Rabbit MQ,Zero MQ,MS MQ 等,需要根据具体的业务场景进行选择。建议可以研究下 Rabbit MQ。 - -#### 其他架构(技术) - -除了以上介绍的业务拆分,应用集群,多级缓存,单点登录,数据库集群,服务化,消息队列外。还有 CDN,反向代理,分布式文件系统,大数据处理等系统。 - -此处不详细介绍,大家可以问度娘/Google,有机会的话也可以分享给大家。 - -### 2.5. 架构总结 - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151201062949187-431217543.png) - -以上是本次分享的架构总结,其中细节可参考前面分享的内容。其中还有很多可以优化和细化的地方,因为是案例分享,主要针对重要部分做了介绍,工作中需要大家根据具体的业务场景进行架构设计。 - -以上是电商网站架构案例的分享一共有三篇,从电商网站的需求,到单机架构,逐步演变为常用的,可供参考的分布式架构的原型。除具备功能需求外,还具备一定的高性能,高可用,可伸缩,可扩展等非功能质量需求(架构目标)。 - -## 3. 资料 - -[大型分布式网站架构技术总结](https://www.cnblogs.com/itfly8/p/4967966.html) - -[大型网站架构系列:电商网站架构案例(1)](https://www.cnblogs.com/itfly8/p/5006197.html) - -[大型网站架构系列:电商网站架构案例(2)](https://www.cnblogs.com/itfly8/p/5006200.html) - -[大型网站架构系列:电商网站架构案例(3)](https://www.cnblogs.com/itfly8/p/5009005.html) diff --git "a/docs/architecture/\345\244\247\345\236\213\347\263\273\347\273\237\350\256\276\350\256\241.md" "b/docs/architecture/\345\244\247\345\236\213\347\263\273\347\273\237\350\256\276\350\256\241.md" deleted file mode 100644 index f1dec274..00000000 --- "a/docs/architecture/\345\244\247\345\236\213\347\263\273\347\273\237\350\256\276\350\256\241.md" +++ /dev/null @@ -1,1239 +0,0 @@ ---- -title: 大型系统架构设计 -date: 2018/07/14 -categories: -- 分布式 -tags: -- 分布式 ---- - -# 大型系统架构设计 - - - -- [1. 性能与可扩展性](#1-性能与可扩展性) -- [2. 延迟与吞吐量](#2-延迟与吞吐量) -- [3. 可用性与一致性](#3-可用性与一致性) -- [4. 一致性模式](#4-一致性模式) -- [5. 可用性模式](#5-可用性模式) -- [6. 域名系统](#6-域名系统) -- [7. 内容分发网络(CDN)](#7-内容分发网络cdn) -- [8. 负载均衡器](#8-负载均衡器) -- [9. 反向代理(web 服务器)](#9-反向代理web-服务器) -- [10. 应用层](#10-应用层) -- [11. 数据库](#11-数据库) -- [12. 缓存](#12-缓存) -- [13. 异步](#13-异步) -- [14. 通讯](#14-通讯) -- [15. 安全](#15-安全) -- [16. 附录](#16-附录) -- [17. 资料](#17-资料) - - - -
- -
- -接下来,我们将看看高阶的权衡和取舍: - -- **性能**与**可扩展性** -- **延迟**与**吞吐量** -- **可用性**与**一致性** - -记住**每个方面都面临取舍和权衡**。 - -然后,我们将深入更具体的主题,如 DNS、CDN 和负载均衡器。 - -## 1. 性能与可扩展性 - -如果服务**性能**的增长与资源的增加是成比例的,服务就是可扩展的。通常,提高性能意味着服务于更多的工作单元,另一方面,当数据集增长时,同样也可以处理更大的工作单位。1 - -另一个角度来看待性能与可扩展性: - -- 如果你的系统有**性能**问题,对于单个用户来说是缓慢的。 -- 如果你的系统有**可扩展性**问题,单个用户较快但在高负载下会变慢。 - -### 来源及延伸阅读 - -- [简单谈谈可扩展性](http://www.allthingsdistributed.com/2006/03/a_word_on_scalability.html) -- [可扩展性,可用性,稳定性和模式](http://www.slideshare.net/jboner/scalability-availability-stability-patterns/) - -## 2. 延迟与吞吐量 - -**延迟**是执行操作或运算结果所花费的时间。 - -**吞吐量**是单位时间内(执行)此类操作或运算的数量。 - -通常,你应该以**可接受级延迟**下**最大化吞吐量**为目标。 - -### 来源及延伸阅读 - -- [理解延迟与吞吐量](https://community.cadence.com/cadence_blogs_8/b/sd/archive/2010/09/13/understanding-latency-vs-throughput) - -## 3. 可用性与一致性 - -### CAP 理论 - -

- -
- 来源:再看 CAP 理论 -

- -在一个分布式计算系统中,只能同时满足下列的两点: - -- **一致性** ─ 每次访问都能获得最新数据但可能会收到错误响应 -- **可用性** ─ 每次访问都能收到非错响应,但不保证获取到最新数据 -- **分区容错性** ─ 在任意分区网络故障的情况下系统仍能继续运行 - -**网络并不可靠,所以你应要支持分区容错性,并需要在软件可用性和一致性间做出取舍。** - -#### CP ─ 一致性和分区容错性 - -等待分区节点的响应可能会导致延时错误。如果你的业务需求需要原子读写,CP 是一个不错的选择。 - -#### AP ─ 可用性与分区容错性 - -响应节点上可用数据的最近版本可能并不是最新的。当分区解析完后,写入(操作)可能需要一些时间来传播。 - -如果业务需求允许[最终一致性](#最终一致性),或当有外部故障时要求系统继续运行,AP 是一个不错的选择。 - -### 来源及延伸阅读 - -- [再看 CAP 理论](http://robertgreiner.com/2014/08/cap-theorem-revisited/) -- [通俗易懂地介绍 CAP 理论](http://ksat.me/a-plain-english-introduction-to-cap-theorem/) -- [CAP FAQ](https://github.com/henryr/cap-faq) - -## 4. 一致性模式 - -有同一份数据的多份副本,我们面临着怎样同步它们的选择,以便让客户端有一致的显示数据。回想 [CAP 理论](#cap-理论)中的一致性定义 ─ 每次访问都能获得最新数据但可能会收到错误响应 - -### 弱一致性 - -在写入之后,访问可能看到,也可能看不到(写入数据)。尽力优化之让其能访问最新数据。 - -这种方式可以 memcached 等系统中看到。弱一致性在 VoIP,视频聊天和实时多人游戏等真实用例中表现不错。打个比方,如果你在通话中丢失信号几秒钟时间,当重新连接时你是听不到这几秒钟所说的话的。 - -### 最终一致性 - -在写入后,访问最终能看到写入数据(通常在数毫秒内)。数据被异步复制。 - -DNS 和 email 等系统使用的是此种方式。最终一致性在高可用性系统中效果不错。 - -### 强一致性 - -在写入后,访问立即可见。数据被同步复制。 - -文件系统和关系型数据库(RDBMS)中使用的是此种方式。强一致性在需要记录的系统中运作良好。 - -### 来源及延伸阅读 - -- [Transactions across data centers](http://snarfed.org/transactions_across_datacenters_io.html) - -## 5. 可用性模式 - -有两种支持高可用性的模式: **故障切换(fail-over)**和**复制(replication)**。 - -### 故障切换 - -#### 工作到备用切换(Active-passive) - -关于工作到备用的故障切换流程是,工作服务器发送周期信号给待机中的备用服务器。如果周期信号中断,备用服务器切换成工作服务器的 IP 地址并恢复服务。 - -宕机时间取决于备用服务器处于“热”待机状态还是需要从“冷”待机状态进行启动。只有工作服务器处理流量。 - -工作到备用的故障切换也被称为主从切换。 - -#### 双工作切换(Active-active) - -在双工作切换中,双方都在管控流量,在它们之间分散负载。 - -如果是外网服务器,DNS 将需要对两方都了解。如果是内网服务器,应用程序逻辑将需要对两方都了解。 - -双工作切换也可以称为主主切换。 - -### 缺陷:故障切换 - -- 故障切换需要添加额外硬件并增加复杂性。 -- 如果新写入数据在能被复制到备用系统之前,工作系统出现了故障,则有可能会丢失数据。 - -### 复制 - -#### 主 ─ 从复制和主 ─ 主复制 - -这个主题进一步探讨了[数据库](#数据库)部分: - -- [主 ─ 从复制](#主从复制) -- [主 ─ 主复制](#主主复制) - -## 6. 域名系统 - -

- -
- 来源:DNS 安全介绍 -

- -域名系统是把 www.example.com 等域名转换成 IP 地址。 - -域名系统是分层次的,一些 DNS 服务器位于顶层。当查询(域名) IP 时,路由或 ISP 提供连接 DNS 服务器的信息。较底层的 DNS 服务器缓存映射,它可能会因为 DNS 传播延时而失效。DNS 结果可以缓存在浏览器或操作系统中一段时间,时间长短取决于[存活时间 TTL](https://en.wikipedia.org/wiki/Time_to_live)。 - -- **NS 记录(域名服务)** ─ 指定解析域名或子域名的 DNS 服务器。 -- **MX 记录(邮件交换)** ─ 指定接收信息的邮件服务器。 -- **A 记录(地址)** ─ 指定域名对应的 IP 地址记录。 -- **CNAME(规范)** ─ 一个域名映射到另一个域名或 `CNAME` 记录( example.com 指向 www.example.com )或映射到一个 `A` 记录。 - -[CloudFlare](https://www.cloudflare.com/dns/) 和 [Route 53](https://aws.amazon.com/route53/) 等平台提供管理 DNS 的功能。某些 DNS 服务通过集中方式来路由流量: - -- [加权轮询调度](http://g33kinfo.com/info/archives/2657) - - 防止流量进入维护中的服务器 - - 在不同大小集群间负载均衡 - - A/B 测试 -- 基于延迟路由 -- 基于地理位置路由 - -### 缺陷:DNS - -- 虽说缓存可以减轻 DNS 延迟,但连接 DNS 服务器还是带来了轻微的延迟。 -- 虽然它们通常由[政府,网络服务提供商和大公司](http://superuser.com/questions/472695/who-controls-the-dns-servers/472729)管理,但 DNS 服务管理仍可能是复杂的。 -- DNS 服务最近遭受 [DDoS 攻击](http://dyn.com/blog/dyn-analysis-summary-of-friday-october-21-attack/),阻止不知道 Twtter IP 地址的用户访问 Twiiter。 - -### 来源及延伸阅读 - -- [DNS 架构]() -- [Wikipedia](https://en.wikipedia.org/wiki/Domain_Name_System) -- [关于 DNS 的文章](https://support.dnsimple.com/categories/dns/) - -## 7. 内容分发网络(CDN) - -

- -
- 来源:为什么使用 CDN -

- -内容分发网络(CDN)是一个全球性的代理服务器分布式网络,它从靠近用户的位置提供内容。通常,HTML/CSS/JS,图片和视频等静态内容由 CDN 提供,虽然亚马逊 CloudFront 等也支持动态内容。CDN 的 DNS 解析会告知客户端连接哪台服务器。 - -将内容存储在 CDN 上可以从两个方面来提供性能: - -- 从靠近用户的数据中心提供资源 -- 通过 CDN 你的服务器不必真的处理请求 - -### CDN 推送(push) - -当你服务器上内容发生变动时,推送 CDN 接受新内容。直接推送给 CDN 并重写 URL 地址以指向你的内容的 CDN 地址。你可以配置内容到期时间及何时更新。内容只有在更改或新增是才推送,流量最小化,但储存最大化。 - -### CDN 拉取(pull) - -CDN 拉取是当第一个用户请求该资源时,从服务器上拉取资源。你将内容留在自己的服务器上并重写 URL 指向 CDN 地址。直到内容被缓存在 CDN 上为止,这样请求只会更慢, - -[存活时间(TTL)](https://en.wikipedia.org/wiki/Time_to_live)决定缓存多久时间。CDN 拉取方式最小化 CDN 上的储存空间,但如果过期文件并在实际更改之前被拉取,则会导致冗余的流量。 - -高流量站点使用 CDN 拉取效果不错,因为只有最近请求的内容保存在 CDN 中,流量才能更平衡地分散。 - -### 缺陷:CDN - -- CDN 成本可能因流量而异,可能在权衡之后你将不会使用 CDN。 -- 如果在 TTL 过期之前更新内容,CDN 缓存内容可能会过时。 -- CDN 需要更改静态内容的 URL 地址以指向 CDN。 - -### 来源及延伸阅读 - -- [全球性内容分发网络](http://repository.cmu.edu/cgi/viewcontent.cgi?article=2112&context=compsci) -- [CDN 拉取和 CDN 推送的区别](http://www.travelblogadvice.com/technical/the-differences-between-push-and-pull-cdns/) -- [Wikipedia](https://en.wikipedia.org/wiki/Content_delivery_network) - -## 8. 负载均衡器 - -

- -
- 来源:可扩展的系统设计模式 -

- -负载均衡器将传入的请求分发到应用服务器和数据库等计算资源。无论哪种情况,负载均衡器将从计算资源来的响应返回给恰当的客户端。负载均衡器的效用在于: - -- 防止请求进入不好的服务器 -- 防止资源过载 -- 帮助消除单一的故障点 - -负载均衡器可以通过硬件(昂贵)或 HAProxy 等软件来实现。 -增加的好处包括: - -- **SSL 终结** ─ 解密传入的请求并加密服务器响应,这样的话后端服务器就不必再执行这些潜在高消耗运算了。 - - 不需要再每台服务器上安装 [X.509 证书](https://en.wikipedia.org/wiki/X.509)。 -- **Session 留存** ─ 如果 Web 应用程序不追踪会话,发出 cookie 并将特定客户端的请求路由到同一实例。 - -通常会设置采用[工作 ─ 备用](#工作到备用切换active-passive) 或 [双工作](#双工作切换active-active) 模式的多个负载均衡器,以免发生故障。 - -负载均衡器能基于多种方式来路由流量: - -- 随机 -- 最少负载 -- Session/cookie -- [轮询调度或加权轮询调度算法](http://g33kinfo.com/info/archives/2657) -- [四层负载均衡](#四层负载均衡) -- [七层负载均衡](#七层负载均衡) - -### 四层负载均衡 - -四层负载均衡根据监看[传输层](#通讯)的信息来决定如何分发请求。通常,这会涉及来源,目标 IP 地址和请求头中的端口,但不包括数据包(报文)内容。四层负载均衡执行[网络地址转换(NAT)](https://www.nginx.com/resources/glossary/layer-4-load-balancing/)来向上游服务器转发网络数据包。 - -### 七层负载均衡器 - -七层负载均衡器根据监控[应用层](#通讯)来决定怎样分发请求。这会涉及请求头的内容,消息和 cookie。七层负载均衡器终结网络流量,读取消息,做出负载均衡判定,然后传送给特定服务器。比如,一个七层负载均衡器能直接将视频流量连接到托管视频的服务器,同时将更敏感的用户账单流量引导到安全性更强的服务器。 - -以损失灵活性为代价,四层负载均衡比七层负载均衡花费更少时间和计算资源,虽然这对现代商用硬件的性能影响甚微。 - -### 水平扩展 - -负载均衡器还能帮助水平扩展,提高性能和可用性。使用商业硬件的性价比更高,并且比在单台硬件上**垂直扩展**更贵的硬件具有更高的可用性。相比招聘特定企业系统人才,招聘商业硬件方面的人才更加容易。 - -#### 缺陷:水平扩展 - -- 水平扩展引入了复杂度并涉及服务器复制 - - 服务器应该是无状态的:它们也不该包含像 session 或资料图片等与用户关联的数据。 - - session 可以集中存储在数据库或持久化[缓存](#缓存)(Redis、Memcached)的数据存储区中。 -- 缓存和数据库等下游服务器需要随着上游服务器进行扩展,以处理更多的并发连接。 - -### 缺陷:负载均衡器 - -- 如果没有足够的资源配置或配置错误,负载均衡器会变成一个性能瓶颈。 -- 引入负载均衡器以帮助消除单点故障但导致了额外的复杂性。 -- 单个负载均衡器会导致单点故障,但配置多个负载均衡器会进一步增加复杂性。 - -### 来源及延伸阅读 - -- [NGINX 架构](https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/) -- [HAProxy 架构指南](http://www.haproxy.org/download/1.2/doc/architecture.txt) -- [可扩展性](http://www.lecloud.net/post/7295452622/scalability-for-dummies-part-1-clones) -- [Wikipedia]() -- [四层负载平衡](https://www.nginx.com/resources/glossary/layer-4-load-balancing/) -- [七层负载平衡](https://www.nginx.com/resources/glossary/layer-7-load-balancing/) -- [ELB 监听器配置](http://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-listener-config.html) - -## 9. 反向代理(web 服务器) - -

- -
- 资料来源:维基百科 -
-

- -反向代理是一种可以集中地调用内部服务,并提供统一接口给公共客户的 web 服务器。来自客户端的请求先被反向代理服务器转发到可响应请求的服务器,然后代理再把服务器的响应结果返回给客户端。 - -带来的好处包括: - -- **增加安全性** - 隐藏后端服务器的信息,屏蔽黑名单中的 IP,限制每个客户端的连接数。 -- **提高可扩展性和灵活性** - 客户端只能看到反向代理服务器的 IP,这使你可以增减服务器或者修改它们的配置。 -- **本地终结 SSL 会话** - 解密传入请求,加密服务器响应,这样后端服务器就不必完成这些潜在的高成本的操作。 - - 免除了在每个服务器上安装 [X.509](https://en.wikipedia.org/wiki/X.509) 证书的需要 -- **压缩** - 压缩服务器响应 -- **缓存** - 直接返回命中的缓存结果 -- **静态内容** - 直接提供静态内容 - - HTML/CSS/JS - - 图片 - - 视频 - - 等等 - -### 负载均衡器与反向代理 - -- 当你有多个服务器时,部署负载均衡器非常有用。通常,负载均衡器将流量路由给一组功能相同的服务器上。 -- 即使只有一台 web 服务器或者应用服务器时,反向代理也有用,可以参考上一节介绍的好处。 -- NGINX 和 HAProxy 等解决方案可以同时支持第七层反向代理和负载均衡。 - -### 不利之处:反向代理 - -- 引入反向代理会增加系统的复杂度。 -- 单独一个反向代理服务器仍可能发生单点故障,配置多台反向代理服务器(如[故障转移](https://en.wikipedia.org/wiki/Failover))会进一步增加复杂度。 - -### 来源及延伸阅读 - -- [反向代理与负载均衡](https://www.nginx.com/resources/glossary/reverse-proxy-vs-load-balancer/) -- [NGINX 架构](https://www.nginx.com/blog/inside-nginx-how-we-designed-for-performance-scale/) -- [HAProxy 架构指南](http://www.haproxy.org/download/1.2/doc/architecture.txt) -- [Wikipedia](https://en.wikipedia.org/wiki/Reverse_proxy) - -## 10. 应用层 - -

- -
- 资料来源:可缩放系统构架介绍 -

- -将 Web 服务层与应用层(也被称作平台层)分离,可以独立缩放和配置这两层。添加新的 API 只需要添加应用服务器,而不必添加额外的 web 服务器。 - -**单一职责原则**提倡小型的,自治的服务共同合作。小团队通过提供小型的服务,可以更激进地计划增长。 - -应用层中的工作进程也有可以实现[异步化](#异步)。 - -### 微服务 - -与此讨论相关的话题是 [微服务](https://en.wikipedia.org/wiki/Microservices),可以被描述为一系列可以独立部署的小型的,模块化服务。每个服务运行在一个独立的线程中,通过明确定义的轻量级机制通讯,共同实现业务目标。1 - -例如,Pinterest 可能有这些微服务: 用户资料、关注者、Feed 流、搜索、照片上传等。 - -### 服务发现 - -像 [Consul](https://www.consul.io/docs/index.html),[Etcd](https://coreos.com/etcd/docs/latest) 和 [Zookeeper](http://www.slideshare.net/sauravhaloi/introduction-to-apache-zookeeper) 这样的系统可以通过追踪注册名、地址、端口等信息来帮助服务互相发现对方。[Health checks](https://www.consul.io/intro/getting-started/checks.html) 可以帮助确认服务的完整性和是否经常使用一个 [HTTP](#超文本传输协议http) 路径。Consul 和 Etcd 都有一个内建的 [key-value 存储](#键-值存储) 用来存储配置信息和其他的共享信息。 - -### 不利之处:应用层 - -- 添加由多个松耦合服务组成的应用层,从架构、运营、流程等层面来讲将非常不同(相对于单体系统)。 -- 微服务会增加部署和运营的复杂度。 - -### 来源及延伸阅读 - -- [可缩放系统构架介绍](http://lethain.com/introduction-to-architecting-systems-for-scale) -- [破解系统设计面试](http://www.puncsky.com/blog/2016/02/14/crack-the-system-design-interview/) -- [面向服务架构](https://en.wikipedia.org/wiki/Service-oriented_architecture) -- [Zookeeper 介绍](http://www.slideshare.net/sauravhaloi/introduction-to-apache-zookeeper) -- [构建微服务,你所需要知道的一切](https://cloudncode.wordpress.com/2016/07/22/msa-getting-started/) - -## 11. 数据库 - -

- -
- 资料来源:扩展你的用户数到第一个一千万 -

- -### 关系型数据库管理系统(RDBMS) - -像 SQL 这样的关系型数据库是一系列以表的形式组织的数据项集合。 - -> 校对注:这里作者 SQL 可能指的是 MySQL - -**ACID** 用来描述关系型数据库[事务](https://en.wikipedia.org/wiki/Database_transaction)的特性。 - -- **原子性** - 每个事务内部所有操作要么全部完成,要么全部不完成。 -- **一致性** - 任何事务都使数据库从一个有效的状态转换到另一个有效状态。 -- **隔离性** - 并发执行事务的结果与顺序执行事务的结果相同。 -- **持久性** - 事务提交后,对系统的影响是永久的。 - -关系型数据库扩展包括许多技术:**主从复制**、**主主复制**、**联合**、**分片**、**非规范化**和 **SQL 调优**。 - -

- -
- 资料来源:可扩展性、可用性、稳定性、模式 -

- -#### 主从复制 - -主库同时负责读取和写入操作,并复制写入到一个或多个从库中,从库只负责读操作。树状形式的从库再将写入复制到更多的从库中去。如果主库离线,系统可以以只读模式运行,直到某个从库被提升为主库或有新的主库出现。 - -##### 不利之处:主从复制 - -- 将从库提升为主库需要额外的逻辑。 -- 参考[不利之处:复制](#不利之处复制)中,主从复制和主主复制**共同**的问题。 - -

- -
- 资料来源:可扩展性、可用性、稳定性、模式 -

- -#### 主主复制 - -两个主库都负责读操作和写操作,写入操作时互相协调。如果其中一个主库挂机,系统可以继续读取和写入。 - -##### 不利之处: 主主复制 - -- 你需要添加负载均衡器或者在应用逻辑中做改动,来确定写入哪一个数据库。 -- 多数主-主系统要么不能保证一致性(违反 ACID),要么因为同步产生了写入延迟。 -- 随着更多写入节点的加入和延迟的提高,如何解决冲突显得越发重要。 -- 参考[不利之处:复制](#不利之处复制)中,主从复制和主主复制**共同**的问题。 - -##### 不利之处:复制 - -- 如果主库在将新写入的数据复制到其他节点前挂掉,则有数据丢失的可能。 -- 写入会被重放到负责读取操作的副本。副本可能因为过多写操作阻塞住,导致读取功能异常。 -- 读取从库越多,需要复制的写入数据就越多,导致更严重的复制延迟。 -- 在某些数据库系统中,写入主库的操作可以用多个线程并行写入,但读取副本只支持单线程顺序地写入。 -- 复制意味着更多的硬件和额外的复杂度。 - -##### 来源及延伸阅读 - -- [扩展性,可用性,稳定性模式](http://www.slideshare.net/jboner/scalability-availability-stability-patterns/) -- [多主复制](https://en.wikipedia.org/wiki/Multi-master_replication) - -#### 联合 - -

- -
- 资料来源:扩展你的用户数到第一个一千万 -

- -联合(或按功能划分)将数据库按对应功能分割。例如,你可以有三个数据库:**论坛**、**用户**和**产品**,而不仅是一个单体数据库,从而减少每个数据库的读取和写入流量,减少复制延迟。较小的数据库意味着更多适合放入内存的数据,进而意味着更高的缓存命中几率。没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。 - -##### 不利之处:联合 - -- 如果你的数据库模式需要大量的功能和数据表,联合的效率并不好。 -- 你需要更新应用程序的逻辑来确定要读取和写入哪个数据库。 -- 用 [server link](http://stackoverflow.com/questions/5145637/querying-data-by-joining-two-tables-in-two-database-on-different-servers) 从两个库联结数据更复杂。 -- 联合需要更多的硬件和额外的复杂度。 - -##### 来源及延伸阅读:联合 - -- [扩展你的用户数到第一个一千万](https://www.youtube.com/watch?v=w95murBkYmU) - -#### 分片 - -

- -
- 资料来源:可扩展性、可用性、稳定性、模式 -

- -分片将数据分配在不同的数据库上,使得每个数据库仅管理整个数据集的一个子集。以用户数据库为例,随着用户数量的增加,越来越多的分片会被添加到集群中。 - -类似[联合](#联合)的优点,分片可以减少读取和写入流量,减少复制并提高缓存命中率。也减少了索引,通常意味着查询更快,性能更好。如果一个分片出问题,其他的仍能运行,你可以使用某种形式的冗余来防止数据丢失。类似联合,没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。 - -常见的做法是用户姓氏的首字母或者用户的地理位置来分隔用户表。 - -##### 不利之处:分片 - -- 你需要修改应用程序的逻辑来实现分片,这会带来复杂的 SQL 查询。 -- 分片不合理可能导致数据负载不均衡。例如,被频繁访问的用户数据会导致其所在分片的负载相对其他分片高。 - - 再平衡会引入额外的复杂度。基于[一致性哈希](http://www.paperplanes.de/2011/12/9/the-magic-of-consistent-hashing.html)的分片算法可以减少这种情况。 -- 联结多个分片的数据操作更复杂。 -- 分片需要更多的硬件和额外的复杂度。 - -#### 来源及延伸阅读:分片 - -- [分片时代来临](http://highscalability.com/blog/2009/8/6/an-unorthodox-approach-to-database-design-the-coming-of-the.html) -- [数据库分片架构]() -- [一致性哈希](http://www.paperplanes.de/2011/12/9/the-magic-of-consistent-hashing.html) - -#### 非规范化 - -非规范化试图以写入性能为代价来换取读取性能。在多个表中冗余数据副本,以避免高成本的联结操作。一些关系型数据库,比如 [PostgreSQL](https://en.wikipedia.org/wiki/PostgreSQL) 和 Oracle 支持[物化视图](https://en.wikipedia.org/wiki/Materialized_view),可以处理冗余信息存储和保证冗余副本一致。 - -当数据使用诸如[联合](#联合)和[分片](#分片)等技术被分割,进一步提高了处理跨数据中心的联结操作复杂度。非规范化可以规避这种复杂的联结操作。 - -在多数系统中,读取操作的频率远高于写入操作,比例可达到 100:1,甚至 1000:1。需要复杂的数据库联结的读取操作成本非常高,在磁盘操作上消耗了大量时间。 - -##### 不利之处:非规范化 - -- 数据会冗余。 -- 约束可以帮助冗余的信息副本保持同步,但这样会增加数据库设计的复杂度。 -- 非规范化的数据库在高写入负载下性能可能比规范化的数据库差。 - -##### 来源及延伸阅读:非规范化 - -- [非规范化](https://en.wikipedia.org/wiki/Denormalization) - -#### SQL 调优 - -SQL 调优是一个范围很广的话题,有很多相关的[书](https://www.amazon.com/s/ref=nb_sb_noss_2?url=search-alias%3Daps&field-keywords=sql+tuning)可以作为参考。 - -利用**基准测试**和**性能分析**来模拟和发现系统瓶颈很重要。 - -- **基准测试** - 用 [ab](http://httpd.apache.org/docs/2.2/programs/ab.html) 等工具模拟高负载情况。 -- **性能分析** - 通过启用如[慢查询日志](http://dev.mysql.com/doc/refman/5.7/en/slow-query-log.html)等工具来辅助追踪性能问题。 - -基准测试和性能分析可能会指引你到以下优化方案。 - -##### 改进模式 - -- 为了实现快速访问,MySQL 在磁盘上用连续的块存储数据。 -- 使用 `CHAR` 类型存储固定长度的字段,不要用 `VARCHAR`。 - - `CHAR` 在快速、随机访问时效率很高。如果使用 `VARCHAR`,如果你想读取下一个字符串,不得不先读取到当前字符串的末尾。 -- 使用 `TEXT` 类型存储大块的文本,例如博客正文。`TEXT` 还允许布尔搜索。使用 `TEXT` 字段需要在磁盘上存储一个用于定位文本块的指针。 -- 使用 `INT` 类型存储高达 2^32 或 40 亿的较大数字。 -- 使用 `DECIMAL` 类型存储货币可以避免浮点数表示错误。 -- 避免使用 `BLOBS` 存储对象,存储存放对象的位置。 -- `VARCHAR(255)` 是以 8 位数字存储的最大字符数,在某些关系型数据库中,最大限度地利用字节。 -- 在适用场景中设置 `NOT NULL` 约束来[提高搜索性能](http://stackoverflow.com/questions/1017239/how-do-null-values-affect-performance-in-a-database-search)。 - -##### 使用正确的索引 - -- 你正查询(`SELECT`、`GROUP BY`、`ORDER BY`、`JOIN`)的列如果用了索引会更快。 -- 索引通常表示为自平衡的 [B 树](https://en.wikipedia.org/wiki/B-tree),可以保持数据有序,并允许在对数时间内进行搜索,顺序访问,插入,删除操作。 -- 设置索引,会将数据存在内存中,占用了更多内存空间。 -- 写入操作会变慢,因为索引需要被更新。 -- 加载大量数据时,禁用索引再加载数据,然后重建索引,这样也许会更快。 - -##### 避免高成本的联结操作 - -- 有性能需要,可以进行非规范化。 - -##### 分割数据表 - -- 将热点数据拆分到单独的数据表中,可以有助于缓存。 - -##### 调优查询缓存 - -- 在某些情况下,[查询缓存](http://dev.mysql.com/doc/refman/5.7/en/query-cache)可能会导致[性能问题](https://www.percona.com/blog/2014/01/28/10-mysql-performance-tuning-settings-after-installation/)。 - -##### 来源及延伸阅读 - -- [MySQL 查询优化小贴士](http://20bits.com/article/10-tips-for-optimizing-mysql-queries-that-dont-suck) -- [为什么 VARCHAR(255) 很常见?](http://stackoverflow.com/questions/1217466/is-there-a-good-reason-i-see-varchar255-used-so-often-as-opposed-to-another-l) -- [Null 值是如何影响数据库性能的?](http://stackoverflow.com/questions/1017239/how-do-null-values-affect-performance-in-a-database-search) -- [慢查询日志](http://dev.mysql.com/doc/refman/5.7/en/slow-query-log.html) - -### NoSQL - -NoSQL 是**键-值数据库**、**文档型数据库**、**列型数据库**或**图数据库**的统称。数据库是非规范化的,表联结大多在应用程序代码中完成。大多数 NoSQL 无法实现真正符合 ACID 的事务,支持[最终一致](#最终一致性)。 - -**BASE** 通常被用于描述 NoSQL 数据库的特性。相比 [CAP 理论](#cap-理论),BASE 强调可用性超过一致性。 - -- **基本可用** - 系统保证可用性。 -- **软状态** - 即使没有输入,系统状态也可能随着时间变化。 -- **最终一致性** - 经过一段时间之后,系统最终会变一致,因为系统在此期间没有收到任何输入。 - -除了在 [SQL 还是 NoSQL](#sql-还是-nosql) 之间做选择,了解哪种类型的 NoSQL 数据库最适合你的用例也是非常有帮助的。我们将在下一节中快速了解下 **键-值存储**、**文档型存储**、**列型存储**和**图存储**数据库。 - -#### 键-值存储 - -> 抽象模型:哈希表 - -键-值存储通常可以实现 O(1) 时间读写,用内存或 SSD 存储数据。数据存储可以按[字典顺序](https://en.wikipedia.org/wiki/Lexicographical_order)维护键,从而实现键的高效检索。键-值存储可以用于存储元数据。 - -键-值存储性能很高,通常用于存储简单数据模型或频繁修改的数据,如存放在内存中的缓存。键-值存储提供的操作有限,如果需要更多操作,复杂度将转嫁到应用程序层面。 - -键-值存储是如文档存储,在某些情况下,甚至是图存储等更复杂的存储系统的基础。 - -#### 来源及延伸阅读 - -- [键-值数据库](https://en.wikipedia.org/wiki/Key-value_database) -- [键-值存储的劣势](http://stackoverflow.com/questions/4056093/what-are-the-disadvantages-of-using-a-key-value-table-over-nullable-columns-or) -- [Redis 架构](http://qnimate.com/overview-of-redis-architecture/) -- [Memcached 架构](https://www.adayinthelifeof.nl/2011/02/06/memcache-internals/) - -#### 文档类型存储 - -> 抽象模型:将文档作为值的键-值存储 - -文档类型存储以文档(XML、JSON、二进制文件等)为中心,文档存储了指定对象的全部信息。文档存储根据文档自身的内部结构提供 API 或查询语句来实现查询。请注意,许多键-值存储数据库有用值存储元数据的特性,这也模糊了这两种存储类型的界限。 - -基于底层实现,文档可以根据集合、标签、元数据或者文件夹组织。尽管不同文档可以被组织在一起或者分成一组,但相互之间可能具有完全不同的字段。 - -MongoDB 和 CouchDB 等一些文档类型存储还提供了类似 SQL 语言的查询语句来实现复杂查询。DynamoDB 同时支持键-值存储和文档类型存储。 - -文档类型存储具备高度的灵活性,常用于处理偶尔变化的数据。 - -#### 来源及延伸阅读:文档类型存储 - -- [面向文档的数据库](https://en.wikipedia.org/wiki/Document-oriented_database) -- [MongoDB 架构](https://www.mongodb.com/mongodb-architecture) -- [CouchDB 架构](https://blog.couchdb.org/2016/08/01/couchdb-2-0-architecture/) -- [Elasticsearch 架构](https://www.elastic.co/blog/found-elasticsearch-from-the-bottom-up) - -#### 列型存储 - -

- -
- 资料来源: SQL 和 NoSQL,一个简短的历史 -

- -> 抽象模型:嵌套的 `ColumnFamily>` 映射 - -类型存储的基本数据单元是列(名/值对)。列可以在列族(类似于 SQL 的数据表)中被分组。超级列族再分组普通列族。你可以使用行键独立访问每一列,具有相同行键值的列组成一行。每个值都包含版本的时间戳用于解决版本冲突。 - -Google 发布了第一个列型存储数据库 [Bigtable](http://www.read.seas.harvard.edu/~kohler/class/cs239-w08/chang06bigtable.pdf),它影响了 Hadoop 生态系统中活跃的开源数据库 [HBase](https://www.mapr.com/blog/in-depth-look-hbase-architecture) 和 Facebook 的 [Cassandra](http://docs.datastax.com/en/archived/cassandra/2.0/cassandra/architecture/architectureIntro_c.html)。像 BigTable,HBase 和 Cassandra 这样的存储系统将键以字母顺序存储,可以高效地读取键列。 - -列型存储具备高可用性和高可扩展性。通常被用于大数据相关存储。 - -##### 来源及延伸阅读:列型存储 - -- [SQL 与 NoSQL 简史](http://blog.grio.com/2015/11/sql-nosql-a-brief-history.html) -- [BigTable 架构](http://www.read.seas.harvard.edu/~kohler/class/cs239-w08/chang06bigtable.pdf) -- [Hbase 架构](https://www.mapr.com/blog/in-depth-look-hbase-architecture) -- [Cassandra 架构](http://docs.datastax.com/en/archived/cassandra/2.0/cassandra/architecture/architectureIntro_c.html) - -#### 图数据库 - -

- -
- 资料来源:图数据库 -

- -> 抽象模型: 图 - -在图数据库中,一个节点对应一条记录,一个弧对应两个节点之间的关系。图数据库被优化用于表示外键繁多的复杂关系或多对多关系。 - -图数据库为存储复杂关系的数据模型,如社交网络,提供了很高的性能。它们相对较新,尚未广泛应用,查找开发工具或者资源相对较难。许多图只能通过 [REST API](#表述性状态转移rest) 访问。 - -##### 相关资源和延伸阅读:图 - -- [图数据库](https://en.wikipedia.org/wiki/Graph_database) -- [Neo4j](https://neo4j.com/) -- [FlockDB](https://blog.twitter.com/2010/introducing-flockdb) - -#### 来源及延伸阅读:NoSQL - -- [数据库术语解释](http://stackoverflow.com/questions/3342497/explanation-of-base-terminology) -- [NoSQL 数据库 - 调查及决策指南](https://medium.com/baqend-blog/nosql-databases-a-survey-and-decision-guidance-ea7823a822d#.wskogqenq) -- [可扩展性](http://www.lecloud.net/post/7994751381/scalability-for-dummies-part-2-database) -- [NoSQL 介绍](https://www.youtube.com/watch?v=qI_g07C_Q5I) -- [NoSQL 模式](http://horicky.blogspot.com/2009/11/nosql-patterns.html) - -### SQL 还是 NoSQL - -

- -
- 资料来源:从 RDBMS 转换到 NoSQL -

- -选取 **SQL** 的原因: - -- 结构化数据 -- 严格的模式 -- 关系型数据 -- 需要复杂的联结操作 -- 事务 -- 清晰的扩展模式 -- 既有资源更丰富:开发者、社区、代码库、工具等 -- 通过索引进行查询非常快 - -选取 **NoSQL** 的原因: - -- 半结构化数据 -- 动态或灵活的模式 -- 非关系型数据 -- 不需要复杂的联结操作 -- 存储 TB (甚至 PB)级别的数据 -- 高数据密集的工作负载 -- IOPS 高吞吐量 - -适合 NoSQL 的示例数据: - -- 埋点数据和日志数据 -- 排行榜或者得分数据 -- 临时数据,如购物车 -- 频繁访问的(“热”)表 -- 元数据/查找表 - -##### 来源及延伸阅读:SQL 或 NoSQL - -- [扩展你的用户数到第一个千万](https://www.youtube.com/watch?v=w95murBkYmU) -- [SQL 和 NoSQL 的不同](https://www.sitepoint.com/sql-vs-nosql-differences/) - -## 12. 缓存 - -

- -
- 资料来源:可扩展的系统设计模式 -

- -缓存可以提高页面加载速度,并可以减少服务器和数据库的负载。在这个模型中,分发器先查看请求之前是否被响应过,如果有则将之前的结果直接返回,来省掉真正的处理。 - -数据库分片均匀分布的读取是最好的。但是热门数据会让读取分布不均匀,这样就会造成瓶颈,如果在数据库前加个缓存,就会抹平不均匀的负载和突发流量对数据库的影响。 - -### 客户端缓存 - -缓存可以位于客户端(操作系统或者浏览器),[服务端](#反向代理web-服务器)或者不同的缓存层。 - -### CDN 缓存 - -[CDN](#内容分发网络cdn) 也被视为一种缓存。 - -### Web 服务器缓存 - -[反向代理](#反向代理web-服务器)和缓存(比如 [Varnish](https://www.varnish-cache.org/))可以直接提供静态和动态内容。Web 服务器同样也可以缓存请求,返回相应结果而不必连接应用服务器。 - -### 数据库缓存 - -数据库的默认配置中通常包含缓存级别,针对一般用例进行了优化。调整配置,在不同情况下使用不同的模式可以进一步提高性能。 - -### 应用缓存 - -基于内存的缓存比如 Memcached 和 Redis 是应用程序和数据存储之间的一种键值存储。由于数据保存在 RAM 中,它比存储在磁盘上的典型数据库要快多了。RAM 比磁盘限制更多,所以例如 [least recently used (LRU)](https://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used) 的[缓存无效算法](https://en.wikipedia.org/wiki/Cache_algorithms)可以将「热门数据」放在 RAM 中,而对一些比较「冷门」的数据不做处理。 - -Redis 有下列附加功能: - -- 持久性选项 -- 内置数据结构比如有序集合和列表 - -有多个缓存级别,分为两大类:**数据库查询**和**对象**: - -- 行级别 -- 查询级别 -- 完整的可序列化对象 -- 完全渲染的 HTML - -一般来说,你应该尽量避免基于文件的缓存,因为这使得复制和自动缩放很困难。 - -### 数据库查询级别的缓存 - -当你查询数据库的时候,将查询语句的哈希值与查询结果存储到缓存中。这种方法会遇到以下问题: - -- 很难用复杂的查询删除已缓存结果。 -- 如果一条数据比如表中某条数据的一项被改变,则需要删除所有可能包含已更改项的缓存结果。 - -### 对象级别的缓存 - -将您的数据视为对象,就像对待你的应用代码一样。让应用程序将数据从数据库中组合到类实例或数据结构中: - -- 如果对象的基础数据已经更改了,那么从缓存中删掉这个对象。 -- 允许异步处理:workers 通过使用最新的缓存对象来组装对象。 - -建议缓存的内容: - -- 用户会话 -- 完全渲染的 Web 页面 -- 活动流 -- 用户图数据 - -### 何时更新缓存 - -由于你只能在缓存中存储有限的数据,所以你需要选择一个适用于你用例的缓存更新策略。 - -#### 缓存模式 - -

- -
- 资料来源:从缓存到内存数据网格 -

- -应用从存储器读写。缓存不和存储器直接交互,应用执行以下操作: - -- 在缓存中查找记录,如果所需数据不在缓存中 -- 从数据库中加载所需内容 -- 将查找到的结果存储到缓存中 -- 返回所需内容 - -``` -def get_user(self, user_id): - user = cache.get("user.{0}", user_id) - if user is None: - user = db.query("SELECT * FROM users WHERE user_id = {0}", user_id) - if user is not None: - key = "user.{0}".format(user_id) - cache.set(key, json.dumps(user)) - return user -``` - -[Memcached](https://memcached.org/) 通常用这种方式使用。 - -添加到缓存中的数据读取速度很快。缓存模式也称为延迟加载。只缓存所请求的数据,这避免了没有被请求的数据占满了缓存空间。 - -##### 缓存的缺点: - -- 请求的数据如果不在缓存中就需要经过三个步骤来获取数据,这会导致明显的延迟。 -- 如果数据库中的数据更新了会导致缓存中的数据过时。这个问题需要通过设置  TTL 强制更新缓存或者直写模式来缓解这种情况。 -- 当一个节点出现故障的时候,它将会被一个新的节点替代,这增加了延迟的时间。 - -#### 直写模式 - -

- -
- 资料来源:可扩展性、可用性、稳定性、模式 -

- -应用使用缓存作为主要的数据存储,将数据读写到缓存中,而缓存负责从数据库中读写数据。 - -- 应用向缓存中添加/更新数据 -- 缓存同步地写入数据存储 -- 返回所需内容 - -应用代码: - -``` -set_user(12345, {"foo":"bar"}) -``` - -缓存代码: - -``` -def set_user(user_id, values): - user = db.query("UPDATE Users WHERE id = {0}", user_id, values) - cache.set(user_id, user) -``` - -由于存写操作所以直写模式整体是一种很慢的操作,但是读取刚写入的数据很快。相比读取数据,用户通常比较能接受更新数据时速度较慢。缓存中的数据不会过时。 - -##### 直写模式的缺点: - -- 由于故障或者缩放而创建的新的节点,新的节点不会缓存,直到数据库更新为止。缓存应用直写模式可以缓解这个问题。 -- 写入的大多数数据可能永远都不会被读取,用 TTL 可以最小化这种情况的出现。 - -#### 回写模式 - -

- -
- 资料来源:可扩展性、可用性、稳定性、模式 -

- -在回写模式中,应用执行以下操作: - -- 在缓存中增加或者更新条目 -- 异步写入数据,提高写入性能。 - -##### 回写模式的缺点: - -- 缓存可能在其内容成功存储之前丢失数据。 -- 执行直写模式比缓存或者回写模式更复杂。 - -#### 刷新 - -

- -
- 资料来源:从缓存到内存数据网格 -

- -你可以将缓存配置成在到期之前自动刷新最近访问过的内容。 - -如果缓存可以准确预测将来可能请求哪些数据,那么刷新可能会导致延迟与读取时间的降低。 - -##### 刷新的缺点: - -- 不能准确预测到未来需要用到的数据可能会导致性能不如不使用刷新。 - -### 缓存的缺点: - -- 需要保持缓存和真实数据源之间的一致性,比如数据库根据[缓存无效](https://en.wikipedia.org/wiki/Cache_algorithms)。 -- 需要改变应用程序比如增加 Redis 或者 memcached。 -- 无效缓存是个难题,什么时候更新缓存是与之相关的复杂问题。 - -### 相关资源和延伸阅读 - -- [从缓存到内存数据](http://www.slideshare.net/tmatyashovsky/from-cache-to-in-memory-data-grid-introduction-to-hazelcast) -- [可扩展系统设计模式](http://horicky.blogspot.com/2010/10/scalable-system-design-patterns.html) -- [可缩放系统构架介绍](http://lethain.com/introduction-to-architecting-systems-for-scale/) -- [可扩展性,可用性,稳定性和模式](http://www.slideshare.net/jboner/scalability-availability-stability-patterns/) -- [可扩展性](http://www.lecloud.net/post/9246290032/scalability-for-dummies-part-3-cache) -- [AWS ElastiCache 策略](http://docs.aws.amazon.com/AmazonElastiCache/latest/UserGuide/Strategies.html) -- [维基百科]() - -## 13. 异步 - -

- -
- 资料来源:可缩放系统构架介绍 -

- -异步工作流有助于减少那些原本顺序执行的请求时间。它们可以通过提前进行一些耗时的工作来帮助减少请求时间,比如定期汇总数据。 - -### 消息队列 - -消息队列接收,保留和传递消息。如果按顺序执行操作太慢的话,你可以使用有以下工作流的消息队列: - -- 应用程序将作业发布到队列,然后通知用户作业状态 -- 一个 worker 从队列中取出该作业,对其进行处理,然后显示该作业完成 - -不去阻塞用户操作,作业在后台处理。在此期间,客户端可能会进行一些处理使得看上去像是任务已经完成了。例如,如果要发送一条推文,推文可能会马上出现在你的时间线上,但是可能需要一些时间才能将你的推文推送到你的所有关注者那里去。 - -**Redis** 是一个令人满意的简单的消息代理,但是消息有可能会丢失。 - -**RabbitMQ** 很受欢迎但是要求你适应「AMQP」协议并且管理你自己的节点。 - -**Amazon SQS** 是被托管的,但可能具有高延迟,并且消息可能会被传送两次。 - -### 任务队列 - -任务队列接收任务及其相关数据,运行它们,然后传递其结果。 它们可以支持调度,并可用于在后台运行计算密集型作业。 - -**Celery** 支持调度,主要是用 Python 开发的。 - -### 背压 - -如果队列开始明显增长,那么队列大小可能会超过内存大小,导致高速缓存未命中,磁盘读取,甚至性能更慢。[背压](http://mechanical-sympathy.blogspot.com/2012/05/apply-back-pressure-when-overloaded.html)可以通过限制队列大小来帮助我们,从而为队列中的作业保持高吞吐率和良好的响应时间。一旦队列填满,客户端将得到服务器忙或者 HTTP 503 状态码,以便稍后重试。客户端可以在稍后时间重试该请求,也许是[指数退避](https://en.wikipedia.org/wiki/Exponential_backoff)。 - -### 异步的缺点: - -- 简单的计算和实时工作流等用例可能更适用于同步操作,因为引入队列可能会增加延迟和复杂性。 - -### 相关资源和延伸阅读 - -- [这是一个数字游戏](https://www.youtube.com/watch?v=1KRYH75wgy4) -- [超载时应用背压](http://mechanical-sympathy.blogspot.com/2012/05/apply-back-pressure-when-overloaded.html) -- [利特尔法则](https://en.wikipedia.org/wiki/Little%27s_law) -- [消息队列与任务队列有什么区别?](https://www.quora.com/What-is-the-difference-between-a-message-queue-and-a-task-queue-Why-would-a-task-queue-require-a-message-broker-like-RabbitMQ-Redis-Celery-or-IronMQ-to-function) - -## 14. 通讯 - -

- -
- 资料来源:OSI 7层模型 -

- -### 超文本传输协议(HTTP) - -HTTP 是一种在客户端和服务器之间编码和传输数据的方法。它是一个请求/响应协议:客户端和服务端针对相关内容和完成状态信息的请求和响应。HTTP 是独立的,允许请求和响应流经许多执行负载均衡,缓存,加密和压缩的中间路由器和服务器。 - -一个基本的 HTTP 请求由一个动词(方法)和一个资源(端点)组成。 以下是常见的 HTTP 动词: - -| 动词 | 描述 | \*幂等 | 安全性 | 可缓存 | -| ------ | ---------------------------- | ------ | ------ | ------------------------- | -| GET | 读取资源 | Yes | Yes | Yes | -| POST | 创建资源或触发处理数据的进程 | No | No | Yes,如果回应包含刷新信息 | -| PUT | 创建或替换资源 | Yes | No | No | -| PATCH | 部分更新资源 | No | No | Yes,如果回应包含刷新信息 | -| DELETE | 删除资源 | Yes | No | No | - -**多次执行不会产生不同的结果**。 - -HTTP 是依赖于较低级协议(如 **TCP** 和 **UDP**)的应用层协议。 - -#### 来源及延伸阅读:HTTP - -- [README](https://www.quora.com/What-is-the-difference-between-HTTP-protocol-and-TCP-protocol) + -- [HTTP 是什么?](https://www.nginx.com/resources/glossary/http/) -- [HTTP 和 TCP 的区别](https://www.quora.com/What-is-the-difference-between-HTTP-protocol-and-TCP-protocol) -- [PUT 和 PATCH 的区别](https://laracasts.com/discuss/channels/general-discussion/whats-the-differences-between-put-and-patch?page=1) - -### 传输控制协议(TCP) - -

- -
- 资料来源:如何制作多人游戏 -

- -TCP 是通过 [IP 网络](https://en.wikipedia.org/wiki/Internet_Protocol)的面向连接的协议。 使用[握手](https://en.wikipedia.org/wiki/Handshaking)建立和断开连接。 发送的所有数据包保证以原始顺序到达目的地,用以下措施保证数据包不被损坏: - -- 每个数据包的序列号和[校验码](https://en.wikipedia.org/wiki/Transmission_Control_Protocol#Checksum_computation)。 -- [确认包]()和自动重传 - -如果发送者没有收到正确的响应,它将重新发送数据包。如果多次超时,连接就会断开。TCP 实行[流量控制]()和[拥塞控制](https://en.wikipedia.org/wiki/Network_congestion#Congestion_control)。这些确保措施会导致延迟,而且通常导致传输效率比 UDP 低。 - -为了确保高吞吐量,Web 服务器可以保持大量的 TCP 连接,从而导致高内存使用。在 Web 服务器线程间拥有大量开放连接可能开销巨大,消耗资源过多,也就是说,一个 [memcached](#memcached) 服务器。[连接池](https://en.wikipedia.org/wiki/Connection_pool) 可以帮助除了在适用的情况下切换到 UDP。 - -TCP 对于需要高可靠性但时间紧迫的应用程序很有用。比如包括 Web 服务器,数据库信息,SMTP,FTP 和 SSH。 - -以下情况使用 TCP 代替 UDP: - -- 你需要数据完好无损。 -- 你想对网络吞吐量自动进行最佳评估。 - -### 用户数据报协议(UDP) - -

- -
- 资料来源:如何制作多人游戏 -

- -UDP 是无连接的。数据报(类似于数据包)只在数据报级别有保证。数据报可能会无序的到达目的地,也有可能会遗失。UDP 不支持拥塞控制。虽然不如 TCP 那样有保证,但 UDP 通常效率更高。 - -UDP 可以通过广播将数据报发送至子网内的所有设备。这对 [DHCP](https://en.wikipedia.org/wiki/Dynamic_Host_Configuration_Protocol) 很有用,因为子网内的设备还没有分配 IP 地址,而 IP 对于 TCP 是必须的。 - -UDP 可靠性更低但适合用在网络电话、视频聊天,流媒体和实时多人游戏上。 - -以下情况使用 UDP 代替 TCP: - -- 你需要低延迟 -- 相对于数据丢失更糟的是数据延迟 -- 你想实现自己的错误校正方法 - -#### 来源及延伸阅读:TCP 与 UDP - -- [游戏编程的网络](http://gafferongames.com/networking-for-game-programmers/udp-vs-tcp/) -- [TCP 与 UDP 的关键区别](http://www.cyberciti.biz/faq/key-differences-between-tcp-and-udp-protocols/) -- [TCP 与 UDP 的不同](http://stackoverflow.com/questions/5970383/difference-between-tcp-and-udp) -- [传输控制协议](https://en.wikipedia.org/wiki/Transmission_Control_Protocol) -- [用户数据报协议](https://en.wikipedia.org/wiki/User_Datagram_Protocol) -- [Memcache 在 Facebook 的扩展](http://www.cs.bu.edu/~jappavoo/jappavoo.github.com/451/papers/memcache-fb.pdf) - -### 远程过程调用协议(RPC) - -

- -
- Source: Crack the system design interview -

- -在 RPC 中,客户端会去调用另一个地址空间(通常是一个远程服务器)里的方法。调用代码看起来就像是调用的是一个本地方法,客户端和服务器交互的具体过程被抽象。远程调用相对于本地调用一般较慢而且可靠性更差,因此区分两者是有帮助的。热门的 RPC 框架包括 [Protobuf](https://developers.google.com/protocol-buffers/)、[Thrift](https://thrift.apache.org/) 和 [Avro](https://avro.apache.org/docs/current/)。 - -RPC 是一个“请求-响应”协议: - -- **客户端程序** ── 调用客户端存根程序。就像调用本地方法一样,参数会被压入栈中。 -- **客户端 stub 程序** ── 将请求过程的 id 和参数打包进请求信息中。 -- **客户端通信模块** ── 将信息从客户端发送至服务端。 -- **服务端通信模块** ── 将接受的包传给服务端存根程序。 -- **服务端 stub 程序** ── 将结果解包,依据过程 id 调用服务端方法并将参数传递过去。 - -RPC 调用示例: - -``` -GET /someoperation?data=anId - -POST /anotheroperation -{ - "data":"anId"; - "anotherdata": "another value" -} -``` - -RPC 专注于暴露方法。RPC 通常用于处理内部通讯的性能问题,这样你可以手动处理本地调用以更好的适应你的情况。 - -当以下情况时选择本地库(也就是 SDK): - -- 你知道你的目标平台。 -- 你想控制如何访问你的“逻辑”。 -- 你想对发生在你的库中的错误进行控制。 -- 性能和终端用户体验是你最关心的事。 - -遵循 **REST** 的 HTTP API 往往更适用于公共 API。 - -#### 缺点:RPC - -- RPC 客户端与服务实现捆绑地很紧密。 -- 一个新的 API 必须在每一个操作或者用例中定义。 -- RPC 很难调试。 -- 你可能没办法很方便的去修改现有的技术。举个例子,如果你希望在 [Squid](http://www.squid-cache.org/) 这样的缓存服务器上确保 [RPC 被正确缓存](http://etherealbits.com/2012/12/debunking-the-myths-of-rpc-rest/)的话可能需要一些额外的努力了。 - -### 表述性状态转移(REST) - -REST 是一种强制的客户端/服务端架构设计模型,客户端基于服务端管理的一系列资源操作。服务端提供修改或获取资源的接口。所有的通信必须是无状态和可缓存的。 - -RESTful 接口有四条规则: - -- **标志资源(HTTP 里的 URI)** ── 无论什么操作都使用同一个 URI。 -- **表示的改变(HTTP 的动作)** ── 使用动作, headers 和 body。 -- **可自我描述的错误信息(HTTP 中的 status code)** ── 使用状态码,不要重新造轮子。 -- **[HATEOAS](http://restcookbook.com/Basics/hateoas/)(HTTP 中的 HTML 接口)** ── 你的 web 服务器应该能够通过浏览器访问。 - -REST 请求的例子: - -``` -GET /someresources/anId - -PUT /someresources/anId -{"anotherdata": "another value"} -``` - -REST 关注于暴露数据。它减少了客户端/服务端的耦合程度,经常用于公共 HTTP API 接口设计。REST 使用更通常与规范化的方法来通过 URI 暴露资源,[通过 header 来表述](https://github.com/for-GET/know-your-http-well/blob/master/headers.md)并通过 GET、POST、PUT、DELETE 和 PATCH 这些动作来进行操作。因为无状态的特性,REST 易于横向扩展和隔离。 - -#### 缺点:REST - -- 由于 REST 将重点放在暴露数据,所以当资源不是自然组织的或者结构复杂的时候它可能无法很好的适应。举个例子,返回过去一小时中与特定事件集匹配的更新记录这种操作就很难表示为路径。使用 REST,可能会使用 URI 路径,查询参数和可能的请求体来实现。 -- REST 一般依赖几个动作(GET、POST、PUT、DELETE 和 PATCH),但有时候仅仅这些没法满足你的需要。举个例子,将过期的文档移动到归档文件夹里去,这样的操作可能没法简单的用上面这几个 verbs 表达。 -- 为了渲染单个页面,获取被嵌套在层级结构中的复杂资源需要客户端,服务器之间多次往返通信。例如,获取博客内容及其关联评论。对于使用不确定网络环境的移动应用来说,这些多次往返通信是非常麻烦的。 -- 随着时间的推移,更多的字段可能会被添加到 API 响应中,较旧的客户端将会接收到所有新的数据字段,即使是那些它们不需要的字段,结果它会增加负载大小并引起更大的延迟。 - -### RPC 与 REST 比较 - -| 操作 | RPC | REST | -| ---------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------ | -| 注册 | **POST** /signup | **POST** /persons | -| 注销 | **POST** /resign
{
"personid": "1234"
} | **DELETE** /persons/1234 | -| 读取用户信息 | **GET** /readPerson?personid=1234 | **GET** /persons/1234 | -| 读取用户物品列表 | **GET** /readUsersItemsList?personid=1234 | **GET** /persons/1234/items | -| 向用户物品列表添加一项 | **POST** /addItemToUsersItemsList
{
"personid": "1234";
"itemid": "456"
} | **POST** /persons/1234/items
{
"itemid": "456"
} | -| 更新一个物品 | **POST** /modifyItem
{
"itemid": "456";
"key": "value"
} | **PUT** /items/456
{
"key": "value"
} | -| 删除一个物品 | **POST** /removeItem
{
"itemid": "456"
} | **DELETE** /items/456 | - -

- 资料来源:你真的知道你为什么更喜欢 REST 而不是 RPC 吗 -

- -#### 来源及延伸阅读:REST 与 RPC - -- [你真的知道你为什么更喜欢 REST 而不是 RPC 吗](https://apihandyman.io/do-you-really-know-why-you-prefer-rest-over-rpc/) -- [什么时候 RPC 比 REST 更合适?](http://programmers.stackexchange.com/a/181186) -- [REST vs JSON-RPC](http://stackoverflow.com/questions/15056878/rest-vs-json-rpc) -- [揭开 RPC 和 REST 的神秘面纱](http://etherealbits.com/2012/12/debunking-the-myths-of-rpc-rest/) -- [使用 REST 的缺点是什么](https://www.quora.com/What-are-the-drawbacks-of-using-RESTful-APIs) -- [破解系统设计面试](http://www.puncsky.com/blog/2016/02/14/crack-the-system-design-interview/) -- [Thrift](https://code.facebook.com/posts/1468950976659943/) -- [为什么在内部使用 REST 而不是 RPC](http://arstechnica.com/civis/viewtopic.php?t=1190508) - -## 15. 安全 - -这一部分需要更多内容。[一起来吧](#贡献)! - -安全是一个宽泛的话题。除非你有相当的经验、安全方面背景或者正在申请的职位要求安全知识,你不需要了解安全基础知识以外的内容: - -- 在运输和等待过程中加密 -- 对所有的用户输入和从用户那里发来的参数进行处理以防止 [XSS](https://en.wikipedia.org/wiki/Cross-site_scripting) 和 [SQL 注入](https://en.wikipedia.org/wiki/SQL_injection)。 -- 使用参数化的查询来防止 SQL 注入。 -- 使用[最小权限原则](https://en.wikipedia.org/wiki/Principle_of_least_privilege)。 - -### 来源及延伸阅读 - -- [为开发者准备的安全引导](https://github.com/FallibleInc/security-guide-for-developers) -- [OWASP top ten](https://www.owasp.org/index.php/OWASP_Top_Ten_Cheat_Sheet) - -## 16. 附录 - -一些时候你会被要求做出保守估计。比如,你可能需要估计从磁盘中生成 100 张图片的缩略图需要的时间或者一个数据结构需要多少的内存。**2 的次方表**和**每个开发者都需要知道的一些时间数据**(译注:OSChina 上有这篇文章的[译文](https://www.oschina.net/news/30009/every-programmer-should-know))都是一些很方便的参考资料。 - -### 2 的次方表 - -``` -Power Exact Value Approx Value Bytes ---------------------------------------------------------------- -7 128 -8 256 -10 1024 1 thousand 1 KB -16 65,536 64 KB -20 1,048,576 1 million 1 MB -30 1,073,741,824 1 billion 1 GB -32 4,294,967,296 4 GB -40 1,099,511,627,776 1 trillion 1 TB -``` - -#### 来源及延伸阅读 - -- [2 的次方](https://en.wikipedia.org/wiki/Power_of_two) - -### 每个程序员都应该知道的延迟数 - -``` -Latency Comparison Numbers --------------------------- -L1 cache reference 0.5 ns -Branch mispredict 5 ns -L2 cache reference 7 ns 14x L1 cache -Mutex lock/unlock 100 ns -Main memory reference 100 ns 20x L2 cache, 200x L1 cache -Compress 1K bytes with Zippy 10,000 ns 10 us -Send 1 KB bytes over 1 Gbps network 10,000 ns 10 us -Read 4 KB randomly from SSD* 150,000 ns 150 us ~1GB/sec SSD -Read 1 MB sequentially from memory 250,000 ns 250 us -Round trip within same datacenter 500,000 ns 500 us -Read 1 MB sequentially from SSD* 1,000,000 ns 1,000 us 1 ms ~1GB/sec SSD, 4X memory -Disk seek 10,000,000 ns 10,000 us 10 ms 20x datacenter roundtrip -Read 1 MB sequentially from 1 Gbps 10,000,000 ns 10,000 us 10 ms 40x memory, 10X SSD -Read 1 MB sequentially from disk 30,000,000 ns 30,000 us 30 ms 120x memory, 30X SSD -Send packet CA->Netherlands->CA 150,000,000 ns 150,000 us 150 ms - -Notes ------ -1 ns = 10^-9 seconds -1 us = 10^-6 seconds = 1,000 ns -1 ms = 10^-3 seconds = 1,000 us = 1,000,000 ns -``` - -基于上述数字的指标: - -- 从磁盘以 30 MB/s 的速度顺序读取 -- 以 100 MB/s 从 1 Gbps 的以太网顺序读取 -- 从 SSD 以 1 GB/s 的速度读取 -- 以 4 GB/s 的速度从主存读取 -- 每秒能绕地球 6-7 圈 -- 数据中心内每秒有 2,000 次往返 - -#### 延迟数可视化 - -![](https://camo.githubusercontent.com/77f72259e1eb58596b564d1ad823af1853bc60a3/687474703a2f2f692e696d6775722e636f6d2f6b307431652e706e67) - -#### 来源及延伸阅读 - -- [每个程序员都应该知道的延迟数 — 1](https://gist.github.com/jboner/2841832) -- [每个程序员都应该知道的延迟数 — 2](https://gist.github.com/hellerbarde/2843375) -- [关于建设大型分布式系统的的设计方案、课程和建议](http://www.cs.cornell.edu/projects/ladis2009/talks/dean-keynote-ladis2009.pdf) -- [关于建设大型可拓展分布式系统的软件工程咨询](https://static.googleusercontent.com/media/research.google.com/en//people/jeff/stanford-295-talk.pdf) - -## 17. 资料 - -- [系统设计入门](https://github.com/donnemartin/system-design-primer/blob/master/README-zh-Hans.md#域名系统) diff --git "a/docs/architecture/\345\244\247\345\236\213\347\275\221\347\253\231\346\236\266\346\236\204\346\246\202\350\277\260.md" "b/docs/architecture/\345\244\247\345\236\213\347\275\221\347\253\231\346\236\266\346\236\204\346\246\202\350\277\260.md" deleted file mode 100644 index feed05ab..00000000 --- "a/docs/architecture/\345\244\247\345\236\213\347\275\221\347\253\231\346\236\266\346\236\204\346\246\202\350\277\260.md" +++ /dev/null @@ -1,339 +0,0 @@ -# 大型网站架构概述 - - - -- [1. 大型网站系统的特点](#1-大型网站系统的特点) -- [2. 大型网站架构演化历程](#2-大型网站架构演化历程) - - [2.1. 初始阶段架构](#21-初始阶段架构) - - [2.2. 应用服务和数据服务分离](#22-应用服务和数据服务分离) - - [2.3. 使用缓存改善性能](#23-使用缓存改善性能) - - [2.4. 使用应用服务器集群](#24-使用应用服务器集群) - - [2.5. 数据库读写分离](#25-数据库读写分离) - - [2.6. 反向代理和 CDN 加速](#26-反向代理和-cdn-加速) - - [2.7. 分布式文件系统和分布式数据库](#27-分布式文件系统和分布式数据库) - - [2.8. 使用 NoSQL 和搜索引擎](#28-使用-nosql-和搜索引擎) - - [2.9. 业务拆分](#29-业务拆分) - - [2.10. 分布式服务](#210-分布式服务) -- [3. 大型网站架构模式](#3-大型网站架构模式) - - [3.1. 分层](#31-分层) - - [3.2. 分割](#32-分割) - - [3.3. 分布式](#33-分布式) - - [3.4. 集群](#34-集群) - - [3.5. 缓存](#35-缓存) - - [3.6. 异步](#36-异步) - - [3.7. 冗余](#37-冗余) - - [3.8. 自动化](#38-自动化) - - [3.9. 安全](#39-安全) -- [4. 大型网站核心架构要素](#4-大型网站核心架构要素) - - [4.1. 性能](#41-性能) - - [4.2. 可用性](#42-可用性) - - [4.3. 伸缩性](#43-伸缩性) - - [4.4. 扩展性](#44-扩展性) - - [4.5. 安全性](#45-安全性) -- [5. 资料](#5-资料) - - - -## 1. 大型网站系统的特点 - -- 高并发、大流量 -- 高可用 -- 海量数据 -- 用户分布广泛,网络情况复杂 -- 安全环境恶劣 -- 需求快速变更,迭代频繁 -- 渐进式发展 - -## 2. 大型网站架构演化历程 - -### 2.1. 初始阶段架构 - -问题:网站运营初期,访问用户少,一台服务器绰绰有余。 - -特征:**应用程序、数据库、文件等所有的资源都在一台服务器上。** - -描述:通常服务器操作系统使用 linux,应用程序使用 PHP 开发,然后部署在 Apache 上,数据库使用 Mysql,通俗称为 LAMP。汇集各种免费开源软件以及一台廉价服务器就可以开始系统的发展之路了。 - -
- -
- -### 2.2. 应用服务和数据服务分离 - -问题:越来越多的用户访问导致性能越来越差,越来越多的数据导致存储空间不足,一台服务器已不足以支撑。 - -特征:**应用服务器、数据库服务器、文件服务器分别独立部署。** - -描述:三台服务器对性能要求各不相同:应用服务器要处理大量业务逻辑,因此需要更快更强大的 CPU;数据库服务器需要快速磁盘检索和数据缓存,因此需要更快的硬盘和更大的内存;文件服务器需要存储大量文件,因此需要更大容量的硬盘。 - -
- -
- -### 2.3. 使用缓存改善性能 - -问题:随着用户逐渐增多,数据库压力太大导致访问延迟。 - -特征:由于网站访问和财富分配一样遵循二八定律:80% 的业务访问集中在 20% 的数据上。**将数据库中访问较集中的少部分数据缓存在内存中,可以减少数据库的访问次数,降低数据库的访问压力。** - -描述:缓存分为两种:应用服务器上的本地缓存和分布式缓存服务器上的远程缓存,本地缓存访问速度更快,但缓存数据量有限,同时存在与应用程序争用内存的情况。分布式缓存可以采用集群方式,理论上可以做到不受内存容量限制的缓存服务。 - -
- -
- -### 2.4. 使用应用服务器集群 - -问题:使用缓存后,数据库访问压力得到有效缓解。但是单一应用服务器能够处理的请求连接有限,在访问高峰期,成为瓶颈。 - -特征:**多台服务器通过负载均衡同时向外部提供服务,解决单一服务器处理能力和存储空间不足的问题。** - -描述:使用集群是系统解决高并发、海量数据问题的常用手段。通过向集群中追加资源,提升系统的并发处理能力,使得服务器的负载压力不再成为整个系统的瓶颈。 - -
- -
- -### 2.5. 数据库读写分离 - -问题:网站使用缓存后,使绝大部分数据读操作访问都可以不通过数据库就能完成,但是仍有一部分读操作和全部的写操作需要访问数据库,在网站的用户达到一定规模后,数据库因为负载压力过高而成为网站的瓶颈。 - -特征:目前大部分的主流数据库都提供主从热备功能,通过配置两台数据库主从关系,可以将一台数据库服务器的数据更新同步到一台服务器上。网站利用数据库的这一功能,实现数据库读写分离,从而改善数据库负载压力。 - -描述:应用服务器在写操作的时候,访问主数据库,主数据库通过主从复制机制将数据更新同步到从数据库。这样当应用服务器在读操作的时候,访问从数据库获得数据。为了便于应用程序访问读写分离后的数据库,通常在应用服务器端使用专门的数据访问模块,使数据库读写分离的对应用透明。 - -
- -
- -### 2.6. 反向代理和 CDN 加速 - -问题:中国网络环境复杂,不同地区的用户访问网站时,速度差别也极大。 - -特征:**采用 CDN 和反向代理加快系统的访问速度。** - -描述:CDN 和反向代理的基本原理都是缓存,区别在于 CDN 部署在网络提供商的机房,使用户在请求网站服务时,可以从距离自己最近的网络提供商机房获取数据;而反向代理则部署在网站的中心机房,当用户请求到达中心机房后,首先访问的服务器时反向代理服务器,如果反向代理服务器中缓存着用户请求的资源,就将其直接返回给用户。 - -
- -
- -### 2.7. 分布式文件系统和分布式数据库 - -问题:随着大型网站业务持续增长,数据库经过读写分离,从一台服务器拆分为两台服务器,依然不能满足需求。 - -特征:**数据库采用分布式数据库,文件系统采用分布式文件系统。** - -描述:分布式数据库是数据库拆分的最后方法,只有在单表数据规模非常庞大的时候才使用。不到不得已时,更常用的数据库拆分手段是业务分库,将不同的业务数据库部署在不同的物理服务器上。 - -
- -
- -### 2.8. 使用 NoSQL 和搜索引擎 - -问题:随着网站业务越来越复杂,对数据存储和检索的需求也越来越复杂。 - -特征:**系统引入 NoSQL 数据库及搜索引擎。** - -描述:NoSQL 数据库及搜索引擎对可伸缩的分布式特性具有更好的支持。应用服务器通过统一数据访问模块访问各种数据,减轻应用程序管理诸多数据源的麻烦。 - -
- -
- -### 2.9. 业务拆分 - -问题:大型网站的业务场景日益复杂,分为多个产品线。 - -特征:采用分而治之的手段将整个网站业务分成不同的产品线。**系统上按照业务进行拆分改造,应用服务器按照业务区分进行分别部署。** - -描述:应用之间可以通过超链接建立关系,也可以通过消息队列进行数据分发,当然更多的还是通过访问同一个数据存储系统来构成一个关联的完整系统。 - -纵向拆分:将一个大应用拆分为多个小应用,如果新业务较为独立,那么就直接将其设计部署为一个独立的 Web 应用系统。纵向拆分相对较为简单,通过梳理业务,将较少相关的业务剥离即可。 - -横向拆分:将复用的业务拆分出来,独立部署为分布式服务,新增业务只需要调用这些分布式服务横向拆分需要识别可复用的业务,设计服务接口,规范服务依赖关系。 - -
- -
- -### 2.10. 分布式服务 - -问题:随着业务越拆越小,存储系统越来越庞大,应用系统整体复杂程度呈指数级上升,部署维护越来越困难。由于所有应用要和所有数据库系统连接,最终导致数据库连接资源不足,拒绝服务。 - -特征:**公共业务提取出来,独立部署。由这些可复用的业务连接数据库,通过分布式服务提供共用业务服务。** - -
- -
- -## 3. 大型网站架构模式 - -### 3.1. 分层 - -大型网站架构中常采用分层结构,将软件系统分为应用层、服务层、数据层: - -- **应用层** - 负责具体业务和视图展示。如网站首页及搜索输入和结果展示。 -- **服务层** - 为应用层提供服务支持。如用户管理服务、购物车服务等。 -- **应用层** - 提供数据存储访问服务。如数据库、缓存、文件、搜索引擎等。 - -分层架构的约束:禁止跨层次的调用(应用层直接调用数据层)及逆向调用(数据层调用服务层,或者服务层调用应用层)。 - -分层结构内部还可以继续分层,如应用可以再细分为视图层和业务逻辑层;服务层也可以细分为数据接口层和逻辑处理层。 - -### 3.2. 分割 - -将不同的功能和服务分割开来,包装成高内聚低耦合的模块单元。这有助于软件的开发和维护,便于不同模块的分布式部署,提高网站的并发处理能力和功能扩展能力。 - -### 3.3. 分布式 - -大于大型网站,分层和分割的一个主要目的是为了切分后的模块便于分布式部署,即将不同模块部署在不同的服务器上,通过远程调用协同工作。 - -分布式意味可以用更多的机器工作,那么 CPU、内存、存储资源也就更丰富,能够处理的并发访问和数据量就越大,进而能够为更多的用户提供服务。 - -分布式也引入了一些问题: - -- 服务调用必须通过网络,网络延迟会影响性能 -- 服务器越多,宕机概率也越大,是可用性降低 -- 数据一致性非常困难,分布式事务也难以保证 -- 网站依赖错综复杂,开发管理维护困难 - -常用的分布式方案: - -- 分布式应用和服务 -- 分布式静态资源 -- 分布式数据和存储 -- 分布式计算 - -### 3.4. 集群 - -集群即多台服务器部署相同应用构成一个集群,通过负载均衡设备共同对外提供服务。 - -集群需要具备伸缩性和故障转移机制:伸缩性是指可以根据用户访问量向集群添加或减少机器;故障转移是指,当某台机器出现故障时,负载均衡设备或失效转移机制将请求转发到集群中的其他机器上,从而不影响用户使用。 - -### 3.5. 缓存 - -缓存就是将数据存放在距离最近的位置以加快处理速度。缓存是改善软件性能的第一手段。 - -网站应用中,缓存除了可以加快数据访问速度以外,还可以减轻后端应用和数据存储的负载压力。 - -常见缓存手段: - -- CDN -- 反向代理 -- 本地缓存 -- 分布式缓存 - -使用缓存有两个前提: - -- 数据访问热点不均匀,频繁访问的数据应该放在缓存中 -- 数据在某个时间段有效,不过很快过期,否则缓存数据会因已经失效而产生脏读 - -### 3.6. 异步 - -软件发展的一个重要目标和驱动力是降低软件耦合性。事物之间直接关系越少,彼此影响就越小,也就更容易独立发展。 - -大型网站架构中,系统解耦的手段除了分层、分割、分布式等,还有一个重要手段——异步。 - -业务间的消息传递不是同步调用,儿时将一个业务操作拆分成多阶段,每个阶段间通过共享数据的方式异步执行进行协作。 - -- 在单一服务器内部可通过多线程共享内存队列的方式实现异步,处在业务操作前面的线程将操作输出到队列,后面的线程从队列中读取数据进行处理; -- 在分布式系统中,多个服务器集群通过分布式消息队列实现异步。 - -异步架构师典型的生产者消费模式,二者不存在直接调用。异步消息队列还有如下特性: - -- 提高系统可用性 -- 加快响应速度 -- 消除并发访问高峰 - -### 3.7. 冗余 - -大型网站,出现服务器宕机是必然事件。要保证部分服务器宕机的情况下网站依然可以继续服务,不丢失数据,就需要一定程度的服务器冗余运行,数据冗余备份。这样当某台服务器宕机是,可以将其上的服务和数据访问转移到其他机器上。 - -访问和负载很小的服务也必须部署 至少两台服务器构成一个集群,目的就是通过冗余实现服务高可用。数据除了定期备份,存档保存,实现 **冷备份** 外;为了保证在线业务高可用,还需要对数据库进行主从分离,实时同步实现 **热备份**。 - -为了抵御地震、海啸等不可抗因素导致的网站完全瘫痪,某些大型网站会对整个数据中心进行备份,全球范围内部署 **灾备数据中心**。网站程序和数据实时同步到多个灾备数据中心。 - -### 3.8. 自动化 - -大型网站架构的自动化架构设计主要集中在发布运维方面: - -- 发布过程自动化 - - 自动化代码管理 - - 自动化测试 - - 自动化安全监测 - - 自动化部署 -- 运维自动化 - - 自动化监控 - - 自动化报警 - - 自动化失效转移 - - 自动化失效恢复 - - 自动化降级 - - 自动化分配资源 - -### 3.9. 安全 - -- **密码** 和 **手机校验码** 进行身份认证 -- 登录、交易等重要操作需要对网络通信进行 **加密**,存储的敏感数据如用户信息等也进行加密处理 -- 防止机器人程序攻击网站,使用 **验证码** 进行识别 -- 对常见用于 **攻击** 网站的 XSS 攻击、SQL 注入、进行编码转换等相应处理 -- 对垃圾信息、敏感信息进行 **过滤** -- 对交易转账等重要操作根据交易模式和交易信息进行 **风险控制** - -## 4. 大型网站核心架构要素 - -**架构** 的一种通俗说法是:**最高层次的规划,难以改变的决定。** - -除了系统功能需求外,架构还需要关注以下架构要素: - -### 4.1. 性能 - -性能问题无处不在,所以网站性能优化手段也十分繁多: - -- 前端 - - 浏览器缓存 - - 静态资源压缩 - - 合理布局页面 - - 减少 cookie 传输 - - CDN -- 应用服务器 - - 本地缓存 - - 分布式缓存 - - 异步消息队列 - - 集群 - - 代码层面:使用多线程、改善内存管理 -- 数据库 - - 索引 - - 数据库缓存 - - SQL 优化 - -### 4.2. 可用性 - -可用性指部分服务器出现故障时,还能否对用户提供服务 - -- 冗余 - - 通过负载均衡设备建立集群共同对外提供服务 - - 数据存储在多台服务器,互相备份 -- 自动化:通过预发布验证、自动化测试、自动化发布、灰度发布等手段,减少将故障引入线上环境的可能 - -### 4.3. 伸缩性 - -衡量伸缩的标准就是是否可以用多台服务器构建集群,是否容易向集群中增删服务器节点。增删服务器节点后是否可以提供和之前无差别的服务。集群中可容纳的总服务器数是否有限制。 - -- 应用服务器集群 - 只要服务器上保存数据,则所有服务器都是对等的,通过负载均衡设备向集群中不断加入服务器即可 -- 缓存服务器集群 - 加入新的服务器可能会导致缓存路由失效,进而导致集群中的大部分缓存数据都无法访问。虽然缓存数据可以通过数据库重新加载,但是如果应用严重依赖缓存,可能会导致网站崩溃。需要改进缓存路由算法保证缓存数据的可访问性。 -- 关系型数据库集群 - 关系型数据库虽然支持数据复制,主从热备等机制,但是很难做到大规模集群的可伸缩性,因此关系型数据库的集群伸缩性方案必须在数据库之外实现,通过路由分区等手段将部署有多个数据库的服务器组成一个集群。 -- NOSql 数据库集群 - 由于先天就是为了应对海量数据而产生,因此对伸缩性的支持通常都非常好。 - -### 4.4. 扩展性 - -衡量扩展性的标准就是增加新的业务产品时,是否可以实现对现有产品透明无影响,不需要任何改动或很少改动,既有功能就可以上线新产品。主要手段有:事件驱动架构和分布式服务。 - -### 4.5. 安全性 - -安全性保护网站不受恶意攻击,保护网站重要数据不被窃取。 - -## 5. 资料 - -- [大型网站技术架构](https://item.jd.com/11322972.html) diff --git "a/docs/architecture/\347\275\221\347\253\231\347\232\204\344\274\270\347\274\251\346\200\247\346\236\266\346\236\204.md" "b/docs/architecture/\347\275\221\347\253\231\347\232\204\344\274\270\347\274\251\346\200\247\346\236\266\346\236\204.md" deleted file mode 100644 index 3b679d9d..00000000 --- "a/docs/architecture/\347\275\221\347\253\231\347\232\204\344\274\270\347\274\251\346\200\247\346\236\266\346\236\204.md" +++ /dev/null @@ -1,132 +0,0 @@ -# 网站的伸缩性架构 - - - -- [1. 网站架构的伸缩性设计](#1-网站架构的伸缩性设计) - - [1.1. 不同功能进行物理分离实现伸缩](#11-不同功能进行物理分离实现伸缩) - - [1.2. 单一功能通过集群规模实现伸缩](#12-单一功能通过集群规模实现伸缩) -- [2. 应用服务器集群的伸缩性设计](#2-应用服务器集群的伸缩性设计) - - [2.1. HTTP 重定向负载均衡](#21-http-重定向负载均衡) - - [2.2. DNS 域名解析负载均衡](#22-dns-域名解析负载均衡) - - [2.3. 反向代理负载均衡](#23-反向代理负载均衡) - - [2.4. IP 负载均衡](#24-ip-负载均衡) - - [2.5. 数据链路层负载均衡](#25-数据链路层负载均衡) - - [2.6. 负载均衡算法](#26-负载均衡算法) -- [3. 分布式缓存集群的伸缩性设计](#3-分布式缓存集群的伸缩性设计) -- [4. 数据存储服务器集群的伸缩性设计](#4-数据存储服务器集群的伸缩性设计) - - [4.1. 关系型数据库的伸缩性设计](#41-关系型数据库的伸缩性设计) - - [4.2. NoSql 数据库的伸缩性设计](#42-nosql-数据库的伸缩性设计) -- [5. 资料](#5-资料) - - - -## 1. 网站架构的伸缩性设计 - -### 1.1. 不同功能进行物理分离实现伸缩 - -纵向分离(分层后分离):将业务处理流程上的不同部分分离部署,实现系统伸缩性。 - -横向分离(业务分割后分离):将不同的业务模块分离部署,实现系统伸缩性。 - -### 1.2. 单一功能通过集群规模实现伸缩 - -将不同功能分离部署可以实现一定程度的伸缩性,但是随着网站的访问量逐步增加,即使分离到最小粒度的独立部署,单一的服务器也不能满足业务规模的要求。因此必须使用服务器集群,即将相同服务部署在多态服务器上构成一个集群整体对外提供服务。 - -## 2. 应用服务器集群的伸缩性设计 - -### 2.1. HTTP 重定向负载均衡 - -
- -
- -利用 HTTP 重定向协议实现负载均衡。 - -这种负载均衡方案的优点是比较简单。缺点是浏览器需要两次请求服务器才能完成一次访问,性能较差:重定向服务器自身的处理能力有可能成为瓶颈,整个集群的伸缩性规模有限;使用 HTTP 302 响应码重定向,可能使搜索引擎判断为 SEO 作弊,降低搜索排名。 - -### 2.2. DNS 域名解析负载均衡 - -
- -
- -利用 DNS 处理域名解析请求的同时进行负载均衡处理的一种方案。 - -在 DNS 服务器中配置多个 A 记录,如: - -``` -114.100.40.1 www.mysite.com -114.100.40.2 www.mysite.com -114.100.40.3 www.mysite.com -``` - -每次域名解析请求都会根据负载均衡算法计算一个不同的 IP 地址返回,这样 A 记录中配置的多个服务器就构成一个集群,并可以实现负载均衡。 - -DNS 域名解析负载均衡的优点: - -- 将负载均衡的工作转交给了 DNS,省掉了网站管理维护的麻烦。 -- 同时,许多 DNS 服务器还支持基于地理位置的域名解析,即将域名解析成距离用户地理最近的一个服务器地址,这样可以加快用户访问速度,改善性能。 - -DNS 域名解析负载均衡的缺点: - -- DNS 是多级解析,每一级 DNS 都可能缓存 A 记录,当某台服务器下线后,即使修改了 DNS 的 A 记录,要使其生效也需要较长时间。这段时间,依然会域名解析到已经下线的服务器,导致用户访问失败。 -- DNS 的负载均衡的控制权在域名服务商那里,网站无法对其做更多改善和更强大的管理。 - -### 2.3. 反向代理负载均衡 - -
- -
- -大多数反向代理服务器同时提供反向代理和负载均衡的功能。 - -反向代理服务器的优点是部署简单。缺点是反向代理服务器时所有请求和响应的中转站,其性能可能会成为瓶颈。 - -### 2.4. IP 负载均衡 - -
- -
- -在网络层通过修改请求目标地址进行负载均衡。负载均衡服务器(网关服务器)在操作系统内核获取网络数据包,根据负载均衡算法计算得到一台真实 Web 服务器 10.0.0.1,然后将目的 IP 地址修改为 10.0.0.1,不需要通过用户进程。真实 Web 服务器处理完成后,响应数据包回到负载均衡服务器,负载均衡服务器再将数据包原地址修改为自身的 IP 地址(114.100.80.10)发送给浏览器。 - -IP 负载均衡在内核完成数据分发,所以处理性能优于反向代理负载均衡。但是因为所有请求响应都要经过负载均衡服务器,集群的最大响应数据吞吐量受制于负载均衡服务器网卡带宽。 - -### 2.5. 数据链路层负载均衡 - -
- -
- -数据链路层负载均衡是指在通信协议的数据链路层修改 mac 地址进行负载均衡。 - -这种方式又称作三角传输方式,负载均衡数据分发过程中不修改 IP 地址,只修改目的 mac 地址,通过配置真实物理服务器集群所有机器虚拟 IP 和负载均衡服务器 IP 地址一致,从而达到不修改数据包的源地址和目的地址就可以进行数据分发的目的,由于实际处理请求的真实物理服务器 IP 和数据请求目的 IP 一致,不需要通过负载均衡服务器进行地址转换,可将响应数据包直接返回给用户浏览器,避免负载均衡服务器网卡带宽成为瓶颈。这种负载方式又称作直接路由方式。 - -在 Linux 平台上最好的链路层负载均衡开源产品是 LVS(Linux Virtual Server)。 - -### 2.6. 负载均衡算法 - -负载均衡服务器的实现可以分为两个部分: - -1. 根据负载均衡算法和 Web 服务器列表计算得到集群中一台 Web 服务器的地址。 -2. 将请求数据发送到该地址对应的 Web 服务器上。 - -负载均衡算法通常有以下几种: - -- **轮询(Round Robin)** - 所有请求被依次分发到每台应用服务器上,即每台服务器需要处理的请求数据都相同,适合于所有服务器硬件都相同的场景。 -- **加权轮询(Weighted Round Robin)** - 根据服务器硬件性能情况,在轮询的基础上,按照配置权重将请求分发到每个服务器,高性能服务器能分配更多请求。 -- **随机(Random)** - 请求被随机分配到各个应用服务器,在许多场合下,这种方案都很简单实用,因为好的随机数本身就很平均,即使应用服务器硬件配置不同,也可以使用加权随机算法。 -- **最少连接(Least Connection)** - 记录每个应用服务器正在处理的连接数,将新到的请求分发到最少连接的服务器上,应该说,这是最符合负载均衡定义的算法。 -- **源地址 Hash(Source Hash)** - 根据请求来源的 IP 地址进行 Hash 计算,得到应用服务器,这样来自同一个 IP 地址的请求总在同一个服务器上处理,该请求的上下文信息可以存储在这台服务器上,在一个会话周期内重复使用,从而实现会话粘滞。 - -## 3. 分布式缓存集群的伸缩性设计 - -## 4. 数据存储服务器集群的伸缩性设计 - -### 4.1. 关系型数据库的伸缩性设计 - -### 4.2. NoSql 数据库的伸缩性设计 - -## 5. 资料 - -- [大型网站技术架构](https://item.jd.com/11322972.html) diff --git "a/docs/architecture/\347\275\221\347\253\231\347\232\204\351\253\230\345\217\257\347\224\250\346\236\266\346\236\204.md" "b/docs/architecture/\347\275\221\347\253\231\347\232\204\351\253\230\345\217\257\347\224\250\346\236\266\346\236\204.md" deleted file mode 100644 index 0498f95b..00000000 --- "a/docs/architecture/\347\275\221\347\253\231\347\232\204\351\253\230\345\217\257\347\224\250\346\236\266\346\236\204.md" +++ /dev/null @@ -1,178 +0,0 @@ -# 网站的高可用架构 - - - -- [1. 网站可用性的度量](#1-网站可用性的度量) -- [2. 高可用的网站架构](#2-高可用的网站架构) -- [3. 高可用的应用](#3-高可用的应用) - - [3.1. 通过负载均衡进行无状态服务的失效转移](#31-通过负载均衡进行无状态服务的失效转移) - - [3.2. 应用服务器集群的 Session 管理](#32-应用服务器集群的-session-管理) -- [4. 高可用的服务](#4-高可用的服务) -- [5. 高可用的数据](#5-高可用的数据) - - [5.1. CAP 原理](#51-cap-原理) - - [5.2. 数据备份](#52-数据备份) - - [5.3. 失效转移](#53-失效转移) -- [6. 高可用网站的软件质量保证](#6-高可用网站的软件质量保证) -- [7. 网站监控](#7-网站监控) -- [8. 资料](#8-资料) - - - -## 1. 网站可用性的度量 - -网站不可用也被称作网站故障,业界通常用多个 9 来衡量网站的可用性。如 QQ 的可用性为 4 个 9,即 99.99% 可用。 - -``` -网站不可用时间 = 故障修复时间点 - 故障发现时间点 -网站年度可用性指标 = (1 - 网站不可用时间/年度总时间) * 100% -``` - -## 2. 高可用的网站架构 - -大型网站的分层架构及服务器的分布式部署使得位于不同层次的服务器具有不同的可用性特点。关闭服务或服务器宕机时产生的影响也不相同,高可用的解决方案也差异甚大。 - -## 3. 高可用的应用 - -### 3.1. 通过负载均衡进行无状态服务的失效转移 - -应用层主要处理网站应用的业务逻辑,一个显著的特点是应用的 **无状态** 性。 - -所谓的 **无状态** 的应用是指应用服务器不保存业务的上下文信息,而仅根据每次请求提交的数据进行相应的业务逻辑处理,多个服务实例之间完全对等,请求提交到任意服务器,处理结果都是完全一样的。 - -负载均衡,顾名思义,主要使用在业务量和数据量较高的情况下,当单台服务器不足以承担所有的负载压力时,通过负载均衡手段,将流量和数据分摊到一个集群组成的多台服务器上,以提高整体的负载处理能力。 - -### 3.2. 应用服务器集群的 Session 管理 - -应用服务器的高可用架构设计主要基于服务无状态这一特性。事实上,业务总是有状态的,如购物车记录用户的购买信息;用户的登录状态;最新发布的消息等等。 - -Web 应用中将这些多次请求修改使用的上下文对象称作会话。单机情况下,Session 可由部署在服务器上的 Web 容器管理。 - -而在集群环境下,Session 管理有以下手段: - -#### Session 复制 - -Session 复制是指应用服务器开发 Web 容器的 Session 复制功能,在集群中的几台服务器之间同步 Session 对象。 - -这种方案很简单,但当集群规模较大时,集群服务间需要大量的通信来进行 Session 复制。 - -#### Session 绑定 - -可以利用负载均衡的源地址 Hash 算法实现,总是将来源于同一 IP 的请求分发到同一台服务器上。这样在整个会话期间,用户所有的请求都在同一台服务器上处理,即 Session 绑定到某台特定服务器上。这种方法又被称作会话粘滞。 - -但是这种策略不符合高可用的需求,因为一旦某台服务器宕机,那么该机器上的 Session 也就不复存在了。 - -#### 利用 Cookie 记录 Session - -可以将 Session 记录在客户端(浏览器 Cookie),每次请求服务器时,将 Session 放在请求中发送给服务器,服务器处理完请求后再将修改过的 Session 响应给客户端。 - -这种策略的缺点是:Cookie 有大小限制,能记录的信息有限;每次请求响应都需要传输 Cookie,影响性能;如果用户关闭 Cookie,访问就不能工作。 - -#### Session 服务器 - -利用独立部署的 Session 服务器(集群)统一管理 Session,应用服务器每次读写 Session 时,都访问 Session 服务器。 - -实现 Session 服务器的一种简单方法时:利用分布式缓存、数据库等,在此基础上进行包装,使其符合 Session 的存储和访问要求。 - -## 4. 高可用的服务 - -高可用的服务策略: - -- 分级管理 - 将服务根据业务重要性进行分级管理,并在服务部署上进行隔离。 -- 超时设置 - 由于服务器宕机、线程死锁等原因,可能导致应用程序对服务端的调用失去响应。所以有必要引入超时机制,一旦调用超时,服务化框架抛出异常,应用程序根据服务调度策略,选择重试或请求转移到其他机器上。 -- 异步调用 - 对于需要即时响应的业务,应用在调用服务时可以通过消息队列等异步方式完成,避免长时间等待服务响应结果。 -- 服务降级 - 网站访问高峰期,服务可能因为大量并发调用而性能下降,严重时可能会导致宕机。为了保证核心功能的正常运行,需要对服务进行降级。降级有两种手段:拒绝服务和关闭服务。 -- 幂等性设计 - 为了避免服务重复调用,可以通过设置编号的方式进行服务调用有效性校验,有效的操作才能继续执行。 - -## 5. 高可用的数据 - -### 5.1. CAP 原理 - -分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。 - -
- -
- -#### 可用性 - -可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。 - -在可用性条件下,系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。 - -#### 分区容忍性 - -网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。 - -在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。 - -#### 一致性 - -一致性指的是多个数据副本是否能保持一致的特性。 - -在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。 - -数据一致性又可以分为以下几点: - -- 强一致性 - 数据更新操作结果和操作响应总是一致的,即操作响应通知更新失败,那么数据一定没有被更新,而不是处于不确定状态。 -- 最终一致性 - 即物理存储的数据可能是不一致的,终端用户访问到的数据可能也是不一致的,但系统经过一段时间的自我修复和修正,数据最终会达到一致。 - -#### 权衡 - -在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际在是要在可用性和一致性之间做权衡。 - -可用性和一致性往往是冲突的,很难都使它们同时满足。在多个节点之间进行数据同步时, - -- 为了保证一致性(CP),就需要让所有节点下线成为不可用的状态,等待同步完成; -- 为了保证可用性(AP),在同步过程中允许读取所有节点的数据,但是数据可能不一致。 - -### 5.2. 数据备份 - -- 冷备份:定期将数据复制到某种存储介质。 -- 热备份 - - 异步热备方式 - 异步方式是指多份数据副本的写入操作异步完成,应用程序收到数据服务系统的写操作成功响应时,只写成功了一份,存储系统将会异步地写其他副本。 - - 同步热备方式 - 同步方式是指多份数据副本的写入操作同步完成,即应用程序收到数据服务系统的写成功响应时,多份数据都已经写操作成功。但是当应用程序收到数据写操作失败的响应式,可能有部分副本或者全部副本都已经写入成功了(因为网络或者系统故障,无法返回操作成功的响应)。 - -### 5.3. 失效转移 - -#### 失效确认 - -
- -
- -判断服务器宕机的手段有两种:心跳检测和访问失败报告。 - -对于应用程序的访问失败报告,控制中心还需要再一次发送心跳检测进行确认,以免错误判断服务器宕机。因为一旦进行数据访问的失效转移,意味着数据存储多份副本不一致,需要进行后续一系列的复杂动作。 - -#### 访问转移 - -确认某台数据服务器宕机后,就需要将数据读写访问重新路由到其他服务器上。对于完全对等存储的服务器,当其中一台宕机后,应用程序根据配置直接切换到对等服务器上。如果存储不对等,就需要重新计算路由,选择存储服务器。 - -#### 数据恢复 - -因为某台服务器宕机,所以数据存储的副本数目会减少,必须将副本的数目恢复到系统设定的值,否则,再有服务器宕机时,就可能出现无法访问转移,数据永久丢失的情况。因此系统需要从健康的服务器复制数据,将数据副本数目恢复到设定值。 - -## 6. 高可用网站的软件质量保证 - -高可用网站的软件质量保证的手段: - -- 自动化发布 -- 自动化测试 -- 预发布验证 -- 代码控制 -- 灰度发布 - -## 7. 网站监控 - -- 监控数据采集 - - 用户行为日志收集 - - 服务器性能监控 - - 运行数据报告 -- 监控管理 - - 系统报警 - - 失效转移 - - 自动优雅降级 - -## 8. 资料 - -- [大型网站技术架构](https://item.jd.com/11322972.html) diff --git "a/docs/architecture/\347\275\221\347\253\231\347\232\204\351\253\230\346\200\247\350\203\275\346\236\266\346\236\204.md" "b/docs/architecture/\347\275\221\347\253\231\347\232\204\351\253\230\346\200\247\350\203\275\346\236\266\346\236\204.md" deleted file mode 100644 index fc241af5..00000000 --- "a/docs/architecture/\347\275\221\347\253\231\347\232\204\351\253\230\346\200\247\350\203\275\346\236\266\346\236\204.md" +++ /dev/null @@ -1,189 +0,0 @@ -# 网站的高性能架构 - - - -- [1. 性能测试](#1-性能测试) - - [1.1. 性能指标](#11-性能指标) - - [1.2. 性能测试方法](#12-性能测试方法) - - [1.3. 性能测试报告](#13-性能测试报告) - - [1.4. 性能优化策略](#14-性能优化策略) -- [2. 前端性能优化](#2-前端性能优化) - - [2.1. 浏览器访问优化](#21-浏览器访问优化) - - [2.2. CDN](#22-cdn) - - [2.3. 反向代理](#23-反向代理) -- [3. 应用服务性能优化](#3-应用服务性能优化) - - [3.1. 分布式缓存](#31-分布式缓存) - - [3.2. 异步操作](#32-异步操作) - - [3.3. 使用集群](#33-使用集群) - - [3.4. 代码优化](#34-代码优化) -- [4. 存储性能优化](#4-存储性能优化) - - [4.1. 机械键盘和固态硬盘](#41-机械键盘和固态硬盘) - - [4.2. B+数和 LSM 树](#42-b数和-lsm-树) - - [4.3. RAID 和 HDFS](#43-raid-和-hdfs) -- [5. 资料](#5-资料) - - - -## 1. 性能测试 - -### 1.1. 性能指标 - -网站性能测试的主要指标有: - -- 响应时间 - 响应时间(RT)是指从客户端发一个请求开始计时,到客户端接收到从服务器端返回的响应结果结束所经历的时间,响应时间由请求发送时间、网络传输时间和服务器处理时间三部分组成。 -- 并发数 - 系统同时处理的请求、事务数。 -- 吞吐量 - TPS(每秒事务数)、HPS(每秒 HTTP 请求数)、QPS(每秒查询数)。 -- 性能计数器 - 系统负载、对象与线程数、内存使用、CPU 使用、磁盘与网络 IO 等。这些指标也是系统监控的重要参数。 - -### 1.2. 性能测试方法 - -- 性能测试 -- 负载测试 -- 压力测试 -- 稳定性测试 - -### 1.3. 性能测试报告 - -性能测试报告示例: - -
- -
- -### 1.4. 性能优化策略 - -1. 性能分析 - 如果请求响应慢,存在性能问题。需要对请求经历的各个环节逐一分析,排查可能出现性能瓶颈的地方,定位问题。检查监控数据,分析影响性能的主要因素:内存、磁盘、网络、CPU,可能是代码或架构设计不合理,又或者是系统资源确实不足。 -2. 性能优化 - 性能优化根据网站分层架构,大致可分为前端性能优化、应用服务性能优化、存储服务性能优化。 - -## 2. 前端性能优化 - -### 2.1. 浏览器访问优化 - -1. 减少 HTTP 请求 - HTTP 请求需要建立通信链路,进行数据传输,开销高昂,所以减少 HTTP 请求数可以有效提高访问性能。减少 HTTP 的主要手段是合并 Css、JavaScript、图片。 -2. 使用浏览器缓存 - 因为静态资源文件更新频率低,可以缓存浏览器中以提高性能。设置 HTTP 头中的 Cache-Control 和 Expires 属性,可以设定浏览器缓存。 -3. 启用压缩 - 在服务器端压缩静态资源文件,在浏览器端解压缩,可以有效减少传输的数据量。由于文本文件压缩率可达 80% 以上,所以可以对静态资源,如 Html、Css、JavaScrip 进行压缩。 -4. CSS 放在页面最上面,JavaScript 放在页面最下面 - 浏览器会在下载完全部的 Css 后才对整个页面进行渲染,所以最好的做法是将 Css 放在页面最上面,让浏览器尽快下载 Css;JavaScript 则相反,浏览器加载 JavaScript 后立即执行,可能会阻塞整个页面,造成页面显示缓慢,因此 JavaScript 最好放在页面最下面。 -5. 减少 Cookie 传输 - Cookie 包含在 HTTP 每次的请求和响应中,太大的 Cookie 会严重影响数据传输。 - -### 2.2. CDN - -CDN 一般缓存的是静态资源。 - -CDN 的本质仍然是一个缓存,而且将数据缓存在离用户最近的地方,使用户已最快速度获取数据,即所谓网络访问第一跳。 - -
- -
- -### 2.3. 反向代理 - -传统代理服务器位于浏览器一侧,代理浏览器将 HTTP 请求发送到互联网上,而反向代理服务器位于网站机房一侧,代理网站服务器接收 HTTP 请求。 - -
- -
- -反向代理服务器可以配置缓存功能加速 Web 请求,当用户第一次访问静态内容时,静态内容就会被缓存在反向代理服务器上。 - -反向代理还可以实现负载均衡,通过负载均衡构建的集群可以提高系统总体处理能力。 - -因为所有请求都必须先经过反向代理服务器,所以可以屏蔽一些攻击 IP,达到保护网站安全的作用。 - -## 3. 应用服务性能优化 - -### 3.1. 分布式缓存 - -网站性能优化第一定律:优先考虑使用缓存优化性能。 - -#### 缓存原理 - -缓存指将数据存储在相对较高访问速度的存储介质中,以供系统处理。一方面缓存访问速度快,可以减少数据访问的时间,另一方面如果缓存的数据是经过计算处理得到的,那么被缓存的数据无需重复计算即可直接使用,因此缓存还起到减少计算时间的作用。 - -缓存的本质是一个内存 HASH 表。 - -缓存主要用来存放那些读写比很高、很少变化的数据,如商品的类目信息,热门词的搜索列表信息、热门商品信息等。 - -#### 合理使用缓存 - -缓存数据的选择: - -- 不要存储频繁修改的数据 -- 不要存储非热点数据 - -数据不一致和脏读: - -- 缓存有有效期,所以存在一定时间的数据不一致和脏读问题。如果不能接受,可以考虑使用数据更新立即更新缓存策略 - -需要考虑缓存问题:缓存雪崩、缓存穿透、缓存预热 - -### 3.2. 异步操作 - -异步处理一般是通过分布式消息队列的方式。 - -异步处理可以解决一下问题: - -- 异步处理 -- 应用解耦 -- 流量削锋 -- 日志处理 -- 消息通讯 - -### 3.3. 使用集群 - -在高并发场景下,使用负载均衡技术为一个应用构建一个由多台服务器组成的服务器集群,将并发访问请求分发到多台服务器上处理,避免单一服务器因负载压力过大而响应缓慢,使用户请求具有更好的响应延迟特性。 - -### 3.4. 代码优化 - -#### 多线程 - -从资源利用的角度看,使用多线程的原因主要有两个:IO 阻塞和多 CPU。 - -线程数并非越多越好,那么启动多少线程合适呢? - -有个参考公式: - -``` -启动线程数 = (任务执行时间 / (任务执行时间 - IO 等待时间)) * CPU 内核数 -``` - -最佳启动线程数和 CPU 内核数成正比,和 IO 阻塞时间成反比。如果任务都是 CPU 计算型任务,那么线程数最多不要超过 CPU 内核数,因为启动再多线程,CPU 也来不及调度;相反如果是任务需要等待磁盘操作,网络响应,那么多启动线程有助于任务并罚赌,提高系统吞吐量。 - -##### 线程安全问题 - -- 将对象设计为无状态对象 -- 使用局部对象 -- 并发访问资源时使用锁 - -#### 资源复用 - -应该尽量减少那些开销很大的系统资源的创建和销毁,如数据库连接、网络通信连接、线程、复杂对象等。从编程角度,资源复用主要有两种模式:单例模式和对象池。 - -#### 数据结构 - -根据具体场景,选择合适的数据结构。 - -#### 垃圾回收 - -如果 Web 应用运行在 JVM 等具有垃圾回收功能的环境中,那么垃圾回收可能会对系统的性能特性产生巨大影响。立即垃圾回收机制有助于程序优化和参数调优,以及编写内存安全的代码。 - -## 4. 存储性能优化 - -### 4.1. 机械键盘和固态硬盘 - -考虑使用固态硬盘替代机械键盘,因为它的读写速度更快。 - -### 4.2. B+数和 LSM 树 - -传统关系数据库的数据库索引一般都使用两级索引的 B+ 树结构,树的层次最多三层。因此可能需要 5 次磁盘访问才能更新一条记录(三次磁盘访问获得数据索引及行 ID,然后再进行一次数据文件读操作及一次数据文件写操作)。 - -由于磁盘访问是随机的,传统机械键盘在数据随机访问时性能较差,每次数据访问都需要多次访问磁盘影响数据访问性能。 - -许多 Nosql 数据库中的索引采用 LSM 树作为主要数据结构。LSM 树可视为一个 N 阶合并树。数据写操作都在内存中进行。在 LSM 树上进行一次数据更新不需要磁盘访问,速度远快于 B+ 树。 - -### 4.3. RAID 和 HDFS - -HDFS(分布式文件系统) 更被大型网站所青睐。它可以配合 MapReduce 并发计算任务框架进行大数据处理,可以在整个集群上并发访问所有磁盘,无需 RAID 支持。 - -## 5. 资料 - -- [大型网站技术架构](https://item.jd.com/11322972.html) diff --git a/docs/cache/README.md b/docs/cache/README.md new file mode 100644 index 00000000..60a94fb1 --- /dev/null +++ b/docs/cache/README.md @@ -0,0 +1,36 @@ +# Java 和缓存 + +> 缓存可以说是优化系统性能的第一手段,在各种技术中都会有缓存的应用。 +> +> 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/cache.md),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200710163555.png) + +## 📖 内容 + +- [缓存面试题](cache-interview.md) 💯 +- [缓存基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/cache.md) +- [Java 缓存框架](cache-framework.md) - 关键词:Spring Cache、J2Cache、JetCache +- [Redis 教程](https://dunwu.github.io/db-tutorial/nosql/redis/) 📚 +- [Memcached 应用指南](memcached.md) +- [Java 缓存库](cache-libs.md) - 关键词:ConcurrentHashMap、LRUHashMap、Guava Cache、Caffeine、Ehcache +- [Ehcache 应用指南](ehcache.md) +- [Http 缓存](http-cache.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/) + +## 🚪 传送 + +◾ 🏠 [JAVATECH 首页](https://github.com/dunwu/javatech) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ diff --git a/docs/cache/cache-framework.md b/docs/cache/cache-framework.md new file mode 100644 index 00000000..e42445ae --- /dev/null +++ b/docs/cache/cache-framework.md @@ -0,0 +1,341 @@ +# Java 缓存框架 + +> 关键词:Spring Cache、J2Cache、JetCache + + + +- [一 、JSR 107](#一-jsr-107) +- [二、Spring Cache](#二spring-cache) + - [开启缓存注解](#开启缓存注解) + - [spring 缓存注解 API](#spring-缓存注解-api) +- [三、Spring Boot Cache](#三spring-boot-cache) + - [Spring Boot Cache 快速入门](#spring-boot-cache-快速入门) +- [四、JetCache](#四jetcache) + - [jetcache 快速入门](#jetcache-快速入门) +- [五、j2cache](#五j2cache) +- [六、总结](#六总结) +- [参考资料](#参考资料) + + + +## 一 、JSR 107 + +[JSR107](https://www.jcp.org/en/jsr/detail?id=107) 中制订了 Java 缓存的规范。 + +因此,在很多缓存框架、缓存库中,其 API 都参考了 JSR 107 规范。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/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) diff --git a/docs/cache/cache-interview.md b/docs/cache/cache-interview.md new file mode 100644 index 00000000..cb619713 --- /dev/null +++ b/docs/cache/cache-interview.md @@ -0,0 +1,571 @@ +# 缓存夺命连环问 + +## 为什么要用缓存? + +用缓存,主要有两个用途:**高性能**、**高并发**。 + +### 高性能 + +假设这么个场景,你有个操作,一个请求过来,吭哧吭哧你各种乱七八糟操作 mysql,半天查出来一个结果,耗时 600ms。但是这个结果可能接下来几个小时都不会变了,或者变了也可以不用立即反馈给用户。那么此时咋办? + +缓存啊,折腾 600ms 查出来的结果,扔缓存里,一个 key 对应一个 value,下次再有人查,别走 mysql 折腾 600ms 了,直接从缓存里,通过一个 key 查出来一个 value,2ms 搞定。性能提升 300 倍。 + +就是说对于一些需要复杂操作耗时查出来的结果,且确定后面不怎么变化,但是有很多读请求,那么直接将查询出来的结果放在缓存中,后面直接读缓存就好。 + +### 高并发 + +mysql 这么重的数据库,压根儿设计不是让你玩儿高并发的,虽然也可以玩儿,但是天然支持不好。mysql 单机支撑到 `2000QPS` 也开始容易报警了。 + +所以要是你有个系统,高峰期一秒钟过来的请求有 1万,那一个 mysql 单机绝对会死掉。你这个时候就只能上缓存,把很多数据放缓存,别放 mysql。缓存功能简单,说白了就是 `key-value` 式操作,单机支撑的并发量轻松一秒几万十几万,支撑高并发 so easy。单机承载并发量是 mysql 单机的几十倍。 + +> 缓存是走内存的,内存天然就支撑高并发。 + +## 用了缓存之后会有什么不良后果? + +常见的缓存问题有以下几个: + +- [缓存与数据库双写不一致](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-consistence.md) +- [缓存雪崩、缓存穿透](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-caching-avalanche-and-caching-penetration.md) +- [缓存并发竞争](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-cas.md) + +后面再详细说明。 + +## redis 和 memcached 有啥区别? + +redis 支持复杂的数据结构 + +redis 相比 memcached 来说,拥有[更多的数据结构](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-data-types.md),能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作, redis 会是不错的选择。 + +redis 原生支持集群模式 + +在 redis3.x 版本中,便能支持 cluster 模式,而 memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。 + +性能对比 + +由于 redis 只使用**单核**,而 memcached 可以使用**多核**,所以平均每一个核上 redis 在存储小数据时比 memcached 性能更高。而在 100k 以上的数据中,memcached 性能要高于 redis。虽然 redis 最近也在存储大数据的性能上进行优化,但是比起 memcached,还是稍有逊色。 + +## 为啥 redis 单线程模型也能效率这么高? + +### redis 的线程模型 + +redis 内部使用文件事件处理器 `file event handler`,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。 + +文件事件处理器的结构包含 4 个部分: + +- 多个 socket +- IO 多路复用程序 +- 文件事件分派器 +- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器) + +多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理。 + +来看客户端与 redis 的一次通信过程: + +![img](https://github.com/doocs/advanced-java/blob/master/images/redis-single-thread-model.png) + +要明白,通信是通过 socket 来完成的,不懂的同学可以先去看一看 socket 网络编程。 + +首先,redis 服务端进程初始化的时候,会将 server socket 的 `AE_READABLE` 事件与连接应答处理器关联。 + +客户端 socket01 向 redis 进程的 server socket 请求建立连接,此时 server socket 会产生一个 `AE_READABLE` 事件,IO 多路复用程序监听到 server socket 产生的事件后,将该 socket 压入队列中。文件事件分派器从队列中获取 socket,交给**连接应答处理器**。连接应答处理器会创建一个能与客户端通信的 socket01,并将该 socket01 的 `AE_READABLE` 事件与命令请求处理器关联。 + +假设此时客户端发送了一个 `set key value` 请求,此时 redis 中的 socket01 会产生 `AE_READABLE` 事件,IO 多路复用程序将 socket01 压入队列,此时事件分派器从队列中获取到 socket01 产生的 `AE_READABLE` 事件,由于前面 socket01 的 `AE_READABLE` 事件已经与命令请求处理器关联,因此事件分派器将事件交给命令请求处理器来处理。命令请求处理器读取 socket01 的 `key value` 并在自己内存中完成 `key value` 的设置。操作完成后,它会将 socket01 的 `AE_WRITABLE` 事件与命令回复处理器关联。 + +如果此时客户端准备好接收返回结果了,那么 redis 中的 socket01 会产生一个 `AE_WRITABLE` 事件,同样压入队列中,事件分派器找到相关联的命令回复处理器,由命令回复处理器对 socket01 输入本次操作的一个结果,比如 `ok`,之后解除 socket01 的 `AE_WRITABLE` 事件与命令回复处理器的关联。 + +这样便完成了一次通信。 + +### 为啥 redis 单线程模型也能效率这么高? + +- 纯内存操作。 +- 核心是基于非阻塞的 IO 多路复用机制。 +- C 语言实现,一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。 +- 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。 + +## Redis 有哪些数据类型 + +redis 主要有以下几种数据类型: + +- string +- hash +- list +- set +- sorted set + +### string + +这是最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。 + +``` +set college szu +``` + +### hash + +这个是类似 map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是**这个对象没嵌套其他的对象**)给缓存在 redis 里,然后每次读写缓存的时候,可以就操作 hash 里的**某个字段**。 + +```bash +hset person name bingo +hset person age 20 +hset person id 1 +hget person name +person = { + "name": "bingo", + "age": 20, + "id": 1 +} +``` + +### list + +list 是有序列表,这个可以玩儿出很多花样。 + +比如可以通过 list 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。 + +比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 list 实现分页查询,这个是很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。 + +``` +# 0开始位置,-1结束位置,结束位置为-1时,表示列表的最后一个位置,即查看所有。 +lrange mylist 0 -1 +``` + +比如可以搞个简单的消息队列,从 list 头怼进去,从 list 尾巴那里弄出来。 + +``` +lpush mylist 1 +lpush mylist 2 +lpush mylist 3 4 5 + +# 1 +rpop mylist +``` + +### set + +set 是无序集合,自动去重。 + +直接基于 set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 jvm 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于 redis 进行全局的 set 去重。 + +可以基于 set 玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁?对吧。 + +把两个大 V 的粉丝都放在两个 set 中,对两个 set 做交集。 + +``` +#-------操作一个set------- +# 添加元素 +sadd mySet 1 + +# 查看全部元素 +smembers mySet + +# 判断是否包含某个值 +sismember mySet 3 + +# 删除某个/些元素 +srem mySet 1 +srem mySet 2 4 + +# 查看元素个数 +scard mySet + +# 随机删除一个元素 +spop mySet + +#-------操作多个set------- +# 将一个set的元素移动到另外一个set +smove yourSet mySet 2 + +# 求两set的交集 +sinter yourSet mySet + +# 求两set的并集 +sunion yourSet mySet + +# 求在yourSet中而不在mySet中的元素 +sdiff yourSet mySet +``` + +### sorted set + +sorted set 是排序的 set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。 + +``` +zadd board 85 zhangsan +zadd board 72 lisi +zadd board 96 wangwu +zadd board 63 zhaoliu + +# 获取排名前三的用户(默认是升序,所以需要 rev 改为降序) +zrevrange board 0 3 + +# 获取某用户的排名 +zrank board zhaoliu +``` + +## 如何保证 redis 的高并发和高可用?redis 的主从复制原理能介绍一下么?redis 的哨兵原理能介绍一下么? + +如果你用 redis 缓存技术的话,肯定要考虑如何用 redis 来加多台机器,保证 redis 是高并发的,还有就是如何让 redis 保证自己不是挂掉以后就直接死掉了,即 redis 高可用。 + +由于此节内容较多,因此,会分为两个小节进行讲解。 + +- [redis 主从架构](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-master-slave.md) +- [redis 基于哨兵实现高可用](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-sentinel.md) + +redis 实现**高并发**主要依靠**主从架构**,一主多从,一般来说,很多项目其实就足够了,单主用来写入数据,单机几万 QPS,多从用来查询数据,多个从实例可以提供每秒 10w 的 QPS。 + +如果想要在实现高并发的同时,容纳大量的数据,那么就需要 redis 集群,使用 redis 集群之后,可以提供每秒几十万的读写并发。 + +redis 高可用,如果是做主从架构部署,那么加上哨兵就可以了,就可以实现,任何一个实例宕机,可以进行主备切换。 + +## redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现? + +### redis 过期策略 + +redis 过期策略是:**定期删除+惰性删除**。 + +所谓**定期删除**,指的是 redis 默认是每隔 100ms 就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。 + +假设 redis 里放了 10w 个 key,都设置了过期时间,你每隔几百毫秒,就检查 10w 个 key,那 redis 基本上就死了,cpu 负载会很高的,消耗在你的检查过期 key 上了。注意,这里可不是每隔 100ms 就遍历所有的设置过期时间的 key,那样就是一场性能上的**灾难**。实际上 redis 是每隔 100ms **随机抽取**一些 key 来检查和删除的。 + +但是问题是,定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个 key 的时候,redis 会检查一下 ,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。 + +> 获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西。 + +但是实际上这还是有问题的,如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽了,咋整? + +答案是:**走内存淘汰机制**。 + +### 内存淘汰机制 + +redis 内存淘汰机制有以下几个: + +- noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。 +- **allkeys-lru**:当内存不足以容纳新写入数据时,在**键空间**中,移除最近最少使用的 key(这个是**最常用**的)。 +- allkeys-random:当内存不足以容纳新写入数据时,在**键空间**中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。 +- volatile-lru:当内存不足以容纳新写入数据时,在**设置了过期时间的键空间**中,移除最近最少使用的 key(这个一般不太合适)。 +- volatile-random:当内存不足以容纳新写入数据时,在**设置了过期时间的键空间**中,**随机移除**某个 key。 +- volatile-ttl:当内存不足以容纳新写入数据时,在**设置了过期时间的键空间**中,有**更早过期时间**的 key 优先移除。 + +### 手写一个 LRU 算法 + +你可以现场手写最原始的 LRU 算法,那个代码量太大了,似乎不太现实。 + +不求自己纯手工从底层开始打造出自己的 LRU,但是起码要知道如何利用已有的 JDK 数据结构实现一个 Java 版的 LRU。 + +```java +class LRUCache extends LinkedHashMap { + private final int CACHE_SIZE; + + /** + * 传递进来最多能缓存多少数据 + * + * @param cacheSize 缓存大小 + */ + public LRUCache(int cacheSize) { + // true 表示让 linkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。 + super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true); + CACHE_SIZE = cacheSize; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + // 当 map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。 + return size() > CACHE_SIZE; + } +} +``` + +## redis 集群模式的工作原理能说一下么?在集群模式下,redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗? + +在前几年,redis 如果要搞几个节点,每个节点存储一部分的数据,得**借助一些中间件**来实现,比如说有 `codis`,或者 `twemproxy`,都有。有一些 redis 中间件,你读写 redis 中间件,redis 中间件负责将你的数据分布式存储在多台机器上的 redis 实例中。 + +这两年,redis 不断在发展,redis 也不断有新的版本,现在的 redis 集群模式,可以做到在多台机器上,部署多个 redis 实例,每个实例存储一部分的数据,同时每个 redis 主实例可以挂 redis 从实例,自动确保说,如果 redis 主实例挂了,会自动切换到 redis 从实例上来。 + +现在 redis 的新版本,大家都是用 redis cluster 的,也就是 redis 原生支持的 redis 集群模式,那么面试官肯定会就 redis cluster 对你来个几连炮。要是你没用过 redis cluster,正常,以前很多人用 codis 之类的客户端来支持集群,但是起码你得研究一下 redis cluster 吧。 + +如果你的数据量很少,主要是承载高并发高性能的场景,比如你的缓存一般就几个 G,单机就足够了,可以使用 replication,一个 master 多个 slaves,要几个 slave 跟你要求的读吞吐量有关,然后自己搭建一个 sentinel 集群去保证 redis 主从架构的高可用性。 + +redis cluster,主要是针对**海量数据+高并发+高可用**的场景。redis cluster 支撑 N 个 redis master node,每个 master node 都可以挂载多个 slave node。这样整个 redis 就可以横向扩容了。如果你要支撑更大数据量的缓存,那就横向扩容更多的 master 节点,每个 master 节点就能存放更多的数据了。 + +### 面试题剖析 + +### redis cluster 介绍 + +- 自动将数据进行分片,每个 master 上放一部分数据 +- 提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的 + +在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。 + +16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议,`gossip` 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。 + +### 节点间的内部通信机制 + +#### 基本通信原理 + +集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通信。 + +**集中式**是将集群元数据(节点信息、故障等等)几种存储在某个节点上。集中式元数据集中存储的一个典型代表,就是大数据领域的 `storm`。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于 zookeeper(分布式协调的中间件)对所有元数据进行存储维护。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/zookeeper-centralized-storage.png) + +redis 维护集群元数据采用另一个方式, `gossip` 协议,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/redis-gossip.png) + +**集中式**的**好处**在于,元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到;**不好**在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力。 + +gossip 好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力;不好在于,元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。 + +- 10000 端口:每个节点都有一个专门用于节点间通信的端口,就是自己提供服务的端口号+10000,比如 7001,那么用于节点间通信的就是 17001 端口。每个节点每隔一段时间都会往另外几个节点发送 `ping` 消息,同时其它几个节点接收到 `ping` 之后返回 `pong`。 +- 交换的信息:信息包括故障信息,节点的增加和删除,hash slot 信息等等。 + +#### gossip 协议 + +gossip 协议包含多种消息,包含 `ping`,`pong`,`meet`,`fail` 等等。 + +- meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。 + +``` +redis-trib.rb add-node +``` + +其实内部就是发送了一个 gossip meet 消息给新加入的节点,通知那个节点去加入我们的集群。 + +- ping:每个节点都会频繁给其它节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据。 +- pong:返回 ping 和 meeet,包含自己的状态和其它信息,也用于信息广播和更新。 +- fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机啦。 + +#### ping 消息深入 + +ping 时要携带一些元数据,如果很频繁,可能会加重网络负担。 + +每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了 `cluster_node_timeout / 2`,那么立即发送 ping,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 `cluster_node_timeout` 可以调节,如果调得比较大,那么会降低 ping 的频率。 + +每次 ping,会带上自己节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 `3` 个其它节点的信息,最多包含 `总节点数减 2` 个其它节点的信息。 + +### 分布式寻址算法 + +- hash 算法(大量缓存重建) +- 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡) +- redis cluster 的 hash slot 算法 + +#### hash 算法 + +来了一个 key,首先计算 hash 值,然后对节点数取模。然后打在不同的 master 节点上。一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致**大部分的请求过来,全部无法拿到有效的缓存**,导致大量的流量涌入数据库。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/hash.png) + +#### 一致性 hash 算法 + +一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。 + +来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环**顺时针“行走”**,遇到的第一个 master 节点就是 key 所在位置。 + +在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。 + +燃鹅,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成**缓存热点**的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/consistent-hashing-algorithm.png) + +#### redis cluster 的 hash slot 算法 + +redis cluster 有固定的 `16384` 个 hash slot,对每个 `key` 计算 `CRC16` 值,然后对 `16384` 取模,可以获取 key 对应的 hash slot。 + +redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 `hash tag` 来实现。 + +任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/hash-slot.png) + +### redis cluster 的高可用与主备切换原理 + +redis cluster 的高可用的原理,几乎跟哨兵是类似的。 + +#### 判断节点宕机 + +如果一个节点认为另外一个节点宕机,那么就是 `pfail`,**主观宕机**。如果多个节点都认为另外一个节点宕机了,那么就是 `fail`,**客观宕机**,跟哨兵的原理几乎一样,sdown,odown。 + +在 `cluster-node-timeout` 内,某个节点一直没有返回 `pong`,那么就被认为 `pfail`。 + +如果一个节点认为某个节点 `pfail` 了,那么会在 `gossip ping` 消息中,`ping` 给其他节点,如果**超过半数**的节点都认为 `pfail` 了,那么就会变成 `fail`。 + +#### 从节点过滤 + +对宕机的 master node,从其所有的 slave node 中,选择一个切换成 master node。 + +检查每个 slave node 与 master node 断开连接的时间,如果超过了 `cluster-node-timeout * cluster-slave-validity-factor`,那么就**没有资格**切换成 `master`。 + +#### 从节点选举 + +每个从节点,都根据自己对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。 + +所有的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node`(N/2 + 1)`都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。 + +从节点执行主备切换,从节点切换为主节点。 + +#### 与哨兵比较 + +整个流程跟哨兵相比,非常类似,所以说,redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。 + +## 如何保证缓存与数据库的双写一致性? + +一般来说,如果允许缓存可以稍微的跟数据库偶尔有不一致的情况,也就是说如果你的系统**不是严格要求** “缓存+数据库” 必须保持一致性的话,最好不要做这个方案,即:**读请求和写请求串行化**,串到一个**内存队列**里去。 + +串行化可以保证一定不会出现不一致的情况,但是它也会导致系统的吞吐量大幅度降低,用比正常情况下多几倍的机器去支撑线上的一个请求。 + +### Cache Aside Pattern + +最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。 + +- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。 +- 更新的时候,**先更新数据库,然后再删除缓存**。 + +**为什么是删除缓存,而不是更新缓存?** + +原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。 + +比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。 + +另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于**比较复杂的缓存数据计算的场景**,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,**这个缓存到底会不会被频繁访问到?** + +举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1 分钟内只被读取了 1 次,有**大量的冷数据**。实际上,如果你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。**用到缓存才去算缓存。** + +其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 mybatis,hibernate,都有懒加载思想。查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都里面的 1000 个员工的数据也同时查出来啊。80% 的情况,查这个部门,就只是要访问这个部门的信息就可以了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询 1000 个员工。 + +### 最初级的缓存不一致问题及解决方案 + +问题:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/redis-junior-inconsistent.png) + +解决思路:先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。 + +### 比较复杂的数据不一致问题分析 + +数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,**查到了修改前的旧数据**,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了... + +**为什么上亿流量高并发场景下,缓存会出现这个问题?** + +只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就**可能会出现上述的数据库+缓存不一致的情况**。 + +**解决方案如下:** + +更新数据的时候,根据**数据的唯一标识**,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。 + +一个队列对应一个工作线程,每个工作线程**串行**拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。 + +这里有一个**优化点**,一个队列中,其实**多个更新缓存请求串在一起是没意义的**,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。 + +待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。 + +如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。 + +高并发的场景下,该解决方案要注意的问题: + +- 读请求长时阻塞 + +由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回。 + +该解决方案,最大的风险点在于说,**可能数据更新很频繁**,导致队列中积压了大量更新操作在里面,然后**读请求会发生大量的超时**,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频率是怎样的。 + +另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要**部署多个服务**,每个服务分摊一些数据的更新操作。如果一个内存队列里居然会挤压 100 个商品的库存修改操作,每隔库存修改操作要耗费 10ms 去完成,那么最后一个商品的读请求,可能等待 10 * 100 = 1000ms = 1s 后,才能得到数据,这个时候就导致**读请求的长时阻塞**。 + +一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会 hang 多少时间,如果读请求在 200ms 返回,如果你计算过后,哪怕是最繁忙的时候,积压 10 个更新操作,最多等待 200ms,那还可以的。 + +**如果一个内存队列中可能积压的更新操作特别多**,那么你就要**加机器**,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。 + +其实根据之前的项目经验,一般来说,数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的。像这种针对读高并发、读缓存架构的项目,一般来说写请求是非常少的,每秒的 QPS 能到几百就不错了。 + +我们来**实际粗略测算一下**。 + +如果一秒有 500 的写操作,如果分成 5 个时间片,每 200ms 就 100 个写操作,放到 20 个内存队列中,每个内存队列,可能就积压 5 个写操作。每个写操作性能测试后,一般是在 20ms 左右就完成,那么针对每个内存队列的数据的读请求,也就最多 hang 一会儿,200ms 以内肯定能返回了。 + +经过刚才简单的测算,我们知道,单机支撑的写 QPS 在几百是没问题的,如果写 QPS 扩大了 10 倍,那么就扩容机器,扩容 10 倍的机器,每个机器 20 个队列。 + +- 读请求并发量过高 + +这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时 hang 在服务上,看服务能不能扛的住,需要多少机器才能扛住最大的极限情况的峰值。 + +但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大。 + +- 多服务实例部署的请求路由 + +可能这个服务部署了多个实例,那么必须**保证**说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 Nginx 服务器**路由到相同的服务实例上**。 + +比如说,对同一个商品的读写请求,全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。 + +- 热点商品的路由问题,导致请求的倾斜 + +万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能会造成某台机器的压力过大。就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以其实要根据业务系统去看,如果更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些。 + +## 了解什么是 redis 的雪崩、穿透和击穿?redis 崩溃之后会怎么样?系统该如何应对这种情况?如何处理 redis 的穿透? + +### 缓存雪崩 + +对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。 + +这就是缓存雪崩。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/redis-caching-avalanche.png) + +大约在 3 年前,国内比较知名的一个互联网公司,曾因为缓存事故,导致雪崩,后台系统全部崩溃,事故从当天下午持续到晚上凌晨 3\~4 点,公司损失了几千万。 + +缓存雪崩的事前事中事后的解决方案如下。 + +- 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。 +- 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。 +- 事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/redis-caching-avalanche-solution.png) + +用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 redis。如果 ehcache 和 redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 redis 中。 + +限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?**走降级**!可以返回一些默认的值,或者友情提示,或者空白的值。 + +好处: + +- 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。 +- 只要数据库不死,就是说,对用户来说,2/5 的请求都是可以被处理的。 +- 只要有 2/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。 + +### 缓存穿透 + +对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。 + +黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。 + +举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/redis-caching-penetration.png) + +解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 `set -999 UNKNOWN`。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。 + +### 缓存击穿 + +缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。 + +解决方式也很简单,可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。 + +## redis 的并发竞争问题是什么?如何解决这个问题?了解 redis 事务的 CAS 方案吗? + +某个时刻,多个系统实例都去更新某个 key。可以基于 zookeeper 实现分布式锁。每个系统通过 zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 key,别人都不允许读和写。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/zookeeper-distributed-lock.png) + +你要写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。 + +每次要**写之前,先判断**一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。 + +## 生产环境中的 redis 是怎么部署的? + +redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。 + +机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。 + +5 台机器对外提供读写,一共有 50g 内存。 + +因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。 + +你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。 + +其实大型的公司,会有基础架构的 team 负责缓存集群的运维。 diff --git a/docs/cache/cache-libs.md b/docs/cache/cache-libs.md new file mode 100644 index 00000000..143e31a0 --- /dev/null +++ b/docs/cache/cache-libs.md @@ -0,0 +1,186 @@ +# Java 缓存库 + +> 关键词:ConcurrentHashMap、LRUHashMap、Guava Cache、Caffeine、Ehcache + + + +- [一、ConcurrentHashMap](#一concurrenthashmap) +- [二、LRUHashMap](#二lruhashmap) +- [三、Guava Cache](#三guava-cache) + - [Guava Cache 缓存回收](#guava-cache-缓存回收) + - [基于容量回收](#基于容量回收) + - [基于定时回收](#基于定时回收) + - [基于引用回收](#基于引用回收) + - [Guava Cache 核心 API](#guava-cache-核心-api) +- [四、Caffeine](#四caffeine) +- [五、Ehcache](#五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](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/) diff --git a/docs/cache/ehcache.md b/docs/cache/ehcache.md new file mode 100644 index 00000000..045102af --- /dev/null +++ b/docs/cache/ehcache.md @@ -0,0 +1,542 @@ +# Ehcache 应用指南 + +> EhCache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/technology/cache/ehcache-architecture.png) + + + +- [一、简介](#一简介) + - [Ehcache 特性](#ehcache-特性) + - [Ehcache 集群](#ehcache-集群) +- [二、快速入门](#二快速入门) + - [引入 Ehcache](#引入-ehcache) + - [添加配置文件](#添加配置文件) + - [Ehcache 工作示例](#ehcache-工作示例) +- [三、Ehcache API](#三ehcache-api) + - [创建 CacheManager](#创建-cachemanager) + - [添加缓存](#添加缓存) + - [删除缓存](#删除缓存) + - [基本缓存操作](#基本缓存操作) +- [四、Ehcache 配置](#四ehcache-配置) + - [xml 配置方式](#xml-配置方式) + - [API 配置方式](#api-配置方式) +- [五、Spring 集成 Ehcache](#五spring-集成-ehcache) + - [绑定 Ehcache](#绑定-ehcache) + - [使用 Spring 的缓存注解](#使用-spring-的缓存注解) + - [注解基本使用方法](#注解基本使用方法) +- [参考资料](#参考资料) + + + +## 一、简介 + +> 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/) diff --git a/docs/cache/http-cache.md b/docs/cache/http-cache.md new file mode 100644 index 00000000..3671cf8b --- /dev/null +++ b/docs/cache/http-cache.md @@ -0,0 +1,76 @@ +# Http 缓存 + +HTTP 缓存分为 2 种,一种是强缓存,另一种是协商缓存。主要作用是可以加快资源获取速度,提升用户体验,减少网络传输,缓解服务端的压力。 + + + +- [Http 强缓存](#http-强缓存) + - [Expires](#expires) + - [Cache-Control](#cache-control) + - [Pragma](#pragma) +- [协商缓存](#协商缓存) + - [ETag/If-None-Match](#etagif-none-match) + - [Last-Modified/If-Modified-Since](#last-modifiedif-modified-since) +- [参考资料](#参考资料) + + + +## 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) diff --git a/docs/cache/memcached.md b/docs/cache/memcached.md new file mode 100644 index 00000000..1700a780 --- /dev/null +++ b/docs/cache/memcached.md @@ -0,0 +1,76 @@ +# Memcached 应用指南 + +## 一、Memcached 简介 + +Memcached 是一个自由开源的,高性能,分布式内存对象缓存系统。 + +Memcached 是一种基于内存的 key-value 存储,用来存储小块的任意数据(字符串、对象)。这些数据可以是数据库调用、API 调用或者是页面渲染的结果。 + +Memcached 简洁而强大。它的简洁设计便于快速开发,减轻开发难度,解决了大数据量缓存的很多问题。它的 API 兼容大部分流行的开发语言。本质上,它是一个简洁的 key-value 存储系统。 + +### Memcached 特性 + +memcached 作为高速运行的分布式缓存服务器,具有以下的特点。 + +- 协议简单 +- 基于 libevent 的事件处理 +- 内置内存存储方式 +- memcached 不互相通信的分布式 + +## 二、Memcached 命令 + +可以通过 telnet 命令并指定主机ip和端口来连接 Memcached 服务。 + +``` +telnet 127.0.0.1 11211 + +Trying 127.0.0.1... +Connected to 127.0.0.1. +Escape character is '^]'. +set foo 0 0 3 保存命令 +bar 数据 +STORED 结果 +get foo 取得命令 +VALUE foo 0 3 数据 +bar 数据 +END 结束行 +quit 退出 +``` + +## 三、Java 连接 Memcached + +使用 Java 程序连接 Memcached,需要在你的 classpath 中添加 Memcached jar 包。 + +本站 jar 包下载地址:[spymemcached-2.10.3.jar](https://www.runoob.com/try/download/spymemcached-2.10.3.jar)。 + +Google Code jar 包下载地址:[spymemcached-2.10.3.jar](http://code.google.com/p/spymemcached/downloads/list)(需要科学上网)。 + +以下程序假定 Memcached 服务的主机为 127.0.0.1,端口为 11211。 + +```java +import net.spy.memcached.MemcachedClient; +import java.net.*; + + +public class MemcachedJava { + public static void main(String[] args) { + try{ + // 本地连接 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress("127.0.0.1", 11211)); + System.out.println("Connection to server sucessful."); + + // 关闭连接 + mcc.shutdown(); + + }catch(Exception ex){ + System.out.println( ex.getMessage() ); + } + } +} +``` + +## 参考资料 + +- [Memcached 官网](https://memcached.org/) +- [Memcached Github](https://github.com/memcached/memcached/) +- [Memcached 教程](https://www.runoob.com/memcached/memcached-tutorial.html) diff --git a/docs/distributed/README.md b/docs/distributed/README.md deleted file mode 100644 index 70644ae5..00000000 --- a/docs/distributed/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# 分布式 - -> 分布式技术 \ No newline at end of file diff --git a/docs/distributed/cache/redis.md b/docs/distributed/cache/redis.md deleted file mode 100644 index 3675a43c..00000000 --- a/docs/distributed/cache/redis.md +++ /dev/null @@ -1,625 +0,0 @@ ---- -title: Redis -date: 2018/06/09 -categories: -- database -tags: -- database -- key-value -- cache ---- - -# Redis - - - -- [1. 概述](#1-概述) - - [1.1. Redis 简介](#11-redis-简介) - - [1.2. Redis 的优势](#12-redis-的优势) - - [1.3. Redis 与 Memcached](#13-redis-与-memcached) -- [2. 数据类型](#2-数据类型) - - [2.1. STRING](#21-string) - - [2.2. LIST](#22-list) - - [2.3. SET](#23-set) - - [2.4. HASH](#24-hash) - - [2.5. ZSET](#25-zset) -- [3. 使用场景](#3-使用场景) -- [4. Redis 管道](#4-redis-管道) -- [5. 键的过期时间](#5-键的过期时间) -- [6. 数据淘汰策略](#6-数据淘汰策略) -- [7. 持久化](#7-持久化) - - [7.1. 快照持久化](#71-快照持久化) - - [7.2. AOF 持久化](#72-aof-持久化) -- [8. 发布与订阅](#8-发布与订阅) -- [9. 事务](#9-事务) - - [9.1. EXEC](#91-exec) - - [9.2. MULTI](#92-multi) - - [9.3. DISCARD](#93-discard) - - [9.4. WATCH](#94-watch) -- [10. 事件](#10-事件) - - [10.1. 文件事件](#101-文件事件) - - [10.2. 时间事件](#102-时间事件) - - [10.3. 事件的调度与执行](#103-事件的调度与执行) -- [11. 集群](#11-集群) - - [11.1. 复制](#111-复制) - - [11.2. 哨兵](#112-哨兵) - - [11.3. 分片](#113-分片) -- [12. Redis Client](#12-redis-client) -- [13. 资料](#13-资料) - - - -## 1. 概述 - -### 1.1. Redis 简介 - -Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。 - -键的类型只能为字符串,值支持的五种类型数据类型为:字符串、列表、集合、有序集合、散列表。 - -Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。 - -### 1.2. Redis 的优势 - -- 性能极高 – Redis 能读的速度是 110000 次/s,写的速度是 81000 次/s。 -- 丰富的数据类型 - 支持字符串、列表、集合、有序集合、散列表。 -- 原子 - Redis 的所有操作都是原子性的。单个操作是原子性的。多个操作也支持事务,即原子性,通过 MULTI 和 EXEC 指令包起来。 -- 持久化 - Redis 支持数据的持久化。可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。 -- 备份 - Redis 支持数据的备份,即 master-slave 模式的数据备份。 -- 丰富的特性 - Redis 还支持发布订阅, 通知, key 过期等等特性。 - -### 1.3. Redis 与 Memcached - -Redis 与 Memcached 因为都可以用于缓存,所以常常被拿来做比较,二者主要有以下区别: - -**数据类型** - -- Memcached 仅支持字符串类型; -- 而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。 - -**数据持久化** - -- Memcached 不支持持久化; -- Redis 支持两种持久化策略:RDB 快照和 AOF 日志。 - -**分布式** - -- Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。 -- Redis Cluster 实现了分布式的支持。 - -**内存管理机制** - -- Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 -- 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。 - -## 2. 数据类型 - -| 数据类型 | 可以存储的值 | 操作 | -| -------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------- | -| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作
对整数和浮点数执行自增或者自减操作 | -| LIST | 列表 | 从两端压入或者弹出元素
读取单个或者多个元素
进行修剪,只保留一个范围内的元素 | -| SET | 无序集合 | 添加、获取、移除单个元素
检查一个元素是否存在于集合中
计算交集、并集、差集
从集合里面随机获取元素 | -| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对
获取所有键值对
检查某个键是否存在 | -| ZSET | 有序集合 | 添加、获取、删除元素
根据分值范围或者成员来获取元素
计算一个键的排名 | - -> [What Redis data structures look like](https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/) - -### 2.1. STRING - -
- -
- -命令: - -| 命令 | 行为 | -| ---- | -------------------------------------------------- | -| GET | 获取存储在给定键中的值 | -| SET | 设置存储在给定键中的值 | -| DEL | 删除存储在给定键中的值(这个命令可以用于所有类型) | - -示例: - -```py -127.0.0.1:6379> set name jack -OK -127.0.0.1:6379> get name -"jack" -127.0.0.1:6379> del name -(integer) 1 -127.0.0.1:6379> get name -(nil) -``` - -### 2.2. LIST - -
- -
- -命令: - -| 命令 | 行为 | -| ------ | -------------------------------------------------- | -| RPUSH | 获取存储在给定键中的值 | -| LRANGE | 设置存储在给定键中的值 | -| LINDEX | 删除存储在给定键中的值(这个命令可以用于所有类型) | -| LPOP | 删除存储在给定键中的值(这个命令可以用于所有类型) | - -示例: - -```py -127.0.0.1:6379> rpush list item1 -(integer) 1 -127.0.0.1:6379> rpush list item2 -(integer) 2 -127.0.0.1:6379> rpush list item3 -(integer) 3 -127.0.0.1:6379> lrange list 0 -1 -1) "item1" -2) "item2" -3) "item3" -127.0.0.1:6379> lindex list 1 -"item2" -127.0.0.1:6379> lpop list -"item1" -127.0.0.1:6379> lrange list 0 -1 -1) "item2" -2) "item3" -``` - -### 2.3. SET - -
- -
- -命令: - -| 命令 | 行为 | -| --------- | -------------------------------- | -| SADD | 添加一个或多个元素到集合里 | -| SMEMBERS | 获取集合里面的所有元素 | -| SISMEMBER | 确定一个给定的值是一个集合的成员 | -| SREM | 从集合里删除一个或多个元素 | - -示例: - -```py -127.0.0.1:6379> sadd set item1 -(integer) 1 -127.0.0.1:6379> sadd set item2 -(integer) 1 -127.0.0.1:6379> sadd set item3 -(integer) 1 -127.0.0.1:6379> sadd set item3 -(integer) 0 - -127.0.0.1:6379> smembers set -1) "item3" -2) "item2" -3) "item1" - -127.0.0.1:6379> sismember set item2 -(integer) 1 -127.0.0.1:6379> sismember set item6 -(integer) 0 - -127.0.0.1:6379> srem set item2 -(integer) 1 -127.0.0.1:6379> srem set item2 -(integer) 0 - -127.0.0.1:6379> smembers set -1) "item3" -2) "item1" -``` - -### 2.4. HASH - -
- -
- -命令: - -| 命令 | 行为 | -| ------- | -------------------------- | -| HSET | 设置 hash 里面一个字段的值 | -| HGET | 获取 hash 中域的值 | -| HGETALL | 从 hash 中读取全部的域和值 | -| HDEL | 删除一个或多个域 | - -示例: - -```py -127.0.0.1:6379> hset myhash key1 value1 -(integer) 1 -127.0.0.1:6379> hset myhash key2 value2 -(integer) 1 -127.0.0.1:6379> hset myhash key3 value3 -(integer) 1 -127.0.0.1:6379> hset myhash key3 value2 -(integer) 0 - -127.0.0.1:6379> hgetall myhash -1) "key1" -2) "value1" -3) "key2" -4) "value2" -5) "key3" -6) "value2" - -127.0.0.1:6379> hdel myhash key2 -(integer) 1 -127.0.0.1:6379> hdel myhash key2 -(integer) 0 - -127.0.0.1:6379> hget myhash key2 -(nil) - -127.0.0.1:6379> hgetall myhash -1) "key1" -2) "value1" -3) "key3" -4) "value2" -127.0.0.1:6379> -``` - -### 2.5. ZSET - -
- -
- -命令: - -| 命令 | 行为 | -| ------------- | ------------------------------------------------------------- | -| ZADD | 添加到有序 set 的一个或多个成员,或更新的分数,如果它已经存在 | -| ZRANGE | 根据指定的 index 返回,返回 sorted set 的成员列表 | -| ZRANGEBYSCORE | 返回有序集合中指定分数区间内的成员,分数由低到高排序。 | -| ZREM | 从排序的集合中删除一个或多个成员 | - -示例: - -```py -127.0.0.1:6379> zadd zset 1 redis -(integer) 1 -127.0.0.1:6379> zadd zset 2 mongodb -(integer) 1 -127.0.0.1:6379> zadd zset 3 mysql -(integer) 1 -127.0.0.1:6379> zadd zset 3 mysql -(integer) 0 -127.0.0.1:6379> zadd zset 4 mysql -(integer) 0 - -127.0.0.1:6379> zrange zset 0 -1 withscores -1) "redis" -2) "1" -3) "mongodb" -4) "2" -5) "mysql" -6) "4" - -127.0.0.1:6379> zrangebyscore zset 0 2 withscores -1) "redis" -2) "1" -3) "mongodb" -4) "2" - -127.0.0.1:6379> zrem zset mysql -(integer) 1 -127.0.0.1:6379> zrange zset 0 -1 withscores -1) "redis" -2) "1" -3) "mongodb" -4) "2" -``` - -## 3. 使用场景 - -- **缓存** - 将热点数据放到内存中,设置内存的最大使用量以及过期淘汰策略来保证缓存的命中率。 -- **计数器** - Redis 这种内存数据库能支持计数器频繁的读写操作。 -- **应用限流** - 限制一个网站访问流量。 -- **消息队列** - 使用 List 数据类型,它是双向链表。 -- **查找表** - 使用 HASH 数据类型。 -- **交集运算** - 使用 SET 类型,例如求两个用户的共同好友。 -- **排行榜** - 使用 ZSET 数据类型。 -- **分布式 Session** - 多个应用服务器的 Session 都存储到 Redis 中来保证 Session 的一致性。 -- **分布式锁** - 除了可以使用 SETNX 实现分布式锁之外,还可以使用官方提供的 RedLock 分布式锁实现。 - -## 4. Redis 管道 - -Redis 是一种基于 C/S 模型以及请求/响应协议的 TCP 服务。 - -Redis 支持管道技术。管道技术允许请求以异步方式发送,即旧请求的应答还未返回的情况下,允许发送新请求。这种方式可以大大提高传输效率。 - -使用管道发送命令时,服务器将被迫回复一个队列答复,占用很多内存。所以,如果你需要发送大量的命令,最好是把他们按照合理数量分批次的处理。 - -## 5. 键的过期时间 - -Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。 - -对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。 - -可以使用 `EXPIRE` 或 `EXPIREAT` 来为 key 设置过期时间。 - -> 注意:当 `EXPIRE` 的时间如果设置的是负数,`EXPIREAT` 设置的时间戳是过期时间,将直接删除 key。 - -示例: - -```py -redis> SET mykey "Hello" -"OK" -redis> EXPIRE mykey 10 -(integer) 1 -redis> TTL mykey -(integer) 10 -redis> SET mykey "Hello World" -"OK" -redis> TTL mykey -(integer) -1 -redis> -``` - -## 6. 数据淘汰策略 - -可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。 - -| 策略 | 描述 | -| --------------- | ---------------------------------------------------- | -| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 | -| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 | -| volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 | -| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 | -| allkeys-random | 从所有数据集中任意选择数据进行淘汰 | -| noeviction | 禁止驱逐数据 | - -如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。 - -作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法(LRU、TTL)实际实现上并非针对所有 key,而是抽样一小部分 key 从中选出被淘汰 key,抽样数量可通过 maxmemory-samples 配置。 - -## 7. 持久化 - -Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。 - -### 7.1. 快照持久化 - -将某个时间点的所有数据都存放到硬盘上。 - -可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。 - -如果系统发生故障,将会丢失最后一次创建快照之后的数据。 - -如果数据量很大,保存快照的时间会很长。 - -### 7.2. AOF 持久化 - -将写命令添加到 AOF 文件(Append Only File)的末尾。 - -对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。可以看出写入文件的数据不会立即同步到硬盘上,在将写命令添加到 AOF 文件时,要根据需求来保证何时同步到硬盘上。 - -有以下同步选项: - -| 选项 | 同步频率 | -| :------: | :----------------------: | -| always | 每个写命令都同步 | -| everysec | 每秒同步一次 | -| no | 让操作系统来决定何时同步 | - -- always 选项会严重减低服务器的性能; -- everysec 选项比较合适,可以保证系统奔溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响; -- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统奔溃时数据丢失的数量。 - -随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。 - -## 8. 发布与订阅 - -订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。 - -某个客户端使用 SUBSCRIBE 订阅一个频道,其它客户端可以使用 PUBLISH 向这个频道发送消息。 - -发布与订阅模式和观察者模式有以下不同: - -- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。 -- 观察者模式是同步的,当事件触发时,主题会去调用观察者的方法;而发布与订阅模式是异步的; - -## 9. 事务 - -MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事务相关的命令。 - -事务可以一次执行多个命令, 并且有以下两个重要的保证: - -- 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 -- 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。 - -### 9.1. EXEC - -EXEC 命令负责触发并执行事务中的所有命令: - -- 如果客户端在使用 MULTI 开启了一个事务之后,却因为断线而没有成功执行 EXEC ,那么事务中的所有命令都不会被执行。 -- 另一方面,如果客户端成功在开启事务之后执行 EXEC ,那么事务中的所有命令都会被执行。 - -### 9.2. MULTI - -MULTI 命令用于开启一个事务,它总是返回 OK 。 MULTI 执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC 命令被调用时, 所有队列中的命令才会被执行。 - -### 9.3. DISCARD - -当执行 DISCARD 命令时, 事务会被放弃, 事务队列会被清空, 并且客户端会从事务状态中退出。 - -示例: - -```py -> SET foo 1 -OK -> MULTI -OK -> INCR foo -QUEUED -> DISCARD -OK -> GET foo -"1" -``` - -### 9.4. WATCH - -WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 - -被 WATCH 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回 nil-reply 来表示事务已经失败。 - -``` -WATCH mykey -val = GET mykey -val = val + 1 -MULTI -SET mykey $val -EXEC -``` - -使用上面的代码, 如果在 WATCH 执行之后, EXEC 执行之前, 有其他客户端修改了 mykey 的值, 那么当前客户端的事务就会失败。 程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞为止。 - -这种形式的锁被称作乐观锁, 它是一种非常强大的锁机制。 并且因为大多数情况下, 不同的客户端会访问不同的键, 碰撞的情况一般都很少, 所以通常并不需要进行重试。 - -WATCH 使得 EXEC 命令需要有条件地执行:事务只能在所有被监视键都没有被修改的前提下执行,如果这个前提不能满足的话,事务就不会被执行。 - -WATCH 命令可以被调用多次。对键的监视从 WATCH 执行之后开始生效,直到调用 EXEC 为止。 - -用户还可以在单个 WATCH 命令中监视任意多个键,例如: - -```py -redis> WATCH key1 key2 key3 -OK -``` - -当 EXEC 被调用时, 不管事务是否成功执行, 对所有键的监视都会被取消。 - -另外, 当客户端断开连接时, 该客户端对键的监视也会被取消。 - -使用无参数的 UNWATCH 命令可以手动取消对所有键的监视。 对于一些需要改动多个键的事务, 有时候程序需要同时对多个键进行加锁, 然后检查这些键的当前值是否符合程序的要求。 当值达不到要求时, 就可以使用 UNWATCH 命令来取消目前对键的监视, 中途放弃这个事务, 并等待事务的下次尝试。 - -## 10. 事件 - -Redis 服务器是一个事件驱动程序。 - -### 10.1. 文件事件 - -服务器通过套接字与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。 - -Redis 基于 Reactor 模式开发了自己的网络时间处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的时间传送给文件事件分派器,分派器会根据套接字产生的事件类型调用响应的时间处理器。 - -### 10.2. 时间事件 - -服务器有一些操作需要在给定的时间点执行,时间事件是对这类定时操作的抽象。 - -时间事件又分为: - -- 定时事件:是让一段程序在指定的时间之内执行一次; -- 周期性事件:是让一段程序每隔指定时间就执行一次。 - -Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用响应的事件处理器。 - -### 10.3. 事件的调度与执行 - -服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能监听太久,否则时间事件无法在规定的时间内执行,因此监听时间应该根据距离现在最近的时间事件来决定。 - -事件调度与执行由 aeProcessEvents 函数负责,伪代码如下: - -```python -def aeProcessEvents(): - - ## 获取到达时间离当前时间最接近的时间事件 - time_event = aeSearchNearestTimer() - - ## 计算最接近的时间事件距离到达还有多少毫秒 - remaind_ms = time_event.when - unix_ts_now() - - ## 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0 - if remaind_ms < 0: - remaind_ms = 0 - - ## 根据 remaind_ms 的值,创建 timeval - timeval = create_timeval_with_ms(remaind_ms) - - ## 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定 - aeApiPoll(timeval) - - ## 处理所有已产生的文件事件 - procesFileEvents() - - ## 处理所有已到达的时间事件 - processTimeEvents() -``` - -将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下: - -```python -def main(): - - ## 初始化服务器 - init_server() - - ## 一直处理事件,直到服务器关闭为止 - while server_is_not_shutdown(): - aeProcessEvents() - - ## 服务器关闭,执行清理操作 - clean_server() -``` - -从事件处理的角度来看,服务器运行流程如下: - -## 11. 集群 - -### 11.1. 复制 - -通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。 - -一个从服务器只能有一个主服务器,并且不支持主主复制。 - -#### 12.1. 连接过程 - -1. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令; - -2. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令; - -3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。 - -#### 12.2. 主从链 - -随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。 - -### 11.2. 哨兵 - -Sentinel(哨兵)可以监听主服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。 - -### 11.3. 分片 - -分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。 - -假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。 - -主要有三种分片方式: - -- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。 -- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。 -- 服务器分片:Redis Cluster。 - -## 12. Redis Client - -Redis 社区中有多种编程语言的客户端,可以在这里查找合适的客户端:https://redis.io/clients - -redis 官方推荐的 Java Redis Client: - -- [jedis](https://github.com/xetorthio/jedis) -- [redisson](https://github.com/redisson/redisson) -- [lettuce](https://github.com/lettuce-io/lettuce-core) - -## 13. 资料 - -- [Redis 官网](https://redis.io/) -- [awesome-redis](https://github.com/JamzyWang/awesome-redis) -- [Redis 实战](https://item.jd.com/11791607.html) diff --git "a/docs/distributed/cache/\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230.md" "b/docs/distributed/cache/\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230.md" deleted file mode 100644 index 5dfaa459..00000000 --- "a/docs/distributed/cache/\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230.md" +++ /dev/null @@ -1,554 +0,0 @@ ---- -title: 分布式缓存 -date: 2018/07/05 -categories: -- 分布式 -tags: -- 分布式 -- 缓存 ---- - -# 分布式缓存 - - - -- [1. 缓存概述](#1-缓存概述) - - [1.1. 缓存的原理](#11-缓存的原理) - - [1.2. 缓存分类](#12-缓存分类) - - [1.3. 缓存媒介](#13-缓存媒介) - - [1.4. 缓存设计](#14-缓存设计) -- [2. CDN 缓存](#2-cdn-缓存) - - [2.1. CDN 原理](#21-cdn-原理) - - [2.2. CDN 优缺点](#22-cdn-优缺点) - - [2.3. CND 架构参考](#23-cnd-架构参考) - - [2.4. CND 技术实践](#24-cnd-技术实践) -- [3. 反向代理缓存](#3-反向代理缓存) - - [3.1. 缓存原理](#31-缓存原理) - - [3.2. Squid 示例](#32-squid-示例) - - [3.3. 代理缓存比较](#33-代理缓存比较) -- [4. 分布式缓存](#4-分布式缓存) - - [4.1. Memcache](#41-memcache) - - [4.2. Memcache 工作原理](#42-memcache-工作原理) - - [4.3. Memcache 集群](#43-memcache-集群) - - [4.4. Redis](#44-redis) - - [4.5. Redis 常用数据类型](#45-redis-常用数据类型) - - [4.6. Redis 集群](#46-redis-集群) - - [4.7. Memcache 与 Redis 的比较](#47-memcache-与-redis-的比较) -- [5. 本地缓存](#5-本地缓存) - - [5.1. 硬盘缓存](#51-硬盘缓存) - - [5.2. 内存缓存](#52-内存缓存) -- [6. 缓存架构示例](#6-缓存架构示例) -- [7. 数据一致性](#7-数据一致性) - - [7.1. 场景介绍](#71-场景介绍) - - [7.2. 解决方法](#72-解决方法) - - [7.3. 其他方法](#73-其他方法) -- [8. 缓存高可用](#8-缓存高可用) - - [8.1. 解决方法](#81-解决方法) - - [8.2. 其他方法](#82-其他方法) -- [9. 缓存问题](#9-缓存问题) - - [9.1. 缓存雪崩](#91-缓存雪崩) - - [9.2. 缓存穿透](#92-缓存穿透) - - [9.3. 缓存预热](#93-缓存预热) - - [9.4. 缓存更新](#94-缓存更新) - - [9.5. 缓存降级](#95-缓存降级) - - - -## 1. 缓存概述 - -缓存是分布式系统中的重要组件,主要解决高并发,大数据场景下,热点数据访问的性能问题。提供高性能的数据快速访问。 - -### 1.1. 缓存的原理 - -1. 将数据写入/读取速度更快的存储(设备); -2. 将数据缓存到离应用最近的位置; -3. 将数据缓存到离用户最近的位置。 - -### 1.2. 缓存分类 - -在分布式系统中,缓存的应用非常广泛,从部署角度有以下几个方面的缓存应用。 - -1. CDN 缓存; -2. 反向代理缓存; -3. 分布式 Cache; -4. 本地应用缓存; - -### 1.3. 缓存媒介 - -- 常用中间件:Varnish,Ngnix,Squid,Memcache,Redis,Ehcache 等; -- 缓存的内容:文件,数据,对象; -- 缓存的介质:CPU,内存(本地,分布式),磁盘(本地,分布式) - -### 1.4. 缓存设计 - -缓存设计需要解决以下几个问题: - -(1)缓存什么? - -哪些数据需要缓存:热点数据、静态资源 - -(2)缓存的位置? - -CDN,反向代理,分布式缓存服务器,本机(内存,硬盘) - -(3)如何缓存的问题? - -- 过期策略 - - 固定时间:比如指定缓存的时间是 30 分钟; - - 相对时间:比如最近 10 分钟内没有访问的数据; -- 同步机制 - - 实时写入;(推) - - 异步刷新;(推拉) - -## 2. CDN 缓存 - -CDN 主要解决将数据缓存到离用户最近的位置,一般缓存静态资源文件(页面,脚本,图片,视频,文件等)。国内网络异常复杂,跨运营商的网络访问会很慢。为了解决跨运营商或各地用户访问问题,可以在重要的城市,部署 CDN 应用。使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。 - -### 2.1. CDN 原理 - -CDN 的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。 - -(1)未部署 CDN 应用前 - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160606062828308-319014468.png) - -网络路径: - -- 请求:本机网络(局域网)——》运营商网络——》应用服务器机房 -- 响应:应用服务器机房——》运营商网络——》本机网络(局域网) - -在不考虑复杂网络的情况下,从请求到响应需要经过 3 个节点,6 个步骤完成一次用户访问操作。 - -(2)部署 CDN 应用后 - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160606062837558-517040681.png) - -网络路径: - -- 请求:本机网络(局域网)——》运营商网络 -- 响应:运营商网络——》本机网络(局域网) - -在不考虑复杂网络的情况下,从请求到响应需要经过 2 个节点,2 个步骤完成一次用户访问操作。 - -与不部署 CDN 服务相比,减少了 1 个节点,4 个步骤的访问。极大的提高的系统的响应速度。 - -### 2.2. CDN 优缺点 - -(1)优点(摘自百度百科) - -- 本地 Cache 加速:提升访问速度,尤其含有大量图片和静态页面站点; -- 镜像服务:消除了不同运营商之间互联的瓶颈造成的影响,实现了跨运营商的网络加速,保证不同网络中的用户都能得到良好的访问质量; -- 远程加速:远程访问用户根据 DNS 负载均衡技术智能自动选择 Cache 服务器,选择最快的 Cache 服务器,加快远程访问的速度; -- 带宽优化:自动生成服务器的远程 Mirror(镜像)cache 服务器,远程用户访问时从 cache 服务器上读取数据,减少远程访问的带宽、分担网络流量、减轻原站点 WEB 服务器负载等功能。 -- 集群抗攻击:广泛分布的 CDN 节点加上节点之间的智能冗余机制,可以有效地预防黑客入侵以及降低各种 D.D.o.S 攻击对网站的影响,同时保证较好的服务质量。 - -(2)缺点 - -a. 动态资源缓存,需要注意实时性; - -解决:主要缓存静态资源,动态资源建立多级缓存或准实时同步; - -b. 如何保证数据的一致性和实时性需要权衡考虑; - -解决: - -1. 设置缓存失效时间(1 个小时,最终一致性); -2. 数据版本号; - -### 2.3. CND 架构参考 - -摘自《云宙视频 CDN 系统》 - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160606062848590-1014999706.jpg) - -### 2.4. CND 技术实践 - -​ 目前,中小型互联网公司,综合成本考虑,一般租用第三方 CDN 服务,大型互联网公司,采用自建或第三方结合的方式。比如淘宝刚开始使用第三方的,当流量很大后,第三方公司无法支撑其 CDN 流量,淘宝最后采用自建 CDN 的方式实现。 - -淘宝 CDN,如下图(来自网络): - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160606062859808-1261244246.png) - -## 3. 反向代理缓存 - -反向代理是指在网站服务器机房部署代理服务器,实现负载均衡,数据缓存,安全控制等功能。 - -### 3.1. 缓存原理 - -反向代理位于应用服务器机房,处理所有对 WEB 服务器的请求。如果用户请求的页面在代理服务器上有缓冲的话,代理服务器直接将缓冲内容发送给用户。如果没有缓冲则先向 WEB 服务器发出请求,取回数据,本地缓存后再发送给用户。通过降低向 WEB 服务器的请求数,从而降低了 WEB 服务器的负载。 - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160606062912886-1223701582.png) - -​ 反向代理一般缓存静态资源,动态资源转发到应用服务器处理。常用的缓存应用服务器有 Varnish,Ngnix,Squid。 - -### 3.2. Squid 示例 - -Squid 反向代理一般只缓存静态资源,动态程序默认不缓存。根据从 WEB 服务器返回的 HTTP 头标记来缓冲静态页面。有四个最重要 HTTP 头标记: - -- Last-Modified: 告诉反向代理页面什么时间被修改 -- Expires: 告诉反向代理页面什么时间应该从缓冲区中删除 -- Cache-Control: 告诉反向代理页面是否应该被缓冲 -- Pragma: 用来包含实现特定的指令,最常用的是 Pragma:no-cache - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160606062921683-1401924233.jpg) - -Squid 反向代理加速网站实例 - -1. 通过 DNS 的轮询技术,将客户端的请求分发给其中一台 Squid 反向代理服务器处理; -2. 如果这台 Squid 缓存了用户的请求资源,则将请求的资源直接返回给用户; -3. 否则这台 Squid 将没有缓存的请求根据配置的规则发送给邻居 Squid 和后台的 WEB 服务器处理; -4. 这样既减轻后台 WEB 服务器的负载,又提高整个网站的性能和安全性。 - -### 3.3. 代理缓存比较 - -常用的代理缓存有 Varnish,Squid,Ngnix,简单比较如下: - -1. Varnish 和 squid 是专业的 cache 服务,nginx 需要第三方模块支持; -2. Varnish 采用内存型缓存,避免了频繁在内存、磁盘中交换文件,性能比 Squid 高; -3. Varnish 由于是内存 cache,所以对小文件如 css,js,小图片啥的支持很棒,后端的持久化缓存可以采用的是 Squid 或 ATS; -4. Squid 功能全而大,适合于各种静态的文件缓存,一般会在前端挂一个 HAProxy 或 nginx 做负载均衡跑多个实例; -5. Nginx 采用第三方模块 ncache 做的缓冲,性能基本达到 varnish,一般作为反向代理使用,可以实现简单的缓存。 - -## 4. 分布式缓存 - -CDN、反向代理缓存,主要解决静态文件,或用户请求资源的缓存,数据源一般为静态文件或动态生成的文件(有缓存头标识)。 - -分布式缓存,主要指缓存用户经常访问数据的缓存,数据源为数据库。一般起到热点数据访问和减轻数据库压力的作用。 - -目前分布式缓存设计,在大型网站架构中是必备的架构要素。常用的中间件有 Memcache,Redis。 - -### 4.1. Memcache - -Memcache 是一个高性能,分布式内存对象缓存系统,通过在内存里维护一个统一的巨大的 hash 表,它能够用来存储各种格式的数据,包括图像、视频、文件以及数据库检索的结果等。简单的说就是将数据调用到内存中,然后从内存中读取,从而大大提高读取速度。 - -Memcache 特性: - -- 使用物理内存作为缓存区,可独立运行在服务器上。每个进程最大 2G,如果想缓存更多的数据,可以开辟更多的 Memcache 进程(不同端口)或者使用分布式 Memcache 进行缓存,将数据缓存到不同的物理机或者虚拟机上。 -- 使用 key-value 的方式来存储数据,这是一种单索引的结构化数据组织形式,可使数据项查询时间复杂度为 O(1)。 -- 协议简单:基于文本行的协议,直接通过 telnet 在 Memcached 服务器上可进行存取数据操作,简单,方便多种缓存参考此协议; -- 基于 libevent 高性能通信:Libevent 是一套利用 C 开发的程序库,它将 BSD 系统的 kqueue,Linux 系统的 epoll 等事件处理功能封装成一个接口,与传统的 select 相比,提高了性能。 -- 内置的内存管理方式:所有数据都保存在内存中,存取数据比硬盘快,当内存满后,通过 LRU 算法自动删除不使用的缓存,但没有考虑数据的容灾问题,重启服务,所有数据会丢失。 -- 分布式:各个 Memcached 服务器之间互不通信,各自独立存取数据,不共享任何信息。服务器并不具有分布式功能,分布式部署取决于 Memcache 客户端。 -- 缓存策略:Memcached 的缓存策略是 LRU(最近最少使用)到期失效策略。在 Memcached 内存储数据项时,可以指定它在缓存的失效时间,默认为永久。当 Memcached 服务器用完分配的内时,失效的数据被首先替换,然后也是最近未使用的数据。在 LRU 中,Memcached 使用的是一种 Lazy Expiration 策略,自己不会监控存入的 key/vlue 对是否过期,而是在获取 key 值时查看记录的时间戳,检查 key/value 对空间是否过期,这样可减轻服务器的负载。 - -### 4.2. Memcache 工作原理 - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160606064023621-1066632858.png) - -Memcache 的工作流程如下: - -1. 先检查客户端的请求数据是否在 Memcached 中,如有,直接把请求数据返回,不再对数据库进行任何操作; -2. 如果请求的数据不在 Memcached 中,就去查数据库,把从数据库中获取的数据返回给客户端,同时把数据缓存一份到 Memcached 中(Memcached 客户端不负责,需要程序实现); -3. 每次更新数据库的同时更新 Memcached 中的数据,保证一致性; -4. 当分配给 Memcached 内存空间用完之后,会使用 LRU(Least Recently Used,最近最少使用)策略加上到期失效策略,失效数据首先被替换,然后再替换掉最近未使用的数据。 - -### 4.3. Memcache 集群 - -Memcached 虽然称为 “ 分布式 ” 缓存服务器,但服务器端并没有 “ 分布式 ” 功能。每个服务器都是完全独立和隔离的服务。 Memcached 的分布式,是由客户端程序实现的。 - -当向 Memcached 集群存入/取出 key value 时,Memcached 客户端程序根据一定的算法计算存入哪台服务器,然后再把 key value 值存到此服务器中。 - -存取数据分二步走,第一步,选择服务器,第二步存取数据。 - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160606064101183-532365936.png) - -**分布式算法(Consistent Hashing):** - -选择服务器算法有两种,一种是根据余数来计算分布,另一种是根据散列算法来计算分布。 - -- 余数算法: - 先求得键的整数散列值,再除以服务器台数,根据余数确定存取服务器。 - - 优点:计算简单,高效; - - 缺点:在 Memcached 服务器增加或减少时,几乎所有的缓存都会失效。 -- 散列算法:(一致性 Hash) - 先算出 Memcached 服务器的散列值,并将其分布到 0 到 2 的 32 次方的圆上,然后用同样的方法算出存储数据的键的散列值并映射至圆上,最后从数据映射到的位置开始顺时针查找,将数据保存到查找到的第一个服务器上,如果超过 2 的 32 次方,依然找不到服务器,就将数据保存到第一台 Memcached 服务器上。 - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160606064109965-1633058692.jpg) - -如果添加了一台 Memcached 服务器,只在圆上增加服务器的逆时针方向的第一台服务器上的键会受到影响。 - -一致性 Hash 算法:解决了余数算法增加节点命中大幅额度降低的问题,理论上,插入一个实体节点,平均会影响到:虚拟节点数 /2 的节点数据的命中。 - -### 4.4. Redis - -Redis 是一个开源(BSD 许可)的,基于内存的,多数据结构存储系统。可以用作数据库、缓存和消息中间件。 支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 - -内置了 复制(replication),LUA 脚本(Lua scripting), LRU 驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis 哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。 - -### 4.5. Redis 常用数据类型 - -1、String - -常用命令:set,get,decr,incr,mget 。 - -应用场景:String 是最常用的一种数据类型,与 Memcache 的 key value 存储方式类似。 - -实现方式:String 在 Redis 内部存储默认就是一个字符串,被 RedisObject 所引用,当遇到 incr,decr 等操作时会转成数值型进行计算,此时 RedisObject 的 encoding 字段为 int。 - -2、Hash - -常用命令:hget,hset,hgetall 。 - -应用场景:以存储一个用户信息对象数据,为例: - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160606064128371-891992227.png) - -实现方式: - -Redis Hash 对应的 Value,内部实际就是一个 HashMap,实际这里会有 2 种不同实现。 - -(1) Hash 的成员比较少时 Redis 为了节省内存会采用类似一维数 组的方式来紧凑存储,而不会采用真正的 HashMap 结构,对应的 value RedisObject 的 encoding 为 zipmap; - -(2) 当成员数量增大时会自动转成真正的 HashMap,此时 encoding 为 ht。 - -3、List - -常用命令:lpush,rpush,lpop,rpop,lrange。 - -应用场景: - -Redis list 的应用场景非常多,也是 Redis 最重要的数据结构之一,比如 twitter 的关注列表,粉丝列表等都可以用 Redis 的 list 结构来实现。 - -实现方式: - -Redis list 的实现为一个双向链表,可以支持反向查找和遍历,方便操作。不过带来了部分额外的内存开销,Redis 内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。 - -4、Set - -常用命令:sadd,spop,smembers,sunion。 - -应用场景: - -Redis set 对外提供的功能与 list 类似是一个列表的功能,特殊之处在于 set 是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set 是一个很好的选择,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。 - -实现方式: - -set 的内部实现是一个 value 永远为 null 的 HashMap,实际就是通过计算 hash 的方式来快速排重的,这也是 set 能提供判断一个成员是否在集合内的原因。 - -5、Sorted set - -常用命令:zadd,zrange,zrem,zcard; - -使用场景: - -Redis sorted set 的使用场景与 set 类似,区别是 set 不是自动有序的,而 sorted set 可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,可以选择 sorted set 数据结构,比如 twitter 的 public timeline 可以以发表时间作为 score 来存储,这样获取时就是自动按时间排好序的。 - -实现方式: - -Redis sorted set 的内部使用 HashMap 和跳跃表(SkipList)来保证数据的存储和有序,HashMap 里放的是成员到 score 的映射,而跳跃表里存放的 是所有的成员,排序依据是 HashMap 里存的 score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。 - -### 4.6. Redis 集群 - -(1)通过 keepalived 实现的高可用方案 - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160606064149465-1648287265.png) - -切换流程: - -1. 当 Master 挂了后,VIP 漂移到 Slave;Slave 上 keepalived 通知 Redis 执行:slaveof no one ,开始提供业务 -2. 当 Master 起来后,VIP 地址不变,Master 的 keepalived 通知 Redis 执行 slaveof slave IP host ,开始作为从同步数据 -3. 依次类推 - -主从同时 Down 机情况: - -- 非计划性,不做考虑,一般也不会存在这种问题 -- 计划性重启,重启之前通过运维手段 SAVE DUMP 主库数据;需要注意顺序: - 1. 关闭其中一台机器上所有 Redis,使得 master 全部切到另外一台机器(多实例部署,单机上既有主又有从的情况);并关闭机器。 - 2. 依次 dump 主上 Redis 服务 - 3. 关闭主 - 4. 启动主,并等待数据 load 完毕 - 5. 启动从 - 6. 删除 DUMP 文件(避免重启加载慢) - -(2)使用 Twemproxy 实现集群方案 - -由 twitter 开源的 c 版本 proxy,同时支持 Memcache 和 Redis,目前最新版本为:0.2.4,持续开发中;https://github.com/twitter/twemproxy .twitter 用它主要减少前端与缓存服务间网络连接数。 - -特点:快、轻量级、减少后端 Cache Server 连接数、易配置、支持 ketama、modula、random、常用 hash 分片算法。 - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160606064241496-1034024146.png) - -这里使用 keepalived 实现高可用主备方案,解决 proxy 单点问题; - -优点: - -- 对于客户端而言,Redis 集群是透明的,客户端简单,遍于动态扩容 -- Proxy 为单点、处理一致性 hash 时,集群节点可用性检测不存在脑裂问题 -- 高性能,CPU 密集型,而 Redis 节点集群多 CPU 资源冗余,可部署在 Redis 节点集群上,不需要额外设备 - -### 4.7. Memcache 与 Redis 的比较 - -- 数据结构:Memcache 只支持 key value 存储方式,Redis 支持更多的数据类型,比如 Key value,hash,list,set,zset; -- 多线程:Memcache 支持多线程,Redis 支持单线程;CPU 利用方面 Memcache 优于 Redis; -- 持久化:Memcache 不支持持久化,Redis 支持持久化; -- 内存利用率:Memcache 高,Redis 低(采用压缩的情况下比 Memcache 高); -- 过期策略:Memcache 过期后,不删除缓存,会导致下次取数据数据的问题,Redis 有专门线程,清除缓存数据; - -## 5. 本地缓存 - -本地缓存是指应用内部的缓存,标准的分布式系统,一般有多级缓存构成。本地缓存是离应用最近的缓存,一般可以将数据缓存到硬盘或内存。 - -### 5.1. 硬盘缓存 - -​ 将数据缓存到硬盘到,读取时从硬盘读取。原理是直接读取本机文件,减少了网络传输消耗,比通过网络读取数据库速度更快。可以应用在对速度要求不是很高,但需要大量缓存存储的场景。 - -### 5.2. 内存缓存 - -直接将数据存储到本机内存中,通过程序直接维护缓存对象,是访问速度最快的方式。 - -## 6. 缓存架构示例 - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160606064322168-810312242.png) - -职责划分: - -- CDN:存放 HTML,CSS,JS 等静态资源; -- 反向代理:动静分离,只缓存用户请求的静态资源; -- 分布式缓存:缓存数据库中的热点数据; -- 本地缓存:缓存应用字典等常用数据; - -请求过程: - -1. 浏览器向客户端发起请求,如果 CDN 有缓存则直接返回; -2. 如果 CDN 无缓存,则访问反向代理服务器; -3. 如果反向代理服务器有缓存则直接返回; -4. 如果反向代理服务器无缓存或动态请求,则访问应用服务器; -5. 应用服务器访问本地缓存;如果有缓存,则返回代理服务器,并缓存数据;(动态请求不缓存) -6. 如果本地缓存无数据,则读取分布式缓存;并返回应用服务器;应用服务器将数据缓存到本地缓存(部分); -7. 如果分布式缓存无数据,则应用程序读取数据库数据,并放入分布式缓存; - -## 7. 数据一致性 - -缓存是在数据持久化之前的一个节点,主要是将热点数据放到离用户最近或访问速度更快的介质中,加快数据的访问,减小响应时间。 - -因为缓存属于持久化数据的一个副本,因此不可避免的会出现数据不一致问题。导致脏读或读不到数据的情况。数据不一致,一般是因为网络不稳定或节点故障导致。根据数据的操作顺序,主要有以下几种情况。 - -### 7.1. 场景介绍 - -(1)先写缓存,再写数据库 - -​ 如下图: - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160619110523914-514476589.jpg) - -假如缓存写成功,但写数据库失败或响应延迟,则下次读取(并发读)缓存时,就出现脏读; - -(2)先写数据库,再写缓存 - -​ 如下图: - -​ ![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160619110532039-1008665032.jpg) - -​ 假如写数据库成功,但写缓存失败,则下次读取(并发读)缓存时,则读不到数据; - -(3)缓存异步刷新 - -​ 指数据库操作和写缓存不在一个操作步骤中,比如在分布式场景下,无法做到同时写缓存或需要异步刷新(补救措施)时候。 - -![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160619110540555-889329617.jpg) - -​ 此种情况,主要考虑数据写入和缓存刷新的时效性。比如多久内刷新缓存,不影响用户对数据的访问。 - -### 7.2. 解决方法 - -第一个场景: - -这个写缓存的方式,本身就是错误的,需要改为先写持久化介质,再写缓存的方式。 - -第二个场景: - -(1)根据写入缓存的响应来进行判断,如果缓存写入失败,则回滚数据库操作;此种方法增加了程序的复杂度,不建议采用; - -(2)缓存使用时,假如读缓存失败,先读数据库,再回写缓存的方式实现。 - -第三个场景: - -(1)首先确定,哪些数据适合此类场景; - -(2)根据经验值确定合理的数据不一致时间,用户数据刷新的时间间隔; - -### 7.3. 其他方法 - -- 超时:设置合理的超时时间; -- 刷新:定时刷新一定范围内(根据时间,版本号)的数据; - -​ 以上是简化数据读写场景,实际中会分为: - -- 缓存与数据库之间的一致性; -- 多级缓存之间的一致性; -- 缓存副本之间的一致性。 - -## 8. 缓存高可用 - -业界有两种理论,第一套缓存就是缓存,临时存储数据的,不需要高可用。第二种缓存逐步演化为重要的存储介质,需要做高可用。 - -本人的看法是,缓存是否高可用,需要根据实际的场景而定。临界点是:是否对后端的数据库造成影响。 - -具体的决策依据需要根据,集群的规模(数据,缓存),成本(服务器,运维),系统性能(并发量,吞吐量,响应时间)等方面综合评价。 - -### 8.1. 解决方法 - -​ 缓存的高可用,一般通过分布式和复制实现。分布式实现数据的海量缓存,复制实现缓存数据节点的高可用。架构图如下: - -​ ![img](https://images2015.cnblogs.com/blog/820332/201606/820332-20160619110556336-2133732642.jpg) - -​ 其中,分布式采用一致性 Hash 算法,复制采用异步复制。 - -### 8.2. 其他方法 - -1. 复制双写:缓存节点的复制,由异步改为双写,只有两份都写成功,才算成功。 -2. 虚拟层:一致性 Hash 存在,假如其中一个 HASH 环不可用,数据会写入临近的环,当 HASH 可用时,数据又写入正常的 HASH 环,会导致数据偏移问题。这种情况,可以考虑在 HASH 环前面加一个虚拟层实现。 -3. 多级缓存:比如一级使用本地缓存,二级采用分布式 Cahce,三级采用分布式 Cache+本地持久化; - -​ 方式很多,需要根据业务场景灵活选择。 - -## 9. 缓存问题 - -### 9.1. 缓存雪崩 - -缓存雪崩是指:在高并发场景下,由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库 CPU 和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。 - -解决方案: - -1. 合理规划缓存的失效时间; -2. 合理评估数据库的负载压力; -3. 对数据库进行过载保护或应用层限流; -4. 多级缓存设计,缓存高可用; - -缓存失效时产生的雪崩效应,将所有请求全部放在数据库上,这样很容易就达到数据库的瓶颈,导致服务无法正常提供。尽量避免这种场景的发生。 - -### 9.2. 缓存穿透 - -​ 缓存一般是 Key,value 方式存在,当某一个 Key 不存在时会查询数据库,假如这个 Key,一直不存在,则会频繁的请求数据库,对数据库造成访问压力。 - -当在流量较大时,出现这样的情况,一直请求 DB,很容易导致服务挂掉。 - -解决方法: - -1. 对结果为空的数据也进行缓存,当此 key 有数据后,清理缓存; -2. 一定不存在的 key,采用布隆过滤器,建立一个大的 Bitmap 中,查询时通过该 bitmap 过滤; - -### 9.3. 缓存预热 - -缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理解,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据! - -解决方案: - -1. 直接写个缓存刷新页面,上线时手工操作下; -2. 数据量不大,可以在项目启动的时候自动进行加载; -3. 定时刷新缓存; - -### 9.4. 缓存更新 - -除了缓存服务器自带的缓存失效策略之外(Redis 默认的有 6 中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种: - -1. 定时去清理过期的缓存; -2. 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。 - -两者各有优劣,第一种的缺点是维护大量缓存的 key 是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。 - -### 9.5. 缓存降级 - -当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。 - -降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。 diff --git a/docs/distributed/mq/ActiveMQ.md b/docs/distributed/mq/ActiveMQ.md deleted file mode 100644 index d3304742..00000000 --- a/docs/distributed/mq/ActiveMQ.md +++ /dev/null @@ -1,315 +0,0 @@ ---- -title: ActiveMQ -date: 2017/8/18 -categories: -- javaweb -tags: -- java -- javaweb -- 分布式 -- mq -- jms ---- - -# ActiveMQ - - - -- [1. JMS 基本概念](#1-jms-基本概念) - - [1.1. 消息模型](#11-消息模型) - - [1.2. JMS 编程模型](#12-jms-编程模型) -- [2. 安装](#2-安装) -- [3. 项目中的应用](#3-项目中的应用) -- [4. 资源](#4-资源) - - - -## 1. JMS 基本概念 - -`JMS` 即 **Java 消息服务(Java Message Service)API**,是一个 Java 平台中关于面向消息中间件的 API。它用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java 消息服务是一个与具体平台无关的 API,绝大多数 MOM 提供商都对 JMS 提供支持。 - -### 1.1. 消息模型 - -JMS 有两种消息模型: - -- Point-to-Point(P2P) -- Publish/Subscribe(Pub/Sub) - -#### P2P 的特点 - -![jms-pointToPoint.gif](http://oyz7npk35.bkt.clouddn.com//image/java/libs/activemq/jms-pointToPoint.gif) - -在点对点的消息系统中,消息分发给一个单独的使用者。点对点消息往往与队列 `javax.jms.Queue` 相关联。 - -每个消息只有一个消费者(Consumer)(即一旦被消费,消息就不再在消息队列中)。 - -发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行,它不会影响到消息被发送到队列。 - -接收者在成功接收消息之后需向队列应答成功。 - -如果你希望发送的每个消息都应该被成功处理的话,那么你需要 P2P 模式。 - -#### Pub/Sub 的特点 - -![jms-publishSubscribe.gif](http://oyz7npk35.bkt.clouddn.com//image/java/libs/activemq/jms-publishSubscribe.gif) - -发布/订阅消息系统支持一个事件驱动模型,消息生产者和消费者都参与消息的传递。生产者发布事件,而使用者订阅感兴趣的事件,并使用事件。该类型消息一般与特定的主题 `javax.jms.Topic` 关联。 - -每个消息可以有多个消费者。 - -发布者和订阅者之间有时间上的依赖性。针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息,而且为了消费消息,订阅者必须保持运行的状态。 - -为了缓和这样严格的时间相关性,JMS 允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有被激活(运行),它也能接收到发布者的消息。 - -如果你希望发送的消息可以不被做任何处理、或者被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用 Pub/Sub 模型。 - -### 1.2. JMS 编程模型 - -![jms-publishSubscribe.gif](http://oyz7npk35.bkt.clouddn.com//image/java/libs/activemq/jms-publishSubscribe.gif) - -#### ConnectionFactory - -创建 `Connection` 对象的工厂,针对两种不同的 jms 消息模型,分别有 `QueueConnectionFactory` 和`TopicConnectionFactory` 两种。可以通过 JNDI 来查找 `ConnectionFactory` 对象。 - -#### Connection - -`Connection` 表示在客户端和 JMS 系统之间建立的链接(对 TCP/IP socket 的包装)。`Connection` 可以产生一个或多个`Session`。跟 `ConnectionFactory` 一样,`Connection` 也有两种类型:`QueueConnection` 和 `TopicConnection`。 - -#### Destination - -`Destination` 是一个包装了消息目标标识符的被管对象。消息目标是指消息发布和接收的地点,或者是队列 `Queue` ,或者是主题 `Topic` 。JMS 管理员创建这些对象,然后用户通过 JNDI 发现它们。和连接工厂一样,管理员可以创建两种类型的目标,点对点模型的 `Queue`,以及发布者/订阅者模型的 `Topic`。 - -#### Session - -`Session` 表示一个单线程的上下文,用于发送和接收消息。由于会话是单线程的,所以消息是连续的,就是说消息是按照发送的顺序一个一个接收的。会话的好处是它支持事务。如果用户选择了事务支持,会话上下文将保存一组消息,直到事务被提交才发送这些消息。在提交事务之前,用户可以使用回滚操作取消这些消息。一个会话允许用户创建消息,生产者来发送消息,消费者来接收消息。同样,`Session` 也分 `QueueSession` 和 `TopicSession`。 - -#### MessageConsumer - -`MessageConsumer` 由 `Session` 创建,并用于将消息发送到 `Destination`。消费者可以同步地(阻塞模式),或(非阻塞)接收 `Queue` 和 `Topic` 类型的消息。同样,消息生产者分两种类型:`QueueSender` 和`TopicPublisher`。 - -#### MessageProducer - -`MessageProducer` 由 `Session` 创建,用于接收被发送到 `Destination` 的消息。`MessageProducer` 有两种类型:`QueueReceiver` 和 `TopicSubscriber`。可分别通过 `session` 的 `createReceiver(Queue)` 或 `createSubscriber(Topic)` 来创建。当然,也可以 `session` 的 `creatDurableSubscriber` 方法来创建持久化的订阅者。 - -#### Message - -是在消费者和生产者之间传送的对象,也就是说从一个应用程序传送到另一个应用程序。一个消息有三个主要部分: - -- 消息头(必须):包含用于识别和为消息寻找路由的操作设置。 -- 一组消息属性(可选):包含额外的属性,支持其他提供者和用户的兼容。可以创建定制的字段和过滤器(消息选择器)。 -- 一个消息体(可选):允许用户创建五种类型的消息(文本消息,映射消息,字节消息,流消息和对象消息)。 - -消息接口非常灵活,并提供了许多方式来定制消息的内容。 - -| Common | Point-to-Point | Publish-Subscribe | -| ----------------- | --------------------------- | ---------------------- | -| ConnectionFactory | QueueConnectionFactory | TopicConnectionFactory | -| Connection | QueueConnection | TopicConnection | -| Destination | Queue | Topic | -| Session | QueueSession | TopicSession | -| MessageProducer | QueueSender | TopicPublisher | -| MessageSender | QueueReceiver, QueueBrowser | TopicSubscriber | - -## 2. 安装 - -**安装条件** - -JDK1.7 及以上版本 - -本地配置了 **JAVA_HOME** 环境变量。 - -**下载** - -支持 Windows/Unix/Linux/Cygwin - -[ActiveMQ 官方下载地址](http://activemq.apache.org/download.html) - -**Windows 下运行** - -1. 解压压缩包到本地; -2. 打开控制台,进入安装目录的 `bin` 目录下; - -``` -cd [activemq_install_dir] -``` - -3. 执行 `activemq start` 来启动 ActiveMQ - -``` -bin\activemq start -``` - -**测试安装是否成功** - -1. ActiveMQ 默认监听端口为 61616 - -``` -netstat -an|find “61616” -``` - -2. 访问 http://127.0.0.1:8161/admin/ - -3. 输入用户名、密码 - Login: admin - Passwort: admin - -4. 点击导航栏的 Queues 菜单 - -5. 添加一个队列(queue) - -## 3. 项目中的应用 - -**引入依赖** - -```xml - - org.apache.activemq - activemq-all - 5.14.1 - -``` - -**Sender.java** - -```java -public class Sender { - private static final int SEND_NUMBER = 4; - - public static void main(String[] args) { - // ConnectionFactory :连接工厂,JMS 用它创建连接 - ConnectionFactory connectionFactory; - // Connection :JMS 客户端到JMS Provider 的连接 - Connection connection = null; - // Session: 一个发送或接收消息的线程 - Session session; - // Destination :消息的目的地;消息发送给谁. - Destination destination; - // MessageProducer:消息发送者 - MessageProducer producer; - // TextMessage message; - // 构造ConnectionFactory实例对象,此处采用ActiveMq的实现jar - connectionFactory = new ActiveMQConnectionFactory( - ActiveMQConnection.DEFAULT_USER, - ActiveMQConnection.DEFAULT_PASSWORD, - "tcp://localhost:61616"); - try { - // 构造从工厂得到连接对象 - connection = connectionFactory.createConnection(); - // 启动 - connection.start(); - // 获取操作连接 - session = connection.createSession(Boolean.TRUE, - Session.AUTO_ACKNOWLEDGE); - // 获取session注意参数值xingbo.xu-queue是一个服务器的queue,须在在ActiveMq的console配置 - destination = session.createQueue("FirstQueue"); - // 得到消息生成者【发送者】 - producer = session.createProducer(destination); - // 设置不持久化,此处学习,实际根据项目决定 - producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); - // 构造消息,此处写死,项目就是参数,或者方法获取 - sendMessage(session, producer); - session.commit(); - } catch (Exception e) { - e.printStackTrace(); - } finally { - try { - if (null != connection) - connection.close(); - } catch (Throwable ignore) { - } - } - } - - public static void sendMessage(Session session, MessageProducer producer) - throws Exception { - for (int i = 1; i <= SEND_NUMBER; i++) { - TextMessage message = session - .createTextMessage("ActiveMq 发送的消息" + i); - // 发送消息到目的地方 - System.out.println("发送消息:" + "ActiveMq 发送的消息" + i); - producer.send(message); - } - } -} -``` - -**Receiver.java** - -```java -public class Receiver { - public static void main(String[] args) { - // ConnectionFactory :连接工厂,JMS 用它创建连接 - ConnectionFactory connectionFactory; - // Connection :JMS 客户端到JMS Provider 的连接 - Connection connection = null; - // Session: 一个发送或接收消息的线程 - Session session; - // Destination :消息的目的地;消息发送给谁. - Destination destination; - // 消费者,消息接收者 - MessageConsumer consumer; - connectionFactory = new ActiveMQConnectionFactory( - ActiveMQConnection.DEFAULT_USER, - ActiveMQConnection.DEFAULT_PASSWORD, - "tcp://localhost:61616"); - try { - // 构造从工厂得到连接对象 - connection = connectionFactory.createConnection(); - // 启动 - connection.start(); - // 获取操作连接 - session = connection.createSession(Boolean.FALSE, - Session.AUTO_ACKNOWLEDGE); - // 获取session注意参数值xingbo.xu-queue是一个服务器的queue,须在在ActiveMq的console配置 - destination = session.createQueue("FirstQueue"); - consumer = session.createConsumer(destination); - while (true) { - //设置接收者接收消息的时间,为了便于测试,这里谁定为100s - TextMessage message = (TextMessage) consumer.receive(100000); - if (null != message) { - System.out.println("收到消息" + message.getText()); - } else { - break; - } - } - } catch (Exception e) { - e.printStackTrace(); - } finally { - try { - if (null != connection) - connection.close(); - } catch (Throwable ignore) { - } - } - } -} -``` - -**运行** - -先运行 Receiver.java 进行消息监听,再运行 Send.java 发送消息。 - -**输出** - -Send 的输出内容 - -``` -发送消息:Activemq 发送消息0 -发送消息:Activemq 发送消息1 -发送消息:Activemq 发送消息2 -发送消息:Activemq 发送消息3 -``` - -Receiver 的输出内容 - -``` -收到消息ActiveMQ 发送消息0 -收到消息ActiveMQ 发送消息1 -收到消息ActiveMQ 发送消息2 -收到消息ActiveMQ 发送消息3 -``` - -## 4. 资源 - -- [ActiveMQ 官网](http://activemq.apache.org/) -- [oracle 官方的 jms 介绍](https://docs.oracle.com/cd/E19575-01/819-3669/6n5sg7cgq/index.html) diff --git a/docs/distributed/mq/kafka-advanced.md b/docs/distributed/mq/kafka-advanced.md deleted file mode 100644 index e0f7f5c5..00000000 --- a/docs/distributed/mq/kafka-advanced.md +++ /dev/null @@ -1,1144 +0,0 @@ ---- -title: Kafka -date: 2018/06/13 -categories: -- 分布式 -tags: -- java -- javaweb -- 分布式 -- mq ---- - -# Kafka - -> Kafka 是一个分布式的、可水平扩展的、基于发布/订阅模式的、支持容错的消息系统。 - - - -- [1. 概述](#1-概述) - - [1.1. 分布式](#11-分布式) - - [1.2. 容错](#12-容错) - - [1.3. 提交日志](#13-提交日志) - - [1.4. 消息队列](#14-消息队列) - - [1.5. 为什么要使用消息系统](#15-为什么要使用消息系统) - - [1.6. Kafka 的关键功能](#16-kafka-的关键功能) - - [1.7. Kafka 基本概念](#17-kafka-基本概念) - - [1.8. Kafka 核心 API](#18-kafka-核心-api) - - [1.9. Topic 和日志](#19-topic-和日志) -- [2. Kafka 工作原理](#2-kafka-工作原理) -- [3. 持久化](#3-持久化) -- [4. 复制](#4-复制) -- [5. 流处理](#5-流处理) - - [5.1. 无状态处理](#51-无状态处理) - - [5.2. 有状态处理](#52-有状态处理) -- [6. Kafka 应用场景](#6-kafka-应用场景) -- [7. 幂等性](#7-幂等性) - - [7.1. 幂等性实现](#71-幂等性实现) - - [7.2. 幂等性的应用实例](#72-幂等性的应用实例) -- [8. 事务](#8-事务) - - [8.1. 事务属性理解](#81-事务属性理解) - - [8.2. 引入事务目的](#82-引入事务目的) - - [8.3. 事务操作的 API](#83-事务操作的-api) - - [8.4. 事务属性的应用实例](#84-事务属性的应用实例) - - [8.5. 生产者事务的实现](#85-生产者事务的实现) - - [8.6. 其他思考](#86-其他思考) -- [9. 资料](#9-资料) - - [9.1. 官方资料](#91-官方资料) - - [9.2. 第三方资料](#92-第三方资料) - - - -## 1. 概述 - -### 1.1. 分布式 - -分布式系统是一个由多个运行机器组成的系统,所有这些机器在一个集群中一起工作,对最终端用户表现为一个节点。 - -Kafka 的分布式意义在于:它在不同的节点上存储、接收和发送消息。 - -### 1.2. 容错 - -分布式系统一般都会设计容错机制,保证集群中几个节点出现故障时,仍能对外提供服务。 - -### 1.3. 提交日志 - -提交日志(也称为预写日志,事务日志)是仅支持附加的持久有序数据结构。您不能修改或删除记录。它从左到右读取并保证项目排序。 - -Kafka 实际上将所有的消息存储到磁盘,并在结构中对它们进行排序,以便利用顺序磁盘读取。 - -### 1.4. 消息队列 - -消息队列技术是分布式应用间交换信息的一种技术。消息队列可驻留在内存或磁盘上, 队列存储消息直到它们被应用程序读走。通过消息队列,应用程序可独立地执行--它们不需要知道彼此的位置、或在继续执行前不需要等待接收程序接收此消息。在分布式计算环境中,为了集成分布式应用,开发者需要对异构网络环境下的分布式应用提供有效的通信手段。为了管理需要共享的信息,对应用提供公共的信息交换机制是重要的。常用的消息队列技术是 Message Queue。 - -Message Queue 的通信模式: - -- **点对点**:点对点方式是最为传统和常见的通讯方式,它支持一对一、一对多、多对多、多对一等多种配置方式,支持树状、网状等多种拓扑结构。 -- **多点广播**:MQ 适用于不同类型的应用。其中重要的,也是正在发展中的是"多点广播"应用,即能够将消息发送到多个目标站点 (Destination List)。可以使用一条 MQ 指令将单一消息发送到多个目标站点,并确保为每一站点可靠地提供信息。MQ 不仅提供了多点广播的功能,而且还拥有智能消息分发功能,在将一条消息发送到同一系统上的多个用户时,MQ 将消息的一个复制版本和该系统上接收者的名单发送到目标 MQ 系统。目标 MQ 系统在本地复制这些消息,并将它们发送到名单上的队列,从而尽可能减少网络的传输量。 -- **发布/订阅 (Publish/Subscribe)**:发布/订阅功能使消息的分发可以突破目的队列地理指向的限制,使消息按照特定的主题甚至内容进行分发,用户或应用程序可以根据主题或内容接收到所需要的消息。发布/订阅功能使得发送者和接收者之间的耦合关系变得更为松散,发送者不必关心接收者的目的地址,而接收者也不必关心消息的发送地址,而只是根据消息的主题进行消息的收发。 -- **集群 (Cluster)**:为了简化点对点通讯模式中的系统配置,MQ 提供 Cluster(集群) 的解决方案。集群类似于一个域 (Domain),集群内部的队列管理器之间通讯时,不需要两两之间建立消息通道,而是采用集群 (Cluster) 通道与其它成员通讯,从而大大简化了系统配置。此外,集群中的队列管理器之间能够自动进行负载均衡,当某一队列管理器出现故障时,其它队列管理器可以接管它的工作,从而大大提高系统的高可靠性。 - -### 1.5. 为什么要使用消息系统 - -- 解耦 - 在项目启动之初来预测将来项目会碰到什么需求,是极其困难的。消息系统在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口。这允许你独立的扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束。 -- 冗余 - 有些情况下,处理数据的过程会失败。除非数据被持久化,否则将造成丢失。消息队列把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。许多消息队列所采用的"插入-获取-删除"范式中,在把一个消息从队列中删除之前,需要你的处理系统明确的指出该消息已经被处理完毕,从而确保你的数据被安全的保存直到你使用完毕。 -- 扩展性 - 因为消息队列解耦了你的处理过程,所以增大消息入队和处理的频率是很容易的,只要另外增加处理过程即可。不需要改变代码、不需要调节参数。扩展就像调大电力按钮一样简单。 -- 灵活性 & 峰值处理能力 - 在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见;如果为以能处理这类峰值访问为标准来投入资源随时待命无疑是巨大的浪费。使用消息队列能够使关键组件顶住突发的访问压力,而不会因为突发的超负荷的请求而完全崩溃。 -- 可恢复性 - 系统的一部分组件失效时,不会影响到整个系统。消息队列降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入队列中的消息仍然可以在系统恢复后被处理。 -- 顺序保证 - 在大多使用场景下,数据处理的顺序都很重要。大部分消息队列本来就是排序的,并且能保证数据会按照特定的顺序来处理。Kafka 保证一个 Partition 内的消息的有序性。 -- 缓冲 - 在任何重要的系统中,都会有需要不同的处理时间的元素。例如,加载一张图片比应用过滤器花费更少的时间。消息队列通过一个缓冲层来帮助任务最高效率的执行———写入队列的处理会尽可能的快速。该缓冲有助于控制和优化数据流经过系统的速度。 -- 异步通信 - 很多时候,用户不想也不需要立即处理消息。消息队列提供了异步处理机制,允许用户把一个消息放入队列,但并不立即处理它。想向队列中放入多少消息就放多少,然后在需要的时候再去处理它们。 - -### 1.6. Kafka 的关键功能 - -- 发布和订阅流记录,类似于消息队列或企业级消息系统。 -- 以容错、持久化的方式存储流记录。 -- 处理流记录。 - -### 1.7. Kafka 基本概念 - -- Kafka 作为一个集群运行在一台或多台可以跨越多个数据中心的服务器上。 -- Kafka 集群在称为 Topic 的类别中存储记录流。 -- Kafka 的每个记录由一个键,一个值和一个时间戳组成。 - -### 1.8. Kafka 核心 API - -
- -
- -- Producer - 允许应用程序将记录流发布到一个或多个 Kafka Topic。 -- Consumer - 允许应用程序订阅一个或多个 Topic 并处理为他们生成的记录流。 -- Streams - 允许应用程序充当流处理器,从一个或多个 Topic 中消费输入流,并将输出流生成为一个或多个输出 Topic,从而将输入流有效地转换为输出流。 -- Connector - 允许构建和运行可重复使用的生产者或消费者,将 Kafka Topic 连接到现有的应用程序或数据系统。例如,连接到关系数据库的连接器可能会捕获对表的每个更改。 - -在 Kafka 中,客户端和服务器之间的通信是采用 TCP 协议方式。 - -### 1.9. Topic 和日志 - -Topic 是一个目录名,它保存着发布记录。kafka 的 Topic 始终是多订阅者的,也就是说,一个主题可以有零个,一个或多个订阅写入数据的 Consumer。 - -在 Kafka 中,任意一个 Topic 维护一个 Partition 的日志,类似下图: - -
- -
- -每个 Partition 都是一个有序的,不可变的记录序列,不断追加到结构化的提交日志中。Partition 中的记录每个分配一个连续的 id 号,称为偏移量,用于唯一标识 Partition 内的每条记录。 - -Kafka 集群持久化保存(使用可配置的保留期限)所有发布记录——无论它们是否被消费。例如,如果保留期限被设置为两天,则在记录发布后的两天之内,它都可以被消费,超过时间后将被丢弃以释放空间。 - -
- -
- -实际上,保留在每个 Consumer 基础上的唯一元数据是该 Consumer 在日志中的抵消或位置。这个偏移量是由 Consumer 控制的:Consumer 通常会在读取记录时线性地推进其偏移量,但实际上,由于位置由 Consumer 控制,因此它可以按照喜欢的任何顺序消费记录。 - -这种功能组合意味着 Kafka Consumer 的开销很小——它们的出现对集群和其他 Consumer 没有多少影响。 - -日志中的 Partition 有多种目的。首先,它们允许日志的大小超出服务器限制的大小。每个单独的 Partition 必须适合承载它的服务器,但是一个主题可能有很多 Partition,因此它可以处理任意数量的数据。其次,它们作为并行的单位。 - -## 2. Kafka 工作原理 - -- **Broker** - Kafka 集群包含一个或多个服务器,这种服务器被称为 broker。 -- **Topic** - 每条发布到 Kafka 集群的消息都有一个类别,这个类别被称为 Topic。(物理上不同 Topic 的消息分开存储,逻辑上一个 Topic 的消息虽然保存于一个或多个 broker 上但用户只需指定消息的 Topic 即可生产或消费数据而不必关心数据存于何处)。 -- **Partition** - Parition 是物理上的概念,每个 Topic 包含一个或多个 Partition。 -- **Producer** - 负责发布消息到 Kafka broker。 -- **Consumer** - 消息消费者,向 Kafka broker 读取消息的客户端。 -- **Consumer Group** - 每个 Consumer 属于一个特定的 Consumer Group(可为每个 Consumer 指定 group name,若不指定 group name 则属于默认的 group)。 - -Producer 将消息(记录)发送到 Kafka 节点(Broker),消息由称为 Consumer 的其他应用程序处理。消息被存储在 Topic 中,并且 Consumer 订阅该主题以接收新消息。 - -随着 Topic 变得日益庞大,它们会被分割成更小的 Partition 以提高性能和可伸缩性。Kafka 保证 Partition 内的所有消息按照它们出现的顺序排序。区分特定消息的方式是通过它的偏移量,您可以将它看作普通数组索引,每个新消息都会增加一个序列号在一个 Partition 中。 - -
- -
- -Kafka 遵循发布/订阅模式。这意味着 Kafka 不会跟踪 Kafka 读取哪些记录并删除它们,而是将它们存储一段时间(例如一天)或直到满足某个大小阈值。Consumer 自己对 Kafka 进行新的消息调查并说出他们想要阅读的记录。这使得他们可以按照自己的意愿递增/递减偏移量,从而能够重播和重新处理事件。 - -Kafka 集群持久化保存(使用可配置的保留期限)所有发布记录——无论它们是否被消费。例如,如果保留期限被设置为两天,则在记录发布后的两天之内,它都可以被消费,超过时间后将被丢弃以释放空间。 - -值得注意的是,Consumer 实际上是内部拥有一个或多个 Consumer 流程的 Consumer 群体。为了避免两个进程读两次相同的消息,每个 Partition 仅与每个组的一个 Consumer 进程相关联。 - -
- -
- -## 3. 持久化 - -Kafka 实际上将其所有记录存储在磁盘中,并且不会将任何内容保留在 RAM 中。 - -- Kafka 有一个将消息分组在一起的协议。它允许网络请求将消息分组在一起以减少网络开销。服务器一气呵成的将消息的数据块持久化并立即获取较大的线性块。 -- 线性读取/写入磁盘速度很快。现代磁盘速度较慢的概念是由于大量的磁盘搜索,这在大型线性操作中不是问题。 -- 所说的线性操作由操作系统通过预读(预取大块数倍)和后写(将小的逻辑写入大物理写入)技术进行了大量优化。 -- 现代操作系统将磁盘缓存在可用 RAM 中。这被称为 pagecache。 -- 由于 Kafka 在整个流程(生产者 -> 经纪 -> 消费者)中以标准化的二进制格式存储未修改的消息,所以它可以利用零拷贝优化。这就是操作系统将数据从页面缓存直接复制到套接字时,完全绕过了 Kafka 经纪人应用程序。 - -所有这些优化都允许 Kafka 以接近网络速度传递消息。 - -## 4. 复制 - -分区数据在多个代理中复制,以便在一个代理死亡的情况下保存数据。 - -在任何时候,一个代理“拥有”一个分区,并且是应用程序通过该分区读写数据的节点。这被称为分区领导。它将它收到的数据复制到 N 个其他代理(称为追随者)。他们也存储数据,并准备在领导者节点死亡的情况下取代领导者。这就是典型的一主多从模式。 - -
- -
- -生产者/消费者如何知道分区的领导者是谁? - -对于生产者/消费者来说,从一个分区写入/读取,他们需要知道它的领导者,对吧?这些信息需要从某处获得。Kafka 将这种元数据存储在一个名为 Zookeeper 的服务中。 - -生产者和消费者都和 Zookeeper 连接并通信。Kafka 一直在摆脱这种耦合,自 0.8 和 0.9 版分别开始,客户端直接从 Kafka 经纪人那里获取元数据信息,他们自己与 Zookeeper 交谈。 - -
- -
- -## 5. 流处理 - -在 Kafka 中,流处理器是任何需要从输入主题中持续输入数据流,对该输入执行一些处理并生成输出主题的数据流(或外部服务,数据库,垃圾桶,无论哪里真的......) - -可以直接使用生产者/消费者 API 进行简单处理,但对于更复杂的转换(如将流连接在一起),Kafka 提供了一个集成的 Streams API 库。 - -此 API 旨在用于您自己的代码库中,它不在代理上运行。它与消费者 API 类似,可帮助您扩展多个应用程序的流处理工作(类似于消费者群体)。 - -
- -
- -### 5.1. 无状态处理 - -流的无状态处理是确定性处理,不依赖于任何外部。你知道,对于任何给定的数据,你将总是产生独立于其他任何东西的相同输出。 - -一个流可以被解释为一个表,一个表可以被解释为一个流。 - -流可以被解释为数据的一系列更新,其中聚合是表的最终结果。 - -如果您看看如何实现同步数据库复制,您会发现它是通过所谓的流式复制,其中表中的每个更改都发送到副本服务器。 - -Kafka 流可以用同样的方式解释 - 当从最终状态积累时的事件。这样的流聚合被保存在本地的 RocksDB 中(默认情况下),被称为 KTable。 - -
- -
- -可以将表格视为流中每个键的最新值的快照。以同样的方式,流记录可以产生一个表,表更新可以产生一个更新日志流。 - -
- -
- -### 5.2. 有状态处理 - -一些简单的操作,如 map() 或 filter() 是无状态的,并且不要求您保留有关处理的任何数据。但是,在现实生活中,你要做的大多数操作都是有状态的(例如 count()),因此需要存储当前的累积状态。 - -维护流处理器上的状态的问题是流处理器可能会失败!你需要在哪里保持这个状态才能容错? - -一种天真的做法是简单地将所有状态存储在远程数据库中,并通过网络连接到该存储。问题在于没有数据的地方和大量的网络往返,这两者都会显著减慢你的应用程序。一个更微妙但重要的问题是,您的流处理作业的正常运行时间将与远程数据库紧密耦合,并且作业不会自成体系(数据库中来自另一个团队的更改可能会破坏您的处理过程)。 - -那么更好的方法是什么? - -回想一下表和流的双重性。这使我们能够将数据流转换为与我们的处理共处一地的表格。它还为我们提供了处理容错的机制 - 通过将流存储在 Kafka 代理中。 - -流处理器可以将其状态保存在本地表(例如 RocksDB)中,该表将从输入流更新(可能是某种任意转换之后)。当进程失败时,它可以通过重放流来恢复其数据。 - -您甚至可以让远程数据库成为流的生产者,从而有效地广播更新日志,以便在本地重建表。 - -
- -
- -## 6. Kafka 应用场景 - -- 构建实时的流数据管道,在系统或应用间获取可靠数据。 -- 构建实时的流应用程序,用于转换或响应数据流。 - -正如我们已经介绍的那样,Kafka 允许您将大量消息通过集中介质存储并存储,而不用担心性能或数据丢失等问题。 - -这意味着它非常适合用作系统架构的核心,充当连接不同应用程序的集中介质。 Kafka 可以成为事件驱动架构的核心部分,并允许您真正将应用程序彼此分离。 - -
- -
- -Kafka 允许您轻松分离不同(微)服务之间的通信。利用 Streams API,现在比以往更容易编写业务逻辑,丰富了 Kafka 主题数据以便服务消费。 - -## 7. 幂等性 - -幂等性引入目的:生产者重复生产消息。生产者进行 retry 会产生重试时,会重复产生消息。有了幂等性之后,在进行 retry 重试时,只会生成一个消息。 - -### 7.1. 幂等性实现 - -#### PID 和 Sequence Number - -为了实现 Producer 的幂等性,Kafka 引入了 Producer ID(即 PID)和 Sequence Number。 - -- PID。每个新的 Producer 在初始化的时候会被分配一个唯一的 PID,这个 PID 对用户是不可见的。 -- Sequence Numbler。(对于每个 PID,该 Producer 发送数据的每个都对应一个从 0 开始单调递增的 Sequence Number。 - -Broker 端在缓存中保存了这 seq number,对于接收的每条消息,如果其序号比 Broker 缓存中序号大于 1 则接受它,否则将其丢弃。这样就可以实现了消息重复提交了。但是,只能保证单个 Producer 对于同一个的 Exactly Once 语义。不能保证同一个 Producer 一个 topic 不同的 partion 幂等。 - -![1](http://www.heartthinkdo.com/wp-content/uploads/2018/05/1-1.png) - -实现幂等之后 - -![2](http://www.heartthinkdo.com/wp-content/uploads/2018/05/2.png) - -#### 生成 PID 的流程 - -在执行创建事务时,如下: - -```java -Producer producer = new KafkaProducer(props); -``` - -会创建一个 Sender,并启动线程,执行如下 run 方法,在 maybeWaitForProducerId()中生成一个 producerId,如下: - -```java -==================================== -类名:Sender -==================================== - -void run(long now) { - if (transactionManager != null) { - try { - ........ - if (!transactionManager.isTransactional()) { - // 为idempotent producer生成一个producer id - maybeWaitForProducerId(); - } else if (transactionManager.hasUnresolvedSequences() && !transactionManager.hasFatalError()) { - ........ -``` - -### 7.2. 幂等性的应用实例 - -(1)配置属性 - -需要设置: - -- enable.idempotence,需要设置为 ture,此时就会默认把 acks 设置为 all,所以不需要再设置 acks 属性了。 - -```java -private Producer buildIdempotProducer(){ - - // create instance for properties to access producer configs - Properties props = new Properties(); - - // bootstrap.servers是Kafka集群的IP地址。多个时,使用逗号隔开 - props.put("bootstrap.servers", "localhost:9092"); - - props.put("enable.idempotence",true); - - //If the request fails, the producer can automatically retry, - props.put("retries", 3); - - //Reduce the no of requests less than 0 - props.put("linger.ms", 1); - - //The buffer.memory controls the total amount of memory available to the producer for buffering. - props.put("buffer.memory", 33554432); - - // Kafka消息是以键值对的形式发送,需要设置key和value类型序列化器 - props.put("key.serializer", - "org.apache.kafka.common.serialization.StringSerializer"); - - props.put("value.serializer", - "org.apache.kafka.common.serialization.StringSerializer"); - - Producer producer = new KafkaProducer(props); - - - return producer; -} -``` - -(2)发送消息 - -跟一般生产者一样,如下 - -```java -public void produceIdempotMessage(String topic, String message) { - // 创建Producer - Producer producer = buildIdempotProducer(); - // 发送消息 - producer.send(new ProducerRecord(topic, message)); - producer.flush(); -} -``` - -此时,因为我们并没有配置 transaction.id 属性,所以不能使用事务相关 API,如下 - -```java -producer.initTransactions(); -``` - -否则会出现如下错误: - -```java -Exception in thread “main” java.lang.IllegalStateException: Transactional method invoked on a non-transactional producer. - at org.apache.kafka.clients.producer.internals.TransactionManager.ensureTransactional(TransactionManager.java:777) - at org.apache.kafka.clients.producer.internals.TransactionManager.initializeTransactions(TransactionManager.java:202) - at org.apache.kafka.clients.producer.KafkaProducer.initTransactions(KafkaProducer.java:544) -``` - -## 8. 事务 - -### 8.1. 事务属性理解 - -事务属性是 2017 年 Kafka 0.11.0.0 引入的新特性。类似于数据库事务,只是这里的数据源是 Kafka,**kafka 事务属性是指一系列的生产者生产消息和消费者提交偏移量的操作在一个事务,或者说是是一个原子操作),同时成功或者失败**。 - -注意:在理解消息的事务时,一直处于一个错误理解就是如下代码中,把操作 db 的业务逻辑跟操作消息当成是一个事务。其实这个是有问题的,操作 DB 数据库的数据源是 DB,消息数据源是 kfaka,这是完全不同两个数据,一种数据源(如 mysql,kafka)对应一个事务,所以它们是两个独立的事务:kafka 事务指 kafka 一系列 生产、消费消息等操作组成一个原子操作;db 事务是指操作数据库的一系列增删改操作组成一个原子操作。 - -```java -void kakfa_in_tranction() { - // 1.kafa的操作:读取消息或者生产消息 - kafkaOperation(); - // 2.db操作 - dbOperation(); -} -``` - -### 8.2. 引入事务目的 - -在事务属性之前先引入了生产者幂等性,它的作用为: - -- 生产者多次发送消息可以封装成一个原子操作,要么都成功,要么失败 -- consumer-transform-producer 模式下,因为消费者提交偏移量出现问题,导致在重复消费消息时,生产者重复生产消息。需要将这个模式下消费者提交偏移量操作和生产者一系列生成消息的操作封装成一个原子操作。 - -**消费者提交偏移量导致重复消费消息的场景**:消费者在消费消息完成提交便宜量 o2 之前挂掉了(假设它最近提交的偏移量是 o1),此时执行再均衡时,其它消费者会重复消费消息(o1 到 o2 之间的消息)。 - -### 8.3. 事务操作的 API - -producer 提供了 initTransactions, beginTransaction, sendOffsets, commitTransaction, abortTransaction 五个事务方法。 - -```java - /** - * 初始化事务。需要注意的有: - * 1、前提 - * 需要保证transation.id属性被配置。 - * 2、这个方法执行逻辑是: - * (1)Ensures any transactions initiated by previous instances of the producer with the same - * transactional.id are completed. If the previous instance had failed with a transaction in - * progress, it will be aborted. If the last transaction had begun completion, - * but not yet finished, this method awaits its completion. - * (2)Gets the internal producer id and epoch, used in all future transactional - * messages issued by the producer. - * - */ - public void initTransactions(); - - /** - * 开启事务 - */ - public void beginTransaction() throws ProducerFencedException ; - - /** - * 为消费者提供的在事务内提交偏移量的操作 - */ - public void sendOffsetsToTransaction(Map offsets, - String consumerGroupId) throws ProducerFencedException ; - - /** - * 提交事务 - */ - public void commitTransaction() throws ProducerFencedException; - - /** - * 放弃事务,类似回滚事务的操作 - */ - public void abortTransaction() throws ProducerFencedException ; -``` - -### 8.4. 事务属性的应用实例 - -在一个原子操作中,根据包含的操作类型,可以分为三种情况,**前两种情况是事务引入的场景**,最后一种情况没有使用价值。 - -只有 Producer 生产消息; - -消费消息和生产消息并存,**这个是事务场景中最常用的情况**,就是我们常说的“consume-transform-produce ”模式 - -只有 consumer 消费消息,这种操作其实没有什么意义,跟使用手动提交效果一样,而且也不是事务属性引入的目的,所以一般不会使用这种情况 - -#### 相关属性配置 - -使用 kafka 的事务 api 时的一些注意事项: - -- 需要消费者的自动模式设置为 false,并且不能子再手动的进行执行 consumer#commitSync 或者 consumer#commitAsyc -- 生产者配置 transaction.id 属性 -- 生产者不需要再配置 enable.idempotence,因为如果配置了 transaction.id,则此时 enable.idempotence 会被设置为 true -- 消费者需要配置 Isolation.level。在 consume-trnasform-produce 模式下使用事务时,必须设置为 READ_COMMITTED。 - -#### 只有写 - -创建一个事务,在这个事务操作中,只有生成消息操作。代码如下: - -```java - /** - * 在一个事务只有生产消息操作 - */ - public 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(); - } - - } -``` - -创建生产者,代码如下,需要: - -- 配置 transactional.id 属性 -- 配置 enable.idempotence 属性 - -```java - /** - * 需要: - * 1、设置transactional.id - * 2、设置enable.idempotence - * @return - */ - private Producer buildProducer() { - - // create instance for properties to access producer configs - Properties props = new Properties(); - - // bootstrap.servers是Kafka集群的IP地址。多个时,使用逗号隔开 - props.put("bootstrap.servers", "localhost:9092"); - - // 设置事务id - props.put("transactional.id", "first-transactional"); - - // 设置幂等性 - props.put("enable.idempotence",true); - - //Set acknowledgements for producer requests. - props.put("acks", "all"); - - //If the request fails, the producer can automatically retry, - props.put("retries", 1); - - //Specify buffer size in config,这里不进行设置这个属性,如果设置了,还需要执行producer.flush()来把缓存中消息发送出去 - //props.put("batch.size", 16384); - - //Reduce the no of requests less than 0 - props.put("linger.ms", 1); - - //The buffer.memory controls the total amount of memory available to the producer for buffering. - props.put("buffer.memory", 33554432); - - // Kafka消息是以键值对的形式发送,需要设置key和value类型序列化器 - props.put("key.serializer", - "org.apache.kafka.common.serialization.StringSerializer"); - - props.put("value.serializer", - "org.apache.kafka.common.serialization.StringSerializer"); - - - Producer producer = new KafkaProducer(props); - - return producer; - } -``` - -#### 消费-生产并存(consume-transform-produce) - -在一个事务中,既有生产消息操作又有消费消息操作,即常说的 Consume-tansform-produce 模式。如下实例代码 - -```java - /** - * 在一个事务内,即有生产消息又有消费消息 - */ - public 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 = Maps.newHashMap(); - 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(); - } - } - } -``` - -创建消费者代码,需要: - -- 将配置中的自动提交属性(auto.commit)进行关闭 -- 而且在代码里面也不能使用手动提交 commitSync( )或者 commitAsync( ) -- 设置 isolation.level - -```java - /** - * 需要: - * 1、关闭自动提交 enable.auto.commit - * 2、isolation.level为 - * @return - */ - public Consumer buildConsumer() { - Properties props = new Properties(); - // bootstrap.servers是Kafka集群的IP地址。多个时,使用逗号隔开 - props.put("bootstrap.servers", "localhost:9092"); - // 消费者群组 - props.put("group.id", "group0323"); - // 设置隔离级别 - props.put("isolation.level","read_committed"); - // 关闭自动提交 - props.put("enable.auto.commit", "false"); - props.put("session.timeout.ms", "30000"); - props.put("key.deserializer", - "org.apache.kafka.common.serialization.StringDeserializer"); - props.put("value.deserializer", - "org.apache.kafka.common.serialization.StringDeserializer"); - KafkaConsumer consumer = new KafkaConsumer - (props); - - return consumer; - } -``` - -#### 只有读 - -创建一个事务,在这个事务操作中,只有生成消息操作,如下代码。这种操作其实没有什么意义,跟使用手动提交效果一样,无法保证消费消息操作和提交偏移量操作在一个事务。 - -```java - /** - * 在一个事务只有消息操作 - */ - public void onlyConsumeInTransaction() { - Producer producer = buildProducer(); - - // 1.初始化事务 - producer.initTransactions(); - - // 2.开启事务 - producer.beginTransaction(); - - // 3.kafka读消息的操作集合 - Consumer consumer = buildConsumer(); - while (true) { - // 3.1 接受消息 - ConsumerRecords records = consumer.poll(500); - - try { - // 3.2 do业务逻辑; - System.out.println("customer Message---"); - Map commits = Maps.newHashMap(); - for (ConsumerRecord record : records) { - // 3.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()); - - // 3.2.2 记录提交偏移量 - commits.put(new TopicPartition(record.topic(), record.partition()), - new OffsetAndMetadata(record.offset())); - } - - // 4.提交偏移量 - producer.sendOffsetsToTransaction(commits, "group0323"); - - // 5.事务提交 - producer.commitTransaction(); - - } catch (Exception e) { - // 6.放弃事务 - producer.abortTransaction(); - } - } - - } -``` - -### 8.5. 生产者事务的实现 - -#### 相关配置 - -#### 幂等性和事务性的关系 - -##### 两者关系 - -事务属性实现前提是幂等性,即在配置事务属性 transaction id 时,必须还得配置幂等性;但是幂等性是可以独立使用的,不需要依赖事务属性。 - -- 幂等性引入了 Porducer ID -- 事务属性引入了 Transaction Id 属性。、 - -设置 - -- enable.idempotence = true,transactional.id 不设置:只支持幂等性。 -- enable.idempotence = true,transactional.id 设置:支持事务属性和幂等性 -- enable.idempotence = false,transactional.id 不设置:没有事务属性和幂等性的 kafka -- enable.idempotence = false,transactional.id 设置:无法获取到 PID,此时会报错 - -##### tranaction id 、productid 和 epoch - -**一个 app 有一个 tid,同一个应用的不同实例 PID 是一样的,只是 epoch 的值不同**。如: - -![3](http://www.heartthinkdo.com/wp-content/uploads/2018/05/3-1.png) - -同一份代码运行两个实例,分步执行如下:_在实例 1 没有进行提交事务前,开始执行实例 2 的初始化事务_ - -![4](http://www.heartthinkdo.com/wp-content/uploads/2018/05/4-1-1024x458.png) - -**step1 实例 1-初始化事务**。的打印出对应 productId 和 epoch,信息如下: - -[2018-04-21 20:56:23,106] INFO [TransactionCoordinator id=0] Initialized transactionalId first-transactional with producerId 8000 and producer epoch 123 on partition \_\_transaction_state-12 (kafka.coordinator.transaction.TransactionCoordinator) - -**step2 实例 1-发送消息。** - -**step3 实例 2-初始化事务**。初始化事务时的打印出对应 productId 和 epoch,信息如下: - -18-04-21 20:56:48,373] INFO [TransactionCoordinator id=0] Initialized transactionalId first-transactional with producerId 8000 and producer epoch 124 on partition \_\_transaction_state-12 (kafka.coordinator.transaction.TransactionCoordinator) - -**step4 实例 1-提交事务**,此时报错 - -org.apache.kafka.common.errors.ProducerFencedException: Producer attempted an operation with an old epoch. Either there is a newer producer with the same transactionalId, or the producer’s transaction has been expired by the broker. - -**step5 实例 2-提交事务** - -为了避免这种错误,同一个事务 ID,只有保证如下顺序 epch 小 producer 执行 init-transaction 和 committransaction,然后 epoch 较大的 procuder 才能开始执行 init-transaction 和 commit-transaction,如下顺序: - -![80061024](http://www.heartthinkdo.com/wp-content/uploads/2018/05/80061024.png) - -有了 transactionId 后,Kafka 可保证: - -- 跨 Session 的数据幂等发送。当具有相同 Transaction ID 的新的 Producer 实例被创建且工作时,旧的且拥有相同 Transaction ID 的 Producer 将不再工作【上面的实例可以验证】。kafka 保证了关联同一个事务的所有 producer(一个应用有多个实例)必须按照顺序初始化事务、和提交事务,否则就会有问题,这保证了同一事务 ID 中消息是有序的(不同实例得按顺序创建事务和提交事务)。 - -#### 事务最佳实践-单实例的事务性 - -通过上面实例中可以看到 kafka 是跨 Session 的数据幂等发送,即如果应用部署多个实例时常会遇到上面的问题“_org.apache.kafka.common.errors.ProducerFencedException: Producer attempted an operation with an old epoch. Either there is a newer producer with the same transactionalId, or the producer’s transaction has been expired by the broker_.”,必须保证这些实例生产者的提交事务顺序和创建顺序保持一致才可以,否则就无法成功。其实,在实践中,我们更多的是**如何实现对应用单实例的事务性**。可以通过 spring-kafaka 实现思路来学习,即**每次创建生产者都设置一个不同的 transactionId 的值**,如下代码: - -在 spring-kafka 中,对于一个线程创建一个 producer,事务提交之后,还会关闭这个 producer 并清除,后续同一个线程或者新的线程重新执行事务时,此时就会重新创建 producer。 - -```java -==================================== -类名:ProducerFactoryUtils -==================================== -/** - * Obtain a Producer that is synchronized with the current transaction, if any. - * @param producerFactory the ConnectionFactory to obtain a Channel for - * @param the key type. - * @param the value type. - * @return the resource holder. - */ -public static KafkaResourceHolder getTransactionalResourceHolder( - final ProducerFactory producerFactory) { - - Assert.notNull(producerFactory, "ProducerFactory must not be null"); - - // 1.对于每一个线程会生成一个唯一key,然后根据key去查找resourceHolder - @SuppressWarnings("unchecked") - KafkaResourceHolder resourceHolder = (KafkaResourceHolder) TransactionSynchronizationManager - .getResource(producerFactory); - if (resourceHolder == null) { - // 2.创建一个消费者 - Producer producer = producerFactory.createProducer(); - // 3.开启事务 - producer.beginTransaction(); - resourceHolder = new KafkaResourceHolder(producer); - bindResourceToTransaction(resourceHolder, producerFactory); - } - return resourceHolder; -} -``` - -创建消费者代码 - -```java -==================================== -类名:DefaultKafkaProducerFactory -==================================== -protected Producer createTransactionalProducer() { - Producer producer = this.cache.poll(); - if (producer == null) { - Map configs = new HashMap<>(this.configs); - // 对于每一次生成producer时,都设置一个不同的transactionId - configs.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, - this.transactionIdPrefix + this.transactionIdSuffix.getAndIncrement()); - producer = new KafkaProducer(configs, this.keySerializer, this.valueSerializer); - // 1.初始化话事务。 - producer.initTransactions(); - return new CloseSafeProducer(producer, this.cache); - } - else { - return producer; - } -} -``` - -#### Consume-transform-Produce 的流程 - -![Snip20180504_56](http://www.heartthinkdo.com/wp-content/uploads/2018/05/Snip20180504_56.png) - -**流程 1** **:**查找 Tranaction Corordinator。 - -Producer 向任意一个 brokers 发送 FindCoordinatorRequest 请求来获取 Transaction Coordinator 的地址。 - -**流程 2:**初始化事务 initTransaction - -Producer 发送 InitpidRequest 给事务协调器,获取一个 Pid**。InitpidRequest 的处理过程是同步阻塞的,一旦该调用正确返回,Producer 就可以开始新的事务**。TranactionalId 通过 InitpidRequest 发送给 Tranciton Corordinator,然后在 Tranaciton Log 中记录这的映射关系。除了返回 PID 之外,还具有如下功能: - -- 对 PID 对应的 epoch 进行递增,这样可以保证同一个 app 的不同实例对应的 PID 是一样的,但是 epoch 是不同的。 -- 回滚之前的 Producer 未完成的事务(如果有)。 - -**流程 3:** 开始事务 beginTransaction - -执行 Producer 的 beginTransacion(),它的作用是 Producer 在本地记录下这个 transaction 的状态为开始状态。 - -注意:这个操作并没有通知 Transaction Coordinator。 - -**流程 4:** Consume-transform-produce loop - -**流程 4.0:** 通过 Consumtor 消费消息,处理业务逻辑 - -**流程 4.1:** producer 向 TransactionCordinantro 发送 AddPartitionsToTxnRequest - -在 producer 执行 send 操作时,如果是第一次给发送数据,此时会向 Trasaction Corrdinator 发送一个 AddPartitionsToTxnRequest 请求,Transaction Corrdinator 会在 transaction log 中记录下 tranasactionId 和一个映射关系,并将状态改为 begin。AddPartionsToTxnRequest 的数据结构如下: - -``` -AddPartitionsToTxnRequest => TransactionalId PID Epoch [Topic [Partition]] - TransactionalId => string - PID => int64 - Epoch => int16 - Topic => string - Partition => int32 -``` - -**流程 4.2:** producer#send 发送 ProduceRequst - -生产者发送数据,虽然没有还没有执行 commit 或者 absrot,但是此时消息已经保存到 kafka 上,可以参考如下图断点位置处,此时已经可以查看到消息了,而且即使后面执行 abort,消息也不会删除,只是更改状态字段标识消息为 abort 状态。 - -![62059279](http://www.heartthinkdo.com/wp-content/uploads/2018/05/62059279-1024x437.png) - -**流程 4.3:** AddOffsetCommitsToTxnRequest - -Producer 通过 KafkaProducer.sendOffsetsToTransaction 向事务协调器器发送一个 AddOffesetCommitsToTxnRequests: - -``` -AddOffsetsToTxnRequest => TransactionalId PID Epoch ConsumerGroupID - TransactionalId => string - PID => int64 - Epoch => int16 - ConsumerGroupID => string -``` - -在执行事务提交时,可以根据 ConsumerGroupID 来推断\_customer_offsets 主题中相应的 TopicPartions 信息。这样在 - -**流程 4.4:** TxnOffsetCommitRequest - -Producer 通过 KafkaProducer.sendOffsetsToTransaction 还会向消费者协调器 Cosumer Corrdinator 发送一个 TxnOffsetCommitRequest,在主题\_consumer_offsets 中保存消费者的偏移量信息。 - -``` -TxnOffsetCommitRequest => ConsumerGroupID - PID - Epoch - RetentionTime - OffsetAndMetadata - ConsumerGroupID => string - PID => int64 - Epoch => int32 - RetentionTime => int64 - OffsetAndMetadata => [TopicName [Partition Offset Metadata]] - TopicName => string - Partition => int32 - Offset => int64 - Metadata => string -``` - -**流程 5:** 事务提交和事务终结(放弃事务) - -通过生产者的 commitTransaction 或 abortTransaction 方法来提交事务和终结事务,这两个操作都会发送一个 EndTxnRequest 给 Transaction Coordinator。 - -**流程 5.1**:EndTxnRequest。Producer 发送一个 EndTxnRequest 给 Transaction Coordinator,然后执行如下操作: - -- Transaction Coordinator 会把 PREPARE_COMMIT or PREPARE_ABORT 消息写入到 transaction log 中记录 -- 执行流程 5.2 -- 执行流程 5.3 - -**流程 5.2**:WriteTxnMarkerRequest - -``` -WriteTxnMarkersRequest => [CoorinadorEpoch PID Epoch Marker [Topic [Partition]]] - CoordinatorEpoch => int32 - PID => int64 - Epoch => int16 - Marker => boolean (false(0) means ABORT, true(1) means COMMIT) - Topic => string - Partition => int32 -``` - -- 对于 Producer 生产的消息。Tranaction Coordinator 会发送 WriteTxnMarkerRequest 给当前事务涉及到每个的 leader,leader 收到请求后,会写入一个 COMMIT(PID) 或者 ABORT(PID)的控制信息到 data log 中 -- 对于消费者偏移量信息,如果在这个事务里面包含\_consumer-offsets 主题。Tranaction Coordinator 会发送 WriteTxnMarkerRequest 给 Transaction Coordinartor,Transaction Coordinartor 收到请求后,会写入一个 COMMIT(PID) 或者 ABORT(PID)的控制信息到 data log 中。 - -**流程 5.3:**Transaction Coordinator 会将最终的 COMPLETE_COMMIT 或 COMPLETE_ABORT 消息写入 Transaction Log 中以标明该事务结束。 - -- 只会保留这个事务对应的 PID 和 timstamp。然后把当前事务其他相关消息删除掉,包括 PID 和 tranactionId 的映射关系。 - -##### 文件类型和查看命令 - -kafka 文件主要包括 broker 的 data(主题:test)、事务协调器对应的 transaction_log(主题:\_\_tranaction_state)、偏移量信息(主题:\_consumer_offsets)三种类型。如下图 - -![1](http://www.heartthinkdo.com/wp-content/uploads/2018/05/1-2-207x300.png) - -这三种文件类型其实都是 topic 的分区,所以对于每一个目录都包含*.log、*.index、_.timeindex、_.txnindex 文件(仅这个文件是为了实现事务属性引入的)。segment 和 segmengt 对应 index、timeindex、txnindex 文件命名中序号表示的是第几个消息。如下图中,00000000000000368769.index 和 00000000000000568769.log 中“368969”就是表示文件中存储的第一个消息是 468969 个消息。 - -对于索引文案包含两部分: - -- baseOffset:索引对应 segment 文件中的第几条 message。 -- position:在 segment 中的绝对位置。 - -![67930538](http://www.heartthinkdo.com/wp-content/uploads/2018/05/67930538-300x179.png) - -查看文件内容: - -bin/kafka-run-class.sh kafka.tools.DumpLogSegments –files /Users/wuzhonghu/data/kafka-logs/firtstopic-0/00000000000000000002.log –print-data-log - -##### ControlMessage 和 Transaction markers - -Trasaction markers 就是 kafka 为了实现事务定义的 Controll Message。这个消息和数据消息都存放在 log 中,在 Consumer 读取事务消息时有用,可以参考下面章节-4.5.1 老版本-读取事务消息顺序。 - -##### Transaction Coordinator 和 Transaction Log - -Transaction Log 如下放置在“\_tranaction_state”主题下面,默认是 50 个分区,每一个分区中文件格式和 broker 存储消息是一样的,都有 log/index/timeindex 文件,如下: - -![57646045](http://www.heartthinkdo.com/wp-content/uploads/2018/05/57646045.png) - -#### 消费读取事务消息(READ_COMMITED) - -Consumer 为了实现事务,新增了一个 isolation.level 配置,有两个值如下, - -- READ_UNCOMMITTED,类似于没有事务属性的消费者。 -- READ_COMMITED,只获取执行了事务提交的消息。 - -在本小节中我们主要讲 READ_COMMITED 模式下读取消息的流程的两种版本的演化 - -##### 老版本-读取事务消息顺序 - -如下图中,按顺序保存到 broker 中消息有:事务 1 消息 T1-M1、对于事务 2 的消息有 T2-M1、事务 1 消息 T1-M2、非事务消息 M1,最终到达 client 端的循序是 M1-> T2-M1 -> T1-M1 -> T1-M2。 - -![84999567](http://www.heartthinkdo.com/wp-content/uploads/2018/05/84999567.png) - -具体步骤如下: - -- **step1** Consumer 接受到事务消息 T1-M1、T2-M2、T1-M2 和非事务消息 M1,因为没有收到事务 T1 和 T2 的控制消息,所以此时把事务相关消息 T1-M1、T2-M2、T1-M2 保存到内存,然后只把非事务消息 M1 返回给 client。 -- **step2** Consumer 接受到事务 2 的控制消息 T2-C,此时就把事务消息 T2-M1 发送给 Clinet。 -- **step3** C onsumer 接受到事务 1 的控制消息 T1-C,此时就把事务消息 T1-M1 和 T1-M2 发送给 Client - -##### 新版本-读取事务消息顺序 - -第一种方式,需要在 consumer 客户端缓存消息,当存在耗时比较长的事务时,占用客户端大量的内存资源。为了解决这个问题,通过 LSO 和 Abort Index 文件来解决这个问题,参考: - - - -(1) LSO,Last stable offset。Broker 在缓存中维护了所有处于运行状态的事务对应的 initial offsets,LSO 的值就是这些 offsets 中最小值-1。这样在 LSO 之前数据都是已经 commit 或者 abort 的数据,只有这些数据才对 Consumer 可见,即 consumer 读取数据只能读取到 LSO 的位置。 - -- LSO 并没有持久化某一个位置,而是实时计算出来的,并保存在缓存中。 - -(2)Absort Index 文件 - -Conusmer 发送 FetchRequest 中,新增了 Isolation 字段,表示是那种模式 - -``` -ReplicaId MaxWaitTime MinBytes [TopicName [Partition FetchOffset MaxBytes]] - - ReplicaId => int32 - MaxWaitTime => int32 - MinBytes => int32 - TopicName => string - Partition => int32 - FetchOffset => int64 - MaxBytes => int32 - Isolation => READ_COMMITTED | READ_UNCOMMITTED -``` - -返回数据类型为 FetchResponse 的格式为: - -ThrottleTime [TopicName [Partition ErrorCode HighwaterMarkOffset AbortedTransactions MessageSetSize MessageSet]] - -对应各个给字段类型为 - -``` - ThrottleTime => int32 - TopicName => string - Partition => int32 - ErrorCode => int16 - HighwaterMarkOffset => int64 - AbortedTransactions => [PID FirstOffset] - PID => int64 - FirstOffset => int64 - MessageSetSize => int32 -``` - -- 设置成 READ_UNCOMMITTED 模式时, the AbortedTransactions array is null. -- 设置为 READ_COMMITTED 时,the Last Stable Offset(LSO),当事务提交之后,LSO 向前移动 offset - -数据如下: - -- 存放数据的 log - -![1](http://www.heartthinkdo.com/wp-content/uploads/2018/05/1-3.png) - -- 存放 Absort Index 的内容如下: - -![3](http://www.heartthinkdo.com/wp-content/uploads/2018/05/3-2.png) - -执行读取数据流程如下: - -**step1:** 假设 consumer 读取数据的 fetched offsets 的区间是 0 到 4。 - -- 首先,broker 读取 data log 中数据 - -![11](http://www.heartthinkdo.com/wp-content/uploads/2018/05/11-1.png) - -- 然后,broker 依次读取 abort index 的内容,发现 LSO 大于等于 4 就停止。如上可以获取到 P2 对应的 offset 从 2 到 5 的消息都是被丢弃的: - -​ ![12](http://www.heartthinkdo.com/wp-content/uploads/2018/05/12-1.png) - -- 最后,broker 将上面 data log 和 abort index 中满足条件的数据返回给 consumer。 - -**step2 :**在 consumer 端根据 absrot index 中返回的内容,过滤丢弃的消息,最终给用户消息为 - -![13](http://www.heartthinkdo.com/wp-content/uploads/2018/05/13-300x103.png) - -##### Absorted Transaction Index - -在 broker 中数据中新增一个索引文件,保存 aborted tranasation 对应的 offsets,只有事务执行 abort 时,才会往这个文件新增一个记录,初始这个文件是不存在的,只有第一条 abort 时,才会创建这个文件。 - -![2](http://www.heartthinkdo.com/wp-content/uploads/2018/05/2-1-300x149.png) - -这个索引文件结构的每一行结构是 TransactionEntry: - -``` -Version => int16 - PID => int64 - FirstOffset => int64 - LastOffset => int64 - LastStableOffset => int64 -``` - -当 broker 接受到控制消息(producer 执行 commitTransaction()或者 abortTransaction())时, 执行如下操作: - -(1)计算 LSO。 - -Broker 在缓存中维护了所有处于运行状态的事务对应的 initial offsets,LSO 的值就是这些 offsets 中最小值-1。 - -举例说明下 LSO 的计算,对于一个 data log 中内如如下 - -![31](http://www.heartthinkdo.com/wp-content/uploads/2018/05/31.png) - -对应的 abort index 文件中内如如下:**LSO 是递增的** - -![32](http://www.heartthinkdo.com/wp-content/uploads/2018/05/32.png) - -(2)第二步 如果事务是提交状态,则在索引文件中新增 TransactionEntry。 - -(3)第三步 从 active 的 tranaction set 中移除这个 transaton,然后更新 LSO。 - -##### 问题 - -1、问题 1:producer 通过事务提交消息时抛异常了, 对于使用非事务的消费者,是否可以获取此消息? - -对于事务消息,必须是执行 commit 或者 abstort 之后,消息才对消费者可见,即使是非事务的消费者。只是非事务消费者相比事务消费者区别,在于可以读取执行了 absort 的消息。 - -### 8.6. 其他思考 - -1、如何保证消息不丢。 - -(1)在消费端可以建立一个日志表,和业务处理在一个事务 - -定时扫描没有表发送没有被处理的消息 - -(2)消费端,消费消息之后,修改消息表的中消息状态为已处理成功。 - -2、如何保证消息提交和业务处理在同一个事务内完成 - -在消费端可以建立一个日志表,和业务处理在一个事务 - -3、消费者角度,如何保证消息不被重复消费。 - -(1)通过 seek 操作 - -(2)通过 kafka 事务操作。 - -4、生产者角度,如何保证消息不重复生产 - -(1)kakfka 幂等性 - -## 9. 资料 - -### 9.1. 官方资料 - -[Github](https://github.com/apache/kafka) | [官方文档](https://kafka.apache.org/documentation/) - -### 9.2. 第三方资料 - -- [Kafka Manager](https://github.com/yahoo/kafka-manager) - Kafka 管理工具 -- [Kafka 剖析(一):Kafka 背景及架构介绍](http://www.infoq.com/cn/articles/kafka-analysis-part-1) -- [Thorough Introduction to Apache Kafka](https://hackernoon.com/thorough-introduction-to-apache-kafka-6fbf2989bbc1) -- [Kafak(04) Kafka生产者事务和幂等](http://www.heartthinkdo.com/?p=2040#43) \ No newline at end of file diff --git a/docs/distributed/mq/kafka-basics.md b/docs/distributed/mq/kafka-basics.md deleted file mode 100644 index facb9737..00000000 --- a/docs/distributed/mq/kafka-basics.md +++ /dev/null @@ -1,1106 +0,0 @@ ---- -title: Kafka 实战篇 -date: 2018/07/12 -categories: -- 分布式 -tags: -- java -- javaweb -- 分布式 -- mq ---- - -# Kafka 实战篇 - -> Kafka 是一个分布式的、可水平扩展的、基于发布/订阅模式的、支持容错的消息系统。 -> -> | [**官网**](http://kafka.apache.org/) | [**官方文档**](https://kafka.apache.org/documentation/) | [**Github**](https://github.com/apache/kafka) | - - - -- [1. 概述](#1-概述) - - [1.1. 简介](#11-简介) - - [1.2. 系统结构和存储结构](#12-系统结构和存储结构) - - [1.3. 小结](#13-小结) -- [2. 安装部署](#2-安装部署) - - [2.1. 下载解压](#21-下载解压) - - [2.2. 启动服务器](#22-启动服务器) - - [2.3. 停止服务器](#23-停止服务器) - - [2.4. 创建主题](#24-创建主题) - - [2.5. 生产者生产消息](#25-生产者生产消息) - - [2.6. 消费者消费消息](#26-消费者消费消息) - - [2.7. 集群部署](#27-集群部署) -- [3. API](#3-api) -- [4. 生产者(Producer)](#4-生产者producer) - - [4.1. 发送消息流程](#41-发送消息流程) - - [4.2. 发送消息方式](#42-发送消息方式) -- [5. 消费者(Consumer)](#5-消费者consumer) - - [5.1. 消费者和消费者群组](#51-消费者和消费者群组) - - [5.2. 消费消息流程](#52-消费消息流程) - - [5.3. 提交偏移量](#53-提交偏移量) - - [5.4. 从指定偏移量获取数据](#54-从指定偏移量获取数据) -- [6. Broker](#6-broker) - - [6.1. 集群控制器](#61-集群控制器) - - [6.2. 分区 leader 和 follower](#62-分区-leader-和-follower) - - [6.3. 群组协调器](#63-群组协调器) -- [7. Zookeeper 集群](#7-zookeeper-集群) - - [7.1. 节点信息](#71-节点信息) - - [7.2. zookeeper 一些总结](#72-zookeeper-一些总结) -- [8. 资料](#8-资料) - - - -## 1. 概述 - -### 1.1. 简介 - -Kafka 是一个分布式的、可水平扩展的、基于发布/订阅模式的、支持容错的 MQ 中间件。具有如下特点: - -- 伸缩性。随着数据量增长,可以通过对 broker 集群水平扩展来提高系统性能。 -- 高性能。通过横向扩展生产者、消费者(通过消费者群组实现)和 broker(通过扩展实现系统伸缩性)可以轻松处理巨大的消息流。 -- 消息持久化。基于磁盘的数据存储,消息不会丢失。 - -通过 Partition 来实现多个生产者(同一个 topic 消息根据 key 放置到一个 Partition 中)和多个消费者(一个 Partition 由消费者群组中每一个消费者来负责)。 - -### 1.2. 系统结构和存储结构 - -#### 系统结构 - -producer 采用 push 方式向 broker 发送消息;customer 采用 pull 方式从 broker 接受消息。 - -- **Producer** - 消息生产者,负责发布消息到 Kafka Broker。 -- **Consumer** - 消息消费者,从 Kafka Broker 读取消息的客户端。 -- **Consumer Group** - 每个 Consumer 属于一个特定的 Consumer Group,若不指定 group name 则属于默认的 group。**在同一个 Group 中,每一个 customer 可以消费多个 Partition,但是一个 Partition 只能指定给一个这个 Group 中一个 Customer**。 -- **Broker** - Kafka 集群包含一个或多个服务器,这种服务器被称为 broker - -
- -
- -#### Topic 的存储结构 - -
- -
- -- **Topic** - 每条发布到 Kafka 集群的消息都有一个类别,这个类别被称为 Topic。(物理上不同 Topic 的消息分开存储,逻辑上一个 Topic 的消息虽然保存于一个或多个 broker 上,但用户只需指定消息的 Topic 即可生产或消费数据而不必关心数据存于何处)。 -- **Partition** - Partition 是物理上的概念,每个 Topic 包含一个或多个 Partition。为了使得 Kafka 的吞吐率可以线性提高,物理上把 Topic 分成一个或多个 Partition,每个 Partition 在物理上对应一个文件夹,该文件夹下存储这个 Partition 的所有消息和索引文件。 -- **Segment** - 是 Partition 目录下的文件,保存存储消息。 -- **索引** - 方便查询 segment - -#### Segment 文件格式 - -可以把 topic 当做一个数据表,表中每一个记录都是 key,value 形式,如下图。 - -注意: 必须要有一个 key,如果没有,则默认会生成一个 key,可以把 key 当做一个消息的标识,同一个 key 可能有多条数据。 - -
- -
- -### 1.3. 小结 - -**(1)一个主题存在多个分区,每一分区属于哪个 leader broker?** - -在任意一个 broker 机器都有每一个分区所属 leader 的信息,所以可以通过访问任意一个 broker 获取这些信息。 - -**(2)每个消费者群组对应的分区偏移量的元数据存储在哪里。** - -最新版本保存在 kafka 中,对应的主题是\_consumer_offsets。老版本是在 zookeeper 中。 - -**(3)假设某一个消息处理业务逻辑失败了。是否还可以继续想下执行?如果可以的话,那么此时怎么保证这个消息还会继续被处理呢?** - -答案是:正常情况下无法再处理有问题的消息。 - -这里举一个例子,如 M1->M2->M3->M4,假设第一次 poll 时,得到 M1 和 M2,M1 处理成功,M2 处理失败,我们采用提交方式为处理一个消息就提交一次,此时我们提交偏移量是 offset1,但是当我们第二次执行 poll 时,此时只会获取到 M3 和 M4,因为 poll 的时候是根据本地偏移量来获取的,不是 kafka 中保存的初始偏移量。解决这个问题方法是通过 seek 操作定位到 M2 的位置,此时再执行 poll 时就会获取到 M2 和 M3。 - -**(4)当一个消费者执行了 close 之后,此时会执行再均衡,那么在均衡是在哪里发生的呢?其他同组的消费者如何感知到?** - -是通过群组中成为群主的消费者执行再均衡,执行完毕之后,通过群组协调器把每一个消费者负责分区信息发送给消费者,每一个消费者只能知道它负责的分区信息。 - -**(5)如何保证时序性** - -因为 kafkaf 只保证一个分区内的消息才有时序性,所以只要消息属于同一个 topic 且在同一个分区内,就可以保证 kafka 消费消息是有顺序的了。 - -## 2. 安装部署 - -环境要求:JDK8、ZooKeeper - -### 2.1. 下载解压 - -进入官方下载地址:http://kafka.apache.org/downloads,选择合适版本。 - -解压到本地: - -``` -> tar -xzf kafka_2.11-1.1.0.tgz -> cd kafka_2.11-1.1.0 -``` - -现在您已经在您的机器上下载了最新版本的 Kafka。 - -### 2.2. 启动服务器 - -由于 Kafka 依赖于 ZooKeeper,所以运行前需要先启动 ZooKeeper - -``` -> bin/zookeeper-server-start.sh config/zookeeper.properties -[2013-04-22 15:01:37,495] INFO Reading configuration from: config/zookeeper.properties (org.apache.zookeeper.server.quorum.QuorumPeerConfig) -... -``` - -然后,启动 Kafka - -``` -> bin/kafka-server-start.sh config/server.properties -[2013-04-22 15:01:47,028] INFO Verifying properties (kafka.utils.VerifiableProperties) -[2013-04-22 15:01:47,051] INFO Property socket.send.buffer.bytes is overridden to 1048576 (kafka.utils.VerifiableProperties) -... -``` - -### 2.3. 停止服务器 - -执行所有操作后,可以使用以下命令停止服务器 - -``` -$ bin/kafka-server-stop.sh config/server.properties -``` - -### 2.4. 创建主题 - -创建一个名为 test 的 Topic,这个 Topic 只有一个分区以及一个备份: - -``` -> bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test -``` - -### 2.5. 生产者生产消息 - -运行生产者,然后可以在控制台中输入一些消息,这些消息会发送到服务器: - -``` -> bin/kafka-console-producer.sh --broker-list localhost:9092 --topic test -This is a message -This is another message -``` - -### 2.6. 消费者消费消息 - -启动消费者,然后获得服务器中 Topic 下的消息: - -``` -> bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic test --from-beginning -This is a message -This is another message -``` - -### 2.7. 集群部署 - -复制配置为多份(Windows 使用 copy 命令代理): - -``` -> cp config/server.properties config/server-1.properties -> cp config/server.properties config/server-2.properties -``` - -修改配置: - -``` -config/server-1.properties: - broker.id=1 - listeners=PLAINTEXT://:9093 - log.dir=/tmp/kafka-logs-1 - -config/server-2.properties: - broker.id=2 - listeners=PLAINTEXT://:9094 - log.dir=/tmp/kafka-logs-2 -``` - -其中,broker.id 这个参数必须是唯一的。 - -端口故意配置的不一致,是为了可以在一台机器启动多个应用节点。 - -根据这两份配置启动三个服务器节点: - -``` -> bin/kafka-server-start.sh config/server.properties & -... -> bin/kafka-server-start.sh config/server-1.properties & -... -> bin/kafka-server-start.sh config/server-2.properties & -... -``` - -创建一个新的 Topic 使用 三个备份: - -``` -> bin/kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 1 --topic my-replicated-topic -``` - -查看主题: - -``` -> bin/kafka-topics.sh --describe --zookeeper localhost:2181 --topic my-replicated-topic -Topic:my-replicated-topic PartitionCount:1 ReplicationFactor:3 Configs: - Topic: my-replicated-topic Partition: 0 Leader: 1 Replicas: 1,2,0 Isr: 1,2,0 -``` - -- leader - 负责指定分区的所有读取和写入的节点。每个节点将成为随机选择的分区部分的领导者。 -- replicas - 是复制此分区日志的节点列表,无论它们是否为领导者,或者即使它们当前处于活动状态。 -- isr - 是“同步”复制品的集合。这是副本列表的子集,该列表当前处于活跃状态并且已经被领导者捕获。 - -## 3. API - -Stream API 的 maven 依赖: - -```xml - - org.apache.kafka - kafka-streams - 1.1.0 - -``` - -其他 API 的 maven 依赖: - -```xml - - org.apache.kafka - kafka-clients - 1.1.0 - -``` - -## 4. 生产者(Producer) - -### 4.1. 发送消息流程 - -发送消息流程如下图,需要注意的有: - -- 分区器 Partitioner,分区器决定了一个消息被分配到哪个分区。在我们创建消息时,我们可以选择性指定一个键值 key 或者分区 Partition,如果传入的是 key,则通过图中的分区器 Partitioner 选择一个分区来保存这个消息;如果 key 和 Partition 都没有指定,则会默认生成一个 key。 -- 批次传输。**批次,就是一组消息,这些消息属于同一个主题和分区**。发送时,会把消息分成批次 Batch 传输,如果每一个消息发送一次,会导致大量的网路开销, -- 如果消息成功写入 kafka,就返回一个 RecoredMetaData 对象,它包含了主题和分区信息,以及记录在分区里的偏移量。 -- 如果消息发送失败,可以进行重试,重试次数可以在配置中指定。 - -
- -
- -生产者在向 broker 发送消息时是怎么确定向哪一个 broker 发送消息? - -- 生产者客户端会向任一个 broker 发送一个元数据请求(MetadataRequest),获取到每一个分区对应的 leader 信息,并缓存到本地。 -- step2:生产者在发送消息时,会指定 Partition 或者通过 key 得到到一个 Partition,然后根据 Partition 从缓存中获取相应的 leader 信息。 - -
- -
- -### 4.2. 发送消息方式 - -#### 发送并忘记(fire-and-forget) - -代码如下,直接通过 send 方法来发送 - -```java -ProducerRecord record = - new ProducerRecord<>("CustomerCountry", "Precision Products", "France"); - try { - producer.send(record); - } catch (Exception e) { - e.printStackTrace(); -} -``` - -#### 同步发送 - -代码如下,与“发送并忘记”的方式区别在于多了一个 get()方法,会一直阻塞等待 broker 返回结果: - -```java -ProducerRecord record = - new ProducerRecord<>("CustomerCountry", "Precision Products", "France"); - try { - producer.send(record).get(); - } catch (Exception e) { - e.printStackTrace(); -} -``` - -#### 异步发送 - -代码如下,异步方式相对于“发送并忘记”的方式的不同在于,在异步返回时可以执行一些操作,如记录错误或者成功日志。 - -首先,定义一个 callback - -```java -private class DemoProducerCallback implements Callback { - @Override - public void onCompletion(RecordMetadata recordMetadata, Exception e) { - if (e != null) { - e.printStackTrace(); - } - } -} -``` - -然后,使用这个 callback - -```java -ProducerRecord record = - new ProducerRecord<>("CustomerCountry", "Biomedical Materials", "USA"); -producer.send(record, new DemoProducerCallback()); -``` - -#### 发送消息示例 - -```java -import java.util.Properties; -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; - -/** - * 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(); - } - } -} -``` - -## 5. 消费者(Consumer) - -### 5.1. 消费者和消费者群组 - -#### 消费者介绍 - -消费者以**pull 方式**从 broker 拉取消息,消费者可以订阅一个或多个主题,然后按照消息生成顺序(**kafka 只能保证分区中消息的顺序**)读取消息。 - -**一个消息消息只有在所有跟随者节点都进行了同步,才会被消费者获取到**。如下图,只能消费 Message0、Message1、Message2: - -
- -
- -#### 消费者分区组 - -消费者群组可以实现并发的处理消息。一个消费者群组作为消费一个 topic 消息的单元,每一个 Partition 只能隶属于一个消费者群组中一个 customer,如下图 - -
- -
- -#### 消费者区组的再均衡 - -当在群组里面 新增/移除消费者 或者 新增/移除 kafka 集群 broker 节点 时,群组协调器 Broker 会触发再均衡,重新为每一个 Partition 分配消费者。**再均衡期间,消费者无法读取消息,造成整个消费者群组一小段时间的不可用。** - -- 新增消费者。customer 订阅主题之后,第一次执行 poll 方法 -- 移除消费者。执行 customer.close()操作或者消费客户端宕机,就不再通过 poll 向群组协调器发送心跳了,当群组协调器检测次消费者没有心跳,就会触发再均衡。 -- 新增 broker。如重启 broker 节点 -- 移除 broker。如 kill 掉 broker 节点。 - -**再均衡是是通过消费者群组中的称为“群主”消费者客户端进行的**。什么是群主呢?“群主”就是第一个加入群组的消费者。消费者第一次加入群组时,它会向群组协调器发送一个 JoinGroup 的请求,如果是第一个,则此消费者被指定为“群主”(群主是不是和 qq 群很想啊,就是那个第一个进群的人)。 - -群主分配分区的过程如下: - -1. 群主从群组协调器获取群组成员列表,然后给每一个消费者进行分配分区 Partition。 -2. 两个分配策略:Range 和 RoundRobin。 - - Range 策略,就是把若干个连续的分区分配给消费者,如存在分区 1-5,假设有 3 个消费者,则消费者 1 负责分区 1-2,消费者 2 负责分区 3-4,消费者 3 负责分区 5。 - - RoundRoin 策略,就是把所有分区逐个分给消费者,如存在分区 1-5,假设有 3 个消费者,则分区 1->消费 1,分区 2->消费者 2,分区 3>消费者 3,分区 4>消费者 1,分区 5->消费者 2。 -3. 群主分配完成之后,把分配情况发送给群组协调器。 -4. 群组协调器再把这些信息发送给消费者。**每一个消费者只能看到自己的分配信息,只有群主知道所有消费者的分配信息**。 - -
- -
- -### 5.2. 消费消息流程 - -#### 消费流程 demo - -具体步骤如下 - -- step1 创建消费者。 -- step2 订阅主题。除了订阅主题方式外还有使用指定分组的模式,但是常用方式都是订阅主题方式 -- stpe3 轮询消息。通过 poll 方法轮询。 -- stpe4 关闭消费者。在不用消费者之后,会执行 close 操作。close 操作会关闭 socket,并触发当前消费者群组的再均衡。 - -```java - // 1.构建KafkaCustomer - Consumer consumer = buildCustomer(); - - // 2.设置主题 - consumer.subscribe(Arrays.asList(topic)); - - // 3.接受消息 - try { - while (true) { - ConsumerRecords records = consumer.poll(500); - System.out.println("customer Message---"); - for (ConsumerRecord record : records) - - // 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()); - } - } finally { - // 4.关闭消息 - consumer.close(); - } -``` - -创建消费者的代码如下: - -```java -public Consumer buildCustomer() { - Properties props = new Properties(); - // bootstrap.servers是Kafka集群的IP地址。多个时,使用逗号隔开 - props.put("bootstrap.servers", "localhost:9092"); - // 消费者群组 - props.put("group.id", "test"); - props.put("enable.auto.commit", "true"); - props.put("auto.commit.interval.ms", "1000"); - props.put("session.timeout.ms", "30000"); - props.put("key.deserializer", - "org.apache.kafka.common.serialization.StringDeserializer"); - props.put("value.deserializer", - "org.apache.kafka.common.serialization.StringDeserializer"); - KafkaConsumer consumer = new KafkaConsumer - (props); - - return consumer; -} -``` - -#### 消费消息方式 - -分为订阅主题和指定分组两种方式: - -- 消费者分组模式。通过订阅主题方式时,消费者必须加入到消费者群组中,即消费者必须有一个自己的分组; -- 独立消费者模式。这种模式就是消费者是独立的不属于任何消费者分组,自己指定消费那些 Partition。 - -1、订阅主题方式 - -```java -consumer.subscribe(Arrays.asList(topic)); -``` - -2、独立消费者模式 - -通过 consumer 的 assign(Collection partitions)方法来为消费者指定分区。 - -```java -public void consumeMessageForIndependentConsumer(String topic){ - // 1.构建KafkaCustomer - Consumer consumer = buildCustomer(); - - // 2.指定分区 - // 2.1获取可用分区 - List partitionInfoList = buildCustomer().partitionsFor(topic); - // 2.2指定分区,这里是指定了所有分区,也可以指定个别的分区 - if(null != partitionInfoList){ - List partitions = Lists.newArrayList(); - for(PartitionInfo partitionInfo : partitionInfoList){ - partitions.add(new TopicPartition(partitionInfo.topic(),partitionInfo.partition())); - } - consumer.assign(partitions); - } - - // 3.接受消息 - while (true) { - ConsumerRecords records = consumer.poll(500); - System.out.println("consume Message---"); - for (ConsumerRecord record : records) { - - // 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()); - - // 异步提交 - consumer.commitAsync(); - - - } - } -} -``` - -#### 轮询获取消息 - -通过 poll 来获取消息,但是获取消息时并不是立刻返回结果,需要考虑两个因素: - -- 消费者通过 customer.poll(time)中设置的等待时间 -- broker 会等待累计一定量数据,然后发送给消费者。这样可以减少网络开销。 - -
- -
- -poll 处了获取消息外,还有其他作用,如下: - -- 发送心跳信息。消费者通过向被指派为群组协调器的 broker 发送心跳来维护他和群组的从属关系,当机器宕掉后,群组协调器触发再分配 - -### 5.3. 提交偏移量 - -#### 偏移量和提交 - -(1)偏移量 - -偏移量 offeset,是指一个消息在分区中位置。在通过生产者向 kafka 推送消息返回的结果中包含了这个偏移量值,或者在消费者拉取信息时,也会包含消息的偏移量信息。 - -(2)提交的解释 - -我们把消息的偏移量提交到 kafka 的操作叫做**提交或提交偏移量。** - -(3)偏移量的应用 - -目前会有两个位置记录这个偏移量: - -a. Kafka Broker 保存。消费者通过提交操作,把读取分区中最新消息的偏移量更新到 kafka 服务器端(老版本的 kafka 是保存在 zookeeper 中),即消费者往一个叫做\_consumer_offset 的特殊主题发送消息,消息里面包消息的偏移量信息,**并且该主题配置清理策略是 compact,即对于每一个 key 只保存最新的值(key 由 groupId、topic 和 partition 组成)**。关于提交操作在本节进行讨论。 - -如果消费者一直处于运行状态,这个偏移量是没有起到作用,只有当加入或者删除一个群组里消费者,然后进行再均衡操作只有,此时为了可以继续之前工作,新的消费者需要知道上一个消费者处理这个分区的位置信息。 - -b. 消费者客户端保存。消费者客户端会保存 poll()每一次执行后的最后一个消息的偏移量,这样每次执行轮询操作 poll 时,都从这个位置获取信息。这个信息修改可以通过后续小节中三个 seek 方法来修改。 - -(4)提交时会遇到两个问题 - -a. 重复处理 - -当提交的偏移量小于客户端处理的最后一个消息的偏移量时,会出现重复处理消息的问题,如下图 - -
- -
- -b. 消息丢失 - -当提交的偏移量大于客户端处理的最后端最后一个消息的偏移量,会出现消息丢失的问题,如下图: - -
- -
- -(5)提交方式 - -主要分为:自动提交和手动提交。 - -a. 自动提交 - -auto.commit.commit ,默认为 true 自动提交,自动提交时通过轮询方式来做,时间间通过 auto.commit.interval.ms 属性来进行设置。 - -b. 手动提交 - -除了自动提交,还可以进行手动提交,手动提交就是通过代码调用函数的方式提交,在使用手动提交时首先需要将 auto.commit.commit 设置为 false,目前有三种方式:同步提交、异步提交、同步和异步结合。 - -#### 同步提交 - -可以通过 commitSync 来进行提交,**同步提交会一直提交直到成功**。如下 - -```java -public void customerMessageWithSyncCommit(String topic) { - // 1.构建KafkaCustomer - Consumer consumer = buildCustomer(); - - // 2.设置主题 - consumer.subscribe(Arrays.asList(topic)); - - // 3.接受消息 - while (true) { - ConsumerRecords records = consumer.poll(500); - System.out.println("customer Message---"); - for (ConsumerRecord record : records) { - - // 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()); - - - // 同步提交 - try { - consumer.commitSync(); - } catch (Exception e) { - logger.error("commit error"); - } - } - } -} -``` - -#### 异步提交 - -同步提交一个缺点是,在进行提交 commitAysnc()会阻塞整个下面流程。所以引入了异步提交 commitAsync(),如下代码,这里定义了 OffsetCommitCallback,也可以只进行 commitAsync(),不设置任何参数。 - -```java -public void customerMessageWithAsyncCommit(String topic) { - // 1.构建KafkaCustomer - Consumer consumer = buildCustomer(); - - // 2.设置主题 - consumer.subscribe(Arrays.asList(topic)); - - // 3.接受消息 - while (true) { - ConsumerRecords records = consumer.poll(500); - System.out.println("customer Message---"); - for (ConsumerRecord record : records) { - - // 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()); - - // 异步提交 - consumer.commitAsync(new OffsetCommitCallback() { - public void onComplete(Map offsets, Exception e) { - if (e != null) { - logger.error("Commit failed for offsets{}", offsets, e); - } - } - }); - - - } - } -} -``` - -#### 同步和异步提交 - -代码如下: - -```java -public void customerMessageWithSyncAndAsyncCommit(String topic) { - // 1.构建KafkaCustomer - Consumer consumer = buildCustomer(); - - // 2.设置主题 - consumer.subscribe(Arrays.asList(topic)); - - // 3.接受消息 - try { - while (true) { - ConsumerRecords records = consumer.poll(500); - System.out.println("customer Message---"); - for (ConsumerRecord record : records) { - - // 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()); - - // 异步提交 - consumer.commitAsync(); - } - } - } catch (Exception e) { - - } finally { - // 同步提交 - try { - consumer.commitSync(); - } finally { - consumer.close(); - } - } -} -``` - -### 5.4. 从指定偏移量获取数据 - -我们读取消息是通过 poll 方法。它根据消费者客户端本地保存的当前偏移量来获取消息。如果我们需要从指定偏移量位置获取数据,此时就需要修改这个值为我们想要读取消息开始的地方,目前有如下三个方法: - -- seekToBeginning(Collection partitions)。可以修改分区当前偏移量为分区的起始位置、 -- seekToEnd(Collection partitions)。可以修改分区当前偏移量为分区的末尾位置 -- seek(TopicPartition partition, long offset); 可以修改分区当前偏移量为分区的起始位置 - -通过 seek(TopicPartition partition, long offset)可以实现处理消息和提交偏移量在一个事务中完成。思路就是需要在可短建立一个数据表,保证处理消息和和消息偏移量位置写入到这个数据表在一个事务中,此时就可以保证处理消息和记录偏移量要么同时成功,要么同时失败。代码如下: - -```java - consumer.subscribe(topic); - // 1.第一次调用pool,加入消费者群组 - consumer.poll(0); - // 2.获取负责的分区,并从本地数据库读取改分区最新偏移量,并通过seek方法修改poll获取消息的位置 - for (TopicPartition partition: consumer.assignment()) - consumer.seek(partition, getOffsetFromDB(partition)); - - while (true) { - ConsumerRecords records = - consumer.poll(100); - for (ConsumerRecord record : records) - { - processRecord(record); - storeRecordInDB(record); - storeOffsetInDB(record.topic(), record.partition(), - record.offset()); - } - commitDBTransaction(); - } -``` - -## 6. Broker - -### 6.1. 集群控制器 - -控制器除了具有一般 broker 的功能,还负责分区 leader 的选举。 - -### 6.2. 分区 leader 和 follower - -Kafka 在 0.8 以前的版本中,如果一个 broker 机器宕机了,其上面的 Partition 都不能用了。为了实现 High Availablity,引入了复制功能,即一个 Partition 还会在其他的 broker 上面进行备份。为了实现复制功能,引入了分区 leader 和 follower: - -(1)Leader 作用 - -生产者和消费者请求都会经过这个 leader。Producer 和 Concumer 往一个 Partition 写入和读取消息时,都会首先查找这个 Partition 的 leader - -保存那些 follower 节点的状态与自己是一致的。 - -(2)follower 作用: - -定时通过类似消费者的 poll 方法从 leader 中获取消息,进行备份 - -同一个 topic 的不同 Partition 会分布在多个 broker 上,而且一个 Partition 还会在其他的 broker 上面进行备份,Producer 在发布消息到某个 Partition 时,先找到该 Partition 的 Leader,然后向这个 leader 推送消息;每个 Follower 都从 Leader 拉取消息,拉取消息成功之后,向 leader 发送一个 ack 确认。如下一个流程图: - -
- -
- -### 6.3. 群组协调器 - -群组协调器,顾名思义就是维护消费者群组 ,消费者通过向被指派为群组协调器的 broker(不同的群组可以有不同的协调器)发送心跳来维护它们和群组的从属关系,以及它们对分区的所有权。 - -1、触发再均衡 - -只要消费者以正常的时间间隔发送心跳,就会被认为是活跃的。消费者是通过 poll 获取消息时,发送的心跳的,当消费者客户端宕机之后,群组协调器在一段内没有收到心跳,则此时会认为消费者已死亡,然后触发一次再均衡。具体再均衡流程,可以参考上面的“再均衡”小节。 - -## 7. Zookeeper 集群 - -### 7.1. 节点信息 - -参考: - -Zookeeper 保存的就是节点信息和节点状态,不会保存 kafka 的消息信息,节点信息包括: - -- broker。broer 启动时在 zookeeper 注册、并通过 watcher 监听 broker 节点变化;并且还记录 topic 和 Partition 的信息。 -- consumers,消费者节点信息 -- admin -- config -- controller 和 controloer_epoch - -
- -
- -#### 5.1. 1 broker - -1、Topic 的注册信息 - -作用:在创建 zookeeper 时,注册 topic 的 Partition 信息,包括每一个分区的复制节点 id。 - -路径:/brokers/topics/[topic] - -数据格式: - -```json -Schema: -{ "fields" : - [ {"name": "version", "type": "int", "doc": "version id"}, - {"name": "partitions", - "type": {"type": "map", - "values": {"type": "array", "items": "int", "doc": "a list of replica ids"}, - "doc": "a map from partition id to replica list"}, - } - ] -} -Example: -{ - "version": 1, - "partitions": {"0": [0, 1, 3] } } # 分区0的对应的复制节点是0、1、3. -} -``` - -2、分区信息 - -作用:记录分区信息,如分区的 leader 信息 - -路径信息:/brokers/topics/[topic]/partitions/[partitionId]/state - -格式: - -```json -Schema: -{ "fields": - [ {"name": "version", "type": "int", "doc": "version id"}, - {"name": "isr", - "type": {"type": "array", - "items": "int", - "doc": "an array of the id of replicas in isr"} - }, - {"name": "leader", "type": "int", "doc": "id of the leader replica"}, - {"name": "controller_epoch", "type": "int", "doc": "epoch of the controller that last updated the leader and isr info"}, - {"name": "leader_epoch", "type": "int", "doc": "epoch of the leader"} - ] -} - -Example: -{ - "version": 1, - "isr": [0,1], - "leader": 0, - "controller_epoch": 1, - "leader_epoch": 0 -} -``` - -3 broker 信息 - -作用:在 borker 启动时,向 zookeeper 注册节点信息 - -路径:/brokers/ids/[brokerId] - -数据格式: - -```json -Schema: -{ "fields": - [ {"name": "version", "type": "int", "doc": "version id"}, - {"name": "host", "type": "string", "doc": "ip address or host name of the broker"}, - {"name": "port", "type": "int", "doc": "port of the broker"}, - {"name": "jmx_port", "type": "int", "doc": "port for jmx"} - ] -} - -Example: -{ - "version": 1, - "host": "192.168.1.148", - "port": 9092, - "jmx_port": 9999 -} -``` - -#### 5.1.2 controller 和 controller_epoch - -1、 控制器的 epoch: - -/controller_epoch -> int (epoch) - -2、控制器的注册信息: - -/controller -> int (broker id of the controller) - -#### 5.1.3 consumer - -1.消费者注册信息: - -路径:/consumers/[groupId]/ids/[consumerId] - -数据格式: - -```json -Schema: -{ "fields": - [ {"name": "version", "type": "int", "doc": "version id"}, - {"name": "pattern", "type": "string", "doc": "can be of static, white_list or black_list"}, - {"name": "subscription", "type" : {"type": "map", "values": {"type": "int"}, - "doc": "a map from a topic or a wildcard pattern to the number of streams"} } ] -} - -Example: -A static subscription: -{ - "version": 1, - "pattern": "static", - "subscription": {"topic1": 1, "topic2": 2} -} - - -A whitelist subscription: -{ - "version": 1, - "pattern": "white_list", - "subscription": {"abc": 1} -} - -A blacklist subscription: -{ - "version": 1, - "pattern": "black_list", - "subscription": {"abc": 1} -} -``` - -#### 5.1.4 admin - -\1. Re-assign partitions - -路径:/admin/reassign_partitions - -数据格式: - -```json -{ - "fields":[ - { - "name":"version", - "type":"int", - "doc":"version id" - }, - { - "name":"partitions", - "type":{ - "type":"array", - "items":{ - "fields":[ - { - "name":"topic", - "type":"string", - "doc":"topic of the partition to be reassigned" - }, - { - "name":"partition", - "type":"int", - "doc":"the partition to be reassigned" - }, - { - "name":"replicas", - "type":"array", - "items":"int", - "doc":"a list of replica ids" - } - ], - } - "doc":"an array of partitions to be reassigned to new replicas" - } - } - ] -} - -Example: -{ - "version": 1, - "partitions": - [ - { - "topic": "Foo", - "partition": 1, - "replicas": [0, 1, 3] - } - ] -} -``` - -\2. Preferred replication election - -路径:/admin/preferred_replica_election - -数据格式: - -```json -{ - "fields":[ - { - "name":"version", - "type":"int", - "doc":"version id" - }, - { - "name":"partitions", - "type":{ - "type":"array", - "items":{ - "fields":[ - { - "name":"topic", - "type":"string", - "doc":"topic of the partition for which preferred replica election should be triggered" - }, - { - "name":"partition", - "type":"int", - "doc":"the partition for which preferred replica election should be triggered" - } - ], - } - "doc":"an array of partitions for which preferred replica election should be triggered" - } - } - ] -} - -Example: - -{ - "version": 1, - "partitions": - [ - { - "topic": "Foo", - "partition": 1 - }, - { - "topic": "Bar", - "partition": 0 - } - ] -} -``` - -\3. Delete topics - -/admin/delete_topics/[topic_to_be_deleted] (the value of the path in empty) - -#### 5.1.5 config - -1 Topic Configuration - -/config/topics/[topic_name] - -数据格式: - -```json -{ - "version": 1, - "config": { - "config.a": "x", - "config.b": "y", - ... - } -} -``` - -### 7.2. zookeeper 一些总结 - -离开了 Zookeeper, Kafka 不能对 Topic 进行新增操作, 但是仍然可以 produce 和 consume 消息. - -## 8. 资料 - -- [Kafka(03) Kafka 介绍](http://www.heartthinkdo.com/?p=2006#233) diff --git a/docs/distributed/mq/rocketmq-advanced.md b/docs/distributed/mq/rocketmq-advanced.md deleted file mode 100644 index 3d84c76a..00000000 --- a/docs/distributed/mq/rocketmq-advanced.md +++ /dev/null @@ -1,218 +0,0 @@ ---- -title: RocketMQ 原理篇 -date: 2018/07/17 -categories: -- javaweb -tags: -- java -- javaweb -- mq ---- - -# RocketMQ 原理篇 - - - -- [架构](#架构) - - [NameServer](#nameserver) - - [Broker](#broker) - - [Producer](#producer) - - [Consumer](#consumer) -- [关键特性以及其实现原理](#关键特性以及其实现原理) - - [顺序消息](#顺序消息) - - [消息重复](#消息重复) - - [事务消息](#事务消息) -- [资料](#资料) - - - -## 架构 - -
- -
更详细信息可以参考官方文档:[here](http://rocketmq.apache.org/rocketmq/four-methods-to-feed-name-server-address-list/) - -### Broker - -Broker 通过提供轻量级的 TOPIC 和 QUEUE 机制来处理消息存储。它们支持 Push 和 Pull 模型,包含容错机制(2 个副本或 3 个副本),并提供强大的峰值填充和以原始时间顺序累积数千亿条消息的能力。此外,Brokers 还提供灾难恢复,丰富的指标统计和警报机制。 - -Broker 有几个重要的子模块: - -- Remoting Module - 即代理的条目,处理来自客户端的请求。 -- Client Manager - 管理客户(生产者/消费者)并维护消费者的主题订阅。 -- Store 服务 - 提供简单的 API 来存储或查询物理磁盘中的消息。 -- HA 服务 - 提供主代理和从代理之间的数据同步功能。 -- Index 服务 - 按指定密钥构建消息索引,并提供快速消息查询。 - -
- -
- -### Producer - -Producers 支持分布式部署。Distributed Producers 通过多种负载均衡模式向 Broker 集群发送消息。发送过程支持快速故障并具有低延迟。 - -### Consumer - -Consumer 也支持 Push 和 Pull 模型中的分布式部署。它还支持群集消费和消息广播。它提供实时消息订阅机制,可以满足大多数消费者的需求。 RocketMQ 的网站为感兴趣的用户提供了一个简单的快速入门指南。 - -## 关键特性以及其实现原理 - -分布式消息系统作为实现分布式系统可扩展、可伸缩性的关键组件,需要具有高吞吐量、高可用等特点。而谈到消息系统的设计,就回避不了两个问题: - -1. 消息的顺序问题 -2. 消息的重复问题 - -### 顺序消息 - -#### 第一种模型 - -假如生产者产生了 2 条消息:M1、M2,要保证这两条消息的顺序,应该怎样做?你脑中想到的可能是这样: - -
- -
- -假定 M1 发送到 S1,M2 发送到 S2,如果要保证 M1 先于 M2 被消费,那么需要 M1 到达消费端被消费后,通知 S2,然后 S2 再将 M2 发送到消费端。 - -这个模型存在的问题是,如果 M1 和 M2 分别发送到两台 Server 上,就不能保证 M1 先达到 MQ 集群,也不能保证 M1 被先消费。换个角度看,如果 M2 先于 M1 达到 MQ 集群,甚至 M2 被消费后,M1 才达到消费端,这时消息也就乱序了,说明以上模型是不能保证消息的顺序的。 - -
- -
- -#### 第二种模型 - -如何才能在 MQ 集群保证消息的顺序?一种简单的方式就是将 M1、M2 发送到同一个 Server 上: - -这样可以保证 M1 先于 M2 到达 MQServer(生产者等待 M1 发送成功后再发送 M2),根据先达到先被消费的原则,M1 会先于 M2 被消费,这样就保证了消息的顺序。 - -这个模型也仅仅是理论上可以保证消息的顺序,在实际场景中可能会遇到下面的问题: - -
- -
- -只要将消息从一台服务器发往另一台服务器,就会存在网络延迟问题。如上图所示,如果发送 M1 耗时大于发送 M2 的耗时,那么 M2 就仍将被先消费,仍然不能保证消息的顺序。即使 M1 和 M2 同时到达消费端,由于不清楚消费端 1 和消费端 2 的负载情况,仍然有可能出现 M2 先于 M1 被消费的情况。 - -如何解决这个问题?将 M1 和 M2 发往同一个消费者,且发送 M1 后,需要消费端响应成功后才能发送 M2。 - -这可能产生另外的问题:如果 M1 被发送到消费端后,消费端 1 没有响应,那是继续发送 M2 呢,还是重新发送 M1?一般为了保证消息一定被消费,肯定会选择重发 M1 到另外一个消费端 2,就如下图所示。 - -
- -
- -这样的模型就严格保证消息的顺序,细心的你仍然会发现问题,消费端 1 没有响应 Server 时有两种情况,一种是 M1 确实没有到达(数据在网络传送中丢失),另外一种消费端已经消费 M1 且已经发送响应消息,只是 MQ Server 端没有收到。如果是第二种情况,重发 M1,就会造成 M1 被重复消费。也就引入了我们要说的第二个问题,消息重复问题,这个后文会详细讲解。 - -回过头来看消息顺序问题,严格的顺序消息非常容易理解,也可以通过文中所描述的方式来简单处理。总结起来,要实现严格的顺序消息,简单且可行的办法就是: - -**保证生产者 - MQServer - 消费者是一对一对一的关系。** - -这样的设计虽然简单易行,但也会存在一些很严重的问题,比如: - -1. 并行度就会成为消息系统的瓶颈(吞吐量不够) -2. 更多的异常处理,比如:只要消费端出现问题,就会导致整个处理流程阻塞,我们不得不花费更多的精力来解决阻塞的问题。 - -RocketMQ 的解决方案:通过合理的设计或者将问题分解来规避。如果硬要把时间花在解决问题本身,实际上不仅效率低下,而且也是一种浪费。从这个角度来看消息的顺序问题,我们可以得出两个结论: - -1. 不关注乱序的应用实际大量存在 -2. 队列无序并不意味着消息无序 - -最后我们从源码角度分析 RocketMQ 怎么实现发送顺序消息。 - -RocketMQ 通过轮询所有队列的方式来确定消息被发送到哪一个队列(负载均衡策略)。比如下面的示例中,订单号相同的消息会被先后发送到同一个队列中: - -```java -// RocketMQ 通过 MessageQueueSelector 中实现的算法来确定消息发送到哪一个队列上 -// RocketMQ 默认提供了两种 MessageQueueSelector 实现:随机/Hash -// 当然你可以根据业务实现自己的 MessageQueueSelector 来决定消息按照何种策略发送到消息队列中 -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); -``` - -在获取到路由信息以后,会根据 MessageQueueSelector 实现的算法来选择一个队列,同一个 OrderId 获取到的肯定是同一个队列。 - -```java -private SendResult send() { - // 获取topic路由信息 - TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic()); - if (topicPublishInfo != null && topicPublishInfo.ok()) { - MessageQueue mq = null; - // 根据我们的算法,选择一个发送队列 - // 这里的arg = orderId - mq = selector.select(topicPublishInfo.getMessageQueueList(), msg, arg); - if (mq != null) { - return this.sendKernelImpl(msg, mq, communicationMode, sendCallback, timeout); - } - } -} -``` - -### 消息重复 - -造成消息重复的根本原因是:网络不可达。只要通过网络交换数据,就无法避免这个问题。所以解决这个问题的办法就是绕过这个问题。那么问题就变成了:如果消费端收到两条一样的消息,应该怎样处理? - -1. 消费端处理消息的业务逻辑保持幂等性。 -2. 保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。 - -第 1 条很好理解,只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样。 - -第 2 条原理就是利用一张日志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条消息。 - -第 1 条解决方案,很明显应该在消费端实现,不属于消息系统要实现的功能。 - -第 2 条可以消息系统实现,也可以业务端实现。正常情况下出现重复消息的概率其实很小,如果由消息系统来实现的话,肯定会对消息系统的吞吐量和高可用有影响,所以最好还是由业务端自己处理消息重复的问题,这也是 RocketMQ 不解决消息重复的问题的原因。 - -**RocketMQ 不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重。** - -### 事务消息 - -RocketMQ 除了支持普通消息,顺序消息,另外还支持事务消息。 - -假设这样的场景: - -![image.png](https://upload-images.jianshu.io/upload_images/3101171-253d8bd65736694f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -图中执行本地事务(Bob 账户扣款)和发送异步消息应该保证同时成功或者同时失败,也就是扣款成功了,发送消息一定要成功,如果扣款失败了,就不能再发送消息。那问题是:我们是先扣款还是先发送消息呢? - -![image](http://upload-images.jianshu.io/upload_images/3101171-088dc074c4ecd192?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -RocketMQ 分布式事务步骤: - -发送 Prepared 消息 2222222222222222222,并拿到接受消息的地址。 -执行本地事务 -通过第 1 步骤拿到的地址去访问消息,并修改消息状态。 - -## 资料 - -- [RocketMQ 官方文档](http://rocketmq.apache.org/docs/quick-start/) -- [分布式开放消息系统(RocketMQ)的原理与实践](https://www.jianshu.com/p/453c6e7ff81c) diff --git a/docs/distributed/mq/rocketmq-basics.md b/docs/distributed/mq/rocketmq-basics.md deleted file mode 100644 index ea2573cb..00000000 --- a/docs/distributed/mq/rocketmq-basics.md +++ /dev/null @@ -1,333 +0,0 @@ ---- -title: RocketMQ 实战篇 -date: 2018/07/17 -categories: -- javaweb -tags: -- java -- javaweb -- mq ---- - -# RocketMQ 实战篇 - - - -- [概述](#概述) - - [简介](#简介) - - [核心概念](#核心概念) -- [安装](#安装) - - [环境要求](#环境要求) - - [下载解压](#下载解压) - - [启动 Name Server](#启动-name-server) - - [启动 Broker](#启动-broker) - - [收发消息](#收发消息) - - [关闭服务器](#关闭服务器) -- [API](#api) - - [Producer](#producer) - - [Consumer](#consumer) - - [FAQ](#faq) -- [资料](#资料) - - - -## 概述 - -### 简介 - -RocketMQ 是一款开源的分布式消息队列,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。 - -RocketMQ 被阿里巴巴捐赠给 Apache,成为 Apache 的孵化项目。 - -### 核心概念 - -
- -
- -RocketMQ 有以下核心概念: - -- **Producer** - 将业务应用程序系统生成的消息发送给代理。RocketMQ 提供多种发送范例:同步,异步和单向。 - - **Producer Group** - 具有相同角色的 Producer 组合在一起。如果原始 Producer 在事务之后崩溃,则代理可以联系同一 Producer 组的不同 Producer 实例以提交或回滚事务。**_警告:考虑到提供的 Producer 在发送消息方面足够强大,每个 Producer 组只允许一个实例,以避免不必要的生成器实例初始化。_** -- **Consumer** - Consumer 从 Broker 那里获取消息并将其提供给应用程序。从用户应用的角度来看,提供了两种类型的 Consumer: - - **PullConsumer** - PullConsumer 积极地从 Broker 那里获取消息。一旦提取了批量消息,用户应用程序就会启动消费过程。 - - **PushConsumer** - PushConsumer 封装消息提取,消费进度并维护其他内部工作,为最终用户留下回调接口,这个借口会在消息到达时被执行。 - - **Consumer Group** - 完全相同角色的 Consumer 被组合在一起并命名为 Consumer Group。Consumer Group 是一个很好的概念,在消息消费方面实现负载平衡和容错目标非常容易。**_警告:Consumer Group 中的 Consumer 实例必须具有完全相同的主题订阅。_** -- **Broker** - Broker 是 RocketMQ 的主要组成部分。它接收从 Producer 发送的消息,存储它们并准备处理来自 Consumer 的消费请求。它还存储与消息相关的元数据,包括 Consumer Group,消耗进度偏移和主题/队列信息。 -- Name Server - 充当路由信息提供者。Producer/Consumer 客户查找主题以查找相应的 Broker 列表。 -- **Topic** - 是 Producer 传递消息和 Consumer 提取消息的类别。 -- **Message** - 是要传递的信息。消息必须有一个主题,可以将其解释为您要发送给的邮件地址。消息还可以具有可选 Tag 和额外的键值对。例如,您可以为消息设置业务密钥,并在代理服务器上查找消息以诊断开发期间的问题。 - - **Message Queue** - 主题被划分为一个或多个子主题“消息队列”。 - - **Tag** - 即子主题,为用户提供了额外的灵活性。对于 Tag,来自同一业务模块的具有不同目的的消息可以具有相同的主题和不同的 Tag。 - -## 安装 - -### 环境要求 - -- 推荐 64 位操作系统:Linux/Unix/Mac -- 64bit JDK 1.8+ -- Maven 3.2.x -- Git - -### 下载解压 - -进入官方下载地址:https://rocketmq.apache.org/dowloading/releases/,选择合适版本 - -建议选择 binary 版本。 - -解压到本地: - -```sh -> unzip rocketmq-all-4.2.0-source-release.zip -> cd rocketmq-all-4.2.0/ -``` - -### 启动 Name Server - -```sh -> nohup sh bin/mqnamesrv & -> tail -f ~/logs/rocketmqlogs/namesrv.log -The Name Server boot success... -``` - -### 启动 Broker - -```sh -> nohup sh bin/mqbroker -n localhost:9876 -c conf/broker.conf & -> tail -f ~/logs/rocketmqlogs/broker.log -The broker[%s, 172.30.30.233:10911] boot success... -``` - -### 收发消息 - -执行收发消息操作之前,不许告诉客户端命名服务器的位置。在 RocketMQ 中有多种方法来实现这个目的。这里,我们使用最简单的方法——设置环境变量 `NAMESRV_ADDR` : - -```sh -> export NAMESRV_ADDR=localhost:9876 -> sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer -SendResult [sendStatus=SEND_OK, msgId= ... - -> sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer -ConsumeMessageThread_%d Receive New Messages: [MessageExt... -``` - -### 关闭服务器 - -```sh -> sh bin/mqshutdown broker -The mqbroker(36695) is running... -Send shutdown request to mqbroker(36695) OK - -> sh bin/mqshutdown namesrv -The mqnamesrv(36664) is running... -Send shutdown request to mqnamesrv(36664) OK -``` - -## API - -首先在项目中引入 maven 依赖: - -```xml - - org.apache.rocketmq - rocketmq-client - 4.2.0 - -``` - -### Producer - -Producer 在 RocketMQ 中负责发送消息。 - -RocketMQ 有三种消息发送方式: - -- 可靠的同步发送 -- 可靠的异步发送 -- 单项发送 - -#### 可靠的同步发送 - -可靠的同步传输用于广泛的场景,如重要的通知消息,短信通知,短信营销系统等。 - -```java -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"); - //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(); - } -} -``` - -#### 可靠的异步发送 - -异步传输通常用于响应时间敏感的业务场景。 - -```java -public class AsyncProducer { - public static void main(String[] args) throws Exception { - //Instantiate with a producer group name. - DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); - //Launch the instance. - producer.start(); - producer.setRetryTimesWhenSendAsyncFailed(0); - for (int i = 0; i < 100; i++) { - final int index = i; - //Create a message instance, specifying topic, tag and message body. - Message msg = new Message("TopicTest", - "TagA", - "OrderID188", - "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); - producer.send(msg, new SendCallback() { - @Override - public void onSuccess(SendResult sendResult) { - System.out.printf("%-10d OK %s %n", index, - sendResult.getMsgId()); - } - @Override - public void onException(Throwable e) { - System.out.printf("%-10d Exception %s %n", index, e); - e.printStackTrace(); - } - }); - } - //Shut down once the producer instance is not longer in use. - producer.shutdown(); - } -} -``` - -#### 单向传输 - -单向传输用于需要中等可靠性的情况,例如日志收集。 - -```java -public class OnewayProducer { - public static void main(String[] args) throws Exception{ - //Instantiate with a producer group name. - DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); - //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); - - } - //Shut down once the producer instance is not longer in use. - producer.shutdown(); - } -} -``` - -### Consumer - -Consumer 在 RocketMQ 中负责接收消息。 - -```java -public class OrderedConsumer { - public static void main(String[] args) throws Exception { - DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); - consumer.setNamesrvAddr(RocketConfig.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"); - } -} -``` - -### FAQ - -#### connect to <172.17.0.1:10909> failed - -启动后,Producer 客户端连接 RocketMQ 时报错: - -```java -org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to <172.17.0.1:10909> failed - at org.apache.rocketmq.remoting.netty.NettyRemotingClient.invokeSync(NettyRemotingClient.java:357) - at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessageSync(MQClientAPIImpl.java:343) - at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessage(MQClientAPIImpl.java:327) - at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessage(MQClientAPIImpl.java:290) - at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendKernelImpl(DefaultMQProducerImpl.java:688) - at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendSelectImpl(DefaultMQProducerImpl.java:901) - at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:878) - at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:873) - at org.apache.rocketmq.client.producer.DefaultMQProducer.send(DefaultMQProducer.java:369) - at com.emrubik.uc.mdm.sync.utils.MdmInit.sendMessage(MdmInit.java:62) - at com.emrubik.uc.mdm.sync.utils.MdmInit.main(MdmInit.java:2149) -``` - -原因:RocketMQ 部署在虚拟机上,内网 ip 为 10.10.30.63,该虚拟机一个 docker0 网卡,ip 为 172.17.0.1。RocketMQ broker 启动时默认使用了 docker0 网卡,Producer 客户端无法连接 172.17.0.1,造成以上问题。 - -解决方案 - -(1)干掉 docker0 网卡或修改网卡名称 - -(2)停掉 broker,修改 broker 配置文件,重启 broker。 - -修改 conf/broker.conf,增加两行来指定启动 broker 的 IP: - -``` -namesrvAddr = 10.10.30.63:9876 -brokerIP1 = 10.10.30.63 -``` - -启动时需要指定配置文件 - -```sh -nohup sh bin/mqbroker -n localhost:9876 -c conf/broker.conf & -``` - -## 资料 - -- [RocketMQ 官方文档](http://rocketmq.apache.org/docs/quick-start/) diff --git "a/docs/distributed/mq/\345\210\206\345\270\203\345\274\217\346\266\210\346\201\257\351\230\237\345\210\227.md" "b/docs/distributed/mq/\345\210\206\345\270\203\345\274\217\346\266\210\346\201\257\351\230\237\345\210\227.md" deleted file mode 100644 index dbfb7080..00000000 --- "a/docs/distributed/mq/\345\210\206\345\270\203\345\274\217\346\266\210\346\201\257\351\230\237\345\210\227.md" +++ /dev/null @@ -1,405 +0,0 @@ ---- -title: 分布式消息队列 -date: 2018/07/05 -categories: -- 分布式 -tags: -- 分布式 -- mq ---- - -# 分布式消息队列 - -> 消息队列是分布式系统中重要的组件,主要解决应用耦合,异步消息,流量削锋等问题。实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 - - - -- [1. 消息队列应用场景](#1-消息队列应用场景) - - [1.1. 异步处理](#11-异步处理) - - [1.2. 应用解耦](#12-应用解耦) - - [1.3. 流量削锋](#13-流量削锋) - - [1.4. 日志处理](#14-日志处理) - - [1.5. 消息通讯](#15-消息通讯) -- [2. JMS 消息服务](#2-jms-消息服务) - - [2.1. 消息模型](#21-消息模型) - - [2.1.1. P2P 模式](#211-p2p-模式) - - [2.1.2. Pub/sub 模式](#212-pubsub-模式) - - [2.2. 消息消费](#22-消息消费) - - [2.3. JMS 编程模型](#23-jms-编程模型) -- [3. 常用 MQ 中间件](#3-常用-mq-中间件) - - [3.1. ActiveMQ](#31-activemq) - - [3.2. RabbitMQ](#32-rabbitmq) - - [3.3. ZeroMQ](#33-zeromq) - - [3.4. Kafka](#34-kafka) -- [4. MQ 示例](#4-mq-示例) - - [4.1. 电商系统](#41-电商系统) - - [4.2. 日志收集系统](#42-日志收集系统) -- [5. 资料](#5-资料) - - - -## 1. 消息队列应用场景 - -### 1.1. 异步处理 - -场景说明:用户注册后,需要发注册邮件和注册短信。传统的做法有两种 1.串行的方式;2.并行方式。 - -(1)串行方式:将注册信息写入数据库成功后,发送注册邮件,再发送注册短信。以上三个任务全部完成后,返回给客户端。 - -![image](http://upload-images.jianshu.io/upload_images/3101171-c60d8f5be3a7a3f2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -(2)并行方式:将注册信息写入数据库成功后,发送注册邮件的同时,发送注册短信。以上三个任务完成后,返回给客户端。与串行的差别是,并行的方式可以提高处理的时间。 - -![image](http://upload-images.jianshu.io/upload_images/3101171-1a78d88cfbfebcd2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -假设三个业务节点每个使用 50 毫秒钟,不考虑网络等其他开销,则串行方式的时间是 150 毫秒,并行的时间可能是 100 毫秒。 - -因为 CPU 在单位时间内处理的请求数是一定的,假设 CPU1 秒内吞吐量是 100 次。则串行方式 1 秒内 CPU 可处理的请求量是 7 次(1000/150)。并行方式处理的请求量是 10 次(1000/100)。 - -小结:如以上案例描述,传统的方式系统的性能(并发量,吞吐量,响应时间)会有瓶颈。如何解决这个问题呢? - -引入消息队列,将不是必须的业务逻辑,异步处理。改造后的架构如下: - -![image](http://upload-images.jianshu.io/upload_images/3101171-105299d1fd6f3093.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -按照以上约定,用户的响应时间相当于是注册信息写入数据库的时间,也就是 50 毫秒。注册邮件,发送短信写入消息队列后,直接返回,因此写入消息队列的速度很快,基本可以忽略,因此用户的响应时间可能是 50 毫秒。因此架构改变后,系统的吞吐量提高到每秒 20 QPS。比串行提高了 3 倍,比并行提高了两倍。 - -### 1.2. 应用解耦 - -场景说明:用户下单后,订单系统需要通知库存系统。传统的做法是,订单系统调用库存系统的接口。如下图: - -![image](http://upload-images.jianshu.io/upload_images/3101171-872f21e480fa4026.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -传统模式的缺点: - -1)  假如库存系统无法访问,则订单减库存将失败,从而导致订单失败; - -2)  订单系统与库存系统耦合; - -如何解决以上问题呢?引入应用消息队列后的方案,如下图: - -![image](http://upload-images.jianshu.io/upload_images/3101171-81e965d35df99238.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -- 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。 -- 库存系统:订阅下单的消息,采用拉/推的方式,获取下单信息,库存系统根据下单信息,进行库存操作。 -- 假如:在下单时库存系统不能正常使用。也不影响正常下单,因为下单后,订单系统写入消息队列就不再关心其他的后续操作了。实现订单系统与库存系统的应用解耦。 - -### 1.3. 流量削锋 - -流量削锋也是消息队列中的常用场景,一般在秒杀或团抢活动中使用广泛。 - -应用场景:秒杀活动,一般会因为流量过大,导致流量暴增,应用挂掉。为解决这个问题,一般需要在应用前端加入消息队列。 - -1. 可以控制活动的人数; -2. 可以缓解短时间内高流量压垮应用; - -![image](http://upload-images.jianshu.io/upload_images/3101171-26394776da461cbf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -1. 用户的请求,服务器接收后,首先写入消息队列。假如消息队列长度超过最大数量,则直接抛弃用户请求或跳转到错误页面; -2. 秒杀业务根据消息队列中的请求信息,再做后续处理。 - -### 1.4. 日志处理 - -日志处理是指将消息队列用在日志处理中,比如 Kafka 的应用,解决大量日志传输的问题。架构简化如下: - -![image](http://upload-images.jianshu.io/upload_images/3101171-54d35d48b2fb643b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -- 日志采集客户端,负责日志数据采集,定时写入 Kafka 队列; -- Kafka 消息队列,负责日志数据的接收,存储和转发; -- 日志处理应用:订阅并消费 kafka 队列中的日志数据; - -以下是新浪 kafka 日志处理应用案例: - -转自(http://cloud.51cto.com/art/201507/484338.htm) - -![image](http://upload-images.jianshu.io/upload_images/3101171-46e0f421a0670f4b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -- Kafka - 接收用户日志的消息队列。 -- Logstash - 负责日志传输和解析,统一成 JSON 输出给 Elasticsearch。 -- Elasticsearch - 实时日志分析服务的核心技术,一个 schemaless,实时的数据存储服务,通过 index 组织数据,兼具强大的搜索和统计功能。 -- Kibana - 基于 Elasticsearch 的数据可视化组件,超强的数据可视化能力是众多公司选择 ELK stack 的重要原因。 - -### 1.5. 消息通讯 - -消息通讯是指,消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。 - -点对点通讯: - -![image](http://upload-images.jianshu.io/upload_images/3101171-ba662aaae7331dee.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -客户端 A 和客户端 B 使用同一队列,进行消息通讯。 - -聊天室通讯: - -![image](http://upload-images.jianshu.io/upload_images/3101171-07ef81ffec631fd1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -客户端 A,客户端 B,客户端 N 订阅同一主题,进行消息发布和接收。实现类似聊天室效果。 - -以上实际是消息队列的两种消息模式,点对点或发布订阅模式。模型为示意图,供参考。 - -## 2. JMS 消息服务 - -讲消息队列就不得不提 JMS 。JMS(JAVA Message Service,java 消息服务)API 是一个消息服务的标准/规范,允许应用程序组件基于 JavaEE 平台创建、发送、接收和读取消息。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。 - -在 EJB 架构中,有消息 bean 可以无缝的与 JM 消息服务集成。在 J2EE 架构模式中,有消息服务者模式,用于实现消息与应用直接的解耦。 - -### 2.1. 消息模型 - -在 JMS 标准中,有两种消息模型: - -- P2P(Point to Point) -- Pub/Sub(Publish/Subscribe) - -#### 2.1.1. P2P 模式 - -![image](http://upload-images.jianshu.io/upload_images/3101171-2adc66e2367cd2c2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -P2P 模式包含三个角色:消息队列(Queue),发送者(Sender),接收者(Receiver)。每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到他们被消费或超时。 - -P2P 的特点 - -- 每个消息只有一个消费者(Consumer)(即一旦被消费,消息就不再在消息队列中) -- 发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行,它不会影响到消息被发送到队列 -- 接收者在成功接收消息之后需向队列应答成功 - -如果希望发送的每个消息都会被成功处理的话,那么需要 P2P 模式。 - -#### 2.1.2. Pub/sub 模式 - -![image](http://upload-images.jianshu.io/upload_images/3101171-12afe9581da889ea.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -包含三个角色主题(Topic),发布者(Publisher),订阅者(Subscriber) 。多个发布者将消息发送到 Topic,系统将这些消息传递给多个订阅者。 - -Pub/Sub 的特点 - -- 每个消息可以有多个消费者 -- 发布者和订阅者之间有时间上的依赖性。针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息。 -- 为了消费消息,订阅者必须保持运行的状态。 - -为了缓和这样严格的时间相关性,JMS 允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有被激活(运行),它也能接收到发布者的消息。 - -如果希望发送的消息可以不被做任何处理、或者只被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用 Pub/Sub 模型。 - -### 2.2. 消息消费 - -在 JMS 中,消息的产生和消费都是异步的。对于消费来说,JMS 的消息者可以通过两种方式来消费消息。 - -(1)同步 - -订阅者或接收者通过 receive 方法来接收消息,receive 方法在接收到消息之前(或超时之前)将一直阻塞; - -(2)异步 - -订阅者或接收者可以注册为一个消息监听器。当消息到达之后,系统自动调用监听器的 onMessage 方法。 - -JNDI:Java 命名和目录接口,是一种标准的 Java 命名系统接口。可以在网络上查找和访问服务。通过指定一个资源名称,该名称对应于数据库或命名服务中的一个记录,同时返回资源连接建立所必须的信息。 - -JNDI 在 JMS 中起到查找和访问发送目标或消息来源的作用。 - -### 2.3. JMS 编程模型 - -(1) ConnectionFactory - -创建 Connection 对象的工厂,针对两种不同的 jms 消息模型,分别有 QueueConnectionFactory 和 TopicConnectionFactory 两种。可以通过 JNDI 来查找 ConnectionFactory 对象。 - -(2) Destination - -Destination 的意思是消息生产者的消息发送目标或者说消息消费者的消息来源。对于消息生产者来说,它的 Destination 是某个队列(Queue)或某个主题(Topic);对于消息消费者来说,它的 Destination 也是某个队列或主题(即消息来源)。 - -所以,Destination 实际上就是两种类型的对象:Queue、Topic。可以通过 JNDI 来查找 Destination。 - -(3) Connection - -Connection 表示在客户端和 JMS 系统之间建立的链接(对 TCP/IP socket 的包装)。Connection 可以产生一个或多个 Session。跟 ConnectionFactory 一样,Connection 也有两种类型:QueueConnection 和 TopicConnection。 - -(4) Session - -Session 是操作消息的接口。可以通过 session 创建生产者、消费者、消息等。Session 提供了事务的功能。当需要使用 session 发送/接收多个消息时,可以将这些发送/接收动作放到一个事务中。同样,也分 QueueSession 和 TopicSession。 - -(5) 消息的生产者 - -消息生产者由 Session 创建,并用于将消息发送到 Destination。同样,消息生产者分两种类型:QueueSender 和 TopicPublisher。可以调用消息生产者的方法(send 或 publish 方法)发送消息。 - -(6) 消息消费者 - -消息消费者由 Session 创建,用于接收被发送到 Destination 的消息。两种类型:QueueReceiver 和 TopicSubscriber。可分别通过 session 的 createReceiver(Queue)或 createSubscriber(Topic)来创建。当然,也可以 session 的 creatDurableSubscriber 方法来创建持久化的订阅者。 - -(7) MessageListener - -消息监听器。如果注册了消息监听器,一旦消息到达,将自动调用监听器的 onMessage 方法。EJB 中的 MDB(Message-Driven Bean)就是一种 MessageListener。 - -深入学习 JMS 对掌握 JAVA 架构,EJB 架构有很好的帮助,消息中间件也是大型分布式系统必须的组件。本次分享主要做全局性介绍,具体的深入需要大家学习,实践,总结,领会。 - -## 3. 常用 MQ 中间件 - -一般商用的容器,比如 WebLogic,JBoss,都支持 JMS 标准,开发上很方便。但免费的比如 Tomcat,Jetty 等则需要使用第三方的消息中间件。本部分内容介绍常用的消息中间件(ActiveMQ、RabbitMQ、RocketMQ、Kafka)以及他们的特点。 - -### 3.1. ActiveMQ - -ActiveMQ 是 Apache 出品,最流行的,能力强劲的开源消息总线。ActiveMQ 是一个完全支持 JMS1.1 和 J2EE 1.4 规范的 JMS Provider 实现,尽管 JMS 规范出台已经是很久的事情了,但是 JMS 在当今的 J2EE 应用中间仍然扮演着特殊的地位。 - -ActiveMQ 特性如下: - -⒈ 多种语言和协议编写客户端。语言: Java,C,C++,C#,Ruby,Perl,Python,PHP。应用协议: OpenWire,Stomp REST,WS Notification,XMPP,AMQP - -⒉ 完全支持 JMS1.1 和 J2EE 1.4 规范 (持久化,XA 消息,事务) - -⒊ 对 Spring 的支持,ActiveMQ 可以很容易内嵌到使用 Spring 的系统里面去,而且也支持 Spring2.0 的特性 - -⒋ 通过了常见 J2EE 服务器(如 Geronimo,JBoss 4,GlassFish,WebLogic)的测试,其中通过 JCA 1.5 resource adaptors 的配置,可以让 ActiveMQ 可以自动的部署到任何兼容 J2EE 1.4 商业服务器上 - -⒌ 支持多种传送协议:in-VM,TCP,SSL,NIO,UDP,JGroups,JXTA - -⒍ 支持通过 JDBC 和 journal 提供高速的消息持久化 - -⒎ 从设计上保证了高性能的集群,客户端-服务器,点对点 - -⒏ 支持 Ajax - -⒐ 支持与 Axis 的整合 - -⒑ 可以很容易得调用内嵌 JMS provider,进行测试 - -### 3.2. RabbitMQ - -RabbitMQ 是流行的开源消息队列系统,用 erlang 语言开发。RabbitMQ 是 AMQP(高级消息队列协议)的标准实现。支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP 等,支持 AJAX,持久化。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。 - -结构图如下: - -![image](http://upload-images.jianshu.io/upload_images/3101171-1109f074e8445c6d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -几个重要概念: - -Broker:简单来说就是消息队列服务器实体。 - -Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。 - -Queue:消息队列载体,每个消息都会被投入到一个或多个队列。 - -Binding:绑定,它的作用就是把 exchange 和 queue 按照路由规则绑定起来。 - -Routing Key:路由关键字,exchange 根据这个关键字进行消息投递。 - -vhost:虚拟主机,一个 broker 里可以开设多个 vhost,用作不同用户的权限分离。 - -producer:消息生产者,就是投递消息的程序。 - -consumer:消息消费者,就是接受消息的程序。 - -channel:消息通道,在客户端的每个连接里,可建立多个 channel,每个 channel 代表一个会话任务。 - -消息队列的使用过程,如下: - -(1)客户端连接到消息队列服务器,打开一个 channel。 - -(2)客户端声明一个 exchange,并设置相关属性。 - -(3)客户端声明一个 queue,并设置相关属性。 - -(4)客户端使用 routing key,在 exchange 和 queue 之间建立好绑定关系。 - -(5)客户端投递消息到 exchange。 - -exchange 接收到消息后,就根据消息的 key 和已经设置的 binding,进行消息路由,将消息投递到一个或多个队列里。 - -### 3.3. ZeroMQ - -号称史上最快的消息队列,它实际类似于 Socket 的一系列接口,他跟 Socket 的区别是:普通的 socket 是端到端的(1:1 的关系),而 ZMQ 却是可以 N:M 的关系,人们对 BSD 套接字的了解较多的是点对点的连接,点对点连接需要显式地建立连接、销毁连接、选择协议(TCP/UDP)和处理错误等,而 ZMQ 屏蔽了这些细节,让你的网络编程更为简单。ZMQ 用于 node 与 node 间的通信,node 可以是主机或者是进程。 - -引用官方的说法: “ZMQ(以下 ZeroMQ 简称 ZMQ)是一个简单好用的传输层,像框架一样的一个 socket library,他使得 Socket 编程更加简单、简洁和性能更高。是一个消息处理队列库,可在多个线程、内核和主机盒之间弹性伸缩。ZMQ 的明确目标是“成为标准网络协议栈的一部分,之后进入 Linux 内核”。现在还未看到它们的成功。但是,它无疑是极具前景的、并且是人们更加需要的“传统”BSD 套接字之上的一 层封装。ZMQ 让编写高性能网络应用程序极为简单和有趣。” - -特点是: - -- 高性能,非持久化; -- 跨平台:支持 Linux、Windows、OS X 等。 -- 多语言支持; C、C++、Java、.NET、Python 等 30 多种开发语言。 -- 可单独部署或集成到应用中使用; -- 可作为 Socket 通信库使用。 - -与 RabbitMQ 相比,ZMQ 并不像是一个传统意义上的消息队列服务器,事实上,它也根本不是一个服务器,更像一个底层的网络通讯库,在 Socket API 之上做了一层封装,将网络通讯、进程通讯和线程通讯抽象为统一的 API 接口。支持“Request-Reply “,”Publisher-Subscriber“,”Parallel Pipeline”三种基本模型和扩展模型。 - -ZeroMQ 高性能设计要点: - -1、无锁的队列模型 - -对于跨线程间的交互(用户端和 session)之间的数据交换通道 pipe,采用无锁的队列算法 CAS;在 pipe 两端注册有异步事件,在读或者写消息到 pipe 的时,会自动触发读写事件。 - -2、批量处理的算法 - -对于传统的消息处理,每个消息在发送和接收的时候,都需要系统的调用,这样对于大量的消息,系统的开销比较大,zeroMQ 对于批量的消息,进行了适应性的优化,可以批量的接收和发送消息。 - -3、多核下的线程绑定,无须 CPU 切换 - -区别于传统的多线程并发模式,信号量或者临界区, zeroMQ 充分利用多核的优势,每个核绑定运行一个工作者线程,避免多线程之间的 CPU 切换开销。 - -### 3.4. Kafka - -Kafka 是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据。 这种动作(网页浏览,搜索和其他用户的行动)是在现代网络上的许多社会功能的一个关键因素。 这些数据通常是由于吞吐量的要求而通过处理日志和日志聚合来解决。 对于像 Hadoop 的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka 的目的是通过 Hadoop 的并行加载机制来统一线上和离线的消息处理,也是为了通过集群机来提供实时的消费。 - -Kafka 是一种高吞吐量的分布式发布订阅消息系统,有如下特性: - -- 通过 O(1)的磁盘数据结构提供消息的持久化,这种结构对于即使数以 TB 的消息存储也能够保持长时间的稳定性能。(文件追加的方式写入数据,过期的数据定期删除) -- 高吞吐量:即使是非常普通的硬件 Kafka 也可以支持每秒数百万的消息。 -- 支持通过 Kafka 服务器和消费机集群来分区消息。 -- 支持 Hadoop 并行数据加载。 - -Kafka 相关概念 - -- Broker - -Kafka 集群包含一个或多个服务器,这种服务器被称为 broker[5] - -- Topic - -每条发布到 Kafka 集群的消息都有一个类别,这个类别被称为 Topic。(物理上不同 Topic 的消息分开存储,逻辑上一个 Topic 的消息虽然保存于一个或多个 broker 上但用户只需指定消息的 Topic 即可生产或消费数据而不必关心数据存于何处) - -- Partition - -Parition 是物理上的概念,每个 Topic 包含一个或多个 Partition. - -- Producer - -负责发布消息到 Kafka broker - -- Consumer - -消息消费者,向 Kafka broker 读取消息的客户端。 - -- Consumer Group - -每个 Consumer 属于一个特定的 Consumer Group(可为每个 Consumer 指定 group name,若不指定 group name 则属于默认的 group)。 - -一般应用在大数据日志处理或对实时性(少量延迟),可靠性(少量丢数据)要求稍低的场景使用。 - -## 4. MQ 示例 - -### 4.1. 电商系统 - -![image](http://upload-images.jianshu.io/upload_images/3101171-1bbed6d1a2274ba1.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -消息队列采用高可用,可持久化的消息中间件。比如 Active MQ,Rabbit MQ,Rocket Mq。 - -(1)应用将主干逻辑处理完成后,写入消息队列。消息发送是否成功可以开启消息的确认模式。(消息队列返回消息接收成功状态后,应用再返回,这样保障消息的完整性) - -(2)扩展流程(发短信,配送处理)订阅队列消息。采用推或拉的方式获取消息并处理。 - -(3)消息将应用解耦的同时,带来了数据一致性问题,可以采用最终一致性方式解决。比如主数据写入数据库,扩展应用根据消息队列,并结合数据库方式实现基于消息队列的后续处理。 - -### 4.2. 日志收集系统 - -![image](http://upload-images.jianshu.io/upload_images/3101171-4275c9f0e9c8c463.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -分为 Zookeeper 注册中心,日志收集客户端,Kafka 集群和 Storm 集群(OtherApp)四部分组成。 - -- Zookeeper 注册中心,提出负载均衡和地址查找服务; -- 日志收集客户端,用于采集应用系统的日志,并将数据推送到 kafka 队列; -- Kafka 集群:接收,路由,存储,转发等消息处理; - -Storm 集群:与 OtherApp 处于同一级别,采用拉的方式消费队列中的数据; - -## 5. 资料 - -- [大型网站架构系列:分布式消息队列(一)](https://www.cnblogs.com/itfly8/p/5155983.html) -- [大型网站架构系列:消息队列(二)](https://www.cnblogs.com/itfly8/p/5156155.html) -- [分布式开放消息系统(RocketMQ)的原理与实践](https://www.jianshu.com/p/453c6e7ff81c) -- [阿里 RocketMQ 优势对比](https://juejin.im/entry/5a0abfb5f265da43062a4a91) \ No newline at end of file diff --git a/docs/distributed/rpc/dubbo.md b/docs/distributed/rpc/dubbo.md deleted file mode 100644 index 3db29e2b..00000000 --- a/docs/distributed/rpc/dubbo.md +++ /dev/null @@ -1,561 +0,0 @@ ---- -title: Dubbo -date: 2018/06/12 -categories: -- 分布式 -tags: -- 分布式 -- rpc ---- - -# Dubbo - -> Dubbo 是一个基于 Java 开发的高性能 RPC 框架。 - - - -- [概述](#概述) -- [QuickStart](#quickstart) -- [Dubbo 配置](#dubbo-配置) - - [配置方式](#配置方式) - - [配置项](#配置项) -- [Dubbo 支持的协议](#dubbo-支持的协议) -- [服务治理](#服务治理) - - [集群容错](#集群容错) - - [负载均衡](#负载均衡) - - [路由规则](#路由规则) - - [服务降级](#服务降级) - - [访问控制](#访问控制) - - [动态配置](#动态配置) -- [Dubbo 架构](#dubbo-架构) - - [整体设计](#整体设计) -- [资料](#资料) - - - -## 概述 - -Dubbo 是一个基于 Java 开发的高性能 RPC 框架。 - -Dubbo 的三个关键功能: - -1. 基于接口的远程调用; -2. 容错机制以及负载均衡; -3. 自动服务注册以及自动服务发现。 - -## QuickStart - -(1)添加 maven 依赖 - -```xml - - com.alibaba - dubbo - ${dubbo.version} - -``` - -(2)定义 Provider - -```java -package com.alibaba.dubbo.demo; - -public interface DemoService { - String sayHello(String name); -} -``` - -(3)实现 Provider - -```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; - } -} -``` - -(4)配置 Provider - -```xml - - - - - - - - -``` - -(5)启动 Provider - -```java -import org.springframework.context.support.ClassPathXmlApplicationContext; - -public class Provider { - public static void main(String[] args) throws Exception { - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - new String[] {"META-INF/spring/dubbo-demo-provider.xml"}); - context.start(); - // press any key to exit - System.in.read(); - } -} -``` - -(6)配置 Consumer - -```xml - - - - - - -``` - -(7)启动 Consumer - -```java -import com.alibaba.dubbo.demo.DemoService; -import org.springframework.context.support.ClassPathXmlApplicationContext; - -public class Consumer { - public static void main(String[] args) throws Exception { - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - new String[]{"META-INF/spring/dubbo-demo-consumer.xml"}); - context.start(); - // obtain proxy object for remote invocation - DemoService demoService = (DemoService) context.getBean("demoService"); - // execute remote invocation - String hello = demoService.sayHello("world"); - // show the result - System.out.println(hello); - } -} -``` - -## Dubbo 配置 - -dubbo 所有配置最终都将转换为 URL 表示,并由服务提供方生成,经注册中心传递给消费方,各属性对应 URL 的参数,参见配置项一览表中的 "对应 URL 参数" 列。 - -> **注意** -> -> 只有 group,interface,version 是服务的匹配条件,三者决定是不是同一个服务,其它配置项均为调优和治理参数。 -> -> **URL 格式** -> -> `protocol://username:password@host:port/path?key=value&key=value` - -### 配置方式 - -Dubbo 支持多种配置方式: - -- xml 配置 -- properties 配置 -- API 配置 -- 注解配置 - -如果同时存在多种配置方式,遵循以下覆盖策略: - -- JVM 启动 -D 参数优先,这样可以使用户在部署和启动时进行参数重写,比如在启动时需改变协议的端口。 -- XML 次之,如果在 XML 中有配置,则 dubbo.properties 中的相应配置项无效。 -- Properties 最后,相当于缺省值,只有 XML 没有配置时,dubbo.properties 的相应配置项才会生效,通常用于共享公共配置,比如应用名。 - -
- -
- -#### xml 配置 - -示例: - -```xml - - - - - - - - -``` - -#### properties 配置 - -示例: - -```properties -dubbo.application.name=foo -dubbo.application.owner=bar -dubbo.registry.address=10.20.153.10:9090 -``` - -### 配置项 - -所有配置项分为三大类 - -- 服务发现:表示该配置项用于服务的注册与发现,目的是让消费方找到提供方。 - -- 服务治理:表示该配置项用于治理服务间的关系,或为开发测试提供便利条件。 - -- 性能调优:表示该配置项用于调优性能,不同的选项对性能会产生影响。 - -| 标签 | 用途 | 解释 | -| ----------------- | ------------ | ------------------------------------------------------------------------------------------------ | -| dubbo:service | 服务配置 | 用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心 | -| dubbo:reference | 引用配置 | 用于创建一个远程服务代理,一个引用可以指向多个注册中心 | -| dubbo:protocol | 协议配置 | 用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受 | -| dubbo:application | 应用配置 | 用于配置当前应用信息,不管该应用是提供者还是消费者 | -| dubbo:module | 模块配置 | 用于配置当前模块信息,可选 | -| dubbo:registry | 注册中心配置 | 用于配置连接注册中心相关信息 | -| dubbo:monitor | 监控中心配置 | 用于配置连接监控中心相关信息,可选 | -| dubbo:provider | 提供方配置 | 当 ProtocolConfig 和 ServiceConfig 某属性没有配置时,采用此缺省值,可选 | -| dubbo:consumer | 消费方配置 | 当 ReferenceConfig 某属性没有配置时,采用此缺省值,可选 | -| dubbo:method | 方法配置 | 用于 ServiceConfig 和 ReferenceConfig 指定方法级的配置信息 | -| dubbo:argument | 参数配置 | 用于指定方法参数配置 | - -> 详细配置说明请参考:[官方配置](http://dubbo.apache.org/books/dubbo-user-book/references/xml/introduction.html) - -#### 配置之间的关系 - -
- -
- -#### 配置覆盖关系 - -以 timeout 为例,显示了配置的查找顺序,其它 retries, loadbalance, actives 等类似: - -- 方法级优先,接口级次之,全局配置再次之。 -- 如果级别一样,则消费方优先,提供方次之。 - -其中,服务提供方配置,通过 URL 经由注册中心传递给消费方。 - -
- -
- -## Dubbo 支持的协议 - -Dubbo 支持以下通信协议: - -- dubbo -- rmi -- hessian -- http -- webservice -- thrift -- memcached -- redis - -不同协议适合不同的服务场景,可以根据实际应用场景来选择合适的协议。 - -dubbo 协议是 dubbo 默认的协议。dubbo 协议采用单一长连接和 NIO 异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。反之,dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。 - -选用哪个协议,可以通过 `` 标签配置。 - -> 更多详情请参考:[Dubbo 官方协议参考手册](https://dubbo.gitbooks.io/dubbo-user-book/references/protocol/introduction.html) - -## 服务治理 - -- 当服务越来越多时,服务 URL 配置管理变得非常困难,F5 硬件负载均衡器的单点压力也越来越大。 -- 当进一步发展,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。 -- 接着,服务的调用量越来越大,服务的容量问题就暴露出来,这个服务需要多少机器支撑?什么时候该加机器? - -以上问题可以归纳为服务治理问题,这也是 Dubbo 的核心功能。 - -### 集群容错 - -
- -
- -- **Failover** - 失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。 -- **Failfast** - 快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 -- **Failsafe** - 失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 -- **Failback** - 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 -- **Forking** - 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。 -- **Broadcast** - 播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。 - -### 负载均衡 - -#### Random - -- 随机,按权重设置随机概率。 -- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。 - -#### RoundRobin - -- 轮循,按公约后的权重设置轮循比率。 -- 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。 - -#### LeastActive - -- 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。 -- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。 - -#### ConsistentHash - -- 一致性 Hash,相同参数的请求总是发到同一提供者。 -- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。 -- 算法参见:http://en.wikipedia.org/wiki/Consistent_hashing -- 缺省只对第一个参数 Hash,如果要修改,请配置 `` -- 缺省用 160 份虚拟节点,如果要修改,请配置 `` - -### 路由规则 - -路由规则决定一次 dubbo 服务调用的目标服务器,分为条件路由规则和脚本路由规则,并且支持可扩展。 - -向注册中心写入路由规则的操作通常由监控中心或治理中心的页面完成。 - -```java -RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); -Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181")); -registry.register(URL.valueOf("condition://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("host = 10.20.153.10 => host = 10.20.153.11") + ")); -``` - -- **condition://** - 表示路由规则的类型,支持条件路由规则和脚本路由规则,可扩展,必填。 -- **0.0.0.0** - 表示对所有 IP 地址生效,如果只想对某个 IP 的生效,请填入具体 IP,必填。 -- **com.foo.BarService** - 表示只对指定服务生效,必填。 -- **category=routers** - 表示该数据为动态配置类型,必填。 -- **dynamic=false** - 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心,必填。 -- **enabled=true** - 覆盖规则是否生效,可不填,缺省生效。 -- **force=false** - 当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为 flase。 -- **runtime=false** - 是否在每次调用时执行路由规则,否则只在提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为 true,需要注意设置会影响调用的性能,可不填,缺省为 flase。 -- **priority=1** - 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 0。 -- **rule=URL.encode("host = 10.20.153.10 => host = 10.20.153.11")** - 表示路由规则的内容,必填。 - -### 服务降级 - -可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。 - -向注册中心写入动态配置覆盖规则: - -```java -RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); -Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181")); -registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null")); -``` - -其中: - -**mock=force:return+null** 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。 -还可以改为 **mock=fail:return+null** 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。 - -### 访问控制 - -#### 直连 - -在开发及测试环境下,经常需要绕过注册中心,只测试指定服务提供者,这时候可能需要点对点直连,点对点直联方式,将以服务接口为单位,忽略注册中心的提供者列表,A 接口配置点对点,不影响 B 接口从注册中心获取列表。 - -
- -
- -配置方式: - -(1)通过 XML 配置 - -如果是线上需求需要点对点,可在 中配置 url 指向提供者,将绕过注册中心,多个地址用分号隔开,配置如下: - -```xml - -``` - -(2)通过 -D 参数指定 - -在 JVM 启动参数中加入-D 参数映射服务地址: - -``` -java -Dcom.alibaba.xxx.XxxService=dubbo://localhost:20890 -``` - -(3)通过文件映射 -如果服务比较多,也可以用文件映射,用 -Ddubbo.resolve.file 指定映射文件路径,此配置优先级高于 中的配置: - -``` -java -Ddubbo.resolve.file=xxx.properties -``` - -然后在映射文件 xxx.properties 中加入配置,其中 key 为服务名,value 为服务提供者 URL: - -```properties -com.alibaba.xxx.XxxService=dubbo://localhost:20890 -``` - -#### 只订阅 - -为方便开发测试,经常会在线下共用一个所有服务可用的注册中心,这时,如果一个正在开发中的服务提供者注册,可能会影响消费者不能正常运行。 - -可以让服务提供者开发方,只订阅服务(开发的服务可能依赖其它服务),而不注册正在开发的服务,通过直连测试正在开发的服务。 - -禁用注册配置: - -```xml - -``` - -或者 - -```xml - -``` - -#### 只注册 - -如果有两个镜像环境,两个注册中心,有一个服务只在其中一个注册中心有部署,另一个注册中心还没来得及部署,而两个注册中心的其它应用都需要依赖此服务。这个时候,可以让服务提供者方只注册服务到另一注册中心,而不从另一注册中心订阅服务。 - -禁用订阅配置 - -```xml - - -``` - -或者 - -```xml - - -``` - -#### 静态服务 - -有时候希望人工管理服务提供者的上线和下线,此时需将注册中心标识为非动态管理模式。 - -``` - -``` - -或者 - -``` - -``` - -服务提供者初次注册时为禁用状态,需人工启用。断线时,将不会被自动删除,需人工禁用。 - -### 动态配置 - -向注册中心写入动态配置覆盖规则。该功能通常由监控中心或治理中心的页面完成。 - -```java -RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); -Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181")); -registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&timeout=1000")); -``` - -其中: - -- **override://** - 表示数据采用覆盖方式,支持 override 和 absent,可扩展,必填。 -- **0.0.0.0** - 表示对所有 IP 地址生效,如果只想覆盖某个 IP 的数据,请填入具体 IP,必填。 -- **com.foo.BarService** - 表示只对指定服务生效,必填。 -- **category=configurators** - 表示该数据为动态配置类型,必填。 -- **dynamic=false** - 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心,必填。 -- **enabled=true** - 覆盖规则是否生效,可不填,缺省生效。 -- **application=foo** - 表示只对指定应用生效,可不填,表示对所有应用生效。 -- **timeout=1000** - 表示将满足以上条件的 timeout 参数的值覆盖为 1000。如果想覆盖其它参数,直接加在 override 的 URL 参数上。 - -示例: - -- 禁用提供者:(通常用于临时踢除某台提供者机器,相似的,禁止消费者访问请使用路由规则) - -``` -override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&disbaled=true -``` - -- 调整权重:(通常用于容量评估,缺省权重为 100) - -``` -override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&weight=200 -``` - -- 调整负载均衡策略:(缺省负载均衡策略为 random) - -``` -override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&loadbalance=leastactive -``` - -- 服务降级:(通常用于临时屏蔽某个出错的非关键服务) - -``` -override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null -``` - -## Dubbo 架构 - -
- -
- -节点角色: - -| 节点 | 角色说明 | -| --------- | -------------------------------------- | -| Provider | 暴露服务的服务提供方 | -| Consumer | 调用远程服务的服务消费方 | -| Registry | 服务注册与发现的注册中心 | -| Monitor | 统计服务的调用次数和调用时间的监控中心 | -| Container | 服务运行容器 | - -调用关系: - -1. 务容器负责启动,加载,运行服务提供者。 -2. 服务提供者在启动时,向注册中心注册自己提供的服务。 -3. 服务消费者在启动时,向注册中心订阅自己所需的服务。 -4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 -5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 -6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 - -### 整体设计 - -
- -
- -图例说明: - -- 图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。 -- 图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。 -- 图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。 -- 图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。 - -#### 各层说明 - -- **config 配置层**:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类 -- **proxy 服务代理层**:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory -- **registry 注册中心层**:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory, Registry, RegistryService -- **cluster 路由层**:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster, Directory, Router, LoadBalance -- **monitor 监控层**:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory, Monitor, MonitorService -- **protocol 远程调用层**:封装 RPC 调用,以 Invocation, Result 为中心,扩展接口为 Protocol, Invoker, Exporter -- **exchange 信息交换层**:封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer -- **transport 网络传输层**:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec -- **serialize 数据序列化层**:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool -- **serialize 数据序列化层**:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool - -#### 各层关系说明 - -- 在 RPC 中,Protocol 是核心层,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 调用,然后在 Invoker 的主过程上 Filter 拦截点。 -- 图中的 Consumer 和 Provider 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不用 Client 和 Server 的原因是 Dubbo 在很多场景下都使用 Provider, Consumer, Registry, Monitor 划分逻辑拓普节点,保持统一概念。 -- 而 Cluster 是外围概念,所以 Cluster 的目的是将多个 Invoker 伪装成一个 Invoker,这样其它人只要关注 Protocol 层 Invoker 即可,加上 Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有一个提供者时,是不需要 Cluster 的。 -- Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 Proxy 将 Invoker 转成接口,或将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。 -- 而 Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会用上,Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展 UDP 传输,而 Exchange 层是在传输层之上封装了 Request-Response 语义。 -- Registry 和 Monitor 实际上不算一层,而是一个独立的节点,只是为了全局概览,用层的方式画在一起。 - -## 资料 - -[Github](https://github.com/apache/incubator-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/) diff --git a/docs/distributed/rpc/zookeeper-advanced.md b/docs/distributed/rpc/zookeeper-advanced.md deleted file mode 100644 index 0e236165..00000000 --- a/docs/distributed/rpc/zookeeper-advanced.md +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: ZooKeeper 高级篇 -date: 2018/07/10 -categories: -- 分布式 -tags: -- 分布式 -- rpc ---- - -# ZooKeeper 高级篇 - -> ZooKeeper 是一个分布式应用协调系统,已经用到了许多分布式项目中,用来完成统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等工作。 -> -> 本文侧重于总结 ZooKeeper 工作原理。 - - - -- [1. 概述](#1-概述) - - [1.1. ZooKeeper 是什么?](#11-zookeeper-是什么) - - [1.2. ZooKeeper 提供了什么?](#12-zookeeper-提供了什么) - - [1.3. Zookeeper 的特性](#13-zookeeper-的特性) - - [1.4. 工作原理](#14-工作原理) - - [1.5. Server 工作状态](#15-server-工作状态) -- [2. 文件系统](#2-文件系统) - - [2.1. znode 类型](#21-znode-类型) -- [3. 通知机制](#3-通知机制) -- [4. 应用场景](#4-应用场景) - - [4.1. 统一命名服务(Name Service)](#41-统一命名服务name-service) - - [4.2. 配置管理(Configuration Management)](#42-配置管理configuration-management) - - [4.3. 集群管理(Group Membership)](#43-集群管理group-membership) - - [4.4. 分布式锁](#44-分布式锁) - - [4.5. 队列管理](#45-队列管理) -- [5. 复制](#5-复制) -- [6. 选举流程](#6-选举流程) -- [7. 同步流程](#7-同步流程) -- [8. 资源](#8-资源) - - [8.1. 官方资源](#81-官方资源) - - [8.2. 文章](#82-文章) - - - -## 1. 概述 - -### 1.1. ZooKeeper 是什么? - -ZooKeeper 作为一个分布式的服务框架,主要用来解决分布式集群中应用系统的一致性问题,它能提供基于类似于文件系统的目录节点树方式的数据存储,但是 ZooKeeper 并不是用来专门存储数据的,它的作用主要是用来维护和监控你存储的数据的状态变化。通过监控这些数据状态的变化,从而可以达到基于数据的集群管理。 - -
- -
- -### 1.2. ZooKeeper 提供了什么? - -1. 文件系统 -2. 通知机制 - -### 1.3. Zookeeper 的特性 - -- 最终一致性:client 不论连接到哪个 Server,展示给它都是同一个视图,这是 zookeeper 最重要的性能。 -- 可靠性:具有简单、健壮、良好的性能,如果消息被到一台服务器接受,那么它将被所有的服务器接受。 -- 实时性:Zookeeper 保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。但由于网络延时等原因,Zookeeper 不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用 sync()接口。 -- 等待无关(wait-free):慢的或者失效的 client 不得干预快速的 client 的请求,使得每个 client 都能有效的等待。 -- 原子性:更新只能成功或者失败,没有中间状态。 -- 顺序性:包括全局有序和偏序两种:全局有序是指如果在一台服务器上消息 a 在消息 b 前发布,则在所有 Server 上消息 a 都将在消息 b 前被发布;偏序是指如果一个消息 b 在消息 a 后被同一个发送者发布,a 必将排在 b 前面。 - -### 1.4. 工作原理 - -ZooKeeper 的核心是原子广播,这个机制保证了各个 Server 之间的同步。实现这个机制的协议叫做 Zab 协议。Zab 协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab 就进入了恢复模式,当领导者被选举出来,且大多数 Server 完成了和 leader 的状态同步以后,恢复模式就结束了。状态同步保证了 leader 和 Server 具有相同的系统状态。 - -为了保证事务的顺序一致性,ZooKeeper 采用了递增的事务 id 号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了 zxid。实现中 zxid 是一个 64 位的数字,它高 32 位是 epoch 用来标识 leader 关系是否改变,每次一个 leader 被选出来,它都会有一个新的 epoch,标识当前属于那个 leader 的统治时期。低 32 位用于递增计数。 - -### 1.5. Server 工作状态 - -每个 Server 在工作过程中有三种状态: - -- LOOKING - 当前 Server 不知道 leader 是谁,正在搜寻 -- LEADING - 当前 Server 即为选举出来的 leader -- FOLLOWING - leader 已经选举出来,当前 Server 与之同步 - -## 2. 文件系统 - -ZooKeeper 会维护一个具有层次关系的数据结构,它非常类似于一个标准的文件系统,如下图所示: - -
- -
- -ZooKeeper 这种数据结构有如下这些特点: - -- 每个子目录项如 NameService 都被称作为 znode,这个 znode 是被它所在的路径唯一标识,如 Server1 这个 znode 的标识为 /NameService/Server1 -- znode 可以有子节点目录,并且每个 znode 可以存储数据,注意 EPHEMERAL 类型的目录节点不能有子节点目录 -- znode 是有版本的,每个 znode 中存储的数据可以有多个版本,也就是一个访问路径中可以存储多份数据 -- znode 可以是临时节点,一旦创建这个 znode 的客户端与服务器失去联系,这个 znode 也将自动删除,ZooKeeper 的客户端和服务器通信采用长连接方式,每个客户端和服务器通过心跳来保持连接,这个连接状态称为 session,如果 znode 是临时节点,这个 session 失效,znode 也就删除了 -- znode 的目录名可以自动编号,如 App1 已经存在,再创建的话,将会自动命名为 App2 -- znode 可以被监控,包括这个目录节点中存储的数据的修改,子节点目录的变化等,一旦变化可以通知设置监控的客户端,这个是 ZooKeeper 的核心特性,ZooKeeper 的很多功能都是基于这个特性实现的,后面在典型的应用场景中会有实例介绍 - -### 2.1. znode 类型 - -1. PERSISTENT(持久化目录节点) - 客户端与 zookeeper 断开连接后,该节点依旧存在 -2. PERSISTENT_SEQUENTIAL(持久化顺序编号目录节点) - 客户端与 zookeeper 断开连接后,该节点依旧存在,只是 Zookeeper 给该节点名称进行顺序编号 -3. EPHEMERAL(临时目录节点) - 客户端与 zookeeper 断开连接后,该节点被删除 -4. EPHEMERAL_SEQUENTIAL(临时顺序编号目录节点) - 客户端与 zookeeper 断开连接后,该节点被删除,只是 Zookeeper 给该节点名称进行顺序编号 - -## 3. 通知机制 - -客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)时,zookeeper 会通知客户端。 - -## 4. 应用场景 - -### 4.1. 统一命名服务(Name Service) - -分布式应用中,通常需要有一套完整的命名规则,既能够产生唯一的名称又便于人识别和记住,通常情况下用树形的名称结构是一个理想的选择,树形的名称结构是一个有层次的目录结构,既对人友好又不会重复。说到这里你可能想到了 JNDI,没错 ZooKeeper 的 Name Service 与 JNDI 能够完成的功能是差不多的,它们都是将有层次的目录结构关联到一定资源上,但是 ZooKeeper 的 Name Service 更加是广泛意义上的关联,也许你并不需要将名称关联到特定资源上,你可能只需要一个不会重复名称,就像数据库中产生一个唯一的数字主键一样。 - -Name Service 已经是 ZooKeeper 内置的功能,你只要调用 ZooKeeper 的 API 就能实现。如调用 create 接口就可以很容易创建一个目录节点。 - -### 4.2. 配置管理(Configuration Management) - -配置的管理在分布式应用环境中很常见,例如同一个应用系统需要多台 PC Server 运行,但是它们运行的应用系统的某些配置项是相同的,如果要修改这些相同的配置项,那么就必须同时修改每台运行这个应用系统的 PC Server,这样非常麻烦而且容易出错。 - -像这样的配置信息完全可以交给 ZooKeeper 来管理,将配置信息保存在 ZooKeeper 的某个目录节点中,然后将所有需要修改的应用机器监控配置信息的状态,一旦配置信息发生变化,每台应用机器就会收到 ZooKeeper 的通知,然后从 ZooKeeper 获取新的配置信息应用到系统中。 - -
- -
- -### 4.3. 集群管理(Group Membership) - -ZooKeeper 能够很容易的实现集群管理的功能,如有多台 Server 组成一个服务集群,那么必须要一个“总管”知道当前集群中每台机器的服务状态,一旦有机器不能提供服务,集群中其它集群必须知道,从而做出调整重新分配服务策略。同样当增加集群的服务能力时,就会增加一台或多台 Server,同样也必须让“总管”知道。 - -ZooKeeper 不仅能够帮你维护当前的集群中机器的服务状态,而且能够帮你选出一个“总管”,让这个总管来管理集群,这就是 ZooKeeper 的另一个功能 Leader Election。 - -它们的实现方式都是在 ZooKeeper 上创建一个 EPHEMERAL 类型的目录节点,然后每个 Server 在它们创建目录节点的父目录节点上调用 getChildren(String path, boolean watch) 方法并设置 watch 为 true,由于是 EPHEMERAL 目录节点,当创建它的 Server 死去,这个目录节点也随之被删除,所以 Children 将会变化,这时 getChildren 上的 Watch 将会被调用,所以其它 Server 就知道已经有某台 Server 死去了。新增 Server 也是同样的原理。 - -ZooKeeper 如何实现 Leader Election,也就是选出一个 Master Server。和前面的一样每台 Server 创建一个 EPHEMERAL 目录节点,不同的是它还是一个 SEQUENTIAL 目录节点,所以它是个 EPHEMERAL_SEQUENTIAL 目录节点。之所以它是 EPHEMERAL_SEQUENTIAL 目录节点,是因为我们可以给每台 Server 编号,我们可以选择当前是最小编号的 Server 为 Master,假如这个最小编号的 Server 死去,由于是 EPHEMERAL 节点,死去的 Server 对应的节点也被删除,所以当前的节点列表中又出现一个最小编号的节点,我们就选择这个节点为当前 Master。这样就实现了动态选择 Master,避免了传统意义上单 Master 容易出现单点故障的问题。 - -
- -
- -### 4.4. 分布式锁 - -ZooKeeper 实现分布式锁的步骤: - -1. 创建一个目录 mylock; -2. 线程 A 想获取锁就在 mylock 目录下创建临时顺序节点; -3. 获取 mylock 目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; -4. 线程 B 获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; -5. 线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。 - -ZooKeeper 版本的分布式锁问题相对比较来说少。 - -- 锁的占用时间限制:redis 就有占用时间限制,而 ZooKeeper 则没有,最主要的原因是 redis 目前没有办法知道已经获取锁的客户端的状态,是已经挂了呢还是正在执行耗时较长的业务逻辑。而 ZooKeeper 通过临时节点就能清晰知道,如果临时节点存在说明还在执行业务逻辑,如果临时节点不存在说明已经执行完毕释放锁或者是挂了。由此看来 redis 如果能像 ZooKeeper 一样添加一些与客户端绑定的临时键,也是一大好事。 -- 是否单点故障:redis 本身有很多中玩法,如客户端一致性 hash,服务器端 sentinel 方案或者 cluster 方案,很难做到一种分布式锁方式能应对所有这些方案。而 ZooKeeper 只有一种玩法,多台机器的节点数据是一致的,没有 redis 的那么多的麻烦因素要考虑。 - -总体上来说 ZooKeeper 实现分布式锁更加的简单,可靠性更高。但 ZooKeeper 因为需要频繁的创建和删除节点,性能上不如 Redis 方式。 - -### 4.5. 队列管理 - -ZooKeeper 可以处理两种类型的队列: - -1. 当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达,这种是同步队列。 -2. 队列按照 FIFO 方式进行入队和出队操作,例如实现生产者和消费者模型。 - -同步队列用 ZooKeeper 实现的实现思路如下: - -创建一个父目录 /synchronizing,每个成员都监控标志(Set Watch)位目录 /synchronizing/start 是否存在,然后每个成员都加入这个队列,加入队列的方式就是创建 /synchronizing/member_i 的临时目录节点,然后每个成员获取 / synchronizing 目录的所有目录节点,也就是 member_i。判断 i 的值是否已经是成员的个数,如果小于成员个数等待 /synchronizing/start 的出现,如果已经相等就创建 /synchronizing/start。 - -## 5. 复制 - -ZooKeeper 作为一个集群提供一致的数据服务,自然,它要在所有机器间做数据复制。 - -从客户端读写访问的透明度来看,数据复制集群系统分下面两种: - -- 写主(WriteMaster) :对数据的修改提交给指定的节点。读无此限制,可以读取任何一个节点。这种情况下客户端需要对读与写进行区别,俗称读写分离; -- 写任意(Write Any):对数据的修改可提交给任意的节点,跟读一样。这种情况下,客户端对集群节点的角色与变化透明。 - -对 ZooKeeper 来说,它采用的方式是写任意。通过增加机器,它的读吞吐能力和响应能力扩展性非常好,而写,随着机器的增多吞吐能力肯定下降(这也是它建立 observer 的原因),而响应能力则取决于具体实现方式,是延迟复制保持最终一致性,还是立即复制快速响应。 - -## 6. 选举流程 - -选举状态: - -- LOOKING,竞选状态。 -- FOLLOWING,随从状态,同步 leader 状态,参与投票。 -- OBSERVING,观察状态,同步 leader 状态,不参与投票。 -- LEADING,领导者状态。 - -ZooKeeper 选举流程基于 Paxos 算法。 - -
- -
- -1. 选举线程由当前 Server 发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的 Server; -2. 选举线程首先向所有 Server 发起一次询问(包括自己); -3. 选举线程收到回复后,验证是否是自己发起的询问(验证 zxid 是否一致),然后获取对方的 id(myid),并存储到当前询问对象列表中,最后获取对方提议的 leader 相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中; -4. 收到所有 Server 回复以后,就计算出 zxid 最大的那个 Server,并将这个 Server 相关信息设置成下一次要投票的 Server; -5. 线程将当前 zxid 最大的 Server 设置为当前 Server 要推荐的 Leader,如果此时获胜的 Server 获得 n/2 + 1 的 Server 票数,设置当前推荐的 leader 为获胜的 Server,将根据获胜的 Server 相关信息设置自己的状态,否则,继续这个过程,直到 leader 被选举出来。 通过流程分析我们可以得出:要使 Leader 获得多数 Server 的支持,则 Server 总数必须是奇数 2n+1,且存活的 Server 的数目不得少于 n+1. 每个 Server 启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的 server 还会从磁盘快照中恢复数据和会话信息,zk 会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。 - -述 Leader 选择过程中的状态变化,这是假设全部实例中均没有数据,假设服务器启动顺序分别为:A,B,C。 - -## 7. 同步流程 - -选完 Leader 以后,zk 就进入状态同步过程。 - -1. Leader 等待 server 连接; -2. Follower 连接 leader,将最大的 zxid 发送给 leader; -3. Leader 根据 follower 的 zxid 确定同步点; -4. 完成同步后通知 follower 已经成为 uptodate 状态; -5. Follower 收到 uptodate 消息后,又可以重新接受 client 的请求进行服务了。 - -## 8. 资源 - -### 8.1. 官方资源 - -| [官网](http://zookeeper.apache.org/) | [官网文档](https://cwiki.apache.org/confluence/display/ZOOKEEPER) | [Github](https://github.com/apache/zookeeper) | - -### 8.2. 文章 - -[分布式服务框架 ZooKeeper -- 管理分布式环境中的数据](https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/index.html) -[ZooKeeper 的功能以及工作原理](https://www.cnblogs.com/felixzh/p/5869212.html) diff --git a/docs/distributed/rpc/zookeeper-basics.md b/docs/distributed/rpc/zookeeper-basics.md deleted file mode 100644 index 0d89c0e7..00000000 --- a/docs/distributed/rpc/zookeeper-basics.md +++ /dev/null @@ -1,1016 +0,0 @@ ---- -title: ZooKeeper 基础篇 -date: 2018/07/12 -categories: -- 分布式 -tags: -- 分布式 -- rpc ---- - -# ZooKeeper 基础篇 - -> ZooKeeper 是一个针对大型分布式系统的可靠协调系统,提供的功能包括:配置维护、名字服务、分布式同步、组服务等。 -> -> ZooKeeper 的目标就是封装好复杂易出错的关键服务,将简单易用的接口和性能高效、功能稳定的系统提供给用户。 -> -> 本文旨在快速入门 ZooKeeper,侧重于介绍如何使用。 - - - -- [1. 安装](#1-安装) - - [1.1. 下载解压 ZooKeeper](#11-下载解压-zookeeper) - - [1.2. 创建配置文件](#12-创建配置文件) - - [1.3. 启动 ZooKeeper 服务器](#13-启动-zookeeper-服务器) - - [1.4. 启动 CLI](#14-启动-cli) - - [1.5. 停止 ZooKeeper 服务器](#15-停止-zookeeper-服务器) -- [2. CLI](#2-cli) - - [2.1. 创建 Znodes](#21-创建-znodes) - - [2.2. 获取数据](#22-获取数据) - - [2.3. Watch(监视)](#23-watch监视) - - [2.4. 设置数据](#24-设置数据) - - [2.5. 创建子项/子节点](#25-创建子项子节点) - - [2.6. 列出子项](#26-列出子项) - - [2.7. 检查状态](#27-检查状态) - - [2.8. 移除 Znode](#28-移除-znode) -- [3. API](#3-api) - - [3.1. ZooKeeper API 的基础知识](#31-zookeeper-api-的基础知识) - - [3.2. Java 绑定](#32-java-绑定) - - [3.3. 连接到 ZooKeeper 集合](#33-连接到-zookeeper-集合) - - [3.4. 创建 Znode](#34-创建-znode) - - [3.5. Exists - 检查 Znode 的存在](#35-exists---检查-znode-的存在) - - [3.6. getData 方法](#36-getdata-方法) - - [3.7. setData 方法](#37-setdata-方法) - - [3.8. getChildren 方法](#38-getchildren-方法) - - [3.9. 删除 Znode](#39-删除-znode) -- [4. 资源](#4-资源) - - - -## 1. 安装 - -在安装 ZooKeeper 之前,请确保你的系统是在以下任一操作系统上运行: - -- **任意 Linux OS** - 支持开发和部署。适合演示应用程序。 -- **Windows OS** - 仅支持开发。 -- **Mac OS** - 仅支持开发。 - -环境要求:JDK6+ - -安装步骤如下: - -### 1.1. 下载解压 ZooKeeper - -进入官方下载地址:http://zookeeper.apache.org/releases.html#download ,选择合适版本。 - -解压到本地: - -``` -$ tar -zxf zookeeper-3.4.6.tar.gz -$ cd zookeeper-3.4.6 -``` - -### 1.2. 创建配置文件 - -你必须创建 `conf/zoo.cfg` 文件,否则启动时会提示你没有此文件。 - -初次尝试,不妨直接使用 Kafka 提供的模板配置文件 `conf/zoo_sample.cfg`: - -``` -$ cp conf/zoo_sample.cfg conf/zoo.cfg -``` - -### 1.3. 启动 ZooKeeper 服务器 - -执行以下命令 - -``` -$ bin/zkServer.sh start -``` - -执行此命令后,你将收到以下响应 - -``` -$ JMX enabled by default -$ Using config: /Users/../zookeeper-3.4.6/bin/../conf/zoo.cfg -$ Starting zookeeper ... STARTED -``` - -### 1.4. 启动 CLI - -键入以下命令 - -``` -$ bin/zkCli.sh -``` - -键入上述命令后,将连接到 ZooKeeper 服务器,你应该得到以下响应。 - -``` -Connecting to localhost:2181 -................ -................ -................ -Welcome to ZooKeeper! -................ -................ -WATCHER:: -WatchedEvent state:SyncConnected type: None path:null -[zk: localhost:2181(CONNECTED) 0] -``` - -### 1.5. 停止 ZooKeeper 服务器 - -连接服务器并执行所有操作后,可以使用以下命令停止 zookeeper 服务器。 - -``` -$ bin/zkServer.sh stop -``` - -> 本节安装内容参考:[Zookeeper 安装](https://www.w3cschool.cn/zookeeper/zookeeper_installation.html) - -## 2. CLI - -ZooKeeper 命令行界面(CLI)用于与 ZooKeeper 集合进行交互以进行开发。它有助于调试和解决不同的选项。 - -要执行 ZooKeeper CLI 操作,首先打开 ZooKeeper 服务器(“bin/zkServer.sh start”),然后打开 ZooKeeper 客户端(“bin/zkCli.sh”)。一旦客户端启动,你可以执行以下操作: - -- 创建 znode -- 获取数据 -- 监视 znode 的变化 -- 设置数据 -- 创建 znode 的子节点 -- 列出 znode 的子节点 -- 检查状态 -- 移除/删除 znode - -现在让我们用一个例子逐个了解上面的命令。 - -### 2.1. 创建 Znodes - -用给定的路径创建一个 znode。flag 参数指定创建的 znode 是临时的,持久的还是顺序的。默认情况下,所有 znode 都是持久的。 - -当会话过期或客户端断开连接时,临时节点(flag:-e)将被自动删除。 - -顺序节点保证 znode 路径将是唯一的。 - -ZooKeeper 集合将向 znode 路径填充 10 位序列号。例如,znode 路径 /myapp 将转换为 /myapp0000000001,下一个序列号将为 /myapp0000000002。如果没有指定 flag,则 znode 被认为是持久的。 - -语法: - -``` -create /path /data -``` - -示例: - -``` -create /FirstZnode “Myfirstzookeeper-app" -``` - -输出: - -``` -[zk: localhost:2181(CONNECTED) 0] create /FirstZnode “Myfirstzookeeper-app" -Created /FirstZnode -``` - -要创建**顺序节点**,请添加 flag:**-s**,如下所示。 - -语法: - -``` -create -s /path /data -``` - -示例: - -``` -create -s /FirstZnode second-data -``` - -输出: - -``` -[zk: localhost:2181(CONNECTED) 2] create -s /FirstZnode “second-data" -Created /FirstZnode0000000023 -``` - -要创建**临时节点**,请添加 flag:**-e** ,如下所示。 - -语法: - -``` -create -e /path /data -``` - -示例: - -``` -create -e /SecondZnode “Ephemeral-data" -``` - -输出: - -``` -[zk: localhost:2181(CONNECTED) 2] create -e /SecondZnode “Ephemeral-data" -Created /SecondZnode -``` - -记住当客户端断开连接时,临时节点将被删除。你可以通过退出 ZooKeeper CLI,然后重新打开 CLI 来尝试。 - -### 2.2. 获取数据 - -它返回 znode 的关联数据和指定 znode 的元数据。你将获得信息,例如上次修改数据的时间,修改的位置以及数据的相关信息。此 CLI 还用于分配监视器以显示数据相关的通知。 - -语法: - -``` -get /path -``` - -示例: - -``` -get /FirstZnode -``` - -输出: - -``` -[zk: localhost:2181(CONNECTED) 1] get /FirstZnode -“Myfirstzookeeper-app" -cZxid = 0x7f -ctime = Tue Sep 29 16:15:47 IST 2015 -mZxid = 0x7f -mtime = Tue Sep 29 16:15:47 IST 2015 -pZxid = 0x7f -cversion = 0 -dataVersion = 0 -aclVersion = 0 -ephemeralOwner = 0x0 -dataLength = 22 -numChildren = 0 -``` - -要访问顺序节点,必须输入 znode 的完整路径。 - -示例: - -``` -get /FirstZnode0000000023 -``` - -输出: - -``` -[zk: localhost:2181(CONNECTED) 1] get /FirstZnode0000000023 -“Second-data" -cZxid = 0x80 -ctime = Tue Sep 29 16:25:47 IST 2015 -mZxid = 0x80 -mtime = Tue Sep 29 16:25:47 IST 2015 -pZxid = 0x80 -cversion = 0 -dataVersion = 0 -aclVersion = 0 -ephemeralOwner = 0x0 -dataLength = 13 -numChildren = 0 -``` - -### 2.3. Watch(监视) - -当指定的 znode 或 znode 的子数据更改时,监视器会显示通知。你只能在 **get** 命令中设置**watch**。 - -语法: - -``` -get /path [watch] 1 -``` - -示例: - -``` -get /FirstZnode 1 -``` - -输出: - -``` -[zk: localhost:2181(CONNECTED) 1] get /FirstZnode 1 -“Myfirstzookeeper-app" -cZxid = 0x7f -ctime = Tue Sep 29 16:15:47 IST 2015 -mZxid = 0x7f -mtime = Tue Sep 29 16:15:47 IST 2015 -pZxid = 0x7f -cversion = 0 -dataVersion = 0 -aclVersion = 0 -ephemeralOwner = 0x0 -dataLength = 22 -numChildren = 0 -``` - -输出类似于普通的 **get** 命令,但它会等待后台等待 znode 更改。<从这里开始> - -### 2.4. 设置数据 - -设置指定 znode 的数据。完成此设置操作后,你可以使用 **get** CLI 命令检查数据。 - -语法: - -``` -set /path /data -``` - -示例: - -``` -set /SecondZnode Data-updated -``` - -输出: - -``` -[zk: localhost:2181(CONNECTED) 1] get /SecondZnode “Data-updated" -cZxid = 0x82 -ctime = Tue Sep 29 16:29:50 IST 2015 -mZxid = 0x83 -mtime = Tue Sep 29 16:29:50 IST 2015 -pZxid = 0x82 -cversion = 0 -dataVersion = 1 -aclVersion = 0 -ephemeralOwner = 0x15018b47db00000 -dataLength = 14 -numChildren = 0 -``` - -如果你在 **get** 命令中分配了**watch**选项(如上一个命令),则输出将类似如下所示。 - -输出: - -``` -[zk: localhost:2181(CONNECTED) 1] get /FirstZnode “Mysecondzookeeper-app" - -WATCHER: : - -WatchedEvent state:SyncConnected type:NodeDataChanged path:/FirstZnode -cZxid = 0x7f -ctime = Tue Sep 29 16:15:47 IST 2015 -mZxid = 0x84 -mtime = Tue Sep 29 17:14:47 IST 2015 -pZxid = 0x7f -cversion = 0 -dataVersion = 1 -aclVersion = 0 -ephemeralOwner = 0x0 -dataLength = 23 -numChildren = 0 -``` - -### 2.5. 创建子项/子节点 - -创建子节点类似于创建新的 znode。唯一的区别是,子 znode 的路径也将具有父路径。 - -语法: - -``` -create /parent/path/subnode/path /data -``` - -示例: - -``` -create /FirstZnode/Child1 firstchildren -``` - -输出: - -``` -[zk: localhost:2181(CONNECTED) 16] create /FirstZnode/Child1 “firstchildren" -created /FirstZnode/Child1 -[zk: localhost:2181(CONNECTED) 17] create /FirstZnode/Child2 “secondchildren" -created /FirstZnode/Child2 -``` - -### 2.6. 列出子项 - -此命令用于列出和显示 znode 的子项。 - -语法: - -``` -ls /path -``` - -示例: - -``` -ls /MyFirstZnode -``` - -输出: - -``` -[zk: localhost:2181(CONNECTED) 2] ls /MyFirstZnode -[mysecondsubnode, myfirstsubnode] -``` - -### 2.7. 检查状态 - -状态描述指定的 znode 的元数据。它包含时间戳,版本号,ACL,数据长度和子 znode 等细项。 - -语法: - -``` -stat /path -``` - -示例: - -``` -stat /FirstZnode -``` - -输出: - -``` -[zk: localhost:2181(CONNECTED) 1] stat /FirstZnode -cZxid = 0x7f -ctime = Tue Sep 29 16:15:47 IST 2015 -mZxid = 0x7f -mtime = Tue Sep 29 17:14:24 IST 2015 -pZxid = 0x7f -cversion = 0 -dataVersion = 1 -aclVersion = 0 -ephemeralOwner = 0x0 -dataLength = 23 -numChildren = 0 -``` - -### 2.8. 移除 Znode - -移除指定的 znode 并递归其所有子节点。只有在这样的 znode 可用的情况下才会发生。 - -语法: - -``` -rmr /path -``` - -示例: - -``` -rmr /FirstZnode -``` - -输出: - -``` -[zk: localhost:2181(CONNECTED) 10] rmr /FirstZnode -[zk: localhost:2181(CONNECTED) 11] get /FirstZnode -Node does not exist: /FirstZnode -``` - -删除(delete/path)命令类似于 remove 命令,除了它只适用于没有子节点的 znode。 - -## 3. API - -ZooKeeper 有一个绑定 Java 和 C 的官方 API。Zookeeper 社区为大多数语言(.NET,python 等)提供非官方 API。 - -使用 ZooKeeper API,应用程序可以连接,交互,操作数据,协调,最后断开与 ZooKeeper 集合的连接。 - -ZooKeeper API 具有丰富的功能,以简单和安全的方式获得 ZooKeeper 集合的所有功能。ZooKeeper API 提供同步和异步方法。 - -ZooKeeper 集合和 ZooKeeper API 在各个方面都完全相辅相成,对开发人员有很大的帮助。让我们在本章讨论 Java 绑定。 - -### 3.1. ZooKeeper API 的基础知识 - -与 ZooKeeper 集合进行交互的应用程序称为 **ZooKeeper 客户端**。 - -Znode 是 ZooKeeper 集合的核心组件,ZooKeeper API 提供了一小组方法使用 ZooKeeper 集合来操纵 znode 的所有细节。 - -客户端应该遵循以步骤,与 ZooKeeper 集合进行清晰和干净的交互。 - -- 连接到 ZooKeeper 集合。ZooKeeper 集合为客户端分配会话 ID。 -- 定期向服务器发送心跳。否则,ZooKeeper 集合将过期会话 ID,客户端需要重新连接。 -- 只要会话 ID 处于活动状态,就可以获取/设置 znode。 -- 所有任务完成后,断开与 ZooKeeper 集合的连接。如果客户端长时间不活动,则 ZooKeeper 集合将自动断开客户端。 - -### 3.2. Java 绑定 - -让我们来了解本章中最重要的一组 ZooKeeper API。ZooKeeper API 的核心部分是**ZooKeeper 类**。它提供了在其构造函数中连接 ZooKeeper 集合的选项,并具有以下方法: - -- **connect** - 连接到 ZooKeeper 集合 -- **create**- 创建 znode -- **exists**- 检查 znode 是否存在及其信息 -- **getData** - 从特定的 znode 获取数据 -- **setData** - 在特定的 znode 中设置数据 -- **getChildren** - 获取特定 znode 中的所有子节点 -- **delete** - 删除特定的 znode 及其所有子项 -- **close** - 关闭连接 - -### 3.3. 连接到 ZooKeeper 集合 - -ZooKeeper 类通过其构造函数提供 connect 功能。构造函数的签名如下 : - -``` -ZooKeeper(String connectionString, int sessionTimeout, Watcher watcher) -``` - -- **connectionString** - ZooKeeper 集合主机。 -- **sessionTimeout** - 会话超时(以毫秒为单位)。 -- **watcher** - 实现“监视器”界面的对象。ZooKeeper 集合通过监视器对象返回连接状态。 - -让我们创建一个新的帮助类 **ZooKeeperConnection** ,并添加一个方法 **connect** 。 **connect** 方法创建一个 ZooKeeper 对象,连接到 ZooKeeper 集合,然后返回对象。 - -这里 **CountDownLatch** 用于停止(等待)主进程,直到客户端与 ZooKeeper 集合连接。 - -ZooKeeper 集合通过监视器回调来回复连接状态。一旦客户端与 ZooKeeper 集合连接,监视器回调就会被调用,并且监视器回调函数调用**CountDownLatch**的**countDown**方法来释放锁,在主进程中**await**。 - -以下是与 ZooKeeper 集合连接的完整代码。 - -示例: - -```java -// import java classes -import java.io.IOException; -import java.util.concurrent.CountDownLatch; - -// import zookeeper classes -import org.apache.zookeeper.KeeperException; -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.AsyncCallback.StatCallback; -import org.apache.zookeeper.KeeperException.Code; -import org.apache.zookeeper.data.Stat; - -public class ZooKeeperConnection { - - // declare zookeeper instance to access ZooKeeper ensemble - private ZooKeeper zoo; - final CountDownLatch connectedSignal = new CountDownLatch(1); - - // Method to connect zookeeper ensemble. - public ZooKeeper connect(String host) throws IOException,InterruptedException { - - zoo = new ZooKeeper(host,5000,new Watcher() { - - 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(); - } -} -``` - -保存上面的代码,它将在下一节中用于连接 ZooKeeper 集合。 - -### 3.4. 创建 Znode - -ZooKeeper 类提供了在 ZooKeeper 集合中创建一个新的 znode 的**create**方法。 **create** 方法的签名如下: - -``` -create(String path, byte[] data, List acl, CreateMode createMode) -``` - -- **path** - Znode 路径。例如,/myapp1,/myapp2,/myapp1/mydata1,myapp2/mydata1/myanothersubdata -- **data** - 要存储在指定 znode 路径中的数据 -- **acl** - 要创建的节点的访问控制列表。ZooKeeper API 提供了一个静态接口 **ZooDefs.Ids** 来获取一些基本的 acl 列表。例如,ZooDefs.Ids.OPEN_ACL_UNSAFE 返回打开 znode 的 acl 列表。 -- **createMode** - 节点的类型,即临时,顺序或两者。这是一个**枚举**。 - -让我们创建一个新的 Java 应用程序来检查 ZooKeeper API 的 **create** 功能。创建文件 **ZKCreate.java** 。在 main 方法中,创建一个类型为 **ZooKeeperConnection** 的对象,并调用 **connect** 方法连接到 ZooKeeper 集合。 - -connect 方法将返回 ZooKeeper 对象 **zk** 。现在,请使用自定义**path**和**data**调用 **zk** 对象的 **create** 方法。 - -创建 znode 的完整程序代码如下: - -示例: - -```java -import java.io.IOException; - -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.KeeperException; -import org.apache.zookeeper.CreateMode; -import org.apache.zookeeper.ZooDefs; - -public class ZKCreate { - // create static instance for zookeeper class. - private static ZooKeeper zk; - - // create static instance for ZooKeeperConnection class. - private static ZooKeeperConnection conn; - - // 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); - } - - public static void main(String[] args) { - - // 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("localhost"); - create(path, data); // Create the data to the specified path - conn.close(); - } catch (Exception e) { - System.out.println(e.getMessage()); //Catch error message - } - } -} -``` - -一旦编译和执行应用程序,将在 ZooKeeper 集合中创建具有指定数据的 znode。你可以使用 ZooKeeper CLI **zkCli.sh** 进行检查。 - -``` -cd /path/to/zookeeper -bin/zkCli.sh ->>> get /MyFirstZnode -``` - -### 3.5. Exists - 检查 Znode 的存在 - -ZooKeeper 类提供了 **exists** 方法来检查 znode 的存在。如果指定的 znode 存在,则返回一个 znode 的元数据。**exists**方法的签名如下: - -``` -exists(String path, boolean watcher) -``` - -- **path**- Znode 路径 -- **watcher** - 布尔值,用于指定是否监视指定的 znode - -让我们创建一个新的 Java 应用程序来检查 ZooKeeper API 的“exists”功能。创建文件“ZKExists.java”。在 main 方法中,使用“ZooKeeperConnection”对象创建 ZooKeeper 对象“zk”。然后,使用自定义“path”调用“zk”对象的“exists”方法。完整的列表如下: - -示例: - -```java -import java.io.IOException; - -import org.apache.zookeeper.ZooKeeper; -import org.apache.zookeeper.KeeperException; -import org.apache.zookeeper.WatchedEvent; -import org.apache.zookeeper.Watcher; -import org.apache.zookeeper.Watcher.Event.KeeperState; -import org.apache.zookeeper.data.Stat; - -public class ZKExists { - private static ZooKeeper zk; - private static ZooKeeperConnection conn; - - // 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); - } - - 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("localhost"); - Stat stat = znode_exists(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 - } - } -} -``` - -一旦编译和执行应用程序,你将获得以下输出。 - -``` -Node exists and the node version is 1. -``` - -### 3.6. getData 方法 - -ZooKeeper 类提供 **getData** 方法来获取附加在指定 znode 中的数据及其状态。 **getData** 方法的签名如下: - -``` -getData(String path, Watcher watcher, Stat stat) -``` - -- **path** - Znode 路径。 -- **watcher** - 监视器类型的回调函数。当指定的 znode 的数据改变时,ZooKeeper 集合将通过监视器回调进行通知。这是一次性通知。 -- **stat** - 返回 znode 的元数据。 - -让我们创建一个新的 Java 应用程序来了解 ZooKeeper API 的 **getData** 功能。创建文件 **ZKGetData.java** 。在 main 方法中,使用 **ZooKeeperConnection** 对象创建一个 ZooKeeper 对象 **zk** 。然后,使用自定义路径调用 zk 对象的 **getData** 方法。 - -下面是从指定节点获取数据的完整程序代码: - -示例: - -```java -import java.io.IOException; -import java.util.concurrent.CountDownLatch; - -import org.apache.zookeeper.ZooKeeper; -import org.apache.zookeeper.KeeperException; -import org.apache.zookeeper.WatchedEvent; -import org.apache.zookeeper.Watcher; -import org.apache.zookeeper.Watcher.Event.KeeperState; -import org.apache.zookeeper.data.Stat; - -public class ZKGetData { - - private static ZooKeeper zk; - private static ZooKeeperConnection conn; - public static Stat znode_exists(String path) throws - KeeperException,InterruptedException { - return zk.exists(path,true); - } - - public static void main(String[] args) throws InterruptedException, KeeperException { - String path = "/MyFirstZnode"; - final CountDownLatch connectedSignal = new CountDownLatch(1); - - try { - conn = new ZooKeeperConnection(); - zk = conn.connect("localhost"); - Stat stat = znode_exists(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()); - } - } -} -``` - -一旦编译和执行应用程序,你将获得以下输出 - -``` -My first zookeeper app -``` - -应用程序将等待 ZooKeeper 集合的进一步通知。使用 ZooKeeper CLI **zkCli.sh** 更改指定 znode 的数据。 - -``` -cd /path/to/zookeeper -bin/zkCli.sh ->>> set /MyFirstZnode Hello -``` - -现在,应用程序将打印以下输出并退出。 - -``` -Hello -``` - -### 3.7. setData 方法 - -ZooKeeper 类提供 **setData** 方法来修改指定 znode 中附加的数据。 **setData** 方法的签名如下: - -``` -setData(String path, byte[] data, int version) -``` - -- **path**- Znode 路径 -- **data** - 要存储在指定 znode 路径中的数据。 -- **version**- znode 的当前版本。每当数据更改时,ZooKeeper 会更新 znode 的版本号。 - -现在让我们创建一个新的 Java 应用程序来了解 ZooKeeper API 的 **setData** 功能。创建文件 **ZKSetData.java** 。在 main 方法中,使用 **ZooKeeperConnection** 对象创建一个 ZooKeeper 对象 **zk** 。然后,使用指定的路径,新数据和节点版本调用 **zk** 对象的 **setData** 方法。 - -以下是修改附加在指定 znode 中的数据的完整程序代码。 - -示例: - -```java -import org.apache.zookeeper.ZooKeeper; -import org.apache.zookeeper.KeeperException; -import org.apache.zookeeper.WatchedEvent; -import org.apache.zookeeper.Watcher; -import org.apache.zookeeper.Watcher.Event.KeeperState; - -import java.io.IOException; - -public class ZKSetData { - private static ZooKeeper zk; - private static ZooKeeperConnection conn; - - // 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()); - } - - public static void main(String[] args) throws InterruptedException,KeeperException { - String path= "/MyFirstZnode"; - byte[] data = "Success".getBytes(); //Assign data which is to be updated. - - try { - conn = new ZooKeeperConnection(); - zk = conn.connect("localhost"); - update(path, data); // Update znode data to the specified path - } catch(Exception e) { - System.out.println(e.getMessage()); - } - } -} -``` - -编译并执行应用程序后,指定的 znode 的数据将被改变,并且可以使用 ZooKeeper CLI **zkCli.sh** 进行检查。 - -``` -cd /path/to/zookeeper -bin/zkCli.sh ->>> get /MyFirstZnode -``` - -### 3.8. getChildren 方法 - -ZooKeeper 类提供 **getChildren** 方法来获取特定 znode 的所有子节点。 **getChildren** 方法的签名如下: - -``` -getChildren(String path, Watcher watcher) -``` - -- **path** - Znode 路径。 -- **watcher** - 监视器类型的回调函数。当指定的 znode 被删除或 znode 下的子节点被创建/删除时,ZooKeeper 集合将进行通知。这是一次性通知。 - -示例: - -```java -import java.io.IOException; -import java.util.*; - -import org.apache.zookeeper.ZooKeeper; -import org.apache.zookeeper.KeeperException; -import org.apache.zookeeper.WatchedEvent; -import org.apache.zookeeper.Watcher; -import org.apache.zookeeper.Watcher.Event.KeeperState; -import org.apache.zookeeper.data.Stat; - -public class ZKGetChildren { - private static ZooKeeper zk; - private static ZooKeeperConnection conn; - - // 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); - } - - public static void main(String[] args) throws InterruptedException,KeeperException { - String path = "/MyFirstZnode"; // Assign path to the znode - - try { - conn = new ZooKeeperConnection(); - zk = conn.connect("localhost"); - 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()); - } - - } -} -``` - -在运行程序之前,让我们使用 ZooKeeper CLI **zkCli.sh** 为 **/MyFirstZnode** 创建两个子节点。 - -``` -cd /path/to/zookeeper -bin/zkCli.sh ->>> create /MyFirstZnode/myfirstsubnode Hi ->>> create /MyFirstZnode/mysecondsubmode Hi -``` - -现在,编译和运行程序将输出上面创建的 znode。 - -``` -myfirstsubnode -mysecondsubnode -``` - -### 3.9. 删除 Znode - -ZooKeeper 类提供了 **delete** 方法来删除指定的 znode。 **delete** 方法的签名如下: - -``` -delete(String path, int version) -``` - -- **path** - Znode 路径。 -- **version** - znode 的当前版本。 - -让我们创建一个新的 Java 应用程序来了解 ZooKeeper API 的 **delete** 功能。创建文件 **ZKDelete.java** 。在 main 方法中,使用 **ZooKeeperConnection** 对象创建一个 ZooKeeper 对象 **zk** 。然后,使用指定的路径和版本号调用 **zk** 对象的 **delete** 方法。 - -删除 znode 的完整程序代码如下: - -示例: - -```java -import org.apache.zookeeper.ZooKeeper; -import org.apache.zookeeper.KeeperException; - -public class ZKDelete { - private static ZooKeeper zk; - private static ZooKeeperConnection conn; - - // 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()); - } - - public static void main(String[] args) throws InterruptedException,KeeperException { - String path = "/MyFirstZnode"; //Assign path to the znode - - try { - conn = new ZooKeeperConnection(); - zk = conn.connect("localhost"); - delete(path); //delete the node with the specified path - } catch(Exception e) { - System.out.println(e.getMessage()); // catches error messages - } - } -} -``` - -## 4. 资源 - -| [官网](http://zookeeper.apache.org/) | [官网文档](https://cwiki.apache.org/confluence/display/ZOOKEEPER) | [Github](https://github.com/apache/zookeeper) | diff --git "a/docs/distributed/\345\210\206\345\270\203\345\274\217\345\216\237\347\220\206.md" "b/docs/distributed/\345\210\206\345\270\203\345\274\217\345\216\237\347\220\206.md" deleted file mode 100644 index d00459d3..00000000 --- "a/docs/distributed/\345\210\206\345\270\203\345\274\217\345\216\237\347\220\206.md" +++ /dev/null @@ -1,497 +0,0 @@ ---- -title: 分布式原理 -date: 2018/07/09 -categories: -- 分布式 -tags: -- 分布式 ---- - -# 分布式原理 - - - -- [1. 分布式术语](#1-分布式术语) - - [1.1. 异常](#11-异常) - - [1.2. 超时](#12-超时) - - [1.3. 衡量指标](#13-衡量指标) -- [2. 数据分布](#2-数据分布) - - [2.1. 哈希分布](#21-哈希分布) - - [2.2. 顺序分布](#22-顺序分布) - - [2.3. 负载均衡](#23-负载均衡) -- [3. 分布式理论](#3-分布式理论) - - [3.1. CAP](#31-cap) - - [3.2. BASE](#32-base) -- [4. 分布式事务问题](#4-分布式事务问题) - - [4.1. 两阶段提交(2PC)](#41-两阶段提交2pc) - - [4.2. 补偿事务(TCC)](#42-补偿事务tcc) - - [4.3. 本地消息表(异步确保)](#43-本地消息表异步确保) - - [4.4. MQ 事务消息](#44-mq-事务消息) -- [5. 共识性问题](#5-共识性问题) - - [5.1. Paxos](#51-paxos) - - [5.2. Raft](#52-raft) -- [6. 分布式缓存问题](#6-分布式缓存问题) - - [6.1. 缓存雪崩](#61-缓存雪崩) - - [6.2. 缓存穿透](#62-缓存穿透) - - [6.3. 缓存预热](#63-缓存预热) - - [6.4. 缓存更新](#64-缓存更新) - - [6.5. 缓存降级](#65-缓存降级) -- [7. 参考资料](#7-参考资料) - - - -## 1. 分布式术语 - -### 1.1. 异常 - -#### 服务器宕机 - -内存错误、服务器停电等都会导致服务器宕机,此时节点无法正常工作,称为不可用。 - -服务器宕机会导致节点失去所有内存信息,因此需要将内存信息保存到持久化介质上。 - -#### 网络异常 - -有一种特殊的网络异常称为——**网络分区** ,即集群的所有节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。 - -#### 磁盘故障 - -磁盘故障是一种发生概率很高的异常。 - -使用冗余机制,将数据存储到多台服务器。 - -### 1.2. 超时 - -在分布式系统中,一个请求除了成功和失败两种状态,还存在着超时状态。 - -可以将服务器的操作设计为具有 **幂等性** ,即执行多次的结果与执行一次的结果相同。如果使用这种方式,当出现超时的时候,可以不断地重新请求直到成功。 - -### 1.3. 衡量指标 - -#### 性能 - -常见的性能指标有:吞吐量、响应时间。 - -其中,吞吐量指系统在某一段时间可以处理的请求总数,通常为每秒的读操作数或者写操作数;响应时间指从某个请求发出到接收到返回结果消耗的时间。 - -这两个指标往往是矛盾的,追求高吞吐的系统,往往很难做到低响应时间,解释如下: - -- 在无并发的系统中,吞吐量为响应时间的倒数,例如响应时间为 10 ms,那么吞吐量为 100 req/s,因此高吞吐也就意味着低响应时间。 - -- 但是在并发的系统中,由于一个请求在调用 I/O 资源的时候,需要进行等待。服务器端一般使用的是异步等待方式,即等待的请求被阻塞之后不需要一直占用 CPU 资源。这种方式能大大提高 CPU 资源的利用率,例如上面的例子中,单个请求在无并发的系统中响应时间为 10 ms,如果在并发的系统中,那么吞吐量将大于 100 req/s。因此为了追求高吞吐量,通常会提高并发程度。但是并发程度的增加,会导致请求的平均响应时间也增加,因为请求不能马上被处理,需要和其它请求一起进行并发处理,响应时间自然就会增高。 - -#### 可用性 - -可用性指系统在面对各种异常时可以提供正常服务的能力。可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。 - -#### 一致性 - -可以从两个角度理解一致性:从客户端的角度,读写操作是否满足某种特性;从服务器的角度,多个数据副本之间是否一致。 - -#### 可扩展性 - -指系统通过扩展集群服务器规模来提高性能的能力。理想的分布式系统需要实现“线性可扩展”,即随着集群规模的增加,系统的整体性能也会线性增加。 - -## 2. 数据分布 - -分布式存储系统的数据分布在多个节点中,常用的数据分布方式有哈希分布和顺序分布。 - -数据库的水平切分(Sharding)也是一种分布式存储方法,下面的数据分布方法同样适用于 Sharding。 - -### 2.1. 哈希分布 - -哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点,数据的主键为 key,则将该数据分配的节点序号为:hash(key)%N。 - -传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。 - -**一致性哈希** - -Distributed Hash Table(DHT):对于哈希空间 [0, 2n-1],将该哈希空间看成一个哈希环,将每个节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。 - -一致性哈希的优点是在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将数据对象 C 重新存放到节点 X 上即可,对于节点 A、B、D 都没有影响。 - -### 2.2. 顺序分布 - -哈希分布式破坏了数据的有序性,顺序分布则不会。 - -顺序分布的数据划分为多个连续的部分,按数据的 ID 或者时间分布到不同节点上。例如下图中,User 表的 ID 范围为 1 \~ 7000,使用顺序分布可以将其划分成多个子表,对应的主键范围为 1 \~ 1000,1001 \~ 2000,...,6001 \~ 7000。 - -顺序分布的优点是可以充分利用每个节点的空间,而哈希分布很难控制一个节点存储多少数据。 - -但是顺序分布需要使用一个映射表来存储数据到节点的映射,这个映射表通常使用单独的节点来存储。当数据量非常大时,映射表也随着变大,那么一个节点就可能无法存放下整个映射表。并且单个节点维护着整个映射表的开销很大,查找速度也会变慢。为了解决以上问题,引入了一个中间层,也就是 Meta 表,从而分担映射表的维护工作。 - -### 2.3. 负载均衡 - -衡量负载的因素很多,如 CPU、内存、磁盘等资源使用情况、读写请求数等。 - -分布式系统存储应当能够自动负载均衡,当某个节点的负载较高,将它的部分数据迁移到其它节点。 - -每个集群都有一个总控节点,其它节点为工作节点,由总控节点根据全局负载信息进行整体调度,工作节点定时发送心跳包(Heartbeat)将节点负载相关的信息发送给总控节点。 - -一个新上线的工作节点,由于其负载较低,如果不加控制,总控节点会将大量数据同时迁移到该节点上,造成该节点一段时间内无法工作。因此负载均衡操作需要平滑进行,新加入的节点需要较长的一段时间来达到比较均衡的状态。 - -## 3. 分布式理论 - -### 3.1. CAP - -分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。 - -
- -
- -#### 一致性 - -一致性指的是多个数据副本是否能保持一致的特性。 - -在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。 - -对系统的一个数据更新成功之后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。 - -#### 可用性 - -可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。 - -在可用性条件下,系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。 - -#### 分区容忍性 - -网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。 - -在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。 - -#### 权衡 - -在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际在是要在可用性和一致性之间做权衡。 - -可用性和一致性往往是冲突的,很难都使它们同时满足。在多个节点之间进行数据同步时, - -- 为了保证一致性(CP),就需要让所有节点下线成为不可用的状态,等待同步完成; -- 为了保证可用性(AP),在同步过程中允许读取所有节点的数据,但是数据可能不一致。 - -### 3.2. BASE - -BASE 是基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)三个短语的缩写。 - -BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 - -
- -
- -#### 基本可用 - -指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。 - -例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。 - -#### 软状态 - -指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在延时。 - -#### 最终一致性 - -最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态。 - -ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过牺牲强一致性来达到可用性,通常运用在大型分布式系统中。 - -在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。 - -## 4. 分布式事务问题 - -### 4.1. 两阶段提交(2PC) - -两阶段提交(Two-phase Commit,2PC) - -主要用于实现分布式事务,分布式事务指的是事务操作跨越多个节点,并且要求满足事务的 ACID 特性。 - -通过引入协调者(Coordinator)来调度参与者的行为,并最终决定这些参与者是否要真正执行事务。 - -#### 运行过程 - -##### 准备阶段 - -协调者询问参与者事务是否执行成功,参与者发回事务执行结果。 - -
- -
- -##### 提交阶段 - -如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。 - -
- -
-需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。 - -#### 问题 - -##### 同步阻塞 - -所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。 - -##### 单点问题 - -协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响,特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。 - -##### 数据不一致 - -在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。 - -##### 太过保守 - -任意一个节点失败就会导致整个事务失败,没有完善的容错机制。 - -#### 2PC 优缺点 - -优点:尽量保证了数据的强一致,适合对数据强一致要求很高的关键领域。(其实也不能 100%保证强一致) -缺点:实现复杂,牺牲了可用性,对性能影响较大,不适合高并发高性能场景。 - -### 4.2. 补偿事务(TCC) - -补偿事务(TCC)其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作。它分为三个阶段: - -1. Try 阶段主要是对业务系统做检测及资源预留。 -2. Confirm 阶段主要是对业务系统做确认提交,Try 阶段执行成功并开始执行 Confirm 阶段时,默认 Confirm 阶段是不会出错的。即:只要 Try 成功,Confirm 一定成功。 -3. Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。 - -举个例子,假设 Bob 要向 Smith 转账,思路大概是: - -1. 首先在 Try 阶段,要先调用远程接口把 Smith 和 Bob 的钱给冻结起来。 -2. 在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。 -3. 如果第 2 步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。 - -#### TCC 优缺点 - -- 优点:跟 2PC 比起来,实现以及流程相对简单了一些,但数据的一致性比 2PC 也要差一些。 -- 缺点:缺点还是比较明显的,在 2,3 步中都有可能失败。TCC 属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用 TCC 不太好定义及处理。 - -### 4.3. 本地消息表(异步确保) - -本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性。 - -1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。 -2. 之后将本地消息表中的消息转发到 Kafka 等消息队列(MQ)中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。 -3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。 - -
- -
- -这种方案遵循 BASE 理论,采用的是最终一致性。 - -本地消息表利用了本地事务来实现分布式事务,并且使用了消息队列来保证最终一致性。 - -#### 本地消息表优缺点 - -- 优点:一种非常经典的实现,避免了分布式事务,实现了最终一致性。 -- 缺点:消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。 - -### 4.4. MQ 事务消息 - -有一些第三方的 MQ 是支持事务消息的,比如 RocketMQ,他们支持事务消息的方式也是类似于采用的二阶段提交。但是市面上一些主流的 MQ 都是不支持事务消息的,比如 RabbitMQ 和 Kafka 都不支持。 - -以阿里的 RocketMQ 中间件为例,其思路大致为: - -1. Prepared 消息,会拿到消息的地址。 -2. 执行本地事务。 -3. 通过第一阶段拿到的地址去访问消息,并修改状态。 - -也就是说在业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了 RocketMQ 会定期扫描消息集群中的事务消息,这时候发现了 Prepared 消息,它会向消息发送者确认,所以生产方需要实现一个 check 接口,RocketMQ 会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。 - -#### MQ 事务消息优缺点 - -- 优点:实现了最终一致性,不需要依赖本地数据库事务。 -- 缺点:实现难度大,主流 MQ 不支持。 - -## 5. 共识性问题 - -### 5.1. Paxos - -用于达成共识性问题,即对多个节点产生的值,该算法能保证只选出唯一一个值。 - -主要有三类节点: - -- 提议者(Proposer):提议一个值; -- 接受者(Acceptor):对每个提议进行投票; -- 告知者(Learner):被告知投票的结果,不参与投票过程。 - -算法需要满足 safety 和 liveness 两方面的约束要求(实际上这两个基础属性是大部分分布式算法都该考虑的): - -- safety:保证决议结果是对的,无歧义的,不会出现错误情况。 - - 决议(value)只有在被 proposers 提出的 proposal 才能被最终批准; - - 在一次执行实例中,只批准(chosen)一个最终决议,意味着多数接受(accept)的结果能成为决议; -- liveness:保证决议过程能在有限时间内完成。 - - 决议总会产生,并且 learners 能获得被批准(chosen)的决议。 - -基本过程包括 proposer 提出提案,先争取大多数 acceptor 的支持,超过一半支持时,则发送结案结果给所有人进行确认。一个潜在的问题是 proposer 在此过程中出现故障,可以通过超时机制来解决。极为凑巧的情况下,每次新的一轮提案的 proposer 都恰好故障,系统则永远无法达成一致(概率很小)。 - -Paxos 能保证在超过 $1/2$ 的正常节点存在时,系统能达成共识。 - -#### 单个提案者+多接收者 - -如果系统中限定只有某个特定节点是提案者,那么一致性肯定能达成(只有一个方案,要么达成,要么失败)。提案者只要收到了来自多数接收者的投票,即可认为通过,因为系统中不存在其他的提案。 - -但一旦提案者故障,则系统无法工作。 - -#### 多个提案者+单个接收者 - -限定某个节点作为接收者。这种情况下,共识也很容易达成,接收者收到多个提案,选第一个提案作为决议,拒绝掉后续的提案即可。 - -缺陷也是容易发生单点故障,包括接收者故障或首个提案者节点故障。 - -以上两种情形其实类似主从模式,虽然不那么可靠,但因为原理简单而被广泛采用。 - -当提案者和接收者都推广到多个的情形,会出现一些挑战。 - -#### 多个提案者+多个接收者 - -既然限定单提案者或单接收者都会出现故障,那么就得允许出现多个提案者和多个接收者。问题一下子变得复杂了。 - -一种情况是同一时间片段(如一个提案周期)内只有一个提案者,这时可以退化到单提案者的情形。需要设计一种机制来保障提案者的正确产生,例如按照时间、序列、或者大家猜拳(出一个数字来比较)之类。考虑到分布式系统要处理的工作量很大,这个过程要尽量高效,满足这一条件的机制非常难设计。 - -另一种情况是允许同一时间片段内可以出现多个提案者。那同一个节点可能收到多份提案,怎么对他们进行区分呢?这个时候采用只接受第一个提案而拒绝后续提案的方法也不适用。很自然的,提案需要带上不同的序号。节点需要根据提案序号来判断接受哪个。比如接受其中序号较大(往往意味着是接受新提出的,因为旧提案者故障概率更大)的提案。 - -如何为提案分配序号呢?一种可能方案是每个节点的提案数字区间彼此隔离开,互相不冲突。为了满足递增的需求可以配合用时间戳作为前缀字段。 - -此外,提案者即便收到了多数接收者的投票,也不敢说就一定通过。因为在此过程中系统可能还有其它的提案。 - -### 5.2. Raft - -Raft 算法是 Paxos 算法的一种简化实现。 - -包括三种角色:leader、candidate 和 follower,其基本过程为: - -- **Leader 选举** - 每个 candidate 随机经过一定时间都会提出选举方案,最近阶段中得票最多者被选为 leader; -- **同步 log** - leader 会找到系统中 log 最新的记录,并强制所有的 follower 来刷新到这个记录; - -_注:此处 log 并非是指日志消息,而是各种事件的发生记录。_ - -#### 单个 Candidate 的竞选 - -有三种节点:Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段。 - -- 下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。 - -
- -
- -- 此时 A 发送投票请求给其它所有节点。 - -
- -
- -- 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。 - -
- -
- -- 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。 - -
- -
- -#### 多个 Candidate 竞选 - -- 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票,例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。 - -
- -
- -- 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低。 - -
- -
- -#### 同步日志 - -- 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。 - -
- -
- -- Leader 会把修改复制到所有 Follower。 - -
- -
- -- Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。 - -
- -
- -- 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。 - -
- -
- -## 6. 分布式缓存问题 - -### 6.1. 缓存雪崩 - -缓存雪崩是指:在高并发场景下,由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库 CPU 和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。 - -解决方案: - -- 用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。 -- 还有一个简单的方案,就是将缓存失效时间分散开,不要所有缓存时间长度都设置成 5 分钟或者 10 分钟;比如我们可以在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 - -缓存失效时产生的雪崩效应,将所有请求全部放在数据库上,这样很容易就达到数据库的瓶颈,导致服务无法正常提供。尽量避免这种场景的发生。 - -### 6.2. 缓存穿透 - -缓存穿透是指:用户查询的数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。 - -当在流量较大时,出现这样的情况,一直请求 DB,很容易导致服务挂掉。 - -解决方案: - -1. 在封装的缓存 SET 和 GET 部分增加个步骤,如果查询一个 KEY 不存在,就以这个 KEY 为前缀设定一个标识 KEY;以后再查询该 KEY 的时候,先查询标识 KEY,如果标识 KEY 存在,就返回一个协定好的非 false 或者 NULL 值,然后 APP 做相应的处理,这样缓存层就不会被穿透。当然这个验证 KEY 的失效时间不能太长。 -2. 如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,一般只有几分钟。 -3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力。 - -### 6.3. 缓存预热 - -缓存预热这个应该是一个比较常见的概念,相信很多小伙伴都应该可以很容易的理解,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据! - -解决方案: - -1. 直接写个缓存刷新页面,上线时手工操作下; -2. 数据量不大,可以在项目启动的时候自动进行加载; -3. 定时刷新缓存; - -### 6.4. 缓存更新 - -除了缓存服务器自带的缓存失效策略之外(Redis 默认的有 6 中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种: - -1. 定时去清理过期的缓存; -2. 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。 - -两者各有优劣,第一种的缺点是维护大量缓存的 key 是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。 - -### 6.5. 缓存降级 - -当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。 - -降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。 - -## 7. 参考资料 - -- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013. -- [区块链技术指南](https://www.gitbook.com/book/yeasy/blockchain_guide/details) -- [NEAT ALGORITHMS - PAXOS](http://harry.me/blog/2014/12/27/neat-algorithms-paxos/) -- [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft) -- [Paxos By Example](https://angus.nyc/2012/paxos-by-example/) -- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html) diff --git "a/docs/distributed/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\256\236\347\216\260.md" "b/docs/distributed/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\256\236\347\216\260.md" deleted file mode 100644 index 7fed708d..00000000 --- "a/docs/distributed/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\256\236\347\216\260.md" +++ /dev/null @@ -1,355 +0,0 @@ ---- -title: 分布式技术实现 -date: 2018/07/09 -categories: -- 分布式 -tags: -- 分布式 ---- - -# 分布式技术实现 - - - -- [1. 分布式事务](#1-分布式事务) -- [2. 分布式锁](#2-分布式锁) - - [2.1. 基于数据库实现分布式锁](#21-基于数据库实现分布式锁) - - [2.2. 基于 Redis 实现分布式锁](#22-基于-redis-实现分布式锁) - - [2.3. 基于 ZooKeeper 实现分布式锁](#23-基于-zookeeper-实现分布式锁) -- [3. 分布式 Session](#3-分布式-session) - - [3.1. Sticky Sessions](#31-sticky-sessions) - - [3.2. Session Replication](#32-session-replication) - - [3.3. Session Server](#33-session-server) -- [4. 分布式存储](#4-分布式存储) -- [5. 分布式缓存](#5-分布式缓存) -- [6. 分布式计算](#6-分布式计算) -- [7. 负载均衡](#7-负载均衡) - - [7.1. 算法](#71-算法) - - [7.2. 实现](#72-实现) -- [8. 资料](#8-资料) - - - -## 1. 分布式事务 - -> 参考:[分布式原理#4-分布式事务问题](分布式原理.md#4-分布式事务问题) - -## 2. 分布式锁 - -Java 原生 API 虽然有并发锁,但并没有提供分布式锁的能力,所以针对分布式场景中的锁需要解决的方案。 - -分布式锁的解决方案大致有以下几种: - -- 基于数据库实现 -- 基于缓存(redis,memcached 等)实现 -- 基于 Zookeeper 实现 - -### 2.1. 基于数据库实现分布式锁 - -#### 实现 - -##### 1. 创建表 - -```sql -CREATE TABLE `methodLock` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', - `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名', - `desc` varchar(1024) NOT NULL DEFAULT '备注信息', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成', - PRIMARY KEY (`id`), - UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法'; -``` - -##### 2. 获取锁 - -想要锁住某个方法时,执行以下 SQL: - -```sql -insert into methodLock(method_name,desc) values (‘method_name’,‘desc’) -``` - -因为我们对 `method_name` 做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。 - -成功插入则获取锁。 - -##### 3. 释放锁 - -当方法执行完毕之后,想要释放锁的话,需要执行以下 Sql: - -```sql -delete from methodLock where method_name ='method_name' -``` - -#### 问题 - -1. 这把锁强依赖数据库的可用性。如果数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。 -2. 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。 -3. 这把锁只能是非阻塞的,因为数据的 insert 操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。 -4. 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。 - -#### 解决办法 - -1. 单点问题可以用多数据库实例,同时塞 N 个表,N/2+1 个成功就任务锁定成功 -2. 写一个定时任务,隔一段时间清除一次过期的数据。 -3. 写一个 while 循环,不断的重试插入,直到成功。 -4. 在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。 - -#### 小结 - -- 优点: 直接借助数据库,容易理解。 -- 缺点: 会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。操作数据库需要一定的开销,性能问题需要考虑。 - -### 2.2. 基于 Redis 实现分布式锁 - -相比于用数据库来实现分布式锁,基于缓存实现的分布式锁的性能会更好一些。目前有很多成熟的分布式产品,包括 Redis、memcache、Tair 等。这里以 Redis 举例。 - -#### Redis 命令 - -- setnx - setnx key val:当且仅当 key 不存在时,set 一个 key 为 val 的字符串,返回 1;若 key 存在,则什么都不做,返回 0。 -- expire - expire key timeout:为 key 设置一个超时时间,单位为 second,超过这个时间锁会自动释放,避免死锁。 -- delete - delete key:删除 key - -#### 实现 - -单点实现步骤: - -1. 获取锁的使用,使用 setnx 加锁,锁的 value 值为一个随机生成的 UUID,再使用 expire 设置一个过期值。 -2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。 -3. 释放锁的时候,通过 UUID 判断是不是该锁,若是该锁,则执行 delete 进行锁释放。 - -#### 问题 - -- 单点问题。如果单机 redis 挂掉了,那么程序会跟着出错。 -- 如果转移使用 slave 节点,复制不是同步复制,会出现多个程序获取锁的情况 - -#### 小结 - -可以考虑使用 [redisson 的解决方案](https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8)。 - -### 2.3. 基于 ZooKeeper 实现分布式锁 - -#### 实现 - -这也是 ZooKeeper 客户端 curator 的分布式锁实现。 - -1. 创建一个目录 mylock; -2. 线程 A 想获取锁就在 mylock 目录下创建临时顺序节点; -3. 获取 mylock 目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; -4. 线程 B 获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; -5. 线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。 - -#### 小结 - -ZooKeeper 版本的分布式锁问题相对比较来说少。 - -- 锁的占用时间限制:redis 就有占用时间限制,而 ZooKeeper 则没有,最主要的原因是 redis 目前没有办法知道已经获取锁的客户端的状态,是已经挂了呢还是正在执行耗时较长的业务逻辑。而 ZooKeeper 通过临时节点就能清晰知道,如果临时节点存在说明还在执行业务逻辑,如果临时节点不存在说明已经执行完毕释放锁或者是挂了。由此看来 redis 如果能像 ZooKeeper 一样添加一些与客户端绑定的临时键,也是一大好事。 -- 是否单点故障:redis 本身有很多中玩法,如客户端一致性 hash,服务器端 sentinel 方案或者 cluster 方案,很难做到一种分布式锁方式能应对所有这些方案。而 ZooKeeper 只有一种玩法,多台机器的节点数据是一致的,没有 redis 的那么多的麻烦因素要考虑。 - -总体上来说 ZooKeeper 实现分布式锁更加的简单,可靠性更高。但 ZooKeeper 因为需要频繁的创建和删除节点,性能上不如 Redis 方式。 - -## 3. 分布式 Session - -在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session,就可能导致用户需要重新进行登录等操作。 - -分布式 Session 的几种实现策略: - -1. 粘性 session -2. 应用服务器间的 session 复制共享 -3. 基于 cache DB 缓存的 session 共享 - -### 3.1. Sticky Sessions - -需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。 - -缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session。 - -
- -
- -### 3.2. Session Replication - -在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点。 - -缺点:占用过多内存;同步过程占用网络带宽以及服务器处理器时间。 - -
- -
- -### 3.3. Session Server - -使用一个单独的服务器存储 Session 数据,可以存在 MySQL 数据库上,也可以存在 Redis 或者 Memcached 这种内存型数据库。 - -缺点:需要去实现存取 Session 的代码。 - -
- -
- -## 4. 分布式存储 - -通常有两种解决方案: - -1. 数据分布:就是把数据分块存在不同的服务器上(分库分表)。 -2. 数据复制:让所有的服务器都有相同的数据,提供相当的服务。 - -> 参考:[分布式原理.md#2-数据分布](分布式原理.md#2-数据分布) - -## 5. 分布式缓存 - -使用缓存的好处: - -- 提升数据读取速度 -- 提升系统扩展能力,通过扩展缓存,提升系统承载能力 -- 降低存储成本,Cache+DB 的方式可以承担原有需要多台 DB 才能承担的请求量,节省机器成本 - -根据业务场景,通常缓存有以下几种使用方式 - -- 懒汉式(读时触发):写入 DB 后, 然后把相关的数据也写入 Cache -- 饥饿式(写时触发):先查询 DB 里的数据, 然后把相关的数据写入 Cache -- 定期刷新:适合周期性的跑数据的任务,或者列表型的数据,而且不要求绝对实时性 - -缓存分类: - -- 应用内缓存:如:EHCache -- 分布式缓存:如:Memached、Redis - -> 参考:[分布式原理.md#6-分布式缓存问题](分布式原理.md#6-分布式缓存问题) - -## 6. 分布式计算 - -## 7. 负载均衡 - -### 7.1. 算法 - -#### 轮询(Round Robin) - -轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。 - -
- -
- -该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担过大的负载(下图的 Server 2)。 - -
- -
- -#### 加权轮询(Weighted Round Robbin) - -加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。 - -
- -
- -#### 最少连接(least Connections) - -由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担过大的负载。 - -
- -
- -最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。 - -
- -
- -#### 加权最少连接(Weighted Least Connection) - -在最少连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。 - -
- -
- -#### 随机算法(Random) - -把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。 - -
- -
- -#### 源地址哈希法 (IP Hash) - -源地址哈希通过对客户端 IP 哈希计算得到的一个数值,用该数值对服务器数量进行取模运算,取模结果便是目标服务器的序号。 - -- 优点:保证同一 IP 的客户端都会被 hash 到同一台服务器上。 -- 缺点:不利于集群扩展,后台服务器数量变更都会影响 hash 结果。可以采用一致性 Hash 改进。 - -
- -
- -### 7.2. 实现 - -#### HTTP 重定向 - -HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后需要再次发送请求。 - -缺点: - -- 用户访问的延迟会增加; -- 如果负载均衡器宕机,就无法访问该站点。 - -
- -
- -#### DNS 重定向 - -使用 DNS 作为负载均衡器,根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。 - -缺点: - -- DNS 查找表可能会被客户端缓存起来,那么之后的所有请求都会被重定向到同一个服务器。 - -
- -
- -#### 修改 MAC 地址 - -使用 LVS(Linux Virtual Server)这种链路层负载均衡器,根据负载情况修改请求的 MAC 地址。 - -
- -
- -#### 修改 IP 地址 - -在网络层修改请求的目的 IP 地址。 - -
- -
- -#### 代理自动配置 - -正向代理与反向代理的区别: - -- 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。 -- 反向代理:发生在服务器端,用户不知道代理的存在。 - -PAC 服务器是用来判断一个请求是否要经过代理。 - -
- -
- -## 8. 资料 - -- https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html -- https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E5%88%86%E5%B8%83%E5%BC%8F%E9%97%AE%E9%A2%98%E5%88%86%E6%9E%90.md -- https://www.jianshu.com/p/453c6e7ff81c -- https://juejin.im/post/5a20cd8bf265da43163cdd9a -- https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8 -- https://github.com/L316476844/distributed-session -- [分布式缓存架构基础](https://juejin.im/entry/57e39e320e3dd90058021bff) -- [阿里 P8 技术专家细究分布式缓存问题](https://www.toutiao.com/i6533812974807679495/?tt_from=weixin&utm_campaign=client_share&from=singlemessage×tamp=1521281305&app=news_article&utm_source=weixin&iid=28128279343&utm_medium=toutiao_android&weixin_list=1&wxshare_count=2&pbid=6517746516513195523) diff --git "a/docs/distributed/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\351\235\242\350\257\225\351\242\230.md" "b/docs/distributed/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\351\235\242\350\257\225\351\242\230.md" deleted file mode 100644 index e1771e10..00000000 --- "a/docs/distributed/\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\351\235\242\350\257\225\351\242\230.md" +++ /dev/null @@ -1,494 +0,0 @@ -# 分布式技术面试题 - - - -- [1. 分布式缓存](#1-分布式缓存) - - [1.1. Redis 有什么数据类型?分别用于什么场景?](#11-redis-有什么数据类型分别用于什么场景) - - [1.2. Redis 的主从复制是如何实现的?](#12-redis-的主从复制是如何实现的) - - [1.3. Redis 的 key 是如何寻址的?](#13-redis-的-key-是如何寻址的) - - [1.4. Redis 的集群模式是如何实现的?](#14-redis-的集群模式是如何实现的) - - [1.5. Redis 如何实现分布式锁?ZooKeeper 如何实现分布式锁?比较二者优劣?](#15-redis-如何实现分布式锁zookeeper-如何实现分布式锁比较二者优劣) - - [1.6. Redis 的持久化方式?有什么优缺点?持久化实现原理?](#16-redis-的持久化方式有什么优缺点持久化实现原理) - - [1.7. Redis 过期策略有哪些?](#17-redis-过期策略有哪些) - - [1.8. Redis 和 Memcached 有什么区别?](#18-redis-和-memcached-有什么区别) - - [1.9. 为什么单线程的 Redis 性能反而优于多线程的 Memcached?](#19-为什么单线程的-redis-性能反而优于多线程的-memcached) -- [2. 分布式消息队列(MQ)](#2-分布式消息队列mq) - - [2.1. 为什么使用 MQ?](#21-为什么使用-mq) - - [2.2. 如何保证 MQ 的高可用?](#22-如何保证-mq-的高可用) - - [2.3. MQ 有哪些常见问题?如何解决这些问题?](#23-mq-有哪些常见问题如何解决这些问题) - - [2.4. Kafka, ActiveMQ, RabbitMQ, RocketMQ 各有什么优缺点?](#24-kafka-activemq-rabbitmq-rocketmq-各有什么优缺点) -- [3. 分布式服务(RPC)](#3-分布式服务rpc) - - [3.1. Dubbo 的实现过程?](#31-dubbo-的实现过程) - - [3.2. Dubbo 负载均衡策略有哪些?](#32-dubbo-负载均衡策略有哪些) - - [3.3. Dubbo 集群容错策略 ?](#33-dubbo-集群容错策略-) - - [3.4. 动态代理策略?](#34-动态代理策略) - - [3.5. Dubbo 支持哪些序列化协议?Hessian?Hessian 的数据结构?](#35-dubbo-支持哪些序列化协议hessianhessian-的数据结构) - - [3.6. Protoco Buffer 是什么?](#36-protoco-buffer-是什么) - - [3.7. 注册中心挂了可以继续通信吗?](#37-注册中心挂了可以继续通信吗) - - [3.8. ZooKeeper 原理是什么?ZooKeeper 有什么用?](#38-zookeeper-原理是什么zookeeper-有什么用) - - [3.9. Netty 有什么用?NIO/BIO/AIO 有什么用?有什么区别?](#39-netty-有什么用niobioaio-有什么用有什么区别) - - [3.10. 为什么要进行系统拆分?拆分不用 Dubbo 可以吗?](#310-为什么要进行系统拆分拆分不用-dubbo-可以吗) - - [3.11. Dubbo 和 Thrift 有什么区别?](#311-dubbo-和-thrift-有什么区别) - - - -## 1. 分布式缓存 - -### 1.1. Redis 有什么数据类型?分别用于什么场景? - -| 数据类型 | 可以存储的值 | 操作 | -| -------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------- | -| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作
对整数和浮点数执行自增或者自减操作 | -| LIST | 列表 | 从两端压入或者弹出元素
读取单个或者多个元素
进行修剪,只保留一个范围内的元素 | -| SET | 无序集合 | 添加、获取、移除单个元素
检查一个元素是否存在于集合中
计算交集、并集、差集
从集合里面随机获取元素 | -| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对
获取所有键值对
检查某个键是否存在 | -| ZSET | 有序集合 | 添加、获取、删除元素
根据分值范围或者成员来获取元素
计算一个键的排名 | - -> [What Redis data structures look like](https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/) - -### 1.2. Redis 的主从复制是如何实现的? - -1. 从服务器连接主服务器,发送 SYNC 命令; -2. 主服务器接收到 SYNC 命名后,开始执行 BGSAVE 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令; -3. 主服务器 BGSAVE 执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令; -4. 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照; -5. 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令; -6. 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令; - -### 1.3. Redis 的 key 是如何寻址的? - -#### 背景 - -(1)redis 中的每一个数据库,都由一个 redisDb 的结构存储。其中: - -- redisDb.id 存储着 redis 数据库以整数表示的号码。 -- redisDb.dict 存储着该库所有的键值对数据。 -- redisDb.expires 保存着每一个键的过期时间。 - -(2)当 redis 服务器初始化时,会预先分配 16 个数据库(该数量可以通过配置文件配置),所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中。当我们选择数据库 select number 时,程序直接通过 redisServer.db[number] 来切换数据库。有时候当程序需要知道自己是在哪个数据库时,直接读取 redisDb.id 即可。 - -(3)redis 的字典使用哈希表作为其底层实现。dict 类型使用的两个指向哈希表的指针,其中 0 号哈希表(ht[0])主要用于存储数据库的所有键值,而 1 号哈希表主要用于程序对 0 号哈希表进行 rehash 时使用,rehash 一般是在添加新值时会触发,这里不做过多的赘述。所以 redis 中查找一个 key,其实就是对进行该 dict 结构中的 ht[0] 进行查找操作。 - -(4)既然是哈希,那么我们知道就会有哈希碰撞,那么当多个键哈希之后为同一个值怎么办呢?redis 采取链表的方式来存储多个哈希碰撞的键。也就是说,当根据 key 的哈希值找到该列表后,如果列表的长度大于 1,那么我们需要遍历该链表来找到我们所查找的 key。当然,一般情况下链表长度都为是 1,所以时间复杂度可看作 o(1)。 - -#### 寻址 key 的步骤 - -1. 当拿到一个 key 后,redis 先判断当前库的 0 号哈希表是否为空,即:if (dict->ht[0].size == 0)。如果为 true 直接返回 NULL。 -2. 判断该 0 号哈希表是否需要 rehash,因为如果在进行 rehash,那么两个表中者有可能存储该 key。如果正在进行 rehash,将调用一次\_dictRehashStep 方法,\_dictRehashStep 用于对数据库字典、以及哈希键的字典进行被动 rehash,这里不作赘述。 -3. 计算哈希表,根据当前字典与 key 进行哈希值的计算。 -4. 根据哈希值与当前字典计算哈希表的索引值。 -5. 根据索引值在哈希表中取出链表,遍历该链表找到 key 的位置。一般情况,该链表长度为 1。 -6. 当 ht[0] 查找完了之后,再进行了次 rehash 判断,如果未在 rehashing,则直接结束,否则对 ht[1]重复 345 步骤。 - -### 1.4. Redis 的集群模式是如何实现的? - -Redis Cluster 是 Redis 的分布式解决方案,在 Redis 3.0 版本正式推出的。 - -Redis Cluster 去中心化,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。 - -#### Redis Cluster 节点分配 - -Redis Cluster 特点: - -1. 所有的 redis 节点彼此互联(PING-PONG 机制),内部使用二进制协议优化传输速度和带宽。 -2. 节点的 fail 是通过集群中超过半数的节点检测失效时才生效。 -3. 客户端与 redis 节点直连,不需要中间 proxy 层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。 -4. redis-cluster 把所有的物理节点映射到[0-16383] 哈希槽 (hash slot)上(不一定是平均分配),cluster 负责维护 node<->slot<->value。 -5. Redis 集群预分好 16384 个桶,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384 的值,决定将一个 key 放到哪个桶中。 - -#### Redis Cluster 主从模式 - -Redis Cluster 为了保证数据的高可用性,加入了主从模式。 - -一个主节点对应一个或多个从节点,主节点提供数据存取,从节点则是从主节点拉取数据备份。当这个主节点挂掉后,就会有这个从节点选取一个来充当主节点,从而保证集群不会挂掉。所以,在集群建立的时候,一定要为每个主节点都添加了从节点。 - -#### Redis Sentinel - -Redis Sentinel 用于管理多个 Redis 服务器,它有三个功能: - -- **监控(Monitoring)** - Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。 -- **提醒(Notification)** - 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。 -- **自动故障迁移(Automatic failover)** - 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。 - -Redis 集群中应该有奇数个节点,所以至少有三个节点。 - -哨兵监控集群中的主服务器出现故障时,需要根据 quorum 选举出一个哨兵来执行故障转移。选举需要 majority,即大多数哨兵是运行的(2 个哨兵的 majority=2,3 个哨兵的 majority=2,5 个哨兵的 majority=3,4 个哨兵的 majority=2)。 - -假设集群仅仅部署 2 个节点 - -``` -+----+ +----+ -| M1 |---------| R1 | -| S1 | | S2 | -+----+ +----+ -``` - -如果 M1 和 S1 所在服务器宕机,则哨兵只有 1 个,无法满足 majority 来进行选举,就不能执行故障转移。 - -### 1.5. Redis 如何实现分布式锁?ZooKeeper 如何实现分布式锁?比较二者优劣? - -分布式锁的三种实现: - -- 基于数据库实现分布式锁; -- 基于缓存(Redis 等)实现分布式锁; -- 基于 Zookeeper 实现分布式锁; - -#### 数据库实现 - -#### Redis 实现 - -1. 获取锁的时候,使用 setnx 加锁,并使用 expire 命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的 value 值为一个随机生成的 UUID,通过此在释放锁的时候进行判断。 -2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。 -3. 释放锁的时候,通过 UUID 判断是不是该锁,若是该锁,则执行 delete 进行锁释放。 - -#### ZooKeeper 实现 - -1. 创建一个目录 mylock; -2. 线程 A 想获取锁就在 mylock 目录下创建临时顺序节点; -3. 获取 mylock 目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; -4. 线程 B 获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; -5. 线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。 - -#### 实现对比 - -ZooKeeper 具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。 -但 ZooKeeper 因为需要频繁的创建和删除节点,性能上不如 Redis 方式。 - -### 1.6. Redis 的持久化方式?有什么优缺点?持久化实现原理? - -#### RDB 快照(snapshot) - -将存在于某一时刻的所有数据都写入到硬盘中。 - -##### 快照的原理 - -在默认情况下,Redis 将数据库快照保存在名字为 dump.rdb 的二进制文件中。你可以对 Redis 进行设置, 让它在“N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。你也可以通过调用 SAVE 或者 BGSAVE,手动让 Redis 进行数据集保存操作。这种持久化方式被称为快照。 - -当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作: - -- Redis 创建一个子进程。 -- 子进程将数据集写入到一个临时快照文件中。 -- 当子进程完成对新快照文件的写入时,Redis 用新快照文件替换原来的快照文件,并删除旧的快照文件。 - -这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益。 - -##### 快照的优点 - -- 它保存了某个时间点的数据集,非常适用于数据集的备份。 -- 很方便传送到另一个远端数据中心或者亚马逊的 S3(可能加密),非常适用于灾难恢复。 -- 快照在保存 RDB 文件时父进程唯一需要做的就是 fork 出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他 IO 操作,所以快照持久化方式可以最大化 redis 的性能。 -- 与 AOF 相比,在恢复大的数据集的时候,DB 方式会更快一些。 - -##### 快照的缺点 - -- 如果你希望在 redis 意外停止工作(例如电源中断)的情况下丢失的数据最少的话,那么快照不适合你。 -- 快照需要经常 fork 子进程来保存数据集到硬盘上。当数据集比较大的时候,fork 的过程是非常耗时的,可能会导致 Redis 在一些毫秒级内不能响应客户端的请求。 - -#### AOF - -AOF 持久化方式记录每次对服务器执行的写操作。当服务器重启的时候会重新执行这些命令来恢复原始的数据。 - -#### AOF 的原理 - -- Redis 创建一个子进程。 -- 子进程开始将新 AOF 文件的内容写入到临时文件。 -- 对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾,这样样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。 -- 当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。 -- 搞定!现在 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。 - -#### AOF 的优点 - -- 使用默认的每秒 fsync 策略,Redis 的性能依然很好(fsync 是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,使用 AOF ,你最多丢失 1 秒的数据。 -- AOF 文件是一个只进行追加的日志文件,所以不需要写入 seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用 redis-check-aof 工具修复这些问题。 -- Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写:重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。整个重写操作是绝对安全的。 -- AOF 文件有序地保存了对数据库执行的所有写入操作,这些写入操作以 Redis 协议的格式保存。因此 AOF 文件的内容非常容易被人读懂,对文件进行分析(parse)也很轻松。 - -#### AOF 的缺点 - -- 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。 -- 根据所使用的 fsync 策略,AOF 的速度可能会慢于快照。在一般情况下,每秒 fsync 的性能依然非常高,而关闭 fsync 可以让 AOF 的速度和快照一样快,即使在高负荷之下也是如此。不过在处理巨大的写入载入时,快照可以提供更有保证的最大延迟时间(latency)。 - -### 1.7. Redis 过期策略有哪些? - -- **noeviction** - 当内存使用达到阈值的时候,所有引起申请内存的命令会报错。 -- **allkeys-lru** - 在主键空间中,优先移除最近未使用的 key。 -- **allkeys-random** - 在主键空间中,随机移除某个 key。 -- **volatile-lru** - 在设置了过期时间的键空间中,优先移除最近未使用的 key。 -- **volatile-random** - 在设置了过期时间的键空间中,随机移除某个 key。 -- **volatile-ttl** - 在设置了过期时间的键空间中,具有更早过期时间的 key 优先移除。 - -### 1.8. Redis 和 Memcached 有什么区别? - -两者都是非关系型内存键值数据库。有以下主要不同: - -**数据类型** - -- Memcached 仅支持字符串类型; -- 而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。 - -**数据持久化** - -- Memcached 不支持持久化; -- Redis 支持两种持久化策略:RDB 快照和 AOF 日志。 - -**分布式** - -- Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。 -- Redis Cluster 实现了分布式的支持。 - -**内存管理机制** - -- Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 -- 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。 - -### 1.9. 为什么单线程的 Redis 性能反而优于多线程的 Memcached? - -Redis 快速的原因: - -1. 绝大部分请求是纯粹的内存操作(非常快速) -2. 采用单线程,避免了不必要的上下文切换和竞争条件 -3. 非阻塞 IO - -内部实现采用 epoll,采用了 epoll+自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,绝不在 io 上浪费一点时间。 - -## 2. 分布式消息队列(MQ) - -### 2.1. 为什么使用 MQ? - -- 异步处理 - 相比于传统的串行、并行方式,提高了系统吞吐量。 -- 应用解耦 - 系统间通过消息通信,不用关心其他系统的处理。 -- 流量削锋 - 可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请求。 -- 日志处理 - 解决大量日志传输。 -- 消息通讯 - 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。 - -### 2.2. 如何保证 MQ 的高可用? - -#### 数据复制 - -1. 将所有 Broker 和待分配的 Partition 排序 -2. 将第 i 个 Partition 分配到第(i mod n)个 Broker 上 -3. 将第 i 个 Partition 的第 j 个 Replica 分配到第((i + j) mode n)个 Broker 上 - -#### 选举主服务器 - -### 2.3. MQ 有哪些常见问题?如何解决这些问题? - -MQ 的常见问题有: - -1. 消息的顺序问题 -2. 消息的重复问题 - -#### 消息的顺序问题 - -消息有序指的是可以按照消息的发送顺序来消费。 - -假如生产者产生了 2 条消息:M1、M2,假定 M1 发送到 S1,M2 发送到 S2,如果要保证 M1 先于 M2 被消费,怎么做? - -![image](http://upload-images.jianshu.io/upload_images/3101171-23145c8b554a0f2f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -解决方案: - -(1)保证生产者 - MQServer - 消费者是一对一对一的关系 - -![image](http://upload-images.jianshu.io/upload_images/3101171-034106d7e04c062d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -缺陷: - -- 并行度就会成为消息系统的瓶颈(吞吐量不够) -- 更多的异常处理,比如:只要消费端出现问题,就会导致整个处理流程阻塞,我们不得不花费更多的精力来解决阻塞的问题。 - -(2)通过合理的设计或者将问题分解来规避。 - -- 不关注乱序的应用实际大量存在 -- 队列无序并不意味着消息无序 - -所以从业务层面来保证消息的顺序而不仅仅是依赖于消息系统,是一种更合理的方式。 - -#### 消息的重复问题 - -造成消息重复的根本原因是:网络不可达。 - -所以解决这个问题的办法就是绕过这个问题。那么问题就变成了:如果消费端收到两条一样的消息,应该怎样处理? - -消费端处理消息的业务逻辑保持幂等性。只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样。 -保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。利用一张日志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条消息。 - -### 2.4. Kafka, ActiveMQ, RabbitMQ, RocketMQ 各有什么优缺点? - -![image](http://upload-images.jianshu.io/upload_images/3101171-c26f4a3048c38af4.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 3. 分布式服务(RPC) - -### 3.1. Dubbo 的实现过程? - -
- -
- -节点角色: - -| 节点 | 角色说明 | -| --------- | -------------------------------------- | -| Provider | 暴露服务的服务提供方 | -| Consumer | 调用远程服务的服务消费方 | -| Registry | 服务注册与发现的注册中心 | -| Monitor | 统计服务的调用次数和调用时间的监控中心 | -| Container | 服务运行容器 | - -调用关系: - -1. 务容器负责启动,加载,运行服务提供者。 -2. 服务提供者在启动时,向注册中心注册自己提供的服务。 -3. 服务消费者在启动时,向注册中心订阅自己所需的服务。 -4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 -5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 -6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 - -### 3.2. Dubbo 负载均衡策略有哪些? - -##### Random - -- 随机,按权重设置随机概率。 -- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。 - -##### RoundRobin - -- 轮循,按公约后的权重设置轮循比率。 -- 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。 - -##### LeastActive - -- 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。 -- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。 - -##### ConsistentHash - -- 一致性 Hash,相同参数的请求总是发到同一提供者。 -- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。 -- 算法参见: -- 缺省只对第一个参数 Hash,如果要修改,请配置 `` -- 缺省用 160 份虚拟节点,如果要修改,请配置 `` - -### 3.3. Dubbo 集群容错策略 ? - -![img](https://raw.githubusercontent.com/dunwu/JavaWeb/master/images/distributed/rpc/dubbo/dubbo%E9%9B%86%E7%BE%A4%E5%AE%B9%E9%94%99.jpg) - -- **Failover** - 失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。 -- **Failfast** - 快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 -- **Failsafe** - 失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 -- **Failback** - 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 -- **Forking** - 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。 -- **Broadcast** - 播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。 - -### 3.4. 动态代理策略? - -Dubbo 作为 RPC 框架,首先要完成的就是跨系统,跨网络的服务调用。消费方与提供方遵循统一的接口定义,消费方调用接口时,Dubbo 将其转换成统一格式的数据结构,通过网络传输,提供方根据规则找到接口实现,通过反射完成调用。也就是说,消费方获取的是对远程服务的一个代理(Proxy),而提供方因为要支持不同的接口实现,需要一个包装层(Wrapper)。调用的过程大概是这样: - -![img](https://oscimg.oschina.net/oscnet/bef19cd5a31b5ae13aff35a8cb4898faaf0.jpg) - -消费方的 Proxy 和提供方的 Wrapper 得以让 Dubbo 构建出复杂、统一的体系。而这种动态代理与包装也是通过基于 SPI 的插件方式实现的,它的接口就是**ProxyFactory**。 - -``` -@SPI("javassist") -public interface ProxyFactory { - - @Adaptive({Constants.PROXY_KEY}) - T getProxy(Invoker invoker) throws RpcException; - - @Adaptive({Constants.PROXY_KEY}) - Invoker getInvoker(T proxy, Class type, URL url) throws RpcException; - -} -``` - -ProxyFactory 有两种实现方式,一种是基于 JDK 的代理实现,一种是基于 javassist 的实现。ProxyFactory 接口上定义了@SPI("javassist"),默认为 javassist 的实现。 - -### 3.5. Dubbo 支持哪些序列化协议?Hessian?Hessian 的数据结构? - -1. dubbo 序列化,阿里尚不成熟的 java 序列化实现。 -2. hessian2 序列化:hessian 是一种跨语言的高效二进制的序列化方式,但这里实际不是原生的 hessian2 序列化,而是阿里修改过的 hessian lite,它是 dubbo RPC 默认启用的序列化方式。 -3. json 序列化:目前有两种实现,一种是采用的阿里的 fastjson 库,另一种是采用 dubbo 中自已实现的简单 json 库,一般情况下,json 这种文本序列化性能不如二进制序列化。 -4. java 序列化:主要是采用 JDK 自带的 java 序列化实现,性能很不理想。 -5. Kryo 和 FST:Kryo 和 FST 的性能依然普遍优于 hessian 和 dubbo 序列化。 - -Hessian 序列化与 Java 默认的序列化区别? - -Hessian 是一个轻量级的 remoting on http 工具,采用的是 Binary RPC 协议,所以它很适合于发送二进制数据,同时又具有防火墙穿透能力。 - -1. Hessian 支持跨语言串行 -2. 比 java 序列化具有更好的性能和易用性 -3. 支持的语言比较多 - -### 3.6. Protoco Buffer 是什么? - -Protocol Buffer 是 Google 出品的一种轻量 & 高效的结构化数据存储格式,性能比 Json、XML 真的强!太!多! - -Protocol Buffer 的序列化 & 反序列化简单 & 速度快的原因是: - -1. 编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等) -2. 采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成 - -Protocol Buffer 的数据压缩效果好(即序列化后的数据量体积小)的原因是: - -1. 采用了独特的编码方式,如 Varint、Zigzag 编码方式等等 -2. 采用 T - L - V 的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑 - -### 3.7. 注册中心挂了可以继续通信吗? - -可以。Dubbo 消费者在应用启动时会从注册中心拉取已注册的生产者的地址接口,并缓存在本地。每次调用时,按照本地存储的地址进行调用。 - -### 3.8. ZooKeeper 原理是什么?ZooKeeper 有什么用? - -ZooKeeper 是一个分布式应用协调系统,已经用到了许多分布式项目中,用来完成统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等工作。 - -
- -
- -1. 每个 Server 在内存中存储了一份数据; -2. Zookeeper 启动时,将从实例中选举一个 leader(Paxos 协议); -3. Leader 负责处理数据更新等操作(Zab 协议); -4. 一个更新操作成功,当且仅当大多数 Server 在内存中成功修改数据。 - -### 3.9. Netty 有什么用?NIO/BIO/AIO 有什么用?有什么区别? - -Netty 是一个“网络通讯框架”。 - -Netty 进行事件处理的流程。`Channel`是连接的通道,是 ChannelEvent 的产生者,而`ChannelPipeline`可以理解为 ChannelHandler 的集合。 - -![event driven in Netty](https://camo.githubusercontent.com/5f7331d15c79fba29474c5be6e9e86db465637c3/687474703a2f2f7374617469632e6f736368696e612e6e65742f75706c6f6164732f73706163652f323031332f303932312f3137343033325f313872625f3139303539312e706e67) - -> 参考:https://github.com/code4craft/netty-learning/blob/master/posts/ch1-overview.md - -IO 的方式通常分为几种: - -- 同步阻塞的 BIO -- 同步非阻塞的 NIO -- 异步非阻塞的 AIO - -在使用同步 I/O 的网络应用中,如果要同时处理多个客户端请求,或是在客户端要同时和多个服务器进行通讯,就必须使用多线程来处理。 - -NIO 基于 Reactor,当 socket 有流可读或可写入 socket 时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。 - -与 NIO 不同,当进行读写操作时,只须直接调用 API 的 read 或 write 方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入 read 方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将 write 方法传递的流写入完毕时,操作系统主动通知应用程序。  即可以理解为,read/write 方法都是异步的,完成后会主动调用回调函数。 - -> 参考:https://blog.csdn.net/skiof007/article/details/52873421 - -### 3.10. 为什么要进行系统拆分?拆分不用 Dubbo 可以吗? - -系统拆分从资源角度分为:应用拆分和数据库拆分。 - -从采用的先后顺序可分为:水平扩展、垂直拆分、业务拆分、水平拆分。 - -![img](http://misc.linkedkeeper.com/misc/img/blog/201804/linkedkeeper0_9c2ed2ed-6156-40f7-ad08-20af067047ca.jpg) - -是否使用服务依据实际业务场景来决定。 - -当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。 - -当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。 - -### 3.11. Dubbo 和 Thrift 有什么区别? - -- Thrift 是跨语言的 RPC 框架。 -- Dubbo 支持服务治理,而 Thrift 不支持。 diff --git "a/docs/distributed/\350\264\237\350\275\275\345\235\207\350\241\241.md" "b/docs/distributed/\350\264\237\350\275\275\345\235\207\350\241\241.md" deleted file mode 100644 index b7104807..00000000 --- "a/docs/distributed/\350\264\237\350\275\275\345\235\207\350\241\241.md" +++ /dev/null @@ -1,488 +0,0 @@ ---- -title: 负载均衡 -date: 2018/07/05 -categories: -- 分布式 -tags: -- 分布式 -- 负载均衡 ---- - -# 负载均衡 - - - -- [1. 负载均衡原理](#1-负载均衡原理) -- [2. 负载均衡分类](#2-负载均衡分类) - - [2.1. DNS 负载均衡](#21-dns-负载均衡) - - [2.2. IP 负载均衡](#22-ip-负载均衡) - - [2.3. 链路层负载均衡](#23-链路层负载均衡) - - [2.4. 混合型负载均衡](#24-混合型负载均衡) -- [3. 负载均衡算法](#3-负载均衡算法) - - [3.1. 轮询](#31-轮询) - - [3.2. 随机](#32-随机) - - [3.3. 最少连接](#33-最少连接) - - [3.4. Hash(源地址散列)](#34-hash源地址散列) - - [3.5. 加权](#35-加权) -- [4. 硬件负载均衡](#4-硬件负载均衡) -- [5. Ngnix 负载均衡](#5-ngnix-负载均衡) - - [5.1. Ngnix 特点](#51-ngnix-特点) - - [5.2. Ngnix 功能](#52-ngnix-功能) - - [5.3. Ngnix 架构](#53-ngnix-架构) - - [5.4. Ngnix 均衡策略](#54-ngnix-均衡策略) - - [5.5. Ngnix 场景](#55-ngnix-场景) -- [6. LVS 负载均衡](#6-lvs-负载均衡) - - [6.1. LVS 功能](#61-lvs-功能) - - [6.2. LVS 架构](#62-lvs-架构) - - [6.3. LVS 均衡策略](#63-lvs-均衡策略) - - [6.4. LVS 场景](#64-lvs-场景) -- [7. HaProxy 负载均衡](#7-haproxy-负载均衡) - - [7.1. HaProxy 特点](#71-haproxy-特点) - - [7.2. HaProxy 均衡策略](#72-haproxy-均衡策略) -- [8. 资料](#8-资料) - - - -## 1. 负载均衡原理 - -系统的扩展可分为纵向(垂直)扩展和横向(水平)扩展。纵向扩展,是从单机的角度通过增加硬件处理能力,比如 CPU 处理能力,内存容量,磁盘等方面,实现服务器处理能力的提升,不能满足大型分布式系统(网站),大流量,高并发,海量数据的问题。因此需要采用横向扩展的方式,通过添加机器来满足大型网站服务的处理能力。比如:一台机器不能满足,则增加两台或者多台机器,共同承担访问压力。这就是典型的集群和负载均衡架构:如下图: - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151213194550278-786046258.png) - -- 应用集群:将同一应用部署到多台机器上,组成处理集群,接收负载均衡设备分发的请求,进行处理,并返回相应数据。 -- 负载均衡设备:将用户访问的请求,根据负载均衡算法,分发到集群中的一台处理服务器。(一种把网络请求分散到一个服务器集群中的可用服务器上去的设备) - -负载均衡的作用(解决的问题): - -1. 解决并发压力,提高应用处理性能(增加吞吐量,加强网络处理能力); - -2. 提供故障转移,实现高可用; - -3. 通过添加或减少服务器数量,提供网站伸缩性(扩展性); - -4. 安全防护;(负载均衡设备上做一些过滤,黑白名单等处理) - -## 2. 负载均衡分类 - -根据实现技术不同,可分为 DNS 负载均衡,HTTP 负载均衡,IP 负载均衡,链路层负载均衡等。 - -### 2.1. DNS 负载均衡 - -最早的负载均衡技术,利用域名解析实现负载均衡,在 DNS 服务器,配置多个 A 记录,这些 A 记录对应的服务器构成集群。大型网站总是部分使用 DNS 解析,作为第一级负载均衡。如下图: - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151213194538622-1255166256.png) - -优点 - -1. 使用简单:负载均衡工作,交给 DNS 服务器处理,省掉了负载均衡服务器维护的麻烦 -2. 提高性能:可以支持基于地址的域名解析,解析成距离用户最近的服务器地址,可以加快访问速度,改善性能; - -缺点 - -1. 可用性差:DNS 解析是多级解析,新增/修改 DNS 后,解析时间较长;解析过程中,用户访问网站将失败; -2. 扩展性低:DNS 负载均衡的控制权在域名商那里,无法对其做更多的改善和扩展; -3. 维护性差:也不能反映服务器的当前运行状态;支持的算法少;不能区分服务器的差异(不能根据系统与服务的状态来判断负载) - -实践建议 - -将 DNS 作为第一级负载均衡,A 记录对应着内部负载均衡的 IP 地址,通过内部负载均衡将请求分发到真实的 Web 服务器上。一般用于互联网公司,复杂的业务系统不合适使用。如下图: - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151213194500497-164261027.png) - -### 2.2. IP 负载均衡 - -在网络层通过修改请求目标地址进行负载均衡。 - -用户请求数据包,到达负载均衡服务器后,负载均衡服务器在操作系统内核进程获取网络数据包,根据负载均衡算法得到一台真实服务器地址,然后将请求目的地址修改为,获得的真实 ip 地址,不需要经过用户进程处理。 - -真实服务器处理完成后,响应数据包回到负载均衡服务器,负载均衡服务器,再将数据包源地址修改为自身的 ip 地址,发送给用户浏览器。如下图: - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151213195925966-1272593644.png) - -IP 负载均衡,真实物理服务器返回给负载均衡服务器,存在两种方式: - -1. 负载均衡服务器在修改目的 ip 地址的同时修改源地址。将数据包源地址设为自身盘,即源地址转换(snat)。 -2. 将负载均衡服务器同时作为真实物理服务器集群的网关服务器。 - -优点:在内核进程完成数据分发,比在应用层分发性能更好; - -缺点:所有请求响应都需要经过负载均衡服务器,集群最大吞吐量受限于负载均衡服务器网卡带宽; - -### 2.3. 链路层负载均衡 - -在通信协议的数据链路层修改 mac 地址,进行负载均衡。 - -数据分发时,不修改 ip 地址,指修改目标 mac 地址,配置真实物理服务器集群所有机器虚拟 ip 和负载均衡服务器 ip 地址一致,达到不修改数据包的源地址和目标地址,进行数据分发的目的。 - -实际处理服务器 ip 和数据请求目的 ip 一致,不需要经过负载均衡服务器进行地址转换,可将响应数据包直接返回给用户浏览器,避免负载均衡服务器网卡带宽成为瓶颈。也称为直接路由模式(DR 模式)。如下图: - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151213195947762-1630980523.png) - -优点:性能好; - -缺点:配置复杂; - -实践建议:DR 模式是目前使用最广泛的一种负载均衡方式。 - -### 2.4. 混合型负载均衡 - -由于多个服务器群内硬件设备、各自的规模、提供的服务等的差异,可以考虑给每个服务器群采用最合适的负载均衡方式,然后又在这多个服务器群间再一次负载均衡或群集起来以一个整体向外界提供服务(即把这多个服务器群当做一个新的服务器群),从而达到最佳的性能。将这种方式称之为混合型负载均衡。 - -此种方式有时也用于单台均衡设备的性能不能满足大量连接请求的情况下。是目前大型互联网公司,普遍使用的方式。 - -方式一,如下图: - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151213200106747-94797427.png) - -以上模式适合有动静分离的场景,反向代理服务器(集群)可以起到缓存和动态请求分发的作用,当时静态资源缓存在代理服务器时,则直接返回到浏览器。如果动态页面则请求后面的应用负载均衡(应用集群)。 - -方式二,如下图: - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151213200117825-1452672107.png) - -以上模式,适合动态请求场景。 - -因混合模式,可以根据具体场景,灵活搭配各种方式,以上两种方式仅供参考。 - -## 3. 负载均衡算法 - -常用的负载均衡算法有:轮询、随机、最少连接、源地址散列、加权等方式。 - -### 3.1. 轮询 - -将所有请求,依次分发到每台服务器上,适合服务器硬件同相同的场景。 - -优点:服务器请求数目相同; - -缺点:服务器压力不一样,不适合服务器配置不同的情况; - -### 3.2. 随机 - -请求随机分配到各个服务器。 - -优点:使用简单; - -缺点:不适合机器配置不同的场景; - -### 3.3. 最少连接 - -将请求分配到连接数最少的服务器(目前处理请求最少的服务器)。 - -优点:根据服务器当前的请求处理情况,动态分配; - -缺点:算法实现相对复杂,需要监控服务器请求连接数; - -### 3.4. Hash(源地址散列) - -根据 IP 地址进行 Hash 计算,得到 IP 地址。 - -优点:将来自同一 IP 地址的请求,同一会话期内,转发到相同的服务器;实现会话粘滞。 - -缺点:目标服务器宕机后,会话会丢失; - -### 3.5. 加权 - -在轮询,随机,最少链接,Hash’等算法的基础上,通过加权的方式,进行负载服务器分配。 - -优点:根据权重,调节转发服务器的请求数目; - -缺点:使用相对复杂; - -## 4. 硬件负载均衡 - -采用硬件的方式实现负载均衡,一般是单独的负载均衡服务器,价格昂贵,一般土豪级公司可以考虑,业界领先的有两款,F5 和 A10。 - -使用硬件负载均衡,主要考虑一下几个方面: - -(1)功能考虑:功能全面支持各层级的负载均衡,支持全面的负载均衡算法,支持全局负载均衡; - -(2)性能考虑:一般软件负载均衡支持到 5 万级并发已经很困难了,硬件负载均衡可以支持 - -(3)稳定性:商用硬件负载均衡,经过了良好的严格的测试,从经过大规模使用,在稳定性方面高; - -(4)安全防护:硬件均衡设备除具备负载均衡功能外,还具备防火墙,防 DDOS 攻击等安全功能; - -(5)维护角度:提供良好的维护管理界面,售后服务和技术支持; - -(6)土豪公司:F5 Big Ip 价格:15w~55w 不等;A10 价格:55w-100w 不等; - -缺点 - -(1)价格昂贵; - -(2)扩展能力差; - -小结 - -(1)一般硬件的负载均衡也要做双机高可用,因此成本会比较高。 - -(2)互联网公司一般使用开源软件,因此大部分应用采用软件负载均衡;部分采用硬件负载均衡。 - -比如某互联网公司,目前是使用几台 F5 做全局负载均衡,内部使用 Nginx 等软件负载均衡。 - -## 5. Ngnix 负载均衡 - -Ngnix 是一款轻量级的 Web 服务器/反向代理服务器,工作在七层 Http 协议的负载均衡系统。具有高性能、高并发、低内存使用等特点。是一个轻量级的 Http 和反向代理服务器。Nginx 使用 epoll and kqueue 作为开发模型。能够支持高达 50,000 个并发连接数的响应。 - -操作系统:Liunx,Windows(Linux、FreeBSD、Solaris、Mac OS X、AIX 以及 Microsoft Windows) - -开发语言:C - -并发性能:官方支持每秒 5 万并发,实际国内一般到每秒 2 万并发,有优化到每秒 10 万并发的。具体性能看应用场景。 - -### 5.1. Ngnix 特点 - -1.模块化设计:良好的扩展性,可以通过模块方式进行功能扩展。 - -2.高可靠性:主控进程和 worker 是同步实现的,一个 worker 出现问题,会立刻启动另一个 worker。 - -3.内存消耗低:一万个长连接(keep-alive),仅消耗 2.5MB 内存。 - -4.支持热部署:不用停止服务器,实现更新配置文件,更换日志文件、更新服务器程序版本。 - -5.并发能力强:官方数据每秒支持 5 万并发; - -6.功能丰富:优秀的反向代理功能和灵活的负载均衡策略 - -### 5.2. Ngnix 功能 - -#### 基本功能 - -- 支持静态资源的 web 服务器。 -- http,smtp,pop3 协议的反向代理服务器、缓存、负载均衡; -- 支持 FASTCGI(fpm) -- 支持模块化,过滤器(让文本可以实现压缩,节约带宽),ssl 及图像大小调整。 -- 内置的健康检查功能 -- 基于名称和 ip 的虚拟主机 -- 定制访问日志 -- 支持平滑升级 -- 支持 KEEPALIVE -- 支持 url rewrite -- 支持路径别名 -- 支持基于 IP 和用户名的访问控制。 -- 支持传输速率限制,支持并发数限制。 - -#### 扩展功能 - -#### 性能 - -Nginx 的高并发,官方测试支持 5 万并发连接。实际生产环境能到 2-3 万并发连接数。10000 个非活跃的 HTTP keep-alive 连接仅占用约 2.5MB 内存。三万并发连接下,10 个 Nginx 进程,消耗内存 150M。淘宝 tengine 团队测试结果是“24G 内存机器上,处理并发请求可达 200 万”。 - -### 5.3. Ngnix 架构 - -#### Nginx 的基本工作模式 - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151227195943640-864372763.jpg) - -一个 master 进程,生成一个或者多个 worker 进程。但是这里 master 是使用 root 身份启动的,因为 nginx 要工作在 80 端口。而只有管理员才有权限启动小于低于 1023 的端口。master 主要是负责的作用只是启动 worker,加载配置文件,负责系统的平滑升级。其它的工作是交给 worker。那么当 worker 被启动之后,也只是负责一些 web 最简单的工作,而其他的工作都是有 worker 中调用的模块来实现的。 - -模块之间是以流水线的方式实现功能的。流水线,指的是一个用户请求,由多个模块组合各自的功能依次实现完成的。比如:第一个模块只负责分析请求首部,第二个模块只负责查找数据,第三个模块只负责压缩数据,依次完成各自工作。来实现整个工作的完成。 - -他们是如何实现热部署的呢?其实是这样的,我们前面说 master 不负责具体的工作,而是调用 worker 工作,他只是负责读取配置文件,因此当一个模块修改或者配置文件发生变化,是由 master 进行读取,因此此时不会影响到 worker 工作。在 master 进行读取配置文件之后,不会立即的把修改的配置文件告知 worker。而是让被修改的 worker 继续使用老的配置文件工作,当 worker 工作完毕之后,直接当掉这个子进程,更换新的子进程,使用新的规则。 - -#### Nginx 支持的 sendfile 机制 - -Sendfile 机制,用户将请求发给内核,内核根据用户的请求调用相应用户进程,进程在处理时需要资源。此时再把请求发给内核(进程没有直接 IO 的能力),由内核加载数据。内核查找到数据之后,会把数据复制给用户进程,由用户进程对数据进行封装,之后交给内核,内核在进行 tcp/ip 首部的封装,最后再发给客户端。这个功能用户进程只是发生了一个封装报文的过程,却要绕一大圈。因此 nginx 引入了 sendfile 机制,使得内核在接受到数据之后,不再依靠用户进程给予封装,而是自己查找自己封装,减少了一个很长一段时间的浪费,这是一个提升性能的核心点。 - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151227195957171-1801771404.jpg) - -以上内容摘自网友发布的文章,简单一句话是资源的处理,直接通过内核层进行数据传递,避免了数据传递到应用层,应用层再传递到内核层的开销。 - -目前高并发的处理,一般都采用 sendfile 模式。通过直接操作内核层数据,减少应用与内核层数据传递。 - -#### Nginx 通信模型(I/O 复用机制) - -开发模型:epoll 和 kqueue。 - -支持的事件机制:kqueue、epoll、rt signals、/dev/poll 、event ports、select 以及 poll。 - -支持的 kqueue 特性包括 EV_CLEAR、EV_DISABLE、NOTE_LOWAT、EV_EOF,可用数据的数量,错误代码. - -支持 sendfile、sendfile64 和 sendfilev;文件 AIO;DIRECTIO;支持 Accept-filters 和 TCP_DEFER_ACCEP. - -以上概念较多,大家自行百度或谷歌,知识领域是网络通信(BIO,NIO,AIO)和多线程方面的知识。 - -### 5.4. Ngnix 均衡策略 - -nginx 的负载均衡策略可以划分为两大类:内置策略和扩展策略。内置策略包含加权轮询和 ip hash,在默认情况下这两种策略会编译进 nginx 内核,只需在 nginx 配置中指明参数即可。扩展策略有很多,如 fair、通用 hash、consistent hash 等,默认不编译进 nginx 内核。由于在 nginx 版本升级中负载均衡的代码没有本质性的变化,因此下面将以 nginx1.0.15 稳定版为例,从源码角度分析各个策略。 - -#### 加权轮询(weighted round robin) - -轮询的原理很简单,首先我们介绍一下轮询的基本流程。如下是处理一次请求的流程图: - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151227201913984-412518987.jpg) - -图中有两点需要注意,第一,如果可以把加权轮询算法分为先深搜索和先广搜索,那么 nginx 采用的是先深搜索算法,即将首先将请求都分给高权重的机器,直到该机器的权值降到了比其他机器低,才开始将请求分给下一个高权重的机器;第二,当所有后端机器都 down 掉时,nginx 会立即将所有机器的标志位清成初始状态,以避免造成所有的机器都处在 timeout 的状态,从而导致整个前端被夯住。 - -#### ip hash - -ip hash 是 nginx 内置的另一个负载均衡的策略,流程和轮询很类似,只是其中的算法和具体的策略有些变化,如下图所示: - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151227201851812-352858632.jpg) - -#### fair - -fair 策略是扩展策略,默认不被编译进 nginx 内核。其原理是根据后端服务器的响应时间判断负载情况,从中选出负载最轻的机器进行分流。这种策略具有很强的自适应性,但是实际的网络环境往往不是那么简单,因此要慎用。 - -#### 通用 hash、一致性 hash - -这两种也是扩展策略,在具体的实现上有些差别,通用 hash 比较简单,可以以 nginx 内置的变量为 key 进行 hash,一致性 hash 采用了 nginx 内置的一致性 hash 环,可以支持 memcache。 - -### 5.5. Ngnix 场景 - -Ngnix 一般作为入口负载均衡或内部负载均衡,结合反向代理服务器使用。以下架构示例,仅供参考,具体使用根据场景而定。 - -#### 入口负载均衡架构 - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151227202044781-2116477406.png) - -Ngnix 服务器在用户访问的最前端。根据用户请求再转发到具体的应用服务器或二级负载均衡服务器(LVS) - -#### 内部负载均衡架构 - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151227202054421-2015542569.png) - -LVS 作为入口负载均衡,将请求转发到二级 Ngnix 服务器,Ngnix 再根据请求转发到具体的应用服务器。 - -#### Ngnix 高可用 - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151227202100921-915093452.png) - -分布式系统中,应用只部署一台服务器会存在单点故障,负载均衡同样有类似的问题。一般可采用主备或负载均衡设备集群的方式节约单点故障或高并发请求分流。 - -Ngnix 高可用,至少包含两个 Ngnix 服务器,一台主服务器,一台备服务器,之间使用 Keepalived 做健康监控和故障检测。开放 VIP 端口,通过防火墙进行外部映射。 - -DNS 解析公网的 IP 实际为 VIP。 - -## 6. LVS 负载均衡 - -LVS 是一个开源的软件,由毕业于国防科技大学的章文嵩博士于 1998 年 5 月创立,用来实现 Linux 平台下的简单负载均衡。LVS 是 Linux Virtual Server 的缩写,意思是 Linux 虚拟服务器。 - -基于 IP 层的负载均衡调度技术,它在操作系统核心层上,将来自 IP 层的 TCP/UDP 请求均衡地转移到不同的 服务器,从而将一组服务器构成一个高性能、高可用的虚拟服务器。 - -操作系统:Liunx - -开发语言:C - -并发性能:默认 4096,可以修改但需要重新编译。 - -### 6.1. LVS 功能 - -LVS 的主要功能是实现 IP 层(网络层)负载均衡,有 NAT,TUN,DR 三种请求转发模式。 - -#### LVS/NAT 方式的负载均衡集群 - -NAT 是指 Network Address Translation,它的转发流程是:Director 机器收到外界请求,改写数据包的目标地址,按相应的调度算法将其发送到相应 Real Server 上,Real Server 处理完该请求后,将结果数据包返回到其默认网关,即 Director 机器上,Director 机器再改写数据包的源地址,最后将其返回给外界。这样就完成一次负载调度。 - -构架一个最简单的 LVS/NAT 方式的负载均衡集群 Real Server 可以是任何的操作系统,而且无需做任何特殊的设定,惟一要做的就是将其默认网关指向 Director 机器。Real Server 可以使用局域网的内部 IP(192.168.0.0/24)。Director 要有两块网卡,一块网卡绑定一个外部 IP 地址 (10.0.0.1),另一块网卡绑定局域网的内部 IP(192.168.0.254),作为 Real Server 的默认网关。 - -LVS/NAT 方式实现起来最为简单,而且 Real Server 使用的是内部 IP,可以节省 Real IP 的开销。但因为执行 NAT 需要重写流经 Director 的数据包,在速度上有一定延迟; - -当用户的请求非常短,而服务器的回应非常大的情况下,会对 Director 形成很大压力,成为新的瓶颈,从而使整个系统的性能受到限制。 - -#### LVS/TUN 方式的负载均衡集群 - -TUN 是指 IP Tunneling,它的转发流程是:Director 机器收到外界请求,按相应的调度算法,通过 IP 隧道发送到相应 Real Server,Real Server 处理完该请求后,将结果数据包直接返回给客户。至此完成一次负载调度。 - -最简单的 LVS/TUN 方式的负载均衡集群架构使用 IP Tunneling 技术,在 Director 机器和 Real Server 机器之间架设一个 IP Tunnel,通过 IP Tunnel 将负载分配到 Real Server 机器上。Director 和 Real Server 之间的关系比较松散,可以是在同一个网络中,也可以是在不同的网络中,只要两者能够通过 IP Tunnel 相连就行。收到负载分配的 Real Server 机器处理完后会直接将反馈数据送回给客户,而不必通过 Director 机器。实际应用中,服务器必须拥有正式的 IP 地址用于与客户机直接通信,并且所有服务器必须支持 IP 隧道协议。 - -该方式中 Director 将客户请求分配到不同的 Real Server,Real Server 处理请求后直接回应给用户,这样 Director 就只处理客户机与服务器的一半连接,极大地提高了 Director 的调度处理能力,使集群系统能容纳更多的节点数。另外 TUN 方式中的 Real Server 可以在任何 LAN 或 WAN 上运行,这样可以构筑跨地域的集群,其应对灾难的能力也更强,但是服务器需要为 IP 封装付出一定的资源开销,而且后端的 Real Server 必须是支持 IP Tunneling 的操作系统。 - -#### LVS/TUN 方式的负载均衡集群 - -DR 是指 Direct Routing,它的转发流程是:Director 机器收到外界请求,按相应的调度算法将其直接发送到相应 Real Server,Real Server 处理完该请求后,将结果数据包直接返回给客户,完成一次负载调度。 - -构架一个最简单的 LVS/DR 方式的负载均衡集群 Real Server 和 Director 都在同一个物理网段中,Director 的网卡 IP 是 192.168.0.253,再绑定另一个 IP: 192.168.0.254 作为对外界的 virtual IP,外界客户通过该 IP 来访问整个集群系统。Real Server 在 lo 上绑定 IP:192.168.0.254,同时加入相应的路由。 - -LVS/DR 方式与前面的 LVS/TUN 方式有些类似,前台的 Director 机器也是只需要接收和调度外界的请求,而不需要负责返回这些请求的反馈结果,所以能够负载更多的 Real Server,提高 Director 的调度处理能力,使集群系统容纳更多的 Real Server。但 LVS/DR 需要改写请求报文的 MAC 地址,所以所有服务器必须在同一物理网段内。 - -### 6.2. LVS 架构 - -LVS 架设的服务器集群系统有三个部分组成:最前端的负载均衡层(Loader Balancer),中间的服务器群组层,用 Server Array 表示,最底层的数据共享存储层,用 Shared Storage 表示。在用户看来所有的应用都是透明的,用户只是在使用一个虚拟服务器提供的高性能服务。 - -LVS 的体系架构如图: - -![img](https://images2015.cnblogs.com/blog/820332/201512/820332-20151227220009109-1768809526.png) - -LVS 的各个层次的详细介绍: - -Load Balancer 层:位于整个集群系统的最前端,有一台或者多台负载调度器(Director Server)组成,LVS 模块就安装在 Director Server 上,而 Director 的主要作用类似于一个路由器,它含有完成 LVS 功能所设定的路由表,通过这些路由表把用户的请求分发给 Server Array 层的应用服务器(Real Server)上。同时,在 Director Server 上还要安装对 Real Server 服务的监控模块 Ldirectord,此模块用于监测各个 Real Server 服务的健康状况。在 Real Server 不可用时把它从 LVS 路由表中剔除,恢复时重新加入。 - -Server Array 层:由一组实际运行应用服务的机器组成,Real Server 可以是 WEB 服务器、MAIL 服务器、FTP 服务器、DNS 服务器、视频服务器中的一个或者多个,每个 Real Server 之间通过高速的 LAN 或分布在各地的 WAN 相连接。在实际的应用中,Director Server 也可以同时兼任 Real Server 的角色。 - -Shared Storage 层:是为所有 Real Server 提供共享存储空间和内容一致性的存储区域,在物理上,一般有磁盘阵列设备组成,为了提供内容的一致性,一般可以通过 NFS 网络文件系统共享数 据,但是 NFS 在繁忙的业务系统中,性能并不是很好,此时可以采用集群文件系统,例如 Red hat 的 GFS 文件系统,oracle 提供的 OCFS2 文件系统等。 - -从整个 LVS 结构可以看出,Director Server 是整个 LVS 的核心,目前,用于 Director Server 的操作系统只能是 Linux 和 FreeBSD,linux2.6 内核不用任何设置就可以支持 LVS 功能,而 FreeBSD 作为 Director Server 的应用还不是很多,性能也不是很好。对于 Real Server,几乎可以是所有的系统平台,Linux、windows、Solaris、AIX、BSD 系列都能很好的支持。 - -### 6.3. LVS 均衡策略 - -LVS 默认支持八种负载均衡策略,简述如下: - -#### 轮询调度(Round Robin) - -调度器通过“轮询”调度算法将外部请求按顺序轮流分配到集群中的真实服务器上,它均等地对待每一台服务器,而不管服务器上实际的连接数和系统负载。 - -#### 加权轮询(Weighted Round Robin) - -调度器通过“加权轮询”调度算法根据真实服务器的不同处理能力来调度访问请求。这样可以保证处理能力强的服务器能处理更多的访问流量。调度器可以自动问询真实服务器的负载情况,并动态地调整其权值。 - -#### 最少链接(Least Connections) - -调度器通过“最少连接”调度算法动态地将网络请求调度到已建立的链接数最少的服务器上。如果集群系统的真实服务器具有相近的系统性能,采用“最小连接”调度算法可以较好地均衡负载。 - -#### 加权最少链接(Weighted Least Connections) - -在集群系统中的服务器性能差异较大的情况下,调度器采用“加权最少链接”调度算法优化负载均衡性能,具有较高权值的服务器将承受较大比例的活动连接负载。调度器可以自动问询真实服务器的负载情况,并动态地调整其权值。 - -#### 基于局部性的最少链接(Locality-Based Least Connections) - -“基于局部性的最少链接”调度算法是针对目标 IP 地址的负载均衡,目前主要用于 Cache 集群系统。该算法根据请求的目标 IP 地址找出该目标 IP 地址最近使用的服务器,若该服务器是可用的且没有超载,将请求发送到该服务器;若服务器不存在,或者该服务器超载且有服务器处于一半的工作负载,则用“最少链接” 的原则选出一个可用的服务器,将请求发送到该服务器。 - -#### 带复制的基于局部性最少链接(Locality-Based Least Connections with Replication) - -“带复制的基于局部性最少链接”调度算法也是针对目标 IP 地址的负载均衡,目前主要用于 Cache 集群系统。它与 LBLC 算法的不同之处是它要维护从一个目标 IP 地址到一组服务器的映射,而 LBLC 算法维护从一个目标 IP 地址到一台服务器的映射。该算法根据请求的目标 IP 地址找出该目标 IP 地址对应的服务器组,按“最小连接”原则从服务器组中选出一台服务器,若服务器没有超载,将请求发送到该服务器;若服务器超载,则按“最小连接”原则从这个集群中选出一台服务器,将该服务器加入到服务器组中,将请求发送到该服务器。同时,当该服务器组有一段时间没有被修改,将最忙的服务器从服务器组中删除,以降低复制的程度。 - -#### 目标地址散列(Destination Hashing) - -“目标地址散列”调度算法根据请求的目标 IP 地址,作为散列键(Hash Key)从静态分配的散列表找出对应的服务器,若该服务器是可用的且未超载,将请求发送到该服务器,否则返回空。 - -#### 源地址散列(Source Hashing) - -“源地址散列”调度算法根据请求的源 IP 地址,作为散列键(Hash Key)从静态分配的散列表找出对应的服务器,若该服务器是可用的且未超载,将请求发送到该服务器,否则返回空。 - -除具备以上负载均衡算法外,还可以自定义均衡策略。 - -### 6.4. LVS 场景 - -一般作为入口负载均衡或内部负载均衡,结合反向代理服务器使用。相关架构可参考 Ngnix 场景架构。 - -## 7. HaProxy 负载均衡 - -HAProxy 也是使用较多的一款负载均衡软件。HAProxy 提供高可用性、负载均衡以及基于 TCP 和 HTTP 应用的代理,支持虚拟主机,是免费、快速并且可靠的一种解决方案。特别适用于那些负载特大的 web 站点。运行模式使得它可以很简单安全的整合到当前的架构中,同时可以保护你的 web 服务器不被暴露到网络上。 - -### 7.1. HaProxy 特点 - -- 支持两种代理模式:TCP(四层)和 HTTP(七层),支持虚拟主机; -- 配置简单,支持 url 检测后端服务器状态; -- 做负载均衡软件使用,在高并发情况下,处理速度高于 nginx; -- TCP 层多用于 Mysql 从(读)服务器负载均衡。 (对 Mysql 进行负载均衡,对后端的 DB 节点进行检测和负载均衡) -- 能够补充 Nginx 的一些缺点比如 Session 的保持,Cookie 引导等工作 - -### 7.2. HaProxy 均衡策略 - -支持四种常用算法: - -1.roundrobin:轮询,轮流分配到后端服务器; - -2.static-rr:根据后端服务器性能分配; - -3.leastconn:最小连接者优先处理; - -4.source:根据请求源 IP,与 Nginx 的 IP_Hash 类似。 - -## 8. 资料 - -- [大型网站架构系列:负载均衡详解(1)](https://www.cnblogs.com/itfly8/p/5043435.html) -- [大型网站架构系列:负载均衡详解(2)](https://www.cnblogs.com/itfly8/p/5043452.html) -- [大型网站架构系列:负载均衡详解(3)](https://www.cnblogs.com/itfly8/p/5080743.html) -- [大型网站架构系列:负载均衡详解(4)](https://www.cnblogs.com/itfly8/p/5080988.html) diff --git a/docs/framework/README.md b/docs/framework/README.md new file mode 100644 index 00000000..7c23dcfe --- /dev/null +++ b/docs/framework/README.md @@ -0,0 +1,27 @@ +# Java 主流框架 + +## 📖 内容 + +- [Spring](https://dunwu.github.io/spring-tutorial/) 📚 +- [Spring Boot](https://dunwu.github.io/spring-boot-tutorial/) 📚 +- [Spring Cloud](https://github.com/dunwu/spring-cloud-tutorial) 📚 +- [MyBatis](mybatis) + - [Mybatis 应用指南](mybatis/Mybatis应用指南.md) + - [Mybatis 原理](mybatis/Mybatis原理.md) +- [Netty](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 通用分页插件 + +## 🚪 传送 + +◾ 🏠 [JAVATECH 首页](https://github.com/dunwu/javatech) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ diff --git "a/docs/framework/mybatis/Mybatis\345\216\237\347\220\206.md" "b/docs/framework/mybatis/Mybatis\345\216\237\347\220\206.md" new file mode 100644 index 00000000..89ced559 --- /dev/null +++ "b/docs/framework/mybatis/Mybatis\345\216\237\347\220\206.md" @@ -0,0 +1,877 @@ +# Mybatis 原理 + +> Mybatis 的前身就是 iBatis ,是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。本文以一个 Mybatis 完整示例为切入点,结合 Mybatis 底层源码分析,图文并茂的讲解 Mybatis 的核心工作机制。 + + + +- [1. Mybatis 完整示例](#1-mybatis-完整示例) + - [1.1. 数据库准备](#11-数据库准备) + - [1.2. 添加 Mybatis](#12-添加-mybatis) + - [1.3. Mybatis 配置](#13-mybatis-配置) + - [1.4. Mapper](#14-mapper) + - [1.5. 测试程序](#15-测试程序) +- [2. Mybatis 生命周期](#2-mybatis-生命周期) + - [2.1. SqlSessionFactoryBuilder](#21-sqlsessionfactorybuilder) + - [2.2. SqlSessionFactory](#22-sqlsessionfactory) + - [2.3. SqlSession](#23-sqlsession) + - [2.4. 映射器](#24-映射器) +- [3. Mybatis 的架构](#3-mybatis-的架构) + - [3.1. 配置层](#31-配置层) + - [3.2. 接口层](#32-接口层) + - [3.3. 数据处理层](#33-数据处理层) + - [3.4. 框架支撑层](#34-框架支撑层) +- [4. SqlSession 内部工作机制](#4-sqlsession-内部工作机制) + - [4.1. SqlSession 子组件](#41-sqlsession-子组件) + - [4.2. SqlSession 和 Mapper](#42-sqlsession-和-mapper) + - [4.3. SqlSession 和 Executor](#43-sqlsession-和-executor) + - [4.4. Executor 工作流程](#44-executor-工作流程) + - [4.5. StatementHandler 工作流程](#45-statementhandler-工作流程) + - [4.6. ParameterHandler 工作流程](#46-parameterhandler-工作流程) + - [4.7. ResultSetHandler 工作流程](#47-resultsethandler-工作流程) +- [5. 参考资料](#5-参考资料) + + + +## 1. 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) + +### 1.1. 数据库准备 + +在本示例中,需要针对一张用户表进行 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'); +``` + +### 1.2. 添加 Mybatis + +如果使用 Maven 来构建项目,则需将下面的依赖代码置于 pom.xml 文件中: + +```xml + + org.Mybatis + Mybatis + x.x.x + +``` + +### 1.3. Mybatis 配置 + +XML 配置文件中包含了对 Mybatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。 + +本示例中只是给出最简化的配置。 + +【示例】Mybatis-config.xml 文件 + +```xml + + + + + + + + + + + + + + + + + + +``` + +> 说明:上面的配置文件中仅仅指定了数据源连接方式和 User 表的映射配置文件。 + +### 1.4. 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 结合起来,更加方便、易于理解。 + +### 1.5. 测试程序 + +【示例】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 语句并获取结果。 + +## 2. Mybatis 生命周期 + +![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210510113446.png) + +### 2.1. SqlSessionFactoryBuilder + +#### SqlSessionFactoryBuilder 的职责 + +**`SqlSessionFactoryBuilder` 负责创建 `SqlSessionFactory` 实例**。`SqlSessionFactoryBuilder` 可以从 XML 配置文件或一个预先定制的 `Configuration` 的实例构建出 `SqlSessionFactory` 的实例。 + +`Configuration` 类包含了对一个 `SqlSessionFactory` 实例你可能关心的所有内容。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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 解析资源可以被释放给更重要的事情。 + +### 2.2. SqlSessionFactory + +#### SqlSessionFactory 职责 + +**`SqlSessionFactory` 负责创建 `SqlSession` 实例。** + +![](https://raw.githubusercontent.com/dunwu/images/dev/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` 应该以单例形式在应用的运行期间一直存在。 + +### 2.3. SqlSession + +#### SqlSession 职责 + +**Mybatis 的主要 Java 接口就是 `SqlSession`。它包含了所有执行语句,获取映射器和管理事务等方法。** + +> 详细内容可以参考:「 [Mybatis 官方文档之 SqlSessions](http://www.Mybatis.org/Mybatis-3/zh/java-api.html#sqlSessions) 」 。 + +SqlSession 类的方法可以按照下图进行大致分类: + +![](https://raw.githubusercontent.com/dunwu/images/dev/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()) { + // 你的应用逻辑代码 +} +``` + +### 2.4. 映射器 + +#### 映射器职责 + +映射器是一些由用户创建的、绑定 SQL 语句的接口。 + +`SqlSession` 中的 `insert`、`update`、`delete` 和 `select` 方法都很强大,但也有些繁琐。更通用的方式是使用映射器类来执行映射语句。**一个映射器类就是一个仅需声明与 `SqlSession` 方法相匹配的方法的接口类**。 + +Mybatis 将配置文件中的每一个 `` 节点抽象为一个 `Mapper` 接口,而这个接口中声明的方法和跟 `` 节点中的 `` 节点相对应,即 `` 节点的 id 值为 Mapper 接口中的方法名称,`parameterType` 值表示 Mapper 对应方法的入参类型,而 `resultMap` 值则对应了 Mapper 接口表示的返回值类型或者返回结果集的元素类型。 + +Mybatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个 Mapper 实例;Mybatis 会根据这个方法的方法名和参数类型,确定 Statement Id,然后和 SqlSession 进行映射,底层还是通过 SqlSession 完成和数据库的交互。 + +下面的示例展示了一些方法签名以及它们是如何映射到 `SqlSession` 上的。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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 支持的注解清单,以及基本用法。 + +## 3. 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 对象之中。 + +这些组件的架构层次如下: + +![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210512114852.png) + +### 3.1. 配置层 + +配置层决定了 Mybatis 的工作方式。 + +Mybatis 提供了两种配置方式: + +- 基于 XML 配置文件的方式 +- 基于 Java API 的方式 + +`SqlSessionFactoryBuilder` 会根据配置创建 `SqlSessionFactory` ; + +`SqlSessionFactory` 负责创建 `SqlSessions` 。 + +### 3.2. 接口层 + +接口层负责和数据库交互的方式。 + +Mybatis 和数据库的交互有两种方式: + +- **使用 SqlSession**:SqlSession 封装了所有执行语句,获取映射器和管理事务的方法。 + - 用户只需要传入 Statement Id 和查询参数给 SqlSession 对象,就可以很方便的和数据库进行交互。 + - 这种方式的缺点是不符合面向对象编程的范式。 +- **使用 Mapper 接口**:Mybatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个 Mapper 实例;Mybatis 会根据这个方法的方法名和参数类型,确定 Statement Id,然后和 SqlSession 进行映射,底层还是通过 SqlSession 完成和数据库的交互。 + +### 3.3. 数据处理层 + +数据处理层可以说是 Mybatis 的核心,从大的方面上讲,它要完成两个功能: + +- 根据传参 `Statement` 和参数构建动态 SQL 语句 + - 动态语句生成可以说是 Mybatis 框架非常优雅的一个设计,Mybatis 通过传入的参数值,**使用 Ognl 来动态地构造 SQL 语句**,使得 Mybatis 有很强的灵活性和扩展性。 + - 参数映射指的是对于 java 数据类型和 jdbc 数据类型之间的转换:这里有包括两个过程:查询阶段,我们要将 java 类型的数据,转换成 jdbc 类型的数据,通过 `preparedStatement.setXXX()` 来设值;另一个就是对 resultset 查询结果集的 jdbcType 数据转换成 java 数据类型。 +- 执行 SQL 语句以及处理响应结果集 ResultSet + - 动态 SQL 语句生成之后,Mybatis 将执行 SQL 语句,并将可能返回的结果集转换成 `List` 列表。 + - Mybatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询。 + +### 3.4. 框架支撑层 + +- **事务管理机制** - 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` 返回的查询结果放置到缓存中,然后在返回给用户。 + - 二级缓存的生命周期是应用级别的。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210512185709.png) + +## 4. SqlSession 内部工作机制 + +从前文,我们已经了解了,Mybatis 封装了对数据库的访问,把对数据库的会话和事务控制放到了 SqlSession 对象中。那么具体是如何工作的呢?接下来,我们通过源码解读来进行分析。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210512173437.png) + +`SqlSession` 对于 insert、update、delete、select 的内部处理机制基本上大同小异。所以,接下来,我会以一次完整的 select 查询流程为例讲解 `SqlSession` 内部的工作机制。相信读者如果理解了 select 的处理流程,对于其他 CRUD 操作也能做到一通百通。 + +### 4.1. SqlSession 子组件 + +前面的内容已经介绍了:SqlSession 是 Mybatis 的顶层接口,它提供了所有执行语句,获取映射器和管理事务等方法。 + +实际上,SqlSession 是通过聚合多个子组件,让每个子组件负责各自功能的方式,实现了任务的下发。 + +在了解各个子组件工作机制前,先让我们简单认识一下 SqlSession 的核心子组件。 + +#### Executor + +Executor 即执行器,它负责生成动态 SQL 以及管理缓存。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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` 的家族成员: + +![](https://raw.githubusercontent.com/dunwu/images/dev/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 类型进行相互转换。 + +### 4.2. 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 相互映射起来。 + +### 4.3. 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 截图: + +![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210511150650.png) + +> 小结: +> +> 通过 "SqlSession 和 Mapper" 以及 "SqlSession 和 Executor" 这两节,我们已经知道: +> +> SqlSession 的职能是:根据 Statement ID, 在 `Configuration` 中获取到对应的 `MappedStatement` 对象,然后调用 `Executor` 来执行具体的操作。 + +### 4.4. 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 语句字符串会包含 若干个? 占位符,我们其后再对占位符进行设值。 + +### 4.5. 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); +} +``` + +### 4.6. 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); + } + } + } + } + } +``` + +### 4.7. 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); +} +``` + +## 5. 参考资料 + +- **官方** + - [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/framework/mybatis/Mybatis\345\272\224\347\224\250\346\214\207\345\215\227.md" "b/docs/framework/mybatis/Mybatis\345\272\224\347\224\250\346\214\207\345\215\227.md" new file mode 100644 index 00000000..420ba995 --- /dev/null +++ "b/docs/framework/mybatis/Mybatis\345\272\224\347\224\250\346\214\207\345\215\227.md" @@ -0,0 +1,400 @@ +# MyBatis 应用指南 + +> MyBatis 的前身就是 iBatis ,是一个作用在数据持久层的对象关系映射(Object Relational Mapping,简称 ORM)框架。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200716162305.png) + + + +- [1. Mybatis 简介](#1-mybatis-简介) + - [1.1. 什么是 MyBatis](#11-什么是-mybatis) + - [1.2. MyBatis vs. Hibernate](#12-mybatis-vs-hibernate) +- [2. 快速入门](#2-快速入门) + - [2.1. 数据库准备](#21-数据库准备) + - [2.2. 添加 Mybatis](#22-添加-mybatis) + - [2.3. Mybatis 配置](#23-mybatis-配置) + - [2.4. Mapper](#24-mapper) + - [2.5. 测试程序](#25-测试程序) +- [3. Mybatis xml 配置](#3-mybatis-xml-配置) +- [4. Mybatis xml 映射器](#4-mybatis-xml-映射器) +- [5. Mybatis 扩展](#5-mybatis-扩展) + - [5.1. Mybatis 类型处理器](#51-mybatis-类型处理器) + - [5.2. Mybatis 插件](#52-mybatis-插件) +- [6. 参考资料](#6-参考资料) + + + +## 1. Mybatis 简介 + +![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210510164925.png) + +### 1.1. 什么是 MyBatis + +MyBatis 是一款持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。 + +### 1.2. 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 本身提供的缓存机制不佳。 + +## 2. 快速入门 + +> 这里,我将以一个入门级的示例来演示 Mybatis 是如何工作的。 +> +> 注:本文后面章节中的原理、源码部分也将基于这个示例来进行讲解。 + +### 2.1. 数据库准备 + +在本示例中,需要针对一张用户表进行 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'); +``` + +### 2.2. 添加 Mybatis + +如果使用 Maven 来构建项目,则需将下面的依赖代码置于 pom.xml 文件中: + +```xml + + org.mybatis + mybatis + x.x.x + +``` + +### 2.3. Mybatis 配置 + +XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。 + +本示例中只是给出最简化的配置。 + +【示例】mybatis-config.xml 文件 + +```xml + + + + + + + + + + + + + + + + + + +``` + +> 说明:上面的配置文件中仅仅指定了数据源连接方式和 User 表的映射配置文件。 + +### 2.4. 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 结合起来,更加方便、易于理解。 + +### 2.5. 测试程序 + +【示例】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 语句并获取结果。 + +## 3. 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) + +## 4. 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` – 映射查询语句 + +## 5. Mybatis 扩展 + +### 5.1. 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 直到语句被执行时才清楚数据类型。 + +### 5.2. 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 是负责执行底层映射语句的内部对象。 + +## 6. 参考资料 + +- **官方** + - [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/framework/mybatis/README.md b/docs/framework/mybatis/README.md new file mode 100644 index 00000000..7830fca9 --- /dev/null +++ b/docs/framework/mybatis/README.md @@ -0,0 +1,34 @@ +# Mybatis + +> Mybatis 的前身就是 iBatis ,是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。本文以一个 Mybatis 完整示例为切入点,结合 Mybatis 底层源码分析,图文并茂的讲解 Mybatis 的核心工作机制。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210522101005.png) + +## 📖 内容 + +### [Mybatis 应用指南](Mybatis应用指南.md) + +### [Mybatis 原理](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) + +## 🚪 传送 + +◾ 🏠 [JAVATECH 首页](https://github.com/dunwu/javatech) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ diff --git a/docs/framework/netty.md b/docs/framework/netty.md new file mode 100644 index 00000000..d4fb6fae --- /dev/null +++ b/docs/framework/netty.md @@ -0,0 +1,131 @@ +# 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) diff --git a/docs/javaee/README.md b/docs/javaee/README.md deleted file mode 100644 index 73f369c9..00000000 --- a/docs/javaee/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# JavaEE - -> [JavaEE](http://www.oracle.com/technetwork/java/javaee/overview/index.html) 即 Java 的企业级应用程序版本。这个版本以前称为 J2EE。能够帮助我们开发和部署可移植、健壮、可伸缩且安全的服务器端 Java 应用程序。 -> -> JavaEE 是在 JavaSE 的基础上构建的,它提供 Web 服务、组件模型、管理和通信 API,可以用来实现企业级的 SOA 和 Web 3.0 应用程序。 - diff --git a/docs/javaee/jsp/jsp-action.md b/docs/javaee/jsp/jsp-action.md deleted file mode 100644 index 5c31f695..00000000 --- a/docs/javaee/jsp/jsp-action.md +++ /dev/null @@ -1,400 +0,0 @@ ---- -title: JSP 行为 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- jsp ---- - -JSP 行为是一组 JSP 内置的标签,只需要书写很少的标记代码就能使用 JSP 提供的丰富功能。JSP 行为是对常用的 JSP 功能的抽象与封装,包括两种,自定义 JSP 行为与标准 JSP 行为。 - -与 JSP 指令元素不同的是,JSP 动作元素在请求处理阶段起作用。JSP 动作元素是用 XML 语法写成的。 - -利用 JSP 动作可以动态地插入文件、重用 JavaBean 组件、把用户重定向到另外的页面、为 Java 插件生成 HTML 代码。 - -动作元素只有一种语法,它符合 XML 标准: - -``` - -``` - -动作元素基本上都是预定义的函数,JSP 规范定义了一系列的标准动作,它用 JSP 作为前缀,可用的标准动作元素如下: - -| 语法 | 描述 | -| --------------- | ----------------------------------------------------- | -| jsp:include | 在页面被请求的时候引入一个文件。 | -| jsp:useBean | 寻找或者实例化一个 JavaBean。 | -| jsp:setProperty | 设置 JavaBean 的属性。 | -| jsp:getProperty | 输出某个 JavaBean 的属性。 | -| jsp:forward | 把请求转到一个新的页面。 | -| jsp:plugin | 根据浏览器类型为 Java 插件生成 OBJECT 或 EMBED 标记。 | -| jsp:element | 定义动态 XML 元素 | -| jsp:attribute | 设置动态定义的 XML 元素属性。 | -| jsp:body | 设置动态定义的 XML 元素内容。 | -| jsp:text | 在 JSP 页面和文档中使用写入文本的模板 | - -## 常见的属性 - -所有的动作要素都有两个属性:id 属性和 scope 属性。 - -- **id 属性:**id 属性是动作元素的唯一标识,可以在 JSP 页面中引用。动作元素创建的 id 值可以通过 PageContext 来调用。 -- **scope 属性:**该属性用于识别动作元素的生命周期。 id 属性和 scope 属性有直接关系,scope 属性定义了相关联 id 对象的寿命。 scope 属性有四个可能的值: (a) page, (b)request, (c)session, 和 (d) application。 - -## - -``用来包含静态和动态的文件。该动作把指定文件插入正在生成的页面。 - -如果被包含的文件为 JSP 程序,则会先执行 JSP 程序,再将执行结果包含进来。 - -语法格式如下: - -``` - -``` - -前面已经介绍过 include 指令,它是在 JSP 文件被转换成 Servlet 的时候引入文件,而这里的 jsp:include 动作不同,插入文件的时间是在页面被请求的时候。 - -以下是 include 动作相关的属性列表。 - -| 属性 | 描述 | -| ----- | ------------------------------------------ | -| page | 包含在页面中的相对 URL 地址。 | -| flush | 布尔属性,定义在包含资源前是否刷新缓存区。 | - -**例** - -以下我们定义了两个文件  **date.jsp**  和  **main.jsp**,代码如下所示: - -date.jsp 文件代码: - -```c++ -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -

- 今天的日期是: <%= (new java.util.Date())%> -

-``` - -main.jsp 文件代码: - -```jsp -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> - - - - -菜鸟教程(runoob.com) - - - -

include 动作实例

- - - - -``` - -现在将以上两个文件放在服务器的根目录下,访问 main.jsp 文件。显示结果如下: - -``` -include 动作实例 - -今天的日期是: 2016-6-25 14:08:17 -``` - ---- - -## - -**jsp:useBean**  动作用来加载一个将在 JSP 页面中使用的 JavaBean。 - -这个功能非常有用,因为它使得我们可以发挥 Java 组件复用的优势。 - -jsp:useBean 动作最简单的语法为: - -``` - -``` - -在类载入后,我们既可以通过 jsp:setProperty 和 jsp:getProperty 动作来修改和检索 bean 的属性。 - -以下是 useBean 动作相关的属性列表。 - -| 属性 | 描述 | -| -------- | ------------------------------------------------------------- | -| class | 指定 Bean 的完整包名。 | -| type | 指定将引用该对象变量的类型。 | -| beanName | 通过 java.beans.Beans 的 instantiate() 方法指定 Bean 的名字。 | - -在给出具体实例前,让我们先来看下 jsp:setProperty 和 jsp:getProperty 动作元素: - ---- - -## - -jsp:setProperty 用来设置已经实例化的 Bean 对象的属性,有两种用法。首先,你可以在 jsp:useBean 元素的外面(后面)使用 jsp:setProperty,如下所示: - -``` - -... - -``` - -此时,不管 jsp:useBean 是找到了一个现有的 Bean,还是新创建了一个 Bean 实例,jsp:setProperty 都会执行。第二种用法是把 jsp:setProperty 放入 jsp:useBean 元素的内部,如下所示: - -``` - -... - - -``` - -此时,jsp:setProperty 只有在新建 Bean 实例时才会执行,如果是使用现有实例则不执行 jsp:setProperty。 - -jsp:setProperty 动作有下面四个属性,如下表: - -| 属性 | 描述 | -| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| name | name 属性是必需的。它表示要设置属性的是哪个 Bean。 | -| property | property 属性是必需的。它表示要设置哪个属性。有一个特殊用法:如果 property 的值是"\*",表示所有名字和 Bean 属性名字匹配的请求参数都将被传递给相应的属性 set 方法。 | -| 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 自己提供默认属性值,只有当请求参数明确指定了新值时才修改默认属性值。 | - ---- - -## - -jsp:getProperty 动作提取指定 Bean 属性的值,转换成字符串,然后输出。语法格式如下: - -``` - -... - -``` - -下表是与 getProperty 相关联的属性: - -| 属性 | 描述 | -| -------- | ----------------------------------------- | -| name | 要检索的 Bean 属性名称。Bean 必须已定义。 | -| property | 表示要提取 Bean 属性的值 | - -实例 - -以下实例我们使用了 Bean: - -``` -package com.runoob.main; - -public class TestBean { - private String message = "菜鸟教程"; - - public String getMessage() { - return(message); - } - public void setMessage(String message) { - this.message = message; - } -} -``` - -编译以上实例文件 TestBean.java : - -``` -$ javac TestBean.java -``` - -编译完成后会在当前目录下生成一个  **TestBean.class**  文件, 将该文件拷贝至当前 JSP 项目的  **WebContent/WEB-INF/classes/com/runoob/main**  下( com/runoob/main 包路径,没有需要手动创建)。 - -下面是一个 Eclipse 中目录结构图: - -![img](http://www.runoob.com/wp-content/uploads/2014/01/6AC33FBA-0B76-4BFD-A690-E856E9E01900.jpg) - -下面是一个很简单的例子,它的功能是装载一个 Bean,然后设置/读取它的 message 属性。 - -现在让我们在 main.jsp 文件中调用该 Bean: - -``` -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> - - - - -菜鸟教程(runoob.com) - - - -

Jsp 使用 JavaBean 实例

- - - - -

输出信息....

- - - - - -``` - -浏览器访问,执行以上文件,输出如下所示: - -![img](http://www.runoob.com/wp-content/uploads/2014/01/D7AD87A8-3392-4D4E-8731-18806B0644CD.jpg) - ---- - -## - -jsp:forward 动作把请求转到另外的页面。jsp:forward 标记只有一个属性 page。语法格式如下所示: - -``` - -``` - -以下是 forward 相关联的属性: - -| 属性 | 描述 | -| ---- | ----------------------------------------------------------------------------------------------------------------------------- | -| page | page 属性包含的是一个相对 URL。page 的值既可以直接给出,也可以在请求的时候动态计算,可以是一个 JSP 页面或者一个 Java Servlet. | - -实例 - -以下实例我们使用了两个文件,分别是: date.jsp 和 main.jsp。 - -date.jsp 文件代码如下: - -``` -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -

- 今天的日期是: <%= (new java.util.Date()).toLocaleString()%> -

-``` - -main.jsp 文件代码: - -``` -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> - - - - -菜鸟教程(runoob.com) - - - -

forward 动作实例

- - - -``` - -现在将以上两个文件放在服务器的根目录下,访问 main.jsp 文件。显示结果如下: - -``` -今天的日期是: 2016-6-25 14:37:25 -``` - ---- - -## - -jsp:plugin 动作用来根据浏览器的类型,插入通过 Java 插件 运行 Java Applet 所必需的 OBJECT 或 EMBED 元素。 - -如果需要的插件不存在,它会下载插件,然后执行 Java 组件。 Java 组件可以是一个 applet 或一个 JavaBean。 - -plugin 动作有多个对应 HTML 元素的属性用于格式化 Java 组件。param 元素可用于向 Applet 或 Bean 传递参数。 - -以下是使用 plugin 动作元素的典型实例: - -``` - - - - - - Unable to initialize Java Plugin - - - -``` - -如果你有兴趣可以尝试使用 applet 来测试 jsp:plugin 动作元素,元素是一个新元素,在组件出现故障的错误是发送给用户错误信息。 - ---- - -## - -动作元素动态定义 XML 元素。动态是非常重要的,这就意味着 XML 元素在编译时是动态生成的而非静态。 - -以下实例动态定义了 XML 元素: - -``` -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> - - - - -菜鸟教程(runoob.com) - - - - - 属性值 - - - XML 元素的主体 - - - - -``` - -浏览器访问以下页面,输出结果如下所示: - -![img](http://www.runoob.com/wp-content/uploads/2014/01/7D8C47F0-0DDE-4F1D-8BE1-B2C9C955683E.jpg) - ---- - -## - -动作元素允许在 JSP 页面和文档中使用写入文本的模板,语法格式如下: - -``` -模板数据 -``` - -以上文本模板不能包含其他元素,只能只能包含文本和 EL 表达式(注:EL 表达式将在后续章节中介绍)。请注意,在 XML 文件中,您不能使用表达式如 ${whatever > 0},因为>符号是非法的。 你可以使用 ${whatever gt 0}表达式或者嵌入在一个 CDATA 部分的值。 - -``` -]]> -``` - -如果你需要在 XHTML 中声明 DOCTYPE,必须使用到动作元素,实例如下: - -``` -]]> - -jsp:text action - - - - Welcome to JSP Programming - - - - -``` - -你可以对以上实例尝试使用及不使用该动作元素执行结果的区别。 diff --git a/docs/javaee/jsp/jsp-directive.md b/docs/javaee/jsp/jsp-directive.md deleted file mode 100644 index 084d1a77..00000000 --- a/docs/javaee/jsp/jsp-directive.md +++ /dev/null @@ -1,115 +0,0 @@ ---- -title: JSP 指令 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- jsp ---- - -## 概述 - -JSP 指令用来设置整个 JSP 页面相关的属性,如网页的编码方式和脚本语言。 - -JSP 指令以开`<%@`开始,以`%>`结束。 - -JSP 指令语法格式如下: - -```jsp -<%@ directive attribute="value" %> -``` - -指令可以有很多个属性,它们以键值对的形式存在,并用逗号隔开。 - -JSP 中的三种指令标签: - -| **指令** | **描述** | -| ------------------ | -------------------------------------------------------- | -| <%@ page ... %> | 定义网页依赖属性,比如脚本语言、error 页面、缓存需求等等 | -| <%@ include ... %> | 包含其他文件 | -| <%@ taglib ... %> | 引入标签库的定义,可以是自定义标签 | - -## Page 指令 - -Page 指令为容器提供当前页面的使用说明。一个 JSP 页面可以包含多个`page`指令。 - -Page 指令的语法格式: - -```jsp -<%@ page attribute="value" %> -``` - -等价的 XML 格式: - -```jsp - -``` - -例: - -```jsp -<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> -``` - -### 属性 - -下表列出与 Page 指令相关的属性: - -| **属性** | **描述** | -| ------------------ | ----------------------------------------------------- | -| buffer | 指定 out 对象使用缓冲区的大小 | -| autoFlush | 控制 out 对象的   缓存区 | -| contentType | 指定当前 JSP 页面的 MIME 类型和字符编码 | -| errorPage | 指定当 JSP 页面发生异常时需要转向的错误处理页面 | -| isErrorPage | 指定当前页面是否可以作为另一个 JSP 页面的错误处理页面 | -| extends | 指定 servlet 从哪一个类继承 | -| import | 导入要使用的 Java 类 | -| info | 定义 JSP 页面的描述信息 | -| isThreadSafe | 指定对 JSP 页面的访问是否为线程安全 | -| language | 定义 JSP 页面所用的脚本语言,默认是 Java | -| session | 指定 JSP 页面是否使用 session | -| isELIgnored | 指定是否执行 EL 表达式 | -| isScriptingEnabled | 确定脚本元素能否被使用 | - -## Include 指令 - -JSP 可以通过`include`指令来包含其他文件。 - -被包含的文件可以是 JSP 文件、HTML 文件或文本文件。包含的文件就好像是该 JSP 文件的一部分,会被同时编译执行。 - -Include 指令的语法格式如下: - -```jsp -<%@ include file="文件相对 url 地址" %> -``` - -**include**  指令中的文件名实际上是一个相对的 URL 地址。 - -如果您没有给文件关联一个路径,JSP 编译器默认在当前路径下寻找。 - -等价的 XML 语法: - -```jsp - -``` - -## Taglib 指令 - -JSP 允许用户自定义标签,一个自定义标签库就是自定义标签的集合。 - -`taglib`指令引入一个自定义标签集合的定义,包括库路径、自定义标签。 - -`taglib`指令的语法: - -```jsp -<%@ taglib uri="uri" prefix="prefixOfTag" %> -``` - -uri 属性确定标签库的位置,prefix 属性指定标签库的前缀。 - -等价的 XML 语法: - -```jsp - -``` diff --git a/docs/javaee/jsp/jsp-el-expression.md b/docs/javaee/jsp/jsp-el-expression.md deleted file mode 100644 index 6deeea0f..00000000 --- a/docs/javaee/jsp/jsp-el-expression.md +++ /dev/null @@ -1,217 +0,0 @@ ---- -title: JSP 表达式语言 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- jsp ---- - -EL 表达式是用`${}`括起来的脚本,用来更方便地读取对象。EL 表达式写在 JSP 的 HTML 代码中,而不能写在`<%`与`%>`引起的 JSP 脚本中。 - -JSP 表达式语言(EL)使得访问存储在 JavaBean 中的数据变得非常简单。JSP EL 既可以用来创建算术表达式也可以用来创建逻辑表达式。在 JSP EL 表达式内可以使用整型数,浮点数,字符串,常量 true、false,还有 null。 - -## 一个简单的语法 - -典型的,当您需要在 JSP 标签中指定一个属性值时,只需要简单地使用字符串即可: - -```jsp - -``` - -JSP EL 允许您指定一个表达式来表示属性值。一个简单的表达式语法如下: - -```jsp -${expr} -``` - -其中,expr 指的是表达式。在 JSP EL 中通用的操作符是"."和"[]"。这两个操作符允许您通过内嵌的 JSP 对象访问各种各样的 JavaBean 属性。 - -举例来说,上面的标签可以使用表达式语言改写成如下形式: - -```jsp - -``` - -当 JSP 编译器在属性中见到"${}"格式后,它会产生代码来计算这个表达式,并且产生一个替代品来代替表达式的值。 - -您也可以在标签的模板文本中使用表达式语言。比如标签简单地将其主体中的文本插入到 JSP 输出中: - -```jsp - -

Hello JSP!

-
-``` - -现在,在标签主体中使用表达式,就像这样: - -```jsp - -Box Perimeter is: ${2*box.width + 2*box.height} - -``` - -在 EL 表达式中可以使用圆括号来组织子表达式。比如${(1 + 2) _ 3}等于 9,但是${1 + (2 _ 3)} 等于 7。 - -想要停用对 EL 表达式的评估的话,需要使用 page 指令将 isELIgnored 属性值设为 true: - -```jsp -<%@ page isELIgnored ="true|false" %> -``` - -这样,EL 表达式就会被忽略。若设为 false,则容器将会计算 EL 表达式。 - ---- - -## EL 中的基础操作符 - -EL 表达式支持大部分 Java 所提供的算术和逻辑操作符: - -| **操作符** | **描述** | -| ---------- | ---------------------------------- | -| . | 访问一个 Bean 属性或者一个映射条目 | -| [] | 访问一个数组或者链表的元素 | -| ( ) | 组织一个子表达式以改变优先级 | -| + | 加 | -| - | 减或负 | -| \* | 乘 | -| / or div | 除 | -| % or mod | 取模 | -| == or eq | 测试是否相等 | -| != or ne | 测试是否不等 | -| < or lt | 测试是否小于 | -| > or gt | 测试是否大于 | -| <= or le | 测试是否小于等于 | -| >= or ge | 测试是否大于等于 | -| && or and | 测试逻辑与 | -| \|\| or or | 测试逻辑或 | -| ! or not | 测试取反 | -| empty | 测试是否空值 | - ---- - -## JSP EL 中的函数 - -JSP EL 允许您在表达式中使用函数。这些函数必须被定义在自定义标签库中。函数的使用语法如下: - -```jsp -${ns:func(param1, param2, ...)} -``` - -ns 指的是命名空间(namespace),func 指的是函数的名称,param1 指的是第一个参数,param2 指的是第二个参数,以此类推。比如,有函数 fn:length,在 JSTL 库中定义,可以像下面这样来获取一个字符串的长度: - -```jsp -${fn:length("Get my length")} -``` - -要使用任何标签库中的函数,您需要将这些库安装在服务器中,然后使用标签在 JSP 文件中包含这些库。 - ---- - -## JSP EL 隐含对象 - -JSP EL 支持下表列出的隐含对象: - -| **隐含对象** | **描述** | -| ---------------- | ------------------------------ | -| pageScope | page 作用域 | -| requestScope | request 作用域 | -| sessionScope | session 作用域 | -| applicationScope | application 作用域 | -| param | Request 对象的参数,字符串 | -| paramValues | Request 对象的参数,字符串集合 | -| header | HTTP 信息头,字符串 | -| headerValues | HTTP 信息头,字符串集合 | -| initParam | 上下文初始化参数 | -| cookie | Cookie 值 | -| pageContext | 当前页面的 pageContext | - -您可以在表达式中使用这些对象,就像使用变量一样。接下来会给出几个例子来更好的理解这个概念。 - ---- - -## pageContext 对象 - -pageContext 对象是 JSP 中 pageContext 对象的引用。通过 pageContext 对象,您可以访问 request 对象。比如,访问 request 对象传入的查询字符串,就像这样: - -```jsp -${pageContext.request.queryString} -``` - ---- - -## Scope 对象 - -pageScope,requestScope,sessionScope,applicationScope 变量用来访问存储在各个作用域层次的变量。 - -举例来说,如果您需要显式访问在 applicationScope 层的 box 变量,可以这样来访问:applicationScope.box。 - ---- - -## param 和 paramValues 对象 - -param 和 paramValues 对象用来访问参数值,通过使用 request.getParameter 方法和 request.getParameterValues 方法。 - -举例来说,访问一个名为 order 的参数,可以这样使用表达式:${param.order},或者${param["order"]}。 - -接下来的例子表明了如何访问 request 中的 username 参数: - -```jsp -<%@ page import="java.io.*,java.util.*" %> -<% - String title = "Accessing Request Param"; -%> - - -<% out.print(title); %> - - -
-

<% out.print(title); %>

-
-
-

${param["username"]}

-
- - -``` - -param 对象返回单一的字符串,而 paramValues 对象则返回一个字符串数组。 - ---- - -## header 和 headerValues 对象 - -header 和 headerValues 对象用来访问信息头,通过使用 request.getHeader 方法和 request.getHeaders 方法。 - -举例来说,要访问一个名为 user-agent 的信息头,可以这样使用表达式:${header.user-agent},或者${header["user-agent"]}。 - -接下来的例子表明了如何访问 user-agent 信息头: - -```jsp -<%@ page import="java.io.*,java.util.*" %> -<% - String title = "User Agent Example"; -%> - - -<% out.print(title); %> - - -
-

<% out.print(title); %>

-
-
-

${header["user-agent"]}

-
- - -``` - -运行结果如下: - -![jsp-expression-language](http://www.runoob.com/wp-content/uploads/2014/01/jsp-expression-language.jpg) - -header 对象返回单一值,而 headerValues 则返回一个字符串数组。 diff --git a/docs/javaee/jsp/jsp-grammar.md b/docs/javaee/jsp/jsp-grammar.md deleted file mode 100644 index 1b36efbc..00000000 --- a/docs/javaee/jsp/jsp-grammar.md +++ /dev/null @@ -1,383 +0,0 @@ ---- -title: JSP 语法 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- jsp ---- - -本小节将会简单地介绍一下 JSP 开发中的基础语法。 - ---- - -## 脚本 - -脚本程序可以包含任意量的 Java 语句、变量、方法或表达式,只要它们在脚本语言中是有效的。 - -脚本程序的语法格式: - -```jsp -<% 代码片段 %> -``` - -或者,您也可以编写与其等价的 XML 语句,就像下面这样: - -```jsp - - 代码片段 - -``` - -任何文本、HTML 标签、JSP 元素必须写在脚本程序的外面。 - -下面给出一个示例,同时也是本教程的第一个 JSP 示例: - -```jsp - -Hello World - -Hello World!
-<% -out.println("Your IP address is " + request.getRemoteAddr()); -%> - - -``` - -**注意:**请确保 Apache Tomcat 已经安装在 C:\apache-tomcat-7.0.2 目录下并且运行环境已经正确设置。 - -将以上代码保存在 hello.jsp 中,然后将它放置在 C:\apache-tomcat-7.0.2\webapps\ROOT 目录下,打开浏览器并在地址栏中输入http://localhost:8080/hello.jsp。运行后得到以下结果: - -![img](http://www.runoob.com/wp-content/uploads/2014/01/jsp_hello_world.jpg) - -### 中文编码问题 - -如果我们要在页面正常显示中文,我们需要在 JSP 文件头部添加以下代码:<> - -```jsp -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -``` - -接下来我们将以上程序修改为: - -```jsp -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> - - - - -菜鸟教程(runoob.com) - - -Hello World!
-<% -out.println("你的 IP 地址 " + request.getRemoteAddr()); -%> - - -``` - -这样中文就可以正常显示了。 - -## JSP 声明 - -一个声明语句可以声明一个或多个变量、方法,供后面的 Java 代码使用。在 JSP 文件中,您必须先声明这些变量和方法然后才能使用它们。 - -JSP 声明的语法格式: - -```jsp -<%! declaration; [ declaration; ]+ ... %> -``` - -或者,您也可以编写与其等价的 XML 语句,就像下面这样: - -```jsp - - 代码片段 - -``` - -程序示例: - -```jsp -<%! int i = 0; %> -<%! int a, b, c; %> -<%! Circle a = new Circle(2.0); %> -``` - -## JSP 表达式 - -一个 JSP 表达式中包含的脚本语言表达式,先被转化成 String,然后插入到表达式出现的地方。 - -由于表达式的值会被转化成 String,所以您可以在一个文本行中使用表达式而不用去管它是否是 HTML 标签。 - -表达式元素中可以包含任何符合 Java 语言规范的表达式,但是不能使用分号来结束表达式。 - -JSP 表达式的语法格式: - -``` -<%= 表达式 %> -``` - -同样,您也可以编写与之等价的 XML 语句: - -``` - - 表达式 - -``` - -程序示例: - -``` -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> - - - - -菜鸟教程(runoob.com) - - -

- 今天的日期是: <%= (new java.util.Date()).toLocaleString()%> -

- - -``` - -运行后得到以下结果: - -``` -今天的日期是: 2016-6-25 13:40:07 -``` - ---- - -## JSP 注释 - -JSP 注释主要有两个作用:为代码作注释以及将某段代码注释掉。 - -JSP 注释的语法格式: - -```jsp -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> - - - - -JSP注释示例 - - -<%-- 该部分注释在网页中不会被显示--%> -

- 今天的日期是: <%= (new java.util.Date()).toLocaleString()%> -

- - -``` - -运行后得到以下结果: - -``` -今天的日期是: 2016-6-25 13:41:26 -``` - -不同情况下使用注释的语法规则: - -| **语法** | 描述 | -| -------------- | ----------------------------------------------------- | -| <%-- 注释 --%> | JSP 注释,注释内容不会被发送至浏览器甚至不会被编译 | -| | HTML 注释,通过浏览器查看网页源代码时可以看见注释内容 | -| <\% | 代表静态 <%常量 | -| %\> | 代表静态 %> 常量 | -| \' | 在属性中使用的单引号 | -| \" | 在属性中使用的双引号 | - -## 控制语句 - -JSP 提供对 Java 语言的全面支持。您可以在 JSP 程序中使用 Java API 甚至建立 Java 代码块,包括判断语句和循环语句等等。 - -### if…else 语句 - -`If…else`块,请看下面这个例子: - -```jsp -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%! int day = 1; %> - - - - - 02.JSP语法 - if...else示例 - - -

IF...ELSE 实例

-<% if (day == 1 | day == 7) { %> -

今天是周末

-<% } else { %> -

今天不是周末

-<% } %> - - -``` - -运行后得到以下结果: - -``` -IF...ELSE 实例 -今天不是周末 -``` - -### switch…case 语句 - -现在来看看 switch…case 块,与 if…else 块有很大的不同,它使用 out.println(),并且整个都装在脚本程序的标签中,就像下面这样: - -```jsp -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%! int day = 3; %> - - - - - 02.JSP语法 - switch...case示例 - - -

Sswitch...case示例

-<% - switch(day) { - case 0: - out.println("星期天"); - break; - case 1: - out.println("星期一"); - break; - case 2: - out.println("星期二"); - break; - case 3: - out.println("星期三"); - break; - case 4: - out.println("星期四"); - break; - case 5: - out.println("星期五"); - break; - default: - out.println("星期六"); - } -%> - - -``` - -浏览器访问,运行后得出以下结果: - -``` -SWITCH...CASE 实例 - -星期三 -``` - -### 循环语句 - -在 JSP 程序中可以使用 Java 的三个基本循环类型:for,while,和 do…while。 - -让我们来看看 for 循环的例子,以下输出的不同字体大小的"菜鸟教程": - -``` -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%! int fontSize; %> - - - - -菜鸟教程(runoob.com) - - -

For 循环实例

-<%for ( fontSize = 1; fontSize <= 3; fontSize++){ %> - - 菜鸟教程 -
-<%}%> - - -``` - -运行后得到以下结果: - -![img](http://www.runoob.com/wp-content/uploads/2014/01/7B4B85CF-FE4B-43CB-AAFF-F8594AD4342C.jpg) - -将上例改用 while 循环来写: - -``` -<%@ page language="java" contentType="text/html; charset=UTF-8" - pageEncoding="UTF-8"%> -<%! int fontSize; %> - - - - -菜鸟教程(runoob.com) - - -

While 循环实例

-<%while ( fontSize <= 3){ %> - - 菜鸟教程 -
-<%fontSize++;%> -<%}%> - - -``` - -浏览器访问,输出结果为(fontSize 初始化为 0,所以多输出了一行): - -![img](http://www.runoob.com/wp-content/uploads/2014/01/4F744CC9-E484-45BA-AF18-27AFCF4AD45C.jpg) - -JSP 运算符 - -JSP 支持所有 Java 逻辑和算术运算符。 - -下表罗列出了 JSP 常见运算符,优先级从高到底: - -| **类别** | **操作符** | **结合性** | -| --------- | ----------------------------------- | ---------- | -| 后缀 | () [] . (点运算符) | 左到右 | -| 一元 | ++ - - ! ~ | 右到左 | -| 可乘性 | \* / % | 左到右 | -| 可加性 | + - | 左到右 | -| 移位 | >> >>> << | 左到右 | -| 关系 | > >= < <= | 左到右 | -| 相等/不等 | == != | 左到右 | -| 位与 | & | 左到右 | -| 位异或 | ^ | 左到右 | -| 位或 | \| | 左到右 | -| 逻辑与 | && | 左到右 | -| 逻辑或 | \|\| | 左到右 | -| 条件判断 | ?: | 右到左 | -| 赋值 | = += -= \*= /= %= >>= <<= &= ^= \|= | 右到左 | -| 逗号 | , | 左到右 | - ---- - -## JSP 字面量 - -JSP 语言定义了以下几个字面量: - -- 布尔值(boolean):true 和 false; -- 整型(int):与 Java 中的一样; -- 浮点型(float):与 Java 中的一样; -- 字符串(string):以单引号或双引号开始和结束; -- Null:null。 diff --git a/docs/javaee/jsp/jsp-implicit-objects.md b/docs/javaee/jsp/jsp-implicit-objects.md deleted file mode 100644 index 0cd99495..00000000 --- a/docs/javaee/jsp/jsp-implicit-objects.md +++ /dev/null @@ -1,113 +0,0 @@ ---- -title: JSP 隐式对象 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- jsp ---- - -JSP 隐式对象是 JSP 容器为每个页面提供的 Java 对象,开发者可以直接使用它们而不用显式声明。JSP 隐式对象也被称为预定义变量。 - -JSP 所支持的九大隐式对象: - -| **对象** | **描述** | -| ----------- | ------------------------------------------------------------------ | -| request | **HttpServletRequest**类的实例 | -| response | **HttpServletResponse**类的实例 | -| out | **PrintWriter**类的实例,用于把结果输出至网页上 | -| session | **HttpSession**类的实例 | -| application | **ServletContext**类的实例,与应用上下文有关 | -| config | **ServletConfig**类的实例 | -| pageContext | **PageContext**类的实例,提供对 JSP 页面所有对象以及命名空间的访问 | -| page | 类似于 Java 类中的 this 关键字 | -| Exception | **Exception**类的对象,代表发生错误的 JSP 页面中对应的异常对象 | - -## request 对象 - -`request`对象是`javax.servlet.http.HttpServletRequest` 类的实例。 - -每当客户端请求一个 JSP 页面时,JSP 引擎就会制造一个新的`request`对象来代表这个请求。 - -`request`对象提供了一系列方法来获取 HTTP 头信息,cookies,HTTP 方法等等。 - -## response 对象 - -`response`对象是`javax.servlet.http.HttpServletResponse`类的实例。 - -当服务器创建`request`对象时会同时创建用于响应这个客户端的`response`对象。 - -`response`对象也定义了处理 HTTP 头模块的接口。通过这个对象,开发者们可以添加新的 cookies,时间戳,HTTP 状态码等等。 - -## out 对象 - -`out`对象是`javax.servlet.jsp.JspWriter`类的实例,用来在`response`对象中写入内容。 - -最初的`JspWriter`类对象根据页面是否有缓存来进行不同的实例化操作。可以在`page`指令中使用`buffered='false'`属性来轻松关闭缓存。 - -`JspWriter`类包含了大部分`java.io.PrintWriter`类中的方法。不过,`JspWriter`新增了一些专为处理缓存而设计的方法。还有就是,`JspWriter`类会抛出`IOExceptions`异常,而`PrintWriter`不会。 - -下表列出了我们将会用来输出`boolean`,`char`,`int`,`double`,`String`,`object`等类型数据的重要方法: - -| **方法** | **描述** | -| ---------------------------- | -------------------------- | -| **out.print(dataType dt)** | 输出 Type 类型的值 | -| **out.println(dataType dt)** | 输出 Type 类型的值然后换行 | -| **out.flush()** | 刷新输出流 | - -## session 对象 - -`session`对象是`javax.servlet.http.HttpSession`类的实例。和 Java Servlets 中的`session`对象有一样的行为。 - -`session`对象用来跟踪在各个客户端请求间的会话。 - -## application 对象 - -`application`对象直接包装了 servlet 的`ServletContext`类的对象,是`javax.servlet.ServletContext`类的实例。 - -这个对象在 JSP 页面的整个生命周期中都代表着这个 JSP 页面。这个对象在 JSP 页面初始化时被创建,随着`jspDestroy()`方法的调用而被移除。 - -通过向`application`中添加属性,则所有组成您 web 应用的 JSP 文件都能访问到这些属性。 - -## config 对象 - -`config`对象是`javax.servlet.ServletConfig`类的实例,直接包装了 servlet 的`ServletConfig`类的对象。 - -这个对象允许开发者访问 Servlet 或者 JSP 引擎的初始化参数,比如文件路径等。 - -以下是 config 对象的使用方法,不是很重要,所以不常用: - -```jsp -config.getServletName(); -``` - -它返回包含在``元素中的 servlet 名字,注意,``元素在`WEB-INF\web.xml`文件中定义。 - -## pageContext 对象 - -`pageContext`对象是`javax.servlet.jsp.PageContext`类的实例,用来代表整个 JSP 页面。 - -这个对象主要用来访问页面信息,同时过滤掉大部分实现细节。 - -这个对象存储了`request`对象和`response`对象的引用。`application`对象,`config`对象,`session`对象,`out`对象可以通过访问这个对象的属性来导出。 - -`pageContext`对象也包含了传给 JSP 页面的指令信息,包括缓存信息,ErrorPage URL,页面 scope 等。 - -`PageContext`类定义了一些字段,包括 PAGE_SCOPE,REQUEST_SCOPE,SESSION_SCOPE, APPLICATION_SCOPE。它也提供了 40 余种方法,有一半继承自`javax.servlet.jsp.JspContext` 类。 - -其中一个重要的方法就是`removeArribute()`,它可接受一个或两个参数。比如,pageContext.removeArribute("attrName")移除四个 scope 中相关属性,但是下面这种方法只移除特定 scope 中的相关属性: - -```jsp -pageContext.removeAttribute("attrName", PAGE_SCOPE); -``` - -## page 对象 - -这个对象就是页面实例的引用。它可以被看做是整个 JSP 页面的代表。 - -`page`对象就是`this`对象的同义词。 - -## exception 对象 - -`exception`对象包装了从先前页面中抛出的异常信息。它通常被用来产生对出错条件的适当响应。 diff --git a/docs/javaee/jsp/jsp-introduction.md b/docs/javaee/jsp/jsp-introduction.md deleted file mode 100644 index 4b9e0a8f..00000000 --- a/docs/javaee/jsp/jsp-introduction.md +++ /dev/null @@ -1,152 +0,0 @@ ---- -title: JSP 概述 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- jsp ---- - -## 什么是 Java Server Pages? - -`JSP`全称`Java Server Pages`,是一种动态网页开发技术。 - -它使用 JSP 标签在 HTML 网页中插入 Java 代码。标签通常以`<%`开头以`%>`结束。 - -JSP 是一种 Java servlet,主要用于实现 Java web 应用程序的用户界面部分。网页开发者们通过结合 HTML 代码、XHTML 代码、XML 元素以及嵌入 JSP 操作和命令来编写 JSP。 - -JSP 通过网页表单获取用户输入数据、访问数据库及其他数据源,然后动态地创建网页。 - -JSP 标签有多种功能,比如访问数据库、记录用户选择信息、访问 JavaBeans 组件等,还可以在不同的网页中传递控制信息和共享信息。 - -## 为什么使用 JSP? - -JSP 也是一种 Servlet,因此 JSP 能够完成 Servlet 能完成的任何工作。 - -JSP 程序与 CGI 程序有着相似的功能,但和 CGI 程序相比,JSP 程序有如下优势: - -- 性能更加优越,因为 JSP 可以直接在 HTML 网页中动态嵌入元素而不需要单独引用 CGI 文件。 -- 服务器调用的是已经编译好的 JSP 文件,而不像 CGI/Perl 那样必须先载入解释器和目标脚本。 -- JSP 基于 Java Servlets API,因此,JSP 拥有各种强大的企业级 Java API,包括 JDBC,JNDI,EJB,JAXP 等等。 -- JSP 页面可以与处理业务逻辑的 servlets 一起使用,这种模式被 Java servlet 模板引擎所支持。 - -最后,JSP 是 Java EE 不可或缺的一部分,是一个完整的企业级应用平台。这意味着 JSP 可以用最简单的方式来实现最复杂的应用。 - -## JSP 的优势 - -以下列出了使用 JSP 带来的其他好处: - -- 与 ASP 相比:JSP 有两大优势。首先,动态部分用 Java 编写,而不是 VB 或其他 MS 专用语言,所以更加强大与易用。第二点就是 JSP 易于移植到非 MS 平台上。 -- 与纯 Servlets 相比:JSP 可以很方便的编写或者修改 HTML 网页而不用去面对大量的 println 语句。 -- 与 SSI 相比:SSI 无法使用表单数据、无法进行数据库链接。 -- 与 JavaScript 相比:虽然 JavaScript 可以在客户端动态生成 HTML,但是很难与服务器交互,因此不能提供复杂的服务,比如访问数据库和图像处理等等。 -- 与静态 HTML 相比:静态 HTML 不包含动态信息。 - -# JSP 工作原理 - -**JSP 是一种 Servlet**,但工作方式和 Servlet 有所差别。 - -Servlet 是先将源代码编译为 class 文件后部署到服务器下的,**先编译后部署**。 - -Jsp 是先将源代码部署到服务器再编译,**先部署后编译**。 - -Jsp 会在客户端第一次请求 Jsp 文件时被编译为 HttpJspPage 类(Servlet 的一个子类)。该类会被服务器临时存放在服务器工作目录里。所以,第一次请求 Jsp 后,访问速度会变快就是这个道理。 - -## JSP 工作流程 - -网络服务器需要一个 JSP 引擎,也就是一个容器来处理 JSP 页面。容器负责截获对 JSP 页面的请求。本教程使用内嵌 JSP 容器的 Apache 来支持 JSP 开发。 - -JSP 容器与 Web 服务器协同合作,为 JSP 的正常运行提供必要的运行环境和其他服务,并且能够正确识别专属于 JSP 网页的特殊元素。 - -下图显示了 JSP 容器和 JSP 文件在 Web 应用中所处的位置。 - -![img](http://www.runoob.com/wp-content/uploads/2014/01/jsp-arch.jpg) - -### 工作步骤 - -以下步骤表明了 Web 服务器是如何使用 JSP 来创建网页的: - -- 就像其他普通的网页一样,您的浏览器发送一个 HTTP 请求给服务器。 -- Web 服务器识别出这是一个对 JSP 网页的请求,并且将该请求传递给 JSP 引擎。通过使用 URL 或者.jsp 文件来完成。 -- JSP 引擎从磁盘中载入 JSP 文件,然后将它们转化为 servlet。这种转化只是简单地将所有模板文本改用 println()语句,并且将所有的 JSP 元素转化成 Java 代码。 -- JSP 引擎将 servlet 编译成可执行类,并且将原始请求传递给 servlet 引擎。 -- Web 服务器的某组件将会调用 servlet 引擎,然后载入并执行 servlet 类。在执行过程中,servlet 产生 HTML 格式的输出并将其内嵌于 HTTP response 中上交给 Web 服务器。 -- Web 服务器以静态 HTML 网页的形式将 HTTP response 返回到您的浏览器中。 -- 最终,Web 浏览器处理 HTTP response 中动态产生的 HTML 网页,就好像在处理静态网页一样。 - -以上提及到的步骤可以用下图来表示: - -一般情况下,JSP 引擎会检查 JSP 文件对应的 servlet 是否已经存在,并且检查 JSP 文件的修改日期是否早于 servlet。如果 JSP 文件的修改日期早于对应的 servlet,那么容器就可以确定 JSP 文件没有被修改过并且 servlet 有效。这使得整个流程与其他脚本语言(比如 PHP)相比要高效快捷一些。 - -# JSP  生命周期 - -理解 JSP 底层功能的关键就是去理解它们所遵守的生命周期。 - -JSP 生命周期就是从创建到销毁的整个过程,类似于 servlet 生命周期,区别在于 JSP 生命周期还包括将 JSP 文件编译成 servlet。 - -以下是 JSP 生命周期中所走过的几个阶段: - -- **编译阶段:**servlet 容器编译 servlet 源文件,生成 servlet 类 -- **初始化阶段:**加载与 JSP 对应的 servlet 类,创建其实例,并调用它的初始化方法 -- **执行阶段:**调用与 JSP 对应的 servlet 实例的服务方法 -- **销毁阶段:**调用与 JSP 对应的 servlet 实例的销毁方法,然后销毁 servlet 实例 - -很明显,JSP 生命周期的四个主要阶段和 servlet 生命周期非常相似,下面给出图示: - -![img](http://www.runoob.com/wp-content/uploads/2014/01/jsp_life_cycle.jpg) - -## JSP 编译 - -当浏览器请求 JSP 页面时,JSP 引擎会首先去检查是否需要编译这个文件。如果这个文件没有被编译过,或者在上次编译后被更改过,则编译这个 JSP 文件。 - -编译的过程包括三个步骤: - -- 解析 JSP 文件。 -- 将 JSP 文件转为 servlet。 -- 编译 servlet。 - -## JSP 初始化 - -容器载入 JSP 文件后,它会在为请求提供任何服务前调用 jspInit()方法。如果您需要执行自定义的 JSP 初始化任务,复写 jspInit()方法就行了,就像下面这样: - -``` -public void jspInit(){ - // 初始化代码 -} -``` - -一般来讲程序只初始化一次,servlet 也是如此。通常情况下您可以在 jspInit()方法中初始化数据库连接、打开文件和创建查询表。 - -## JSP 执行 - -这一阶段描述了 JSP 生命周期中一切与请求相关的交互行为,直到被销毁。 - -当 JSP 网页完成初始化后,JSP 引擎将会调用\_jspService()方法。 - -\_jspService()方法需要一个 HttpServletRequest 对象和一个 HttpServletResponse 对象作为它的参数,就像下面这样: - -``` -void _jspService(HttpServletRequest request, - HttpServletResponse response) -{ - // 服务端处理代码 -} -``` - -\_jspService()方法在每个 request 中被调用一次并且负责产生与之相对应的 response,并且它还负责产生所有 7 个 HTTP 方法的回应,比如 GET、POST、DELETE 等等。 - -## JSP 清理 - -JSP 生命周期的销毁阶段描述了当一个 JSP 网页从容器中被移除时所发生的一切。 - -jspDestroy()方法在 JSP 中等价于 servlet 中的销毁方法。当您需要执行任何清理工作时复写 jspDestroy()方法,比如释放数据库连接或者关闭文件夹等等。 - -jspDestroy()方法的格式如下: - -``` -public void jspDestroy() -{ - // 清理代码 -} -``` diff --git a/docs/javaee/jsp/jsp-locale.md b/docs/javaee/jsp/jsp-locale.md deleted file mode 100644 index 8a200ec5..00000000 --- a/docs/javaee/jsp/jsp-locale.md +++ /dev/null @@ -1,62 +0,0 @@ ---- -title: JSP 国际化 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- jsp ---- - -在开始前,需要解释几个重要的概念: - -- 国际化(i18n):表明一个页面根据访问者的语言或国家来呈现不同的翻译版本。 -- 本地化(l10n):向网站添加资源,以使它适应不同的地区和文化。比如网站的印度语版本。 -- 区域:这是一个特定的区域或文化,通常认为是一个语言标志和国家标志通过下划线连接起来。比如"en_US"代表美国英语地区。 - -如果想要建立一个全球化的网站,就需要关心一系列项目。本章将会详细告诉您如何处理国际化问题,并给出了一些例子来加深理解。 - -JSP 容器能够根据 request 的 locale 属性来提供正确地页面版本。接下来给出了如何通过 request 对象来获得 Locale 对象的语法: - -``` -java.util.Locale request.getLocale() -``` - -## 检测 Locale - -下表列举出了 Locale 对象中比较重要的方法,用于检测 request 对象的地区,语言,和区域。所有这些方法都会在浏览器中显示国家名称和语言名称: - -| **序号** | **方法** & **描述** | -| -------- | --------------------------------------------------------------------------------- | -| 1 | **String getCountry()**返回国家/地区码的英文大写,或 ISO 3166 2-letter 格式的区域 | -| 2 | **String getDisplayCountry()**返回要显示给用户的国家名称 | -| 3 | **String getLanguage()**返回语言码的英文小写,或 ISO 639 格式的区域 | -| 4 | **String getDisplayLanguage()**返回要给用户看的语言名称 | -| 5 | **String getISO3Country()**返回国家名称的 3 字母缩写 | -| 6 | **String getISO3Language()**返回语言名称的 3 字母缩写 | - -## 语言设置 - -JSP 可以使用西欧语言来输出一个页面,比如英语,西班牙语,德语,法语,意大利语等等。由此可见,设置`Content-Language`信息头来正确显示所有字符是很重要的。 - -第二点就是,需要使用 HTML 字符实体来显示特殊字符,比如"ñ" 代表的是"?","¡"代表的是 "?" : - -## 区域特定日期 - -可以使用`java.text.DateFormat`类和它的静态方法`getDateTimeInstance()`来格式化日期和时间。接下来的这个例子显示了如何根据指定的区域来格式化日期和时间: - -## 区域特定货币 - -可以使用`java.text.NumberFormat`类和它的静态方法`getCurrencyInstance()`来格式化数字。比如在区域特定货币中的 long 型和 double 型。接下来的例子显示了如何根据指定的区域来格式化货币: - -## 区域特定百分比 - -可以使用`java.text.NumberFormat`类和它的静态方法`getPercentInstance()`来格式化百分比。接下来的例子告诉我们如何根据指定的区域来格式化百分比: - -# 参考资料 - -参考代码见: - -``` -javaee-notes\javaee-jsp\src\main\webapp\examples\locale -``` diff --git a/docs/javaee/jsp/taglib.md b/docs/javaee/jsp/taglib.md deleted file mode 100644 index 493fa6ab..00000000 --- a/docs/javaee/jsp/taglib.md +++ /dev/null @@ -1,15 +0,0 @@ - - -自定义标签是用户定义的JSP语言元素。 - -当JSP页面包含一个自定义标签时将被转化为servlet,标签转化为对被称为tag handler的对象的操作,即当servlet执行时Web container调用那些操作。 - -JSP标签扩展可以让你创建新的标签并且可以直接插入到一个JSP页面。 JSP 2.0规范中引入Simple Tag Handlers来编写这些自定义标记。 - -编写自定义步骤的方法 - -任何一个标签都对应一个Java类,该类必须实现Tag接口。 - -声明一个tld标签库描述文件 - -如果tld文件位于/WEB-INF目录下面,Tomcat会自动加载tld文件中的标签库。如果是某些不支持自动加载或者放于其他位置的tld文件,可以在web.xml中配置。 \ No newline at end of file diff --git a/docs/javaee/jstl/jstl-core.md b/docs/javaee/jstl/jstl-core.md deleted file mode 100644 index c51aa609..00000000 --- a/docs/javaee/jstl/jstl-core.md +++ /dev/null @@ -1,350 +0,0 @@ ---- -title: JSTL 核心标签 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- jstl ---- - -核心标签是最常用的 JSTL 标签。引用核心标签库的语法如下: - -```jsp -<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> -``` - -| 标签 | 描述 | -| --------------- | --------------------------------------------------------------------------------- | -| `` | 用于在 JSP 中显示数据,就像<%= ... > | -| `` | 用于保存数据 | -| `` | 用于删除数据 | -| `` | 用来处理产生错误的异常状况,并且将错误信息储存起来 | -| `` | 与我们在一般程序中用的 if 一样 | -| `` | 本身只当做``和``的父标签 | -| `` | ``的子标签,用来判断条件是否成立 | -| `` | ``的子标签,接在``标签后,当``标签判断为 false 时被执行 | -| `` | 检索一个绝对或相对 URL,然后将其内容暴露给页面 | -| `` | 基础迭代标签,接受多种集合类型 | -| `` | 根据指定的分隔符来分隔内容并迭代输出 | -| `` | 用来给包含或重定向的页面传递参数 | -| `` | 重定向至一个新的 URL. | -| `` | 使用可选的查询参数来创造一个 URL | - -## `` 标签 - -``标签用来显示一个表达式的结果,与`<%= %>`作用相似,它们的区别就是``标签可以直接通过"."操作符来访问属性。 - -举例来说,如果想要访问 customer.address.street,只需要这样写: - -```jsp - -``` - -``标签会自动忽略 XML 标记字符,所以它们不会被当做标签来处理。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| --------- | --------------------- | ------------ | ------------ | -| value | 要输出的内容 | 是 | 无 | -| default | 输出的默认值 | 否 | 主体中的内容 | -| escapeXml | 是否忽略 XML 特殊字符 | 否 | true | - -## `` 标签 - -``标签用于设置变量值和对象属性。 - -``标签就是``行为标签的孪生兄弟。 - -这个标签之所以很有用呢,是因为它会计算表达式的值,然后使用计算结果来设置 JavaBean 对象或 java.util.Map 对象的值。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | ---------------------- | ------------ | ---------- | -| value | 要存储的值 | 否 | 主体的内容 | -| target | 要修改的属性所属的对象 | 否 | 无 | -| property | 要修改的属性 | 否 | 无 | -| var | 存储信息的变量 | 否 | 无 | -| scope | var 属性的作用域 | 否 | Page | - -如果指定了 target 属性,那么 property 属性也需要被指定。 - -## `` 标签 - -``标签用于移除一个变量,可以指定这个变量的作用域,若未指定,则默认为变量第一次出现的作用域。 - -这个标签不是特别有用,不过可以用来确保 JSP 完成清理工作。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -`c:remove>`标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | ---------------- | ------------ | ---------- | -| var | 要移除的变量名称 | 是 | 无 | -| scope | 变量所属的作用域 | 否 | 所有作用域 | - -## `` 标签 - -`` 标签主要用来处理产生错误的异常状况,并且将错误信息储存起来。 - -### 语法格式 - -```jsp - -... - -``` - -### 属性 - -``标签有如下属性: - -| 属性 | 描述 | 是否必要 | 默认值 | -| ---- | ---------------------- | -------- | ------ | -| var | 用来储存错误信息的变量 | 否 | None | - -## `` 标签 - -``标签判断表达式的值,如果表达式的值为 true 则执行其主体内容。 - -### 语法格式 - -```jsp - - ... - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | ---------------------- | ------------ | ---------- | -| test | 条件 | 是 | 无 | -| var | 用于存储条件结果的变量 | 否 | 无 | -| scope | var 属性的作用域 | 否 | page | - -## ``, ``, `` 标签 - -``标签与 Java switch 语句的功能一样,用于在众多选项中做出选择。 - -switch 语句中有 case,而``标签中对应有``,switch 语句中有 default,而``标签中有``。 - -### 语法格式 - -```jsp - - - ... - - - ... - - ... - ... - - ... - - -``` - -### 属性 - -- ``标签没有属性。 -- ``标签只有一个属性,在下表中有给出。 -- ``标签没有属性。 - -``标签的属性如下: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | -------- | ------------ | ---------- | -| test | 条件 | 是 | 无 | - -## `` 标签 - -``标签提供了所有``行为标签所具有的功能,同时也允许包含绝对 URL。 - -举例来说,使用``标签可以包含一个 FTP 服务器中不同的网页内容。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| ------------ | --------------------------------------------------------------------- | ------------ | ------------ | -| url | 待导入资源的 URL,可以是相对路径和绝对路径,并且可以导入其他主机资源 | 是 | 无 | -| context | 当使用相对路径访问外部 context 资源时,context 指定了这个资源的名字。 | 否 | 当前应用程序 | -| charEncoding | 所引入的数据的字符编码集 | 否 | ISO-8859-1 | -| var | 用于存储所引入的文本的变量 | 否 | 无 | -| scope | var 属性的作用域 | 否 | page | -| varReader | 可选的用于提供 java.io.Reader 对象的变量 | 否 | 无 | - -## ``, `` 标签 - -[![JSP 标准标签库](http://www.runoob.com/images/up.gif)JSP 标准标签库](http://www.runoob.com/jsp/jsp-jstl.html) - -这些标签封装了 Java 中的 for,while,do-while 循环。 - -相比而言,``标签是更加通用的标签,因为它迭代一个集合中的对象。 - -``标签通过指定分隔符将字符串分隔为一个数组然后迭代它们。 - -### `forEach` 语法格式 - -```jsp - - - ... -``` - -### `forTokens` 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| --------- | ------------------------------------------ | ------------ | ------------ | -| items | 要被循环的信息 | 否 | 无 | -| begin | 开始的元素(0=第一个元素,1=第二个元素) | 否 | 0 | -| end | 最后一个元素(0=第一个元素,1=第二个元素) | 否 | Last element | -| step | 每一次迭代的步长 | 否 | 1 | -| var | 代表当前条目的变量名称 | 否 | 无 | -| varStatus | 代表循环状态的变量名称 | 否 | 无 | - -``标签与``标签有相似的属性,不过``还有另一个属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | -------- | ------------ | ---------- | -| delims | 分隔符 | 是 | 无 | - -## `` 标签 - -``标签用于在``标签中指定参数,而且与 URL 编码相关。 - -在``标签内,name 属性表明参数的名称,value 属性表明参数的值。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | ------------------------ | ------------ | ---------- | -| name | URL 中要设置的参数的名称 | 是 | 无 | -| value | 参数的值 | 否 | Body | - -## `` 标签 - -``标签通过自动重写 URL 来将浏览器重定向至一个新的 URL,它提供内容相关的 URL,并且支持 c:param 标签。 - -### 语法格式 - -``` - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | -------------------------------- | ------------ | ------------ | -| url | 目标 URL | 是 | 无 | -| context | 紧接着一个本地网络应用程序的名称 | 否 | 当前应用程序 | - -## `` 标签 - -``标签将 URL 格式化为一个字符串,然后存储在一个变量中。 - -这个标签在需要的时候会自动重写 URL。 - -var 属性用于存储格式化后的 URL。 - -``标签只是用于调用`response.encodeURL()`方法的一种可选的方法。它真正的优势在于提供了合适的 URL 编码,包括``中指定的参数。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | ---------------------- | ------------ | ------------- | -| value | 基础 URL | 是 | 无 | -| context | 本地网络应用程序的名称 | 否 | 当前应用程序 | -| var | 代表 URL 的变量名 | 否 | Print to page | -| scope | var 属性的作用域 | 否 | Page | diff --git a/docs/javaee/jstl/jstl-format.md b/docs/javaee/jstl/jstl-format.md deleted file mode 100644 index 1f450358..00000000 --- a/docs/javaee/jstl/jstl-format.md +++ /dev/null @@ -1,362 +0,0 @@ ---- -title: JSTL 格式化标签 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- jstl ---- - -JSTL 格式化标签用来格式化并输出文本、日期、时间、数字。引用格式化标签库的语法如下: - -```jsp -<%@ taglib prefix="fmt" - uri="http://java.sun.com/jsp/jstl/fmt" %> -``` - -| 标签 | 描述 | -| ----------------------- | ---------------------------------------- | -| `` | 使用指定的格式或精度格式化数字 | -| `` | 解析一个代表着数字,货币或百分比的字符串 | -| `` | 使用指定的风格或模式格式化日期和时间 | -| `` | 解析一个代表着日期或时间的字符串 | -| `` | 绑定资源 | -| `` | 指定地区 | -| `` | 绑定资源 | -| `` | 指定时区 | -| `` | 指定时区 | -| `` | 显示资源配置文件信息 | -| `` | 设置 request 的字符编码 | - -## ``标签 - -``标签用于格式化数字,百分比,货币。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| ----------------- | ---------------------------------- | ------------ | -------------- | -| value | 要显示的数字 | 是 | 无 | -| type | NUMBER,CURRENCY,或 PERCENT 类型 | 否 | Number | -| pattern | 指定一个自定义的格式化模式用与输出 | 否 | 无 | -| currencyCode | 货币码(当 type="currency"时) | 否 | 取决于默认区域 | -| currencySymbol | 货币符号 (当 type="currency"时) | 否 | 取决于默认区域 | -| groupingUsed | 是否对数字分组 (TRUE 或 FALSE) | 否 | true | -| maxIntegerDigits | 整型数最大的位数 | 否 | 无 | -| minIntegerDigits | 整型数最小的位数 | 否 | 无 | -| maxFractionDigits | 小数点后最大的位数 | 否 | 无 | -| minFractionDigits | 小数点后最小的位数 | 否 | 无 | -| var | 存储格式化数字的变量 | 否 | Print to page | -| scope | var 属性的作用域 | 否 | page | - -如果`type`属性为 percent 或 number,那么您就可以使用其它几个格式化数字属性。 - -`maxIntegerDigits`属性和`minIntegerDigits`属性允许您指定整数的长度。若实际数字超过了`maxIntegerDigits`所指定的最大值,则数字将会被截断。 - -有一些属性允许您指定小数点后的位数。`minFractionalDigits`属性和`maxFractionalDigits`属性允许您指定小数点后的位数。若实际的数字超出了所指定的范围,则这个数字会被截断。 - -数字分组可以用来在每三个数字中插入一个逗号。`groupingIsUsed`属性用来指定是否使用数字分组。当与`minIntegerDigits`属性一同使用时,就必须要很小心地来获取预期的结果了。 - -您或许会使用`pattern`属性。这个属性可以让您在对数字编码时包含指定的字符。接下来的表格中列出了这些字符。 - -| **符号** | **描述** | -| -------- | -------------------------------- | -| 0 | 代表一位数字 | -| E | 使用指数格式 | -| # | 代表一位数字,若没有则显示 0 | -| . | 小数点 | -| , | 数字分组分隔符 | -| ; | 分隔格式 | -| - | 使用默认负数前缀 | -| % | 百分数 | -| ? | 千分数 | -| ¤ | 货币符号,使用实际的货币符号代替 | -| X | 指定可以作为前缀或后缀的字符 | -| ' | 在前缀或后缀中引用特殊字符 | - -## `` 标签 - -``标签用来解析数字,百分数,货币。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| ----------- | ----------------------------------------- | ------------ | ------------- | -| value | 要解析的数字 | 否 | Body | -| type | NUMBER,,CURRENCY,或 PERCENT | 否 | number | -| parseLocale | 解析数字时所用的区域 | 否 | 默认区域 | -| integerOnly | 是否只解析整型数(true)或浮点数(false) | 否 | false | -| pattern | 自定义解析模式 | 否 | 无 | -| timeZone | 要显示的日期的时区 | 否 | 默认时区 | -| var | 存储待解析数字的变量 | 否 | Print to page | -| scope | var 属性的作用域 | 否 | page | - -`pattern`属性与``标签中的`pattern`有相同的作用。在解析时,`pattern`属性告诉解析器期望的格式。 - -## `` 标签 - -``标签用于使用不同的方式格式化日期。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| 属性 | 描述 | 是否必要 | 默认值 | -| --------- | ------------------------------------- | -------- | ---------- | -| value | 要显示的日期 | 是 | 无 | -| type | DATE, TIME, 或 BOTH | 否 | date | -| dateStyle | FULL, LONG, MEDIUM, SHORT, 或 DEFAULT | 否 | default | -| timeStyle | FULL, LONG, MEDIUM, SHORT, 或 DEFAULT | 否 | default | -| pattern | 自定义格式模式 | 否 | 无 | -| timeZone | 显示日期的时区 | 否 | 默认时区 | -| var | 存储格式化日期的变量名 | 否 | 显示在页面 | -| scope | 存储格式化日志变量的范围 | 否 | 页面 | - -`` 标签格式模式 - -| 代码 | 描述 | 实例 | -| ---- | ------------------------------------------------------------------------- | -------------------------- | -| G | 时代标志 | AD | -| y | 不包含纪元的年份。如果不包含纪元的年份小于 10,则显示不具有前导零的年份。 | 2002 | -| M | 月份数字。一位数的月份没有前导零。 | April & 04 | -| d | 月中的某一天。一位数的日期没有前导零。 | 20 | -| h | 12 小时制的小时。一位数的小时数没有前导零。 | 12 | -| H | 24 小时制的小时。一位数的小时数没有前导零。 | 0 | -| m | 分钟。一位数的分钟数没有前导零。 | 45 | -| s | 秒。一位数的秒数没有前导零。 | 52 | -| S | 毫秒 | 970 | -| E | 周几 | Tuesday | -| D | 一年中的第几天 | 180 | -| F | 一个月中的第几个周几 | 2 (一个月中的第二个星期三) | -| w | 一年中的第几周 r | 27 | -| W | 一个月中的第几周 | 2 | -| a | a.m./p.m. 指示符 | PM | -| k | 小时(12 小时制的小时) | 24 | -| K | 小时(24 小时制的小时) | 0 | -| z | 时区 | 中部标准时间 | -| ' | | 转义文本 | -| '' | | 单引号 | - -## `` 标签 - -`` 标签用于解析日期。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| 属性 | 描述 | 是否必要 | 默认值 | -| --------- | ------------------------------------- | -------- | ---------- | -| value | 要显示的日期 | 是 | 无 | -| type | DATE, TIME, 或 BOTH | 否 | date | -| dateStyle | FULL, LONG, MEDIUM, SHORT, 或 DEFAULT | 否 | default | -| timeStyle | FULL, LONG, MEDIUM, SHORT, 或 DEFAULT | 否 | default | -| pattern | 自定义格式模式 | 否 | 无 | -| timeZone | 显示日期的时区 | 否 | 默认时区 | -| var | 存储格式化日期的变量名 | 否 | 显示在页面 | -| scope | 存储格式化日志变量的范围 | 否 | 页面 | - -属性设置我们需要的输出的时间格式。 - -## `` 标签 - -``标签将指定的资源束对出现在``标签中的``标签可用。这可以使您省去为每个``标签指定资源束的很多步骤。 - -举例来说,下面的两个``块将产生同样的输出: - -```jsp - - - - - - - -``` - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | -------------------------------------- | ------------ | ---------- | -| basename | 指定被载入的资源束的基础名称 | 是 | 无 | -| prefix | 指定``标签 key 属性的前缀 | 否 | 无 | - -## `` 标签 - -``标签用来将给定的区域存储在 locale 配置变量中。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | ------------------------------------- | ------------ | ---------- | -| value | 指定 ISO-639 语言码和 ISO-3166 国家码 | 是 | en_US | -| variant | 特定浏览器变体 | 否 | 无 | -| scope | Locale 配置变量的作用域 | 否 | Page | - -## `` 标签 - -``标签用来指定时区,供其它标签使用。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | -------- | ------------ | ---------- | -| value | 时区 | 是 | 无 | - -## `` 标签 - -``标签用来复制一个时区对象至指定的作用域。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | ------------------ | ------------ | --------------- | -| value | 时区 | 是 | 无 | -| var | 存储新时区的变量名 | 否 | Replace default | -| scope | 变量的作用域 | 否 | Page | - -## `` 标签 - -``标签映射一个关键字给局部消息,然后执行参数替换。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | -------------------- | ------------ | ------------- | -| key | 要检索的消息关键字 | 否 | Body | -| bundle | 要使用的资源束 | 否 | 默认资源束 | -| var | 存储局部消息的变量名 | 否 | Print to page | -| scope | var 属性的作用域 | 否 | Page | - -## `` 标签 - -``标签用来指定返回给 Web 应用程序的表单编码类型。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | --------------------------------------- | ------------ | ---------- | -| key | 字符编码集的名称,用于解码 request 参数 | 是 | 无 | - -使用``标签来指定字符集,用于解码来自表单的数据。在字符集不是 ISO-8859-1 时必须使用这个标签。由于大多数浏览器在它们的请求中不包含 Content-Type 头,所以需要这个标签。 - -``标签的目的就是用来指定请求的 Content-Type。您必须指定一个 Content-Type,就算 response 是通过 Page 指令的 contentType 属性来编码。这是因为 response 的实际区域可能与 Page 指令所指定的不同。 - -如果页面包含 I18N-capable 格式行为用于设置 response 的 locale 属性(通过调用 ServletResponse.setLocale()方法),任何在页面中指定的编码集将会被覆盖。 diff --git a/docs/javaee/jstl/jstl-function.md b/docs/javaee/jstl/jstl-function.md deleted file mode 100644 index 1675bc1b..00000000 --- a/docs/javaee/jstl/jstl-function.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -title: JSTL 函数 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- jstl ---- - -JSTL 包含一系列标准函数,大部分是通用的字符串处理函数。引用 JSTL 函数库的语法如下: - -```jsp -<%@ taglib prefix="fn" - uri="http://java.sun.com/jsp/jstl/functions" %> -``` - -| 函数 | 描述 | -| ------------------------------------------------------------------------------------------ | -------------------------------------------------------- | -| [fn:contains()](http://www.runoob.com/jsp/jstl-function-contains.html) | 测试输入的字符串是否包含指定的子串 | -| [fn:containsIgnoreCase()](http://www.runoob.com/jsp/jstl-function-containsignoreCase.html) | 测试输入的字符串是否包含指定的子串,大小写不敏感 | -| [fn:endsWith()](http://www.runoob.com/jsp/jstl-function-endswith.html) | 测试输入的字符串是否以指定的后缀结尾 | -| [fn:escapeXml()](http://www.runoob.com/jsp/jstl-function-escapexml.html) | 跳过可以作为 XML 标记的字符 | -| [fn:indexOf()](http://www.runoob.com/jsp/jstl-function-indexof.html) | 返回指定字符串在输入字符串中出现的位置 | -| [fn:join()](http://www.runoob.com/jsp/jstl-function-join.html) | 将数组中的元素合成一个字符串然后输出 | -| [fn:length()](http://www.runoob.com/jsp/jstl-function-length.html) | 返回字符串长度 | -| [fn:replace()](http://www.runoob.com/jsp/jstl-function-replace.html) | 将输入字符串中指定的位置替换为指定的字符串然后返回 | -| [fn:split()](http://www.runoob.com/jsp/jstl-function-split.html) | 将字符串用指定的分隔符分隔然后组成一个子字符串数组并返回 | -| [fn:startsWith()](http://www.runoob.com/jsp/jstl-function-startswith.html) | 测试输入字符串是否以指定的前缀开始 | -| [fn:substring()](http://www.runoob.com/jsp/jstl-function-substring.html) | 返回字符串的子集 | -| [fn:substringAfter()](http://www.runoob.com/jsp/jstl-function-substringafter.html) | 返回字符串在指定子串之后的子集 | -| [fn:substringBefore()](http://www.runoob.com/jsp/jstl-function-substringbefore.html) | 返回字符串在指定子串之前的子集 | -| [fn:toLowerCase()](http://www.runoob.com/jsp/jstl-function-tolowercase.html) | 将字符串中的字符转为小写 | -| [fn:toUpperCase()](http://www.runoob.com/jsp/jstl-function-touppercase.html) | 将字符串中的字符转为大写 | -| [fn:trim()](http://www.runoob.com/jsp/jstl-function-trim.html) | 移除首位的空白符 | diff --git a/docs/javaee/jstl/jstl-sql.md b/docs/javaee/jstl/jstl-sql.md deleted file mode 100644 index 928f23c6..00000000 --- a/docs/javaee/jstl/jstl-sql.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -title: JSTL SQL标签 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- jstl ---- - -JSTL SQL 标签库提供了与关系型数据库(Oracle,MySQL,SQL Server 等等)进行交互的标签。引用 SQL 标签库的语法如下: - -```jsp -<%@ taglib prefix="sql" - uri="http://java.sun.com/jsp/jstl/sql" %> -``` - -| 标签 | 描述 | -| --------------------- | ---------------------------------------------------------------------------- | -| `` | 指定数据源 | -| `` | 运行 SQL 查询语句 | -| `` | 运行 SQL 更新语句 | -| `` | 将 SQL 语句中的参数设为指定值 | -| `` | 将 SQL 语句中的日期参数设为指定的 java.util.Date  对象值 | -| `` | 在共享数据库连接中提供嵌套的数据库行为元素,将所有语句以一个事务的形式来运行 | - -## `` 标签 - -``标签用来配置数据源或者将数据源信息存储在某作用域的变量中,用来作为其它 JSTL 数据库操作的数据源。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| ---------- | --------------------- | ------------ | ---------- | -| driver | 要注册的 JDBC 驱动 | 否 | 无 | -| url | 数据库连接的 JDBC URL | 否 | 无 | -| user | 数据库用户名 | 否 | 无 | -| password | 数据库密码 | 否 | 无 | -| dataSource | 事先准备好的数据库 | 否 | 无 | -| var | 代表数据库的变量 | 否 | 默认设置 | -| scope | var 属性的作用域 | 否 | Page | - -## `` 标签 - -``标签用来执行一个没有返回值的 SQL 语句,比如 SQL INSERT,UPDATE,DELETE 语句。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| ---------- | -------------------------------------------- | ------------ | ---------- | -| sql | 需要执行的 SQL 命令(不返回 ResultSet 对象) | 否 | Body | -| dataSource | 所使用的数据库连接(覆盖默认值) | 否 | 默认数据库 | -| var | 用来存储所影响行数的变量 | 否 | 无 | -| scope | var 属性的作用域 | 否 | Page | - -## `` 标签 - -``标签与``标签和``标签嵌套使用,用来提供一个值占位符。如果是一个 null 值,则将占位符设为 SQL NULL。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | ---------------- | ------------ | ---------- | -| value | 需要设置的参数值 | 否 | Body | - -## `` 标签 - -``标签与``标签和``标签嵌套使用,用来提供日期和时间的占位符。如果是一个 null 值,则将占位符设为 SQL NULL。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | -------------------------------------------------------------- | ------------ | ---------- | -| value | 需要设置的日期参数(java.util.Date) | 否 | Body | -| type | DATE (只有日期),TIME(只有时间), TIMESTAMP (日期和时间) | 否 | TIMESTAMP | - -## `` 标签 - -``标签用来将``标签和``标签封装至事务中。可以将大量的``和``操作装入``中,使它们成为单一的事务。 - -它确保对数据库的修改不是被提交就是被回滚。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| ---------- | ------------------------------------------------------------------------------------ | ------------ | ---------- | -| dataSource | 所使用的数据库(覆盖默认值) | 否 | 默认数据库 | -| isolation | 事务隔离等级 (READ_COMMITTED,,READ_UNCOMMITTED, REPEATABLE_READ 或 SERIALIZABLE) | 否 | 数据库默认 | diff --git a/docs/javaee/jstl/jstl-xml.md b/docs/javaee/jstl/jstl-xml.md deleted file mode 100644 index 3572e27c..00000000 --- a/docs/javaee/jstl/jstl-xml.md +++ /dev/null @@ -1,246 +0,0 @@ ---- -title: JSTL XML标签 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- jstl ---- - -JSTL XML 标签库提供了创建和操作 XML 文档的标签。引用 XML 标签库的语法如下: - -```jsp -<%@ taglib prefix="x" - uri="http://java.sun.com/jsp/jstl/xml" %> -``` - -在使用 xml 标签前,你必须将 XML 和 XPath 的相关包拷贝至你的`\lib`下: - -- **XercesImpl.jar**下载地址: [http://www.apache.org/dist/xerces/j/](http://www.apache.org/dist/xerces/j/) - -- **xalan.jar**下载地址: [http://xml.apache.org/xalan-j/index.html](http://xml.apache.org/xalan-j/index.html) - -| 标签 | 描述 | -| --------------- | ----------------------------------------------------------- | -| `` | 与`<%= ... >`,类似,不过只用于 XPath 表达式 | -| `` | 解析 XML 数据 | -| `` | 设置 XPath 表达式 | -| `` | 判断 XPath 表达式,若为真,则执行本体中的内容,否则跳过本体 | -| `` | 迭代 XML 文档中的节点 | -| `` | ``和``的父标签 | -| `` | ``的子标签,用来进行条件判断 | -| `` | ``的子标签,当``判断为 false 时被执行 | -| `` | 将 XSL 转换应用在 XML 文档中 | -| `` | 与``共同使用,用于设置 XSL 样式表 | - -## `` 标签 - -``标签显示 XPath 表达式的结果,与`<%= %>`功能相似。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| --------- | -------------------------------------------- | ------------ | ---------- | -| select | 需要计算的 XPath 表达式,通常使用 XPath 变量 | 是 | 无 | -| escapeXml | 是否忽略 XML 特殊字符 | 否 | true | - -## `` 标签 - -``标签用来解析属性中或标签主体中的 XML 数据。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | -------------------------------------------- | ------------ | ---------- | -| var | 包含已解析 XML 数据的变量 | 否 | 无 | -| xml | 需要解析的文档的文本内容(String 或 Reader) | 否 | Body | -| systemId | 系统标识符 URI,用来解析文档 | 否 | 无 | -| filter | 应用于源文档的过滤器 | 否 | 无 | -| doc | 需要解析的 XML 文档 | 否 | Page | -| scope | var 属性的作用域 | 否 | Page | -| varDom | 包含已解析 XML 数据的变量 | 否 | Page | -| scopeDom | varDom 属性的作用域 | 否 | Page | - -## `` 标签 - -``标签为 XPath 表达式的值设置一个变量。 - -如果 XPath 表达式的值是 boolean 类型,则``将会设置一个 java.lang.Boolean 对象,若是字符串,则设置一个 java.lang.String 对象,若是数字,则设置一个 java.lang.Number 对象。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | ------------------------- | ------------ | ---------- | -| var | 代表 XPath 表达式值得变量 | 是 | Body | -| select | 需要计算的 XPath 表达式 | 否 | 无 | -| scope | var 属性的作用域 | 否 | Page | - -## `` 标签 - -``标签用于判断一个 XPath 表达式的值,若为真,则执行其主体中的内容,若为假则其主体的内容将会被忽略。 - -### 语法格式 - -```jsp - - ... - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | ----------------------- | ------------ | ---------- | -| select | 需要计算的 XPath 表达式 | 是 | 无 | -| var | 存储条件结果的变量 | 否 | 无 | -| scope | var 属性的作用域 | 否 | Page | - -## `` 标签 - -``标签用来循环遍历 XML 文档的节点。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| --------- | ---------------------------- | ------------ | ---------- | -| select | 需要计算的 XPath 表达式 | 是 | 无 | -| var | 用于存储当前项目的变量 | 否 | 无 | -| begin | 迭代器的开始索引 | 否 | 无 | -| end | 迭代器的结束索引 | 否 | 无 | -| step | 迭代的步长 | 否 | 无 | -| varStatus | 代表迭代器所存储的状态的变量 | 否 | 无 | - -## ``, ``, `` 标签 - -``标签与 Java switch 语句有相同的功能。switch 语句有 case 语句,而``标签有``标签。switch 语句有 default 语句,而``标签有``标签。 - -### 语法格式 - -```jsp - - - ... - - - ... - - ... - ... - - ... - -` -``` - -### 属性 - -- ``没有属性。 -- ``的属性在下表中给出。 -- ``没有属性。 - -``标签的属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | -------- | ------------ | ---------- | -| select | 条件 | 是 | 无 | - -## `` 标签 - -``标签在 XML 文档中应用 XSL。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| ------------ | --------------------------- | ------------ | ------------- | -| doc | 源 XML 文档 | 否 | Body | -| docSystemId | 源 XML 文档的 URI | 否 | 无 | -| xslt | XSLT 样式表 | 是 | 无 | -| xsltSystemId | 源 XSLT 文档的 URI | 否 | 无 | -| result | 接收转换结果的对象 | 否 | Print to page | -| var | 代表被转换的 XML 文档的变量 | 否 | Print to page | -| scope | var 属性的作用域 | 否 | 无 | - -## `` 标签 - -``标签与``标签一同使用,用于设置 XSLT 样式表的参数。 - -### 语法格式 - -```jsp - -``` - -### 属性 - -``标签有如下属性: - -| **属性** | **描述** | **是否必要** | **默认值** | -| -------- | --------------- | ------------ | ---------- | -| name | XSLT 参数的名称 | 是 | Body | -| value | XSLT 参数的值 | 否 | 无 | diff --git a/docs/javaee/listener/listener.md b/docs/javaee/listener/listener.md deleted file mode 100644 index 36c702ad..00000000 --- a/docs/javaee/listener/listener.md +++ /dev/null @@ -1,130 +0,0 @@ ---- -title: JavaEE 监听器 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- listener ---- - -监听器是一个专门用于对其他对象身上发生的事件或状态改变进行监听和相应处理的对象,当被监视的对象发生情况时,立即采取相应的行动。监听器其实就是一个实现特定接口的普通 java 程序,这个程序专门用于监听另一个 java 对象的方法调用或属性改变,当被监听对象发生上述事件后,监听器某个方法立即被执行。 - -## 概述 - -JavaWeb 中的监听器是 Servlet 规范中定义的一种特殊类,它用于监听 web 应用程序中的`ServletContext`, `HttpSession`和 `ServletRequest`等域对象的创建与销毁事件,以及监听这些域对象中的属性发生修改的事件。 - -## 监听器的分类 - -在 Servlet 规范中定义了多种类型的监听器,它们用于监听的事件源分别为`ServletContext`,`HttpSession`和`ServletRequest`这三个域对象 -Servlet 规范针对这三个对象上的操作,又把多种类型的监听器划分为三种类型: - -1. 监听域对象自身的创建和销毁的事件监听器。 -2. 监听域对象中的属性的增加和删除的事件监听器。 -3. 监听绑定到 HttpSession 域中的某个对象的状态的事件监听器。 - -### 监听对象的创建和销毁 - -#### HttpSessionListener - -`HttpSessionListener` 接口用于监听`HttpSession`对象的创建和销毁 - -创建一个`Session`时,激发`sessionCreated (HttpSessionEvent se)` 方法 - -销毁一个`Session`时,激发`sessionDestroyed (HttpSessionEvent se)` 方法。 - -#### ServletContextListener - -`ServletContextListener`接口用于监听`ServletContext`对象的创建和销毁事件。 - -实现了`ServletContextListener`接口的类都可以对`ServletContext`对象的创建和销毁进行监听。 - -当`ServletContext`对象被创建时,激发`contextInitialized (ServletContextEvent sce)`方法。 - -当`ServletContext`对象被销毁时,激发`contextDestroyed(ServletContextEvent sce)`方法。 - -`ServletContext`域对象创建和销毁时机: - -- 创建:服务器启动针对每一个 Web 应用创建`ServletContext` -- 销毁:服务器关闭前先关闭代表每一个 web 应用的`ServletContext` - -#### ServletRequestListener - -`ServletRequestListener`接口用于监听`ServletRequest` 对象的创建和销毁 - -`Request`对象被创建时,监听器的`requestInitialized(ServletRequestEvent sre)`方法将会被调用 - -`Request`对象被销毁时,监听器的`requestDestroyed(ServletRequestEvent sre)`方法将会被调用 - -`ServletRequest`域对象创建和销毁时机: - -创建:用户每一次访问都会创建 request 对象 - -销毁:当前访问结束,request 对象就会销毁 - -### 监听对象的属性变化 - -域对象中属性的变更的事件监听器就是用来监听 ServletContext, HttpSession, HttpServletRequest 这三个对象中的属性变更信息事件的监听器。 -这三个监听器接口分别是 ServletContextAttributeListener, HttpSessionAttributeListener 和 ServletRequestAttributeListener,这三个接口中都定义了三个方法来处理被监听对象中的属性的增加,删除和替换的事件,同一个事件在这三个接口中对应的方法名称完全相同,只是接受的参数类型不同。 - -#### attributeAdded 方法 - -当向被监听对象中增加一个属性时,web 容器就调用事件监听器的`attributeAdded`方法进行响应,这个方法接收一个事件类型的参数,监听器可以通过这个参数来获得正在增加属性的域对象和被保存到域中的属性对象 -各个域属性监听器中的完整语法定义为: - -```java -public void attributeAdded(ServletContextAttributeEvent scae) -public void attributeReplaced(HttpSessionBindingEvent hsbe) -public void attributeRmoved(ServletRequestAttributeEvent srae) -``` - -#### attributeRemoved 方法 - -当删除被监听对象中的一个属性时,web 容器调用事件监听器的`attributeRemoved`方法进行响应 -各个域属性监听器中的完整语法定义为: - -```java -public void attributeRemoved(ServletContextAttributeEvent scae) -public void attributeRemoved (HttpSessionBindingEvent hsbe) -public void attributeRemoved (ServletRequestAttributeEvent srae) -``` - -#### attributeReplaced 方法 - -当监听器的域对象中的某个属性被替换时,web 容器调用事件监听器的`attributeReplaced`方法进行响应 -各个域属性监听器中的完整语法定义为: - -```java -public void attributeReplaced(ServletContextAttributeEvent scae) -public void attributeReplaced (HttpSessionBindingEvent hsbe) -public void attributeReplaced (ServletRequestAttributeEvent srae) -``` - -### 监听 Session 内的对象 - -保存在 Session 域中的对象可以有多种状态: - -- 绑定(session.setAttribute("bean",Object))到 Session 中; -- 从 Session 域中解除(session.removeAttribute("bean"))绑定; -- 随 Session 对象持久化到一个存储设备中; -- 随 Session 对象从一个存储设备中恢复 - -Servlet 规范中定义了两个特殊的监听器接口`HttpSessionBindingListener`和`HttpSessionActivationListener`来帮助 JavaBean 对象了解自己在 Session 域中的这些状态。 - -实现这两个接口的类不需要 web.xml 文件中进行注册。 - -#### HttpSessionBindingListener - -实现了`HttpSessionBindingListener`接口的 JavaBean 对象可以感知自己被绑定到 Session 中和 Session 中删除的事件。 - -- 当对象被绑定到`HttpSession`对象中时,web 服务器调用该对象的`valueBound(HttpSessionBindingEvent event)`方法。 - -- 当对象从`HttpSession`对象中解除绑定时,web 服务器调用该对象的`valueUnbound(HttpSessionBindingEvent event)`方法。 - -#### HttpSessionActivationListener - -实现了`HttpSessionActivationListener`接口的 JavaBean 对象可以感知自己被活化(反序列化)和钝化(序列化)的事件。 - -当绑定到 HttpSession 对象中的 javabean 对象将要随 HttpSession 对象被钝化(序列化)之前,web 服务器调用该 javabean 对象的`sessionWillPassivate(HttpSessionEvent event)` 方法。这样 javabean 对象就可以知道自己将要和 HttpSession 对象一起被序列化(钝化)到硬盘中. - -当绑定到 HttpSession 对象中的 javabean 对象将要随 HttpSession 对象被活化(反序列化)之后,web 服务器调用该 javabean 对象的`sessionDidActive(HttpSessionEvent event)`方法。这样 javabean 对象就可以知道自己将要和 HttpSession 对象一起被反序列化(活化)回到内存中 diff --git a/docs/javaee/servlet/servlet-and-status.md b/docs/javaee/servlet/servlet-and-status.md deleted file mode 100644 index 4e706a5f..00000000 --- a/docs/javaee/servlet/servlet-and-status.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -title: JavaEE Servlet HTTP 状态码 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- servlet -- http ---- - -## HTTP 状态码 - -HTTP 请求和 HTTP 响应消息的格式是类似的,结构如下: - -- 初始状态行 + 回车换行符(回车+换行) -- 零个或多个标题行+回车换行符 -- 一个空白行,即回车换行符 -- 一个可选的消息主体,比如文件、查询数据或查询输出 - -例如,服务器的响应头如下所示: - -``` -HTTP/1.1 200 OK -Content-Type: text/html -Header2: ... -... -HeaderN: ... - (Blank Line) - - -... - -... - - -``` - -状态行包括 HTTP 版本(在本例中为 HTTP/1.1)、一个状态码(在本例中为 200)和一个对应于状态码的短消息(在本例中为 OK)。 - -以下是可能从 Web 服务器返回的 HTTP 状态码和相关的信息列表: - -| 代码 | 消息 | 描述 | -| ---- | ----------------------------- | ------------------------------------------------------------------------------------------------------ | -| 100 | Continue | 只有请求的一部分已经被服务器接收,但只要它没有被拒绝,客户端应继续该请求。 | -| 101 | Switching Protocols | 服务器切换协议。 | -| 200 | OK | 请求成功。 | -| 201 | Created | 该请求是完整的,并创建一个新的资源。 | -| 202 | Accepted | 该请求被接受处理,但是该处理是不完整的。 | -| 203 | Non-authoritative Information | | -| 204 | No Content | | -| 205 | Reset Content | | -| 206 | Partial Content | | -| 300 | Multiple Choices | 链接列表。用户可以选择一个链接,进入到该位置。最多五个地址。 | -| 301 | Moved Permanently | 所请求的页面已经转移到一个新的 URL。 | -| 302 | Found | 所请求的页面已经临时转移到一个新的 URL。 | -| 303 | See Other | 所请求的页面可以在另一个不同的 URL 下被找到。 | -| 304 | Not Modified | | -| 305 | Use Proxy | | -| 306 | _Unused_ | 在以前的版本中使用该代码。现在已不再使用它,但代码仍被保留。 | -| 307 | Temporary Redirect | 所请求的页面已经临时转移到一个新的 URL。 | -| 400 | Bad Request | 服务器不理解请求。 | -| 401 | Unauthorized | 所请求的页面需要用户名和密码。 | -| 402 | Payment Required | _您还不能使用该代码。_ | -| 403 | Forbidden | 禁止访问所请求的页面。 | -| 404 | Not Found | 服务器无法找到所请求的页面。. | -| 405 | Method Not Allowed | 在请求中指定的方法是不允许的。 | -| 406 | Not Acceptable | 服务器只生成一个不被客户端接受的响应。 | -| 407 | Proxy Authentication Required | 在请求送达之前,您必须使用代理服务器的验证。 | -| 408 | Request Timeout | 请求需要的时间比服务器能够等待的时间长,超时。 | -| 409 | Conflict | 请求因为冲突无法完成。 | -| 410 | Gone | 所请求的页面不再可用。 | -| 411 | Length Required | "Content-Length" 未定义。服务器无法处理客户端发送的不带 Content-Length 的请求信息。 | -| 412 | Precondition Failed | 请求中给出的先决条件被服务器评估为 false。 | -| 413 | Request Entity Too Large | 服务器不接受该请求,因为请求实体过大。 | -| 414 | Request-url Too Long | 服务器不接受该请求,因为 URL 太长。当您转换一个 "post" 请求为一个带有长的查询信息的 "get" 请求时发生。 | -| 415 | Unsupported Media Type | 服务器不接受该请求,因为媒体类型不被支持。 | -| 417 | Expectation Failed | | -| 500 | Internal Server Error | 未完成的请求。服务器遇到了一个意外的情况。 | -| 501 | Not Implemented | 未完成的请求。服务器不支持所需的功能。 | -| 502 | Bad Gateway | 未完成的请求。服务器从上游服务器收到无效响应。 | -| 503 | Service Unavailable | 未完成的请求。服务器暂时超载或死机。 | -| 504 | Gateway Timeout | 网关超时。 | -| 505 | HTTP Version Not Supported | 服务器不支持"HTTP 协议"版本。 | - -## 设置 HTTP 状态码的方法 - -下面的方法可用于在 Servlet 程序中设置 HTTP 状态码。这些方法通过  `HttpServletResponse`  对象可用。 - -| 序号 | 方法 & 描述 | -| ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 1 | **public void setStatus ( int statusCode )**该方法设置一个任意的状态码。setStatus 方法接受一个 int(状态码)作为参数。如果您的反应包含了一个特殊的状态码和文档,请确保在使用  *PrintWriter*  实际返回任何内容之前调用 setStatus。 | -| 2 | **public void sendRedirect(String url)**该方法生成一个 302 响应,连同一个带有新文档 URL 的  *Location*  头。 | -| 3 | **public void sendError(int code, String message)**该方法发送一个状态码(通常为 404),连同一个在 HTML 文档内部自动格式化并发送到客户端的短消息。 | - -## HTTP 状态码实例 - -下面的例子把 407 错误代码发送到客户端浏览器,浏览器会显示 "Need authentication!!!" 消息。 - -```java -// 导入必需的 java 库 -import java.io.*; -import javax.servlet.*; -import javax.servlet.http.*; -import java.util.*; - -// 扩展 HttpServlet 类 -public class showError extends HttpServlet { - - // 处理 GET 方法请求的方法 - public void doGet(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException - { - // 设置错误代码和原因 - response.sendError(407, "Need authentication!!!" ); - } - // 处理 POST 方法请求的方法 - public void doPost(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException { - doGet(request, response); - } -} -``` - -现在,调用上面的 Servlet 将显示以下结果: - -``` -HTTP Status 407 - Need authentication!!! -type Status report -message Need authentication!!! -description The client must first authenticate itself with the proxy (Need authentication!!!). -Apache Tomcat/5.5.29 -``` diff --git a/docs/javaee/servlet/servlet-demo.md b/docs/javaee/servlet/servlet-demo.md deleted file mode 100644 index 25f29464..00000000 --- a/docs/javaee/servlet/servlet-demo.md +++ /dev/null @@ -1,87 +0,0 @@ ---- -title: JavaEE Servlet 示例 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- servlet ---- - -## 编写 Servlet - -### 部署 Servlet - -### web.xml 是什么 - -### servlet 元素 - -``元素用于注册 Servlet。 - -``包含有两个主要的子元素:``和``,分别用于设置 Servlet 的注册名称和 Servlet 的完整类名。 - -如果在``元素中配置了一个``元素,那么 WEB 应用程序在启动时,就会装载并创建 Servlet 的实例对象、以及调用 Servlet 实例对象的 init()方法。 - -```xml - - HelloServlet - org.zp.notes.javaee.servlet.HelloServlet - 1 - -``` - -### servlet-mapping 元素 - -``元素用于映射一个已注册的 Servlet 的一个对外访问路径。 - -``包含有两个子元素:``和``,分别用于指定 Servlet 的注册名称和 Servlet 的对外访问路径。 - -**注:同一个 Servlet 可以被映射到多个 URL 上。**例: - -```xml - - HelloServlet - org.zp.notes.javaee.servlet.HelloServlet - - - HelloServlet - /servlet/HelloServlet - /servlet/HelloServlet.asp - /servlet/HelloServlet.jsp - /servlet/HelloServlet.php - /servlet/HelloServlet.aspx - -``` - -### url-pattern 的通配符 - -`/` - -如果某个 Servlet 的映射路径仅仅为一个正斜杠`/`,那么这个 Servlet 就成为当前 Web 应用程序的缺省 Servlet。  凡是在`web.xml`文件中找不到匹配的``元素的 URL,它们的访问请求都将交给缺省 Servlet 处理,也就是说,缺省 Servlet 用于处理所有其他 Servlet 都不处理的访问请求。 - -`*` - -`*`可以匹配任意的字符。 - -### init-param 元素和 context-param 元素 - -``标签用于为当前 servlet 配置一些初始化参数。 - -``标签用于配置全局的初始化参数,所有 servlet 都可以使用。 - -```xml - - - - - - datasource - jdbc:mysql://localhost:3306/test - - - - index.jsp - - -``` diff --git a/docs/javaee/servlet/servlet-introduction.md b/docs/javaee/servlet/servlet-introduction.md deleted file mode 100644 index 10b23e0c..00000000 --- a/docs/javaee/servlet/servlet-introduction.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -title: JavaEE Servlet 简介 -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- servlet ---- - -## Servlet 是什么? - -Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。 - -使用 Servlet,您可以收集来自网页表单的用户输入,呈现来自数据库或者其他源的记录,还可以动态创建网页。 - -Java Servlet 通常情况下与使用 CGI(Common Gateway Interface,公共网关接口)实现的程序可以达到异曲同工的效果。但是相比于 CGI,Servlet 有以下几点优势: - -- 性能明显更好。 -- Servlet 在 Web 服务器的地址空间内执行。这样它就没有必要再创建一个单独的进程来处理每个客户端请求。 -- Servlet 是独立于平台的,因为它们是用 Java 编写的。 -- 服务器上的 Java 安全管理器执行了一系列限制,以保护服务器计算机上的资源。因此,Servlet 是可信的。 -- Java 类库的全部功能对 Servlet 来说都是可用的。它可以通过 sockets 和 RMI 机制与 applets、数据库或其他软件进行交互。 - -## Servlet 架构 - -下图显示了 Servlet 在 Web 应用程序中的位置。 - -![Servlet 架构](http://www.runoob.com/wp-content/uploads/2014/07/servlet-arch.jpg) - -## Servlet 任务 - -Servlet 执行以下主要任务: - -- 读取客户端(浏览器)发送的显式的数据。这包括网页上的 HTML 表单,或者也可以是来自 applet 或自定义的 HTTP 客户端程序的表单。 -- 读取客户端(浏览器)发送的隐式的 HTTP 请求数据。这包括 cookies、媒体类型和浏览器能理解的压缩格式等等。 -- 处理数据并生成结果。这个过程可能需要访问数据库,执行 RMI 或 CORBA 调用,调用 Web 服务,或者直接计算得出对应的响应。 -- 发送显式的数据(即文档)到客户端(浏览器)。该文档的格式可以是多种多样的,包括文本文件(HTML 或 XML)、二进制文件(GIF 图像)、Excel 等。 -- 发送隐式的 HTTP 响应到客户端(浏览器)。这包括告诉浏览器或其他客户端被返回的文档类型(例如 HTML),设置 cookies 和缓存参数,以及其他类似的任务。 - -## Servlet 包 - -Java Servlet 是运行在带有支持 Java Servlet 规范的解释器的 web 服务器上的 Java 类。 - -Servlet 可以使用  **javax.servlet**  和  **javax.servlet.http**  包创建,它是 Java 企业版的标准组成部分,Java 企业版是支持大型开发项目的 Java 类库的扩展版本。 - -这些类实现 Java Servlet 和 JSP 规范。在写本教程的时候,二者相应的版本分别是 Java Servlet 2.5 和 JSP 2.1。 - -Java Servlet 就像任何其他的 Java 类一样已经被创建和编译。在您安装 Servlet 包并把它们添加到您的计算机上的 Classpath 类路径中之后,您就可以通过 JDK 的 Java 编译器或任何其他编译器来编译 Servlet。 - -# Servlet  生命周期 - -Servlet 生命周期可被定义为从创建直到毁灭的整个过程。以下是 Servlet 遵循的过程: - -- Servlet 通过调用  **init ()**  方法进行初始化。 -- Servlet 调用  **service()**  方法来处理客户端的请求。 -- Servlet 通过调用  **destroy()**  方法终止(结束)。 -- 最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。 - -现在让我们详细讨论生命周期的方法。 - -## init() 方法 - -init 方法被设计成只调用一次。它在第一次创建 Servlet 时被调用,在后续每次用户请求时不再调用。因此,它是用于一次性初始化,就像 Applet 的 init 方法一样。 - -Servlet 创建于用户第一次调用对应于该 Servlet 的 URL 时,但是您也可以指定 Servlet 在服务器第一次启动时被加载。 - -当用户调用一个 Servlet 时,就会创建一个 Servlet 实例,每一个用户请求都会产生一个新的线程,适当的时候移交给 doGet 或 doPost 方法。init() 方法简单地创建或加载一些数据,这些数据将被用于 Servlet 的整个生命周期。 - -init 方法的定义如下: - -```java -public void init() throws ServletException { - // 初始化代码... -} -``` - -## service() 方法 - -service() 方法是执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调用 service() 方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。 - -每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用服务。service() 方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用 doGet、doPost、doPut,doDelete 等方法。 - -下面是该方法的特征: - -```java -public void service(ServletRequest request, - ServletResponse response) - throws ServletException, IOException{ -} -``` - -service() 方法由容器调用,service 方法在适当的时候调用 doGet、doPost、doPut、doDelete 等方法。所以,您不用对 service() 方法做任何动作,您只需要根据来自客户端的请求类型来重写 doGet() 或 doPost() 即可。 - -doGet() 和 doPost() 方法是每次服务请求中最常用的方法。下面是这两种方法的特征。 - -## doGet() 方法 - -GET 请求来自于一个 URL 的正常请求,或者来自于一个未指定 METHOD 的 HTML 表单,它由 doGet() 方法处理。 - -```java -public void doGet(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException { - // Servlet 代码 -} -``` - -## doPost() 方法 - -POST 请求来自于一个特别指定了 METHOD 为 POST 的 HTML 表单,它由 doPost() 方法处理。 - -```java -public void doPost(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException { - // Servlet 代码 -} -``` - -## destroy() 方法 - -destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。 - -在调用 destroy() 方法之后,servlet 对象被标记为垃圾回收。destroy 方法定义如下所示: - -```java - public void destroy() { - // 终止化代码... - } -``` - -## 架构图 - -下图显示了一个典型的 Servlet 生命周期方案。 - -- 第一个到达服务器的 HTTP 请求被委派到 Servlet 容器。 -- Servlet 容器在调用 service() 方法之前加载 Servlet。 -- 然后 Servlet 容器处理由多个线程产生的多个请求,每个线程执行一个单一的 Servlet 实例的 service() 方法。 - -![Servlet 生命周期](http://www.runoob.com/wp-content/uploads/2014/07/Servlet-LifeCycle.jpg) diff --git a/docs/javaee/session/cookie-and-sesion.md b/docs/javaee/session/cookie-and-sesion.md deleted file mode 100644 index cf7abfd6..00000000 --- a/docs/javaee/session/cookie-and-sesion.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: JavaEE Cookie vs. Session -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- cookie -- session ---- - -## 存取方式 - -Cookie 只能保存`ASCII`字符串,如果需要存取 Unicode 字符或二进制数据,需要进行`UTF-8`、`GBK`或`BASE64`等方式的编码。 - -Session 可以存取任何类型的数据,甚至是任何 Java 类。可以将 Session 看成是一个 Java 容器类。 - -## 隐私安全 - -Cookie 存于客户端浏览器,一些客户端的程序可能会窥探、复制或修改 Cookie 内容。 - -Session 存于服务器,对客户端是透明的,不存在敏感信息泄露的危险。 - -## 有效期 - -使用 Cookie 可以保证长时间登录有效,只要设置 Cookie 的`maxAge`属性为一个很大的数字。 - -而 Session 虽然理论上也可以通过设置很大的数值来保持长时间登录有效,但是,由于 Session 依赖于名为`JESSIONID`的 Cookie,而 Cookie `JESSIONID`的`maxAge`默认为-1,只要关闭了浏览器该 Session 就会失效,因此,Session 不能实现信息永久有效的效果。使用 URL 地址重写也不能实现。 - -## 服务器的开销 - -由于 Session 是保存在服务器的,每个用户都会产生一个 Session,如果并发访问的用户非常多,会产生很多的 Session,消耗大量的内存。 - -而 Cookie 由于保存在客户端浏览器上,所以不占用服务器资源。 - -## 浏览器的支持 - -Cookie 需要浏览器支持才能使用。 - -如果浏览器不支持 Cookie,需要使用 Session 以及 URL 地址重写。 - -需要注意的事所有的用到 Session 程序的 URL 都要使用`response.encodeURL(StringURL)` 或`response.encodeRediretURL(String URL)`进行 URL 地址重写,否则导致 Session 会话跟踪失效。 - -## 跨域名 - -Cookie 支持跨域名。 - -Session 不支持跨域名。 diff --git a/docs/javaee/session/cookie.md b/docs/javaee/session/cookie.md deleted file mode 100644 index 61da1135..00000000 --- a/docs/javaee/session/cookie.md +++ /dev/null @@ -1,367 +0,0 @@ ---- -title: JavaEE Cookie -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- cookie -- session ---- - -由于 Http 是一种无状态的协议,服务器单从网络连接上无从知道客户身份。 - -会话跟踪是 Web 程序中常用的技术,用来跟踪用户的整个会话。常用会话跟踪技术是 Cookie 与 Session。 - -## Cookie 是什么? - -Cookie 实际上是存储在客户端上的文本信息,并保留了各种跟踪的信息。 - -**Cookie 工作步骤:** - -1. 客户端请求服务器,如果服务器需要记录该用户的状态,就是用 response 向客户端浏览器颁发一个 Cookie。 -2. 客户端浏览器会把 Cookie 保存下来。 -3. 当浏览器再请求该网站时,浏览器把该请求的网址连同 Cookie 一同提交给服务器。服务器检查该 Cookie,以此来辨认用户状态。 - -**_注:Cookie 功能需要浏览器的支持,如果浏览器不支持 Cookie 或者 Cookie 禁用了,Cookie 功能就会失效。_** - -Java 中把 Cookie 封装成了`javax.servlet.http.Cookie`类。 - -## Cookie 剖析 - -Cookies 通常设置在 HTTP 头信息中(虽然 JavaScript 也可以直接在浏览器上设置一个 Cookie)。 - -设置 Cookie 的 Servlet 会发送如下的头信息: - -``` -HTTP/1.1 200 OK -Date: Fri, 04 Feb 2000 21:03:38 GMT -Server: Apache/1.3.9 (UNIX) PHP/4.0b3 -Set-Cookie: name=xyz; expires=Friday, 04-Feb-07 22:03:38 GMT; - path=/; domain=w3cschool.cc -Connection: close -Content-Type: text/html -``` - -正如您所看到的,`Set-Cookie` 头包含了一个名称值对、一个 GMT 日期、一个路径和一个域。名称和值会被 URL 编码。expires 字段是一个指令,告诉浏览器在给定的时间和日期之后"忘记"该 Cookie。 - -如果浏览器被配置为存储 Cookies,它将会保留此信息直到到期日期。如果用户的浏览器指向任何匹配该 Cookie 的路径和域的页面,它会重新发送 Cookie 到服务器。浏览器的头信息可能如下所示: - -``` -GET / HTTP/1.0 -Connection: Keep-Alive -User-Agent: Mozilla/4.6 (X11; I; Linux 2.2.6-15apmac ppc) -Host: zink.demon.co.uk:1126 -Accept: image/gif, */* -Accept-Encoding: gzip -Accept-Language: en -Accept-Charset: iso-8859-1,*,utf-8 -Cookie: name=xyz -``` - -## Cookie 类中的方法 - -| 方法 | 功能 | -| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -| public void setDomain(String pattern) | 该方法设置 cookie 适用的域。 | -| public String getDomain() | 该方法获取 cookie 适用的域。 | -| public void setMaxAge(int expiry) | 该方法设置 cookie 过期的时间(以秒为单位)。如果不这样设置,cookie 只会在当前 session 会话中持续有效。 | -| public int getMaxAge() | 该方法返回 cookie 的最大生存周期(以秒为单位),默认情况下,-1 表示 cookie 将持续下去,直到浏览器关闭。 | -| public String getName() | 该方法返回 cookie 的名称。名称在创建后不能改变。 | -| public void setValue(String newValue) | 该方法设置与 cookie 关联的值。 | -| public String getValue() | 该方法获取与 cookie 关联的值。 | -| public void setPath(String uri) | 该方法设置 cookie 适用的路径。如果您不指定路径,与当前页面相同目录下的(包括子目录下的)所有 URL 都会返回 cookie。 | -| public String getPath() | 该方法获取 cookie 适用的路径。 | -| public void setSecure(boolean flag) | 该方法设置布尔值,向浏览器指示,只会在 HTTPS 和 SSL 等安全协议中传输此类 Cookie。 | -| public void setComment(String purpose) | 该方法规定了描述 cookie 目的的注释。该注释在浏览器向用户呈现 cookie 时非常有用。 | -| public String getComment() | 该方法返回了描述 cookie 目的的注释,如果 cookie 没有注释则返回 null。 | - -## Cookie 的有效期 - -`Cookie`的`maxAge`决定着 Cookie 的有效期,单位为秒。 - -如果 maxAge 为 0,则表示删除该 Cookie; - -如果为负数,表示该 Cookie 仅在本浏览器中以及本窗口打开的子窗口内有效,关闭窗口后该 Cookie 即失效。 - -Cookie 中提供`getMaxAge()`**和**`setMaxAge(int expiry)`方法来读写`maxAge`属性。 - -## Cookie 的域名 - -Cookie 是不可以跨域名的。域名 www.google.com 颁发的 Cookie 不会被提交到域名 www.baidu.com 去。这是由 Cookie 的隐私安全机制决定的。隐私安全机制能够禁止网站非法获取其他网站的 Cookie。 - -正常情况下,同一个一级域名的两个二级域名之间也不能互相使用 Cookie。如果想让某域名下的子域名也可以使用该 Cookie,需要设置 Cookie 的 domain 参数。 - -Java 中使用`setDomain(Stringdomain)`和`getDomain()`方法来设置、获取 domain。 - -## Cookie 的路径 - -Path 属性决定允许访问 Cookie 的路径。 - -Java 中使用`setPath(Stringuri)`和`getPath()`方法来设置、获取 path。 - -## Cookie 的安全属性 - -HTTP 协议不仅是无状态的,而且是不安全的。 - -使用 HTTP 协议的数据不经过任何加密就直接在网络上传播,有被截获的可能。如果不希望 Cookie 在 HTTP 等非安全协议中传输,可以设置 Cookie 的 secure 属性为 true。浏览器只会在 HTTPS 和 SSL 等安全协议中传输此类 Cookie。 - -Java 中使用`setSecure(booleanflag)`和`getSecure ()`方法来设置、获取 Secure。 - -## 实例 - -### 添加 Cookie - -通过 Servlet 添加 Cookies 包括三个步骤: - -1. 创建一个 Cookie 对象:您可以调用带有 cookie 名称和 cookie 值的 Cookie 构造函数,cookie 名称和 cookie 值都是字符串。 - -2. 设置最大生存周期:您可以使用 `setMaxAge` 方法来指定 cookie 能够保持有效的时间(以秒为单位)。 - -3. 发送 Cookie 到 HTTP 响应头:您可以使用 `response.addCookie` 来添加 HTTP 响应头中的 Cookies。 - -AddCookies.java - -```java -import java.io.IOException; -import java.io.PrintWriter; -import java.net.URLEncoder; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -@WebServlet("/servlet/AddCookies") -public class AddCookies extends HttpServlet { - private static final long serialVersionUID = 1L; - - /** - * @see HttpServlet#HttpServlet() - */ - public AddCookies() { - super(); - } - - /** - * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) - */ - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - // 为名字和姓氏创建 Cookie - Cookie name = new Cookie("name", URLEncoder.encode(request.getParameter("name"), "UTF-8")); // 中文转码 - Cookie url = new Cookie("url", request.getParameter("url")); - - // 为两个 Cookie 设置过期日期为 24 小时后 - name.setMaxAge(60 * 60 * 24); - url.setMaxAge(60 * 60 * 24); - - // 在响应头中添加两个 Cookie - response.addCookie(name); - response.addCookie(url); - - // 设置响应内容类型 - response.setContentType("text/html;charset=UTF-8"); - - PrintWriter out = response.getWriter(); - String title = "设置 Cookie 实例"; - String docType = "\n"; - out.println(docType + "\n" + "" + title + "\n" - + "\n" + "

" + title - + "

\n" + "
    \n" + "
  • 站点名::" + request.getParameter("name") - + "\n
  • " + "
  • 站点 URL::" + request.getParameter("url") - + "\n
  • " + "
\n" + ""); - } - - /** - * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) - */ - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - doGet(request, response); - } - -} -``` - -addCookies.jsp - -```jsp -<%@ page language="java" pageEncoding="UTF-8" %> - - - - - 添加Cookie - - -
- 站点名 : -
- 站点 URL:
- -
- - -``` - -### 显示 Cookie - -要读取 Cookies,您需要通过调用 `HttpServletRequest` 的 `getCookies()` 方法创建一个 `javax.servlet.http.Cookie` 对象的数组。然后循环遍历数组,并使用 `getName()` 和 `getValue()` 方法来访问每个 cookie 和关联的值。 - -ReadCookies.java - -```java -import java.io.IOException; -import java.io.PrintWriter; -import java.net.URLDecoder; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -@WebServlet("/servlet/ReadCookies") -public class ReadCookies extends HttpServlet { - private static final long serialVersionUID = 1L; - - /** - * @see HttpServlet#HttpServlet() - */ - public ReadCookies() { - super(); - } - - /** - * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) - */ - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - Cookie cookie = null; - Cookie[] cookies = null; - // 获取与该域相关的 Cookie 的数组 - cookies = request.getCookies(); - - // 设置响应内容类型 - response.setContentType("text/html;charset=UTF-8"); - - PrintWriter out = response.getWriter(); - String title = "Delete Cookie Example"; - String docType = "\n"; - out.println(docType + "\n" + "" + title + "\n" - + "\n"); - if (cookies != null) { - out.println("

Cookie 名称和值

"); - for (int i = 0; i < cookies.length; i++) { - cookie = cookies[i]; - if ((cookie.getName()).compareTo("name") == 0) { - cookie.setMaxAge(0); - response.addCookie(cookie); - out.print("已删除的 cookie:" + cookie.getName() + "
"); - } - out.print("名称:" + cookie.getName() + ","); - out.print("值:" + URLDecoder.decode(cookie.getValue(), "utf-8") + "
"); - } - } else { - out.println("

No Cookie founds

"); - } - out.println(""); - out.println(""); - } - - /** - * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) - */ - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - doGet(request, response); - } - -} -``` - -### 删除 Cookie - -Java 中并没有提供直接删除 Cookie 的方法,如果想要删除一个 Cookie,直接将这个 Cookie 的有效期设为 0 就可以了。步骤如下: - -1. 读取一个现有的 cookie,并把它存储在 Cookie 对象中。 - -2. 使用 `setMaxAge()` 方法设置 cookie 的年龄为零,来删除现有的 cookie。 - -3. 把这个 cookie 添加到响应头。 - -DeleteCookies.java - -```java -import java.io.IOException; -import java.io.PrintWriter; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.Cookie; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -@WebServlet("/servlet/DeleteCookies") -public class DeleteCookies extends HttpServlet { - private static final long serialVersionUID = 1L; - - /** - * @see HttpServlet#HttpServlet() - */ - public DeleteCookies() { - super(); - } - - /** - * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response) - */ - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - Cookie cookie = null; - Cookie[] cookies = null; - // 获取与该域相关的 Cookie 的数组 - cookies = request.getCookies(); - - // 设置响应内容类型 - response.setContentType("text/html;charset=UTF-8"); - - PrintWriter out = response.getWriter(); - String title = "删除 Cookie 实例"; - String docType = "\n"; - out.println(docType + "\n" + "" + title + "\n" - + "\n"); - if (cookies != null) { - out.println("

Cookie 名称和值

"); - for (int i = 0; i < cookies.length; i++) { - cookie = cookies[i]; - if ((cookie.getName()).compareTo("url") == 0) { - cookie.setMaxAge(0); - response.addCookie(cookie); - out.print("已删除的 cookie:" + cookie.getName() + "
"); - } - out.print("名称:" + cookie.getName() + ","); - out.print("值:" + cookie.getValue() + "
"); - } - } else { - out.println("

No Cookie founds

"); - } - out.println(""); - out.println(""); - } - - /** - * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response) - */ - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - doGet(request, response); - } - -} -``` diff --git a/docs/javaee/session/session.md b/docs/javaee/session/session.md deleted file mode 100644 index 29a13e0a..00000000 --- a/docs/javaee/session/session.md +++ /dev/null @@ -1,182 +0,0 @@ ---- -title: JavaEE Session -date: 2017/11/08 -categories: -- javaee -tags: -- javaee -- session ---- - -## Session 是什么? - -不同于 Cookie 保存在客户端浏览器中,Session 保存在服务器上。 - -如果说 Cookie 机制是通过检查客户身上的“通行证”来确定客户身份的话,那么 Session 机制就是通过检查服务器上的“客户明细表”来确认客户身份。 - -Session 对应的类为 `javax.servlet.http.HttpSession` 类。Session 对象是在客户第一次请求服务器时创建的。 - -## Session 类中的方法 - -`javax.servlet.http.HttpSession` 类中的方法: - -| **方法** | **功能** | -| --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -| public Object getAttribute(String name) | 该方法返回在该 session 会话中具有指定名称的对象,如果没有指定名称的对象,则返回 null。 | -| public Enumeration getAttributeNames() | 该方法返回 String 对象的枚举,String 对象包含所有绑定到该 session 会话的对象的名称。 | -| public long getCreationTime() | 该方法返回该 session 会话被创建的时间,自格林尼治标准时间 1970 年 1 月 1 日午夜算起,以毫秒为单位。 | -| public String getId() | 该方法返回一个包含分配给该 session 会话的唯一标识符的字符串。 | -| public long getLastAccessedTime() | 该方法返回客户端最后一次发送与该 session 会话相关的请求的时间自格林尼治标准时间 1970 年 1 月 1 日午夜算起,以毫秒为单位。 | -| public int getMaxInactiveInterval() | 该方法返回 Servlet 容器在客户端访问时保持 session 会话打开的最大时间间隔,以秒为单位。 | -| public void invalidate() | 该方法指示该 session 会话无效,并解除绑定到它上面的任何对象。 | -| public boolean isNew() | 如果客户端还不知道该 session 会话,或者如果客户选择不参入该 session 会话,则该方法返回 true。 | -| public void removeAttribute(String name) | 该方法将从该 session 会话移除指定名称的对象。 | -| public void setAttribute(String name, Object value) | 该方法使用指定的名称绑定一个对象到该 session 会话。 | -| public void setMaxInactiveInterval(int interval) | 该方法在 Servlet 容器指示该 session 会话无效之前,指定客户端请求之间的时间,以秒为单位。 | - -## Session 的有效期 - -由于会有越来越多的用户访问服务器,因此 Session 也会越来越多。为防止内存溢出,服务器会把长时间没有活跃的 Session 从内存中删除。 - -Session 的超时时间为`maxInactiveInterval`属性,可以通过`getMaxInactiveInterval()`、`setMaxInactiveInterval(longinterval)`来读写这个属性。 - -Tomcat 中 Session 的默认超时时间为 20 分钟。可以修改 web.xml 改变 Session 的默认超时时间。 - -例: - -```xml - - 60 - -``` - -## Session 对浏览器的要求 - -HTTP 协议是无状态的,Session 不能依据 HTTP 连接来判断是否为同一客户。因此服务器向客户端浏览器发送一个名为 JESSIONID 的 Cookie,他的值为该 Session 的 id(也就是 HttpSession.getId()的返回值)。Session 依据该 Cookie 来识别是否为同一用户。 - -该 Cookie 为服务器自动生成的,它的`maxAge`属性一般为-1,表示仅当前浏览器内有效,并且各浏览器窗口间不共享,关闭浏览器就会失效。 - -## URL 地址重写 - -URL 地址重写的原理是将该用户 Session 的 id 信息重写到 URL 地址中。服务器能够解析重写后的 URL 获取 Session 的 id。这样即使客户端不支持 Cookie,也可以使用 Session 来记录用户状态。 - -`HttpServletResponse`类提供了`encodeURL(Stringurl)`实现 URL 地址重写。 - -## Session 中禁用 Cookie - -在`META-INF/context.xml`中编辑如下: - -```xml - - -``` - -部署后,TOMCAT 便不会自动生成名 JESSIONID 的 Cookie,Session 也不会以 Cookie 为识别标志,而仅仅以重写后的 URL 地址为识别标志了。 - -## 实例 - -### Session 跟踪 - -SessionTrackServlet.java - -```java -import java.io.IOException; -import java.io.PrintWriter; -import java.text.SimpleDateFormat; -import java.util.Date; - -import javax.servlet.ServletException; -import javax.servlet.annotation.WebServlet; -import javax.servlet.http.HttpServlet; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; - -@WebServlet("/servlet/SessionTrackServlet") -public class SessionTrackServlet extends HttpServlet { - private static final long serialVersionUID = 1L; - - public void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - // 如果不存在 session 会话,则创建一个 session 对象 - HttpSession session = request.getSession(true); - // 获取 session 创建时间 - Date createTime = new Date(session.getCreationTime()); - // 获取该网页的最后一次访问时间 - Date lastAccessTime = new Date(session.getLastAccessedTime()); - - // 设置日期输出的格式 - SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); - - String title = "Servlet Session 实例"; - Integer visitCount = new Integer(0); - String visitCountKey = new String("visitCount"); - String userIDKey = new String("userID"); - String userID = new String("admin"); - - // 检查网页上是否有新的访问者 - if (session.isNew()) { - session.setAttribute(userIDKey, userID); - } else { - visitCount = (Integer) session.getAttribute(visitCountKey); - visitCount = visitCount + 1; - userID = (String) session.getAttribute(userIDKey); - } - session.setAttribute(visitCountKey, visitCount); - - // 设置响应内容类型 - response.setContentType("text/html;charset=UTF-8"); - PrintWriter out = response.getWriter(); - - String docType = "\n"; - out.println(docType + "\n" + "" + title + "\n" - + "\n" + "

" + title - + "

\n" + "

Session 信息

\n" - + "\n" + "\n" - + " \n" + "\n" + " \n" - + " \n" + "\n" - + " \n" + " \n" - + "\n" + " \n" + " \n" + "\n" + " \n" + " \n" + "\n" + " \n" + " \n" + "
Session 信息
id" + session.getId() + "
创建时间" + df.format(createTime) + "
最后访问时间" + df.format(lastAccessTime) - + "
用户 ID" + userID - + "
访问统计:" + visitCount - + "
\n" + ""); - } -} -``` - -web.xml - -```xml - - SessionTrackServlet - SessionTrackServlet - - - SessionTrackServlet - /servlet/SessionTrackServlet - -``` - -### 删除 Session 会话数据 - -当您完成了一个用户的 session 会话数据,您有以下几种选择: - -**移除一个特定的属性:**您可以调用  `removeAttribute(String name)` 方法来删除与特定的键相关联的值。 - -**删除整个 session 会话:**您可以调用  `invalidate()`  方法来丢弃整个 session 会话。 - -**设置 session 会话过期时间:**您可以调用 `setMaxInactiveInterval(int interval)`  方法来单独设置 session 会话超时。 - -**注销用户:**如果使用的是支持 servlet 2.4 的服务器,您可以调用  `logout`  来注销 Web 服务器的客户端,并把属于所有用户的所有 session 会话设置为无效。 - -**web.xml 配置:**如果您使用的是 Tomcat,除了上述方法,您还可以在 web.xml 文件中配置 session 会话超时,如下所示: - -```xml - - 15 - -``` - -上面实例中的超时时间是以分钟为单位,将覆盖 Tomcat 中默认的 30 分钟超时时间。 - -在一个 Servlet 中的 `getMaxInactiveInterval()` 方法会返回 session 会话的超时时间,以秒为单位。所以,如果在 web.xml 中配置 session 会话超时时间为 15 分钟,那么`getMaxInactiveInterval()` 会返回 900。 diff --git a/docs/javaee/taglib/taglib.md b/docs/javaee/taglib/taglib.md deleted file mode 100644 index 9329d315..00000000 --- a/docs/javaee/taglib/taglib.md +++ /dev/null @@ -1,13 +0,0 @@ -自定义标签是用户定义的 JSP 语言元素。 - -当 JSP 页面包含一个自定义标签时将被转化为 servlet,标签转化为对被称为 tag handler 的对象的操作,即当 servlet 执行时 Web container 调用那些操作。 - -JSP 标签扩展可以让你创建新的标签并且可以直接插入到一个 JSP 页面。 JSP 2.0 规范中引入 Simple Tag Handlers 来编写这些自定义标记。 - -编写自定义步骤的方法 - -任何一个标签都对应一个 Java 类,该类必须实现 Tag 接口。 - -声明一个 tld 标签库描述文件 - -如果 tld 文件位于/WEB-INF 目录下面,Tomcat 会自动加载 tld 文件中的标签库。如果是某些不支持自动加载或者放于其他位置的 tld 文件,可以在 web.xml 中配置。 diff --git a/docs/lib/README.md b/docs/lib/README.md new file mode 100644 index 00000000..ae6297f7 --- /dev/null +++ b/docs/lib/README.md @@ -0,0 +1,23 @@ +# Java 库 + +## 📖 内容 + +- [Dozer 应用指南](bean/dozer.md) +- [Freemark 应用指南](template/freemark.md) +- [Java 与 JSON](serialized/javalib-json.md) +- [细说 Java 主流日志工具库](javalib-log.md) +- [细说 Java 主流工具包](javalib-util.md) +- [JavaMail 应用指南](javamail.md) +- [Jsoup 应用指南](jsoup.md) +- [JUnit5 应用指南](../test/junit.md) +- [Lombok 应用指南](bean/lombok.md) +- [Mockito 应用指南](../test/mockito.md) +- [Reflections 应用指南](reflections.md) +- [Thumbnailator 应用指南](thumbnailator.md) +- [ZXing 应用指南](zxing.md) + +## 📚 资料 + +## 🚪 传送 + +◾ 🏠 [JAVATECH 首页](https://github.com/dunwu/javatech) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ diff --git a/docs/lib/bean/dozer.md b/docs/lib/bean/dozer.md new file mode 100644 index 00000000..da9f89aa --- /dev/null +++ b/docs/lib/bean/dozer.md @@ -0,0 +1,793 @@ +# Dozer 应用指南 + +这篇文章是本人在阅读 Dozer 官方文档(5.5.1 版本,官网已经一年多没更新了)的过程中,整理下来我认为比较基础的应用场景。 + +本文中提到的例子应该能覆盖 JavaBean 映射的大部分场景,希望对你有所帮助。 + + + +- [简介](#简介) +- [安装](#安装) + - [引入 jar 包](#引入-jar-包) + - [Eclipse 插件](#eclipse-插件) +- [使用](#使用) + - [准备](#准备) + - [Dozer 的配置](#dozer-的配置) + - [与 Spring 整合](#与-spring-整合) +- [Dozer 支持的数据类型转换](#dozer-支持的数据类型转换) +- [Dozer 的映射配置](#dozer-的映射配置) + - [用注解来配置映射](#用注解来配置映射) + - [用 API 来配置映射](#用-api-来配置映射) + - [用 XML 来配置映射](#用-xml-来配置映射) +- [参考](#参考) + + + +## 简介 + +**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) diff --git a/docs/lib/bean/lombok.md b/docs/lib/bean/lombok.md new file mode 100644 index 00000000..a36b5487 --- /dev/null +++ b/docs/lib/bean/lombok.md @@ -0,0 +1,542 @@ +# Lombok 应用指南 + + + +- [1. Lombok 简介](#1-lombok-简介) +- [2. Lombok 安装](#2-lombok-安装) +- [3. Lombok 使用](#3-lombok-使用) + - [3.1. @Getter and @Setter](#31-getter-and-setter) + - [3.2. @NonNull](#32-nonnull) + - [3.3. @ToString](#33-tostring) + - [3.4. @EqualsAndHashCode](#34-equalsandhashcode) + - [3.5. @Data](#35-data) + - [3.6. @Cleanup](#36-cleanup) + - [3.7. @Synchronized](#37-synchronized) + - [3.8. @SneakyThrows](#38-sneakythrows) + - [3.9. 示例源码](#39-示例源码) +- [4. Lombok 使用注意点](#4-lombok-使用注意点) + - [4.1. 谨慎使用 `@Builder`](#41-谨慎使用-builder) + - [4.2. `@Data` 注解和继承](#42-data-注解和继承) +- [5. 参考资料](#5-参考资料) + + + +## 1. Lombok 简介 + +Lombok 是一种 Java 实用工具,可用来帮助开发人员消除 Java 的冗长,尤其是对于简单的 Java 对象(POJO)。它通过注释实现这一目的。通过在开发环境中实现 Lombok,开发人员可以节省构建诸如 `hashCode()` 和 `equals()` 、`getter / setter` 这样的方法以及以往用来分类各种 accessor 和 mutator 的大量时间。 + +## 2. 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 + +``` + +## 3. Lombok 使用 + +Lombok 提供注解 API 来修饰指定的类: + +### 3.1. @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; +} +``` + +### 3.2. @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; +} +``` + +### 3.3. @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 + ")"; + } +} +``` + +### 3.4. @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; + } +} +``` + +### 3.5. @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 + ")"; + } +} +``` + +### 3.6. @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(); + } +} +``` + +### 3.7. @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); + } +} +``` + +### 3.8. @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); + } +} +``` + +### 3.9. 示例源码 + +> 示例源码:[javalib-bean](https://github.com/dunwu/java-tutorial/tree/master/javalib-bean) + +## 4. Lombok 使用注意点 + +### 4.1. 谨慎使用 `@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()); + } + +} +``` + +### 4.2. `@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" })` 注掉就会报错。 + +## 5. 参考资料 + +- [Lombok 官网](https://projectlombok.org/) +- [Lombok Github](https://github.com/rzwitserloot/lombok) +- [IntelliJ IDEA - Lombok Plugin](http://plugins.jetbrains.com/plugin/6317-lombok-plugin) diff --git a/docs/lib/javalib-log.md b/docs/lib/javalib-log.md new file mode 100644 index 00000000..caab593c --- /dev/null +++ b/docs/lib/javalib-log.md @@ -0,0 +1,762 @@ +# 细说 Java 主流日志工具库 + +> 在项目开发中,为了跟踪代码的运行情况,常常要使用日志来记录信息。 +> +> 在 Java 世界,有很多的日志工具库来实现日志功能,避免了我们重复造轮子。 +> +> 我们先来逐一了解一下主流日志工具。 +> +> 📦 本文已归档到:「[blog](https://github.com/dunwu/blog)」 + + + +- [日志框架](#日志框架) + - [java.util.logging (JUL)](#javautillogging-jul) + - [Log4j](#log4j) + - [Logback](#logback) + - [Log4j2](#log4j2) + - [Log4j vs Logback vs Log4j2](#log4j-vs-logback-vs-log4j2) +- [日志门面](#日志门面) + - [common-logging](#common-logging) + - [slf4j](#slf4j) + - [common-logging vs slf4j](#common-logging-vs-slf4j) + - [总结](#总结) +- [实施日志解决方案](#实施日志解决方案) + - [引入 jar 包](#引入-jar-包) + - [使用 API](#使用-api) +- [log4j2 配置](#log4j2-配置) +- [logback 配置](#logback-配置) + - [``](#configuration) + - [``](#appender) + - [``](#logger) + - [``](#root) + - [完整的 logback.xml 参考示例](#完整的-logbackxml-参考示例) +- [log4j 配置](#log4j-配置) + - [完整的 log4j.xml 参考示例](#完整的-log4jxml-参考示例) +- [参考](#参考) + + + +## 日志框架 + +### 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/dev/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/dev/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/dev/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/dev/cs/java/javalib/log/logback-configuration.png) + +### `` + +- 作用:将记录日志的任务委托给名为 appender 的组件。 +- 要点 + - 可以配置零个或多个。 + - 它有 ``、``、``、`` 四个子元素。 +- 属性 + - name:设置 appender 名称。 + - class:设置具体的实例化类。 + +#### `` + +- 作用:设置日志文件路径。 + +#### `` + +- 作用:设置过滤器。 +- 要点 + - 可以配置零个或多个。 + +#### `` + +- 作用:设置 appender。 +- 要点 + - 可以配置零个或一个。 +- 属性 + - class:设置具体的实例化类。 + +#### `` + +- 作用:设置编码。 +- 要点 + - 可以配置零个或多个。 +- 属性 + - class:设置具体的实例化类。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/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/) +- diff --git a/docs/lib/javalib-util.md b/docs/lib/javalib-util.md new file mode 100644 index 00000000..005aec39 --- /dev/null +++ b/docs/lib/javalib-util.md @@ -0,0 +1,7 @@ +# 细说 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) diff --git a/docs/lib/javamail.md b/docs/lib/javamail.md new file mode 100644 index 00000000..89c351d6 --- /dev/null +++ b/docs/lib/javamail.md @@ -0,0 +1,466 @@ +# JavaMail 应用指南 + + + +- [简介](#简介) + - [邮件相关的标准](#邮件相关的标准) + - [JavaMail 简介](#javamail-简介) + - [邮件传输过程](#邮件传输过程) + - [Message 结构](#message-结构) +- [JavaMail 的核心类](#javamail-的核心类) + - [java.util.Properties 类(属性对象)](#javautilproperties-类属性对象) + - [javax.mail.Session 类(会话对象)](#javaxmailsession-类会话对象) + - [javax.mail.Transport 类(邮件传输)](#javaxmailtransport-类邮件传输) + - [javax.mail.Store 类(邮件存储 )](#javaxmailstore-类邮件存储-) + - [javax.mail.Message 类(消息对象)](#javaxmailmessage-类消息对象) + - [javax.mail.Address 类(地址)](#javaxmailaddress-类地址) + - [Authenticator 类(认证者)](#authenticator-类认证者) +- [实例](#实例) + - [发送文本邮件](#发送文本邮件) + - [发送 HTML 格式的邮件](#发送-html-格式的邮件) + - [发送带附件的邮件](#发送带附件的邮件) + - [获取邮箱中的邮件](#获取邮箱中的邮件) + - [转发邮件](#转发邮件) + + + +## 简介 + +### 邮件相关的标准 + +厂商所提供的 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...."); +} +``` diff --git a/docs/lib/jsoup.md b/docs/lib/jsoup.md new file mode 100644 index 00000000..936dba8c --- /dev/null +++ b/docs/lib/jsoup.md @@ -0,0 +1,461 @@ +# Jsoup 应用指南 + + + +- [简介](#简介) +- [加载](#加载) + - [从 HTML 字符串加载一个文档](#从-html-字符串加载一个文档) + - [解析一个 body 片断](#解析一个-body-片断) + - [从 URL 加载一个文档](#从-url-加载一个文档) + - [从一个文件加载一个文档](#从一个文件加载一个文档) +- [解析](#解析) + - [使用 DOM 方法来遍历一个文档](#使用-dom-方法来遍历一个文档) + - [使用选择器语法来查找元素](#使用选择器语法来查找元素) + - [从元素抽取属性,文本和 HTML](#从元素抽取属性文本和-html) + - [处理 URLs](#处理-urls) +- [数据修改](#数据修改) + - [设置属性的值](#设置属性的值) + - [设置一个元素的 HTML 内容](#设置一个元素的-html-内容) + - [设置元素的文本内容](#设置元素的文本内容) +- [HTML 清理](#html-清理) + - [消除不受信任的 HTML (来防止 XSS 攻击)](#消除不受信任的-html-来防止-xss-攻击) +- [参考](#参考) + + + +## 简介 + +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) diff --git a/docs/lib/reflections.md b/docs/lib/reflections.md new file mode 100644 index 00000000..e24c081d --- /dev/null +++ b/docs/lib/reflections.md @@ -0,0 +1,100 @@ +# Reflections 应用指南 + + + +- [使用](#使用) +- [ReflectionUtils](#reflectionutils) + + + +引入 + +```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)); +``` diff --git a/docs/lib/serialized/README.md b/docs/lib/serialized/README.md new file mode 100644 index 00000000..b159d977 --- /dev/null +++ b/docs/lib/serialized/README.md @@ -0,0 +1,23 @@ +# Java 序列化库 + +Java 官方的序列化存在许多问题,因此,很多人更愿意使用优秀的第三方序列化工具来替代 Java 自身的序列化机制。 如果想详细了解 Java 自身序列化方式,可以参考:[深入理解 Java 序列化](https://github.com/dunwu/javacore/blob/master/docs/io/java-serialization.md) + +序列化库技术选型: + +- [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](docs/lib/serialized/javalib-json.md) - Fastjson、Jackson、Gson +- [二进制](javatech/docs/lib/serialized/javalib-binary.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) diff --git a/docs/lib/serialized/javalib-binary.md b/docs/lib/serialized/javalib-binary.md new file mode 100644 index 00000000..60d762d0 --- /dev/null +++ b/docs/lib/serialized/javalib-binary.md @@ -0,0 +1,338 @@ +# Java 二进制序列化库 + +## 简介 + +### 为什么需要二进制序列化库 + +原因很简单,就是 Java 默认的序列化机制(`ObjectInputStream` 和 `ObjectOutputStream`)具有很多缺点。 + +> 不了解 Java 默认的序列化机制,可以参考:[Java 序列化](https://github.com/dunwu/javacore/blob/master/docs/io/Java序列化.md) + +Java 自身的序列化方式具有以下缺点: + +- **无法跨语言使用**。这点最为致命,对于很多需要跨语言通信的异构系统来说,不能跨语言序列化,即意味着完全无法通信(彼此数据不能识别,当然无法交互了)。 +- **序列化的性能不高**。序列化后的数据体积较大,这大大影响存储和传输的效率。 +- 序列化一定需要实现 `Serializable` 接口。 +- 需要关注 `serialVersionUID`。 + +引入二进制序列化库就是为了解决这些问题,这在 RPC 应用中尤为常见。 + +### 主流序列化库简介 + +#### Protobuf + +[Protobuf](https://developers.google.com/protocol-buffers/) 是 Google 开发的结构序列化库。 + +它具有以下特性: + +- 结构化数据存储格式(xml,json 等) +- 高性能编解码技术 +- 语言和平台无关,扩展性好 +- 支持 Java, C++, Python 三种语言 + +#### Thrift + +> [Thrift](https://github.com/apache/thrift) 是 apache 开源项目,是一个点对点的 RPC 实现。 + +它具有以下特性: + +- 支持多种语言(目前支持 28 种语言,如:C++、go、Java、Php、Python、Ruby 等等)。 +- 使用了组建大型数据交换及存储工具,对于大型系统中的内部数据传输,相对于 Json 和 xml 在性能上和传输大小上都有明显的优势。 +- 支持三种比较典型的编码方式(通用二进制编码,压缩二进制编码,优化的可选字段压缩编解码)。 + +#### Hessian + +> [Hessian](http://hessian.caucho.com/) 是一种二进制传输协议。 +> +> RPC 框架 Dubbo 就支持 Thrift 和 Hession。 + +它具有以下特性: + +- 支持多种语言。如:Java、Python、C++、C#、PHP、Ruby 等。 +- 相对其他二进制序列化库较慢。 + +#### 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)); +``` + +## 参考资料 + +- **官方** + - [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) diff --git a/docs/lib/serialized/javalib-json.md b/docs/lib/serialized/javalib-json.md new file mode 100644 index 00000000..12145b5a --- /dev/null +++ b/docs/lib/serialized/javalib-json.md @@ -0,0 +1,495 @@ +# Java 和 JSON 序列化 + +> JSON(JavaScript Object Notation)是一种基于文本的数据交换格式。几乎所有的编程语言都有很好的库或第三方工具来提供基于 JSON 的 API 支持,因此你可以非常方便地使用任何自己喜欢的编程语言来处理 JSON 数据。 +> +> 本文主要从 Java 语言的角度来讲解 JSON 的应用。 + + + +- [1. JSON 简介](#1-json-简介) + - [1.1. JSON 是什么](#11-json-是什么) + - [1.2. JSON 标准](#12-json-标准) + - [1.3. JSON 优缺点](#13-json-优缺点) + - [1.4. JSON 工具](#14-json工具) + - [1.5. Java JSON 库](#15-java-json-库) + - [1.6. JSON 编码指南](#16-json-编码指南) +- [2. Fastjson 应用](#2-fastjson-应用) + - [2.1. 添加 maven 依赖](#21-添加-maven-依赖) + - [2.2. Fastjson API](#22-fastjson-api) + - [2.3. Fastjson 注解](#23-fastjson-注解) +- [3. Jackson 应用](#3-jackson-应用) + - [3.1. 添加 maven 依赖](#31-添加-maven-依赖) + - [3.2. Jackson API](#32-jackson-api) + - [3.3. Jackson 注解](#33-jackson-注解) +- [4. Gson 应用](#4-gson-应用) + - [4.1. 添加 maven 依赖](#41-添加-maven-依赖) + - [4.2. Gson API](#42-gson-api) + - [4.3. Gson 注解](#43-gson-注解) +- [5. 示例源码](#5-示例源码) +- [6. 参考资料](#6-参考资料) + + + +## 1. JSON 简介 + +### 1.1. 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)。 + +### 1.2. 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) + +### 1.3. 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/)出现。 + +### 1.4. 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)也可以。 + +### 1.5. 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 + +### 1.6. 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 设计用来最小化请求的数量,以及客户端与服务器间传输的数据量。在高效实现的同时,无需牺牲可读性、灵活性和可发现性。 + +## 2. Fastjson 应用 + +### 2.1. 添加 maven 依赖 + +```xml + + com.alibaba + fastjson + x.x.x + +``` + +### 2.2. 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); +``` + +### 2.3. 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; +} +``` + +## 3. Jackson 应用 + +> 扩展阅读:更多 API 使用细节可以参考 [jackson-databind 官方说明](https://github.com/FasterXML/jackson-databind) + +### 3.1. 添加 maven 依赖 + +```xml + + com.fasterxml.jackson.core + jackson-databind + 2.9.8 + +``` + +### 3.2. 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(); +} +``` + +### 3.3. 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 {} +``` + +## 4. Gson 应用 + +> 详细内容可以参考官方文档:[Gson 用户指南](https://github.com/google/gson/blob/master/UserGuide.md) + +### 4.1. 添加 maven 依赖 + +```xml + + com.google.code.gson + gson + 2.8.6 + +``` + +### 4.2. 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(); +``` + +### 4.3. 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; + } +} +``` + +## 5. 示例源码 + +> 示例源码:[javalib-io-json](https://github.com/dunwu/java-tutorial/tree/master/javalib-io-json) + +## 6. 参考资料 + +- **官方** + - [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) diff --git a/docs/lib/template/README.md b/docs/lib/template/README.md new file mode 100644 index 00000000..4cad92b7 --- /dev/null +++ b/docs/lib/template/README.md @@ -0,0 +1,43 @@ +# 模板引擎 + +模板引擎不属于特定技术领域,它是跨领域跨平台的概念。 模板引擎的作用就是分离业务数据和最终呈现内容,它可以生成特定格式的文档(模板) 。 + +模板引擎简单来说,就是:***`模板 + 数据模型 = 输出`*** + +较早,也比较经典的模板引擎是 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](freemark.md) +- [Thymeleaf](thymeleaf.md) +- [Velocity](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) diff --git a/docs/lib/template/freemark.md b/docs/lib/template/freemark.md new file mode 100644 index 00000000..8eff5f72 --- /dev/null +++ b/docs/lib/template/freemark.md @@ -0,0 +1,160 @@ +# Freemark Cheat Sheet + +> 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/dev/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/) diff --git a/docs/lib/template/thymeleaf.md b/docs/lib/template/thymeleaf.md new file mode 100644 index 00000000..7c6b46bf --- /dev/null +++ b/docs/lib/template/thymeleaf.md @@ -0,0 +1,469 @@ +# Thymeleaf Cheat Sheet + +## 标准方言 + +标准方言是指 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) diff --git a/docs/lib/template/velocity.md b/docs/lib/template/velocity.md new file mode 100644 index 00000000..d4d3a621 --- /dev/null +++ b/docs/lib/template/velocity.md @@ -0,0 +1,316 @@ +# Velocity Cheat Sheet + +**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) diff --git a/docs/lib/thumbnailator.md b/docs/lib/thumbnailator.md new file mode 100644 index 00000000..1f9dfc94 --- /dev/null +++ b/docs/lib/thumbnailator.md @@ -0,0 +1,244 @@ +# Thumbnailator 应用指南 + + + +- [简介](#简介) +- [核心 API](#核心-api) + - [Thumbnails](#thumbnails) + - [Thumbnails.Builder](#thumbnailsbuilder) + - [工作流](#工作流) +- [实战](#实战) + - [安装](#安装) + - [图片缩放](#图片缩放) + - [图片旋转](#图片旋转) + - [加水印](#加水印) + - [批量处理图片](#批量处理图片) +- [参考](#参考) + + + +## 简介 + +`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) diff --git a/docs/lib/zxing.md b/docs/lib/zxing.md new file mode 100644 index 00000000..62a7ae77 --- /dev/null +++ b/docs/lib/zxing.md @@ -0,0 +1,93 @@ +# 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) diff --git a/docs/limiting/hystrix.md b/docs/limiting/hystrix.md new file mode 100644 index 00000000..5df34b08 --- /dev/null +++ b/docs/limiting/hystrix.md @@ -0,0 +1,537 @@ +# Hystrix 应用指南 + + + +- [一、Hystrix 简介](#一hystrix-简介) + - [Hystrix 是什么](#hystrix-是什么) + - [为什么需要 Hystrix](#为什么需要-hystrix) + - [Hystrix 的功能](#hystrix-的功能) +- [Hystrix 核心概念](#hystrix-核心概念) +- [二、Hystrix 工作流程](#二hystrix-工作流程) + - [(一)包装命令](#一包装命令) + - [(二)执行命令](#二执行命令) + - [(三)是否缓存](#三是否缓存) + - [(四)是否开启断路器](#四是否开启断路器) + - [(五)信号量、线程池是否拒绝](#五信号量线程池是否拒绝) + - [(六)construct() 或 run()](#六construct-或-run) + - [(七)健康检查](#七健康检查) + - [(八)获取 Fallback](#八获取-fallback) + - [(九)返回结果](#九返回结果) +- [三、断路器工作原理](#三断路器工作原理) + - [系统指标](#系统指标) +- [四、资源隔离技术](#四资源隔离技术) + - [线程池隔离](#线程池隔离) + - [信号量隔离](#信号量隔离) +- [五、Hystrix 应用](#五hystrix-应用) + - [Spring Cloud + Hystrix](#spring-cloud--hystrix) +- [六、其他限流技术](#六其他限流技术) +- [Hystrix 配置](#hystrix-配置) + - [执行配置](#执行配置) + - [断路配置](#断路配置) + - [指标配置](#指标配置) + - [线程池配置](#线程池配置) +- [参考资料](#参考资料) + + + +## 一、Hystrix 简介 + +### Hystrix 是什么 + +Hystrix 是 Netflix 开源的一款容错框架,包含常用的容错方法:线程池隔离、信号量隔离、熔断、降级。 + +Hystrix 官方宣布**不再发布新版本**。 + +但是 Hystrix 的客户端熔断保护,断路器设计理念,有非常高的学习价值。 + +### 为什么需要 Hystrix + +复杂的分布式系统架构中的应用程序往往具有数十个依赖项,每个依赖项都会不可避免地在某个时刻失败。 如果主机应用程序未与这些外部故障隔离开来,则可能会被波及。 + +例如,对于依赖于 30 个服务的应用程序,假设每个服务的正常运行时间为 99.99%,则可以期望: + +> 99.9930 = 99.7% 的正常运行时间 +> +> 10 亿个请求中的 0.3%= 3,000,000 个失败 +> +> 即使所有依赖项都具有出色的正常运行时间,每月也会有 2 个小时以上的停机时间。 +> +> 然而,现实情况一般比这种估量情况更糟糕。 + +--- + +当一切正常时,整体系统如下所示: + +![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200717141615.png) + +在高并发场景,这些依赖的稳定性与否对系统的影响非常大,但是依赖有很多不可控问题:如网络连接、资源繁忙、服务宕机等。例如:下图中有一个 QPS 为 50 的依赖 I 出现不可用,但是其他依赖服务是可用的。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200717141749.png) + +但是,在高并发场景下,当依赖 I 阻塞时,大多数服务器的线程池就出现阻塞(BLOCK)。当这种级联故障愈演愈烈,就可能造成整个线上服务不可用的雪崩效应,如下图: + +![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200717141859.png) + +Hystrix 就是为了解决这类问题而应运而生。 + +### Hystrix 的功能 + +Hystrix 具有以下功能: + +- 避免资源耗尽:阻止任何一个依赖服务耗尽所有的资源,比如 tomcat 中的所有线程资源。 +- 避免请求排队和积压:采用限流和 `fail fast` 来控制故障。 +- 支持降级:提供 fallback 降级机制来应对故障。 +- 资源隔离:比如 `bulkhead`(舱壁隔离技术)、`swimlane`(泳道技术)、`circuit breaker`(断路技术)来限制任何一个依赖服务的故障的影响。 +- 统计/监控/报警:通过近实时的统计/监控/报警功能,来提高故障发现的速度。 +- 通过近实时的属性和配置**热修改**功能,来提高故障处理和恢复的速度。 +- 保护依赖服务调用的所有故障情况,而不仅仅只是网络故障情况。 + +如果使用 Hystrix 对每个基础依赖服务进行过载保护,则整个系统架构将会类似下图所示,每个依赖项彼此隔离,受到延迟时发生饱和的资源的被限制访问,并包含 fallback 逻辑(用于降级处理),该逻辑决定了在依赖项中发生任何类型的故障时做出对应的处理。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200717142842.png) + +## Hystrix 核心概念 + +## 二、Hystrix 工作流程 + +如下图所示,Hystrix 的工作流程大致可以分为 9 个步骤。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200717143247.png) + +### (一)包装命令 + +x 支持资源隔离。 + +资源隔离,就是说,你如果要把对某一个依赖服务的所有调用请求,全部隔离在同一份资源池内,不会去用其它资源了,这就叫资源隔离。哪怕对这个依赖服务,比如说商品服务,现在同时发起的调用量已经到了 1000,但是分配给商品服务线程池内就 10 个线程,最多就只会用这 10 个线程去执行。不会因为对商品服务调用的延迟,将 Tomcat 内部所有的线程资源全部耗尽。 + +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` | +| `toObvsevable` | 基于 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 命令 + +### (五)信号量、线程池是否拒绝 + +如果当前要执行的 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 应用 + +### Spring Cloud + 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 [Apache Dubbo](https://dubbo.apache.org/zh-cn/) 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。 + +## 一、Dubbo 简介 + +[Apache Dubbo](https://dubbo.apache.org/zh-cn/) 是一款高性能、轻量级的开源 Java RPC 框架。 + +Dubbo 提供了三大核心能力: + +- 面向接口的远程方法调用 +- 智能容错和负载均衡 +- 服务自动注册和发现 + +### RPC 原理简介 + +#### 什么是 RPC + +RPC(Remote Procedure Call),即远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务 A、B 部署在两台不同的机器上,那么服务 A 如果想要调用服务 B 中的某个方法该怎么办呢?使用 HTTP 请求 当然可以,但是可能会比较慢而且一些优化做的并不好。 RPC 的出现就是为了解决这个问题。 + +#### RPC 工作流程 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200305121252.jpg) + +1. 服务消费方(client)调用以本地调用方式调用服务; +2. client stub 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体; +3. client stub 找到服务地址,并将消息发送到服务端; +4. server stub 收到消息后进行解码; +5. server stub 根据解码结果调用本地的服务; +6. 本地服务执行并将结果返回给 server stub; +7. server stub 将返回结果打包成消息并发送至消费方; +8. client stub 接收到消息,并进行解码; +9. 服务消费方得到最终结果。 + +### 为什么需要 Dubbo + +**如果你要开发分布式程序,你也可以直接基于 HTTP 接口进行通信,但是为什么要用 Dubbo 呢?** + +我觉得主要可以从 Dubbo 提供的下面四点特性来说为什么要用 Dubbo: + +1. **负载均衡**——同一个服务部署在不同的机器时该调用那一台机器上的服务。 +2. **服务调用链路**——随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如何调用的。 +3. **服务访问压力以及时长统计、资源调度和治理**——基于访问压力实时管理集群容量,提高集群利用率。 +4. **服务治理**——某个服务挂掉之后调用备用服务。 + +另外,Dubbo 除了能够应用在分布式系统中,也可以应用在现在比较火的微服务系统中。不过,由于 Spring Cloud 在微服务中应用更加广泛,所以,我觉得一般我们提 Dubbo 的话,大部分是分布式系统的情况。 + +## 二、QuickStart + +(1)添加 maven 依赖 + +```xml + + com.alibaba + dubbo + ${dubbo.version} + +``` + +(2)定义 Provider + +```java +package com.alibaba.dubbo.demo; + +public interface DemoService { + String sayHello(String name); +} +``` + +(3)实现 Provider + +```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; + } +} +``` + +(4)配置 Provider + +```xml + + + + + + + + +``` + +(5)启动 Provider + +```java +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class Provider { + public static void main(String[] args) throws Exception { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( + new String[] {"META-INF/spring/dubbo-demo-provider.xml"}); + context.start(); + // press any key to exit + System.in.read(); + } +} +``` + +(6)配置 Consumer + +```xml + + + + + + +``` + +(7)启动 Consumer + +```java +import com.alibaba.dubbo.demo.DemoService; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class Consumer { + public static void main(String[] args) throws Exception { + ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( + new String[]{"META-INF/spring/dubbo-demo-consumer.xml"}); + context.start(); + // obtain proxy object for remote invocation + DemoService demoService = (DemoService) context.getBean("demoService"); + // execute remote invocation + String hello = demoService.sayHello("world"); + // show the result + System.out.println(hello); + } +} +``` + +## 三、Dubbo 配置 + +Dubbo 所有配置最终都将转换为 URL 表示,并由服务提供方生成,经注册中心传递给消费方,各属性对应 URL 的参数,参见配置项一览表中的 "对应 URL 参数" 列。 + +只有 group,interface,version 是服务的匹配条件,三者决定是不是同一个服务,其它配置项均为调优和治理参数。 + +URL 格式:`protocol://username:password@host:port/path?key=value&key=value` + +### 配置方式 + +Dubbo 支持多种配置方式: + +- xml 配置 +- properties 配置 +- API 配置 +- 注解配置 + +如果同时存在多种配置方式,遵循以下覆盖策略: + +- JVM 启动 -D 参数优先,这样可以使用户在部署和启动时进行参数重写,比如在启动时需改变协议的端口。 +- XML 次之,如果在 XML 中有配置,则 dubbo.properties 中的相应配置项无效。 +- Properties 最后,相当于缺省值,只有 XML 没有配置时,dubbo.properties 的相应配置项才会生效,通常用于共享公共配置,比如应用名。 + +
    + +
    + +#### xml 配置 + +示例: + +```xml + + + + + + + + + + + + + + + + +``` + +#### properties 配置 + +示例: + +```properties +dubbo.application.name=foo +dubbo.application.owner=bar +dubbo.registry.address=10.20.153.10:9090 +``` + +### 配置项 + +所有配置项分为三大类: + +- 服务发现:表示该配置项用于服务的注册与发现,目的是让消费方找到提供方。 +- 服务治理:表示该配置项用于治理服务间的关系,或为开发测试提供便利条件。 +- 性能调优:表示该配置项用于调优性能,不同的选项对性能会产生影响。 + +配置项清单: + +| 标签 | 用途 | 解释 | +| ------------------- | ------------ | ------------------------------------------------------------------------------------------------ | +| `dubbo:service` | 服务配置 | 用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心 | +| `dubbo:reference` | 引用配置 | 用于创建一个远程服务代理,一个引用可以指向多个注册中心 | +| `dubbo:protocol` | 协议配置 | 用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受 | +| `dubbo:application` | 应用配置 | 用于配置当前应用信息,不管该应用是提供者还是消费者 | +| `dubbo:module` | 模块配置 | 用于配置当前模块信息,可选 | +| `dubbo:registry` | 注册中心配置 | 用于配置连接注册中心相关信息 | +| `dubbo:monitor` | 监控中心配置 | 用于配置连接监控中心相关信息,可选 | +| `dubbo:provider` | 提供方配置 | 当 ProtocolConfig 和 ServiceConfig 某属性没有配置时,采用此缺省值,可选 | +| `dubbo:consumer` | `消费方配置` | `当 ReferenceConfig 某属性没有配置时,采用此缺省值,可选` | +| `dubbo:method` | 方法配置 | 用于 ServiceConfig 和 ReferenceConfig 指定方法级的配置信息 | +| `dubbo:argument` | 参数配置 | 用于指定方法参数配置 | + +> 详细配置说明请参考:[官方配置](http://dubbo.apache.org/books/dubbo-user-book/references/xml/introduction.html) + +#### 配置之间的关系 + +
    + +
    + +#### 配置覆盖关系 + +以 timeout 为例,显示了配置的查找顺序,其它 retries, loadbalance, actives 等类似: + +- **方法级优先,接口级次之,全局配置再次之**。 +- **如果级别一样,则消费方优先,提供方次之**。 + +其中,服务提供方配置,通过 URL 经由注册中心传递给消费方。 + +
    + +
    +### 动态配置中心 + +配置中心(v2.7.0)在 Dubbo 中承担两个职责: + +1. 外部化配置。启动配置的集中式存储 (简单理解为 dubbo.properties 的外部化存储)。 +2. 服务治理。服务治理规则的存储与通知。 + +启用动态配置: + +```xml + +``` + +或者 + +```properties +dubbo.config-center.address=zookeeper://127.0.0.1:2181 +``` + +或者 + +```java +ConfigCenterConfig configCenter = new ConfigCenterConfig(); +configCenter.setAddress("zookeeper://127.0.0.1:2181"); +``` + +## 四、Dubbo 架构 + +### Dubbo 核心组件 + +
    + +
    + +节点角色: + +| 节点 | 角色说明 | +| --------- | -------------------------------------- | +| Provider | 暴露服务的服务提供方 | +| Consumer | 调用远程服务的服务消费方 | +| Registry | 服务注册与发现的注册中心 | +| Monitor | 统计服务的调用次数和调用时间的监控中心 | +| Container | 服务运行容器 | + +调用关系: + +1. 服务容器负责启动,加载,运行服务提供者。 +2. 服务提供者在启动时,向注册中心注册自己提供的服务。 +3. 服务消费者在启动时,向注册中心订阅自己所需的服务。 +4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 +5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 +6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 + +**重要知识点总结:** + +- **注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小** +- **监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示** +- **注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外** +- **注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者** +- **注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表** +- **注册中心和监控中心都是可选的,服务消费者可以直连服务提供者** +- **服务提供者无状态,任意一台宕掉后,不影响使用** +- **服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复** + +> 问:注册中心挂了可以继续通信吗? +> +> 答:可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息**拉取到本地缓存**,所以注册中心挂了可以继续通信。 + +### Dubbo 架构层次 + +
    + +
    + +图例说明: + +- 图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。 +- 图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。 +- 图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。 +- 图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。 + +#### 各层说明 + +- **config 配置层**:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类 +- **proxy 服务代理层**:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory +- **registry 注册中心层**:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory, Registry, RegistryService +- **cluster 路由层**:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster, Directory, Router, LoadBalance +- **monitor 监控层**:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory, Monitor, MonitorService +- **protocol 远程调用层**:封装 RPC 调用,以 Invocation, Result 为中心,扩展接口为 Protocol, Invoker, Exporter +- **exchange 信息交换层**:封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer +- **transport 网络传输层**:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec +- **serialize 数据序列化层**:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool +- **serialize 数据序列化层**:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool + +#### 各层关系说明 + +- 在 RPC 中,Protocol 是核心层,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 调用,然后在 Invoker 的主过程上 Filter 拦截点。 +- 图中的 Consumer 和 Provider 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不用 Client 和 Server 的原因是 Dubbo 在很多场景下都使用 Provider, Consumer, Registry, Monitor 划分逻辑拓普节点,保持统一概念。 +- 而 Cluster 是外围概念,所以 Cluster 的目的是将多个 Invoker 伪装成一个 Invoker,这样其它人只要关注 Protocol 层 Invoker 即可,加上 Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有一个提供者时,是不需要 Cluster 的。 +- Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 Proxy 将 Invoker 转成接口,或将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。 +- 而 Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会用上,Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展 UDP 传输,而 Exchange 层是在传输层之上封装了 Request-Response 语义。 +- Registry 和 Monitor 实际上不算一层,而是一个独立的节点,只是为了全局概览,用层的方式画在一起。 + +## 五、服务发现 + +### 启动时检查 + +Dubbo 缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring 初始化完成,以便上线时,能及早发现问题,默认 `check="true"`。 + +可以通过 xml、properties、-D 参数三种方式设置。启动时检查 + +## 六、Dubbo 协议 + +Dubbo 支持多种通信协议,不同的协议针对不同的序列化方式。 + +### dubbo 协议 + +[dubbo](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/dubbo.html) 协议是 Dubbo 的默认通信协议,采用单一长连接和 NIO 异步通信,基于 hessian 作为序列化协议。 + +[dubbo](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/dubbo.html) 协议适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。反之,Dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。 + +为了要支持高并发场景,一般是服务提供者就几台机器,但是服务消费者有上百台,可能每天调用量达到上亿次!此时用长连接是最合适的,就是跟每个服务消费者维持一个长连接就可以,可能总共就 100 个连接。然后后面直接基于长连接 NIO 异步通信,可以支撑高并发请求。 + +### rmi 协议 + +[rmi](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/rmi.html) - 采用 JDK 标准的 `java.rmi.*` 实现,采用阻塞式短连接和 JDK 标准序列化方式。 + +注意:如果正在使用 RMI 提供服务给外部访问,同时应用里依赖了老的 `common-collections` 包的情况下,存在反序列化安全风险。 + +### hessian 协议 + +[hessian](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/hessian.html) 协议用于集成 Hessian 的服务,Hessian 底层采用 Http 通讯,采用 Servlet 暴露服务,Dubbo 缺省内嵌 Jetty 作为服务器实现。 + +Dubbo 的 Hessian 协议可以和原生 Hessian 服务互操作,即: + +- 提供者用 Dubbo 的 Hessian 协议暴露服务,消费者直接用标准 Hessian 接口调用 +- 或者提供方用标准 Hessian 暴露服务,消费方用 Dubbo 的 Hessian 协议调用。 + +### thrift 协议 + +当前 dubbo 支持的 [thrift](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/thrift.html) 协议是对 thrift 原生协议的扩展,在原生协议的基础上添加了一些额外的头信息,比如 service name,magic number 等。 + +使用 dubbo thrift 协议同样需要使用 thrift 的 idl compiler 编译生成相应的 java 代码,后续版本中会在这方面做一些增强。 + +### http 协议 + +[http](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/http.html) 协议基于 HTTP 表单的远程调用协议,采用 Spring 的 HttpInvoker 实现。 + +使用 JSON 序列化方式。 + +### webservice 协议 + +基于 WebService 的远程调用协议,基于 [Apache CXF](http://cxf.apache.org/) 的 `frontend-simple` 和 `transports-http` 实现。 + +使用 SOAP 序列化方式。 + +可以和原生 WebService 服务互操作,即: + +- 提供者用 Dubbo 的 WebService 协议暴露服务,消费者直接用标准 WebService 接口调用, +- 或者提供方用标准 WebService 暴露服务,消费方用 Dubbo 的 WebService 协议调用。 + +### rest 协议 + +基于标准的 Java REST API——JAX-RS 2.0(Java API for RESTful Web Services 的简写)实现的 REST 调用支持 + +### memcached 协议 + +基于 memcached 实现的 RPC 协议。 + +### redis 协议 + +基于 redis 实现的 RPC 协议。 + +> 在现实世界中,序列化有多种方式。 +> +> JDK 自身提供的序列化方式,效率不高,但是 Java 程序使用最多。 +> +> 如果想要较好的可读性,可以使用 JSON (常见库有:[jackson](https://github.com/FasterXML/jackson)、[gson](https://github.com/google/gson)、[fastjson](https://github.com/alibaba/fastjson))或 SOAP (即 xml 形式) +> +> 如果想要更好的性能,可以使用 [thrift](https://github.com/apache/thrift)、[protobuf](https://github.com/protocolbuffers/protobuf)、[hessian](http://hessian.caucho.com/doc/hessian-overview.xtp) +> +> 想深入了解可以参考:[序列化](https://github.com/dunwu/javatech/blob/master/docs/lib/serialized) + +## 七、集群容错 + +在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。 + +
    + +
    + +- **Failover** - **失败自动切换**,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。 +- **Failfast** - **快速失败**,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 +- **Failsafe** - **失败安全**,出现异常时,直接忽略。通常用于写入审计日志等操作。 +- **Failback** - **失败自动恢复**,后台记录失败请求,定时重发。通常用于消息通知操作。 +- **Forking** - **并行调用多个服务器**,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。 +- **Broadcast** - **广播调用所有提供者**,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。 + +集群容错配置示例: + +```xml + + +``` + +## 八、负载均衡 + +Dubbo 提供了多种负载均衡(LoadBalance)策略,缺省为 `Random` 随机调用。 + +Dubbo 的负载均衡配置可以细粒度到服务、方法级别,且 `dubbo:service` 和 `dubbo:reference` 均可配置。 + +```xml + + + + + + + + + + + + +``` + +#### Random + +- **随机**,按权重设置随机概率。 +- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。 + +#### RoundRobin + +- **轮询**,按公约后的权重设置轮询比率。 +- 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。 + +#### LeastActive + +- **最少活跃调用数**,相同活跃数的随机,活跃数指调用前后计数差。 +- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。 + +#### ConsistentHash + +- **一致性 Hash**,相同参数的请求总是发到同一提供者。 +- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。 +- 算法参见:[http://en.wikipedia.org/wiki/Consistent_hashing](http://en.wikipedia.org/wiki/Consistent_hashing) +- 缺省只对第一个参数 Hash,如果要修改,请配置 `` +- 缺省用 160 份虚拟节点,如果要修改,请配置 `` + +## 九、Dubbo 服务治理 + +### 服务治理简介 + +- 当服务越来越多时,服务 URL 配置管理变得非常困难,F5 硬件负载均衡器的单点压力也越来越大。 +- 当进一步发展,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。 +- 接着,服务的调用量越来越大,服务的容量问题就暴露出来,这个服务需要多少机器支撑?什么时候该加机器? + +以上问题可以归纳为服务治理问题,这也是 Dubbo 的核心功能。 + +#### 调用链路 + +一个微服务架构,往往由大量分布式服务组成。那么这些服务之间互相是如何调用的?调用链路是啥?说实话,几乎到后面没人搞的清楚了,因为服务实在太多了,可能几百个甚至几千个服务。 + +那就需要基于 dubbo 做的分布式系统中,对各个服务之间的调用自动记录下来,然后自动将**各个服务之间的依赖关系和调用链路生成出来**,做成一张图,显示出来,大家才可以看到对吧。 + +#### 服务访问压力以及时长统计 + +需要自动统计**各个接口和服务之间的调用次数以及访问延时**,而且要分成两个级别。 + +- 一个级别是接口粒度,就是每个服务的每个接口每天被调用多少次,TP50/TP90/TP99,三个档次的请求延时分别是多少; +- 第二个级别是从源头入口开始,一个完整的请求链路经过几十个服务之后,完成一次请求,每天全链路走多少次,全链路请求延时的 TP50/TP90/TP99,分别是多少。 + +#### 其他 + +- 服务分层(避免循环依赖) +- 调用链路失败监控和报警 +- 服务鉴权 +- 每个服务的可用性的监控(接口调用成功率?几个 9?99.99%,99.9%,99%) + +所谓失败重试,就是 consumer 调用 provider 要是失败了,比如抛异常了,此时应该是可以重试的,或者调用超时了也可以重试。配置如下: + +``` + +``` + +举个栗子。 + +某个服务的接口,要耗费 5s,你这边不能干等着,你这边配置了 timeout 之后,我等待 2s,还没返回,我直接就撤了,不能干等你。 + +可以结合你们公司具体的场景来说说你是怎么设置这些参数的: + +- `timeout`:一般设置为 `200ms`,我们认为不能超过 `200ms` 还没返回。 +- `retries`:设置 retries,一般是在读请求的时候,比如你要查询个数据,你可以设置个 retries,如果第一次没读到,报错,重试指定的次数,尝试再次读取。 + +### 路由规则 + +路由规则决定一次 dubbo 服务调用的目标服务器,分为条件路由规则和脚本路由规则,并且支持可扩展。 + +向注册中心写入路由规则的操作通常由监控中心或治理中心的页面完成。 + +```java +RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); +Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181")); +registry.register(URL.valueOf("condition://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("host = 10.20.153.10 => host = 10.20.153.11") + ")); +``` + +- **condition://** - 表示路由规则的类型,支持条件路由规则和脚本路由规则,可扩展,必填。 +- **0.0.0.0** - 表示对所有 IP 地址生效,如果只想对某个 IP 的生效,请填入具体 IP,必填。 +- **com.foo.BarService** - 表示只对指定服务生效,必填。 +- **category=routers** - 表示该数据为动态配置类型,必填。 +- **dynamic=false** - 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心,必填。 +- **enabled=true** - 覆盖规则是否生效,可不填,缺省生效。 +- **force=false** - 当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为 flase。 +- **runtime=false** - 是否在每次调用时执行路由规则,否则只在提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为 true,需要注意设置会影响调用的性能,可不填,缺省为 flase。 +- **priority=1** - 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 0。 +- **rule=URL.encode("host = 10.20.153.10 => host = 10.20.153.11")** - 表示路由规则的内容,必填。 + +### 服务降级 + +可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。 + +向注册中心写入动态配置覆盖规则: + +```java +RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); +Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181")); +registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null")); +``` + +其中: + +**`mock=force:return+null`** 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。 +还可以改为 **`mock=fail:return+null`** 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。 + +比如说服务 A 调用服务 B,结果服务 B 挂掉了,服务 A 重试几次调用服务 B,还是不行,那么直接降级,走一个备用的逻辑,给用户返回响应。 + +举个例子,我们有接口 `HelloService`。`HelloServiceImpl` 有该接口的具体实现。 + +```java +public interface HelloService { + void sayHello(); +} + +public class HelloServiceImpl implements HelloService { + public void sayHello() { + System.out.println("hello world......"); + } +} +``` + +Dubbo 配置: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + +``` + +我们调用接口失败的时候,可以通过 `mock` 统一返回 null。 + +mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+`Mock`” 后缀。然后在 Mock 类里实现自己的降级逻辑。 + +```java +public class HelloServiceMock implements HelloService { + public void sayHello() { + // 降级逻辑 + } +} +``` + +### 访问控制 + +#### 直连 + +在开发及测试环境下,经常需要绕过注册中心,只测试指定服务提供者,这时候可能需要点对点直连,点对点直联方式,将以服务接口为单位,忽略注册中心的提供者列表,A 接口配置点对点,不影响 B 接口从注册中心获取列表。 + +
    + +
    + +配置方式: + +(1)通过 XML 配置 + +如果是线上需求需要点对点,可在 中配置 url 指向提供者,将绕过注册中心,多个地址用分号隔开,配置如下: + +```xml + +``` + +(2)通过 -D 参数指定 + +在 JVM 启动参数中加入-D 参数映射服务地址: + +``` +java -Dcom.alibaba.xxx.XxxService=dubbo://localhost:20890 +``` + +(3)通过文件映射 +如果服务比较多,也可以用文件映射,用 -Ddubbo.resolve.file 指定映射文件路径,此配置优先级高于 中的配置: + +``` +java -Ddubbo.resolve.file=xxx.properties +``` + +然后在映射文件 xxx.properties 中加入配置,其中 key 为服务名,value 为服务提供者 URL: + +```properties +com.alibaba.xxx.XxxService=dubbo://localhost:20890 +``` + +#### 只订阅 + +为方便开发测试,经常会在线下共用一个所有服务可用的注册中心,这时,如果一个正在开发中的服务提供者注册,可能会影响消费者不能正常运行。 + +可以让服务提供者开发方,只订阅服务(开发的服务可能依赖其它服务),而不注册正在开发的服务,通过直连测试正在开发的服务。 + +禁用注册配置: + +```xml + +``` + +或者 + +```xml + +``` + +#### 只注册 + +如果有两个镜像环境,两个注册中心,有一个服务只在其中一个注册中心有部署,另一个注册中心还没来得及部署,而两个注册中心的其它应用都需要依赖此服务。这个时候,可以让服务提供者方只注册服务到另一注册中心,而不从另一注册中心订阅服务。 + +禁用订阅配置 + +```xml + + +``` + +或者 + +```xml + + +``` + +#### 静态服务 + +有时候希望人工管理服务提供者的上线和下线,此时需将注册中心标识为非动态管理模式。 + +``` + +``` + +或者 + +``` + +``` + +服务提供者初次注册时为禁用状态,需人工启用。断线时,将不会被自动删除,需人工禁用。 + +### 动态配置 + +向注册中心写入动态配置覆盖规则。该功能通常由监控中心或治理中心的页面完成。 + +```java +RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); +Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181")); +registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&timeout=1000")); +``` + +其中: + +- **override://** - 表示数据采用覆盖方式,支持 override 和 absent,可扩展,必填。 +- **0.0.0.0** - 表示对所有 IP 地址生效,如果只想覆盖某个 IP 的数据,请填入具体 IP,必填。 +- **com.foo.BarService** - 表示只对指定服务生效,必填。 +- **category=configurators** - 表示该数据为动态配置类型,必填。 +- **dynamic=false** - 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心,必填。 +- **enabled=true** - 覆盖规则是否生效,可不填,缺省生效。 +- **application=foo** - 表示只对指定应用生效,可不填,表示对所有应用生效。 +- **timeout=1000** - 表示将满足以上条件的 timeout 参数的值覆盖为 1000。如果想覆盖其它参数,直接加在 override 的 URL 参数上。 + +示例: + +- 禁用提供者:(通常用于临时踢除某台提供者机器,相似的,禁止消费者访问请使用路由规则) + +``` +override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&disbaled=true +``` + +- 调整权重:(通常用于容量评估,缺省权重为 100) + +``` +override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&weight=200 +``` + +- 调整负载均衡策略:(缺省负载均衡策略为 random) + +``` +override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&loadbalance=leastactive +``` + +- 服务降级:(通常用于临时屏蔽某个出错的非关键服务) + +``` +override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null +``` + +## 十、多版本 + +当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。 + +可以按照以下的步骤进行版本迁移: + +1. 在低压力时间段,先升级一半提供者为新版本 +2. 再将所有消费者升级为新版本 +3. 然后将剩下的一半提供者升级为新版本 + +老版本服务提供者配置: + +```xml + +``` + +新版本服务提供者配置: + +```xml + +``` + +老版本服务消费者配置: + +```xml + +``` + +新版本服务消费者配置: + +```xml + +``` + +如果不需要区分版本,可以按照以下的方式配置 [[1\]](http://dubbo.apache.org/zh-cn/docs/user/demos/multi-versions.html#fn1): + +```xml + +``` + +## 十一、Dubbo SPI + +SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是**将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类**。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。 + +Dubbo SPI 的相关逻辑被封装在了 `ExtensionLoader` 类中,通过 `ExtensionLoader`,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 `META-INF/dubbo` 路径下。 + +## 参考资料 + +- **官方** + - [Dubbo Github](https://github.com/apache/dubbo) + - [Dubbo 官方文档](https://dubbo.apache.org/zh-cn/) + - [管理员手册](https://dubbo.gitbooks.io/dubbo-admin-book/content/) +- **文章** + - [如何基于 Dubbo 进行服务治理、服务降级、失败重试以及超时重试?](https://github.com/doocs/advanced-java/blob/master/docs/distributed-system/dubbo-service-management.md) diff --git a/docs/mq/README.md b/docs/mq/README.md new file mode 100644 index 00000000..befe06ff --- /dev/null +++ b/docs/mq/README.md @@ -0,0 +1,48 @@ +# Java 和消息队列 + +> 消息队列(Message Queue,简称 MQ)技术是分布式应用间交换信息的一种技术。 +> +> 消息队列主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 +> +> 如果想深入学习各种消息队列产品,建议先了解一下 [消息队列基本原理](https://github.com/dunwu/blog/blob/master/source/_posts/theory/mq.md) ,有助于理解消息队列特性的实现和设计思路。 + +## 内容 + +- [消息队列基本原理](消息队列基本原理.md) +- [消息队列面经](消息队列面试.md) +- [Kafka 教程](https://dunwu.github.io/bigdata-tutorial/kafka) 📚 +- [RocketMQ](rocketmq.md) +- [ActiveMQ](activemq.md) + +## 技术对比 + +| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | +| ------------------------ | ------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| 单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 | +| topic 数量对吞吐量的影响 | | | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | +| 时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 | +| 可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | +| 消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ | +| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 | + +综上,各种对比之后,有如下建议: + +- 一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了; +- 后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高; +- 不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 [Apache](https://github.com/apache/rocketmq),但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。 +- 所以**中小型公司**,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;**大型公司**,基础架构研发实力较强,用 RocketMQ 是很好的选择。 +- 如果是**大数据领域**的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 + +## 📚 资料 + +- **Kafka** + - [Kafka Github](https://github.com/apache/kafka) + - [Kafka 官网](http://kafka.apache.org/) + - [Kafka 官方文档](https://kafka.apache.org/documentation/) + - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) +- **ActiveMQ** + - [ActiveMQ 官网](http://activemq.apache.org/) + +## 🚪 传送 + +◾ 🏠 [JAVATECH 首页](https://github.com/dunwu/javatech) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ diff --git a/docs/mq/activemq.md b/docs/mq/activemq.md new file mode 100644 index 00000000..0dbedcc1 --- /dev/null +++ b/docs/mq/activemq.md @@ -0,0 +1,295 @@ +# ActiveMQ + +## JMS 基本概念 + +`JMS` 即 **Java 消息服务(Java Message Service)API**,是一个 Java 平台中关于面向消息中间件的 API。它用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java 消息服务是一个与具体平台无关的 API,绝大多数 MOM 提供商都对 JMS 提供支持。 + +### 消息模型 + +JMS 有两种消息模型: + +- Point-to-Point(P2P) +- Publish/Subscribe(Pub/Sub) + +#### P2P 的特点 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javalib/jms/jms-pointToPoint.gif) + +在点对点的消息系统中,消息分发给一个单独的使用者。点对点消息往往与队列 `javax.jms.Queue` 相关联。 + +每个消息只有一个消费者(Consumer)(即一旦被消费,消息就不再在消息队列中)。 + +发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行,它不会影响到消息被发送到队列。 + +接收者在成功接收消息之后需向队列应答成功。 + +如果你希望发送的每个消息都应该被成功处理的话,那么你需要 P2P 模式。 + +#### Pub/Sub 的特点 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javalib/jms/jms-publishSubscribe.gif) + +发布/订阅消息系统支持一个事件驱动模型,消息生产者和消费者都参与消息的传递。生产者发布事件,而使用者订阅感兴趣的事件,并使用事件。该类型消息一般与特定的主题 `javax.jms.Topic` 关联。 + +每个消息可以有多个消费者。 + +发布者和订阅者之间有时间上的依赖性。针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息,而且为了消费消息,订阅者必须保持运行的状态。 + +为了缓和这样严格的时间相关性,JMS 允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有被激活(运行),它也能接收到发布者的消息。 + +如果你希望发送的消息可以不被做任何处理、或者被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用 Pub/Sub 模型。 + +### JMS 编程模型 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javalib/jms/jms-publishSubscribe.gif) + +#### ConnectionFactory + +创建 `Connection` 对象的工厂,针对两种不同的 jms 消息模型,分别有 `QueueConnectionFactory` 和`TopicConnectionFactory` 两种。可以通过 JNDI 来查找 `ConnectionFactory` 对象。 + +#### Connection + +`Connection` 表示在客户端和 JMS 系统之间建立的链接(对 TCP/IP socket 的包装)。`Connection` 可以产生一个或多个`Session`。跟 `ConnectionFactory` 一样,`Connection` 也有两种类型:`QueueConnection` 和 `TopicConnection`。 + +#### Destination + +`Destination` 是一个包装了消息目标标识符的被管对象。消息目标是指消息发布和接收的地点,或者是队列 `Queue` ,或者是主题 `Topic` 。JMS 管理员创建这些对象,然后用户通过 JNDI 发现它们。和连接工厂一样,管理员可以创建两种类型的目标,点对点模型的 `Queue`,以及发布者/订阅者模型的 `Topic`。 + +#### Session + +`Session` 表示一个单线程的上下文,用于发送和接收消息。由于会话是单线程的,所以消息是连续的,就是说消息是按照发送的顺序一个一个接收的。会话的好处是它支持事务。如果用户选择了事务支持,会话上下文将保存一组消息,直到事务被提交才发送这些消息。在提交事务之前,用户可以使用回滚操作取消这些消息。一个会话允许用户创建消息,生产者来发送消息,消费者来接收消息。同样,`Session` 也分 `QueueSession` 和 `TopicSession`。 + +#### MessageConsumer + +`MessageConsumer` 由 `Session` 创建,并用于将消息发送到 `Destination`。消费者可以同步地(阻塞模式),或(非阻塞)接收 `Queue` 和 `Topic` 类型的消息。同样,消息生产者分两种类型:`QueueSender` 和`TopicPublisher`。 + +#### MessageProducer + +`MessageProducer` 由 `Session` 创建,用于接收被发送到 `Destination` 的消息。`MessageProducer` 有两种类型:`QueueReceiver` 和 `TopicSubscriber`。可分别通过 `session` 的 `createReceiver(Queue)` 或 `createSubscriber(Topic)` 来创建。当然,也可以 `session` 的 `creatDurableSubscriber` 方法来创建持久化的订阅者。 + +#### Message + +是在消费者和生产者之间传送的对象,也就是说从一个应用程序传送到另一个应用程序。一个消息有三个主要部分: + +- 消息头(必须):包含用于识别和为消息寻找路由的操作设置。 +- 一组消息属性(可选):包含额外的属性,支持其他提供者和用户的兼容。可以创建定制的字段和过滤器(消息选择器)。 +- 一个消息体(可选):允许用户创建五种类型的消息(文本消息,映射消息,字节消息,流消息和对象消息)。 + +消息接口非常灵活,并提供了许多方式来定制消息的内容。 + +| Common | Point-to-Point | Publish-Subscribe | +| ----------------- | --------------------------- | ---------------------- | +| ConnectionFactory | QueueConnectionFactory | TopicConnectionFactory | +| Connection | QueueConnection | TopicConnection | +| Destination | Queue | Topic | +| Session | QueueSession | TopicSession | +| MessageProducer | QueueSender | TopicPublisher | +| MessageSender | QueueReceiver, QueueBrowser | TopicSubscriber | + +## 安装 + +**安装条件** + +JDK1.7 及以上版本 + +本地配置了 **JAVA_HOME** 环境变量。 + +**下载** + +支持 Windows/Unix/Linux/Cygwin + +[ActiveMQ 官方下载地址](http://activemq.apache.org/download.html) + +**Windows 下运行** + +(1)解压压缩包到本地; + +(2)打开控制台,进入安装目录的 `bin` 目录下; + +``` +cd [activemq_install_dir] +``` + +(3)执行 `activemq start` 来启动 ActiveMQ + +``` +bin\activemq start +``` + +**测试安装是否成功** + +(1)ActiveMQ 默认监听端口为 61616 + +``` +netstat -an|find “61616” +``` + +(2)访问 + +(3)输入用户名、密码 + +``` +Login: admin +Passwort: admin +``` + +(4)点击导航栏的 Queues 菜单 + +(5)添加一个队列(queue) + +## 项目中的应用 + +**引入依赖** + +```xml + + org.apache.activemq + activemq-all + 5.14.1 + +``` + +**Sender.java** + +```java +public class Sender { + private static final int SEND_NUMBER = 4; + + public static void main(String[] args) { + // ConnectionFactory :连接工厂,JMS 用它创建连接 + ConnectionFactory connectionFactory; + // Connection :JMS 客户端到JMS Provider 的连接 + Connection connection = null; + // Session: 一个发送或接收消息的线程 + Session session; + // Destination :消息的目的地;消息发送给谁. + Destination destination; + // MessageProducer:消息发送者 + MessageProducer producer; + // TextMessage message; + // 构造ConnectionFactory实例对象,此处采用ActiveMq的实现jar + connectionFactory = new ActiveMQConnectionFactory( + ActiveMQConnection.DEFAULT_USER, + ActiveMQConnection.DEFAULT_PASSWORD, + "tcp://localhost:61616"); + try { + // 构造从工厂得到连接对象 + connection = connectionFactory.createConnection(); + // 启动 + connection.start(); + // 获取操作连接 + session = connection.createSession(Boolean.TRUE, + Session.AUTO_ACKNOWLEDGE); + // 获取session注意参数值xingbo.xu-queue是一个服务器的queue,须在在ActiveMq的console配置 + destination = session.createQueue("FirstQueue"); + // 得到消息生成者【发送者】 + producer = session.createProducer(destination); + // 设置不持久化,此处学习,实际根据项目决定 + producer.setDeliveryMode(DeliveryMode.NON_PERSISTENT); + // 构造消息,此处写死,项目就是参数,或者方法获取 + sendMessage(session, producer); + session.commit(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (null != connection) + connection.close(); + } catch (Throwable ignore) { + } + } + } + + public static void sendMessage(Session session, MessageProducer producer) + throws Exception { + for (int i = 1; i <= SEND_NUMBER; i++) { + TextMessage message = session + .createTextMessage("ActiveMq 发送的消息" + i); + // 发送消息到目的地方 + System.out.println("发送消息:" + "ActiveMq 发送的消息" + i); + producer.send(message); + } + } +} +``` + +**Receiver.java** + +```java +public class Receiver { + public static void main(String[] args) { + // ConnectionFactory :连接工厂,JMS 用它创建连接 + ConnectionFactory connectionFactory; + // Connection :JMS 客户端到JMS Provider 的连接 + Connection connection = null; + // Session: 一个发送或接收消息的线程 + Session session; + // Destination :消息的目的地;消息发送给谁. + Destination destination; + // 消费者,消息接收者 + MessageConsumer consumer; + connectionFactory = new ActiveMQConnectionFactory( + ActiveMQConnection.DEFAULT_USER, + ActiveMQConnection.DEFAULT_PASSWORD, + "tcp://localhost:61616"); + try { + // 构造从工厂得到连接对象 + connection = connectionFactory.createConnection(); + // 启动 + connection.start(); + // 获取操作连接 + session = connection.createSession(Boolean.FALSE, + Session.AUTO_ACKNOWLEDGE); + // 获取session注意参数值xingbo.xu-queue是一个服务器的queue,须在在ActiveMq的console配置 + destination = session.createQueue("FirstQueue"); + consumer = session.createConsumer(destination); + while (true) { + //设置接收者接收消息的时间,为了便于测试,这里谁定为100s + TextMessage message = (TextMessage) consumer.receive(100000); + if (null != message) { + System.out.println("收到消息" + message.getText()); + } else { + break; + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + if (null != connection) + connection.close(); + } catch (Throwable ignore) { + } + } + } +} +``` + +**运行** + +先运行 Receiver.java 进行消息监听,再运行 Send.java 发送消息。 + +**输出** + +Send 的输出内容 + +``` +发送消息:Activemq 发送消息0 +发送消息:Activemq 发送消息1 +发送消息:Activemq 发送消息2 +发送消息:Activemq 发送消息3 +``` + +Receiver 的输出内容 + +``` +收到消息ActiveMQ 发送消息0 +收到消息ActiveMQ 发送消息1 +收到消息ActiveMQ 发送消息2 +收到消息ActiveMQ 发送消息3 +``` + +## 资源 + +- [ActiveMQ 官网](http://activemq.apache.org/) +- [oracle 官方的 jms 介绍](https://docs.oracle.com/cd/E19575-01/819-3669/6n5sg7cgq/index.html) diff --git a/docs/mq/rocketmq.md b/docs/mq/rocketmq.md new file mode 100644 index 00000000..0aba5e5c --- /dev/null +++ b/docs/mq/rocketmq.md @@ -0,0 +1,511 @@ +# RocketMQ + + + +- [简介](#简介) +- [安装](#安装) + - [环境要求](#环境要求) + - [下载解压](#下载解压) + - [启动 Name Server](#启动-name-server) + - [启动 Broker](#启动-broker) + - [收发消息](#收发消息) + - [关闭服务器](#关闭服务器) +- [API](#api) + - [Producer](#producer) + - [Consumer](#consumer) + - [FAQ](#faq) +- [架构](#架构) + - [NameServer](#nameserver) + - [Broker](#broker) + - [Producer](#producer-1) + - [Consumer](#consumer-1) +- [原理](#原理) + - [顺序消息](#顺序消息) + - [消息重复](#消息重复) + - [事务消息](#事务消息) +- [参考资料](#参考资料) + + + +## 简介 + +RocketMQ 是一款开源的分布式消息队列,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。 + +RocketMQ 被阿里巴巴捐赠给 Apache,成为 Apache 的孵化项目。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/distributed/mq/rocketmq/rmq-model.png) + +RocketMQ 有以下核心概念: + +- **Producer** - 将业务应用程序系统生成的消息发送给代理。RocketMQ 提供多种发送范例:同步,异步和单向。 + - **Producer Group** - 具有相同角色的 Producer 组合在一起。如果原始 Producer 在事务之后崩溃,则代理可以联系同一 Producer 组的不同 Producer 实例以提交或回滚事务。**_警告:考虑到提供的 Producer 在发送消息方面足够强大,每个 Producer 组只允许一个实例,以避免不必要的生成器实例初始化。_** +- **Consumer** - Consumer 从 Broker 那里获取消息并将其提供给应用程序。从用户应用的角度来看,提供了两种类型的 Consumer: + - **PullConsumer** - PullConsumer 积极地从 Broker 那里获取消息。一旦提取了批量消息,用户应用程序就会启动消费过程。 + - **PushConsumer** - PushConsumer 封装消息提取,消费进度并维护其他内部工作,为最终用户留下回调接口,这个借口会在消息到达时被执行。 + - **Consumer Group** - 完全相同角色的 Consumer 被组合在一起并命名为 Consumer Group。Consumer Group 是一个很好的概念,在消息消费方面实现负载平衡和容错目标非常容易。**_警告:Consumer Group 中的 Consumer 实例必须具有完全相同的主题订阅。_** +- **Broker** - Broker 是 RocketMQ 的主要组成部分。它接收从 Producer 发送的消息,存储它们并准备处理来自 Consumer 的消费请求。它还存储与消息相关的元数据,包括 Consumer Group,消耗进度偏移和主题/队列信息。 +- Name Server - 充当路由信息提供者。Producer/Consumer 客户查找主题以查找相应的 Broker 列表。 +- **Topic** - 是 Producer 传递消息和 Consumer 提取消息的类别。 +- **Message** - 是要传递的信息。消息必须有一个主题,可以将其解释为您要发送给的邮件地址。消息还可以具有可选 Tag 和额外的键值对。例如,您可以为消息设置业务密钥,并在代理服务器上查找消息以诊断开发期间的问题。 + - **Message Queue** - 主题被划分为一个或多个子主题“消息队列”。 + - **Tag** - 即子主题,为用户提供了额外的灵活性。对于 Tag,来自同一业务模块的具有不同目的的消息可以具有相同的主题和不同的 Tag。 + +## 安装 + +### 环境要求 + +- 推荐 64 位操作系统:Linux/Unix/Mac +- 64bit JDK 1.8+ +- Maven 3.2.x +- Git + +### 下载解压 + +进入官方下载地址: + +建议选择 binary 版本。 + +解压到本地: + +```bash +> unzip rocketmq-all-4.2.0-source-release.zip +> cd rocketmq-all-4.2.0/ +``` + +### 启动 Name Server + +```bash +> nohup sh bin/mqnamesrv & +> tail -f ~/logs/rocketmqlogs/namesrv.log +The Name Server boot success... +``` + +### 启动 Broker + +```bash +> nohup sh bin/mqbroker -n localhost:9876 -c conf/broker.conf & +> tail -f ~/logs/rocketmqlogs/broker.log +The broker[%s, 172.30.30.233:10911] boot success... +``` + +### 收发消息 + +执行收发消息操作之前,不许告诉客户端命名服务器的位置。在 RocketMQ 中有多种方法来实现这个目的。这里,我们使用最简单的方法——设置环境变量 `NAMESRV_ADDR` : + +```bash +> export NAMESRV_ADDR=localhost:9876 +> sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer +SendResult [sendStatus=SEND_OK, msgId= ... + +> sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer +ConsumeMessageThread_%d Receive New Messages: [MessageExt... +``` + +### 关闭服务器 + +```bash +> sh bin/mqshutdown broker +The mqbroker(36695) is running... +Send shutdown request to mqbroker(36695) OK + +> sh bin/mqshutdown namesrv +The mqnamesrv(36664) is running... +Send shutdown request to mqnamesrv(36664) OK +``` + +## API + +首先在项目中引入 maven 依赖: + +```xml + + org.apache.rocketmq + rocketmq-client + 4.2.0 + +``` + +### Producer + +Producer 在 RocketMQ 中负责发送消息。 + +RocketMQ 有三种消息发送方式: + +- 可靠的同步发送 +- 可靠的异步发送 +- 单项发送 + +#### 可靠的同步发送 + +可靠的同步传输用于广泛的场景,如重要的通知消息,短信通知,短信营销系统等。 + +```java +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"); + //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(); + } +} +``` + +#### 可靠的异步发送 + +异步传输通常用于响应时间敏感的业务场景。 + +```java +public class AsyncProducer { + public static void main(String[] args) throws Exception { + //Instantiate with a producer group name. + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); + //Launch the instance. + producer.start(); + producer.setRetryTimesWhenSendAsyncFailed(0); + for (int i = 0; i < 100; i++) { + final int index = i; + //Create a message instance, specifying topic, tag and message body. + Message msg = new Message("TopicTest", + "TagA", + "OrderID188", + "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); + producer.send(msg, new SendCallback() { + @Override + public void onSuccess(SendResult sendResult) { + System.out.printf("%-10d OK %s %n", index, + sendResult.getMsgId()); + } + @Override + public void onException(Throwable e) { + System.out.printf("%-10d Exception %s %n", index, e); + e.printStackTrace(); + } + }); + } + //Shut down once the producer instance is not longer in use. + producer.shutdown(); + } +} +``` + +#### 单向传输 + +单向传输用于需要中等可靠性的情况,例如日志收集。 + +```java +public class OnewayProducer { + public static void main(String[] args) throws Exception{ + //Instantiate with a producer group name. + DefaultMQProducer producer = new DefaultMQProducer("ExampleProducerGroup"); + //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); + + } + //Shut down once the producer instance is not longer in use. + producer.shutdown(); + } +} +``` + +### Consumer + +Consumer 在 RocketMQ 中负责接收消息。 + +```java +public class OrderedConsumer { + public static void main(String[] args) throws Exception { + DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); + consumer.setNamesrvAddr(RocketConfig.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"); + } +} +``` + +### FAQ + +#### connect to `<172.17.0.1:10909>` failed + +启动后,Producer 客户端连接 RocketMQ 时报错: + +```java +org.apache.rocketmq.remoting.exception.RemotingConnectException: connect to <172.17.0.1:10909> failed + at org.apache.rocketmq.remoting.netty.NettyRemotingClient.invokeSync(NettyRemotingClient.java:357) + at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessageSync(MQClientAPIImpl.java:343) + at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessage(MQClientAPIImpl.java:327) + at org.apache.rocketmq.client.impl.MQClientAPIImpl.sendMessage(MQClientAPIImpl.java:290) + at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendKernelImpl(DefaultMQProducerImpl.java:688) + at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.sendSelectImpl(DefaultMQProducerImpl.java:901) + at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:878) + at org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl.send(DefaultMQProducerImpl.java:873) + at org.apache.rocketmq.client.producer.DefaultMQProducer.send(DefaultMQProducer.java:369) + at com.emrubik.uc.mdm.sync.utils.MdmInit.sendMessage(MdmInit.java:62) + at com.emrubik.uc.mdm.sync.utils.MdmInit.main(MdmInit.java:2149) +``` + +原因:RocketMQ 部署在虚拟机上,内网 ip 为 10.10.30.63,该虚拟机一个 docker0 网卡,ip 为 172.17.0.1。RocketMQ broker 启动时默认使用了 docker0 网卡,Producer 客户端无法连接 172.17.0.1,造成以上问题。 + +解决方案 + +(1)干掉 docker0 网卡或修改网卡名称 + +(2)停掉 broker,修改 broker 配置文件,重启 broker。 + +修改 conf/broker.conf,增加两行来指定启动 broker 的 IP: + +``` +namesrvAddr = 10.10.30.63:9876 +brokerIP1 = 10.10.30.63 +``` + +启动时需要指定配置文件 + +```bash +nohup sh bin/mqbroker -n localhost:9876 -c conf/broker.conf & +``` + +## 架构 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/distributed/mq/rocketmq/rmq-basic-arc.png) + +RocketMQ 由四部分组成:NameServer、Broker、Producer、Consumer。其中任意一个组成都可以水平扩展为集群模式,以避免单点故障问题。 + +### Producer + +Producers 支持分布式集群方式部署。Producer 通过 MQ 的负载均衡模块选择相应的 Broker 集群队列进行消息投递,投递的过程支持快速失败并且低延迟。 + +### Consumer + +Consumer 支持分布式集群方式部署。支持以 push 推,pull 拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,它提供实时消息订阅机制,可以满足大多数用户的需求。 + +### NameServer + +NameServer 是一个 Topic 路由注册中心,其角色类似 Dubbo 中的 zookeeper,支持 Broker 的动态注册与发现。主要包括两个功能: + +- **Broker 管理**,NameServer 接受 Broker 集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查 Broker 是否还存活; +- **路由信息管理**,每个 NameServer 将保存关于 Broker 集群的整个路由信息和用于客户端查询的队列信息。然后 Producer 和 Conumser 通过 NameServer 就可以知道整个 Broker 集群的路由信息,从而进行消息的投递和消费。 + +NameServer 通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker 是向每一台 NameServer 注册自己的路由信息,所以每一个 NameServer 实例上面都保存一份完整的路由信息。当某个 NameServer 因某种原因下线了,Broker 仍然可以向其它 NameServer 同步其路由信息,Producer、Consumer 仍然可以动态感知 Broker 的路由的信息。 + +NameServer 是一个功能齐全的服务器,主要包括两个功能: + +1. Broker 管理 - NameServer 接受来自 Broker 集群的注册,并提供心跳机制来检查 Broker 节点是否存活。 +2. 路由管理 - 每个 NameServer 将保存有关 Broker 集群的完整路由信息和客户端查询的查询队列。 + +RocketMQ 客户端(Producer/Consumer)将从 NameServer 查询队列路由信息。 + +将 NameServer 地址列表提供给客户端有四种方法: + +1. 编程方式 - 类似:`producer.setNamesrvAddr("ip:port")` +2. Java 选项 - 使用 `rocketmq.namesrv.addr` 参数 +3. 环境变量 - 设置环境变量 `NAMESRV_ADDR` +4. HTTP 端点 + +> 更详细信息可以参考官方文档:[here](http://rocketmq.apache.org/rocketmq/four-methods-to-feed-name-server-address-list/) + +### Broker + +Broker 主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker 包含了以下几个重要子模块。 + +Broker 有几个重要的子模块: + +- **Remoting Module**:整个 Broker 的实体,负责处理来自 clients 端的请求。 +- **Client Manager**:负责管理客户端(Producer/Consumer)和维护 Consumer 的 Topic 订阅信息。 +- **Store Service**:提供方便简单的 API 接口处理消息存储到物理硬盘和查询功能。 +- **HA Service**:高可用服务,提供 Master Broker 和 Slave Broker 之间的数据同步功能。 +- **Index Service**:根据特定的 Message key 对投递到 Broker 的消息进行索引服务,以提供消息的快速查询。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/distributed/mq/rocketmq/rmq-basic-component.png) + +## 原理 + +分布式消息系统作为实现分布式系统可扩展、可伸缩性的关键组件,需要具有高吞吐量、高可用等特点。而谈到消息系统的设计,就回避不了两个问题: + +1. 消息的顺序问题 +2. 消息的重复问题 + +### 顺序消息 + +#### 第一种模型 + +假如生产者产生了 2 条消息:M1、M2,要保证这两条消息的顺序,应该怎样做?你脑中想到的可能是这样: + +
    + +
    + +假定 M1 发送到 S1,M2 发送到 S2,如果要保证 M1 先于 M2 被消费,那么需要 M1 到达消费端被消费后,通知 S2,然后 S2 再将 M2 发送到消费端。 + +这个模型存在的问题是,如果 M1 和 M2 分别发送到两台 Server 上,就不能保证 M1 先达到 MQ 集群,也不能保证 M1 被先消费。换个角度看,如果 M2 先于 M1 达到 MQ 集群,甚至 M2 被消费后,M1 才达到消费端,这时消息也就乱序了,说明以上模型是不能保证消息的顺序的。 + +
    + +
    + +#### 第二种模型 + +如何才能在 MQ 集群保证消息的顺序?一种简单的方式就是将 M1、M2 发送到同一个 Server 上: + +这样可以保证 M1 先于 M2 到达 MQServer(生产者等待 M1 发送成功后再发送 M2),根据先达到先被消费的原则,M1 会先于 M2 被消费,这样就保证了消息的顺序。 + +这个模型也仅仅是理论上可以保证消息的顺序,在实际场景中可能会遇到下面的问题: + +
    + +
    + +只要将消息从一台服务器发往另一台服务器,就会存在网络延迟问题。如上图所示,如果发送 M1 耗时大于发送 M2 的耗时,那么 M2 就仍将被先消费,仍然不能保证消息的顺序。即使 M1 和 M2 同时到达消费端,由于不清楚消费端 1 和消费端 2 的负载情况,仍然有可能出现 M2 先于 M1 被消费的情况。 + +如何解决这个问题?将 M1 和 M2 发往同一个消费者,且发送 M1 后,需要消费端响应成功后才能发送 M2。 + +这可能产生另外的问题:如果 M1 被发送到消费端后,消费端 1 没有响应,那是继续发送 M2 呢,还是重新发送 M1?一般为了保证消息一定被消费,肯定会选择重发 M1 到另外一个消费端 2,就如下图所示。 + +
    + +
    + +这样的模型就严格保证消息的顺序,细心的你仍然会发现问题,消费端 1 没有响应 Server 时有两种情况,一种是 M1 确实没有到达(数据在网络传送中丢失),另外一种消费端已经消费 M1 且已经发送响应消息,只是 MQ Server 端没有收到。如果是第二种情况,重发 M1,就会造成 M1 被重复消费。也就引入了我们要说的第二个问题,消息重复问题,这个后文会详细讲解。 + +回过头来看消息顺序问题,严格的顺序消息非常容易理解,也可以通过文中所描述的方式来简单处理。总结起来,要实现严格的顺序消息,简单且可行的办法就是: + +**保证生产者 - MQServer - 消费者是一对一对一的关系。** + +这样的设计虽然简单易行,但也会存在一些很严重的问题,比如: + +1. 并行度就会成为消息系统的瓶颈(吞吐量不够) +2. 更多的异常处理,比如:只要消费端出现问题,就会导致整个处理流程阻塞,我们不得不花费更多的精力来解决阻塞的问题。 + +RocketMQ 的解决方案:通过合理的设计或者将问题分解来规避。如果硬要把时间花在解决问题本身,实际上不仅效率低下,而且也是一种浪费。从这个角度来看消息的顺序问题,我们可以得出两个结论: + +1. 不关注乱序的应用实际大量存在 +2. 队列无序并不意味着消息无序 + +最后我们从源码角度分析 RocketMQ 怎么实现发送顺序消息。 + +RocketMQ 通过轮询所有队列的方式来确定消息被发送到哪一个队列(负载均衡策略)。比如下面的示例中,订单号相同的消息会被先后发送到同一个队列中: + +```java +// RocketMQ 通过 MessageQueueSelector 中实现的算法来确定消息发送到哪一个队列上 +// RocketMQ 默认提供了两种 MessageQueueSelector 实现:随机/Hash +// 当然你可以根据业务实现自己的 MessageQueueSelector 来决定消息按照何种策略发送到消息队列中 +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); +``` + +在获取到路由信息以后,会根据 MessageQueueSelector 实现的算法来选择一个队列,同一个 OrderId 获取到的肯定是同一个队列。 + +```java +private SendResult send() { + // 获取topic路由信息 + TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic()); + if (topicPublishInfo != null && topicPublishInfo.ok()) { + MessageQueue mq = null; + // 根据我们的算法,选择一个发送队列 + // 这里的arg = orderId + mq = selector.select(topicPublishInfo.getMessageQueueList(), msg, arg); + if (mq != null) { + return this.sendKernelImpl(msg, mq, communicationMode, sendCallback, timeout); + } + } +} +``` + +### 消息重复 + +造成消息重复的根本原因是:网络不可达。只要通过网络交换数据,就无法避免这个问题。所以解决这个问题的办法就是绕过这个问题。那么问题就变成了:如果消费端收到两条一样的消息,应该怎样处理? + +1. 消费端处理消息的业务逻辑保持幂等性。 +2. 保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。 + +第 1 条很好理解,只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样。 + +第 2 条原理就是利用一张日志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条消息。 + +第 1 条解决方案,很明显应该在消费端实现,不属于消息系统要实现的功能。 + +第 2 条可以消息系统实现,也可以业务端实现。正常情况下出现重复消息的概率其实很小,如果由消息系统来实现的话,肯定会对消息系统的吞吐量和高可用有影响,所以最好还是由业务端自己处理消息重复的问题,这也是 RocketMQ 不解决消息重复的问题的原因。 + +**RocketMQ 不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重。** + +### 事务消息 + +RocketMQ 除了支持普通消息,顺序消息,另外还支持事务消息。 + +假设这样的场景: + +![img](https://upload-images.jianshu.io/upload_images/3101171-253d8bd65736694f.png) + +图中执行本地事务(Bob 账户扣款)和发送异步消息应该保证同时成功或者同时失败,也就是扣款成功了,发送消息一定要成功,如果扣款失败了,就不能再发送消息。那问题是:我们是先扣款还是先发送消息呢? + +![img](http://upload-images.jianshu.io/upload_images/3101171-088dc074c4ecd192) + +RocketMQ 分布式事务步骤: + +发送 Prepared 消息 2222222222222222222,并拿到接受消息的地址。 +执行本地事务 +通过第 1 步骤拿到的地址去访问消息,并修改消息状态。 + +## 参考资料 + +- [RocketMQ 官方文档](http://rocketmq.apache.org/docs/quick-start/) +- [分布式开放消息系统(RocketMQ)的原理与实践](https://www.jianshu.com/p/453c6e7ff81c) diff --git "a/docs/mq/\346\266\210\346\201\257\351\230\237\345\210\227\345\237\272\346\234\254\345\216\237\347\220\206.md" "b/docs/mq/\346\266\210\346\201\257\351\230\237\345\210\227\345\237\272\346\234\254\345\216\237\347\220\206.md" new file mode 100644 index 00000000..33ce32ca --- /dev/null +++ "b/docs/mq/\346\266\210\346\201\257\351\230\237\345\210\227\345\237\272\346\234\254\345\216\237\347\220\206.md" @@ -0,0 +1,597 @@ +--- +title: 消息队列基本原理 +categories: ['分布式'] +tags: ['分布式', '消息队列'] +date: 2019-07-05 15:11 +--- + +# 消息队列基本原理 + +> 📦 本文已归档到:「[blog](https://github.com/dunwu/blog)」 +> +> 消息队列(Message Queue,简称 MQ)技术是**应用间交换信息**的一种技术。 +> +> 消息队列主要解决异步处理、应用间耦合,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 +> +> 目前主流的 MQ 有:Kafka、RabbitMQ、RocketMQ、ActiveMQ,而部分数据库如 Redis、MySQL 以及 phxsql 也可实现消息队列的功能。 +> +> 注意:_为了简便,下文中除了文章标题,一律使用 MQ 简称_。 + + + +- [1. MQ 的简介](#1-mq-的简介) + - [1.1. 什么是 MQ](#11-什么是-mq) + - [1.2. MQ 通信模型](#12-mq-通信模型) +- [2. MQ 的应用](#2-mq-的应用) + - [2.1. 异步处理](#21-异步处理) + - [2.2. 系统解耦](#22-系统解耦) + - [2.3. 流量削峰](#23-流量削峰) + - [2.4. 传输缓冲](#24-传输缓冲) + - [2.5. 最终一致性](#25-最终一致性) + - [2.6. 系统间通信](#26-系统间通信) +- [3. MQ 的问题](#3-mq-的问题) + - [3.1. 重复消费](#31-重复消费) + - [3.2. 消息丢失](#32-消息丢失) + - [3.3. 消息的顺序性](#33-消息的顺序性) + - [3.4. 消息积压](#34-消息积压) +- [4. MQ 的高可用](#4-mq-的高可用) + - [4.1. Kafka 的高可用](#41-kafka-的高可用) +- [5. 主流 MQ](#5-主流-mq) + - [5.1. ActiveMQ](#51-activemq) + - [5.2. RabbitMQ](#52-rabbitmq) + - [5.3. RocketMQ](#53-rocketmq) + - [5.4. Kafka](#54-kafka) + - [5.5. MQ 的技术选型](#55-mq-的技术选型) +- [6. JMS](#6-jms) + - [6.1. 消息模型](#61-消息模型) + - [6.2. 消息消费](#62-消息消费) + - [6.3. JMS 编程模型](#63-jms-编程模型) +- [7. 参考资料](#7-参考资料) + + + +## 1. MQ 的简介 + +### 1.1. 什么是 MQ + +消息队列(Message Queue,简称 MQ)技术是应用间交换信息的一种技术。 + +消息队列主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 + +MQ 是消费-生产者模型的一个典型的代表,一端往消息队列中不断写入消息,而另一端则可以读取队列中的消息。消息发布者只管把消息发布到 MQ 中而不用管谁来取,消息使用者只管从 MQ 中取消息而不管是谁发布的。这样发布者和使用者都不用知道对方的存在。 + +MQ 的数据可驻留在内存或磁盘上,直到它们被应用程序读取。通过 MQ,应用程序可独立地执行,它们不需要知道彼此的位置,不需要等待接收程序接收此消息。在分布式计算环境中,为了集成分布式应用,开发者需要对异构网络环境下的分布式应用提供有效的通信手段。为了管理需要共享的信息,对应用提供公共的信息交换机制是重要的。 + +目前主流的 MQ 有:Kafka、RabbitMQ、RocketMQ、ActiveMQ。 + +### 1.2. MQ 通信模型 + +MQ 通信模型大致有以下类型: + +- **点对点** - 点对点方式是最为传统和常见的通讯方式,它支持一对一、一对多、多对多、多对一等多种配置方式,支持树状、网状等多种拓扑结构。 +- **多点广播** - MQ 适用于不同类型的应用。其中重要的,也是正在发展中的是"多点广播"应用,即能够将消息发送到多个目标站点 (Destination List)。可以使用一条 MQ 指令将单一消息发送到多个目标站点,并确保为每一站点可靠地提供信息。MQ 不仅提供了多点广播的功能,而且还拥有智能消息分发功能,在将一条消息发送到同一系统上的多个用户时,MQ 将消息的一个复制版本和该系统上接收者的名单发送到目标 MQ 系统。目标 MQ 系统在本地复制这些消息,并将它们发送到名单上的队列,从而尽可能减少网络的传输量。 +- **发布/订阅 (Publish/Subscribe)** - 发布/订阅模式使消息的分发可以突破目的队列地理位置的限制,使消息按照特定的主题甚至内容进行分发,用户或应用程序可以根据主题或内容接收到所需要的消息。发布/订阅模式使得发送者和接收者之间的耦合关系变得更为松散,发送者不必关心接收者的目的地址,而接收者也不必关心消息的发送地址,而只是根据消息的主题进行消息的收发。 +- **集群 (Cluster)** - 为了简化点对点通讯模式中的系统配置,MQ 提供 Cluster(集群) 的解决方案。集群类似于一个域 (Domain),集群内部的队列管理器之间通讯时,不需要两两之间建立消息通道,而是采用集群 (Cluster) 通道与其它成员通讯,从而大大简化了系统配置。此外,集群中的队列管理器之间能够自动进行负载均衡,当某一队列管理器出现故障时,其它队列管理器可以接管它的工作,从而大大提高系统的高可靠性。 + +## 2. MQ 的应用 + +### 2.1. 异步处理 + +> MQ 可以将系统间的处理流程异步化,减少等待响应的时间,从而提高整体并发吞吐量。 +> +> 一般,MQ 异步处理应用于非核心流程,例如:短信/邮件通知、数据推送、上报数据到监控中心/日志中心等。 + +假设这样一个场景,用户向系统 A 发起请求,系统 A 处理计算只需要 10 ms,然后通知系统 BCD 写库,系统 BCD 写库耗时分别为:100ms、200ms、300ms。最终总耗时为: 10+100ms+200ms+300ms=610ms。此外,加上请求和响应的网络传输时间,从用户角度看,可能要等待将近 1s 才能得到结果。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/design/theory/mq/mq_3.png) + +如果使用 MQ,系统 A 接到请求后,耗时 10ms 处理计算,然后向系统 BCD 连续发送消息,假设耗时 5ms。那么 这一过程的总耗时为 3ms + 5ms = 8ms,这相比于 610 ms,大大缩短了响应时间。至于系统 BCD 的写库操作,只要自行消费 MQ 后处理即可,用户无需关注。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/design/theory/mq/mq_4.png) + +### 2.2. 系统解耦 + +> 通过 MQ,可以消除系统间的强耦合。它的好处在于: +> +> - 消息的消费者系统可以随意增加,无需修改生产者系统的代码。 +> - 生产者系统、消费者系统彼此不会影响对方的流程。 +> - 如果生产者系统宕机,消费者系统收不到消息,就不会有下一步的动作。 +> - 如果消费者系统宕机,生产者系统让然可以正常发送消息,不影响流程。 + +不同系统如果要建立通信,传统的做法是:调用接口。 + +如果需要和新的系统建立通信或删除已建立的通信,都需要修改代码,这种方案显然耦合度很高。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/design/theory/mq/mq_1.png) + +如果使用 MQ,系统间的通信只需要通过发布/订阅(Pub/Sub)模型即可,彼此没有直接联系,也就不需要相互感知,从而达到 **解耦**。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/design/theory/mq/mq_2.png) + +### 2.3. 流量削峰 + +> 当 **上下游系统** 处理能力存在差距的时候,利用 MQ 做一个 “**漏斗**” 模型,进行 **流控**。把 MQ 当成可靠的 **消息暂存地**,进行一定程度的 **消息堆积**;在下游有能力处理的时候,再发送消息。 +> +> MQ 的流量削峰常用于高并发场景(例如:秒杀、团抢等业务场景),它是缓解瞬时暴增流量的核心手段之一。 +> +> 如果没有 MQ,两个系统之间通过 **协商**、**滑动窗口**、**限流**/**降级**/**熔断** 等复杂的方案也能实现 **流控**。但 **系统复杂性** 指数级增长,势必在上游或者下游做存储,并且要处理 **定时**、**拥塞** 等一系列问题。而且每当有 **处理能力有差距** 的时候,都需要 **单独** 开发一套逻辑来维护这套逻辑。 + +假设某个系统读写数据库的稳定性能为每秒处理 1000 条数据。平常情况下,远远达不到这么大的处理量。假设,因为因为做活动,系统的瞬时请求量剧增,达到每秒 10000 个并发请求,数据库根本承受不了,可能直接就把数据库给整崩溃了,这样系统服务就不可用了。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/design/theory/mq/mq_5.png) + +如果使用 MQ,每秒写入 10000 条请求,但是系统 A 每秒只从 MQ 中消费 1000 条请求,然后写入数据库。这样,就不会超过数据库的承受能力,而是把请求积压在 MQ 中。只要高峰期一过,系统 A 就会很快把积压的消息给处理掉。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/design/theory/mq/mq_6.png) + +### 2.4. 传输缓冲 + +(1)MQ 常被用于做海量数据的传输缓冲。 + +例如,Kafka 常被用于做为各种日志数据、采集数据的数据中转。然后,Kafka 将数据转发给 Logstash、Elasticsearch 中,然后基于 Elasticsearch 来做日志中心,提供检索、聚合、分析日志的能力。开发者可以通过 Kibana 集成 Elasticsearch 数据进行可视化展示,或自行进行定制化开发。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/snap/20200930164342.png) + +(2)MQ 也可以被用于流式处理。 + +例如,Kafka 几乎已经是流计算的数据采集端的标准组件。而流计算通过实时数据处理能力,提供了更为快捷的聚合计算能力,被大量应用于链路监控、实时监控、实时数仓、实时大屏、风控、推荐等应用领域。 + +### 2.5. 最终一致性 + +**最终一致性** 不是 **消息队列** 的必备特性,但确实可以依靠 **消息队列** 来做 **最终一致性** 的事情。 + +- **先写消息再操作**,确保操作完成后再修改消息状态。**定时任务补偿机制** 实现消息 **可靠发送接收**、业务操作的可靠执行,要注意 **消息重复** 与 **幂等设计**。 +- 所有不保证 `100%` **不丢消息** 的消息队列,理论上无法实现 **最终一致性**。 + +> 像 `Kafka` 一类的设计,在设计层面上就有 **丢消息** 的可能(比如 **定时刷盘**,如果掉电就会丢消息)。哪怕只丢千分之一的消息,业务也必须用其他的手段来保证结果正确。 + +### 2.6. 系统间通信 + +消息队列一般都内置了 **高效的通信机制**,因此也可以用于单纯的 **消息通讯**,比如实现 **点对点消息队列** 或者 **聊天室** 等。 + +**生产者/消费者** 模式,只需要关心消息是否 **送达队列**,至于谁希望订阅和需要消费,是 **下游** 的事情,无疑极大地减少了开发和联调的工作量。 + +## 3. MQ 的问题 + +任何技术都会有利有弊,MQ 给整体系统架构带来很多好处,但也会付出一定的代价。 + +MQ 主要引入了以下问题: + +- **系统可用性降低**:引入了 MQ 后,通信需要基于 MQ 完成,如果 MQ 宕机,则服务不可用。因此,MQ 要保证是高可用的,详情参考:[MQ 的高可用](#MQ-的高可用) +- **系统复杂度提高**:使用 MQ,需要关注一些新的问题: + - 如何保证消息没有 **重复消费**? + - 如何处理 **消息丢失** 的问题? + - 如何保证传递 **消息的顺序性**? + - 如何处理大量 **消息积压** 的问题? +- **一致性问题**:假设系统 A 处理完直接返回成功的结果给用户,用户认为请求成功。但如果此时,系统 BCD 中只要有任意一个写库失败,那么数据就不一致了。这种情况如何处理? + +下面,我们针对以上问题来一一分析。 + +### 3.1. 重复消费 + +**如何保证消息不被重复消费** 和 **如何保证消息消费的幂等性** 是同一个问题。 + +必须先明确产生重复消费的原因,才能对症下药。 + +#### 重复消费问题原因 + +重复消费问题通常不是 MQ 来处理,而是由开发来处理的。 + +以 Kafka 举例,Kafka 每个 Partition 都是一个有序的、不可变的记录序列,不断追加到结构化的提交日志中。Partition 中为每条记录分配一个连续的 id 号,称为偏移量(Offset),用于唯一标识 Partition 内的记录。 + +Kafka 的客户端和 Broker 都会保存 Offset。客户端消费消息后,每隔一段时间,就把已消费的 Offset 提交给 Kafka Broker,表示已消费。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210427194009.png) + +在这个过程中,如果客户端应用消费消息后,因为宕机、重启等情况而没有提交已消费的 Offset 。当系统恢复后,会继续消费消息,由于 Offset 未提交,就会出现重复消费的问题。 + +#### 重复消费解决方案 + +应对重复消费问题,需要在业务层面,通过 **幂等性设计** 来解决。 + +MQ 重复消费不可怕,可怕的是没有应对机制,可以借鉴的思路有: + +- 如果是写关系型数据库,可以先根据主键查询,判断数据是否已存在,存在则更新,不存在则插入; +- 如果是写 Redis,由于 set 操作天然具有幂等性,所以什么都不用做; +- 如果是根据消息做较复杂的逻辑处理,可以在消息中加入全局唯一 ID,例如:订单 ID 等。在客户端存储中(Mysql、Redis 等)保存已消费消息的 ID。一旦接受到新消息,先判断消息中的 ID 是否在已消费消息 ID 表中存在,存在则不再处理,不存在则处理。 + +在实际开发中,可以参考上面的例子,结合现实场景,设计合理的幂等性方案。 + +### 3.2. 消息丢失 + +**如何处理消息丢失的问题** 和 **如何保证消息不被重复消费** 是同一个问题。关注点有: + +- MQ Server 丢失数据 +- 消费方丢失数据 +- 生产方丢失数据 + +#### 消费方丢失数据 + +唯一可能导致消费方丢失数据的情况是:消费方设置了**自动提交 Offset**。一旦设置了自动提交 Offset,接受到消息后就会自动提交 Offset 给 Kafka ,Kafka 就认为消息已被消费。如果此时,消费方尚未来得及处理消息就挂了,那么消息就丢了。 + +解决方法就是:消费方关闭自动提交 Offset,处理完消息后**手动提交 Offset**。但这种情况下可能会出现重复消费的情形,需要自行保证幂等性。 + +#### Kafka Server 丢失数据 + +当 Kafka 某个 Broker 宕机,需要重新选举 Partition 的 Leader。若此时其他的 Follower 尚未同步 Leader 的数据,那么新选某个 Follower 为 Leader 后,就丢失了部分数据。 + +为此,一般要求至少设置 4 个参数: + +- 给 Topic 设置 `replication.factor` 参数 - 这个值必须大于 1,要求每个 Partition 必须有至少 2 个副本。 +- 在 Kafka 服务端设置 `min.insync.replicas` 参数 - 这个值必须大于 1,这是要求一个 Leader 需要和至少一个 Follower 保持通信,这样才能确保 Leader 挂了还有替补。 +- 在 Producer 端设置 `acks=all` - 这意味着:要求每条数据,必须是**写入所有 replica 之后,才能认为写入成功了**。 +- 在 Producer 端设置 `retries=MAX`(很大很大很大的一个值,无限次重试的意思) - 这意味着**要求一旦写入失败,就无限重试**,卡在这里了。 + +#### 生产方丢失数据 + +如果按照上述的思路设置了 `acks=all`,生产方一定不会丢数据。 + +要求是,你的 Leader 接收到消息,所有的 Follower 都同步到了消息之后,才认为本生产消息成功了。如果未满足这个条件,生产者会自动不断的重试,重试无限次。 + +### 3.3. 消息的顺序性 + +要保证 MQ 的顺序性,势必要付出一定的代价,所以实施方案前,要先明确业务场景是不是有必要保证消息的顺序性。只有那些明确对消息处理顺序有要求的业务场景才值得去保证消息顺序性。 + +方案一 + +一个 Topic,一个 Partition,一个 Consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。 + +方案二 + +- 写入数据到 Partition 时指定一个全局唯一的 ID,例如订单 ID。发送方保证相同 ID 的消息有序的发送到同一个 Partition。 +- 基于上一点,消费方从 Kafka Partition 中消费消息时,此刻一定是顺序的。但如果消费方式以并发方式消费消息,顺序就可能会被打乱。为此,还有做到以下几点: + - 消费方维护 N 个缓存队列,具有相同 ID 的数据都写入同一个队列中; + - 创建 N 个线程,每个线程只负责从指定的一个队列中取数据。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20210427194215.png) + +### 3.4. 消息积压 + +假设一个 MQ 消费者可以一秒处理 1000 条消息,三个 MQ 消费者可以一秒处理 3000 条消息,那么一分钟的处理量是 18 万条。如果 MQ 中积压了几百万到上千万的数据,即使消费者恢复了,也需要大概很长的时间才能恢复过来。 + +对于产线环境来说,漫长的等待是不可接受的,所以面临这种窘境时,只能临时紧急扩容以应对了,具体操作步骤和思路如下: + +- 先修复 Consumer 的问题,确保其恢复消费速度,然后将现有 Consumer 都停掉。 +- 新建一个 Topic,Partition 是原来的 10 倍,临时建立好原先 10 倍的 Queue 数量。 +- 然后写一个临时的分发数据的 Consumer 程序,这个程序部署上去消费积压的数据,**消费之后不做耗时的处理**,直接均匀轮询写入临时建立好的 10 倍数量的 Queue。 +- 接着临时征用 10 倍的机器来部署 Consumer ,每一批 Consumer 消费一个临时 Queue 的数据。这种做法相当于是临时将 Queue 资源和 Consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。 +- 等快速消费完积压数据之后,**得恢复原先部署的架构**,**重新**用原先的 consumer 机器来消费消息。 + +## 4. MQ 的高可用 + +不同 MQ 实现高可用的原理各不相同。因为 Kafka 比较具有代表性,所以这里以 Kafka 为例。 + +### 4.1. Kafka 的高可用 + +#### Kafka 的核心概念 + +了解 Kafka,必须先了解 Kafka 的核心概念: + +- **Broker** - Kafka 集群包含一个或多个节点,这种节点被称为 Broker。 + +- **Topic** - 每条发布到 Kafka 集群的消息都有一个类别,这个类别被称为 Topic。(不同 Topic 的消息是物理隔离的;同一个 Topic 的消息保存在一个或多个 Broker 上,但用户只需指定消息的 Topic 即可生产或消费数据而不必关心数据存于何处)。对于每一个 Topic, Kafka 集群都会维持一个分区日志。 + +- **Partition** - 了提高 Kafka 的吞吐率,每个 Topic 包含一个或多个 Partition,每个 Partition 在物理上对应一个文件夹,该文件夹下存储这个 Partition 的所有消息和索引文件。 + + - Kafka 日志的分区(Partition)分布在 Kafka 集群的节点上。每个节点在处理数据和请求时,共享这些分区。每一个分区都会在已配置的节点上进行备份,确保容错性。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/distributed/mq/kafka/kafka-cluster-roles.png) + +#### Kafka 的副本机制 + +Kafka 是如何实现高可用的呢? + +Kafka 在 0.8 以前的版本中,如果一个 Broker 宕机了,其上面的 Partition 都不能用了,这自然不是高可用的。 + +为了实现高可用,Kafka 引入了复制功能。 + +简单来说,就是副本机制( Replicate )。 + +**每个 Partition 都有一个 Leader,零个或多个 Follower**。Leader 和 Follower 都是 Broker,每个 Broker 都会成为某些分区的 Leader 和某些分区的 Follower,因此集群的负载是平衡的。 + +- **Leader 处理一切对 Partition (分区)的读写请求**; +- **而 Follower 只需被动的同步 Leader 上的数据**。 + +**同一个 Topic 的不同 Partition 会分布在多个 Broker 上,而且一个 Partition 还会在其他的 Broker 上面进行备份**,Producer 在发布消息到某个 Partition 时,先找到该 Partition 的 Leader,然后向这个 Leader 推送消息;每个 Follower 都从 Leader 拉取消息,拉取消息成功之后,向 Leader 发送一个 ACK 确认。 + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/distributed/mq/kafka/kafka-replication.png) + +> FAQ +> +> 问:为什么让 Leader 处理一切对对 Partition (分区)的读写请求? +> +> 答:因为如果允许所有 Broker 都可以处理读写请求,就可能产生数据一致性问题。 + +#### Kafka 选举 Leader + +由上文可知,Partition 在多个 Broker 上存在副本。 + +如果某个 Follower 宕机,啥事儿没有,正常工作。 + +如果 Leader 宕机了,会从 Follower 中**重新选举**一个新的 Leader。 + +## 5. 主流 MQ + +### 5.1. ActiveMQ + +`ActiveMQ` 是由 `Apache` 出品,`ActiveMQ` 是一个完全支持`JMS1.1` 和 `J2EE 1.4` 规范的 `JMS Provider` 实现。它非常快速,支持 **多种语言的客户端** 和 **协议**,而且可以非常容易的嵌入到企业的应用环境中,并有许多高级功能。 + +![img](https://user-gold-cdn.xitu.io/2018/7/8/16479c8ea7cdc2c0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +#### (a) 主要特性 + +1. **服从 JMS 规范**:`JMS` 规范提供了良好的标准和保证,包括:**同步** 或 **异步** 的消息分发,一次和仅一次的消息分发,**消息接收** 和 **订阅** 等等。遵从 `JMS` 规范的好处在于,不论使用什么 `JMS` 实现提供者,这些基础特性都是可用的; +2. **连接灵活性**:`ActiveMQ` 提供了广泛的 **连接协议**,支持的协议有:`HTTP/S`,`IP` **多播**,`SSL`,`TCP`,`UDP` 等等。对众多协议的支持让 `ActiveMQ` 拥有了很好的灵活性; +3. **支持的协议种类多**:`OpenWire`、`STOMP`、`REST`、`XMPP`、`AMQP`; +4. **持久化插件和安全插件**:`ActiveMQ` 提供了 **多种持久化** 选择。而且,`ActiveMQ` 的安全性也可以完全依据用户需求进行 **自定义鉴权** 和 **授权**; +5. **支持的客户端语言种类多**:除了 `Java` 之外,还有:`C/C++`,`.NET`,`Perl`,`PHP`,`Python`,`Ruby`; +6. **代理集群**:多个 `ActiveMQ` **代理** 可以组成一个 **集群** 来提供服务; +7. **异常简单的管理**:`ActiveMQ` 是以开发者思维被设计的。所以,它并不需要专门的管理员,因为它提供了简单又使用的管理特性。有很多中方法可以 **监控** `ActiveMQ` 不同层面的数据,包括使用在 `JConsole` 或者在 `ActiveMQ` 的 `Web Console` 中使用 `JMX`。通过处理 `JMX` 的告警消息,通过使用 **命令行脚本**,甚至可以通过监控各种类型的 **日志**。 + +#### (b) 部署环境 + +`ActiveMQ` 可以运行在 `Java` 语言所支持的平台之上。使用 `ActiveMQ` 需要: + +- `Java JDK` +- `ActiveMQ` 安装包 + +#### (c) 优点 + +1. **跨平台** (`JAVA` 编写与平台无关,`ActiveMQ` 几乎可以运行在任何的 `JVM` 上); +2. 可以用 `JDBC`:可以将 **数据持久化** 到数据库。虽然使用 `JDBC` 会降低 `ActiveMQ` 的性能,但是数据库一直都是开发人员最熟悉的存储介质; +3. 支持 `JMS` 规范:支持 `JMS` 规范提供的 **统一接口**; +4. 支持 **自动重连** 和 **错误重试机制**; +5. 有安全机制:支持基于 `shiro`,`jaas` 等多种 **安全配置机制**,可以对 `Queue/Topic` 进行 **认证和授权**; +6. 监控完善:拥有完善的 **监控**,包括 `Web Console`,`JMX`,`Shell` 命令行,`Jolokia` 的 `RESTful API`; +7. 界面友善:提供的 `Web Console` 可以满足大部分情况,还有很多 **第三方的组件** 可以使用,比如 `hawtio`; + +#### (d) 缺点 + +1. 社区活跃度不及 `RabbitMQ` 高; +2. 根据其他用户反馈,会出莫名其妙的问题,会 **丢失消息**; +3. 目前重心放到 `activemq 6.0` 产品 `Apollo`,对 `5.x` 的维护较少; +4. 不适合用于 **上千个队列** 的应用场景; + +### 5.2. RabbitMQ + +`RabbitMQ` 于 `2007` 年发布,是一个在 `AMQP` (**高级消息队列协议**)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。 + +![img](https://user-gold-cdn.xitu.io/2018/7/8/16479c8ece3b5d7a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +#### (a) 主要特性 + +1. **可靠性**:提供了多种技术可以让你在 **性能** 和 **可靠性** 之间进行 **权衡**。这些技术包括 **持久性机制**、**投递确认**、**发布者证实** 和 **高可用性机制**; +2. **灵活的路由**:消息在到达队列前是通过 **交换机** 进行 **路由** 的。`RabbitMQ` 为典型的路由逻辑提供了 **多种内置交换机** 类型。如果你有更复杂的路由需求,可以将这些交换机组合起来使用,你甚至可以实现自己的交换机类型,并且当做 `RabbitMQ` 的 **插件** 来使用; +3. **消息集群**:在相同局域网中的多个 `RabbitMQ` 服务器可以 **聚合** 在一起,作为一个独立的逻辑代理来使用; +4. **队列高可用**:队列可以在集群中的机器上 **进行镜像**,以确保在硬件问题下还保证 **消息安全**; +5. **支持多种协议**:支持 **多种消息队列协议**; +6. **支持多种语言**:用 `Erlang` 语言编写,支持只要是你能想到的 **所有编程语言**; +7. **管理界面**: `RabbitMQ` 有一个易用的 **用户界面**,使得用户可以 **监控** 和 **管理** 消息 `Broker` 的许多方面; +8. **跟踪机制**:如果 **消息异常**,`RabbitMQ` 提供消息跟踪机制,使用者可以找出发生了什么; +9. **插件机制**:提供了许多 **插件**,来从多方面进行扩展,也可以编写自己的插件。 + +#### (b) 部署环境 + +`RabbitMQ` 可以运行在 `Erlang` 语言所支持的平台之上,包括 `Solaris`,`BSD`,`Linux`,`MacOSX`,`TRU64`,`Windows` 等。使用 `RabbitMQ` 需要: + +- `ErLang` 语言包 +- `RabbitMQ` 安装包 + +#### (c) 优点 + +1. 由于 `Erlang` 语言的特性,消息队列性能较好,支持 **高并发**; +2. 健壮、稳定、易用、**跨平台**、支持 **多种语言**、文档齐全; +3. 有消息 **确认机制** 和 **持久化机制**,可靠性高; +4. 高度可定制的 **路由**; +5. **管理界面** 较丰富,在互联网公司也有较大规模的应用,社区活跃度高。 + +#### (d) 缺点 + +1. 尽管结合 `Erlang` 语言本身的并发优势,性能较好,但是不利于做 **二次开发和维护**; +2. 实现了 **代理架构**,意味着消息在发送到客户端之前可以在 **中央节点** 上排队。此特性使得 `RabbitMQ` 易于使用和部署,但是使得其 **运行速度较慢**,因为中央节点 **增加了延迟**,**消息封装后** 也比较大; +3. 需要学习 **比较复杂** 的 **接口和协议**,学习和维护成本较高。 + +### 5.3. RocketMQ + +`RocketMQ` 出自 **阿里** 的开源产品,用 `Java` 语言实现,在设计时参考了 `Kafka`,并做出了自己的一些改进,**消息可靠性上** 比 `Kafka` 更好。`RocketMQ` 在阿里内部  被广泛应用在 **订单**,**交易**,**充值**,**流计算**,**消息推送**,**日志流式处理**,`binglog` **分发** 等场景。 + +#### (a) 主要特性 + +![img](data:image/svg+xml;utf8,) + +1. 基于 **队列模型**:具有 **高性能**、**高可靠**、**高实时**、**分布式** 等特点; +2. `Producer`、`Consumer`、**队列** 都支持 **分布式**; +3. `Producer` 向一些队列轮流发送消息,**队列集合** 称为 `Topic`。`Consumer` 如果做 **广播消费**,则一个 `Consumer` 实例消费这个 `Topic` 对应的 **所有队列**;如果做 **集群消费**,则 **多个** `Consumer` 实例 **平均消费** 这个 `Topic` 对应的队列集合; +4. 能够保证 **严格的消息顺序**; +5. 提供丰富的 **消息拉取模式**; +6. 高效的订阅者 **水平扩展**能力; +7. **实时** 的 **消息订阅机制**; +8. 亿级 **消息堆积** 能力; +9. 较少的外部依赖。 + +#### (b) 部署环境 + +`RocketMQ` 可以运行在 `Java` 语言所支持的平台之上。使用 `RocketMQ` 需要: + +- `Java JDK` +- 安装 `git`、`Maven` +- `RocketMQ` 安装包 + +#### (c) 优点 + +1. **单机** 支持 `1` 万以上 **持久化队列**; +2. `RocketMQ` 的所有消息都是 **持久化的**,先写入系统 `PAGECACHE`,然后 **刷盘**,可以保证 **内存** 与 **磁盘** 都有一份数据,而 **访问** 时,直接 **从内存读取**。 +3. 模型简单,接口易用(`JMS` 的接口很多场合并不太实用); +4. **性能非常好**,可以允许 **大量堆积消息** 在 `Broker` 中; +5. 支持 **多种消费模式**,包括 **集群消费**、**广播消费**等; +6. 各个环节 **分布式扩展设计**,支持 **主从** 和 **高可用**; +7. 开发度较活跃,版本更新很快。 + +#### (d) 缺点 + +1. 支持的 **客户端语言** 不多,目前是 `Java` 及 `C++`,其中 `C++` 还不成熟; +2. `RocketMQ` 社区关注度及成熟度也不及前两者; +3. 没有 `Web` 管理界面,提供了一个 `CLI` (命令行界面) 管理工具带来 **查询**、**管理** 和 **诊断各种问题**; +4. 没有在 `MQ` 核心里实现 `JMS` 等接口; + +### 5.4. Kafka + +`Apache Kafka` 是一个 **分布式消息发布订阅** 系统。它最初由 `LinkedIn` 公司基于独特的设计实现为一个 **分布式的日志提交系统** (`a distributed commit log`),之后成为 `Apache` 项目的一部分。`Kafka` **性能高效**、**可扩展良好** 并且 **可持久化**。它的 **分区特性**,**可复制** 和 **可容错** 都是其不错的特性。 + +![img](data:image/svg+xml;utf8,) + +#### (a) 主要特性 + +1. **快速持久化**:可以在 `O(1)` 的系统开销下进行 **消息持久化**; +2. **高吞吐**:在一台普通的服务器上既可以达到 `10W/s` 的 **吞吐速率**; +3. **完全的分布式系统**:`Broker`、`Producer` 和 `Consumer` 都原生自动支持 **分布式**,自动实现 **负载均衡**; +4. 支持 **同步** 和 **异步** 复制两种 **高可用机制**; +5. 支持 **数据批量发送** 和 **拉取**; +6. **零拷贝技术(zero-copy)**:减少 `IO` 操作步骤,提高 **系统吞吐量**; +7. **数据迁移**、**扩容** 对用户透明; +8. **无需停机** 即可扩展机器; +9. **其他特性**:丰富的 **消息拉取模型**、高效 **订阅者水平扩展**、实时的 **消息订阅**、亿级的 **消息堆积能力**、定期删除机制; + +#### (b) 部署环境 + +使用 `Kafka` 需要: + +- `Java JDK` +- `Kafka` 安装包 + +#### (c) 优点 + +1. **客户端语言丰富**:支持 `Java`、`.Net`、`PHP`、`Ruby`、`Python`、`Go` 等多种语言; +2. **高性能**:单机写入 `TPS` 约在 `100` 万条/秒,消息大小 `10` 个字节; +3. 提供 **完全分布式架构**,并有 `replica` 机制,拥有较高的 **可用性** 和 **可靠性**,理论上支持 **消息无限堆积**; +4. 支持批量操作; +5. **消费者** 采用 `Pull` 方式获取消息。**消息有序**,**通过控制** 能够保证所有消息被消费且仅被消费 **一次**; +6. 有优秀的第三方 `Kafka Web` 管理界面 `Kafka-Manager`; +7. 在 **日志领域** 比较成熟,被多家公司和多个开源项目使用。 + +#### (d) 缺点 + +1. `Kafka` 单机超过 `64` 个 **队列/分区** 时,`Load` 时会发生明显的飙高现象。**队列** 越多,**负载** 越高,发送消息 **响应时间变长**; +2. 使用 **短轮询方式**,**实时性** 取决于 **轮询间隔时间**; +3. 消费失败 **不支持重试**; +4. 支持 **消息顺序**,但是 **一台代理宕机** 后,就会产生 **消息乱序**; +5. 社区更新较慢。 + +### 5.5. MQ 的技术选型 + +MQ 的技术选型一般要考虑以下几点: + +- **是否开源**:这决定了能否商用,所以最为重要。 +- **社区活跃度越高越好**:高社区活跃度,一般保证了低 Bug 率,因为大部分 Bug,已经有人遇到并解决了。 +- **技术生态适配性**:客户端对各种编程语言的支持。比如:如果使用 MQ 的都是 Java 应用,那么 ActiveMQ、RabbitMQ、RocketMQ、Kafka 都可以。如果需要支持其他语言,那么 RMQ 比较合适,因为它支持的编程语言比较丰富。如果 MQ 是应用于大数据或流式计算,那么 Kafka 几乎是标配。如果是应用于在线业务系统,那么 Kafka 就不合适了,可以考虑 RabbitMQ、 RocketMQ 很合适。 +- **高可用性**:应用于线上的准入标准。 +- **性能**:具备足够好的性能,能满足绝大多数场景的性能要求。 + +| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | +| ------------------------ | ------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| 单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行流式计算、日志采集等场景 | +| topic 数量对吞吐量的影响 | | | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | +| 时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 | +| 可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | +| 消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ | +| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 | + +综上,各种对比之后,有如下建议: + +- 业务系统场景,建议使用 RocketMQ、RabbitMQ。如果所有应用都是 Java,优选 RocketMQ,因为 RocketMQ 本身就是 Java 开发的,所以最适配。如果业务中有多种编程语言的应用,建议选择 RabbitMQ。 +- 大数据和流式计算领域,或是作为日志缓冲,强烈建议选择 Kafka,业界标准,久经考验。 + +## 6. JMS + +提到 MQ,就顺便提一下 JMS 。 + +**JMS(JAVA Message Service,java 消息服务)API 是一个消息服务的标准/规范,允许应用程序组件基于 JavaEE 平台创建、发送、接收和读取消息**。它使分布式通信耦合度更低,消息服务更加可靠以及异步性。 + +在 EJB 架构中,有消息 bean 可以无缝的与 JMS 消息服务集成。在 J2EE 架构模式中,有消息服务者模式,用于实现消息与应用直接的解耦。 + +### 6.1. 消息模型 + +在 JMS 标准中,有两种消息模型: + +- P2P(Point to Point) +- Pub/Sub(Publish/Subscribe) + +#### P2P 模式 + +
    +P2P 模式包含三个角色:MQ(Queue),发送者(Sender),接收者(Receiver)。每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到他们被消费或超时。 + +P2P 的特点 + +- 每个消息只有一个消费者(Consumer)(即一旦被消费,消息就不再在 MQ 中) +- 发送者和接收者之间在时间上没有依赖性,也就是说当发送者发送了消息之后,不管接收者有没有正在运行,它不会影响到消息被发送到队列 +- 接收者在成功接收消息之后需向队列应答成功 + +如果希望发送的每个消息都会被成功处理的话,那么需要 P2P 模式。 + +#### Pub/sub 模式 + +
    +包含三个角色主题(Topic),发布者(Publisher),订阅者(Subscriber) 。多个发布者将消息发送到 Topic,系统将这些消息传递给多个订阅者。 + +Pub/Sub 的特点 + +- 每个消息可以有多个消费者 +- 发布者和订阅者之间有时间上的依赖性。针对某个主题(Topic)的订阅者,它必须创建一个订阅者之后,才能消费发布者的消息。 +- 为了消费消息,订阅者必须保持运行的状态。 + +为了缓和这样严格的时间相关性,JMS 允许订阅者创建一个可持久化的订阅。这样,即使订阅者没有被激活(运行),它也能接收到发布者的消息。 + +如果希望发送的消息可以不被做任何处理、或者只被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用 Pub/Sub 模型。 + +### 6.2. 消息消费 + +在 JMS 中,消息的产生和消费都是异步的。对于消费来说,JMS 的消息者可以通过两种方式来消费消息。 + +- **同步** - 订阅者或接收者通过 `receive` 方法来接收消息,`receive` 方法在接收到消息之前(或超时之前)将一直阻塞; +- **异步** - 订阅者或接收者可以注册为一个消息监听器。当消息到达之后,系统自动调用监听器的 `onMessage` 方法。 + +`JNDI` - Java 命名和目录接口,是一种标准的 Java 命名系统接口。可以在网络上查找和访问服务。通过指定一个资源名称,该名称对应于数据库或命名服务中的一个记录,同时返回资源连接建立所必须的信息。 + +JNDI 在 JMS 中起到查找和访问发送目标或消息来源的作用。 + +### 6.3. JMS 编程模型 + +#### ConnectionFactory + +创建 Connection 对象的工厂,针对两种不同的 jms 消息模型,分别有 QueueConnectionFactory 和 TopicConnectionFactory 两种。可以通过 JNDI 来查找 ConnectionFactory 对象。 + +#### Destination + +Destination 的意思是消息生产者的消息发送目标或者说消息消费者的消息来源。对于消息生产者来说,它的 Destination 是某个队列(Queue)或某个主题(Topic);对于消息消费者来说,它的 Destination 也是某个队列或主题(即消息来源)。 + +所以,Destination 实际上就是两种类型的对象:Queue、Topic。可以通过 JNDI 来查找 Destination。 + +#### Connection + +Connection 表示在客户端和 JMS 系统之间建立的链接(对 TCP/IP socket 的包装)。Connection 可以产生一个或多个 Session。跟 ConnectionFactory 一样,Connection 也有两种类型:QueueConnection 和 TopicConnection。 + +#### Session + +Session 是操作消息的接口。可以通过 session 创建生产者、消费者、消息等。Session 提供了事务的功能。当需要使用 session 发送/接收多个消息时,可以将这些发送/接收动作放到一个事务中。同样,也分 QueueSession 和 TopicSession。 + +#### 消息的生产者 + +消息生产者由 Session 创建,并用于将消息发送到 Destination。同样,消息生产者分两种类型:QueueSender 和 TopicPublisher。可以调用消息生产者的方法(send 或 publish 方法)发送消息。 + +#### 消息消费者 + +消息消费者由 Session 创建,用于接收被发送到 Destination 的消息。两种类型:QueueReceiver 和 TopicSubscriber。可分别通过 session 的 createReceiver(Queue)或 createSubscriber(Topic)来创建。当然,也可以 session 的 creatDurableSubscriber 方法来创建持久化的订阅者。 + +#### MessageListener + +消息监听器。如果注册了消息监听器,一旦消息到达,将自动调用监听器的 onMessage 方法。EJB 中的 MDB(Message-Driven Bean)就是一种 MessageListener。 + +## 7. 参考资料 + +- [大型网站架构系列:分布式 MQ(一)](https://www.cnblogs.com/itfly8/p/5155983.html) +- [大型网站架构系列:MQ(二)](https://www.cnblogs.com/itfly8/p/5156155.html) +- [分布式开放 MQ(RocketMQ)的原理与实践](https://www.jianshu.com/p/453c6e7ff81c) +- [阿里 RocketMQ 优势对比](https://juejin.im/entry/5a0abfb5f265da43062a4a91) +- [advanced-java 之 MQ](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/mq-interview.md) +- [浅谈消息队列及常见的消息中间件](https://juejin.im/post/6844903635046924296) diff --git "a/docs/mq/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225.md" "b/docs/mq/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225.md" new file mode 100644 index 00000000..72c38194 --- /dev/null +++ "b/docs/mq/\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225.md" @@ -0,0 +1,361 @@ +# 消息队列面试夺命连环问 + +## 1. 为什么使用消息队列? + +### 解耦 + +看这么个场景。A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃...... + +![img](https://github.com/doocs/advanced-java/blob/master/images/mq-1.png) + +在这个场景中,A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。A 系统要时时刻刻考虑 BCDE 四个系统如果挂了该咋办?要不要重发,要不要把消息存起来?头发都白了啊! + +如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/mq-2.png) + +**总结**:通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。 + +**面试技巧**:你需要去考虑一下你负责的系统中是否有类似的场景,就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦,也是可以的,你就需要去考虑在你的项目里,是不是可以运用这个 MQ 去进行系统的解耦。在简历中体现出来这块东西,用 MQ 作解耦。 + +### 异步 + +再来看一个场景,A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求,等待个 1s,这几乎是不可接受的。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/mq-3.png) + +一般互联网类的企业,对于用户直接的操作,一般要求是每个请求都必须在 200 ms 以内完成,对用户几乎是无感知的。 + +如果**使用 MQ**,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了,爽!网站做得真好,真快! + +![img](https://github.com/doocs/advanced-java/blob/master/images/mq-4.png) + +### 削峰 + +每天 0:00 到 12:00,A 系统风平浪静,每秒并发请求数量就 50 个。结果每次一到 12:00 \~ 13:00 ,每秒并发请求数量突然会暴增到 5k+ 条。但是系统是直接基于 MySQL 的,大量的请求涌入 MySQL,每秒钟对 MySQL 执行约 5k 条 SQL。 + +一般的 MySQL,扛到每秒 2k 个请求就差不多了,如果每秒请求到 5k 的话,可能就直接把 MySQL 给打死了,导致系统崩溃,用户也就没法再使用系统了。 + +但是高峰期一过,到了下午的时候,就成了低峰期,可能也就 1w 的用户同时在网站上操作,每秒中的请求数量可能也就 50 个请求,对整个系统几乎没有任何的压力。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/mq-5.png) + +如果使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,A 系统也绝对不会挂掉。而 MQ 每秒钟 5k 个请求进来,就 2k 个请求出去,结果就导致在中午高峰期(1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/mq-6.png) + +这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ,但是 A 系统依然会按照每秒 2k 个请求的速度在处理。所以说,只要高峰期一过,A 系统就会快速将积压的消息给解决掉。 + +## 2. 消息队列有什么优缺点 + +优点上面已经说了,就是**在特殊场景下有其对应的好处**,**解耦**、**异步**、**削峰**。 + +缺点有以下几个: + +- 系统可用性降低 + 系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了,人 ABCD 四个系统好好的,没啥问题,你偏加个 MQ 进来,万一 MQ 挂了咋整,MQ 一挂,整套系统崩溃的,你不就完了?如何保证消息队列的高可用,可以[点击这里查看](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-high-availability-of-message-queues.md)。 +- 系统复杂度提高 + 硬生生加个 MQ 进来,你怎么[保证消息没有重复消费](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-that-messages-are-not-repeatedly-consumed.md)?怎么[处理消息丢失的情况](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-the-reliable-transmission-of-messages.md)?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已。 +- 一致性问题 + A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。 + +所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,做好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了 10 倍。但是关键时刻,用,还是得用的。 + +## 3. Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点? + +| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | +| ------------------------ | ------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| 单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 | +| topic 数量对吞吐量的影响 | | | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | +| 时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 | +| 可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | +| 消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ | +| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 | + +综上,各种对比之后,有如下建议: + +- 一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了; +- 后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高; +- 不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 [Apache](https://github.com/apache/rocketmq),但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。 +- 所以**中小型公司**,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;**大型公司**,基础架构研发实力较强,用 RocketMQ 是很好的选择。 +- 如果是**大数据领域**的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 + +## 4. 如何保证消息队列的高可用? + +### RabbitMQ 的高可用性 + +RabbitMQ 是比较有代表性的,因为是**基于主从**(非分布式)做高可用性的,我们就以 RabbitMQ 为例子讲解第一种 MQ 的高可用性怎么实现。 + +RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。 + +#### 单机模式 + +单机模式,就是 Demo 级别的,一般就是你本地启动了玩玩儿的 😄,没人生产用单机模式。 + +#### 普通集群模式(无高可用性) + +普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你**创建的 queue,只会放在一个 RabbitMQ 实例上**,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/mq-7.png) + +这种方式确实很麻烦,也不怎么好,**没做到所谓的分布式**,就是个普通集群。因为这导致你要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个 queue 所在实例消费数据,前者有**数据拉取的开销**,后者导致**单实例性能瓶颈**。 + +而且如果那个放 queue 的实例宕机了,会导致接下来其他实例就无法从那个实例拉取,如果你**开启了消息持久化**,让 RabbitMQ 落地存储消息的话,**消息不一定会丢**,得等这个实例恢复了,然后才可以继续从这个 queue 拉取数据。 + +所以这个事儿就比较尴尬了,这就**没有什么所谓的高可用性**,**这方案主要是提高吞吐量的**,就是说让集群中多个节点来服务某个 queue 的读写操作。 + +#### 镜像集群模式(高可用性) + +这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会**存在于多个实例上**,就是说,每个 RabbitMQ 节点都有这个 queue 的一个**完整镜像**,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把**消息同步**到多个实例的 queue 上。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/mq-8.png) + +那么**如何开启这个镜像集群模式**呢?其实很简单,RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是**镜像集群模式的策略**,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。 + +这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!第二,这么玩儿,不是分布式的,就**没有扩展性可言**了,如果某个 queue 负载很重,你加机器,新增的机器也包含了这个 queue 的所有数据,并**没有办法线性扩展**你的 queue。你想,如果这个 queue 的数据量很大,大到这个机器上的容量无法容纳了,此时该怎么办呢? + +### Kafka 的高可用性 + +Kafka 一个最基本的架构认识:由多个 broker 组成,每个 broker 是一个节点;你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。 + +这就是**天然的分布式消息队列**,就是说一个 topic 的数据,是**分散放在多个机器上的,每个机器就放一部分数据**。 + +实际上 RabbmitMQ 之类的,并不是分布式消息队列,它就是传统的消息队列,只不过提供了一些集群、HA(High Availability, 高可用性) 的机制而已,因为无论怎么玩儿,RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。 + +Kafka 0.8 以前,是没有 HA 机制的,就是任何一个 broker 宕机了,那个 broker 上的 partition 就废了,没法写也没法读,没有什么高可用性可言。 + +比如说,我们假设创建了一个 topic,指定其 partition 数量是 3 个,分别在三台机器上。但是,如果第二台机器宕机了,会导致这个 topic 的 1/3 的数据就丢了,因此这个是做不到高可用的。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/kafka-before.png) + +Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,**要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题**,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/kafka-after.png) + +这么搞,就有所谓的**高可用性**了,因为如果某个 broker 宕机了,没事儿,那个 broker 上面的 partition 在其他机器上都有副本的。如果这个宕机的 broker 上面有某个 partition 的 leader,那么此时会从 follower 中**重新选举**一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。 + +**写数据**的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为) + +**消费**的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。 + +看到这里,相信你大致明白了 Kafka 是如何保证高可用机制的了,对吧?不至于一无所知,现场还能给面试官画画图。要是遇上面试官确实是 Kafka 高手,深挖了问,那你只能说不好意思,太深入的你没研究过。 + +## 5. 如何保证消息不被重复消费?(如何保证消息消费的幂等性) + +首先,比如 RabbitMQ、RocketMQ、Kafka,都有可能会出现消息重复消费的问题,正常。因为这问题通常不是 MQ 自己保证的,是由我们开发来保证的。挑一个 Kafka 来举个例子,说说怎么重复消费吧。 + +Kafka 实际上有个 offset 的概念,就是每个消息写进去,都有一个 offset,代表消息的序号,然后 consumer 消费了数据之后,**每隔一段时间**(定时定期),会把自己消费过的消息的 offset 提交一下,表示“我已经消费过了,下次我要是重启啥的,你就让我继续从上次消费到的 offset 来继续消费吧”。 + +但是凡事总有意外,比如我们之前生产经常遇到的,就是你有时候重启系统,看你怎么重启了,如果碰到点着急的,直接 kill 进程了,再重启。这会导致 consumer 有些消息处理了,但是没来得及提交 offset,尴尬了。重启之后,少数消息会再次消费一次。 + +举个栗子。 + +有这么个场景。数据 1/2/3 依次进入 kafka,kafka 会给这三条数据每条分配一个 offset,代表这条数据的序号,我们就假设分配的 offset 依次是 152/153/154。消费者从 kafka 去消费的时候,也是按照这个顺序去消费。假如当消费者消费了 `offset=153` 的这条数据,刚准备去提交 offset 到 zookeeper,此时消费者进程被重启了。那么此时消费过的数据 1/2 的 offset 并没有提交,kafka 也就不知道你已经消费了 `offset=153` 这条数据。那么重启之后,消费者会找 kafka 说,嘿,哥儿们,你给我接着把上次我消费到的那个地方后面的数据继续给我传递过来。由于之前的 offset 没有提交成功,那么数据 1/2 会再次传过来,如果此时消费者没有去重的话,那么就会导致重复消费。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/mq-10.png) + +如果消费者干的事儿是拿一条数据就往数据库里写一条,会导致说,你可能就把数据 1/2 在数据库里插入了 2 次,那么数据就错啦。 + +其实重复消费不可怕,可怕的是你没考虑到重复消费之后,**怎么保证幂等性**。 + +举个例子吧。假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。 + +一条数据重复出现两次,数据库里就只有一条数据,这就保证了系统的幂等性。 + +幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,**不能出错**。 + +所以第二个问题来了,怎么保证消息队列消费的幂等性? + +其实还是得结合业务来思考,我这里给几个思路: + +- 比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。 +- 比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。 +- 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。 +- 比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/mq-11.png) + +当然,如何保证 MQ 的消费是幂等性的,需要结合具体的业务来看。 + +## 6. 如何保证消息的可靠性传输?(如何处理消息丢失的问题) + +数据的丢失问题,可能出现在生产者、MQ、消费者中,咱们从 RabbitMQ 和 Kafka 分别来分析一下吧。 + +### RabbitMQ + +![img](https://github.com/doocs/advanced-java/blob/master/images/rabbitmq-message-lose.png) + +#### 生产者弄丢了数据 + +生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能。 + +此时可以选择用 RabbitMQ 提供的事务功能,就是生产者**发送数据之前**开启 RabbitMQ 事务`channel.txSelect`,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务`channel.txRollback`,然后重试发送消息;如果收到了消息,那么可以提交事务`channel.txCommit`。 + +```java +// 开启事务 +channel.txSelect +try { + // 这里发送消息 +} catch (Exception e) { + channel.txRollback + + // 这里再次重发这条消息 +} + +// 提交事务 +channel.txCommit +``` + +但是问题是,RabbitMQ 事务机制(同步)一搞,基本上**吞吐量会下来,因为太耗性能**。 + +所以一般来说,如果你要确保说写 RabbitMQ 的消息别丢,可以开启 `confirm` 模式,在生产者那里设置开启 `confirm`模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 `ack` 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 `nack` 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。 + +事务机制和 `confirm` 机制最大的不同在于,**事务机制是同步的**,你提交一个事务之后会**阻塞**在那儿,但是 `confirm` 机制是**异步**的,你发送个消息之后就可以发送下一个消息,然后那个消息 RabbitMQ 接收了之后会异步回调你的一个接口通知你这个消息接收到了。 + +所以一般在生产者这块**避免数据丢失**,都是用 `confirm` 机制的。 + +#### RabbitMQ 弄丢了数据 + +就是 RabbitMQ 自己弄丢了数据,这个你必须**开启 RabbitMQ 的持久化**,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,**恢复之后会自动读取之前存储的数据**,一般数据不会丢。除非极其罕见的是,RabbitMQ 还没持久化,自己就挂了,**可能导致少量数据丢失**,但是这个概率较小。 + +设置持久化有**两个步骤**: + +- 创建 queue 的时候将其设置为持久化 + 这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。 +- 第二个是发送消息的时候将消息的 `deliveryMode` 设置为 2 + 就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。 + +必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。 + +注意,哪怕是你给 RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。 + +所以,持久化可以跟生产者那边的 `confirm` 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 `ack` 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 `ack`,你也是可以自己重发的。 + +#### 消费端弄丢了数据 + +RabbitMQ 如果丢失了数据,主要是因为你消费的时候,**刚消费到,还没处理,结果进程挂了**,比如重启了,那么就尴尬了,RabbitMQ 认为你都消费了,这数据就丢了。 + +这个时候得用 RabbitMQ 提供的 `ack` 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 `ack`,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 `ack` 一把。这样的话,如果你还没处理完,不就没有 `ack` 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/rabbitmq-message-lose-solution.png) + +### Kafka + +#### 消费端弄丢了数据 + +唯一可能导致消费者弄丢数据的情况,就是说,你消费到了这个消息,然后消费者那边**自动提交了 offset**,让 Kafka 以为你已经消费好了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢咯。 + +这不是跟 RabbitMQ 差不多吗,大家都知道 Kafka 会自动提交 offset,那么只要**关闭自动提交** offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。但是此时确实还是**可能会有重复消费**,比如你刚处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。 + +生产环境碰到的一个问题,就是说我们的 Kafka 消费者消费到了数据之后是写到一个内存的 queue 里先缓冲一下,结果有的时候,你刚把消息写入内存 queue,然后消费者会自动提交 offset。然后此时我们重启了系统,就会导致内存 queue 里还没来得及处理的数据就丢失了。 + +#### Kafka 弄丢了数据 + +这块比较常见的一个场景,就是 Kafka 某个 broker 宕机,然后重新选举 partition 的 leader。大家想想,要是此时其他的 follower 刚好还有些数据没有同步,结果此时 leader 挂了,然后选举某个 follower 成 leader 之后,不就少了一些数据?这就丢了一些数据啊。 + +生产环境也遇到过,我们也是,之前 Kafka 的 leader 机器宕机了,将 follower 切换为 leader 之后,就会发现说这个数据就丢了。 + +所以此时一般是要求起码设置如下 4 个参数: + +- 给 topic 设置 `replication.factor` 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。 +- 在 Kafka 服务端设置 `min.insync.replicas` 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。 +- 在 producer 端设置 `acks=all`:这个是要求每条数据,必须是**写入所有 replica 之后,才能认为是写成功了**。 +- 在 producer 端设置 `retries=MAX`(很大很大很大的一个值,无限次重试的意思):这个是**要求一旦写入失败,就无限重试**,卡在这里了。 + +我们生产环境就是按照上述要求配置的,这样配置之后,至少在 Kafka broker 端就可以保证在 leader 所在 broker 发生故障,进行 leader 切换时,数据不会丢失。 + +#### 生产者会不会弄丢数据? + +如果按照上述的思路设置了 `acks=all`,一定不会丢,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次。 + +## 7. 如何保证消息的顺序性? + +我举个例子,我们以前做过一个 mysql `binlog` 同步的系统,压力还是非常大的,日同步数据要达到上亿,就是说数据从一个 mysql 库原封不动地同步到另一个 mysql 库里面去(mysql -> mysql)。常见的一点在于说比如大数据 team,就需要同步一个 mysql 库过来,对公司的业务系统的数据做各种复杂的操作。 + +你在 mysql 里增删改一条数据,对应出来了增删改 3 条 `binlog` 日志,接着这三条 `binlog` 发送到 MQ 里面,再消费出来依次执行,起码得保证人家是按照顺序来的吧?不然本来是:增加、修改、删除;你楞是换了顺序给执行成删除、修改、增加,不全错了么。 + +本来这个数据同步过来,应该最后这个数据被删除了;结果你搞错了这个顺序,最后这个数据保留下来了,数据同步就出错了。 + +先看看顺序会错乱的俩场景: + +- **RabbitMQ**:一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者2先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/rabbitmq-order-01.png) + +- **Kafka**:比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。 + 消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞**多个线程来并发处理消息**。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/kafka-order-01.png) + +### 解决方案 + +#### RabbitMQ + +![img](https://github.com/doocs/advanced-java/blob/master/images/rabbitmq-order-02.png) + +#### Kafka + +- 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。 +- 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。 + +![img](https://github.com/doocs/advanced-java/blob/master/images/kafka-order-02.png) + +## 8. 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决? + +你看这问法,其实本质针对的场景,都是说,可能你的消费端出了问题,不消费了;或者消费的速度极其慢。接着就坑爹了,可能你的消息队列集群的磁盘都快写满了,都没人消费,这个时候怎么办?或者是这整个就积压了几个小时,你这个时候怎么办?或者是你积压的时间太长了,导致比如 RabbitMQ 设置了消息过期时间后就没了怎么办? + +所以就这事儿,其实线上挺常见的,一般不出,一出就是大 case。一般常见于,举个例子,消费端每次消费之后要写 mysql,结果 mysql 挂了,消费端 hang 那儿了,不动了;或者是消费端出了个什么岔子,导致消费速度极其慢。 + +### 面试题剖析 + +关于这个事儿,我们一个一个来梳理吧,先假设一个场景,我们现在消费端出故障了,然后大量消息在 mq 里积压,现在出事故了,慌了。 + +### 大量消息在 mq 里积压了几个小时了还没解决 + +几千万条数据在 MQ 里积压了七八个小时,从下午 4 点多,积压到了晚上 11 点多。这个是我们真实遇到过的一个场景,确实是线上故障了,这个时候要不然就是修复 consumer 的问题,让它恢复消费速度,然后傻傻的等待几个小时消费完毕。这个肯定不能在面试的时候说吧。 + +一个消费者一秒是 1000 条,一秒 3 个消费者是 3000 条,一分钟就是 18 万条。所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概 1 小时的时间才能恢复过来。 + +一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下: + +- 先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。 +- 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。 +- 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,**消费之后不做耗时的处理**,直接均匀轮询写入临时建立好的 10 倍数量的 queue。 +- 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。 +- 等快速消费完积压数据之后,**得恢复原先部署的架构**,**重新**用原先的 consumer 机器来消费消息。 + +### mq 中的消息过期失效了 + +假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是**大量的数据会直接搞丢**。 + +这个情况下,就不是说要增加 consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是**批量重导**,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上12点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。 + +假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。 + +### mq 都快写满了 + +如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,**消费一个丢弃一个,都不要了**,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。 + +## 9. 如果让你写一个消息队列,该如何进行架构设计啊?说一下你的思路。 + +### 面试官心理分析 + +其实聊到这个问题,一般面试官要考察两块: + +- 你有没有对某一个消息队列做过较为深入的原理的了解,或者从整体了解把握住一个消息队列的架构原理。 +- 看看你的设计能力,给你一个常见的系统,就是消息队列系统,看看你能不能从全局把握一下整体架构设计,给出一些关键点出来。 + +说实话,问类似问题的时候,大部分人基本都会蒙,因为平时从来没有思考过类似的问题,大多数人就是平时埋头用,从来不去思考背后的一些东西。类似的问题,比如,如果让你来设计一个 Spring 框架你会怎么做?如果让你来设计一个 Dubbo 框架你会怎么做?如果让你来设计一个 MyBatis 框架你会怎么做? + +### 面试题剖析 + +其实回答这类问题,说白了,不求你看过那技术的源码,起码你要大概知道那个技术的基本原理、核心组成部分、基本架构构成,然后参照一些开源的技术把一个系统设计出来的思路说一下就好。 + +比如说这个消息队列系统,我们从以下几个角度来考虑一下: + +- 首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了? +- 其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。 +- 其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。 +- 能不能支持数据 0 丢失啊?可以的,参考我们之前说的那个 kafka 数据零丢失方案。 + +mq 肯定是很复杂的,面试官问你这个问题,其实是个开放题,他就是看看你有没有从架构角度整体构思和设计的思维以及能力。确实这个问题可以刷掉一大批人,因为大部分人平时不思考这些东西。 diff --git a/docs/notmd/kafka.pptx b/docs/notmd/kafka.pptx deleted file mode 100644 index 4e0a304b..00000000 Binary files a/docs/notmd/kafka.pptx and /dev/null differ diff --git a/docs/package.json b/docs/package.json new file mode 100644 index 00000000..c6c80406 --- /dev/null +++ b/docs/package.json @@ -0,0 +1,30 @@ +{ + "name": "javatech", + "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.8.2", + "@vuepress/plugin-back-to-top": "^1.8.2", + "@vuepress/plugin-medium-zoom": "^1.8.2", + "@vuepress/plugin-pwa": "^1.8.2", + "@vuepress/theme-vue": "^1.8.2", + "markdownlint-cli": "^0.25.0", + "markdownlint-rule-emphasis-style": "^1.0.1", + "rimraf": "^3.0.1", + "vue-toasted": "^1.1.25", + "vuepress": "^1.8.2", + "vuepress-plugin-flowchart": "^1.4.2" + }, + "dependencies": { + "moment": "^2.29.1" + } +} diff --git a/docs/security/README.md b/docs/security/README.md new file mode 100644 index 00000000..dafda9b9 --- /dev/null +++ b/docs/security/README.md @@ -0,0 +1,12 @@ +# Java 和安全 + +## 📖 内容 + +- [shiro](shiro.md) +- [spring-security](spring-security.md) + +## 📚 资料 + +## 🚪 传送 + +◾ 🏠 [JAVATECH 首页](https://github.com/dunwu/javatech) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ diff --git a/docs/security/shiro.md b/docs/security/shiro.md new file mode 100644 index 00000000..4d846b50 --- /dev/null +++ b/docs/security/shiro.md @@ -0,0 +1,425 @@ +# 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/dev/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/dev/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/dev/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) diff --git a/docs/security/spring-security.md b/docs/security/spring-security.md new file mode 100644 index 00000000..8c2eafd0 --- /dev/null +++ b/docs/security/spring-security.md @@ -0,0 +1,221 @@ +# 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/dev/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/) diff --git a/docs/server/README.md b/docs/server/README.md new file mode 100644 index 00000000..3f14da25 --- /dev/null +++ b/docs/server/README.md @@ -0,0 +1,24 @@ +# Java 和服务器 + +## 📖 内容 + +- [Tomcat 应用指南](Tomcat应用指南.md) +- [Tomcat 连接器](Tomcat连接器.md) +- [Tomcat 容器](Tomcat容器.md) +- [Tomcat 优化](Tomcat优化.md) +- [Jetty](jetty.md) +- Tomcat 和 Jetty +- [Nginx](https://github.com/dunwu/nginx-tutorial) 📚 + +## 📚 资料 + +- [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) + +## 🚪 传送 + +◾ 🏠 [JAVACORE 首页](https://github.com/dunwu/javatech) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ diff --git "a/docs/server/Tomcat\344\274\230\345\214\226.md" "b/docs/server/Tomcat\344\274\230\345\214\226.md" new file mode 100644 index 00000000..259ef5e4 --- /dev/null +++ "b/docs/server/Tomcat\344\274\230\345\214\226.md" @@ -0,0 +1,154 @@ +# Tomcat 优化 + + + +- [1. Tomcat 启动优化](#1-tomcat-启动优化) + - [1.1. 清理 Tomcat](#11-清理-tomcat) + - [1.2. 禁止 Tomcat TLD 扫描](#12-禁止-tomcat-tld-扫描) + - [1.3. 关闭 WebSocket 支持](#13-关闭-websocket-支持) + - [1.4. 关闭 JSP 支持](#14-关闭-jsp-支持) + - [1.5. 禁止扫描 Servlet 注解](#15-禁止扫描-servlet-注解) + - [1.6. 配置 Web-Fragment 扫描](#16-配置-web-fragment-扫描) + - [1.7. 随机数熵源优化](#17-随机数熵源优化) + - [1.8. 并行启动多个 Web 应用](#18-并行启动多个-web-应用) +- [2. 参考资料](#2-参考资料) + + + +## 1. Tomcat 启动优化 + +如果 Tomcat 启动比较慢,可以考虑一些优化点 + +### 1.1. 清理 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 下次启动的时候会重新生成。 + +### 1.2. 禁止 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 + ``` + +### 1.3. 关闭 WebSocket 支持 + +Tomcat 会扫描 WebSocket 注解的 API 实现,比如 `@ServerEndpoint` 注解的类。如果不需要使用 WebSockets 就可以关闭它。具体方法是,找到 Tomcat 的 `conf/` 目录下的 `context.xml` 文件,给 `Context` 标签加一个 **`containerSciFilter`** 的属性: + +```xml + +... + +``` + +更进一步,如果你不需要 WebSockets 这个功能,你可以把 Tomcat `lib` 目录下的 `websocket-api.jar` 和 `tomcat-websocket.jar` 这两个 JAR 文件删除掉,进一步提高性能。 + +### 1.4. 关闭 JSP 支持 + +如果不需要使用 JSP,可以关闭 JSP 功能: + +```xml + +... + +``` + +如果要同时关闭 WebSocket 和 Jsp,可以这样配置: + +```xml + +... + +``` + +### 1.5. 禁止扫描 Servlet 注解 + +Servlet 3.0 引入了注解 Servlet,Tomcat 为了支持这个特性,会在 Web 应用启动时扫描你的类文件,因此如果你没有使用 Servlet 注解这个功能,可以告诉 Tomcat 不要去扫描 Servlet 注解。具体配置方法是,在你的 Web 应用的`web.xml`文件中,设置``元素的属性`metadata-complete="true"`,像下面这样。 + +```xml + + +``` + +`metadata-complete` 的意思是,`web.xml` 里配置的 Servlet 是完整的,不需要再去库类中找 Servlet 的定义。 + +### 1.6. 配置 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 + +... + +... + +``` + +### 1.7. 随机数熵源优化 + +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`)安全性会低一些,因为如果你对随机数的要求比较高, 可以考虑使用硬件方式生成熵源。 + +### 1.8. 并行启动多个 Web 应用 + +Tomcat 启动的时候,默认情况下 Web 应用都是一个一个启动的,等所有 Web 应用全部启动完成,Tomcat 才算启动完毕。如果在一个 Tomcat 下有多个 Web 应用,为了优化启动速度,你可以配置多个应用程序并行启动,可以通过修改 `server.xml` 中 Host 元素的 `startStopThreads` 属性来完成。`startStopThreads` 的值表示你想用多少个线程来启动你的 Web 应用,如果设成 0 表示你要并行启动 Web 应用,像下面这样的配置。 + +```xml + + ... + + ... + + ... + +``` + +需要注意的是,Engine 元素里也配置了这个参数,这意味着如果你的 Tomcat 配置了多个 Host(虚拟主机),Tomcat 会以并行的方式启动多个 Host。 + +## 2. 参考资料 + +- **官方** + - [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) diff --git "a/docs/server/Tomcat\345\256\271\345\231\250.md" "b/docs/server/Tomcat\345\256\271\345\231\250.md" new file mode 100644 index 00000000..f572bb6c --- /dev/null +++ "b/docs/server/Tomcat\345\256\271\345\231\250.md" @@ -0,0 +1,733 @@ +# Tomcat 容器 + + + +- [1. Tomcat 实现热部署和热加载](#1-tomcat-实现热部署和热加载) + - [1.1. ContainerBackgroundProcessor 实现](#11-containerbackgroundprocessor-实现) + - [1.2. backgroundProcess 方法](#12-backgroundprocess-方法) + - [1.3. Tomcat 热加载](#13-tomcat-热加载) + - [1.4. Tomcat 热部署](#14-tomcat-热部署) +- [2. Tomcat 的类加载机制](#2-tomcat-的类加载机制) + - [2.1. findClass 方法](#21-findclass-方法) + - [2.2. loadClass 方法](#22-loadclass-方法) + - [2.3. Tomcat 实现应用隔离](#23-tomcat-实现应用隔离) +- [3. Tomcat 实现 Servlet 规范](#3-tomcat-实现-servlet-规范) + - [3.1. Servlet 管理](#31-servlet-管理) + - [3.2. Filter 管理](#32-filter-管理) + - [3.3. Listener 管理](#33-listener-管理) +- [4. Tomcat 支持异步 Servlet](#4-tomcat-支持异步servlet) + - [4.1. 异步示例](#41-异步示例) + - [4.2. 异步 Servlet 原理](#42-异步-servlet-原理) +- [5. 参考资料](#5-参考资料) + + + +## 1. 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 等,它们具有父子关系。 + +### 1.1. 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 容器的周期性任务,它还会执行所有子容器的周期性任务**。 + +### 1.2. 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 方法,各种子容器和通用组件不需要各自弄一个后台线程来处理周期性任务,这样的设计显得优雅和整洁。 + +### 1.3. 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 参数来开启这个功能,像下面这样: + +``` + +``` + +### 1.4. 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 应用目录级别的变化。 + +## 2. Tomcat 的类加载机制 + +Tomcat 的自定义类加载器 `WebAppClassLoader` 打破了双亲委派机制,它**首先自己尝试去加载某个类,如果找不到再代理给父类加载器**,其目的是优先加载 Web 应用自己定义的类。具体实现就是重写 ClassLoader 的两个方法:findClass 和 loadClass。 + +### 2.1. 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 异常。 + +### 2.2. 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 类加载器的巧妙之处。 + +### 2.3. 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 应用的类。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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 实例之间相互隔离。 + +## 3. 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 也需要创建它们的实例,并在合适的时机去调用它们的方法。 + +### 3.1. 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 的实现原理。 + +### 3.2. 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 本质都是责任链模式,但是在具体实现上稍有不同,你可以细细体会一下。 + +### 3.3. 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 统一管理。 + +## 4. Tomcat 支持异步 Servlet + +### 4.1. 异步示例 + +```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 异常。 + +### 4.2. 异步 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 上监听新的请求就行了。 + +## 5. 参考资料 + +- **官方** + - [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) diff --git "a/docs/server/Tomcat\345\272\224\347\224\250\346\214\207\345\215\227.md" "b/docs/server/Tomcat\345\272\224\347\224\250\346\214\207\345\215\227.md" new file mode 100644 index 00000000..5f6c502f --- /dev/null +++ "b/docs/server/Tomcat\345\272\224\347\224\250\346\214\207\345\215\227.md" @@ -0,0 +1,895 @@ +# Tomcat 应用指南 + +> 🎁 版本说明 +> +> 当前最新版本:Tomcat 8.5.24 +> +> 环境要求:JDK7+ + + + +- [1. Tomcat 简介](#1-tomcat-简介) + - [1.1. Tomcat 是什么](#11-tomcat-是什么) + - [1.2. Tomcat 重要目录](#12-tomcat-重要目录) + - [1.3. web 工程发布目录结构](#13-web-工程发布目录结构) + - [1.4. Tomcat 功能](#14-tomcat-功能) +- [2. Tomcat 入门](#2-tomcat-入门) + - [2.1. 安装](#21-安装) + - [2.2. 配置](#22-配置) + - [2.3. 启动](#23-启动) +- [3. Tomcat 架构](#3-tomcat-架构) + - [3.1. Service](#31-service) + - [3.2. 连接器](#32-连接器) + - [3.3. 容器](#33-容器) +- [4. Tomcat 生命周期](#4-tomcat-生命周期) + - [4.1. Tomcat 的启动过程](#41-tomcat-的启动过程) + - [4.2. Web 应用的部署方式](#42-web-应用的部署方式) + - [4.3. LifeCycle](#43-lifecycle) + - [4.4. Connector 流程](#44-connector-流程) + - [4.5. Comet](#45-comet) + - [4.6. 异步 Servlet](#46-异步-servlet) +- [5. 参考资料](#5-参考资料) + + + +## 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/dev/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/dev/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 架构 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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`**。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201027091819.png) + +#### ProtocolHandler 组件 + +**连接器用 ProtocolHandler 接口来封装通信协议和 I/O 模型的差异**。ProtocolHandler 内部又分为 EndPoint 和 Processor 模块,EndPoint 负责底层 Socket 通信,Proccesor 负责应用层协议解析。 + +##### EndPoint + +EndPoint 是通信端点,即通信监听的接口,是具体的 Socket 接收和发送处理器,是对传输层的抽象,因此 EndPoint 是用来实现 TCP/IP 协议的。 + +EndPoint 是一个接口,对应的抽象实现类是 AbstractEndpoint,而 AbstractEndpoint 的具体子类,比如在 NioEndpoint 和 Nio2Endpoint 中,有两个重要的子组件:Acceptor 和 SocketProcessor。 + +其中 Acceptor 用于监听 Socket 连接请求。SocketProcessor 用于处理接收到的 Socket 请求,它实现 Runnable 接口,在 Run 方法里调用协议处理组件 Processor 进行处理。为了提高处理能力,SocketProcessor 被提交到线程池来执行。而这个线程池叫作执行器(Executor)。 + +##### Processor + +如果说 EndPoint 是用来实现 TCP/IP 协议的,那么 Processor 用来实现 HTTP 协议,Processor 接收来自 EndPoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象。 + +Processor 是一个接口,定义了请求的处理等方法。它的抽象实现类 AbstractProcessor 对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有 AJPProcessor、HTTP11Processor 等,这些具体实现类实现了特定协议的解析方法和请求处理方式。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201113185929.png) + +从图中我们看到,EndPoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池去处理,SocketProcessor 的 Run 方法会调用 Processor 组件去解析应用层协议,Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 方法。 + +#### 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 实例的创 建、执行和销毁。 + +#### 请求分发 Servlet 过程 + +Tomcat 是怎么确定请求是由哪个 Wrapper 容器里的 Servlet 来处理的呢?答案是,Tomcat 是用 Mapper 组件来完成这个任务的。 + +举例来说,假如有一个网购系统,有面向网站管理人员的后台管理系统,还有面向终端客户的在线购物系统。这两个系统跑在同一个 Tomcat 上,为了隔离它们的访问域名,配置了两个虚拟域名:`manage.shopping.com`和`user.shopping.com`,网站管理人员通过`manage.shopping.com`域名访问 Tomcat 去管理用户和商品,而用户管理和商品管理是两个单独的 Web 应用。终端客户通过`user.shopping.com`域名去搜索商品和下订单,搜索功能和订单管理也是两个独立的 Web 应用。如下所示,演示了 url 应声 Servlet 的处理流程。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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 管道。 + +#### Pipeline-Value + +Pipeline 可以理解为现实中的管道,Valve 为管道中的阀门,Request 和 Response 对象在管道中经过各个阀门的处理和控制。 + +Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。Valve 表示一个处理点,比如权限认证和记录日志。 + +先来了解一下 Valve 和 Pipeline 接口的设计: + +![img](https://raw.githubusercontent.com/dunwu/images/dev/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 链起来了。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/tools/tomcat/请求处理过程.png) + +整个调用过程由连接器中的 Adapter 触发的,它会调用 Engine 的第一个 Valve: + +```java +connector.getService().getContainer().getPipeline().getFirst().invoke(request, response); +``` + +## 4. Tomcat 生命周期 + +### 4.1. Tomcat 的启动过程 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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` 方法。 + +#### 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` 方法会释放和清理所有的资源。 + +#### 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 流程。 + +#### 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 在容器组件之后启动。组件停止的顺序跟启动顺序正好相反的,也是基于它们的依赖关系。 + +#### 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 + +![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201118105012.png) + +#### 4.2.3. 请求处理过程 + +
    + +
    + +1. 根据 server.xml 配置的指定的 connector 以及端口监听 http、或者 ajp 请求 +2. 请求到来时建立连接,解析请求参数,创建 Request 和 Response 对象,调用顶层容器 pipeline 的 invoke 方法 +3. 容器之间层层调用,最终调用业务 servlet 的 service 方法 +4. Connector 将 response 流中的数据写到 socket 中 + +### 4.4. Connector 流程 + +
    + +
    + +#### 4.3.1. 阻塞 IO + +
    + +
    + +#### 4.3.2. 非阻塞 IO + +
    + +
    + +#### 4.3.3. IO 多路复用 + +
    + +
    + +阻塞与非阻塞的区别在于进行读操作和写操作的系统调用时,如果此时内核态没有数据可读或者没有缓冲空间可写时,是否阻塞。 + +IO 多路复用的好处在于可同时监听多个 socket 的可读和可写事件,这样就能使得应用可以同时监听多个 socket,释放了应用线程资源。 + +#### 4.3.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-) diff --git "a/docs/server/Tomcat\350\277\236\346\216\245\345\231\250.md" "b/docs/server/Tomcat\350\277\236\346\216\245\345\231\250.md" new file mode 100644 index 00000000..c90c3845 --- /dev/null +++ "b/docs/server/Tomcat\350\277\236\346\216\245\345\231\250.md" @@ -0,0 +1,529 @@ +# Tomcat 连接器 + + + +- [1. NioEndpoint 组件](#1-nioendpoint-组件) + - [1.1. LimitLatch](#11-limitlatch) + - [1.2. Acceptor](#12-acceptor) + - [1.3. Poller](#13-poller) + - [1.4. SocketProcessor](#14-socketprocessor) +- [2. Nio2Endpoint 组件](#2-nio2endpoint-组件) + - [2.1. Nio2Acceptor](#21-nio2acceptor) + - [2.2. Nio2SocketWrapper](#22-nio2socketwrapper) +- [3. AprEndpoint 组件](#3-aprendpoint-组件) + - [3.1. AprEndpoint 工作流程](#31-aprendpoint-工作流程) + - [3.2. APR 提升性能的秘密](#32-apr-提升性能的秘密) +- [4. Executor 组件](#4-executor-组件) + - [4.1. Tomcat 定制线程池](#41-tomcat-定制线程池) + - [4.2. Tomcat 定制任务队列](#42-tomcat-定制任务队列) +- [5. WebSocket 组件](#5-websocket-组件) + - [5.1. WebSocket 加载](#51-websocket-加载) + - [5.2. WebSocket 请求处理](#52-websocket-请求处理) +- [6. 参考资料](#6-参考资料) + + + +## 1. NioEndpoint 组件 + +Tomcat 的 NioEndPoint 组件利用 Java NIO 实现了 I/O 多路复用模型。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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 较为相似。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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 工作流程 + +![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201127145740.jpg) + +#### 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。 + +#### 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 程序代码在运行过程中用到的内存就是本地内存中分配的。下面我们通过一张图来理解一下。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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 没有去冒这个险。 + +#### sendfile + +我们再来考虑另一个网络通信的场景,也就是静态文件的处理。浏览器通过 Tomcat 来获取一个 HTML 文件,而 Tomcat 的处理逻辑无非是两步: + +1. 从磁盘读取 HTML 到内存。 +2. 将这段内存的内容通过 Socket 发送出去。 + +但是在传统方式下,有很多次的内存拷贝: + +- 读取文件时,首先是内核把文件内容读取到内核缓冲区。 +- 如果使用 HeapByteBuffer,文件数据从内核到 JVM 堆内存需要经过本地内存中转。 +- 同样在将文件内容推入网络时,从 JVM 堆到内核缓冲区需要经过本地内存中转。 +- 最后还需要把文件从内核缓冲区拷贝到网卡缓冲区。 + +从下面的图你会发现这个过程有 6 次内存拷贝,并且 read 和 write 等系统调用将导致进程从用户态到内核态的切换,会耗费大量的 CPU 和内存资源。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/snap/20201127151041.jpg) + +而 Tomcat 的 AprEndpoint 通过操作系统层面的 sendfile 特性解决了这个问题,sendfile 系统调用方式非常简洁。 + +``` +sendfile(socket, file, len); +``` + +它带有两个关键参数:Socket 和文件句柄。将文件从磁盘写入 Socket 的过程只有两步: + +第一步:将文件内容读取到内核缓冲区。 + +第二步:数据并没有从内核缓冲区复制到 Socket 关联的缓冲区,只有记录数据位置和长度的描述符被添加到 Socket 缓冲区中;接着把数据直接从内核缓冲区传递给网卡。这个过程你可以看下面的图。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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 实例来处理请求。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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) diff --git a/docs/server/jetty.md b/docs/server/jetty.md new file mode 100644 index 00000000..b884d07f --- /dev/null +++ b/docs/server/jetty.md @@ -0,0 +1,679 @@ +# Jetty 应用指南 + + + +- [1. Jetty 简介](#1-jetty-简介) +- [2. Jetty 的使用](#2-jetty-的使用) + - [2.1. API 方式](#21-api-方式) + - [2.2. Maven 插件方式](#22-maven-插件方式) +- [3. Jetty 的架构](#3-jetty-的架构) + - [3.1. Jetty 架构简介](#31-jetty-架构简介) + - [3.2. Jetty 和 Tomcat 架构区别](#32-jetty-和-tomcat-架构区别) + - [3.3. Connector 组件](#33-connector-组件) + - [3.4. Handler 组件](#34-handler-组件) +- [4. Jetty 的线程策略](#4-jetty-的线程策略) + - [4.1. 传统 Selector 编程模型](#41-传统-selector-编程模型) + - [4.2. Jetty 的 Selector 编程模型](#42-jetty-的-selector-编程模型) +- [5. 参考资料](#5-参考资料) + + + +## 1. 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 的需要。 + +## 2. Jetty 的使用 + +我觉得嵌入式启动方式的一个好处在于:可以直接运行项目,无需每次部署都得再配置服务器。 + +jetty 的嵌入式启动使用有两种方式: + +API 方式 + +maven 插件方式 + +### 2.1. 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) + +### 2.2. 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 + + + + + +``` + +## 3. Jetty 的架构 + +### 3.1. Jetty 架构简介 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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 方法启动它们。 + +### 3.2. Jetty 和 Tomcat 架构区别 + +对比一下 Tomcat 的整体架构图,你会发现 Tomcat 在整体上跟 Jetty 很相似,它们的第一个区别是 Jetty 中没有 Service 的概念,Tomcat 中的 Service 包装了多个连接器和一个容器组件,一个 Tomcat 实例可以配置多个 Service,不同的 Service 通过不同的连接器监听不同的端口;而 Jetty 中 Connector 是被所有 Handler 共享的。 + +第二个区别是,在 Tomcat 中每个连接器都有自己的线程池,而在 Jetty 中所有的 Connector 共享一个全局的线程池。 + +### 3.3. 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 的工作流程。 + +![](https://raw.githubusercontent.com/dunwu/images/dev/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 组件去处理。 + +### 3.4. 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 容器功能的呢? + +![](https://raw.githubusercontent.com/dunwu/images/dev/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。 + +## 4. Jetty 的线程策略 + +### 4.1. 传统 Selector 编程模型 + +常规的 NIO 编程思路是,将 I/O 事件的侦测和请求的处理分别用不同的线程处理。具体过程是: + +启动一个线程,在一个死循环里不断地调用 select 方法,检测 Channel 的 I/O 状态,一旦 I/O 事件达到,比如数据就绪,就把该 I/O 事件以及一些数据包装成一个 Runnable,将 Runnable 放到新线程中去处理。 + +在这个过程中按照职责划分,有两个线程在干活,一个是 I/O 事件检测线程,另一个是 I/O 事件处理线程。这样的好处是它们互不干扰和阻塞对方。 + +### 4.2. 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 就绪事件。 + +## 5. 参考资料 + +- [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) diff --git a/docs/server/tomcat-and-jetty.md b/docs/server/tomcat-and-jetty.md new file mode 100644 index 00000000..b7ca24d8 --- /dev/null +++ b/docs/server/tomcat-and-jetty.md @@ -0,0 +1,17 @@ +## 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/standalone/README.md b/docs/standalone/README.md deleted file mode 100644 index 2f00b454..00000000 --- a/docs/standalone/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# 单机 - -> Web 领域的单机技术 diff --git a/docs/standalone/mvc/spring.md b/docs/standalone/mvc/spring.md deleted file mode 100644 index 2e45adc2..00000000 --- a/docs/standalone/mvc/spring.md +++ /dev/null @@ -1,238 +0,0 @@ ---- -title: Spring面试题 -date: 2018/07/02 -categories: -- javaweb -tags: -- java -- javaweb -- mvc - ---- - -# Spring 面试题 - - - -- [IoC](#ioc) - - [什么是控制反转(IoC)?什么是依赖注入?](#什么是控制反转ioc什么是依赖注入) - - [Spring 中的 IoC](#spring-中的-ioc) - - [BeanFactory 和 ApplicationContext 有什么区别?](#beanfactory-和-applicationcontext-有什么区别) - - [Spring 有几种配置方式](#spring-有几种配置方式) - - [Spring Bean 的生命周期](#spring-bean-的生命周期) - - [Spring Bean 的作用域](#spring-bean-的作用域) - - [Spring 框架中的单例 Beans 是线程安全的么?](#spring-框架中的单例-beans-是线程安全的么) - - [Spring 中注入一个 Java Collection?](#spring-中注入一个-java-collection) - - [自动装配模式](#自动装配模式) -- [AOP](#aop) - - [什么是 AOP](#什么是-aop) - - [AOP 的原理](#aop-的原理) - - [动态代理](#动态代理) - - [静态代理](#静态代理) - - - -## IoC - -### 什么是控制反转(IoC)?什么是依赖注入? - -IoC,是 Inversion of Control 的缩写,即控制反转。 - -- 上层模块不应该依赖于下层模块,它们共同依赖于一个抽象 -- 抽象不能依赖于具体实现,具体实现依赖于抽象。 - -注:又称为依赖倒置原则。这是设计模式六大原则之一。 - -DI,是 Dependency Injection 的缩写,即依赖注入。 - -- 依赖注入是 IoC 的最常见形式。 -- 容器全权负责的组件的装配,它会把符合依赖关系的对象通过 JavaBean 属性或者构造函数传递给需要的对象。 - -依赖注入三种形式: - -1. 构造器注入 -2. Setter 方法注入 -3. 接口注入 - -### Spring 中的 IoC - -BeanFactory 是 Spring IoC 容器的具体实现,用来包装和管理前面提到的各种 bean。 - -BeanFactory 接口是 Spring IoC 容器的核心接口。 - -IOC:把对象的创建、初始化、销毁交给 spring 来管理,而不是由开发者控制,实现控制反转。 - -### BeanFactory 和 ApplicationContext 有什么区别? - -BeanFactory 包含了种 bean 的定义,以便在接收到客户端请求时将对应的 bean 实例化。 - -BeanFactory 还能在实例化对象的时生成协作类之间的关系。 - -BeanFactory 还包含了 bean 生命周期的控制,调用客户端的初始化方法(initialization methods)和销毁方法(destruction methods)。 - -ApplicationContext 扩展了 BeanFactory: - -1. 提供了支持国际化的文本消息 -2. 统一的资源文件读取方式 -3. 已在监听器中注册的 bean 的事件 - -三种较常见的 ApplicationContext 实现: - -1. ClassPathXmlApplicationContext - 从 classpath 的 XML 配置文件中读取上下文。 -2. FileSystemXmlApplicationContext - 由文件系统中的 XML 配置文件读取上下文。 -3. XmlWebApplicationContext - 由 Web 应用的 XML 文件读取上下文。 - -### Spring 有几种配置方式 - -1. 基于 XML 的配置 -2. 基于注解的配置 -3. 基于 Java 的配置 - -#### xml 配置方式 - -在 `` 元素下指定 schema 命令空间,如:context、beans、jdbc、tx、aop、mvc 等。 - -然后声明 bean 或命名空间下提供的能力。 - -#### Java 配置方式 - -使用 @Configuration - -```java -@Configuration -public class AppConfig{ - @Bean - public MyService myService() { - return new MyServiceImpl(); - } -} -``` - -利用 AnnotationConfigApplicationContext 类进行实例化 - -```java -public static void main(String[] args) { - ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class); - MyService myService = ctx.getBean(MyService.class); - myService.doStuff(); -} -``` - -#### 注解方式配置 - -开启注解扫描 - -```xml - - - - -``` - -引入 JavaBean 的注解 - -- @Required - 该注解应用于设值方法。 -- @Autowired - 该注解应用于有值设值方法、非设值方法、构造方法和变量。 -- @Qualifier - 该注解和@Autowired 注解搭配使用,用于消除特定 bean 自动装配的歧义。 -- JSR-250 Annotations - Spring 支持基于 JSR-250 注解的以下注解,@Resource、@PostConstruct 和 @PreDestroy - -### Spring Bean 的生命周期 - -BeanFactory 负责管理在 spring 容器中被创建的 bean 的生命周期。Bean 的生命周期由两组回调(call back)方法组成。 - -1. 初始化之后调用的回调方法。 -2. 销毁之前调用的回调方法。 - -Spring 框架提供了以下四种方式来管理 bean 的生命周期事件: - -1. InitializingBean 和 DisposableBean 回调接口 -2. 针对特殊行为的其他 Aware 接口 -3. Bean 配置文件中的 Custom init()方法和 destroy()方法 -4. @PostConstruct 和@PreDestroy 注解方式 - -### Spring Bean 的作用域 - -- singleton:这种 bean 范围是默认的,这种范围确保不管接受到多少个请求,每个容器中只有一个 bean 的实例,单例的模式由 bean factory 自身来维护。 -- prototype:原形范围与单例范围相反,为每一个 bean 请求提供一个实例。 -- request:在请求 bean 范围内会每一个来自客户端的网络请求创建一个实例,在请求完成以后,bean 会失效并被垃圾回收器回收。 -- Session:与请求范围类似,确保每个 session 中有一个 bean 的实例,在 session 过期后,bean 会随之失效。 -- global-session:global-session 和 Portlet 应用相关。当你的应用部署在 Portlet 容器中工作时,它包含很多 portlet。如果你想要声明让所有的 portlet 共用全局的存储变量的话,那么这全局变量需要存储在 global-session 中。 - -### Spring 框架中的单例 Beans 是线程安全的么? - -Spring 框架并没有对单例 bean 进行任何多线程的封装处理。关于单例 bean 的线程安全和并发问题需要开发者自行去搞定。 - -最浅显的解决办法就是将多态 bean 的作用域由“singleton”变更为“prototype”。 - -### Spring 中注入一个 Java Collection? - -Spring 提供了以下四种集合类的配置元素: - -1. `` - 该标签用来装配可重复的 list 值。 -2. `` - 该标签用来装配没有重复的 set 值。 -3. `` - 该标签可用来注入键和值可以为任何类型的键值对。 -4. `` - 该标签支持注入键和值都是字符串类型的键值对。 - -### 自动装配模式 - -- no:这是 Spring 框架的默认设置,在该设置下自动装配是关闭的,开发者需要自行在 bean 定义中用标签明确的设置依赖关系。 -- byName:该选项可以根据 bean 名称设置依赖关系。当向一个 bean 中自动装配一个属性时,容器将根据 bean 的名称自动在在配置文件中查询一个匹配的 bean。如果找到的话,就装配这个属性,如果没找到的话就报错。 -- byType:该选项可以根据 bean 类型设置依赖关系。当向一个 bean 中自动装配一个属性时,容器将根据 bean 的类型自动在在配置文件中查询一个匹配的 bean。如果找到的话,就装配这个属性,如果没找到的话就报错。 -- constructor:造器的自动装配和 byType 模式类似,但是仅仅适用于与有构造器相同参数的 bean,如果在容器中没有找到与构造器参数类型一致的 bean,那么将会抛出异常。 -- autodetect:该模式自动探测使用构造器自动装配或者 byType 自动装配。首先,首先会尝试找合适的带参数的构造器,如果找到的话就是用构造器自动装配,如果在 bean 内部没有找到相应的构造器或者是无参构造器,容器就会自动选择 byTpe 的自动装配方式。 - -## AOP - -### 什么是 AOP - -AOP 把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似。比如权限认证、日志、事务处理。 - -Aop 的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。正如 Avanade 公司的高级方案构架师 Adam Magee 所说,AOP 的核心思想就是“将应用程序中的商业逻辑同对其提供支持的通用服务进行分离。” - -### AOP 的原理 - -实现 AOP 的技术,主要分为两大类: - -1. 采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行; -2. 采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。 - -### 动态代理 - -#### JDK - -需要实现 InvocationHandler 接口。 - -#### CGLIB - -### 静态代理 - -```java -public interface IPerson { - public void doSomething(); -} - -public class Person implements IPerson { - public void doSomething(){ - System.out.println("I want wo sell this house"); - } -} - -public class PersonProxy { - private IPerson iPerson; - private final static Logger logger = LoggerFactory.getLogger(PersonProxy.class); - - public PersonProxy(IPerson iPerson) { - this.iPerson = iPerson; - } - public void doSomething() { - logger.info("Before Proxy"); - iPerson.doSomething(); - logger.info("After Proxy"); - } - - public static void main(String[] args) { - PersonProxy personProxy = new PersonProxy(new Person()); - personProxy.doSomething(); - } -} -``` diff --git a/docs/standalone/orm/mybatis.md b/docs/standalone/orm/mybatis.md deleted file mode 100644 index b97f0b2f..00000000 --- a/docs/standalone/orm/mybatis.md +++ /dev/null @@ -1,195 +0,0 @@ ---- -title: MyBatis -date: 2018/06/19 -categories: -- orm -tags: -- java -- orm ---- - -# MyBatis - -> MyBatis 的前身就是 iBatis ,是一个作用在数据持久层的对象关系映射(Object Relational Mapping,简称 ORM)框架。 - - - -- [概述](#概述) - - [MyBatis vs. Hibernate](#mybatis-vs-hibernate) - - [MyBatis 的架构](#mybatis-的架构) -- [接口层](#接口层) - - [使用传统的 MyBatis 提供的 API](#使用传统的-mybatis-提供的-api) - - [使用 Mapper 接口](#使用-mapper-接口) -- [数据处理层](#数据处理层) - - [参数映射和动态 SQL 语句生成](#参数映射和动态-sql-语句生成) - - [SQL 语句的执行以及封装查询结果集成 `List`](#sql-语句的执行以及封装查询结果集成-liste) -- [框架支撑层](#框架支撑层) - - [事务管理机制](#事务管理机制) - - [连接池管理机制](#连接池管理机制) - - [缓存机制](#缓存机制) - - [SQL 语句的配置方式](#sql-语句的配置方式) -- [引导层](#引导层) -- [主要组件](#主要组件) -- [资料](#资料) - - [官方](#官方) - - [第三方](#第三方) - - - -## 概述 - -### MyBatis vs. Hibernate - -Mybatis 优势 - -- MyBatis 可以进行更为细致的 SQL 优化,可以减少查询字段。 -- MyBatis 容易掌握,而 Hibernate 门槛较高。 - -Hibernate 优势 - -- Hibernate 的 DAO 层开发比 MyBatis 简单,Mybatis 需要维护 SQL 和结果映射。 -- Hibernate 对对象的维护和缓存要比 MyBatis 好,对增删改查的对象的维护要方便。 -- Hibernate 数据库移植性很好,MyBatis 的数据库移植性不好,不同的数据库需要写不同 SQL。 -- Hibernate 有更好的二级缓存机制,可以使用第三方缓存。MyBatis 本身提供的缓存机制不佳。 - -### MyBatis 的架构 - -
    - -
    - -## 接口层 - -接口层负责和数据库交互的方式 - -MyBatis 和数据库的交互有两种方式: - -1. 使用传统的 MyBatis 提供的 API; -2. 使用 Mapper 接口 - -### 使用传统的 MyBatis 提供的 API - -这是传统的传递 Statement Id 和查询参数给 SqlSession 对象,使用 SqlSession 对象完成和数据库的交互;MyBatis 提供了非常方便和简单的 API,供用户实现对数据库的增删改查数据操作,以及对数据库连接信息和 MyBatis 自身配置信息的维护操作。 - -
    - -
    - -上述使用 MyBatis 的方法,是创建一个和数据库打交道的 SqlSession 对象,然后根据 Statement Id 和参数来操作数据库,这种方式固然很简单和实用,但是它不符合面向对象语言的概念和面向接口编程的编程习惯。由于面向接口的编程是面向对象的大趋势,MyBatis 为了适应这一趋势,增加了第二种使用 MyBatis 支持接口(Interface)调用方式。 - -### 使用 Mapper 接口 - -MyBatis 将配置文件中的每一个 `` 节点抽象为一个 Mapper 接口,而这个接口中声明的方法和跟 `` 节点中的 `` 节点相对应,即 `` 节点的 id 值为 Mapper 接口中的方法名称,parameterType 值表示 Mapper 对应方法的入参类型,而 resultMap 值则对应了 Mapper 接口表示的返回值类型或者返回结果集的元素类型。 - -
    - -
    - -根据 MyBatis 的配置规范配置好后,通过 `SqlSession.getMapper(XXXMapper.class)` 方法,MyBatis 会根据相应的接口声明的方法信息,通过动态代理机制生成一个 Mapper 实例,我们使用 Mapper 接口的某一个方法时,MyBatis 会根据这个方法的方法名和参数类型,确定 Statement Id,底层还是通过`SqlSession.select("statementId",parameterObject);` 或者 `SqlSession.update("statementId",parameterObject);` 等等来实现对数据库的操作。 - -MyBatis 引用 Mapper 接口这种调用方式,纯粹是为了满足面向接口编程的需要。(其实还有一个原因是在于,面向接口的编程,使得用户在接口上可以使用注解来配置 SQL 语句,这样就可以脱离 XML 配置文件,实现“0 配置”)。 - -## 数据处理层 - -数据处理层可以说是 MyBatis 的核心,从大的方面上讲,它要完成两个功能: - -1. 通过传入参数构建动态 SQL 语句; -2. SQL 语句的执行以及封装查询结果集成 `List` - -### 参数映射和动态 SQL 语句生成 - -动态语句生成可以说是 MyBatis 框架非常优雅的一个设计,MyBatis 通过传入的参数值,**使用 Ognl 来动态地构造 SQL 语句**,使得 MyBatis 有很强的灵活性和扩展性。 - -参数映射指的是对于 java 数据类型和 jdbc 数据类型之间的转换:这里有包括两个过程:查询阶段,我们要将 java 类型的数据,转换成 jdbc 类型的数据,通过 `preparedStatement.setXXX()` 来设值;另一个就是对 resultset 查询结果集的 jdbcType 数据转换成 java 数据类型。 - -### SQL 语句的执行以及封装查询结果集成 `List` - -动态 SQL 语句生成之后,MyBatis 将执行 SQL 语句,并将可能返回的结果集转换成 `List` 列表。MyBatis 在对结果集的处理中,支持结果集关系一对多和多对一的转换,并且有两种支持方式,一种为嵌套查询语句的查询,还有一种是嵌套结果集的查询。 - -## 框架支撑层 - -### 事务管理机制 - -对数据库的事务而言,应该具有以下几点:创建(create)、提交(commit)、回滚(rollback)、关闭(close)。对应地,MyBatis 将事务抽象成了 Transaction 接口。 - -MyBatis 的事务管理分为两种形式: - -1. 使用 JDBC 的事务管理机制:即利用 java.sql.Connection 对象完成对事务的提交(commit())、回滚(rollback())、关闭(close())等。 -2. 使用 MANAGED 的事务管理机制:这种机制 MyBatis 自身不会去实现事务管理,而是让程序的容器如(JBOSS,Weblogic)来实现对事务的管理。 - -### 连接池管理机制 - -由于创建一个数据库连接所占用的资源比较大, 对于数据吞吐量大和访问量非常大的应用而言,连接池的设计就显得非常重要,对于连接池管理机制我已经在我的博文《深入理解 mybatis 原理》 Mybatis 数据源与连接池 中有非常详细的讨论,感兴趣的读者可以点击查看。 - -### 缓存机制 - -MyBatis 将数据缓存设计成两级结构,分为一级缓存、二级缓存: - -- **一级缓存是 Session 会话级别的缓存**,位于表示一次数据库会话的 SqlSession 对象之中,又被称之为本地缓存。一级缓存是 MyBatis 内部实现的一个特性,用户不能配置,默认情况下自动支持的缓存,用户没有定制它的权利(不过这也不是绝对的,可以通过开发插件对它进行修改); -- **二级缓存是 Application 应用级别的缓存**,它的是生命周期很长,跟 Application 的声明周期一样,也就是说它的作用范围是整个 Application 应用。 - -
    - -
    - -#### 一级缓存的工作机制 - -一级缓存是 Session 会话级别的,一般而言,一个 SqlSession 对象会使用一个 Executor 对象来完成会话操作,Executor 对象会维护一个 Cache 缓存,以提高查询性能。 - -
    - -
    - - -#### 二级缓存的工作机制 - -如上所言,一个 SqlSession 对象会使用一个 Executor 对象来完成会话操作,MyBatis 的二级缓存机制的关键就是对这个 Executor 对象做文章。如果用户配置了 `"cacheEnabled=true"`,那么 MyBatis 在为 SqlSession 对象创建 Executor 对象时,会对 Executor 对象加上一个装饰者:CachingExecutor,这时 SqlSession 使用 CachingExecutor 对象来完成操作请求。CachingExecutor 对于查询请求,会先判断该查询请求在 Application 级别的二级缓存中是否有缓存结果,如果有查询结果,则直接返回缓存结果;如果缓存中没有,再交给真正的 Executor 对象来完成查询操作,之后 CachingExecutor 会将真正 Executor 返回的查询结果放置到缓存中,然后在返回给用户。 - -
    - -
    - - -### SQL 语句的配置方式 - -传统的 MyBatis 配置 SQL 语句方式就是使用 XML 文件进行配置的,但是这种方式不能很好地支持面向接口编程的理念,为了支持面向接口的编程,MyBatis 引入了 Mapper 接口的概念,面向接口的引入,对使用注解来配置 SQL 语句成为可能,用户只需要在接口上添加必要的注解即可,不用再去配置 XML 文件了,但是,目前的 MyBatis 只是对注解配置 SQL 语句提供了有限的支持,某些高级功能还是要依赖 XML 配置文件配置 SQL 语句。 - -## 引导层 - -引导层是配置和启动 MyBatis 配置信息的方式。MyBatis 提供两种方式来引导 MyBatis : - -1. 基于 XML 配置文件的方式 -2. 基于 Java API 的方式 - -## 主要组件 - -从 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 对象之中。 - -它们的关系如下图所示: - -
    - -
    - -## 资料 - -### 官方 - -| [Github](https://github.com/mybatis/mybatis-3) | [官网](http://www.mybatis.org/mybatis-3/) | [MyBatis Generator](https://github.com/mybatis/generator) - -### 第三方 - -- [Mybatis-PageHelper](https://github.com/pagehelper/Mybatis-PageHelper) - Mybatis 通用分页插件 -- [MyBatis Spring Adapter](https://github.com/mybatis/spring) - Mybatis 和 Spring 的桥接包 -- [深入理解 mybatis 原理](https://blog.csdn.net/luanlouis/article/details/40422941) diff --git a/docs/standalone/security/shiro.md b/docs/standalone/security/shiro.md deleted file mode 100644 index b85ee7a7..00000000 --- a/docs/standalone/security/shiro.md +++ /dev/null @@ -1,79 +0,0 @@ ---- -title: Shiro -date: 2018/06/13 -categories: -- security -tags: -- java -- security -- shiro ---- - -# Shiro - -> Shiro 是一个安全框架,具有认证、授权、加密、会话管理功能。 - - - -- [概述](#概述) - - [Shiro 功能](#shiro-功能) - - [Shiro 架构](#shiro-架构) -- [认证](#认证) -- [授权](#授权) -- [资料](#资料) - - - -## 概述 - -### Shiro 功能 - -

    - -

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

    - -

    - -- **Subject** - 主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是 Subject,如网络爬虫,机器人等;即一个抽象概念;所有 Subject 都绑定到 SecurityManager,与 Subject 的所有交互都会委托给 SecurityManager;可以把 Subject 认为是一个门面;SecurityManager 才是实际的执行者; -- **SecurityManager** - 安全管理器;即所有与安全有关的操作都会与 SecurityManager 交互;且它管理着所有 Subject;可以看出它是 Shiro 的核心,它负责与后边介绍的其他组件进行交互,如果学习过 SpringMVC,你可以把它看成 DispatcherServlet 前端控制器; -- **Realm** - 域,Shiro 从从 Realm 获取安全数据(如用户、角色、权限),就是说 SecurityManager 要验证用户身份,那么它需要从 Realm 获取相应的用户进行比较以确定用户身份是否合法;也需要从 Realm 得到用户相应的角色/权限进行验证用户是否能进行操作;可以把 Realm 看成 DataSource,即安全数据源。 - -

    - -

    - -- **Subject** - 主体,可以看到主体可以是任何可以与应用交互的“用户”; -- **SecurityManager** - 相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。 -- **Authenticator** - 认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了; -- **Authrizer** - 授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能; -- **Realm** - 可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;注意 - Shiro 不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的 Realm; -- **SessionManager** - 如果写过 Servlet 就应该知道 Session 的概念,Session 呢需要有人去管理它的生命周期,这个组件就是 SessionManager;而 Shiro 并不仅仅可以用在 Web 环境,也可以用在如普通的 JavaSE 环境、EJB 等环境;所有呢,Shiro 就抽象了一个自己的 Session 来管理主体与应用之间交互的数据;这样的话,比如我们在 Web 环境用,刚开始是一台 Web 服务器;接着又上了台 EJB 服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到 Memcached 服务器); -- **SessionDAO** - DAO 大家都用过,数据访问对象,用于会话的 CRUD,比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能; -- **CacheManager** - 缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能 -- **Cryptography** - 密码模块,Shiro 提高了一些常见的加密组件用于如密码加密/解密的。 - -## 认证 - -## 授权 - -## 资料 - -- [shiro 官方文档](http://shiro.apache.org/reference.html) -- [跟我学 Shiro](http://jinnianshilongnian.iteye.com/category/305053) diff --git a/docs/test/README.md b/docs/test/README.md new file mode 100644 index 00000000..9373deb6 --- /dev/null +++ b/docs/test/README.md @@ -0,0 +1,14 @@ +# Java 和测试 + +## 📖 内容 + +- [Junit](junit.md) +- [Mockito](mockito.md) +- [JMH](jmh.md) +- [Jmeter](jmeter.md) + +## 📚 资料 + +## 🚪 传送 + +◾ 🏠 [JAVACORE 首页](https://github.com/dunwu/javatech) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ diff --git a/docs/test/jmeter.md b/docs/test/jmeter.md new file mode 100644 index 00000000..028c260f --- /dev/null +++ b/docs/test/jmeter.md @@ -0,0 +1,204 @@ +# JMeter 应用指南 + +> [Jmeter](https://github.com/apache/jmeter) 是一款基于 Java 开发的功能和性能测试软件。 +> +> 🎁 本文编辑时的最新版本为:5.1.1 + +## 1. 简介 + +[Jmeter](https://github.com/apache/jmeter) 是一款使用 Java 开发的功能和性能测试软件。 + +### 1.1. 特性 + +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 对象 + +### 1.2. 工作流 + +Jmeter 的工作原理是仿真用户向服务器发送请求,并收集服务器应答信息并计算统计信息。 + +Jmeter 的工作流如下图所示: + +![img](https://raw.githubusercontent.com/dunwu/images/dev/cs/java/javaweb/technology/test/jmeter-workflow.png) + +### 1.3. 主要元素 + +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/dev/cs/java/javaweb/technology/test/jmeter-elements.png) + +> 📌 提示: +> +> Jmeter 元素的数量关系大致如下: +> +> 1. 脚本中最多只能有一个测试计划。 +> 2. 测试计划中至少要有一个线程组。 +> 3. 线程组中至少要有一个取样器。 +> 4. 线程组中至少要有一个监听器。 + +## 2. 安装 + +### 2.1. 环境要求 + +- 必要的。Jmeter 基于 JDK8 开发,所以必须运行在 JDK8 环境。 + + - JDK8 + +- 可选的。有些 jar 包不是 Jmeter 提供的,如果需要相应的功能,需要自行下载并置于 `lib` 目录。 + - JDBC + - JMS + - [Bouncy Castle](http://www.bouncycastle.org/test_releases.html) + +### 2.2. 下载 + +进入 [**Jmeter 官网下载地址**](https://jmeter.apache.org/download_jmeter.cgi) 选择需要版本进行下载。 + +### 2.3. 启动 + +解压 Jmeter 压缩包,进入 bin 目录 + +Unix 类系统运行 `jmeter` ;Windows 系统运行 `jmeter.bat` + +![image-20191024104517721](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024104517721.png) + +## 3. 使用 + +### 3.1. 创建测试计划 + +> 🔔 注意: +> +> - 在运行整个测试计划之前,应保存测试计划。 +> +> - JMeter 的测试计划以 `.jmx` 扩展文件的形式保存。 + +#### 3.1.1. 创建线程组 + +- 在“测试计划”上右键 【添加】=>【线程(用户)】=>【线程组】。 + +- 设置线程数和循环次数 + +![image-20191024105545736](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024105545736.png) + +#### 3.1.2. 配置原件 + +- 在新建的线程组上右键 【添加】=>【配置元件】=>【HTTP 请求默认值】。 + +- 填写协议、服务器名称或 IP、端口号 + +![image-20191024110016264](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024110016264.png) + +#### 3.1.3. 构造 HTTP 请求 + +- 在“线程组”上右键 【添加-】=>【取样器】=>【HTTP 请求】。 + +- 填写协议、服务器名称或 IP、端口号(如果配置了 HTTP 请求默认值可以忽略) +- 填写方法、路径 +- 填写参数、消息体数据、文件上传 + +![image-20191024110953063](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024110953063.png) + +#### 3.1.4. 添加 HTTP 请求头 + +- 在“线程组”上右键 【添加】=>【配置元件】=>【HTTP 信息头管理器】 +- 由于我的测试例中传输的数据为 json 形式,所以设置键值对 `Content-Type`:`application/json` + +![image-20191024111825226](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024111825226.png) + +#### 3.1.5. 添加断言 + +- 在“线程组”上右键 【添加】=>【断言】=>【 响应断言 】 +- 在我的案例中,以 HTTP 应答状态码为 200 来判断请求是否成功 + +![image-20191024112335130](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024112335130.png) + +#### 3.1.6. 添加察看结果树 + +- 在“线程组”上右键 【添加】=>【监听器】=>【察看结果树】 +- 直接点击运行,就可以查看测试结果 + +![image-20191024113849270](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024113849270.png) + +#### 3.1.7. 添加汇总报告 + +- 在“线程组”上右键 【添加】=>【监听器】=>【汇总报告】 +- 直接点击运行,就可以查看测试结果 + +![image-20191024114016424](https://raw.githubusercontent.com/dunwu/images/dev/snap/jmeter/image-20191024114016424.png) + +#### 3.1.8. 保存测试计划 + +执行测试计划前,GUI 会提示先保存配置为 `jmx` 文件。 + +### 3.2. 执行测试计划 + +官方建议不要直接使用 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/dev/snap/jmeter/image-20191024120233058.png) + +## 4. 问题 + +### 4.1. 如何读取本地 txt/csv 文件作为请求参数 + +参考:[Jmeter 读取本地 txt/csv 文件作为请求参数,实现接口自动化](https://www.jianshu.com/p/3b2d3b643415) + +(1)依次点击【添加】=>【配置元件】=>【CSV 数据文件设置】 + +配置如下所示: + +![image-20191127175820747](https://raw.githubusercontent.com/dunwu/images/dev/snap/image-20191127175820747.png) + +重要配置说明(其他配置根据实际情况填): + +- 文件名:输入需要导入的数据文件位置。 +- 文件编码:设为 UTF-8,避免乱码。 +- 变量名称:使用 `,` 分隔输入变量列表。如截图中设置了两个变量 `a` 和 `b` + +(2)在 HTTP 请求的消息体数据中配置参数 + +``` +[{"a":"${a}","b":"${b}"}] +``` + +### 4.2. 如何有序发送数据 + +依次点击【添加】=>【逻辑控制器】=>【事务控制器】 + +## 5. 参考资料 + +- [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) diff --git a/docs/test/jmh.md b/docs/test/jmh.md new file mode 100644 index 00000000..a14f54a9 --- /dev/null +++ b/docs/test/jmh.md @@ -0,0 +1,340 @@ +# 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/) diff --git a/docs/test/junit.md b/docs/test/junit.md new file mode 100644 index 00000000..f8750cf5 --- /dev/null +++ b/docs/test/junit.md @@ -0,0 +1,582 @@ +# JUnit5 应用指南 + +> version: junit5 + + + +- [1. 安装](#1-安装) +- [2. JUnit 注解](#2-junit-注解) +- [3. 编写单元测试](#3-编写单元测试) + - [3.1. 基本的单元测试类和方法](#31-基本的单元测试类和方法) + - [3.2. 定制测试类和方法的显示名称](#32-定制测试类和方法的显示名称) + - [3.3. 断言(Assertions)](#33-断言assertions) + - [3.4. 假想(Assumptions)](#34-假想assumptions) + - [3.5. 禁用](#35-禁用) + - [3.6. 测试条件](#36-测试条件) + - [3.7. 嵌套测试](#37-嵌套测试) + - [3.8. 重复测试](#38-重复测试) + - [3.9. 参数化测试](#39-参数化测试) +- [4. 引用和引申](#4-引用和引申) + + + +## 1. 安装 + +在 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/dev/cs/java/javalib/test/junit/junit5-components.png) + +## 2. JUnit 注解 + +| 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_. | + +## 3. 编写单元测试 + +### 3.1. 基本的单元测试类和方法 + +```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()"); + } +} +``` + +### 3.2. 定制测试类和方法的显示名称 + +支持普通字符、特殊符号、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() { } +} +``` + +### 3.3. 断言(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!"; + } + +} +``` + +### 3.4. 假想(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"); + } + +} +``` + +### 3.5. 禁用 + +禁用单元测试类示例: + +```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() { + } +} +``` + +### 3.6. 测试条件 + +#### 操作系统条件 + +```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() { + // ... +} +``` + +### 3.7. 嵌套测试 + +```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()); + } + } + } +} +``` + +### 3.8. 重复测试 + +```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() { + // ... + } + +} +``` + +### 3.9. 参数化测试 + +```java +@ParameterizedTest +@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) +void palindromes(String candidate) { + assertTrue(isPalindrome(candidate)); +} +``` + +## 4. 引用和引申 + +- [Github](https://github.com/junit-team/junit5) +- [官方用户手册](https://junit.org/junit5/docs/current/user-guide/) +- [Javadoc](https://junit.org/junit5/docs/current/api/) +- [版本声明](https://junit.org/junit5/docs/current/release-notes/) +- [官方示例](https://github.com/junit-team/junit5-samples) diff --git a/docs/test/mockito.md b/docs/test/mockito.md new file mode 100644 index 00000000..74551215 --- /dev/null +++ b/docs/test/mockito.md @@ -0,0 +1,562 @@ +# 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) diff --git a/docs/tools/README.md b/docs/tools/README.md deleted file mode 100644 index d10c34fc..00000000 --- a/docs/tools/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# 分布式 - -> Java Web 领域常用工具。 \ No newline at end of file diff --git a/docs/tools/jetty.md b/docs/tools/jetty.md deleted file mode 100644 index df9e258d..00000000 --- a/docs/tools/jetty.md +++ /dev/null @@ -1,261 +0,0 @@ ---- -title: Jetty 使用小结 -date: 2017/11/08 -categories: -- javatool -tags: -- java -- javatool -- server ---- - -# Jetty 使用小结 - - - -- [概述](#概述) -- [jetty 的嵌入式启动](#jetty-的嵌入式启动) - - [API 方式](#api-方式) - - [Maven 插件方式](#maven-插件方式) -- [参考](#参考) - - - -## 概述 - -**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.jettyjetty-webapp9.3.2.v20150730test - -org.eclipse.jettyjetty-annotations9.3.2.v20150730test - -org.eclipse.jettyapache-jsp9.3.2.v20150730test - -org.eclipse.jettyapache-jstl9.3.2.v20150730test - -``` - -官方的启动代码 - -```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.jettyjetty-maven-plugin9.3.12.v20160915 - -``` - -(2) 执行 maven 命令: - -``` -mvn jetty:run -``` - -讲真,就是这么简单。jetty 默认会为你创建一个 web 服务,地址为 127.0.0.1:8080。 - -当然,你也可以在插件中配置你的 webapp 环境 - -```xml -org.eclipse.jettyjetty-maven-plugin9.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 wiki](http://wiki.eclipse.org/Jetty/Reference/jetty-env.xml) -- [jetty 官方文档](http://www.eclipse.org/jetty/documentation/current/) diff --git a/docs/tools/nginx.md b/docs/tools/nginx.md deleted file mode 100644 index 4325d03f..00000000 --- a/docs/tools/nginx.md +++ /dev/null @@ -1,545 +0,0 @@ ---- -title: Nginx 快速入门 -date: 2016-10-10 -categories: -- web -tags: -- server -- nginx -- load balance ---- - -# Nginx 快速入门 - - - -- [概述](#概述) -- [安装与使用](#安装与使用) - - [安装](#安装) - - [从源代码编译 Nginx](#从源代码编译-nginx) - - [Windows 安装](#windows-安装) - - [使用](#使用) -- [nginx 配置实战](#nginx-配置实战) - - [http 反向代理配置](#http-反向代理配置) - - [负载均衡配置](#负载均衡配置) - - [网站有多个 webapp 的配置](#网站有多个-webapp-的配置) - - [https 反向代理配置](#https-反向代理配置) - - [静态站点配置](#静态站点配置) - - [跨域解决方案](#跨域解决方案) -- [参考](#参考) - - - -## 概述 - -**什么是 Nginx?** - -**Nginx (engine x)** 是一款轻量级的 Web 服务器 、反向代理服务器及电子邮件(IMAP/POP3)代理服务器。 - -
    - -
    - -**什么是反向代理?** - -反向代理(Reverse Proxy)方式是指以代理服务器来接受 internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。 - -
    - -
    - -## 安装与使用 - -### 安装 - -[nginx 官网下载地址](http://nginx.org/) - -发布版本分为 Linux 和 windows 版本。 - -也可以下载源码,编译后运行。 - -#### 从源代码编译 Nginx - -把源码解压缩之后,在终端里运行如下命令: - -```sh -$ ./configure -$ make -$ sudo make install -``` - -默认情况下,Nginx 会被安装在 `/usr/local/nginx`。通过设定[编译选项](http://tool.oschina.net/uploads/apidocs/nginx-zh/NginxChsInstallOptions.htm),你可以改变这个设定。 - -#### Windows 安装 - -为了安装 Nginx / Win32,需先下载它。然后解压之,然后运行即可。下面以 C 盘根目录为例说明下: - -```sh -cd C: -cd C:\nginx-0.8.54 start nginx -``` - -Nginx / Win32 是运行在一个控制台程序,而非 windows 服务方式的。服务器方式目前还是开发尝试中。 - -### 使用 - -nginx 的使用比较简单,就是几条命令。 - -常用到的命令如下: - -```sh -nginx -s stop 快速关闭Nginx,可能不保存相关信息,并迅速终止web服务。 -nginx -s quit 平稳关闭Nginx,保存相关信息,有安排的结束web服务。 -nginx -s reload 因改变了Nginx相关配置,需要重新加载配置而重载。 -nginx -s reopen 重新打开日志文件。 -nginx -c filename 为 Nginx 指定一个配置文件,来代替缺省的。 -nginx -t 不运行,而仅仅测试配置文件。nginx 将检查配置文件的语法的正确性,并尝试打开配置文件中所引用到的文件。 -nginx -v 显示 nginx 的版本。 -nginx -V 显示 nginx 的版本,编译器版本和配置参数。 -``` - -如果不想每次都敲命令,可以在 nginx 安装目录下新添一个启动批处理文件**startup.bat**,双击即可运行。内容如下: - -```sh -@echo off -rem 如果启动前已经启动nginx并记录下pid文件,会kill指定进程 -nginx.exe -s stop - -rem 测试配置文件语法正确性 -nginx.exe -t -c conf/nginx.conf - -rem 显示版本信息 -nginx.exe -v - -rem 按照指定配置去启动nginx -nginx.exe -c conf/nginx.conf -``` - -如果是运行在 Linux 下,写一个 shell 脚本,大同小异。 - -## nginx 配置实战 - -我始终认为,各种开发工具的配置还是结合实战来讲述,会让人更易理解。 - -### http 反向代理配置 - -我们先实现一个小目标:不考虑复杂的配置,仅仅是完成一个 http 反向代理。 - -nginx.conf 配置文件如下: -**_注:conf / nginx.conf 是 nginx 的默认配置文件。你也可以使用 nginx -c 指定你的配置文件_** - -``` -#运行用户 -#user somebody; - -#启动进程,通常设置成和cpu的数量相等 -worker_processes 1; - -#全局错误日志 -error_log D:/Tools/nginx-1.10.1/logs/error.log; -error_log D:/Tools/nginx-1.10.1/logs/notice.log notice; -error_log D:/Tools/nginx-1.10.1/logs/info.log info; - -#PID文件,记录当前启动的nginx的进程ID -pid D:/Tools/nginx-1.10.1/logs/nginx.pid; - -#工作模式及连接数上限 -events { - worker_connections 1024; #单个后台worker process进程的最大并发链接数 -} - -#设定http服务器,利用它的反向代理功能提供负载均衡支持 -http { - #设定mime类型(邮件支持类型),类型由mime.types文件定义 - include D:/Tools/nginx-1.10.1/conf/mime.types; - default_type application/octet-stream; - - #设定日志 - log_format main '[$remote_addr] - [$remote_user] [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; - - access_log D:/Tools/nginx-1.10.1/logs/access.log main; - rewrite_log on; - - #sendfile 指令指定 nginx 是否调用 sendfile 函数(zero copy 方式)来输出文件,对于普通应用, - #必须设为 on,如果用来进行下载等应用磁盘IO重负载应用,可设置为 off,以平衡磁盘与网络I/O处理速度,降低系统的uptime. - sendfile on; - #tcp_nopush on; - - #连接超时时间 - keepalive_timeout 120; - tcp_nodelay on; - - #gzip压缩开关 - #gzip on; - - #设定实际的服务器列表 - upstream zp_server1{ - server 127.0.0.1:8089; - } - - #HTTP服务器 - server { - #监听80端口,80端口是知名端口号,用于HTTP协议 - listen 80; - - #定义使用www.xx.com访问 - server_name www.helloworld.com; - - #首页 - index index.html - - #指向webapp的目录 - root D:\01_Workspace\Project\github\zp\SpringNotes\spring-security\spring-shiro\src\main\webapp; - - #编码格式 - charset utf-8; - - #代理配置参数 - proxy_connect_timeout 180; - proxy_send_timeout 180; - proxy_read_timeout 180; - proxy_set_header Host $host; - proxy_set_header X-Forwarder-For $remote_addr; - - #反向代理的路径(和upstream绑定),location 后面设置映射的路径 - location / { - proxy_pass http://zp_server1; - } - - #静态文件,nginx自己处理 - location ~ ^/(images|javascript|js|css|flash|media|static)/ { - root D:\01_Workspace\Project\github\zp\SpringNotes\spring-security\spring-shiro\src\main\webapp\views; - #过期30天,静态文件不怎么更新,过期可以设大一点,如果频繁更新,则可以设置得小一点。 - expires 30d; - } - - #设定查看Nginx状态的地址 - location /NginxStatus { - stub_status on; - access_log on; - auth_basic "NginxStatus"; - auth_basic_user_file conf/htpasswd; - } - - #禁止访问 .htxxx 文件 - location ~ /\.ht { - deny all; - } - - #错误处理页面(可选择性配置) - #error_page 404 /404.html; - #error_page 500 502 503 504 /50x.html; - #location = /50x.html { - # root html; - #} - } -} -``` - -好了,让我们来试试吧: - -1. 启动 webapp,注意启动绑定的端口要和 nginx 中的 `upstream` 设置的端口保持一致。 -2. 更改 host:在 C:\Windows\System32\drivers\etc 目录下的 host 文件中添加一条 DNS 记录 - -``` -127.0.0.1 www.helloworld.com -``` - -3. 启动前文中 startup.bat 的命令 -4. 在浏览器中访问 www.helloworld.com,不出意外,已经可以访问了。 - -### 负载均衡配置 - -上一个例子中,代理仅仅指向一个服务器。 - -但是,网站在实际运营过程中,多半都是有多台服务器运行着同样的 app,这时需要使用负载均衡来分流。 - -nginx 也可以实现简单的负载均衡功能。 - -假设这样一个应用场景:将应用部署在 192.168.1.11:80、192.168.1.12:80、192.168.1.13:80 三台 linux 环境的服务器上。网站域名叫 www.helloworld.com,公网 IP 为 192.168.1.11。在公网 IP 所在的服务器上部署 nginx,对所有请求做负载均衡处理。 - -nginx.conf 配置如下: - -``` -http { - #设定mime类型,类型由mime.type文件定义 - include /etc/nginx/mime.types; - default_type application/octet-stream; - #设定日志格式 - access_log /var/log/nginx/access.log; - - #设定负载均衡的服务器列表 - upstream load_balance_server { - #weigth参数表示权值,权值越高被分配到的几率越大 - server 192.168.1.11:80 weight=5; - server 192.168.1.12:80 weight=1; - server 192.168.1.13:80 weight=6; - } - - #HTTP服务器 - server { - #侦听80端口 - listen 80; - - #定义使用www.xx.com访问 - server_name www.helloworld.com; - - #对所有请求进行负载均衡请求 - location / { - root /root; #定义服务器的默认网站根目录位置 - index index.html index.htm; #定义首页索引文件的名称 - proxy_pass http://load_balance_server ;#请求转向load_balance_server 定义的服务器列表 - - #以下是一些反向代理的配置(可选择性配置) - #proxy_redirect off; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - #后端的Web服务器可以通过X-Forwarded-For获取用户真实IP - proxy_set_header X-Forwarded-For $remote_addr; - proxy_connect_timeout 90; #nginx跟后端服务器连接超时时间(代理连接超时) - proxy_send_timeout 90; #后端服务器数据回传时间(代理发送超时) - proxy_read_timeout 90; #连接成功后,后端服务器响应时间(代理接收超时) - proxy_buffer_size 4k; #设置代理服务器(nginx)保存用户头信息的缓冲区大小 - proxy_buffers 4 32k; #proxy_buffers缓冲区,网页平均在32k以下的话,这样设置 - proxy_busy_buffers_size 64k; #高负荷下缓冲大小(proxy_buffers*2) - proxy_temp_file_write_size 64k; #设定缓存文件夹大小,大于这个值,将从upstream服务器传 - - client_max_body_size 10m; #允许客户端请求的最大单文件字节数 - client_body_buffer_size 128k; #缓冲区代理缓冲用户端请求的最大字节数 - } - } -} -``` - -### 网站有多个 webapp 的配置 - -当一个网站功能越来越丰富时,往往需要将一些功能相对独立的模块剥离出来,独立维护。这样的话,通常,会有多个 webapp。 - -举个例子:假如 www.helloworld.com 站点有好几个 webapp,finance(金融)、product(产品)、admin(用户中心)。访问这些应用的方式通过上下文(context)来进行区分: - -www.helloworld.com/finance/ - -www.helloworld.com/product/ - -www.helloworld.com/admin/ - -我们知道,http 的默认端口号是 80,如果在一台服务器上同时启动这 3 个 webapp 应用,都用 80 端口,肯定是不成的。所以,这三个应用需要分别绑定不同的端口号。 - -那么,问题来了,用户在实际访问 www.helloworld.com 站点时,访问不同 webapp,总不会还带着对应的端口号去访问吧。所以,你再次需要用到反向代理来做处理。 - -配置也不难,来看看怎么做吧: - -``` -http { - #此处省略一些基本配置 - - upstream product_server{ - server www.helloworld.com:8081; - } - - upstream admin_server{ - server www.helloworld.com:8082; - } - - upstream finance_server{ - server www.helloworld.com:8083; - } - - server { - #此处省略一些基本配置 - #默认指向product的server - location / { - proxy_pass http://product_server; - } - - location /product/{ - proxy_pass http://product_server; - } - - location /admin/ { - proxy_pass http://admin_server; - } - - location /finance/ { - proxy_pass http://finance_server; - } - } -} -``` - -### https 反向代理配置 - -一些对安全性要求比较高的站点,可能会使用 HTTPS(一种使用 ssl 通信标准的安全 HTTP 协议)。 - -这里不科普 HTTP 协议和 SSL 标准。但是,使用 nginx 配置 https 需要知道几点: - -* HTTPS 的固定端口号是 443,不同于 HTTP 的 80 端口 -* SSL 标准需要引入安全证书,所以在 nginx.conf 中你需要指定证书和它对应的 key - -其他和 http 反向代理基本一样,只是在 `Server` 部分配置有些不同。 - -``` - #HTTP服务器 - server { - #监听443端口。443为知名端口号,主要用于HTTPS协议 - listen 443 ssl; - - #定义使用www.xx.com访问 - server_name www.helloworld.com; - - #ssl证书文件位置(常见证书文件格式为:crt/pem) - ssl_certificate cert.pem; - #ssl证书key位置 - ssl_certificate_key cert.key; - - #ssl配置参数(选择性配置) - ssl_session_cache shared:SSL:1m; - ssl_session_timeout 5m; - #数字签名,此处使用MD5 - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; - - location / { - root /root; - index index.html index.htm; - } - } -``` - -### 静态站点配置 - -有时候,我们需要配置静态站点(即 html 文件和一堆静态资源)。 - -举例来说:如果所有的静态资源都放在了 `/app/dist` 目录下,我们只需要在 `nginx.conf` 中指定首页以及这个站点的 host 即可。 - -配置如下: - -``` -worker_processes 1; - -events { - worker_connections 1024; -} - -http { - include mime.types; - default_type application/octet-stream; - sendfile on; - keepalive_timeout 65; - - gzip on; - gzip_types text/plain application/x-javascript text/css application/xml text/javascript application/javascript image/jpeg image/gif image/png; - gzip_vary on; - - server { - listen 80; - server_name static.zp.cn; - - location / { - root /app/dist; - index index.html; - #转发任何请求到 index.html - } - } -} -``` - -然后,添加 HOST: - -127.0.0.1 static.zp.cn - -此时,在本地浏览器访问 static.zp.cn ,就可以访问静态站点了。 - -### 跨域解决方案 - -web 领域开发中,经常采用前后端分离模式。这种模式下,前端和后端分别是独立的 web 应用程序,例如:后端是 Java 程序,前端是 React 或 Vue 应用。 - -各自独立的 web app 在互相访问时,势必存在跨域问题。解决跨域问题一般有两种思路: - -1. **CORS** - -在后端服务器设置 HTTP 响应头,把你需要运行访问的域名加入加入 `Access-Control-Allow-Origin` 中。 - -2. **jsonp** - -把后端根据请求,构造 json 数据,并返回,前端用 jsonp 跨域。 - -这两种思路,本文不展开讨论。 - -需要说明的是,nginx 根据第一种思路,也提供了一种解决跨域的解决方案。 - -举例:www.helloworld.com 网站是由一个前端 app ,一个后端 app 组成的。前端端口号为 9000, 后端端口号为 8080。 - -前端和后端如果使用 http 进行交互时,请求会被拒绝,因为存在跨域问题。来看看,nginx 是怎么解决的吧: - -首先,在 enable-cors.conf 文件中设置 cors : - -``` -# allow origin list -set $ACAO '*'; - -# set single origin -if ($http_origin ~* (www.helloworld.com)$) { - set $ACAO $http_origin; -} - -if ($cors = "trueget") { - add_header 'Access-Control-Allow-Origin' "$http_origin"; - add_header 'Access-Control-Allow-Credentials' 'true'; - add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; - add_header 'Access-Control-Allow-Headers' 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type'; -} - -if ($request_method = 'OPTIONS') { - set $cors "${cors}options"; -} - -if ($request_method = 'GET') { - set $cors "${cors}get"; -} - -if ($request_method = 'POST') { - set $cors "${cors}post"; -} -``` - -接下来,在你的服务器中 `include enable-cors.conf` 来引入跨域配置: - -``` -# ---------------------------------------------------- -# 此文件为项目 nginx 配置片段 -# 可以直接在 nginx config 中 include(推荐) -# 或者 copy 到现有 nginx 中,自行配置 -# www.helloworld.com 域名需配合 dns hosts 进行配置 -# 其中,api 开启了 cors,需配合本目录下另一份配置文件 -# ---------------------------------------------------- -upstream front_server{ - server www.helloworld.com:9000; -} -upstream api_server{ - server www.helloworld.com:8080; -} - -server { - listen 80; - server_name www.helloworld.com; - - location ~ ^/api/ { - include enable-cors.conf; - proxy_pass http://api_server; - rewrite "^/api/(.*)$" /$1 break; - } - - location ~ ^/ { - proxy_pass http://front_server; - } -} -``` - -到此,就完成了。 - -## 参考 - -[Nginx 的中文维基](http://tool.oschina.net/apidocs/apidoc?api=nginx-zh) diff --git a/docs/tools/tomcat.md b/docs/tools/tomcat.md deleted file mode 100644 index 04726962..00000000 --- a/docs/tools/tomcat.md +++ /dev/null @@ -1,661 +0,0 @@ ---- -title: Tomcat 快速入门 -date: 2018/01/08 -categories: -- javatool -tags: -- java -- javatool -- server ---- - -# Tomcat 快速入门 - -> 版本说明 -> -> 本文使用 Tomcat 版本为 Tomcat 8.5.24。 -> -> Tomcat 8.5 要求 JDK 版本为 1.7 以上。 - - - -- [简介](#简介) - - [Tomcat 是什么](#tomcat-是什么) - - [Tomcat 重要目录](#tomcat-重要目录) - - [web 工程发布目录结构](#web-工程发布目录结构) -- [QuickStart](#quickstart) - - [安装](#安装) - - [配置](#配置) - - [启动](#启动) -- [Tomcat 工作原理](#tomcat-工作原理) - - [Tomcat 主要组件](#tomcat-主要组件) - - [Tomcat 生命周期](#tomcat-生命周期) - - [Connector](#connector) - - [Comet](#comet) - - [异步 Servlet](#异步-servlet) -- [资料](#资料) - - [官方](#官方) - - [第三方](#第三方) - - - -## 简介 - -### 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 格式的配置文件来进行配置。 - -### Tomcat 重要目录 - -- **/bin** - Tomcat 脚本存放目录(如启动、关闭脚本)。 `*.sh` 文件用于 Unix 系统; `*.bat` 文件用于 Windows 系统。 -- **/conf** - Tomcat 配置文件目录。 -- **/logs** - Tomcat 默认日志目录。 -- **/webapps** - webapp 运行的目录。 - -### 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 和组成应用的其它组件,以及应用初始化参数、安全管理约束等。 - -## QuickStart - -### 安装 - -**前提条件** - -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 版本为例,包含了下载、解压、启动操作。 - -```sh -# 下载解压到本地 -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 安装成功的测试页面。 - -![tomcat.png](https://raw.githubusercontent.com/dunwu/JavaWeb/master/images/tools/tomcat/tomcat.png) - -### 配置 - -本节将列举一些重要、常见的配置项。详细的 Tomcat8 配置可以参考 [Tomcat 8 配置官方参考文档](http://tomcat.apache.org/tomcat-8.5-doc/config/index.html) 。 - -#### 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。 | | - -#### Service - -> Service 元素表示一个或多个连接器组件的组合,这些组件共享一个用于处理传入请求的引擎组件。Server 中可以有多个 Service。 - -**属性表** - -| 属性 | 描述 | 备注 | -| --------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------- | -| className | 这个类必须实现`org.apache.catalina.Service`接口。 | 默认 `org.apache.catalina.core.StandardService` | -| name | 此服务的显示名称,如果您使用标准 Catalina 组件,将包含在日志消息中。与特定服务器关联的每个服务的名称必须是唯一的。 | | - -**实例 - `conf/server.xml` 配置文件示例** - -```xml - - - - ... - - -``` - -#### 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 - - - -``` - -#### 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 的最大值 | - -#### 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 | - -#### Engine - -> Engine 元素表示与特定的 Catalina 服务相关联的整个请求处理机器。它接收并处理来自一个或多个连接器的所有请求,并将完成的响应返回给连接器,以便最终传输回客户端。 - -**属性表** - -| 属性 | 描述 | 备注 | -| ----------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | -| defaultHost | 默认主机名,用于标识将处理指向此服务器上主机名称但未在此配置文件中配置的请求的主机。 | 这个名字必须匹配其中一个嵌套的主机元素的名字属性。 | -| name | 此引擎的逻辑名称,用于日志和错误消息。 | 在同一服务器中使用多个服务元素时,每个引擎必须分配一个唯一的名称。 | - -#### 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 | - -#### Cluster - -由于在实际开发中,我从未用过 Tomcat 集群配置,所以没研究。 - -### 启动 - -#### 部署方式 - -这种方式要求本地必须安装 Tomcat 。 - -将打包好的 war 包放在 Tomcat 安装目录下的 `webapps` 目录下,然后在 bin 目录下执行 `startup.bat` 或 `startup.sh` ,Tomcat 会自动解压 `webapps` 目录下的 war 包。 - -成功后,可以访问 http://localhost:8080/xxx (xxx 是 war 包文件名)。 - -> **注意** -> -> 以上步骤是最简单的示例。步骤中的 war 包解压路径、启动端口以及一些更多的功能都可以修改配置文件来定制 (主要是 `server.xml` 或 `context.xml` 文件)。 - -#### 嵌入式 - -##### 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) - -##### 使用 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} 指定的项目名)。 - -#### 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 中设置上下文名称)。 - -![](https://raw.githubusercontent.com/dunwu/JavaWeb/master/images/tools/tomcat/tomcat-intellij-run-config.png) - -> **说明** -> -> 个人认为这个插件不如 Eclipse 的 Tomcat 插件好用,Eclipse 的 Tomcat 插件支持对 Tomcat xml 配置文件进行配置。而这里,你只能自己去 Tomcat 安装路径下修改配置文件。 - -文中的嵌入式启动示例可以参考[**我的示例项目**](https://github.com/dunwu/JavaStack/tree/master/codes/javatool/server) - -## Tomcat 工作原理 - -### Tomcat 主要组件 - -
    - -
    - -- **Server** - 指的就是整个 Tomcat 服 务器,包含多组服务,负责管理和 启动各个 Service,同时监听 8005 端口发过来的 shutdown 命令,用 于关闭整个容器。 -- **Service** - Tomcat 封装的、对外提 供完整的、基于组件的 web 服务, 包含 Connectors、Container 两个核心组件,以及多个功能组件,各 个 Service 之间是独立的,但是共享 同一 JVM 的资源。 -- **Connector** - Tomcat 与外部世界的连接器,监听固定端口接收外部请求,传递给 Container,并 将 Container 处理的结果返回给外部; -- **Container** - Catalina,Servlet 容器,内部有多层容器组成,用于管理 Servlet 生命周期,调用 servlet 相关方法。 -- **Loader** - 封装了 Java ClassLoader,用于 Container 加载类文件; Realm - Tomcat 中为 web 应用程序提供访问认证和角色管理的机制。 -- **JMX** - Java SE 中定义技术规范,是一个为应用程序、设备、系统等植入管理功能的框架,通过 JMX 可以远程监控 Tomcat 的运行状态。 -- **Jasper** - Tomcat 的 Jsp 解析引擎,用于将 Jsp 转换成 Java 文件,并编译成 class 文件。 Session - 负责管理和创建 session,以及 Session 的持久化(可自定义),支持 session 的集 -- 群。 -- **Pipeline** - 在容器中充当管道的作用,管道中可以设置各种 valve(阀门),请求和响应在经由管 道中各个阀门处理,提供了一种灵活可配置的处理请求和响应的机制。 -- **Naming** - 命名服务,JNDI, Java 命名和目录接口,是一组在 Java 应用中访问命名和目录服务的 API。命名服务将名称和对象联系起来,使得我们可以用名称访问对象,目录服务也是一种命名 服务,对象不但有名称,还有属性。Tomcat 中可以使用 JNDI 定义数据源、配置信息,用于开发 与部署的分离。 - -#### Container 组件 - -
    - -
    - -- **Engine** - Servlet 的顶层容器,包含一 个或多个 Host 子容器; -- **Host** - 虚拟主机,负责 web 应用的部 署和 Context 的创建; -- **Context** - Web 应用上下文,包含多个 Wrapper,负责 web 配置的解析、管 理所有的 Web 资源; -- **Wrapper** - 最底层的容器,是对 Servlet 的封装,负责 Servlet 实例的创 建、执行和销毁。 - -### Tomcat 生命周期 - -#### Tomcat 生命周期管理 - -Tomcat 为了方便管理组件和容器的生命周期,定义了从创建、启动、到停止、销毁共 12 中状态,tomcat 生命周期管理了内部状态变化的规则控制,组件和容器只需实现相应的生命周期 方法即可完成各生命周期内的操作(initInternal、startInternal、stopInternal、 destroyInternal); - -比如执行初始化操作时,会判断当前状态是否 New,如果不是则抛出生命周期异常;是的 话则设置当前状态为 Initializing,并执行 initInternal 方法,由子类实现,方法执行成功则设置当 前状态为 Initialized,执行失败则设置为 Failed 状态; - -
    - -
    - -Tomcat 的生命周期管理引入了事件机制,在组件或容器的生命周期状态发生变化时会通 知事件监听器,监听器通过判断事件的类型来进行相应的操作。 -事件监听器的添加可以在 server.xml 文件中进行配置; - -Tomcat 各类容器的配置过程就是通过添加 listener 的方式来进行的,从而达到配置逻辑与 容器的解耦。如 EngineConfig、HostConfig、ContextConfig。 - -- **EngineConfig** - 主要打印启动和停止日志 -- **HostConfig** - 主要处理部署应用,解析应用 META-INF/context.xml 并创建应用的 Context。 -- **ContextConfig** - 主要解析并合并 web.xml,扫描应用的各类 web 资源 (filter、servlet、listener)。 - -#### Tomcat 的启动过程 - -
    - -
    - -启动从 Tomcat 提供的 start.sh 脚本开始,shell 脚本会调用 Bootstrap 的 main 方法,实际 调用了 Catalina 相应的 load、start 方法。 - -load 方法会通过 Digester 进行 config/server.xml 的解析,在解析的过程中会根据 xml 中的关系 和配置信息来创建容器,并设置相关的属性。接着 Catalina 会调用 StandardServer 的 init 和 start 方法进行容器的初始化和启动。 - -按照 xml 的配置关系,server 的子元素是 service,service 的子元素是顶层容器 Engine,每层容器有持有自己的子容器,而这些元素都实现了生命周期管理 的各个方法,因此就很容易的完成整个容器的启动、关闭等生命周期的管理。 - -StandardServer 完成 init 和 start 方法调用后,会一直监听来自 8005 端口(可配置),如果接收 到 shutdown 命令,则会退出循环监听,执行后续的 stop 和 destroy 方法,完成 Tomcat 容器的 关闭。同时也会调用 JVM 的 Runtime.getRuntime()﴿.addShutdownHook 方法,在虚拟机意外退 出的时候来关闭容器。 - -所有容器都是继承自 ContainerBase,基类中封装了容器中的重复工作,负责启动容器相关的组 件 Loader、Logger、Manager、Cluster、Pipeline,启动子容器(线程池并发启动子容器,通过 线程池 submit 多个线程,调用后返回 Future 对象,线程内部启动子容器,接着调用 Future 对象 的 get 方法来等待执行结果)。 - -```java -List> results = new ArrayList>(); -for (int i = 0; i < children.length; i++) { - results.add(startStopExecutor.submit(new StartChild(children[i]))); -} -boolean fail = false; -for (Future result : results) { - try { - result.get(); - } catch (Exception e) { - log.error(sm.getString("containerBase.threadedStartFailed"), e); - fail = true; - } -} -``` - -##### 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 标志位来控制) - -#### 请求处理过程 - -
    - -
    - -1. 根据 server.xml 配置的指定的 connector 以及端口监听 http、或者 ajp 请求 -2. 请求到来时建立连接,解析请求参数,创建 Request 和 Response 对象,调用顶层容器 pipeline 的 invoke 方法 -3. 容器之间层层调用,最终调用业务 servlet 的 service 方法 -4. Connector 将 response 流中的数据写到 socket 中 - -#### Pipeline 与 Valve - -
    - -
    - -Pipeline 可以理解为现实中的管道,Valve 为管道中的阀门,Request 和 Response 对象在管道中 经过各个阀门的处理和控制。 - -每个容器的管道中都有一个必不可少的 basic valve,其他的都是可选的,basic valve 在管道中最 后调用,同时负责调用子容器的第一个 valve。 - -Valve 中主要的三个方法:setNext、getNext、invoke;valve 之间的关系是单向链式结构,本身 invoke 方法中会调用下一个 valve 的 invoke 方法。 - -各层容器对应的 basic valve 分别是 StandardEngineValve、StandardHostValve、 StandardContextValve、StandardWrapperValve。 - -### Connector - -
    - -
    - -#### 阻塞 IO - -
    - -
    - -#### 非阻塞 IO - -
    - -
    - -#### IO 多路复用 - -
    - -
    - -阻塞与非阻塞的区别在于进行读操作和写操作的系统调用时,如果此时内核态没有数据可读或者没有缓冲空间可写时,是否阻塞。 - -IO 多路复用的好处在于可同时监听多个 socket 的可读和可写事件,这样就能使得应用可以同时监听多个 socket,释放了应用线程资源。 - -#### 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 方法 - -### 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:在事件触发时要做好线程安全的操作 - -### 异步 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 - -## 资料 - -### 官方 - -- [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-) diff --git a/images/architecture/cdn.png b/images/architecture/cdn.png deleted file mode 100644 index 390fe8a7..00000000 Binary files a/images/architecture/cdn.png and /dev/null differ diff --git a/images/architecture/check-fail.png b/images/architecture/check-fail.png deleted file mode 100644 index 9ddc6524..00000000 Binary files a/images/architecture/check-fail.png and /dev/null differ diff --git a/images/architecture/reverse-proxy.jpg b/images/architecture/reverse-proxy.jpg deleted file mode 100644 index f0aa7e66..00000000 Binary files a/images/architecture/reverse-proxy.jpg and /dev/null differ diff --git "a/images/architecture/\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223\351\233\206\347\276\244.png" "b/images/architecture/\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223\351\233\206\347\276\244.png" deleted file mode 100644 index bdc2bc52..00000000 Binary files "a/images/architecture/\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223\351\233\206\347\276\244.png" and /dev/null differ diff --git "a/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-DNS\345\237\237\345\220\215\350\247\243\346\236\220.png" "b/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-DNS\345\237\237\345\220\215\350\247\243\346\236\220.png" deleted file mode 100644 index 8eb654cf..00000000 Binary files "a/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-DNS\345\237\237\345\220\215\350\247\243\346\236\220.png" and /dev/null differ diff --git "a/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-HTTP\351\207\215\345\256\232\345\220\221.png" "b/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-HTTP\351\207\215\345\256\232\345\220\221.png" deleted file mode 100644 index 93aa2fb1..00000000 Binary files "a/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-HTTP\351\207\215\345\256\232\345\220\221.png" and /dev/null differ diff --git "a/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-IP\345\261\202.png" "b/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-IP\345\261\202.png" deleted file mode 100644 index f1b63a89..00000000 Binary files "a/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-IP\345\261\202.png" and /dev/null differ diff --git "a/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-\345\217\215\345\220\221\344\273\243\347\220\206.png" "b/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-\345\217\215\345\220\221\344\273\243\347\220\206.png" deleted file mode 100644 index 6c969f78..00000000 Binary files "a/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-\345\217\215\345\220\221\344\273\243\347\220\206.png" and /dev/null differ diff --git "a/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.png" "b/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.png" deleted file mode 100644 index ae54c180..00000000 Binary files "a/images/architecture/\350\264\237\350\275\275\345\235\207\350\241\241-\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.png" and /dev/null differ diff --git "a/images/distributed/architecture/Dns\351\207\215\345\256\232\345\220\221.png" "b/images/distributed/architecture/Dns\351\207\215\345\256\232\345\220\221.png" deleted file mode 100644 index 300b6e06..00000000 Binary files "a/images/distributed/architecture/Dns\351\207\215\345\256\232\345\220\221.png" and /dev/null differ diff --git "a/images/distributed/architecture/Http\351\207\215\345\256\232\345\220\221.png" "b/images/distributed/architecture/Http\351\207\215\345\256\232\345\220\221.png" deleted file mode 100644 index cb78d856..00000000 Binary files "a/images/distributed/architecture/Http\351\207\215\345\256\232\345\220\221.png" and /dev/null differ diff --git a/images/distributed/architecture/MultiNode-SessionReplication.jpg b/images/distributed/architecture/MultiNode-SessionReplication.jpg deleted file mode 100644 index 0223bd80..00000000 Binary files a/images/distributed/architecture/MultiNode-SessionReplication.jpg and /dev/null differ diff --git a/images/distributed/architecture/MultiNode-SpringSession.jpg b/images/distributed/architecture/MultiNode-SpringSession.jpg deleted file mode 100644 index 38d56e2c..00000000 Binary files a/images/distributed/architecture/MultiNode-SpringSession.jpg and /dev/null differ diff --git a/images/distributed/architecture/MultiNode-StickySessions.jpg b/images/distributed/architecture/MultiNode-StickySessions.jpg deleted file mode 100644 index a7e1c6aa..00000000 Binary files a/images/distributed/architecture/MultiNode-StickySessions.jpg and /dev/null differ diff --git a/images/distributed/architecture/http-session.jpg b/images/distributed/architecture/http-session.jpg deleted file mode 100644 index d61d0534..00000000 Binary files a/images/distributed/architecture/http-session.jpg and /dev/null differ diff --git a/images/distributed/architecture/raft-candidate-01.gif b/images/distributed/architecture/raft-candidate-01.gif deleted file mode 100644 index 5c31da1d..00000000 Binary files a/images/distributed/architecture/raft-candidate-01.gif and /dev/null differ diff --git a/images/distributed/architecture/raft-candidate-02.gif b/images/distributed/architecture/raft-candidate-02.gif deleted file mode 100644 index 323d129c..00000000 Binary files a/images/distributed/architecture/raft-candidate-02.gif and /dev/null differ diff --git a/images/distributed/architecture/raft-candidate-03.gif b/images/distributed/architecture/raft-candidate-03.gif deleted file mode 100644 index a81124dd..00000000 Binary files a/images/distributed/architecture/raft-candidate-03.gif and /dev/null differ diff --git a/images/distributed/architecture/raft-candidate-04.gif b/images/distributed/architecture/raft-candidate-04.gif deleted file mode 100644 index 7a7b05a9..00000000 Binary files a/images/distributed/architecture/raft-candidate-04.gif and /dev/null differ diff --git a/images/distributed/architecture/raft-multi-candidate-01.gif b/images/distributed/architecture/raft-multi-candidate-01.gif deleted file mode 100644 index 37cdb5a5..00000000 Binary files a/images/distributed/architecture/raft-multi-candidate-01.gif and /dev/null differ diff --git a/images/distributed/architecture/raft-multi-candidate-02.gif b/images/distributed/architecture/raft-multi-candidate-02.gif deleted file mode 100644 index 216c3033..00000000 Binary files a/images/distributed/architecture/raft-multi-candidate-02.gif and /dev/null differ diff --git a/images/distributed/architecture/raft-sync-log-01.gif b/images/distributed/architecture/raft-sync-log-01.gif deleted file mode 100644 index fad88513..00000000 Binary files a/images/distributed/architecture/raft-sync-log-01.gif and /dev/null differ diff --git a/images/distributed/architecture/raft-sync-log-02.gif b/images/distributed/architecture/raft-sync-log-02.gif deleted file mode 100644 index f81aa264..00000000 Binary files a/images/distributed/architecture/raft-sync-log-02.gif and /dev/null differ diff --git a/images/distributed/architecture/raft-sync-log-03.gif b/images/distributed/architecture/raft-sync-log-03.gif deleted file mode 100644 index d52a911e..00000000 Binary files a/images/distributed/architecture/raft-sync-log-03.gif and /dev/null differ diff --git a/images/distributed/architecture/raft-sync-log-04.gif b/images/distributed/architecture/raft-sync-log-04.gif deleted file mode 100644 index 5d9c6f00..00000000 Binary files a/images/distributed/architecture/raft-sync-log-04.gif and /dev/null differ diff --git "a/images/distributed/architecture/\344\273\243\347\220\206\350\207\252\345\212\250\351\205\215\347\275\256.jpg" "b/images/distributed/architecture/\344\273\243\347\220\206\350\207\252\345\212\250\351\205\215\347\275\256.jpg" deleted file mode 100644 index 809abead..00000000 Binary files "a/images/distributed/architecture/\344\273\243\347\220\206\350\207\252\345\212\250\351\205\215\347\275\256.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\344\277\256\346\224\271IP\345\234\260\345\235\200.png" "b/images/distributed/architecture/\344\277\256\346\224\271IP\345\234\260\345\235\200.png" deleted file mode 100644 index bb564f6e..00000000 Binary files "a/images/distributed/architecture/\344\277\256\346\224\271IP\345\234\260\345\235\200.png" and /dev/null differ diff --git "a/images/distributed/architecture/\344\277\256\346\224\271Mac\345\234\260\345\235\200.png" "b/images/distributed/architecture/\344\277\256\346\224\271Mac\345\234\260\345\235\200.png" deleted file mode 100644 index 8f797bdf..00000000 Binary files "a/images/distributed/architecture/\344\277\256\346\224\271Mac\345\234\260\345\235\200.png" and /dev/null differ diff --git "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241MQ\344\272\213\345\212\241\346\266\210\346\201\257.png" "b/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241MQ\344\272\213\345\212\241\346\266\210\346\201\257.png" deleted file mode 100644 index b7dc4b87..00000000 Binary files "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241MQ\344\272\213\345\212\241\346\266\210\346\201\257.png" and /dev/null differ diff --git "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241Sagas\344\272\213\345\212\241\346\250\241\345\236\213.png" "b/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241Sagas\344\272\213\345\212\241\346\250\241\345\236\213.png" deleted file mode 100644 index f8d63a38..00000000 Binary files "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241Sagas\344\272\213\345\212\241\346\250\241\345\236\213.png" and /dev/null differ diff --git "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\344\270\244\351\230\266\346\256\265\346\217\220\344\272\244-01.jpg" "b/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\344\270\244\351\230\266\346\256\265\346\217\220\344\272\244-01.jpg" deleted file mode 100644 index 1414ae0a..00000000 Binary files "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\344\270\244\351\230\266\346\256\265\346\217\220\344\272\244-01.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\344\270\244\351\230\266\346\256\265\346\217\220\344\272\244-02.jpg" "b/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\344\270\244\351\230\266\346\256\265\346\217\220\344\272\244-02.jpg" deleted file mode 100644 index e69fff5a..00000000 Binary files "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\344\270\244\351\230\266\346\256\265\346\217\220\344\272\244-02.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\346\234\254\345\234\260\346\266\210\346\201\257.jpg" "b/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\346\234\254\345\234\260\346\266\210\346\201\257.jpg" deleted file mode 100644 index a3ead324..00000000 Binary files "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241\346\234\254\345\234\260\346\266\210\346\201\257.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\234\215\345\212\241\345\214\226\345\216\237\347\220\206.jpg" "b/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\234\215\345\212\241\345\214\226\345\216\237\347\220\206.jpg" deleted file mode 100644 index c9cafea3..00000000 Binary files "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\234\215\345\212\241\345\214\226\345\216\237\347\220\206.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\234\215\345\212\241\345\214\226\346\241\206\346\236\266.jpg" "b/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\234\215\345\212\241\345\214\226\346\241\206\346\236\266.jpg" deleted file mode 100644 index 43e1919a..00000000 Binary files "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\234\215\345\212\241\345\214\226\346\241\206\346\236\266.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\234\215\345\212\241\346\200\273\347\272\277\345\216\237\347\220\206.jpg" "b/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\234\215\345\212\241\346\200\273\347\272\277\345\216\237\347\220\206.jpg" deleted file mode 100644 index 8e568d17..00000000 Binary files "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\234\215\345\212\241\346\200\273\347\272\277\345\216\237\347\220\206.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\234\215\345\212\241\346\200\273\347\272\277\346\241\206\346\236\266.jpg" "b/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\234\215\345\212\241\346\200\273\347\272\277\346\241\206\346\236\266.jpg" deleted file mode 100644 index cde948c5..00000000 Binary files "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\234\215\345\212\241\346\200\273\347\272\277\346\241\206\346\236\266.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\266\210\346\201\257\351\230\237\345\210\227\345\216\237\347\220\206.jpg" "b/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\266\210\346\201\257\351\230\237\345\210\227\345\216\237\347\220\206.jpg" deleted file mode 100644 index c0d4ad46..00000000 Binary files "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\266\210\346\201\257\351\230\237\345\210\227\345\216\237\347\220\206.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\266\210\346\201\257\351\230\237\345\210\227\346\241\206\346\236\266.jpg" "b/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\266\210\346\201\257\351\230\237\345\210\227\346\241\206\346\236\266.jpg" deleted file mode 100644 index f7cc2dec..00000000 Binary files "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\346\236\266\346\236\204-\346\266\210\346\201\257\351\230\237\345\210\227\346\241\206\346\236\266.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272-BASE.png" "b/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272-BASE.png" deleted file mode 100644 index 6c9a572e..00000000 Binary files "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272-BASE.png" and /dev/null differ diff --git "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272-CAP.jpg" "b/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272-CAP.jpg" deleted file mode 100644 index 1a363f1e..00000000 Binary files "a/images/distributed/architecture/\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272-CAP.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\344\270\232\345\212\241\346\213\206\345\210\206.jpg" "b/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\344\270\232\345\212\241\346\213\206\345\210\206.jpg" deleted file mode 100644 index 81ef1644..00000000 Binary files "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\344\270\232\345\212\241\346\213\206\345\210\206.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\344\275\277\347\224\250NoSQL\345\222\214\346\220\234\347\264\242\345\274\225\346\223\216.jpg" "b/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\344\275\277\347\224\250NoSQL\345\222\214\346\220\234\347\264\242\345\274\225\346\223\216.jpg" deleted file mode 100644 index 529c0e3f..00000000 Binary files "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\344\275\277\347\224\250NoSQL\345\222\214\346\220\234\347\264\242\345\274\225\346\223\216.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\344\275\277\347\224\250\345\272\224\347\224\250\346\234\215\345\212\241\345\231\250\351\233\206\347\276\244.jpg" "b/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\344\275\277\347\224\250\345\272\224\347\224\250\346\234\215\345\212\241\345\231\250\351\233\206\347\276\244.jpg" deleted file mode 100644 index d9b95260..00000000 Binary files "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\344\275\277\347\224\250\345\272\224\347\224\250\346\234\215\345\212\241\345\231\250\351\233\206\347\276\244.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\344\275\277\347\224\250\347\274\223\345\255\230\346\224\271\345\226\204\346\200\247\350\203\275.jpg" "b/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\344\275\277\347\224\250\347\274\223\345\255\230\346\224\271\345\226\204\346\200\247\350\203\275.jpg" deleted file mode 100644 index 8591c281..00000000 Binary files "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\344\275\277\347\224\250\347\274\223\345\255\230\346\224\271\345\226\204\346\200\247\350\203\275.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\210\206\345\270\203\345\274\217\346\226\207\344\273\266\347\263\273\347\273\237\345\222\214\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223.jpg" "b/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\210\206\345\270\203\345\274\217\346\226\207\344\273\266\347\263\273\347\273\237\345\222\214\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223.jpg" deleted file mode 100644 index e61cd4a8..00000000 Binary files "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\210\206\345\270\203\345\274\217\346\226\207\344\273\266\347\263\273\347\273\237\345\222\214\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\210\206\345\270\203\345\274\217\346\234\215\345\212\241.jpg" "b/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\210\206\345\270\203\345\274\217\346\234\215\345\212\241.jpg" deleted file mode 100644 index 89a4518e..00000000 Binary files "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\210\206\345\270\203\345\274\217\346\234\215\345\212\241.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\210\235\345\247\213\351\230\266\346\256\265\346\236\266\346\236\204.jpg" "b/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\210\235\345\247\213\351\230\266\346\256\265\346\236\266\346\236\204.jpg" deleted file mode 100644 index 2e07a3f2..00000000 Binary files "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\210\235\345\247\213\351\230\266\346\256\265\346\236\266\346\236\204.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\217\215\345\220\221\344\273\243\347\220\206\345\222\214CDN\345\212\240\351\200\237.jpg" "b/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\217\215\345\220\221\344\273\243\347\220\206\345\222\214CDN\345\212\240\351\200\237.jpg" deleted file mode 100644 index 2b230fa9..00000000 Binary files "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\217\215\345\220\221\344\273\243\347\220\206\345\222\214CDN\345\212\240\351\200\237.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\272\224\347\224\250\346\234\215\345\212\241\345\222\214\346\225\260\346\215\256\346\234\215\345\212\241\345\210\206\347\246\273.jpg" "b/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\272\224\347\224\250\346\234\215\345\212\241\345\222\214\346\225\260\346\215\256\346\234\215\345\212\241\345\210\206\347\246\273.jpg" deleted file mode 100644 index d9396535..00000000 Binary files "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\345\272\224\347\224\250\346\234\215\345\212\241\345\222\214\346\225\260\346\215\256\346\234\215\345\212\241\345\210\206\347\246\273.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\346\225\260\346\215\256\345\272\223\350\257\273\345\206\231\345\210\206\347\246\273.jpg" "b/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\346\225\260\346\215\256\345\272\223\350\257\273\345\206\231\345\210\206\347\246\273.jpg" deleted file mode 100644 index 2d373a8c..00000000 Binary files "a/images/distributed/architecture/\347\263\273\347\273\237\346\236\266\346\236\204\350\277\233\345\214\226-\346\225\260\346\215\256\345\272\223\350\257\273\345\206\231\345\210\206\347\246\273.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213IpHash.jpg" "b/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213IpHash.jpg" deleted file mode 100644 index 27daefae..00000000 Binary files "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213IpHash.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\345\212\240\346\235\203\346\234\200\345\260\221\350\277\236\346\216\245.jpg" "b/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\345\212\240\346\235\203\346\234\200\345\260\221\350\277\236\346\216\245.jpg" deleted file mode 100644 index 32d0f3d2..00000000 Binary files "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\345\212\240\346\235\203\346\234\200\345\260\221\350\277\236\346\216\245.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\345\212\240\346\235\203\350\275\256\350\257\242.jpg" "b/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\345\212\240\346\235\203\350\275\256\350\257\242.jpg" deleted file mode 100644 index efb47ffc..00000000 Binary files "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\345\212\240\346\235\203\350\275\256\350\257\242.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\346\234\200\345\260\221\350\277\236\346\216\245-01.jpg" "b/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\346\234\200\345\260\221\350\277\236\346\216\245-01.jpg" deleted file mode 100644 index f7e9f145..00000000 Binary files "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\346\234\200\345\260\221\350\277\236\346\216\245-01.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\346\234\200\345\260\221\350\277\236\346\216\245-02.jpg" "b/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\346\234\200\345\260\221\350\277\236\346\216\245-02.jpg" deleted file mode 100644 index e4f0f3ac..00000000 Binary files "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\346\234\200\345\260\221\350\277\236\346\216\245-02.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\350\275\256\350\257\242-01.jpg" "b/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\350\275\256\350\257\242-01.jpg" deleted file mode 100644 index f9a9489b..00000000 Binary files "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\350\275\256\350\257\242-01.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\350\275\256\350\257\242-02.jpg" "b/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\350\275\256\350\257\242-02.jpg" deleted file mode 100644 index ab51d486..00000000 Binary files "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\350\275\256\350\257\242-02.jpg" and /dev/null differ diff --git "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\351\232\217\346\234\272.jpg" "b/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\351\232\217\346\234\272.jpg" deleted file mode 100644 index f3e7163b..00000000 Binary files "a/images/distributed/architecture/\350\264\237\350\275\275\345\235\207\350\241\241\347\256\227\346\263\225\344\271\213\351\232\217\346\234\272.jpg" and /dev/null differ diff --git a/images/distributed/mq/kafka/kafka-consumer-groups.png b/images/distributed/mq/kafka/kafka-consumer-groups.png deleted file mode 100644 index 16fe2936..00000000 Binary files a/images/distributed/mq/kafka/kafka-consumer-groups.png and /dev/null differ diff --git a/images/distributed/mq/kafka/kafka-core-api.png b/images/distributed/mq/kafka/kafka-core-api.png deleted file mode 100644 index db6053cc..00000000 Binary files a/images/distributed/mq/kafka/kafka-core-api.png and /dev/null differ diff --git a/images/distributed/mq/kafka/kafka-event-system.png b/images/distributed/mq/kafka/kafka-event-system.png deleted file mode 100644 index ddc65f4f..00000000 Binary files a/images/distributed/mq/kafka/kafka-event-system.png and /dev/null differ diff --git a/images/distributed/mq/kafka/kafka-ktable.png b/images/distributed/mq/kafka/kafka-ktable.png deleted file mode 100644 index 70f18d2e..00000000 Binary files a/images/distributed/mq/kafka/kafka-ktable.png and /dev/null differ diff --git a/images/distributed/mq/kafka/kafka-log-anatomy.png b/images/distributed/mq/kafka/kafka-log-anatomy.png deleted file mode 100644 index a6494999..00000000 Binary files a/images/distributed/mq/kafka/kafka-log-anatomy.png and /dev/null differ diff --git a/images/distributed/mq/kafka/kafka-log-consumer.png b/images/distributed/mq/kafka/kafka-log-consumer.png deleted file mode 100644 index fbc45f20..00000000 Binary files a/images/distributed/mq/kafka/kafka-log-consumer.png and /dev/null differ diff --git a/images/distributed/mq/kafka/kafka-metadata-flow.png b/images/distributed/mq/kafka/kafka-metadata-flow.png deleted file mode 100644 index ca1cafb8..00000000 Binary files a/images/distributed/mq/kafka/kafka-metadata-flow.png and /dev/null differ diff --git a/images/distributed/mq/kafka/kafka-producer-consumer.png b/images/distributed/mq/kafka/kafka-producer-consumer.png deleted file mode 100644 index 90d5a5e5..00000000 Binary files a/images/distributed/mq/kafka/kafka-producer-consumer.png and /dev/null differ diff --git a/images/distributed/mq/kafka/kafka-replication.png b/images/distributed/mq/kafka/kafka-replication.png deleted file mode 100644 index 699c273c..00000000 Binary files a/images/distributed/mq/kafka/kafka-replication.png and /dev/null differ diff --git "a/images/distributed/mq/kafka/kafka-segment\347\273\223\346\236\204.png" "b/images/distributed/mq/kafka/kafka-segment\347\273\223\346\236\204.png" deleted file mode 100644 index bfe66147..00000000 Binary files "a/images/distributed/mq/kafka/kafka-segment\347\273\223\346\236\204.png" and /dev/null differ diff --git a/images/distributed/mq/kafka/kafka-stateful-process.png b/images/distributed/mq/kafka/kafka-stateful-process.png deleted file mode 100644 index e61b21e9..00000000 Binary files a/images/distributed/mq/kafka/kafka-stateful-process.png and /dev/null differ diff --git a/images/distributed/mq/kafka/kafka-stream-processor.png b/images/distributed/mq/kafka/kafka-stream-processor.png deleted file mode 100644 index 4f93b853..00000000 Binary files a/images/distributed/mq/kafka/kafka-stream-processor.png and /dev/null differ diff --git a/images/distributed/mq/kafka/kafka-table-as-stream.png b/images/distributed/mq/kafka/kafka-table-as-stream.png deleted file mode 100644 index 1506f4dd..00000000 Binary files a/images/distributed/mq/kafka/kafka-table-as-stream.png and /dev/null differ diff --git "a/images/distributed/mq/kafka/kafka\347\263\273\347\273\237\347\273\223\346\236\204.png" "b/images/distributed/mq/kafka/kafka\347\263\273\347\273\237\347\273\223\346\236\204.png" deleted file mode 100644 index 8205c7eb..00000000 Binary files "a/images/distributed/mq/kafka/kafka\347\263\273\347\273\237\347\273\223\346\236\204.png" and /dev/null differ diff --git "a/images/distributed/mq/kafka/topic\345\255\230\345\202\250\347\273\223\346\236\204.png" "b/images/distributed/mq/kafka/topic\345\255\230\345\202\250\347\273\223\346\236\204.png" deleted file mode 100644 index 06f41b39..00000000 Binary files "a/images/distributed/mq/kafka/topic\345\255\230\345\202\250\347\273\223\346\236\204.png" and /dev/null differ diff --git a/images/distributed/mq/rocketmq/rmq-basic-arc.png b/images/distributed/mq/rocketmq/rmq-basic-arc.png deleted file mode 100644 index 33927bea..00000000 Binary files a/images/distributed/mq/rocketmq/rmq-basic-arc.png and /dev/null differ diff --git a/images/distributed/mq/rocketmq/rmq-basic-component.png b/images/distributed/mq/rocketmq/rmq-basic-component.png deleted file mode 100644 index 4cdc3062..00000000 Binary files a/images/distributed/mq/rocketmq/rmq-basic-component.png and /dev/null differ diff --git a/images/distributed/mq/rocketmq/rmq-model.png b/images/distributed/mq/rocketmq/rmq-model.png deleted file mode 100644 index a5eebf82..00000000 Binary files a/images/distributed/mq/rocketmq/rmq-model.png and /dev/null differ diff --git "a/images/distributed/rpc/dubbo/dubbo\345\237\272\346\234\254\346\236\266\346\236\204.png" "b/images/distributed/rpc/dubbo/dubbo\345\237\272\346\234\254\346\236\266\346\236\204.png" deleted file mode 100644 index cf76625f..00000000 Binary files "a/images/distributed/rpc/dubbo/dubbo\345\237\272\346\234\254\346\236\266\346\236\204.png" and /dev/null differ diff --git "a/images/distributed/rpc/dubbo/dubbo\346\225\264\344\275\223\350\256\276\350\256\241.jpg" "b/images/distributed/rpc/dubbo/dubbo\346\225\264\344\275\223\350\256\276\350\256\241.jpg" deleted file mode 100644 index 005408a5..00000000 Binary files "a/images/distributed/rpc/dubbo/dubbo\346\225\264\344\275\223\350\256\276\350\256\241.jpg" and /dev/null differ diff --git "a/images/distributed/rpc/dubbo/dubbo\346\250\241\345\235\227\345\210\206\345\214\205.jpg" "b/images/distributed/rpc/dubbo/dubbo\346\250\241\345\235\227\345\210\206\345\214\205.jpg" deleted file mode 100644 index 946cb094..00000000 Binary files "a/images/distributed/rpc/dubbo/dubbo\346\250\241\345\235\227\345\210\206\345\214\205.jpg" and /dev/null differ diff --git "a/images/distributed/rpc/dubbo/dubbo\350\256\277\351\227\256\346\216\247\345\210\266-\347\233\264\350\277\236.jpg" "b/images/distributed/rpc/dubbo/dubbo\350\256\277\351\227\256\346\216\247\345\210\266-\347\233\264\350\277\236.jpg" deleted file mode 100644 index 1632a88c..00000000 Binary files "a/images/distributed/rpc/dubbo/dubbo\350\256\277\351\227\256\346\216\247\345\210\266-\347\233\264\350\277\236.jpg" and /dev/null differ diff --git "a/images/distributed/rpc/dubbo/dubbo\351\205\215\347\275\256\345\205\263\347\263\273.jpg" "b/images/distributed/rpc/dubbo/dubbo\351\205\215\347\275\256\345\205\263\347\263\273.jpg" deleted file mode 100644 index f03f2e38..00000000 Binary files "a/images/distributed/rpc/dubbo/dubbo\351\205\215\347\275\256\345\205\263\347\263\273.jpg" and /dev/null differ diff --git "a/images/distributed/rpc/dubbo/dubbo\351\205\215\347\275\256\350\246\206\347\233\226\345\205\263\347\263\273.jpg" "b/images/distributed/rpc/dubbo/dubbo\351\205\215\347\275\256\350\246\206\347\233\226\345\205\263\347\263\273.jpg" deleted file mode 100644 index 742f42ab..00000000 Binary files "a/images/distributed/rpc/dubbo/dubbo\351\205\215\347\275\256\350\246\206\347\233\226\345\205\263\347\263\273.jpg" and /dev/null differ diff --git "a/images/distributed/rpc/dubbo/dubbo\351\205\215\347\275\256\350\246\206\347\233\226\347\255\226\347\225\245.jpg" "b/images/distributed/rpc/dubbo/dubbo\351\205\215\347\275\256\350\246\206\347\233\226\347\255\226\347\225\245.jpg" deleted file mode 100644 index 5d97c179..00000000 Binary files "a/images/distributed/rpc/dubbo/dubbo\351\205\215\347\275\256\350\246\206\347\233\226\347\255\226\347\225\245.jpg" and /dev/null differ diff --git "a/images/distributed/rpc/dubbo/dubbo\351\233\206\347\276\244\345\256\271\351\224\231.jpg" "b/images/distributed/rpc/dubbo/dubbo\351\233\206\347\276\244\345\256\271\351\224\231.jpg" deleted file mode 100644 index 79a68e16..00000000 Binary files "a/images/distributed/rpc/dubbo/dubbo\351\233\206\347\276\244\345\256\271\351\224\231.jpg" and /dev/null differ diff --git "a/images/distributed/rpc/zookeeper/ZooKeeper\351\200\211\344\270\276\346\265\201\347\250\213\345\233\276.jpg" "b/images/distributed/rpc/zookeeper/ZooKeeper\351\200\211\344\270\276\346\265\201\347\250\213\345\233\276.jpg" deleted file mode 100644 index aea5fa7a..00000000 Binary files "a/images/distributed/rpc/zookeeper/ZooKeeper\351\200\211\344\270\276\346\265\201\347\250\213\345\233\276.jpg" and /dev/null differ diff --git "a/images/distributed/rpc/zookeeper/Zookeeper\346\225\260\346\215\256\347\273\223\346\236\204.gif" "b/images/distributed/rpc/zookeeper/Zookeeper\346\225\260\346\215\256\347\273\223\346\236\204.gif" deleted file mode 100644 index c6862dda..00000000 Binary files "a/images/distributed/rpc/zookeeper/Zookeeper\346\225\260\346\215\256\347\273\223\346\236\204.gif" and /dev/null differ diff --git "a/images/distributed/rpc/zookeeper/Zookeeper\351\205\215\347\275\256\347\256\241\347\220\206.gif" "b/images/distributed/rpc/zookeeper/Zookeeper\351\205\215\347\275\256\347\256\241\347\220\206.gif" deleted file mode 100644 index fb7e0099..00000000 Binary files "a/images/distributed/rpc/zookeeper/Zookeeper\351\205\215\347\275\256\347\256\241\347\220\206.gif" and /dev/null differ diff --git "a/images/distributed/rpc/zookeeper/Zookeeper\351\233\206\347\276\244\347\256\241\347\220\206\347\273\223\346\236\204.gif" "b/images/distributed/rpc/zookeeper/Zookeeper\351\233\206\347\276\244\347\256\241\347\220\206\347\273\223\346\236\204.gif" deleted file mode 100644 index c210eb86..00000000 Binary files "a/images/distributed/rpc/zookeeper/Zookeeper\351\233\206\347\276\244\347\256\241\347\220\206\347\273\223\346\236\204.gif" and /dev/null differ diff --git a/images/distributed/rpc/zookeeper/zookeeper-service.png b/images/distributed/rpc/zookeeper/zookeeper-service.png deleted file mode 100644 index 9cd3fdf9..00000000 Binary files a/images/distributed/rpc/zookeeper/zookeeper-service.png and /dev/null differ diff --git "a/images/standalone/orm/mybatis/SqlSession\344\270\200\347\272\247\347\274\223\345\255\230\347\232\204\345\267\245\344\275\234\346\265\201\347\250\213.png" "b/images/standalone/orm/mybatis/SqlSession\344\270\200\347\272\247\347\274\223\345\255\230\347\232\204\345\267\245\344\275\234\346\265\201\347\250\213.png" deleted file mode 100644 index 2b288345..00000000 Binary files "a/images/standalone/orm/mybatis/SqlSession\344\270\200\347\272\247\347\274\223\345\255\230\347\232\204\345\267\245\344\275\234\346\265\201\347\250\213.png" and /dev/null differ diff --git "a/images/standalone/orm/mybatis/SqlSession\345\222\214Executor\344\273\245\345\217\212Cache\347\232\204\345\205\263\347\263\273\347\261\273\345\233\276.png" "b/images/standalone/orm/mybatis/SqlSession\345\222\214Executor\344\273\245\345\217\212Cache\347\232\204\345\205\263\347\263\273\347\261\273\345\233\276.png" deleted file mode 100644 index 70348107..00000000 Binary files "a/images/standalone/orm/mybatis/SqlSession\345\222\214Executor\344\273\245\345\217\212Cache\347\232\204\345\205\263\347\263\273\347\261\273\345\233\276.png" and /dev/null differ diff --git "a/images/standalone/orm/mybatis/mybaits\346\265\201\347\250\213\345\233\2762.png" "b/images/standalone/orm/mybatis/mybaits\346\265\201\347\250\213\345\233\2762.png" deleted file mode 100644 index c781717d..00000000 Binary files "a/images/standalone/orm/mybatis/mybaits\346\265\201\347\250\213\345\233\2762.png" and /dev/null differ diff --git "a/images/standalone/orm/mybatis/mybatis\344\270\244\347\247\215\345\267\245\344\275\234\346\226\271\345\274\217\344\271\213\344\270\200\344\274\240\347\273\237\346\250\241\345\274\217.png" "b/images/standalone/orm/mybatis/mybatis\344\270\244\347\247\215\345\267\245\344\275\234\346\226\271\345\274\217\344\271\213\344\270\200\344\274\240\347\273\237\346\250\241\345\274\217.png" deleted file mode 100644 index fe59aeba..00000000 Binary files "a/images/standalone/orm/mybatis/mybatis\344\270\244\347\247\215\345\267\245\344\275\234\346\226\271\345\274\217\344\271\213\344\270\200\344\274\240\347\273\237\346\250\241\345\274\217.png" and /dev/null differ diff --git "a/images/standalone/orm/mybatis/mybatis\344\270\244\347\247\215\345\267\245\344\275\234\346\226\271\345\274\217\344\271\213\344\270\200\346\216\245\345\217\243\346\250\241\345\274\217.png" "b/images/standalone/orm/mybatis/mybatis\344\270\244\347\247\215\345\267\245\344\275\234\346\226\271\345\274\217\344\271\213\344\270\200\346\216\245\345\217\243\346\250\241\345\274\217.png" deleted file mode 100644 index d145242b..00000000 Binary files "a/images/standalone/orm/mybatis/mybatis\344\270\244\347\247\215\345\267\245\344\275\234\346\226\271\345\274\217\344\271\213\344\270\200\346\216\245\345\217\243\346\250\241\345\274\217.png" and /dev/null differ diff --git "a/images/standalone/orm/mybatis/mybatis\346\236\266\346\236\204\345\233\276.png" "b/images/standalone/orm/mybatis/mybatis\346\236\266\346\236\204\345\233\276.png" deleted file mode 100644 index da86f13c..00000000 Binary files "a/images/standalone/orm/mybatis/mybatis\346\236\266\346\236\204\345\233\276.png" and /dev/null differ diff --git "a/images/standalone/orm/mybatis/mybatis\346\236\266\346\236\204\345\233\2762.png" "b/images/standalone/orm/mybatis/mybatis\346\236\266\346\236\204\345\233\2762.png" deleted file mode 100644 index 509c09ee..00000000 Binary files "a/images/standalone/orm/mybatis/mybatis\346\236\266\346\236\204\345\233\2762.png" and /dev/null differ diff --git "a/images/standalone/orm/mybatis/mybatis\346\265\201\347\250\213\345\233\276.jpg" "b/images/standalone/orm/mybatis/mybatis\346\265\201\347\250\213\345\233\276.jpg" deleted file mode 100644 index b70e51b1..00000000 Binary files "a/images/standalone/orm/mybatis/mybatis\346\265\201\347\250\213\345\233\276.jpg" and /dev/null differ diff --git "a/images/standalone/orm/mybatis/mybatis\347\274\223\345\255\230\346\236\266\346\236\204\347\244\272\346\204\217\345\233\276.png" "b/images/standalone/orm/mybatis/mybatis\347\274\223\345\255\230\346\236\266\346\236\204\347\244\272\346\204\217\345\233\276.png" deleted file mode 100644 index 18a2adaf..00000000 Binary files "a/images/standalone/orm/mybatis/mybatis\347\274\223\345\255\230\346\236\266\346\236\204\347\244\272\346\204\217\345\233\276.png" and /dev/null differ diff --git "a/images/standalone/orm/mybatis/\344\270\200\347\272\247\347\274\223\345\255\230\347\224\237\345\221\275\345\221\250\346\234\237.png" "b/images/standalone/orm/mybatis/\344\270\200\347\272\247\347\274\223\345\255\230\347\224\237\345\221\275\345\221\250\346\234\237.png" deleted file mode 100644 index 134a8c6f..00000000 Binary files "a/images/standalone/orm/mybatis/\344\270\200\347\272\247\347\274\223\345\255\230\347\224\237\345\221\275\345\221\250\346\234\237.png" and /dev/null differ diff --git "a/images/standalone/orm/mybatis/\344\275\277\347\224\250\344\270\216\346\234\252\344\275\277\347\224\250\344\272\214\347\272\247\347\274\223\345\255\230\347\232\204\345\214\272\345\210\253.png" "b/images/standalone/orm/mybatis/\344\275\277\347\224\250\344\270\216\346\234\252\344\275\277\347\224\250\344\272\214\347\272\247\347\274\223\345\255\230\347\232\204\345\214\272\345\210\253.png" deleted file mode 100644 index d3dfd35b..00000000 Binary files "a/images/standalone/orm/mybatis/\344\275\277\347\224\250\344\270\216\346\234\252\344\275\277\347\224\250\344\272\214\347\272\247\347\274\223\345\255\230\347\232\204\345\214\272\345\210\253.png" and /dev/null differ diff --git "a/images/standalone/orm/mybatis/\347\274\223\345\255\230decorator\346\250\241\345\274\217\347\232\204\350\277\220\347\224\250.png" "b/images/standalone/orm/mybatis/\347\274\223\345\255\230decorator\346\250\241\345\274\217\347\232\204\350\277\220\347\224\250.png" deleted file mode 100644 index 675db268..00000000 Binary files "a/images/standalone/orm/mybatis/\347\274\223\345\255\230decorator\346\250\241\345\274\217\347\232\204\350\277\220\347\224\250.png" and /dev/null differ diff --git a/images/standalone/security/shiro/ShiroArchitecture.png b/images/standalone/security/shiro/ShiroArchitecture.png deleted file mode 100644 index e921e16a..00000000 Binary files a/images/standalone/security/shiro/ShiroArchitecture.png and /dev/null differ diff --git a/images/standalone/security/shiro/ShiroBasicArchitecture.png b/images/standalone/security/shiro/ShiroBasicArchitecture.png deleted file mode 100644 index ad73a56b..00000000 Binary files a/images/standalone/security/shiro/ShiroBasicArchitecture.png and /dev/null differ diff --git a/images/standalone/security/shiro/shiro-features.png b/images/standalone/security/shiro/shiro-features.png deleted file mode 100644 index f02c7b08..00000000 Binary files a/images/standalone/security/shiro/shiro-features.png and /dev/null differ diff --git "a/images/standalone/security/shiro/shiro\346\216\210\346\235\203\346\265\201\347\250\213.png" "b/images/standalone/security/shiro/shiro\346\216\210\346\235\203\346\265\201\347\250\213.png" deleted file mode 100644 index d77de22b..00000000 Binary files "a/images/standalone/security/shiro/shiro\346\216\210\346\235\203\346\265\201\347\250\213.png" and /dev/null differ diff --git "a/images/standalone/security/shiro/shiro\350\256\244\350\257\201\346\265\201\347\250\213.png" "b/images/standalone/security/shiro/shiro\350\256\244\350\257\201\346\265\201\347\250\213.png" deleted file mode 100644 index d4cd82d3..00000000 Binary files "a/images/standalone/security/shiro/shiro\350\256\244\350\257\201\346\265\201\347\250\213.png" and /dev/null differ diff --git a/images/tools/nginx/nginx.jpg b/images/tools/nginx/nginx.jpg deleted file mode 100644 index 1e0267ee..00000000 Binary files a/images/tools/nginx/nginx.jpg and /dev/null differ diff --git "a/images/tools/tomcat/IO\345\244\232\350\267\257\345\244\215\347\224\250.png" "b/images/tools/tomcat/IO\345\244\232\350\267\257\345\244\215\347\224\250.png" deleted file mode 100644 index 9160e94c..00000000 Binary files "a/images/tools/tomcat/IO\345\244\232\350\267\257\345\244\215\347\224\250.png" and /dev/null differ diff --git "a/images/tools/tomcat/NIO\345\244\204\347\220\206\347\233\270\345\205\263\347\261\273.jpg" "b/images/tools/tomcat/NIO\345\244\204\347\220\206\347\233\270\345\205\263\347\261\273.jpg" deleted file mode 100644 index 381e66fe..00000000 Binary files "a/images/tools/tomcat/NIO\345\244\204\347\220\206\347\233\270\345\205\263\347\261\273.jpg" and /dev/null differ diff --git "a/images/tools/tomcat/Pipeline\344\270\216Valve.png" "b/images/tools/tomcat/Pipeline\344\270\216Valve.png" deleted file mode 100644 index 4eb1d67d..00000000 Binary files "a/images/tools/tomcat/Pipeline\344\270\216Valve.png" and /dev/null differ diff --git "a/images/tools/tomcat/Servlet\347\224\237\345\221\275\345\221\250\346\234\237.jpg" "b/images/tools/tomcat/Servlet\347\224\237\345\221\275\345\221\250\346\234\237.jpg" deleted file mode 100644 index 532c5848..00000000 Binary files "a/images/tools/tomcat/Servlet\347\224\237\345\221\275\345\221\250\346\234\237.jpg" and /dev/null differ diff --git "a/images/tools/tomcat/Tomcat-Container\347\273\204\344\273\266.jpg" "b/images/tools/tomcat/Tomcat-Container\347\273\204\344\273\266.jpg" deleted file mode 100644 index 57ba5786..00000000 Binary files "a/images/tools/tomcat/Tomcat-Container\347\273\204\344\273\266.jpg" and /dev/null differ diff --git "a/images/tools/tomcat/Tomcat\344\270\273\350\246\201\347\273\204\344\273\266.jpg" "b/images/tools/tomcat/Tomcat\344\270\273\350\246\201\347\273\204\344\273\266.jpg" deleted file mode 100644 index d225b9a3..00000000 Binary files "a/images/tools/tomcat/Tomcat\344\270\273\350\246\201\347\273\204\344\273\266.jpg" and /dev/null differ diff --git "a/images/tools/tomcat/Tomcat\345\220\204\347\261\273Connector\345\257\271\346\257\224.jpg" "b/images/tools/tomcat/Tomcat\345\220\204\347\261\273Connector\345\257\271\346\257\224.jpg" deleted file mode 100644 index a9283fe3..00000000 Binary files "a/images/tools/tomcat/Tomcat\345\220\204\347\261\273Connector\345\257\271\346\257\224.jpg" and /dev/null differ diff --git "a/images/tools/tomcat/Tomcat\345\220\257\345\212\250\350\277\207\347\250\213.jpg" "b/images/tools/tomcat/Tomcat\345\220\257\345\212\250\350\277\207\347\250\213.jpg" deleted file mode 100644 index 606910a3..00000000 Binary files "a/images/tools/tomcat/Tomcat\345\220\257\345\212\250\350\277\207\347\250\213.jpg" and /dev/null differ diff --git "a/images/tools/tomcat/Tomcat\347\224\237\345\221\275\345\221\250\346\234\237.jpg" "b/images/tools/tomcat/Tomcat\347\224\237\345\221\275\345\221\250\346\234\237.jpg" deleted file mode 100644 index cf40b380..00000000 Binary files "a/images/tools/tomcat/Tomcat\347\224\237\345\221\275\345\221\250\346\234\237.jpg" and /dev/null differ diff --git a/images/tools/tomcat/connector.png b/images/tools/tomcat/connector.png deleted file mode 100644 index 65cb84f9..00000000 Binary files a/images/tools/tomcat/connector.png and /dev/null differ diff --git "a/images/tools/tomcat/jsp\345\274\225\346\223\216.png" "b/images/tools/tomcat/jsp\345\274\225\346\223\216.png" deleted file mode 100644 index 0ea1febc..00000000 Binary files "a/images/tools/tomcat/jsp\345\274\225\346\223\216.png" and /dev/null differ diff --git "a/images/tools/tomcat/jsp\350\247\243\346\236\220\350\277\207\347\250\213.png" "b/images/tools/tomcat/jsp\350\247\243\346\236\220\350\277\207\347\250\213.png" deleted file mode 100644 index c46d7dd6..00000000 Binary files "a/images/tools/tomcat/jsp\350\247\243\346\236\220\350\277\207\347\250\213.png" and /dev/null differ diff --git a/images/tools/tomcat/tomcat-intellij-run-config.png b/images/tools/tomcat/tomcat-intellij-run-config.png deleted file mode 100644 index 9861699d..00000000 Binary files a/images/tools/tomcat/tomcat-intellij-run-config.png and /dev/null differ diff --git a/images/tools/tomcat/tomcat.png b/images/tools/tomcat/tomcat.png deleted file mode 100644 index a07cb9f1..00000000 Binary files a/images/tools/tomcat/tomcat.png and /dev/null differ diff --git "a/images/tools/tomcat/\344\274\240\347\273\237Servlet\345\244\204\347\220\206\346\265\201\347\250\213.png" "b/images/tools/tomcat/\344\274\240\347\273\237Servlet\345\244\204\347\220\206\346\265\201\347\250\213.png" deleted file mode 100644 index 4e6e9041..00000000 Binary files "a/images/tools/tomcat/\344\274\240\347\273\237Servlet\345\244\204\347\220\206\346\265\201\347\250\213.png" and /dev/null differ diff --git "a/images/tools/tomcat/\345\274\202\346\255\245Servlet\345\244\204\347\220\206\346\265\201\347\250\213.png" "b/images/tools/tomcat/\345\274\202\346\255\245Servlet\345\244\204\347\220\206\346\265\201\347\250\213.png" deleted file mode 100644 index 601bb935..00000000 Binary files "a/images/tools/tomcat/\345\274\202\346\255\245Servlet\345\244\204\347\220\206\346\265\201\347\250\213.png" and /dev/null differ diff --git "a/images/tools/tomcat/\350\257\267\346\261\202\345\244\204\347\220\206\350\277\207\347\250\213.png" "b/images/tools/tomcat/\350\257\267\346\261\202\345\244\204\347\220\206\350\277\207\347\250\213.png" deleted file mode 100644 index 0a32222d..00000000 Binary files "a/images/tools/tomcat/\350\257\267\346\261\202\345\244\204\347\220\206\350\277\207\347\250\213.png" and /dev/null differ diff --git "a/images/tools/tomcat/\351\230\273\345\241\236IO.png" "b/images/tools/tomcat/\351\230\273\345\241\236IO.png" deleted file mode 100644 index 786b7185..00000000 Binary files "a/images/tools/tomcat/\351\230\273\345\241\236IO.png" and /dev/null differ diff --git "a/images/tools/tomcat/\351\235\236\351\230\273\345\241\236IO.png" "b/images/tools/tomcat/\351\235\236\351\230\273\345\241\236IO.png" deleted file mode 100644 index 3795b5fc..00000000 Binary files "a/images/tools/tomcat/\351\235\236\351\230\273\345\241\236IO.png" and /dev/null differ diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..9ad154bb --- /dev/null +++ b/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + io.github.dunwu.javatech + javatech + 1.0.0 + pom + JAVATECH + Java 技术示例源码 + + + codes/cache + codes/javatech-io + codes/javatech-log + codes/javatech-mq + codes/javatech-server + codes/javatech-test + codes/javatech-file + codes/javatech-others + + diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 00000000..84310806 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env sh + +# ------------------------------------------------------------------------------ +# gh-pages 部署脚本 +# @author Zhang Peng +# @since 2020/2/10 +# ------------------------------------------------------------------------------ + +# 装载其它库 +ROOT_DIR=$(cd `dirname $0`/..; pwd) + +# 确保脚本抛出遇到的错误 +set -e + +cd "${ROOT_DIR}/docs" + +# 生成静态文件 +npm install +npm run build + +# 进入生成的文件夹 +cd dist + +# 如果是发布到自定义域名 +# 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/javatech.git" gh-pages + git push --force --quiet "https://turnon:${GITEE_TOKEN}@gitee.com/turnon/javatech.git" gh-pages +else + echo "使用 ssh 公钥部署 gh-pages" + git push -f git@github.com:dunwu/javatech.git gh-pages + git push -f git@gitee.com:turnon/javatech.git gh-pages +fi + +cd "${ROOT_DIR}"