diff --git a/.babelrc b/.babelrc new file mode 100644 index 00000000..55754d07 --- /dev/null +++ b/.babelrc @@ -0,0 +1,3 @@ +{ + "compact": false +} diff --git a/.editorconfig b/.editorconfig index d9c3abd5..d72a75ea 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,29 +1,25 @@ -# EditorConfig helps developers define and maintain consistent -# coding styles between different editors and IDEs -# http://editorconfig.org -# 所有文件换行使用 Unix like 风格(LF),bat 文件使用 win 风格(CRLF) -# 缩进 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_size = 2 indent_style = space max_line_length = 120 - -# We recommend you to keep these unchanged charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.bat] +[*.{bat, cmd}] end_of_line = crlf -[*.java] +[*.{java, gradle, groovy, kt, sh}] indent_size = 4 [*.md] diff --git a/.gitattributes b/.gitattributes index e2a6e5e0..eaae227f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -22,6 +22,7 @@ *.less text *.sql text *.properties text +*.md text # unix style *.sh text eol=lf @@ -50,26 +51,28 @@ *.bin binary *.exe binary -# 图片 +# images *.png binary *.jpg binary *.ico binary *.gif binary -# 音视频 +# medias *.mp3 binary *.swf binary -# 字体 +# fonts *.eot binary *.svg binary *.ttf binary *.woff binary -# other doc +# others *.pdf binary *.doc binary *.docx binary +*.ppt binary +*.pptx binary *.xls binary *.xlsx binary *.xmind binary diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000..04010943 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,36 @@ +name: CI + +# 在master分支发生push事件时触发。 +on: + push: + branches: + - master + +env: # 设置环境变量 + TZ: Asia/Shanghai # 时区(设置时区可使页面中的`最近更新时间`使用时区时间) + +jobs: + build: # 自定义名称 + runs-on: ubuntu-latest # 运行在虚拟机环境ubuntu-latest + + strategy: + matrix: + node-version: [16.x] + + steps: + # 使用的动作。格式:userName/repoName。作用:检出仓库,获取源码。 官方actions库:https://github.com/actions + - name: Checkout + uses: actions/checkout@master + + # 指定 nodejs 版本 + - name: Use Nodejs ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + # 部署 + - name: Deploy + env: # 设置环境变量 + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} + GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} + run: npm install && npm run deploy diff --git a/.gitignore b/.gitignore index cb2d5824..7d98dac9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,47 @@ -################ JAVA ################ -# temp folders +# --------------------------------------------------------------------- +# more gitignore templates see https://github.com/github/gitignore +# --------------------------------------------------------------------- + +# ------------------------------- java ------------------------------- +# compiled folders classes target logs -work +.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 -################ JAVASCRIPT ################ +# ------------------------------- javascript ------------------------------- # dependencies node_modules # temp folders +build dist _book _jsdoc +.temp +.deploy*/ # temp files *.log @@ -26,13 +49,18 @@ npm-debug.log* yarn-debug.log* yarn-error.log* bundle*.js +.DS_Store +Thumbs.db +db.json +book.pdf +package-lock.json -################ IDEA ################ +# ------------------------------- intellij ------------------------------- .idea *.iml -################ Eclipse ################ +# ------------------------------- eclipse ------------------------------- .classpath .project diff --git a/LICENSE b/LICENSE index 261eeb9e..3b7b82d0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,427 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +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 0363c30b..7d3dbdd3 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,253 @@ -# java-stack +

+ + logo + +

-## 简介 +

-**建项原因** + + star + -系统化的学习知识,有利于将知识碎片拼凑起来,融会贯通。这对于掌握任何一门技术,都是非常必要的。所以,作为一名 Java 工程师,应该系统化管理自己的 Java 技术栈知识。 + + fork + -**范围** + + build + -学海无涯,学的越多,越感到个人精力有限,难以对所有技术都面面俱到。所以,根据自己的工作领域,有侧重点的去掌握知识才是明智的。 + + code style + -由于本人人从事 Java Web 领域的开发,所以本项目主要是整理 Java Web 及相关领域的知识点。 +

-## 目录 +

JavaTutorial

-### [JavaSE](https://github.com/dunwu/javase-notes) +> ☕ **java-tutorial** 是一个 Java 教程,汇集一个老司机在 Java 领域的十年积累。 +> +> - 🔁 项目同步维护:[Github](https://github.com/dunwu/java-tutorial/) | [Gitee](https://gitee.com/turnon/java-tutorial/) +> - 📖 电子书阅读:[Github Pages](https://dunwu.github.io/java-tutorial/) | [Gitee Pages](https://turnon.gitee.io/java-tutorial/) +> +> 说明: +> +> - 下面的内容清单中,凡是有 📚 标记的技术,都已整理成详细的教程。 +> - 部分技术因为可以应用于不同领域,所以可能会同时出现在不同的类别下。 -### [JavaEE](https://github.com/dunwu/javaee-notes) +## 📖 内容 -### [Java 框架](docs/framework/README.md) +### JavaSE -* [Spring](https://github.com/dunwu/spring-notes) -* [Dubbo](docs/framework/dubbo/README.md) +> 📚 [javacore](https://dunwu.github.io/javacore/) 是一个 Java 核心技术教程。内容包含:Java 基础特性、Java 高级特性、Java 并发、JVM、Java IO 等。 -### [Java 库](docs/javalib/README.md) +### JavaEE -* [ActiveMQ 使用小结](docs/javalib/activemq.md) -* [Dozer 使用小结](docs/javalib/dozer.md) -* [细说 Java 主流日志工具库](docs/javalib/java-log.md) -* [JavaMail 使用小结](docs/javalib/javamail.md) -* [jsoup 使用小结](docs/javalib/jsoup.md) -* [JUnit 使用小结](docs/javalib/junit.md) -* [Lombok 使用小结](docs/javalib/lombok.md) -* [Thumbnailator 使用小结](docs/javalib/thumbnailator.md) -* [ZXing 使用小结](docs/javalib/zxing.md) +#### JavaWeb -### [Java 工具](docs/javatool/README.md) +- [JavaWeb 面经](docs/02.JavaEE/01.JavaWeb/99.JavaWeb面经.md) +- [JavaWeb 之 Servlet 指南](docs/02.JavaEE/01.JavaWeb/01.JavaWeb之Servlet指南.md) +- [JavaWeb 之 Jsp 指南](docs/02.JavaEE/01.JavaWeb/02.JavaWeb之Jsp指南.md) +- [JavaWeb 之 Filter 和 Listener](docs/02.JavaEE/01.JavaWeb/03.JavaWeb之Filter和Listener.md) +- [JavaWeb 之 Cookie 和 Session](docs/02.JavaEE/01.JavaWeb/04.JavaWeb之Cookie和Session.md) -* [构建工具](docs/javatool/build/README.md) - * [Maven 快速指南(一)](docs/javatool/build/maven/maven-quickstart-01.md) - * [Maven 快速指南(二)](docs/javatool/build/maven/maven-quickstart-02.md) - * [Maven 之 pom.xml 详解(一)](docs/javatool/build/maven/maven-pom-01.md) - * [Maven 之 pom.xml 详解(二)](docs/javatool/build/maven/maven-pom-02.md) - * [Maven 之 pom.xml 详解(三)](docs/javatool/build/maven/maven-pom-03.md) - * [Maven 之 settings.xml 详解](docs/javatool/build/maven/maven-settings-config.md) - * [发布项目到中央仓库](docs/javatool/build/maven/maven-deploy.md) - * [Maven 排错](docs/javatool/build/maven/maven-faq.md) - * [Ant 简易教程](docs/javatool/build/ant.md) -* [Elastic](docs/javatool/elastic/README.md) - * [Elastic 技术栈之快速指南](docs/javatool/elastic/elastic-quickstart.md) - * [Elastic 技术栈之 Logstash 基础](docs/javatool/elastic/elastic-logstash.md) -* [Java IDE](docs/javatool/ide/README.md) - * [Intellij IDEA 使用小结](docs/javatool/ide/intellij.md) - * [Eclipse 使用小结](docs/javatool/ide/eclipse.md) -* [Java 服务器](docs/javatool/server/README.md) - * [Jetty 使用小结](docs/javatool/server/jetty.md) +#### Java 服务器 -### [技术扩展](docs/extend/README.md) +> Tomcat 和 Jetty 都是 Java 比较流行的轻量级服务器。 +> +> Nginx 是目前最流行的反向代理服务器,也常用于负载均衡。 -* [数据库](docs/extend/database.md) -* [数据结构和算法](docs/extend/algorithm.md) -* [Linux](docs/extend/os.md) -* [网络](docs/extend/web/README.md) - * [nginx 快速入门](docs/extend/web/nginx-quickstart.md) - -### [附录](docs/appendix/README.md) +- [Tomcat 快速入门](docs/02.JavaEE/02.服务器/01.Tomcat/01.Tomcat快速入门.md) +- [Tomcat 连接器](docs/02.JavaEE/02.服务器/01.Tomcat/02.Tomcat连接器.md) +- [Tomcat 容器](docs/02.JavaEE/02.服务器/01.Tomcat/03.Tomcat容器.md) +- [Tomcat 优化](docs/02.JavaEE/02.服务器/01.Tomcat/04.Tomcat优化.md) +- [Tomcat 和 Jetty](docs/02.JavaEE/02.服务器/01.Tomcat/05.Tomcat和Jetty.md) +- [Jetty](docs/02.JavaEE/02.服务器/02.Jetty.md) -* [Java 学习资源](docs/appendix/resources.md) +### Java 软件 + +#### Java 构建 + +> Java 项目需要通过 [**构建工具**](docs/11.软件/01.构建) 来管理项目依赖,完成编译、打包、发布、生成 JavaDoc 等任务。 +> +> - 目前最主流的构建工具是 Maven,它的功能非常强大。 +> - Gradle 号称是要替代 Maven 等构件工具,它的版本管理确实简洁,但是需要学习 Groovy,学习成本比 Maven 高。 +> - Ant 功能比 Maven 和 Gradle 要弱,现代 Java 项目基本不用了,但也有一些传统的 Java 项目还在使用。 + +- [Maven](docs/11.软件/01.构建/01.Maven) 📚 + - [Maven 快速入门](docs/11.软件/01.构建/01.Maven/01.Maven快速入门.md) + - [Maven 教程之 pom.xml 详解](docs/11.软件/01.构建/01.Maven/02.Maven教程之pom.xml详解.md) + - [Maven 教程之 settings.xml 详解](docs/11.软件/01.构建/01.Maven/03.Maven教程之settings.xml详解.md) + - [Maven 实战问题和最佳实践](docs/11.软件/01.构建/01.Maven/04.Maven实战问题和最佳实践.md) + - [Maven 教程之发布 jar 到私服或中央仓库](docs/11.软件/01.构建/01.Maven/05.Maven教程之发布jar到私服或中央仓库.md) + - [Maven 插件之代码检查](docs/11.软件/01.构建/01.Maven/06.Maven插件之代码检查.md) +- [Ant 简易教程](docs/11.软件/01.构建/02.Ant.md) + +#### Java IDE + +> 自从有了 [**IDE**](docs/11.软件/02.IDE),写代码从此就告别了刀耕火种的蛮荒时代。 +> +> - [Eclipse](docs/11.软件/02.IDE/02.Eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 +> - 曾经抗拒从转 [Intellij Idea](docs/11.软件/02.IDE/01.Intellij.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 +> - 你可以在 [vscode](docs/11.软件/02.IDE/03.VsCode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 + +- [Intellij Idea](docs/11.软件/02.IDE/01.Intellij.md) +- [Eclipse](docs/11.软件/02.IDE/02.Eclipse.md) +- [vscode](docs/11.软件/02.IDE/03.VsCode.md) + +#### Java 监控诊断 + +> [监控/诊断](docs/11.软件/03.监控诊断) 工具主要用于 Java 应用的运维。通过采集、分析、存储、可视化应用的有效数据,帮助开发者、使用者快速定位问题,找到性能瓶颈。 + +- [监控工具对比](docs/11.软件/03.监控诊断/01.监控工具对比.md) +- [CAT](docs/11.软件/03.监控诊断/02.CAT.md) +- [Zipkin](docs/11.软件/03.监控诊断/03.Zipkin.md) +- [SkyWalking](docs/11.软件/03.监控诊断/04.Skywalking.md) +- [Arthas](docs/11.软件/03.监控诊断/05.Arthas.md) + +### Java 工具 + +#### Java IO + +- [JSON 序列化](docs/12.工具/01.IO/01.JSON序列化.md) - [fastjson](https://github.com/alibaba/fastjson)、[Jackson](https://github.com/FasterXML/jackson)、[Gson](https://github.com/google/gson) +- [二进制序列化](docs/12.工具/01.IO/02.二进制序列化.md) - [Protobuf](https://developers.google.com/protocol-buffers)、[Thrift](https://thrift.apache.org/)、[Hessian](http://hessian.caucho.com/)、[Kryo](https://github.com/EsotericSoftware/kryo)、[FST](https://github.com/RuedigerMoeller/fast-serialization) + +#### JavaBean 工具 + +- [Lombok](docs/12.工具/02.JavaBean/01.Lombok.md) +- [Dozer](docs/12.工具/02.JavaBean/02.Dozer.md) + +#### Java 模板引擎 + +- [Freemark](docs/12.工具/03.模板引擎/01.Freemark.md) +- [Velocity](docs/12.工具/03.模板引擎/03.Velocity.md) +- [Thymeleaf](docs/12.工具/03.模板引擎/02.Thymeleaf.md) + +#### Java 测试工具 + +- [Junit](docs/12.工具/04.测试/01.Junit.md) +- [Mockito](docs/12.工具/04.测试/02.Mockito.md) +- [Jmeter](docs/12.工具/04.测试/03.Jmeter.md) +- [JMH](docs/12.工具/04.测试/04.JMH.md) + +#### 其他 + +- [Java 日志](docs/12.工具/99.其他/01.Java日志.md) +- [Java 工具包](docs/12.工具/99.其他/02.Java工具包.md) +- [Reflections](docs/12.工具/99.其他/03.Reflections.md) +- [JavaMail](docs/12.工具/99.其他/04.JavaMail.md) +- [Jsoup](docs/12.工具/99.其他/05.Jsoup.md) +- [Thumbnailator](docs/12.工具/99.其他/06.Thumbnailator.md) +- [Zxing](docs/12.工具/99.其他/07.Zxing.md) + +### Java 框架 + +#### ORM + +- [Mybatis 快速入门](docs/13.框架/11.ORM/01.Mybatis快速入门.md) +- [Mybatis 原理](docs/13.框架/11.ORM/02.Mybatis原理.md) + +#### Spring + +📚 [spring-tutorial](https://dunwu.github.io/spring-tutorial/) 是一个 Spring 实战教程。 + +#### Spring Boot + +📚 [Spring Boot 教程](https://dunwu.github.io/spring-boot-tutorial/) 是一个 Spring Boot 实战教程。 + +#### 安全 + +> Java 领域比较流行的安全框架就是 shiro 和 spring-security。 +> +> shiro 更为简单、轻便,容易理解,能满足大多数基本安全场景下的需要。 +> +> spring-security 功能更丰富,也比 shiro 更复杂。值得一提的是由于 spring-security 是 spring 团队开发,所以集成 spring 和 spring-boot 框架更容易。 + +- [Shiro](docs/13.框架/12.安全/01.Shiro.md) +- [SpringSecurity](docs/13.框架/12.安全/02.SpringSecurity.md) + +#### IO + +- [Shiro](docs/13.框架/13.IO/01.Netty.md) + +#### 微服务 + +- [Dubbo](docs/13.框架/14.微服务/01.Dubbo.md) + +### Java 中间件 + +#### MQ + +> 消息队列(Message Queue,简称 MQ)技术是分布式应用间交换信息的一种技术。 +> +> 消息队列主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 +> +> 如果想深入学习各种消息队列产品,建议先了解一下 [消息队列基本原理](https://dunwu.github.io/blog/pages/1fd240/) ,有助于理解消息队列特性的实现和设计思路。 + +- [消息队列面试](docs/14.中间件/01.MQ/01.消息队列面试.md) +- [消息队列基本原理](docs/14.中间件/01.MQ/02.消息队列基本原理.md) +- [RocketMQ](docs/14.中间件/01.MQ/03.RocketMQ.md) +- [ActiveMQ](docs/14.中间件/01.MQ/04.ActiveMQ.md) + +#### 缓存 + +> 缓存可以说是优化系统性能的第一手段,在各种技术中都会有缓存的应用。 +> +> 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://dunwu.github.io/design/distributed/分布式缓存.html),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 + +- [缓存面试题](docs/14.中间件/02.缓存/01.缓存面试题.md) +- [Java 缓存中间件](docs/14.中间件/02.缓存/02.Java缓存中间件.md) +- [Memcached 快速入门](docs/14.中间件/02.缓存/03.Memcached.md) +- [Ehcache 快速入门](docs/14.中间件/02.缓存/04.Ehcache.md) +- [Java 进程内缓存](docs/14.中间件/02.缓存/05.Java进程内缓存.md) +- [Http 缓存](docs/14.中间件/02.缓存/06.Http缓存.md) + +#### 流量控制 + +- [Hystrix](docs/14.中间件/03.流量控制/01.Hystrix.md) + +### [大数据](https://dunwu.github.io/bigdata-tutorial) + +> 大数据技术点以归档在:[bigdata-tutorial](https://dunwu.github.io/bigdata-tutorial) + +- [Hdfs](https://dunwu.github.io/bigdata-tutorial/hdfs) 📚 +- [Hbase](https://dunwu.github.io/bigdata-tutorial/hbase) 📚 +- [Hive](https://dunwu.github.io/bigdata-tutorial/hive) 📚 +- [MapReduce](https://dunwu.github.io/bigdata-tutorial/mapreduce) +- [Yarn](https://dunwu.github.io/bigdata-tutorial/yarn) +- [ZooKeeper](https://dunwu.github.io/bigdata-tutorial/zookeeper) 📚 +- [Kafka](https://dunwu.github.io/bigdata-tutorial/kafka) 📚 +- Spark +- Storm +- [Flink](https://dunwu.github.io/bigdata-tutorial/tree/master/docs/flink) + +## 📚 资料 + +- Java 经典书籍 + - [《Effective Java 中文版》](https://item.jd.com/12507084.html) - 本书介绍了在 Java 编程中 78 条极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。同推荐《重构 : 改善既有代码的设计》、《代码整洁之道》、《代码大全》,有一定的内容重叠。 + - [《Java 并发编程实战》](https://item.jd.com/10922250.html) - 本书深入浅出地介绍了 Java 线程和并发,是一本完美的 Java 并发参考手册。 + - [《深入理解 Java 虚拟机》](https://item.jd.com/11252778.html) - 不去了解 JVM 的工程师,和咸鱼有什么区 + - [《Maven 实战》](https://item.jd.com/10476794.html) - 国内最权威的 Maven 专家的力作,唯一一本哦! +- 其他领域书籍 + - [《Redis 设计与实现》](https://item.jd.com/11486101.html) - 系统而全面地描述了 Redis 内部运行机制。图示丰富,描述清晰,并给出大量参考信息,是 NoSQL 数据库开发人员案头必备。 + - [《鸟哥的 Linux 私房菜 (基础学习篇)》](https://item.jd.com/12443890.html) - 本书是最具知名度的 Linux 入门书《鸟哥的 Linux 私房菜基础学习篇》的最新版,全面而详细地介绍了 Linux 操作系统。内容非常全面,建议挑选和自己实际工作相关度较高的,其他部分有需要再阅读。 + - [《Head First 设计模式》](https://item.jd.com/10100236.html) - 《Head First 设计模式》(中文版)共有 14 章,每章都介绍了几个设计模式,完整地涵盖了四人组版本全部 23 个设计模式。 + - [《HTTP 权威指南》](https://item.jd.com/11056556.html) - 本书尝试着将 HTTP 中一些互相关联且常被误解的规则梳理清楚,并编写了一系列基于各种主题的章节,对 HTTP 各方面的特性进行了介绍。纵观全书,对 HTTP“为什么”这样做进行了详细的解释,而不仅仅停留在它是“怎么做”的。 + - [《TCP/IP 详解 系列》](https://item.jd.com/11966296.html) - 完整而详细的 TCP/IP 协议指南。针对任何希望理解 TCP/IP 协议是如何实现的读者设计。 + - [《剑指 Offer:名企面试官精讲典型编程题》](https://item.jd.com/12163054.html) - 剖析了 80 个典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这 5 个面试要点。 + +## 🚪 传送 + +◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ + +> 你可能会感兴趣: + +- [Java 教程](https://github.com/dunwu/java-tutorial) 📚 +- [JavaCore 教程](https://dunwu.github.io/javacore/) 📚 +- [Spring 教程](https://dunwu.github.io/spring-tutorial/) 📚 +- [Spring Boot 教程](https://dunwu.github.io/spring-boot-tutorial/) 📚 +- [数据库教程](https://dunwu.github.io/db-tutorial/) 📚 +- [数据结构和算法教程](https://dunwu.github.io/algorithm-tutorial/) 📚 +- [Linux 教程](https://dunwu.github.io/linux-tutorial/) 📚 +- [Nginx 教程](https://github.com/dunwu/nginx-tutorial/) 📚 diff --git a/book.json b/book.json deleted file mode 100644 index 1ff0b6eb..00000000 --- a/book.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "gitbook": "3.2.2", - "title": "java-stack", - "description": "Java 技术栈", - "author": "Zhang Peng", - "language": "zh-hans", - "root": "./docs", - "links": { - "sidebar": { - "java-stack": "https://github.com/dunwu/java-stack" - } - }, - "plugins": [ - "-lunr", - "-search", - "advanced-emoji@^0.2.2", - "anchor-navigation-ex@1.0.10", - "anchors@^0.7.1", - "edit-link@^2.0.2", - "expandable-chapters-small@^0.1.7", - "github@^2.0.0", - "search-plus@^0.0.11", - "simple-page-toc@^0.1.1", - "splitter@^0.0.8", - "tbfed-pagefooter@^0.0.1" - ], - "pluginsConfig": { - "anchor-navigation-ex": { - "showLevel": false, - "associatedWithSummary": true, - "multipleH1": true, - "mode": "float", - "isRewritePageTitle": false, - "float": { - "showLevelIcon": false, - "level1Icon": "fa fa-hand-o-right", - "level2Icon": "fa fa-hand-o-right", - "level3Icon": "fa fa-hand-o-right" - }, - "pageTop": { - "showLevelIcon": false, - "level1Icon": "fa fa-hand-o-right", - "level2Icon": "fa fa-hand-o-right", - "level3Icon": "fa fa-hand-o-right" - } - }, - "edit-link": { - "base": "https://github.com/dunwu/java-stack/blob/master/docs/", - "label": "编辑此页面" - }, - "github": { - "url": "https://github.com/dunwu" - }, - "simple-page-toc": { - "maxDepth": 4, - "skipFirstH1": true - }, - "sharing": { - "weibo": true, - "all": [ - "weibo" - ] - }, - "tbfed-pagefooter": { - "copyright": "Copyright © Zhang Peng 2016-2017", - "modify_label": "该文件上次修订时间:", - "modify_format": "YYYY-MM-DD HH:mm:ss" - } - } -} diff --git a/codes/README.md b/codes/README.md new file mode 100644 index 00000000..6af47b94 --- /dev/null +++ b/codes/README.md @@ -0,0 +1,3 @@ +# Source Code + +> 示例代码。 diff --git a/codes/deadloop/pom.xml b/codes/deadloop/pom.xml new file mode 100644 index 00000000..62f8f6f0 --- /dev/null +++ b/codes/deadloop/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.2.1.RELEASE + + + io.github.dunwu.trouble + deadloop + war + 故障诊断2 + 故障诊断示例源码2 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-starter-test + test + + + io.github.dunwu + dunwu-tool-core + + + org.assertj + assertj-core + + + org.projectlombok + lombok + + + mysql + mysql-connector-java + + + org.apache.httpcomponents + httpclient + 4.5.9 + + + org.apache.httpcomponents + fluent-hc + 4.5.9 + + + + + + io.github.dunwu + dunwu-dependencies + 0.5.7 + pom + import + + + + + + ${project.artifactId} + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/codes/deadloop/src/main/java/io/github/dunwu/trouble/FooService.java b/codes/deadloop/src/main/java/io/github/dunwu/trouble/FooService.java new file mode 100644 index 00000000..655b1bbb --- /dev/null +++ b/codes/deadloop/src/main/java/io/github/dunwu/trouble/FooService.java @@ -0,0 +1,21 @@ +package io.github.dunwu.trouble; + +import org.springframework.stereotype.Component; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +@Component +public class FooService { + + List data = new ArrayList<>(); + + public void oom() { + data.add(IntStream.rangeClosed(1, 100000) + .mapToObj(__ -> "a") + .collect(Collectors.joining(""))); + } + +} diff --git a/codes/deadloop/src/main/java/io/github/dunwu/trouble/OOMApplication.java b/codes/deadloop/src/main/java/io/github/dunwu/trouble/OOMApplication.java new file mode 100644 index 00000000..a39833ea --- /dev/null +++ b/codes/deadloop/src/main/java/io/github/dunwu/trouble/OOMApplication.java @@ -0,0 +1,56 @@ +package io.github.dunwu.trouble; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +import java.lang.management.ManagementFactory; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * 启动参数: + *

+ * java -verbose:gc -Xms128M -Xmx256M -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps + * -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -Dcom.sun.management.jmxremote=true + * -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false + * -Djava.rmi.server.hostname=172.22.6.43 -Dcom.sun.management.jmxremote.port=18888 -Xdebug -Xnoagent + * -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=28888,server=y,suspend=n -jar deadloop.war + */ +@Slf4j +@SpringBootApplication +public class OOMApplication implements CommandLineRunner { + + private final FooService fooService; + + public OOMApplication(FooService fooService) { + this.fooService = fooService; + } + + public static void main(String[] args) { + SpringApplication.run(OOMApplication.class, args); + } + + @Override + public void run(String... args) throws Exception { + log.info("VM options"); + log.info(ManagementFactory.getRuntimeMXBean() + .getInputArguments() + .stream() + .collect(Collectors.joining(System.lineSeparator()))); + log.info("Program arguments"); + log.info(Arrays.stream(args).collect(Collectors.joining(System.lineSeparator()))); + + while (true) { + fooService.oom(); + try { + TimeUnit.SECONDS.sleep(3); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + +} diff --git a/codes/deadloop/src/main/resources/application.properties b/codes/deadloop/src/main/resources/application.properties new file mode 100644 index 00000000..8acb1a51 --- /dev/null +++ b/codes/deadloop/src/main/resources/application.properties @@ -0,0 +1,11 @@ +server.port = 8888 +#server.port = ${random.int[1024,65536]} +spring.datasource.url = jdbc:mysql://172.22.6.9:3316/trouble_shooting?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +#spring.datasource.url = jdbc:mysql://172.22.6.9:3316/trouble_shooting?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true +spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver +spring.datasource.username = root +spring.datasource.password = 604330436 +# 强制每次启动使用 sql 初始化数据,本项目仅为了演示方便,真实环境应避免这种模式 +spring.datasource.initialization-mode = ALWAYS +spring.datasource.schema = classpath:sql/schema.sql +spring.datasource.data = classpath:sql/data.sql diff --git a/codes/deadloop/src/main/resources/banner.txt b/codes/deadloop/src/main/resources/banner.txt new file mode 100644 index 00000000..449413d5 --- /dev/null +++ b/codes/deadloop/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/deadloop/src/main/resources/logback.xml b/codes/deadloop/src/main/resources/logback.xml new file mode 100644 index 00000000..7667f240 --- /dev/null +++ b/codes/deadloop/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + + + %d{HH:mm:ss.SSS} [%boldYellow(%thread)] [%highlight(%-5level)] %boldGreen(%c{36}.%M) - %boldBlue(%m%n) + + + + + + + + diff --git a/codes/deadloop/src/main/resources/sql/data.sql b/codes/deadloop/src/main/resources/sql/data.sql new file mode 100644 index 00000000..733abe41 --- /dev/null +++ b/codes/deadloop/src/main/resources/sql/data.sql @@ -0,0 +1,11 @@ +-- ------------------------------------------------------------------- +-- 运行本项目的初始化 DML 脚本 +-- Mysql 知识点可以参考: +-- https://dunwu.github.io/db-tutorial/#/sql/mysql/README +-- ------------------------------------------------------------------- + +# INSERT INTO user (username, password, email) +# VALUES ('admin', '$2a$10$Y9uV9YjFuNlATDGz5MeTZeuo8LbebbpP6jRgtZYQcgiCZRlf8rJYG', 'admin@xxx.com'); +# +# INSERT INTO user (username, password, email) +# VALUES ('user', '$2a$10$Y9uV9YjFuNlATDGz5MeTZeuo8LbebbpP6jRgtZYQcgiCZRlf8rJYG', 'user@xxx.com'); diff --git a/codes/deadloop/src/main/resources/sql/schema.sql b/codes/deadloop/src/main/resources/sql/schema.sql new file mode 100644 index 00000000..13e82ae2 --- /dev/null +++ b/codes/deadloop/src/main/resources/sql/schema.sql @@ -0,0 +1,15 @@ +-- ------------------------------------------------------------------- +-- 运行本项目的初始化 DDL 脚本 +-- Mysql 知识点可以参考: +-- https://dunwu.github.io/db-tutorial/#/sql/mysql/README +-- ------------------------------------------------------------------- + +-- 强制新建用户表 +DROP TABLE IF EXISTS `testuser`; +CREATE TABLE `testuser` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; diff --git a/codes/java-distributed/README.md b/codes/java-distributed/README.md new file mode 100644 index 00000000..a96b29fe --- /dev/null +++ b/codes/java-distributed/README.md @@ -0,0 +1,3 @@ +# Java 和分布式 + +> 分布式系统中常用算法的 Java 实现方案 diff --git a/codes/java-distributed/java-distributed-id/pom.xml b/codes/java-distributed/java-distributed-id/pom.xml new file mode 100644 index 00000000..5f72fc0d --- /dev/null +++ b/codes/java-distributed/java-distributed-id/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + + io.github.dunwu.distributed + java-distributed + 1.0.0 + + + io.github.dunwu.javatech + java-distributed-id + 1.0.0 + jar + ${project.artifactId} + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + org.apache.zookeeper + zookeeper + + + org.apache.curator + curator-recipes + + + redis.clients + jedis + + + cn.hutool + hutool-all + + + org.projectlombok + lombok + + + ch.qos.logback + logback-classic + true + + + diff --git a/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId.java b/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId.java new file mode 100644 index 00000000..06d7092c --- /dev/null +++ b/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId.java @@ -0,0 +1,55 @@ +package io.github.dunwu.distributed.id; + +import cn.hutool.core.collection.CollectionUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.ExponentialBackoffRetry; +import org.apache.zookeeper.CreateMode; + +import java.util.List; + +/** + * ZK 分布式 ID + * + * @author Zhang Peng + * @date 2024-12-20 + */ +@Slf4j +public class ZookeeperDistributedId { + + public static void main(String[] args) throws Exception { + + // 获取客户端 + RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); + CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy); + + // 开启会话 + client.start(); + + String id1 = client.create() + .creatingParentsIfNeeded() + .withMode(CreateMode.PERSISTENT_SEQUENTIAL) + .forPath("/zkid/id_"); + log.info("id: {}", id1); + + String id2 = client.create() + .creatingParentsIfNeeded() + .withMode(CreateMode.PERSISTENT_SEQUENTIAL) + .forPath("/zkid/id_"); + log.info("id: {}", id2); + + List children = client.getChildren().forPath("/zkid"); + if (CollectionUtil.isNotEmpty(children)) { + for (String child : children) { + client.delete().forPath("/zkid/" + child); + } + } + client.delete().forPath("/zkid"); + + // 关闭客户端 + client.close(); + } + +} diff --git a/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId2.java b/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId2.java new file mode 100644 index 00000000..69d5d543 --- /dev/null +++ b/codes/java-distributed/java-distributed-id/src/main/java/io/github/dunwu/distributed/id/ZookeeperDistributedId2.java @@ -0,0 +1,46 @@ +package io.github.dunwu.distributed.id; + +import lombok.extern.slf4j.Slf4j; +import org.apache.curator.RetryPolicy; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.framework.recipes.atomic.AtomicValue; +import org.apache.curator.framework.recipes.atomic.DistributedAtomicLong; +import org.apache.curator.retry.ExponentialBackoffRetry; + +/** + * ZK 分布式 ID + *

+ * 基于原子计数器生成 ID + * + * @author Zhang Peng + * @date 2024-12-20 + */ +@Slf4j +public class ZookeeperDistributedId2 { + + public static void main(String[] args) throws Exception { + + // 获取客户端 + RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); + CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy); + DistributedAtomicLong atomicLong = new DistributedAtomicLong(client, "/zkid", retryPolicy); + + // 开启会话 + client.start(); + + // 基于原子计数器生成 ID + AtomicValue id1 = atomicLong.increment(); + log.info("id: {}", id1.postValue()); + + AtomicValue id2 = atomicLong.increment(); + log.info("id: {}", id2.postValue()); + + // 清理节点 + client.delete().forPath("/zkid"); + + // 关闭客户端 + client.close(); + } + +} diff --git a/codes/java-distributed/java-distributed-id/src/main/resources/scripts/fixed_window_rate_limit.lua b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/fixed_window_rate_limit.lua new file mode 100644 index 00000000..e0c9ad00 --- /dev/null +++ b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/fixed_window_rate_limit.lua @@ -0,0 +1,21 @@ +-- 缓存 Key +local key = KEYS[1] +-- 访问请求数 +local permits = tonumber(ARGV[1]) +-- 过期时间 +local seconds = tonumber(ARGV[2]) +-- 限流阈值 +local limit = tonumber(ARGV[3]) + +-- 获取统计值 +local count = tonumber(redis.call('GET', key) or "0") + +if count + permits > limit then + -- 请求拒绝 + return -1 +else + -- 请求通过 + redis.call('INCRBY', key, permits) + redis.call('EXPIRE', key, seconds) + return count + permits +end \ No newline at end of file diff --git a/codes/java-distributed/java-distributed-id/src/main/resources/scripts/token_bucket_rate_limit.lua b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/token_bucket_rate_limit.lua new file mode 100644 index 00000000..541d70c9 --- /dev/null +++ b/codes/java-distributed/java-distributed-id/src/main/resources/scripts/token_bucket_rate_limit.lua @@ -0,0 +1,39 @@ +local tokenKey = KEYS[1] +local timeKey = KEYS[2] + +-- 申请令牌数 +local permits = tonumber(ARGV[1]) +-- QPS +local qps = tonumber(ARGV[2]) +-- 桶的容量 +local capacity = tonumber(ARGV[3]) +-- 当前时间(单位:毫秒) +local nowMillis = tonumber(ARGV[4]) +-- 填满令牌桶所需要的时间 +local fillTime = capacity / qps +local ttl = math.min(capacity, math.floor(fillTime * 2)) + +local currentTokenNum = tonumber(redis.call("GET", tokenKey)) +if currentTokenNum == nil then + currentTokenNum = capacity +end + +local endTimeMillis = tonumber(redis.call("GET", timeKey)) +if endTimeMillis == nil then + endTimeMillis = 0 +end + +local gap = nowMillis - endTimeMillis +local newTokenNum = math.max(0, gap * qps / 1000) +local currentTokenNum = math.min(capacity, currentTokenNum + newTokenNum) + +if currentTokenNum < permits then + -- 请求拒绝 + return -1 +else + -- 请求通过 + local finalTokenNum = currentTokenNum - permits + redis.call("SETEX", tokenKey, ttl, finalTokenNum) + redis.call("SETEX", timeKey, ttl, nowMillis) + return finalTokenNum +end diff --git a/codes/java-distributed/java-load-balance/pom.xml b/codes/java-distributed/java-load-balance/pom.xml new file mode 100644 index 00000000..b4e73f0f --- /dev/null +++ b/codes/java-distributed/java-load-balance/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + io.github.dunwu.distributed + java-load-balance + 1.0.0 + jar + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + cn.hutool + hutool-all + 5.4.1 + + + org.projectlombok + lombok + 1.18.12 + + + junit + junit + 4.13 + test + + + diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/BaseLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/BaseLoadBalance.java new file mode 100644 index 00000000..ebb317a8 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/BaseLoadBalance.java @@ -0,0 +1,37 @@ +package io.github.dunwu.distributed; + +import cn.hutool.core.collection.CollectionUtil; + +import java.util.List; + +/** + * @author Zhang Peng + * @since 2021-01-18 + */ +public abstract class BaseLoadBalance implements LoadBalance { + + @Override + public N select(List nodes, String ip) { + // nodes 列表为空,返回 null + if (CollectionUtil.isEmpty(nodes)) { + return null; + } + + // 如果 nodes 列表中仅有一个 node,直接返回即可 + if (nodes.size() == 1) { + return nodes.get(0); + } + + return doSelect(nodes, ip); + } + + /** + * 负载均衡算法抽象方法,各个算法需要自行实现 + * + * @param nodes 节点列表 + * @param ip 请求方 IP + * @return 被选中的节点 + */ + protected abstract N doSelect(List nodes, String ip); + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/ConsistentHashLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/ConsistentHashLoadBalance.java new file mode 100644 index 00000000..c67558a2 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/ConsistentHashLoadBalance.java @@ -0,0 +1,129 @@ +package io.github.dunwu.distributed; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class ConsistentHashLoadBalance extends BaseLoadBalance implements LoadBalance { + + private final ConcurrentMap> selectors = new ConcurrentHashMap<>(); + + @SuppressWarnings("unchecked") + @Override + protected N doSelect(List nodes, String ip) { + // 分片数,这里设为节点数的 4 倍 + Integer replicaNum = nodes.size() * 4; + // 获取 nodes 原始的 hashcode + int identityHashCode = System.identityHashCode(nodes); + + // 如果 nodes 是一个新的 List 对象,意味着节点数量发生了变化 + // 此时 selector.identityHashCode != identityHashCode 条件成立 + ConsistentHashSelector selector = (ConsistentHashSelector) selectors.get(ip); + if (selector == null || selector.identityHashCode != identityHashCode) { + // 创建新的 ConsistentHashSelector + selectors.put(ip, new ConsistentHashSelector<>(nodes, identityHashCode, replicaNum)); + selector = (ConsistentHashSelector) selectors.get(ip); + } + // 调用 ConsistentHashSelector 的 select 方法选择 Node + return selector.select(ip); + } + + /** + * 一致性哈希选择器 + */ + private static final class ConsistentHashSelector { + + /** + * 存储虚拟节点 + */ + private final TreeMap virtualNodes; + + private final int identityHashCode; + + /** + * 构造器 + * + * @param nodes 节点列表 + * @param identityHashCode hashcode + * @param replicaNum 分片数 + */ + ConsistentHashSelector(List nodes, int identityHashCode, Integer replicaNum) { + this.virtualNodes = new TreeMap<>(); + this.identityHashCode = identityHashCode; + // 获取虚拟节点数,默认为 100 + if (replicaNum == null) { + replicaNum = 100; + } + for (N node : nodes) { + for (int i = 0; i < replicaNum / 4; i++) { + // 对 url 进行 md5 运算,得到一个长度为16的字节数组 + byte[] digest = md5(node.getUrl()); + // 对 digest 部分字节进行 4 次 hash 运算,得到四个不同的 long 型正整数 + for (int j = 0; j < 4; j++) { + // h = 0 时,取 digest 中下标为 0 ~ 3 的4个字节进行位运算 + // h = 1 时,取 digest 中下标为 4 ~ 7 的4个字节进行位运算 + // h = 2, h = 3 时过程同上 + long m = hash(digest, j); + // 将 hash 到 node 的映射关系存储到 virtualNodes 中, + // virtualNodes 需要提供高效的查询操作,因此选用 TreeMap 作为存储结构 + virtualNodes.put(m, node); + } + } + } + } + + public N select(String key) { + // 对参数 key 进行 md5 运算 + byte[] digest = md5(key); + // 取 digest 数组的前四个字节进行 hash 运算,再将 hash 值传给 selectForKey 方法, + // 寻找合适的 Node + return selectForKey(hash(digest, 0)); + } + + private N selectForKey(long hash) { + // 查找第一个大于或等于当前 hash 的节点 + Map.Entry entry = virtualNodes.ceilingEntry(hash); + // 如果 hash 大于 Node 在哈希环上最大的位置,此时 entry = null, + // 需要将 TreeMap 的头节点赋值给 entry + if (entry == null) { + entry = virtualNodes.firstEntry(); + } + // 返回 Node + return entry.getValue(); + } + + } + + /** + * 计算 hash 值 + */ + public static long hash(byte[] digest, int number) { + return (((long) (digest[3 + number * 4] & 0xFF) << 24) + | ((long) (digest[2 + number * 4] & 0xFF) << 16) + | ((long) (digest[1 + number * 4] & 0xFF) << 8) + | (digest[number * 4] & 0xFF)) + & 0xFFFFFFFFL; + } + + /** + * 计算 MD5 值 + */ + public static byte[] md5(String value) { + MessageDigest md5; + try { + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException(e.getMessage(), e); + } + md5.reset(); + byte[] bytes = value.getBytes(StandardCharsets.UTF_8); + md5.update(bytes); + return md5.digest(); + } + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/IpHashLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/IpHashLoadBalance.java new file mode 100644 index 00000000..3d71cbb7 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/IpHashLoadBalance.java @@ -0,0 +1,29 @@ +package io.github.dunwu.distributed; + +import cn.hutool.core.util.HashUtil; +import cn.hutool.core.util.StrUtil; + +import java.util.List; + +/** + * @author peng.zhang + * @date 2021/1/19 + */ +public class IpHashLoadBalance extends BaseLoadBalance implements LoadBalance { + + @Override + protected N doSelect(List nodes, String ip) { + if (StrUtil.isBlank(ip)) { + ip = "127.0.0.1"; + } + + int length = nodes.size(); + int index = hash(ip) % length; + return nodes.get(index); + } + + public int hash(String text) { + return HashUtil.fnvHash(text); + } + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LeastActiveLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LeastActiveLoadBalance.java new file mode 100644 index 00000000..23a7f03c --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LeastActiveLoadBalance.java @@ -0,0 +1,83 @@ +package io.github.dunwu.distributed; + +import java.util.List; +import java.util.Random; + +/** + * @author peng.zhang + * @date 2021/1/18 + */ +public class LeastActiveLoadBalance extends BaseLoadBalance implements LoadBalance { + + private final Random random = new Random(); + + @Override + protected N doSelect(List nodes, String ip) { + int length = nodes.size(); + // 最小的活跃数 + int leastActive = -1; + // 具有相同“最小活跃数”的服务者提供者(以下用 Node 代称)数量 + int leastCount = 0; + // leastIndexs 用于记录具有相同“最小活跃数”的 Node 在 nodes 列表中的下标信息 + int[] leastIndexs = new int[length]; + int totalWeight = 0; + // 第一个最小活跃数的 Node 权重值,用于与其他具有相同最小活跃数的 Node 的权重进行对比, + // 以检测是否“所有具有相同最小活跃数的 Node 的权重”均相等 + int firstWeight = 0; + boolean sameWeight = true; + + // 遍历 nodes 列表 + for (int i = 0; i < length; i++) { + N node = nodes.get(i); + // 发现更小的活跃数,重新开始 + if (leastActive == -1 || node.getActive() < leastActive) { + // 使用当前活跃数更新最小活跃数 leastActive + leastActive = node.getActive(); + // 更新 leastCount 为 1 + leastCount = 1; + // 记录当前下标值到 leastIndexs 中 + leastIndexs[0] = i; + totalWeight = node.getWeight(); + firstWeight = node.getWeight(); + sameWeight = true; + + // 当前 Node 的活跃数 node.getActive() 与最小活跃数 leastActive 相同 + } else if (node.getActive() == leastActive) { + // 在 leastIndexs 中记录下当前 Node 在 nodes 集合中的下标 + leastIndexs[leastCount++] = i; + // 累加权重 + totalWeight += node.getWeight(); + // 检测当前 Node 的权重与 firstWeight 是否相等, + // 不相等则将 sameWeight 置为 false + if (sameWeight && i > 0 + && node.getWeight() != firstWeight) { + sameWeight = false; + } + } + } + + // 当只有一个 Node 具有最小活跃数,此时直接返回该 Node 即可 + if (leastCount == 1) { + return nodes.get(leastIndexs[0]); + } + + // 有多个 Node 具有相同的最小活跃数,但它们之间的权重不同 + if (!sameWeight && totalWeight > 0) { + // 随机生成一个 [0, totalWeight) 之间的数字 + int offsetWeight = random.nextInt(totalWeight); + // 循环让随机数减去具有最小活跃数的 Node 的权重值, + // 当 offset 小于等于0时,返回相应的 Node + for (int i = 0; i < leastCount; i++) { + int leastIndex = leastIndexs[i]; + // 获取权重值,并让随机数减去权重值 + offsetWeight -= nodes.get(leastIndex).getWeight(); + if (offsetWeight <= 0) { + return nodes.get(leastIndex); + } + } + } + // 如果权重相同或权重为0时,随机返回一个 Node + return nodes.get(leastIndexs[random.nextInt(leastCount)]); + } + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalance.java new file mode 100644 index 00000000..f2d0c561 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalance.java @@ -0,0 +1,15 @@ +package io.github.dunwu.distributed; + +import java.util.List; + +/** + * 负载均衡策略接口 + * + * @author Zhang Peng + * @since 2020-01-21 + */ +public interface LoadBalance { + + N select(List nodes, String ip); + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalanceDemo.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalanceDemo.java new file mode 100644 index 00000000..57dc61f1 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/LoadBalanceDemo.java @@ -0,0 +1,146 @@ +package io.github.dunwu.distributed; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.TreeMap; + +/** + * 负载均衡算法测试例 + * + * @author peng.zhang + * @date 2021/1/19 + */ +public class LoadBalanceDemo { + + private static final Random random = new Random(); + + public static String randomIpv4() { + int[][] range = { { 607649792, 608174079 }, // 36.56.0.0-36.63.255.255 + { 1038614528, 1039007743 }, // 61.232.0.0-61.237.255.255 + { 1783627776, 1784676351 }, // 106.80.0.0-106.95.255.255 + { 2035023872, 2035154943 }, // 121.76.0.0-121.77.255.255 + { 2078801920, 2079064063 }, // 123.232.0.0-123.235.255.255 + { -1950089216, -1948778497 }, // 139.196.0.0-139.215.255.255 + { -1425539072, -1425014785 }, // 171.8.0.0-171.15.255.255 + { -1236271104, -1235419137 }, // 182.80.0.0-182.92.255.255 + { -770113536, -768606209 }, // 210.25.0.0-210.47.255.255 + { -569376768, -564133889 }, // 222.16.0.0-222.95.255.255 + }; + + Random rdint = new Random(); + int index = rdint.nextInt(10); + String ip = num2ip(range[index][0] + + new Random().nextInt(range[index][1] - range[index][0])); + return ip; + } + + private static String num2ip(final int ip) { + int[] b = new int[4]; + String result = ""; + b[0] = (ip >> 24) & 0xff; + b[1] = ((ip >> 16) & 0xff); + b[2] = ((ip >> 8) & 0xff); + b[3] = (ip & 0xff); + result = Integer.toString(b[0]) + "." + Integer.toString(b[1]) + "." + + Integer.toString(b[2]) + "." + Integer.toString(b[3]); + return result; + } + + /** + * 生成 num 个随机 IP 地址 + */ + private static List initRandomIpList(int num) { + List list = new ArrayList<>(); + for (int i = 1; i <= num; i++) { + list.add(randomIpv4()); + } + return list; + } + + /** + * 生成 num 个样本节点 + * + * @param num 节点数 + * @param sameWeight 各节点权重是否相同 + * @param sameActive 各节点活跃数是否相同 + */ + private static List initNodeList(Integer num, boolean sameWeight, boolean sameActive) { + + List nodes = new ArrayList<>(); + for (int i = 1; i <= num; i++) { + Node node = new Node("192.168.0." + i); + if (!sameWeight) { + node.setWeight(random.nextInt(10)); + } + if (!sameActive) { + node.setActive(random.nextInt(10)); + } + nodes.add(node); + } + return nodes; + } + + /** + * 统计负载均衡命中次数,样本数为 10000 次访问 + */ + private static Map loadBalance10000(LoadBalance algorithm, List nodes, + List ipList) { + Map staticMap = new TreeMap<>(); + + int ipLength = ipList.size(); + for (int i = 0; i < 10000; i++) { + String ip = ipList.get(random.nextInt(ipLength)); + Node node = algorithm.select(nodes, ip); + // 打印每一次负载均衡的选择结果 + // System.out.println(StrUtil.format("ip = {}, node url = {}", ip, node.getUrl())); + if (staticMap.containsKey(node)) { + Long value = staticMap.get(node); + staticMap.put(node, ++value); + } else { + staticMap.put(node, 1L); + } + } + + System.out.println("======================= 统计数据 ======================="); + staticMap.forEach((key, value) -> { + System.out.printf("key = %s, value = %s\n", key, value); + }); + System.out.printf("方差:%s, ", StatisticsUtil.variance(staticMap.values().toArray(new Long[0]))); + System.out.printf("标准差:%s\n", StatisticsUtil.standardDeviation(staticMap.values().toArray(new Long[] {}))); + return staticMap; + } + + public static void main(String[] args) { + // 构造 100 个候选服务器节点 + List nodes = initNodeList(100, false, false); + // 构造 100 个随机IP + List ipList = initRandomIpList(100); + + // ============================================================================ + // 基于以上构造数据,对每种算法都 负载均衡选择 10000 次,然后统计方差、标准差,查看负载均衡效果。 + + System.out.println("======================= 随机负载均衡 ======================="); + loadBalance10000(new RandomLoadBalance<>(), nodes, ipList); + + System.out.println("======================= 加权随机负载均衡 ======================="); + loadBalance10000(new WeightRandomLoadBalance<>(), nodes, ipList); + + System.out.println("======================= 轮询负载均衡 ======================="); + loadBalance10000(new RoundRobinLoadBalance<>(), nodes, ipList); + + System.out.println("======================= 加权轮询负载均衡 ======================="); + loadBalance10000(new WeightRoundRobinLoadBalance<>(), nodes, ipList); + + System.out.println("======================= 源地址哈希负载均衡 ======================="); + loadBalance10000(new IpHashLoadBalance<>(), nodes, ipList); + + System.out.println("======================= 最小活跃数负载均衡 ======================="); + loadBalance10000(new LeastActiveLoadBalance<>(), nodes, ipList); + + System.out.println("======================= 一致性哈希负载均衡 ======================="); + loadBalance10000(new ConsistentHashLoadBalance<>(), nodes, ipList); + } + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/Node.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/Node.java new file mode 100644 index 00000000..2fc9c712 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/Node.java @@ -0,0 +1,87 @@ +package io.github.dunwu.distributed; + +import java.util.Objects; + +/** + * 负载均衡节点 + * + * @author Zhang Peng + * @since 2020-01-23 + */ +public class Node implements Comparable { + + public static final Integer DEFAULT_WEIGHT = 1; + public static final Integer DEFAULT_ACTIVE = 0; + + protected String url; + + protected Integer weight; + + protected Integer active; + + public Node(String url) { + this(url, DEFAULT_WEIGHT, DEFAULT_ACTIVE); + } + + public Node(String url, Integer weight, Integer active) { + this.url = url; + this.weight = weight; + this.active = active; + } + + @Override + public int compareTo(Node o) { + return url.compareTo(o.url); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Node)) { + return false; + } + Node node = (Node) o; + return url.equals(node.url); + } + + @Override + public int hashCode() { + return Objects.hash(url); + } + + @Override + public String toString() { + return "Node{" + + "url='" + url + '\'' + + ", weight=" + weight + + ", active=" + active + + '}'; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public Integer getWeight() { + return weight; + } + + public void setWeight(Integer weight) { + this.weight = weight; + } + + public Integer getActive() { + return active; + } + + public void setActive(Integer active) { + this.active = active; + } + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RandomLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RandomLoadBalance.java new file mode 100644 index 00000000..5b775dd2 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RandomLoadBalance.java @@ -0,0 +1,23 @@ +package io.github.dunwu.distributed; + +import java.util.List; +import java.util.Random; + +/** + * (加权)随机负载均衡策略 + * + * @author Zhang Peng + * @see Zhang Peng + * @since 2020-01-20 + */ +public class RandomLoadBalance extends BaseLoadBalance implements LoadBalance { + + private final Random random = new Random(); + + @Override + protected N doSelect(List nodes, String ip) { + int index = random.nextInt(nodes.size()); + return nodes.get(index); + } + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RoundRobinLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RoundRobinLoadBalance.java new file mode 100644 index 00000000..c0858152 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/RoundRobinLoadBalance.java @@ -0,0 +1,26 @@ +package io.github.dunwu.distributed; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * (加权)轮询负载均衡策略 + * + * @author Zhang Peng + * @since 2020-01-20 + */ +public class RoundRobinLoadBalance extends BaseLoadBalance implements LoadBalance { + + private final AtomicInteger position = new AtomicInteger(0); + + @Override + protected N doSelect(List nodes, String ip) { + int length = nodes.size(); + // 如果位置值已经等于节点数,重置为 0 + position.compareAndSet(length, 0); + N node = nodes.get(position.get()); + position.getAndIncrement(); + return node; + } + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/StatisticsUtil.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/StatisticsUtil.java new file mode 100644 index 00000000..cbb66d13 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/StatisticsUtil.java @@ -0,0 +1,35 @@ +package io.github.dunwu.distributed; + +public class StatisticsUtil { + + private StatisticsUtil() {} + + /** + * 方差计算 + * 公式:s^2 = [(x1-x)^2 +...(xn-x)^2]/n + */ + public static double variance(Long[] array) { + int m = array.length; + double sum = 0; + for (Long item : array) {// 求和 + sum += item; + } + double avg = sum / m;// 求平均值 + double value = 0; + for (Long item : array) {// 求方差 + value += (item - avg) * (item - avg); + } + return value / m; + } + + /** + * 标准差 + * 公式 result = sqrt(s^2),即 sqrt(variance(array)) + */ + public static double standardDeviation(Long[] array) { + int m = array.length; + double value = variance(array); + return Math.sqrt(value / m); + } + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRandomLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRandomLoadBalance.java new file mode 100644 index 00000000..c7135667 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRandomLoadBalance.java @@ -0,0 +1,42 @@ +package io.github.dunwu.distributed; + +import java.util.List; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author peng.zhang + * @date 2021/1/19 + */ +public class WeightRandomLoadBalance extends BaseLoadBalance implements LoadBalance { + + private final Random random = ThreadLocalRandom.current(); + + @Override + protected N doSelect(List nodes, String ip) { + + int length = nodes.size(); + AtomicInteger totalWeight = new AtomicInteger(0); + for (N node : nodes) { + Integer weight = node.getWeight(); + totalWeight.getAndAdd(weight); + } + + if (totalWeight.get() > 0) { + int offset = random.nextInt(totalWeight.get()); + for (N node : nodes) { + // 让随机值 offset 减去权重值 + offset -= node.getWeight(); + if (offset < 0) { + // 返回相应的 Node + return node; + } + } + } + + // 直接随机返回一个 + return nodes.get(random.nextInt(length)); + } + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRoundRobinLoadBalance.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRoundRobinLoadBalance.java new file mode 100644 index 00000000..3f71a573 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/WeightRoundRobinLoadBalance.java @@ -0,0 +1,160 @@ +package io.github.dunwu.distributed; + +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; + +/** + * @author peng.zhang + * @date 2021/1/19 + */ +public class WeightRoundRobinLoadBalance extends BaseLoadBalance implements LoadBalance { + + /** + * 60秒 + */ + private static final int RECYCLE_PERIOD = 60000; + + /** + * Node hashcode 到 WeightedRoundRobin 的映射关系 + */ + private ConcurrentMap weightMap = new ConcurrentHashMap<>(); + + /** + * 原子更新锁 + */ + private AtomicBoolean updateLock = new AtomicBoolean(); + + @Override + protected N doSelect(List nodes, String ip) { + + int totalWeight = 0; + long maxCurrent = Long.MIN_VALUE; + + // 获取当前时间 + long now = System.currentTimeMillis(); + N selectedNode = null; + WeightedRoundRobin selectedWRR = null; + + // 下面这个循环主要做了这样几件事情: + // 1. 遍历 Node 列表,检测当前 Node 是否有相应的 WeightedRoundRobin,没有则创建 + // 2. 检测 Node 权重是否发生了变化,若变化了,则更新 WeightedRoundRobin 的 weight 字段 + // 3. 让 current 字段加上自身权重,等价于 current += weight + // 4. 设置 lastUpdate 字段,即 lastUpdate = now + // 5. 寻找具有最大 current 的 Node,以及 Node 对应的 WeightedRoundRobin, + // 暂存起来,留作后用 + // 6. 计算权重总和 + for (N node : nodes) { + int hashCode = node.hashCode(); + WeightedRoundRobin weightedRoundRobin = weightMap.get(hashCode); + int weight = node.getWeight(); + if (weight < 0) { + weight = 0; + } + + // 检测当前 Node 是否有对应的 WeightedRoundRobin,没有则创建 + if (weightedRoundRobin == null) { + weightedRoundRobin = new WeightedRoundRobin(); + // 设置 Node 权重 + weightedRoundRobin.setWeight(weight); + // 存储 url 唯一标识 identifyString 到 weightedRoundRobin 的映射关系 + weightMap.putIfAbsent(hashCode, weightedRoundRobin); + weightedRoundRobin = weightMap.get(hashCode); + } + // Node 权重不等于 WeightedRoundRobin 中保存的权重,说明权重变化了,此时进行更新 + if (weight != weightedRoundRobin.getWeight()) { + weightedRoundRobin.setWeight(weight); + } + + // 让 current 加上自身权重,等价于 current += weight + long current = weightedRoundRobin.increaseCurrent(); + // 设置 lastUpdate,表示近期更新过 + weightedRoundRobin.setLastUpdate(now); + // 找出最大的 current + if (current > maxCurrent) { + maxCurrent = current; + // 将具有最大 current 权重的 Node 赋值给 selectedNode + selectedNode = node; + // 将 Node 对应的 weightedRoundRobin 赋值给 selectedWRR,留作后用 + selectedWRR = weightedRoundRobin; + } + + // 计算权重总和 + totalWeight += weight; + } + + // 对 weightMap 进行检查,过滤掉长时间未被更新的节点。 + // 该节点可能挂了,nodes 中不包含该节点,所以该节点的 lastUpdate 长时间无法被更新。 + // 若未更新时长超过阈值后,就会被移除掉,默认阈值为60秒。 + if (!updateLock.get() && nodes.size() != weightMap.size()) { + if (updateLock.compareAndSet(false, true)) { + try { + // 遍历修改,即移除过期记录 + weightMap.entrySet().removeIf(item -> now - item.getValue().getLastUpdate() > RECYCLE_PERIOD); + } finally { + updateLock.set(false); + } + } + } + + if (selectedNode != null) { + // 让 current 减去权重总和,等价于 current -= totalWeight + selectedWRR.decreaseCurrent(totalWeight); + // 返回具有最大 current 的 Node + return selectedNode; + } + + // should not happen here + return nodes.get(0); + } + + protected static class WeightedRoundRobin { + + // 服务提供者权重 + private int weight; + // 当前权重 + private AtomicLong current = new AtomicLong(0); + // 最后一次更新时间 + private long lastUpdate; + + public long increaseCurrent() { + // current = current + weight; + return current.addAndGet(weight); + } + + public long decreaseCurrent(int total) { + // current = current - total; + return current.addAndGet(-1 * total); + } + + public int getWeight() { + return weight; + } + + public void setWeight(int weight) { + this.weight = weight; + // 初始情况下,current = 0 + current.set(0); + } + + public AtomicLong getCurrent() { + return current; + } + + public void setCurrent(AtomicLong current) { + this.current = current; + } + + public long getLastUpdate() { + return lastUpdate; + } + + public void setLastUpdate(long lastUpdate) { + this.lastUpdate = lastUpdate; + } + + } + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/package-info.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/package-info.java new file mode 100644 index 00000000..4d8b7a26 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/package-info.java @@ -0,0 +1,7 @@ +/** + * 负载均衡算法实现 + * + * @author Zhang Peng + * @since 2020-01-22 + */ +package io.github.dunwu.distributed; diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/CRCHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/CRCHashStrategy.java new file mode 100644 index 00000000..5c732a5b --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/CRCHashStrategy.java @@ -0,0 +1,68 @@ +package io.github.dunwu.distributed.support; + +import java.nio.charset.StandardCharsets; + +public class CRCHashStrategy implements HashStrategy { + + private static final int LOOKUP_TABLE[] = { 0x0000, 0x1021, 0x2042, 0x3063, + 0x4084, 0x50A5, 0x60C6, 0x70E7, 0x8108, 0x9129, 0xA14A, 0xB16B, + 0xC18C, 0xD1AD, 0xE1CE, 0xF1EF, 0x1231, 0x0210, 0x3273, 0x2252, + 0x52B5, 0x4294, 0x72F7, 0x62D6, 0x9339, 0x8318, 0xB37B, 0xA35A, + 0xD3BD, 0xC39C, 0xF3FF, 0xE3DE, 0x2462, 0x3443, 0x0420, 0x1401, + 0x64E6, 0x74C7, 0x44A4, 0x5485, 0xA56A, 0xB54B, 0x8528, 0x9509, + 0xE5EE, 0xF5CF, 0xC5AC, 0xD58D, 0x3653, 0x2672, 0x1611, 0x0630, + 0x76D7, 0x66F6, 0x5695, 0x46B4, 0xB75B, 0xA77A, 0x9719, 0x8738, + 0xF7DF, 0xE7FE, 0xD79D, 0xC7BC, 0x48C4, 0x58E5, 0x6886, 0x78A7, + 0x0840, 0x1861, 0x2802, 0x3823, 0xC9CC, 0xD9ED, 0xE98E, 0xF9AF, + 0x8948, 0x9969, 0xA90A, 0xB92B, 0x5AF5, 0x4AD4, 0x7AB7, 0x6A96, + 0x1A71, 0x0A50, 0x3A33, 0x2A12, 0xDBFD, 0xCBDC, 0xFBBF, 0xEB9E, + 0x9B79, 0x8B58, 0xBB3B, 0xAB1A, 0x6CA6, 0x7C87, 0x4CE4, 0x5CC5, + 0x2C22, 0x3C03, 0x0C60, 0x1C41, 0xEDAE, 0xFD8F, 0xCDEC, 0xDDCD, + 0xAD2A, 0xBD0B, 0x8D68, 0x9D49, 0x7E97, 0x6EB6, 0x5ED5, 0x4EF4, + 0x3E13, 0x2E32, 0x1E51, 0x0E70, 0xFF9F, 0xEFBE, 0xDFDD, 0xCFFC, + 0xBF1B, 0xAF3A, 0x9F59, 0x8F78, 0x9188, 0x81A9, 0xB1CA, 0xA1EB, + 0xD10C, 0xC12D, 0xF14E, 0xE16F, 0x1080, 0x00A1, 0x30C2, 0x20E3, + 0x5004, 0x4025, 0x7046, 0x6067, 0x83B9, 0x9398, 0xA3FB, 0xB3DA, + 0xC33D, 0xD31C, 0xE37F, 0xF35E, 0x02B1, 0x1290, 0x22F3, 0x32D2, + 0x4235, 0x5214, 0x6277, 0x7256, 0xB5EA, 0xA5CB, 0x95A8, 0x8589, + 0xF56E, 0xE54F, 0xD52C, 0xC50D, 0x34E2, 0x24C3, 0x14A0, 0x0481, + 0x7466, 0x6447, 0x5424, 0x4405, 0xA7DB, 0xB7FA, 0x8799, 0x97B8, + 0xE75F, 0xF77E, 0xC71D, 0xD73C, 0x26D3, 0x36F2, 0x0691, 0x16B0, + 0x6657, 0x7676, 0x4615, 0x5634, 0xD94C, 0xC96D, 0xF90E, 0xE92F, + 0x99C8, 0x89E9, 0xB98A, 0xA9AB, 0x5844, 0x4865, 0x7806, 0x6827, + 0x18C0, 0x08E1, 0x3882, 0x28A3, 0xCB7D, 0xDB5C, 0xEB3F, 0xFB1E, + 0x8BF9, 0x9BD8, 0xABBB, 0xBB9A, 0x4A75, 0x5A54, 0x6A37, 0x7A16, + 0x0AF1, 0x1AD0, 0x2AB3, 0x3A92, 0xFD2E, 0xED0F, 0xDD6C, 0xCD4D, + 0xBDAA, 0xAD8B, 0x9DE8, 0x8DC9, 0x7C26, 0x6C07, 0x5C64, 0x4C45, + 0x3CA2, 0x2C83, 0x1CE0, 0x0CC1, 0xEF1F, 0xFF3E, 0xCF5D, 0xDF7C, + 0xAF9B, 0xBFBA, 0x8FD9, 0x9FF8, 0x6E17, 0x7E36, 0x4E55, 0x5E74, + 0x2E93, 0x3EB2, 0x0ED1, 0x1EF0, }; + + /** + * Create a CRC16 checksum from the bytes. implementation is from mp911de/lettuce, modified with some more + * optimizations + * + * @param bytes + * @return CRC16 as integer value + */ + public static int getCRC16(byte[] bytes) { + int crc = 0x0000; + + for (byte b : bytes) { + crc = ((crc << 8) ^ LOOKUP_TABLE[((crc >>> 8) ^ (b & 0xFF)) & 0xFF]); + } + return crc & 0xFFFF; + } + + public static int getCRC16(String key) { + return getCRC16(key.getBytes(StandardCharsets.UTF_8)); + } + + @Override + public int hashCode(String key) { + // optimization with modulo operator with power of 2 + // equivalent to getCRC16(key) % 16384 + return getCRC16(key) & (16384 - 1); + } + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/FnvHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/FnvHashStrategy.java new file mode 100644 index 00000000..fc7cea13 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/FnvHashStrategy.java @@ -0,0 +1,24 @@ +package io.github.dunwu.distributed.support; + +public class FnvHashStrategy implements HashStrategy { + + private static final long FNV_32_INIT = 2166136261L; + + private static final int FNV_32_PRIME = 16777619; + + @Override + public int hashCode(String key) { + final int p = FNV_32_PRIME; + int hash = (int) FNV_32_INIT; + for (int i = 0; i < key.length(); i++) + hash = (hash ^ key.charAt(i)) * p; + hash += hash << 13; + hash ^= hash >> 7; + hash += hash << 3; + hash ^= hash >> 17; + hash += hash << 5; + hash = Math.abs(hash); + return hash; + } + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/HashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/HashStrategy.java new file mode 100644 index 00000000..f574c86e --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/HashStrategy.java @@ -0,0 +1,7 @@ +package io.github.dunwu.distributed.support; + +public interface HashStrategy { + + int hashCode(String key); + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/JdkHashCodeStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/JdkHashCodeStrategy.java new file mode 100644 index 00000000..6541e2a4 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/JdkHashCodeStrategy.java @@ -0,0 +1,10 @@ +package io.github.dunwu.distributed.support; + +public class JdkHashCodeStrategy implements HashStrategy { + + @Override + public int hashCode(String key) { + return key.hashCode(); + } + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/KetamaHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/KetamaHashStrategy.java new file mode 100644 index 00000000..ef479aaf --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/KetamaHashStrategy.java @@ -0,0 +1,42 @@ +package io.github.dunwu.distributed.support; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class KetamaHashStrategy implements HashStrategy { + + private static MessageDigest md5Digest; + + static { + try { + md5Digest = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("MD5 not supported", e); + } + } + + @Override + public int hashCode(String key) { + byte[] bKey = computeMd5(key); + long rv = ((long) (bKey[3] & 0xFF) << 24) + | ((long) (bKey[2] & 0xFF) << 16) + | ((long) (bKey[1] & 0xFF) << 8) + | (bKey[0] & 0xFF); + return (int) (rv & 0xffffffffL); + } + + /** + * Get the md5 of the given key. + */ + public static byte[] computeMd5(String k) { + MessageDigest md5; + try { + md5 = (MessageDigest) md5Digest.clone(); + } catch (CloneNotSupportedException e) { + throw new RuntimeException("clone of MD5 not supported", e); + } + md5.update(k.getBytes()); + return md5.digest(); + } + +} diff --git a/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/MurmurHashStrategy.java b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/MurmurHashStrategy.java new file mode 100644 index 00000000..82ee1620 --- /dev/null +++ b/codes/java-distributed/java-load-balance/src/main/java/io/github/dunwu/distributed/support/MurmurHashStrategy.java @@ -0,0 +1,51 @@ +package io.github.dunwu.distributed.support; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +public class MurmurHashStrategy implements HashStrategy { + + @Override + public int hashCode(String key) { + + ByteBuffer buf = ByteBuffer.wrap(key.getBytes()); + int seed = 0x1234ABCD; + + ByteOrder byteOrder = buf.order(); + buf.order(ByteOrder.LITTLE_ENDIAN); + + long m = 0xc6a4a7935bd1e995L; + int r = 47; + + long h = seed ^ (buf.remaining() * m); + + long k; + while (buf.remaining() >= 8) { + k = buf.getLong(); + + k *= m; + k ^= k >>> r; + k *= m; + + h ^= k; + h *= m; + } + + if (buf.remaining() > 0) { + ByteBuffer finish = ByteBuffer.allocate(8).order( + ByteOrder.LITTLE_ENDIAN); + // for big-endian version, do this first: + // finish.position(8-buf.remaining()); + finish.put(buf).rewind(); + h ^= finish.getLong(); + h *= m; + } + h ^= h >>> r; + h *= m; + h ^= h >>> r; + + buf.order(byteOrder); + return (int) (h & 0xffffffffL); + } + +} diff --git a/codes/java-distributed/java-rate-limit/pom.xml b/codes/java-distributed/java-rate-limit/pom.xml new file mode 100644 index 00000000..312d7f21 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + io.github.dunwu.distributed + java-rate-limit + 1.0.0 + jar + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + redis.clients + jedis + 5.1.0 + + + cn.hutool + hutool-all + 5.8.25 + + + org.projectlombok + lombok + 1.18.30 + + + ch.qos.logback + logback-classic + 1.2.3 + true + + + diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/FixedWindowRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/FixedWindowRateLimiter.java new file mode 100644 index 00000000..0af8d142 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/FixedWindowRateLimiter.java @@ -0,0 +1,59 @@ +package io.github.dunwu.distributed.ratelimit; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 固定时间窗口限流算法 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public class FixedWindowRateLimiter implements RateLimiter { + + /** + * 允许的最大请求数 + */ + private final long maxPermits; + + /** + * 窗口期时长 + */ + private final long periodMillis; + + /** + * 窗口期截止时间 + */ + private long lastPeriodMillis; + + /** + * 请求计数 + */ + private AtomicLong count = new AtomicLong(0); + + public FixedWindowRateLimiter(long qps) { + this(qps, 1000, TimeUnit.MILLISECONDS); + } + + public FixedWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit) { + this.maxPermits = maxPermits; + this.periodMillis = timeUnit.toMillis(period); + this.lastPeriodMillis = System.currentTimeMillis() + this.periodMillis; + } + + @Override + public synchronized boolean tryAcquire(int permits) { + long now = System.currentTimeMillis(); + if (lastPeriodMillis <= now) { + this.lastPeriodMillis = now + this.periodMillis; + count = new AtomicLong(0); + } + if (count.get() + permits <= maxPermits) { + count.addAndGet(permits); + return true; + } else { + return false; + } + } + +} \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/LeakyBucketRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/LeakyBucketRateLimiter.java new file mode 100644 index 00000000..0d99a227 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/LeakyBucketRateLimiter.java @@ -0,0 +1,64 @@ +package io.github.dunwu.distributed.ratelimit; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * 漏桶限流算法 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public class LeakyBucketRateLimiter implements RateLimiter { + + /** + * QPS + */ + private final int qps; + + /** + * 桶的容量 + */ + private final long capacity; + + /** + * 计算的起始时间 + */ + private long beginTimeMillis; + + /** + * 桶中当前的水量 + */ + private final AtomicLong waterNum = new AtomicLong(0); + + public LeakyBucketRateLimiter(int qps, int capacity) { + this.qps = qps; + this.capacity = capacity; + } + + @Override + public synchronized boolean tryAcquire(int permits) { + + // 如果桶中没有水,直接放行 + if (waterNum.get() == 0) { + beginTimeMillis = System.currentTimeMillis(); + waterNum.addAndGet(permits); + return true; + } + + // 计算水量 + long leakedWaterNum = ((System.currentTimeMillis() - beginTimeMillis) / 1000) * qps; + long currentWaterNum = waterNum.get() - leakedWaterNum; + waterNum.set(Math.max(0, currentWaterNum)); + + // 重置时间 + beginTimeMillis = System.currentTimeMillis(); + + if (waterNum.get() + permits < capacity) { + waterNum.addAndGet(permits); + return true; + } else { + return false; + } + } + +} \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiter.java new file mode 100644 index 00000000..4fbc9646 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiter.java @@ -0,0 +1,13 @@ +package io.github.dunwu.distributed.ratelimit; + +/** + * 限流器 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public interface RateLimiter { + + boolean tryAcquire(int permits); + +} diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiterDemo.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiterDemo.java new file mode 100644 index 00000000..e4a50641 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RateLimiterDemo.java @@ -0,0 +1,95 @@ +package io.github.dunwu.distributed.ratelimit; + +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.RandomUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 限流器示例 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +@Slf4j +public class RateLimiterDemo { + + public static void main(String[] args) { + + // ============================================================================ + + int qps = 20; + + System.out.println("======================= 固定时间窗口限流算法 ======================="); + FixedWindowRateLimiter fixedWindowRateLimiter = new FixedWindowRateLimiter(qps); + testRateLimit(fixedWindowRateLimiter, qps); + + System.out.println("======================= 滑动时间窗口限流算法 ======================="); + SlidingWindowRateLimiter slidingWindowRateLimiter = new SlidingWindowRateLimiter(qps, 10); + testRateLimit(slidingWindowRateLimiter, qps); + + System.out.println("======================= 漏桶限流算法 ======================="); + LeakyBucketRateLimiter leakyBucketRateLimiter = new LeakyBucketRateLimiter(qps, 100); + testRateLimit(leakyBucketRateLimiter, qps); + + System.out.println("======================= 令牌桶限流算法 ======================="); + TokenBucketRateLimiter tokenBucketRateLimiter = new TokenBucketRateLimiter(qps, 100); + testRateLimit(tokenBucketRateLimiter, qps); + } + + private static void testRateLimit(RateLimiter rateLimiter, int qps) { + + AtomicInteger okNum = new AtomicInteger(0); + AtomicInteger limitNum = new AtomicInteger(0); + ExecutorService executorService = ThreadUtil.newFixedExecutor(10, "限流测试", true); + long beginTime = System.currentTimeMillis(); + + int threadNum = 4; + final CountDownLatch latch = new CountDownLatch(threadNum); + for (int i = 0; i < threadNum; i++) { + executorService.submit(() -> { + try { + batchRequest(rateLimiter, okNum, limitNum, 1000); + } catch (Exception e) { + log.error("发生异常!", e); + } finally { + latch.countDown(); + } + }); + } + + try { + latch.await(10, TimeUnit.SECONDS); + long endTime = System.currentTimeMillis(); + long gap = endTime - beginTime; + log.info("限流 QPS: {} -> 实际结果:耗时 {} ms,{} 次请求成功,{} 次请求被限流,实际 QPS: {}", + qps, gap, okNum.get(), limitNum.get(), okNum.get() * 1000 / gap); + if (okNum.get() == qps) { + log.info("限流符合预期"); + } + } catch (Exception e) { + log.error("发生异常!", e); + } finally { + executorService.shutdown(); + } + } + + private static void batchRequest(RateLimiter rateLimiter, AtomicInteger okNum, AtomicInteger limitNum, int num) + throws InterruptedException { + for (int j = 0; j < num; j++) { + if (rateLimiter.tryAcquire(1)) { + log.info("请求成功"); + okNum.getAndIncrement(); + } else { + log.info("请求限流"); + limitNum.getAndIncrement(); + } + TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(0, 10)); + } + } + +} diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisFixedWindowRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisFixedWindowRateLimiter.java new file mode 100644 index 00000000..ec5d77d9 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisFixedWindowRateLimiter.java @@ -0,0 +1,100 @@ +package io.github.dunwu.distributed.ratelimit; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 基于 Redis + Lua 实现的固定时间窗口限流算法 + * + * @author Zhang Peng + * @date 2024-01-23 + */ +public class RedisFixedWindowRateLimiter implements RateLimiter { + + private static final String REDIS_HOST = "localhost"; + + private static final int REDIS_PORT = 6379; + + private static final Jedis JEDIS; + + public static final String SCRIPT; + + static { + // Jedis 有多种构造方法,这里选用最简单的一种情况 + JEDIS = new Jedis(REDIS_HOST, REDIS_PORT); + + // 触发 ping 命令 + try { + JEDIS.ping(); + System.out.println("jedis 连接成功"); + } catch (JedisConnectionException e) { + e.printStackTrace(); + } + + SCRIPT = FileUtil.readString(ResourceUtil.getResource("scripts/fixed_window_rate_limit.lua"), + StandardCharsets.UTF_8); + } + + private final long maxPermits; + private final long periodSeconds; + private final String key; + + public RedisFixedWindowRateLimiter(long qps, String key) { + this(qps * 60, 60, TimeUnit.SECONDS, key); + } + + public RedisFixedWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit, String key) { + this.maxPermits = maxPermits; + this.periodSeconds = timeUnit.toSeconds(period); + this.key = key; + } + + @Override + public boolean tryAcquire(int permits) { + List keys = Collections.singletonList(key); + List args = CollectionUtil.newLinkedList(String.valueOf(permits), String.valueOf(periodSeconds), + String.valueOf(maxPermits)); + Object eval = JEDIS.eval(SCRIPT, keys, args); + long value = (long) eval; + return value != -1; + } + + public static void main(String[] args) throws InterruptedException { + + int qps = 20; + RateLimiter jedisFixedWindowRateLimiter = new RedisFixedWindowRateLimiter(qps, "rate:limit:20240122210000"); + + // 模拟在一分钟内,不断收到请求,限流是否有效 + int seconds = 60; + long okNum = 0L; + long total = 0L; + long beginTime = System.currentTimeMillis(); + int num = RandomUtil.randomInt(qps, 100); + for (int second = 0; second < seconds; second++) { + for (int i = 0; i < num; i++) { + total++; + if (jedisFixedWindowRateLimiter.tryAcquire(1)) { + okNum++; + System.out.println("请求成功"); + } else { + System.out.println("请求限流"); + } + } + TimeUnit.SECONDS.sleep(1); + } + long endTime = System.currentTimeMillis(); + long time = (endTime - beginTime) / 1000; + System.out.println(StrUtil.format("请求通过数:{},总请求数:{},实际 QPS:{}", okNum, total, okNum / time)); + } + +} diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisTokenBucketRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisTokenBucketRateLimiter.java new file mode 100644 index 00000000..9dd219df --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/RedisTokenBucketRateLimiter.java @@ -0,0 +1,104 @@ +package io.github.dunwu.distributed.ratelimit; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 基于 Redis + Lua 实现的令牌桶限流算法 + * + * @author Zhang Peng + * @date 2024-01-23 + */ +public class RedisTokenBucketRateLimiter implements RateLimiter { + + private static final String REDIS_HOST = "localhost"; + + private static final int REDIS_PORT = 6379; + + private static final Jedis JEDIS; + + public static final String SCRIPT; + + static { + // Jedis 有多种构造方法,这里选用最简单的一种情况 + JEDIS = new Jedis(REDIS_HOST, REDIS_PORT); + + // 触发 ping 命令 + try { + JEDIS.ping(); + System.out.println("jedis 连接成功"); + } catch (JedisConnectionException e) { + e.printStackTrace(); + } + + SCRIPT = FileUtil.readString(ResourceUtil.getResource("scripts/token_bucket_rate_limit.lua"), + StandardCharsets.UTF_8); + } + + private final long qps; + private final long capacity; + private final String tokenKey; + private final String timeKey; + + public RedisTokenBucketRateLimiter(long qps, long capacity, String tokenKey, String timeKey) { + this.qps = qps; + this.capacity = capacity; + this.tokenKey = tokenKey; + this.timeKey = timeKey; + } + + @Override + public boolean tryAcquire(int permits) { + long now = System.currentTimeMillis(); + List keys = CollectionUtil.newLinkedList(tokenKey, timeKey); + List args = CollectionUtil.newLinkedList(String.valueOf(permits), String.valueOf(qps), + String.valueOf(capacity), String.valueOf(now)); + Object eval = JEDIS.eval(SCRIPT, keys, args); + long value = (long) eval; + return value != -1; + } + + public static void main(String[] args) throws InterruptedException { + + int qps = 20; + int bucket = 100; + RedisTokenBucketRateLimiter redisTokenBucketRateLimiter = + new RedisTokenBucketRateLimiter(qps, bucket, "token:rate:limit", "token:rate:limit:time"); + + // 先将令牌桶预热令牌申请完,后续才能真实反映限流 QPS + redisTokenBucketRateLimiter.tryAcquire(bucket); + TimeUnit.SECONDS.sleep(1); + + // 模拟在一分钟内,不断收到请求,限流是否有效 + int seconds = 60; + long okNum = 0L; + long total = 0L; + long beginTime = System.currentTimeMillis(); + for (int second = 0; second < seconds; second++) { + int num = RandomUtil.randomInt(qps, 100); + for (int i = 0; i < num; i++) { + total++; + if (redisTokenBucketRateLimiter.tryAcquire(1)) { + okNum++; + System.out.println("请求成功"); + } else { + System.out.println("请求限流"); + } + } + TimeUnit.SECONDS.sleep(1); + } + long endTime = System.currentTimeMillis(); + long time = (endTime - beginTime) / 1000; + System.out.println(StrUtil.format("请求通过数:{},总请求数:{},实际 QPS:{}", okNum, total, okNum / time)); + } + +} diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/SlidingWindowRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/SlidingWindowRateLimiter.java new file mode 100644 index 00000000..a93613a2 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/SlidingWindowRateLimiter.java @@ -0,0 +1,87 @@ +package io.github.dunwu.distributed.ratelimit; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * 滑动时间窗口限流算法 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public class SlidingWindowRateLimiter implements RateLimiter { + + /** + * 允许的最大请求数 + */ + private final long maxPermits; + + /** + * 窗口期时长 + */ + private final long periodMillis; + + /** + * 分片窗口期时长 + */ + private final long shardPeriodMillis; + + /** + * 窗口期截止时间 + */ + private long lastPeriodMillis; + + /** + * 分片窗口数 + */ + private final int shardNum; + + /** + * 请求总计数 + */ + private final AtomicLong totalCount = new AtomicLong(0); + + /** + * 分片窗口计数列表 + */ + private final List countList = new LinkedList<>(); + + public SlidingWindowRateLimiter(long qps, int shardNum) { + this(qps, 1000, TimeUnit.MILLISECONDS, shardNum); + } + + public SlidingWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit, int shardNum) { + this.maxPermits = maxPermits; + this.periodMillis = timeUnit.toMillis(period); + this.lastPeriodMillis = System.currentTimeMillis(); + this.shardPeriodMillis = timeUnit.toMillis(period) / shardNum; + this.shardNum = shardNum; + for (int i = 0; i < shardNum; i++) { + countList.add(new AtomicLong(0)); + } + } + + @Override + public synchronized boolean tryAcquire(int permits) { + long now = System.currentTimeMillis(); + if (now > lastPeriodMillis) { + for (int shardId = 0; shardId < shardNum; shardId++) { + long shardCount = countList.get(shardId).get(); + totalCount.addAndGet(-shardCount); + countList.set(shardId, new AtomicLong(0)); + lastPeriodMillis += shardPeriodMillis; + } + } + int shardId = (int) (now % periodMillis / shardPeriodMillis); + if (totalCount.get() + permits <= maxPermits) { + countList.get(shardId).addAndGet(permits); + totalCount.addAndGet(permits); + return true; + } else { + return false; + } + } + +} \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/TokenBucketRateLimiter.java b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/TokenBucketRateLimiter.java new file mode 100644 index 00000000..e03e4c7d --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/java/io/github/dunwu/distributed/ratelimit/TokenBucketRateLimiter.java @@ -0,0 +1,59 @@ +package io.github.dunwu.distributed.ratelimit; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * 令牌桶限流算法 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public class TokenBucketRateLimiter implements RateLimiter { + + /** + * QPS + */ + private final long qps; + + /** + * 桶的容量 + */ + private final long capacity; + + /** + * 上一次令牌发放时间 + */ + private long endTimeMillis; + + /** + * 桶中当前的令牌数量 + */ + private final AtomicLong tokenNum = new AtomicLong(0); + + public TokenBucketRateLimiter(long qps, long capacity) { + this.qps = qps; + this.capacity = capacity; + this.endTimeMillis = System.currentTimeMillis(); + } + + @Override + public synchronized boolean tryAcquire(int permits) { + + long now = System.currentTimeMillis(); + long gap = now - endTimeMillis; + + // 计算令牌数 + long newTokenNum = (gap * qps / 1000); + long currentTokenNum = tokenNum.get() + newTokenNum; + tokenNum.set(Math.min(capacity, currentTokenNum)); + + if (tokenNum.get() < permits) { + return false; + } else { + tokenNum.addAndGet(-permits); + endTimeMillis = now; + return true; + } + } + +} \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/resources/scripts/fixed_window_rate_limit.lua b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/fixed_window_rate_limit.lua new file mode 100644 index 00000000..e0c9ad00 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/fixed_window_rate_limit.lua @@ -0,0 +1,21 @@ +-- 缓存 Key +local key = KEYS[1] +-- 访问请求数 +local permits = tonumber(ARGV[1]) +-- 过期时间 +local seconds = tonumber(ARGV[2]) +-- 限流阈值 +local limit = tonumber(ARGV[3]) + +-- 获取统计值 +local count = tonumber(redis.call('GET', key) or "0") + +if count + permits > limit then + -- 请求拒绝 + return -1 +else + -- 请求通过 + redis.call('INCRBY', key, permits) + redis.call('EXPIRE', key, seconds) + return count + permits +end \ No newline at end of file diff --git a/codes/java-distributed/java-rate-limit/src/main/resources/scripts/token_bucket_rate_limit.lua b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/token_bucket_rate_limit.lua new file mode 100644 index 00000000..541d70c9 --- /dev/null +++ b/codes/java-distributed/java-rate-limit/src/main/resources/scripts/token_bucket_rate_limit.lua @@ -0,0 +1,39 @@ +local tokenKey = KEYS[1] +local timeKey = KEYS[2] + +-- 申请令牌数 +local permits = tonumber(ARGV[1]) +-- QPS +local qps = tonumber(ARGV[2]) +-- 桶的容量 +local capacity = tonumber(ARGV[3]) +-- 当前时间(单位:毫秒) +local nowMillis = tonumber(ARGV[4]) +-- 填满令牌桶所需要的时间 +local fillTime = capacity / qps +local ttl = math.min(capacity, math.floor(fillTime * 2)) + +local currentTokenNum = tonumber(redis.call("GET", tokenKey)) +if currentTokenNum == nil then + currentTokenNum = capacity +end + +local endTimeMillis = tonumber(redis.call("GET", timeKey)) +if endTimeMillis == nil then + endTimeMillis = 0 +end + +local gap = nowMillis - endTimeMillis +local newTokenNum = math.max(0, gap * qps / 1000) +local currentTokenNum = math.min(capacity, currentTokenNum + newTokenNum) + +if currentTokenNum < permits then + -- 请求拒绝 + return -1 +else + -- 请求通过 + local finalTokenNum = currentTokenNum - permits + redis.call("SETEX", tokenKey, ttl, finalTokenNum) + redis.call("SETEX", timeKey, ttl, nowMillis) + return finalTokenNum +end diff --git a/codes/java-distributed/java-task/pom.xml b/codes/java-distributed/java-task/pom.xml new file mode 100644 index 00000000..8d7cc462 --- /dev/null +++ b/codes/java-distributed/java-task/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + + io.github.dunwu.distributed + java-distributed + 1.0.0 + + + io.github.dunwu.distributed + java-task + 1.0.0 + jar + + + UTF-8 + 1.8 + ${java.version} + ${java.version} + + + + + cn.hutool + hutool-all + + + org.projectlombok + lombok + + + ch.qos.logback + logback-classic + 1.2.3 + true + + + diff --git a/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/DelayQueueExample.java b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/DelayQueueExample.java new file mode 100644 index 00000000..d510eed1 --- /dev/null +++ b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/DelayQueueExample.java @@ -0,0 +1,52 @@ +package io.github.dunwu.local.task; + +import cn.hutool.core.date.DateUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.DelayQueue; +import java.util.concurrent.Delayed; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class DelayQueueExample { + + public static void main(String[] args) throws InterruptedException { + BlockingQueue delayQueue = new DelayQueue<>(); + long now = System.currentTimeMillis(); + delayQueue.put(new SampleTask(now + 1000)); + delayQueue.put(new SampleTask(now + 2000)); + delayQueue.put(new SampleTask(now + 3000)); + for (int i = 0; i < 3; i++) { + log.info("task 执行时间:{}", DateUtil.format(new Date(delayQueue.take().getTime()), "yyyy-MM-dd HH:mm:ss")); + } + } + + static class SampleTask implements Delayed { + + long time; + + public SampleTask(long time) { + this.time = time; + } + + public long getTime() { + return time; + } + + @Override + public int compareTo(Delayed o) { + return Long.compare(this.getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS)); + } + + @Override + public long getDelay(TimeUnit unit) { + return unit.convert(time - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + + } + +} + + diff --git a/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/ScheduledExecutorServiceExample.java b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/ScheduledExecutorServiceExample.java new file mode 100644 index 00000000..78e8f5bd --- /dev/null +++ b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/ScheduledExecutorServiceExample.java @@ -0,0 +1,37 @@ +package io.github.dunwu.local.task; + +import cn.hutool.core.date.DateUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class ScheduledExecutorServiceExample { + + public static void main(String[] args) { + // 创建一个 ScheduledExecutorService 对象,它将使用一个线程池来执行任务 + ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + + // 创建一个 Runnable 对象,这个任务将在 2 秒后执行,并且每 1 秒重复执行一次 + Runnable task = () -> { + log.info("task 执行时间:{}", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss")); + }; + + // 安排任务在 2 秒后执行,并且每 1 秒重复执行一次 + executor.scheduleAtFixedRate(task, 2, 1, TimeUnit.SECONDS); + + // 主线程等待 10 秒后结束 + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // 关闭 executor,这将停止所有正在执行的任务,并拒绝新任务的提交 + executor.shutdown(); + } + +} diff --git a/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/TimerExample.java b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/TimerExample.java new file mode 100644 index 00000000..ce1d7756 --- /dev/null +++ b/codes/java-distributed/java-task/src/main/java/io/github/dunwu/local/task/TimerExample.java @@ -0,0 +1,39 @@ +package io.github.dunwu.local.task; + +import cn.hutool.core.date.DateUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.Date; +import java.util.Timer; +import java.util.TimerTask; + +@Slf4j +public class TimerExample { + + public static void main(String[] args) { + // 创建一个 Timer 对象 + Timer timer = new Timer(); + + // 创建一个 TimerTask 对象,这个任务将在 2 秒后执行,并且每 1 秒重复执行一次 + TimerTask task = new TimerTask() { + @Override + public void run() { + log.info("task 执行时间:{}", DateUtil.format(new Date(), "yyyy-MM-dd HH:mm:ss")); + } + }; + + // 安排任务在 2 秒后执行,并且每 1 秒重复执行一次 + timer.schedule(task, 2000, 1000); + + // 主线程等待 10 秒后结束 + try { + Thread.sleep(10000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + // 取消定时器和所有已安排的任务 + timer.cancel(); + } + +} diff --git a/codes/java-distributed/pom.xml b/codes/java-distributed/pom.xml new file mode 100644 index 00000000..aa88d15d --- /dev/null +++ b/codes/java-distributed/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + io.github.dunwu.distributed + java-distributed + 1.0.0 + pom + + + java-load-balance + java-rate-limit + java-distributed-id + java-task + + + + + + org.apache.zookeeper + zookeeper + 3.9.2 + + + org.apache.curator + curator-recipes + 4.3.0 + + + redis.clients + jedis + 5.1.0 + + + cn.hutool + hutool-all + 5.8.34 + + + org.projectlombok + lombok + 1.18.30 + + + ch.qos.logback + logback-classic + 1.4.12 + true + + + + + diff --git a/codes/javaee/README.md b/codes/javaee/README.md new file mode 100644 index 00000000..7587bd07 --- /dev/null +++ b/codes/javaee/README.md @@ -0,0 +1 @@ +# JavaEE 示例代码 \ No newline at end of file diff --git a/codes/javaee/javaee-filter/pom.xml b/codes/javaee/javaee-filter/pom.xml new file mode 100644 index 00000000..a0f8f7e9 --- /dev/null +++ b/codes/javaee/javaee-filter/pom.xml @@ -0,0 +1,107 @@ + + 4.0.0 + + + io.github.dunwu.javaee + javaee + 1.0.0 + + + javaee-filter + 1.0.0 + war + javaee-filter + JavaEE 学习笔记之 Filter + + + UTF-8 + 1.7 + ${java.version} + ${java.version} + + + + + + ch.qos.logback + logback-classic + + + ch.qos.logback + logback-core + + + org.slf4j + jcl-over-slf4j + + + + + + net.coobird + thumbnailator + + + + + + commons-fileupload + commons-fileupload + + + commons-io + commons-io + + + org.apache.commons + commons-lang3 + + + + + + javax.servlet + javax.servlet-api + provided + + + + + + org.eclipse.jetty + jetty-webapp + test + + + org.eclipse.jetty + jetty-server + test + + + org.eclipse.jetty + jetty-annotations + test + + + org.eclipse.jetty + apache-jsp + test + + + + + + junit + junit + test + + + org.assertj + assertj-core + test + + + + + diff --git a/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/CacheFilter.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/CacheFilter.java new file mode 100644 index 00000000..b7bb001a --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/CacheFilter.java @@ -0,0 +1,97 @@ +package io.github.dunwu.javaee.filter; + +import io.github.dunwu.javaee.filter.wrapper.CacheResponseWrapper; + +import java.io.*; +import java.net.URLEncoder; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author Zhang Peng + * @since 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) { + 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/CharacterEncodingFilter.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/CharacterEncodingFilter.java new file mode 100644 index 00000000..7b90de06 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/CharacterEncodingFilter.java @@ -0,0 +1,65 @@ +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.*; +import javax.servlet.http.HttpServletRequest; + +/** + * @author Zhang Peng + * @since 2017/3/27. + */ +public class CharacterEncodingFilter extends MyFilter { + + private String characterEncoding; + + private boolean enabled; + + @Override + public void init(FilterConfig config) { + 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 { + + @Override + public void destroy() { + + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + + UploadRequestWrapper uploadRequest = new UploadRequestWrapper((HttpServletRequest) request); + + chain.doFilter(uploadRequest, response); + } + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + } + +} diff --git a/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/ExceptionHandlerFilter.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/ExceptionHandlerFilter.java new file mode 100644 index 00000000..aba7ca1a --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/ExceptionHandlerFilter.java @@ -0,0 +1,48 @@ +package io.github.dunwu.javaee.filter; + +import io.github.dunwu.javaee.filter.exception.AccountException; +import io.github.dunwu.javaee.filter.exception.BusinessException; +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +/** + * @author Zhang Peng + * @since 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/FilterImpl.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/FilterImpl.java new file mode 100644 index 00000000..7424572d --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/FilterImpl.java @@ -0,0 +1,36 @@ +package io.github.dunwu.javaee.filter; + +import java.io.IOException; +import javax.servlet.*; + +/** + * @author Zhang Peng + * @since 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/GZipFilter.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/GZipFilter.java new file mode 100644 index 00000000..96a39a07 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/GZipFilter.java @@ -0,0 +1,44 @@ +package io.github.dunwu.javaee.filter; + +import io.github.dunwu.javaee.filter.wrapper.GZipResponseWrapper; +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 + * @since 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/ImageRedirectFilter.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/ImageRedirectFilter.java new file mode 100644 index 00000000..9fca074a --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/ImageRedirectFilter.java @@ -0,0 +1,44 @@ +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 + * @since 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/LogFilter.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/LogFilter.java new file mode 100644 index 00000000..c57eb31e --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/LogFilter.java @@ -0,0 +1,36 @@ +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 + * @since 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/MyFilter.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/MyFilter.java new file mode 100644 index 00000000..5c6f76a3 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/MyFilter.java @@ -0,0 +1,36 @@ +package io.github.dunwu.javaee.filter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import javax.servlet.*; + +/** + * @author Zhang Peng + * @since 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) { + // 获取 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/OutputReplaceFilter.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/OutputReplaceFilter.java new file mode 100644 index 00000000..96e6996f --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/OutputReplaceFilter.java @@ -0,0 +1,58 @@ +package io.github.dunwu.javaee.filter; + +import io.github.dunwu.javaee.filter.wrapper.HttpCharacterResponseWrapper; + +import java.io.FileInputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Properties; +import javax.servlet.*; +import javax.servlet.http.HttpServletResponse; + +/** + * @author Zhang Peng + * @since 2017/3/27. + */ +public class OutputReplaceFilter extends MyFilter { + + private Properties properties = new Properties(); + + @Override + public void init(FilterConfig filterConfig) { + 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/PrivilegeFilter.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/PrivilegeFilter.java new file mode 100644 index 00000000..37ef77b8 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/PrivilegeFilter.java @@ -0,0 +1,79 @@ +package io.github.dunwu.javaee.filter; + +import io.github.dunwu.javaee.filter.exception.AccountException; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; + +/** + * @author Zhang Peng + * @since 2017/3/27. + */ +public class PrivilegeFilter extends MyFilter { + + private Properties pp = new Properties(); + + @Override + public void init(FilterConfig config) { + + // 从 初始化参数 中获取权 限配置文件 的位置 + 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/UploadFilter.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/UploadFilter.java new file mode 100644 index 00000000..3ae65193 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/UploadFilter.java @@ -0,0 +1,24 @@ +package io.github.dunwu.javaee.filter; + +import io.github.dunwu.javaee.filter.wrapper.UploadRequestWrapper; +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; + +/** + * @author Zhang Peng + * @since 2017-04-04 + */ +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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/WaterMarkFilter.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/WaterMarkFilter.java new file mode 100644 index 00000000..836b2065 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/WaterMarkFilter.java @@ -0,0 +1,49 @@ +package io.github.dunwu.javaee.filter; + +import io.github.dunwu.javaee.filter.wrapper.WaterMarkResponseWrapper; + +import java.io.IOException; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * @author Zhang Peng + * @since 2017/3/27. + */ +public class WaterMarkFilter extends MyFilter { + + // 水印图片,配置在初始化参数中 + private String waterMarkFile; + + @Override + public void init(FilterConfig filterConfig) { + 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/XSLTFilter.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/XSLTFilter.java new file mode 100644 index 00000000..2e013440 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/XSLTFilter.java @@ -0,0 +1,69 @@ +package io.github.dunwu.javaee.filter; + +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.PrintWriter; +import javax.servlet.*; +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 + * @since 2017/3/28. + */ +public class XSLTFilter extends MyFilter { + + private ServletContext servletContext; + + @Override + public void init(FilterConfig filterConfig) { + 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/exception/AccountException.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/exception/AccountException.java new file mode 100644 index 00000000..96a6d42b --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/exception/AccountException.java @@ -0,0 +1,11 @@ +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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/exception/BusinessException.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/exception/BusinessException.java new file mode 100644 index 00000000..c3662556 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/exception/BusinessException.java @@ -0,0 +1,11 @@ +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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/test/Download.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/test/Download.java new file mode 100644 index 00000000..26c115bf --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/test/Download.java @@ -0,0 +1,36 @@ +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 void main(String[] args) throws Exception { + System.out.println(getContent("http://localhost:8080/filter/book/thinkInJava.xml")); + } + + 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(); + } + +} diff --git a/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/test/GZipTest.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/test/GZipTest.java new file mode 100644 index 00000000..9b4899aa --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/test/GZipTest.java @@ -0,0 +1,34 @@ +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 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"); + } + + 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(); + } + +} diff --git a/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/CacheResponseWrapper.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/CacheResponseWrapper.java new file mode 100644 index 00000000..79d8bf0c --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/CacheResponseWrapper.java @@ -0,0 +1,44 @@ +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 + * @since 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/GZipOutputStream.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/GZipOutputStream.java new file mode 100644 index 00000000..48e1adc1 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/GZipOutputStream.java @@ -0,0 +1,74 @@ +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 + * @since 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/GZipResponseWrapper.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/GZipResponseWrapper.java new file mode 100644 index 00000000..f230b860 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/GZipResponseWrapper.java @@ -0,0 +1,61 @@ +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 + * @since 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/HttpCharacterResponseWrapper.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/HttpCharacterResponseWrapper.java new file mode 100644 index 00000000..d9798ee2 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/HttpCharacterResponseWrapper.java @@ -0,0 +1,30 @@ +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 + * @since 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/UploadRequestWrapper.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/UploadRequestWrapper.java new file mode 100644 index 00000000..612bf7c2 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/UploadRequestWrapper.java @@ -0,0 +1,105 @@ +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 + * @since 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(); + } + } + } + + public static void main(String[] args) { + + System.out.println(System.getProperties().toString().replace(", ", "\r\n")); + } + + @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); + } + +} diff --git a/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/WaterMarkOutputStream.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/WaterMarkOutputStream.java new file mode 100644 index 00000000..b94322c4 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/WaterMarkOutputStream.java @@ -0,0 +1,55 @@ +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 + * @since 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/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/WaterMarkResponseWrapper.java b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/WaterMarkResponseWrapper.java new file mode 100644 index 00000000..5f57cca4 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/java/io/github/dunwu/javaee/filter/wrapper/WaterMarkResponseWrapper.java @@ -0,0 +1,60 @@ +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 + * @since 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/javaee-filter/src/main/resources/logback.xml b/codes/javaee/javaee-filter/src/main/resources/logback.xml new file mode 100644 index 00000000..5f9e7061 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/resources/logback.xml @@ -0,0 +1,45 @@ + + + + + + + + + + %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/javatool/server/src/main/webapp/META-INF/MANIFEST.MF b/codes/javaee/javaee-filter/src/main/webapp/META-INF/MANIFEST.MF similarity index 100% rename from codes/javatool/server/src/main/webapp/META-INF/MANIFEST.MF rename to codes/javaee/javaee-filter/src/main/webapp/META-INF/MANIFEST.MF diff --git a/codes/javaee/javaee-filter/src/main/webapp/WEB-INF/logo.png b/codes/javaee/javaee-filter/src/main/webapp/WEB-INF/logo.png new file mode 100644 index 00000000..39ac67c3 Binary files /dev/null and b/codes/javaee/javaee-filter/src/main/webapp/WEB-INF/logo.png differ diff --git a/codes/javaee/javaee-filter/src/main/webapp/WEB-INF/privilege.properties b/codes/javaee/javaee-filter/src/main/webapp/WEB-INF/privilege.properties new file mode 100644 index 00000000..ab78f73c --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/WEB-INF/privilege.properties @@ -0,0 +1,8 @@ +# 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 diff --git a/codes/javaee/javaee-filter/src/main/webapp/WEB-INF/sensitive.properties b/codes/javaee/javaee-filter/src/main/webapp/WEB-INF/sensitive.properties new file mode 100644 index 00000000..cdc23204 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/WEB-INF/sensitive.properties @@ -0,0 +1,7 @@ +# \u81ea\u52a8\u66f4\u6b63 +Chna=China +www.baidu.com.cn=www.baidu.com +# \u81ea\u52a8\u66ff\u6362 +\u8272\u60c5=** +\u60c5\u8272=** +\u8d4c\u535a=** diff --git a/codes/javaee/javaee-filter/src/main/webapp/WEB-INF/web.xml b/codes/javaee/javaee-filter/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..4a7c507e --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,182 @@ + + + + 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/javaee-filter/src/main/webapp/views/images/error.gif b/codes/javaee/javaee-filter/src/main/webapp/views/images/error.gif new file mode 100644 index 00000000..b6922ac1 Binary files /dev/null and b/codes/javaee/javaee-filter/src/main/webapp/views/images/error.gif differ diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/images/mm.jpg b/codes/javaee/javaee-filter/src/main/webapp/views/images/mm.jpg new file mode 100644 index 00000000..4c6de35c Binary files /dev/null and b/codes/javaee/javaee-filter/src/main/webapp/views/images/mm.jpg differ diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/images/sunset.jpg b/codes/javaee/javaee-filter/src/main/webapp/views/images/sunset.jpg new file mode 100644 index 00000000..860f6eec Binary files /dev/null and b/codes/javaee/javaee-filter/src/main/webapp/views/images/sunset.jpg differ diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/images/winter.jpg b/codes/javaee/javaee-filter/src/main/webapp/views/images/winter.jpg new file mode 100644 index 00000000..6db32ca1 Binary files /dev/null and b/codes/javaee/javaee-filter/src/main/webapp/views/images/winter.jpg differ diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/js/dojo.js b/codes/javaee/javaee-filter/src/main/webapp/views/js/dojo.js new file mode 100644 index 00000000..d5a0fc94 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/js/dojo.js @@ -0,0 +1,6812 @@ +/* + 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 = 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 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 = {}; + 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; + + } 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(); + + } + } + } + }, + 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() + }; + + }; + 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([]), _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([]), _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') ? (String(_34e)).toString() : _34e; + var a1o = (typeof arr[x][1] == 'object') ? (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 = 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 : 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 = 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; + } + 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/javaee-filter/src/main/webapp/views/jsp/accountException.jsp b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/accountException.jsp new file mode 100644 index 00000000..308c7052 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/accountException.jsp @@ -0,0 +1,44 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> + + + + + accountException + + + + +
+ ${ message } +
+ +
+ + + + + + + + + + + + + +
Account
Password
 
+
+ + + diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/jsp/businessException.jsp b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/businessException.jsp new file mode 100644 index 00000000..54abdded --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/businessException.jsp @@ -0,0 +1,24 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> + + + + + businessException + + + + +
+ ${ message } 返回 +
+ + + diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/jsp/dispatcher.jsp b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/dispatcher.jsp new file mode 100644 index 00000000..3372bdc7 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/dispatcher.jsp @@ -0,0 +1,13 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> + + + + + ${ pageContext.request.requestURI } + + + +你正在访问 ${ pageContext.request.requestURI }?${ pageContext.request.queryString }. + + + diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/jsp/exception.jsp b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/exception.jsp new file mode 100644 index 00000000..7e581362 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/exception.jsp @@ -0,0 +1,24 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> + + + + + exception + + + + +
+ ${ message } 返回 +
+ + + diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/jsp/image.jsp b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/image.jsp new file mode 100644 index 00000000..b1608a46 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/image.jsp @@ -0,0 +1,19 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> + + + + + Insert title here + + + + +刷新 +直接访问 +
+ +request.getHeader("referer"): ${ header['referer'] } + + + diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/jsp/index.jsp b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/index.jsp new file mode 100644 index 00000000..ffc11dfa --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/index.jsp @@ -0,0 +1,52 @@ +<%@ 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/javaee-filter/src/main/webapp/views/jsp/testCacheFilter.jsp b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testCacheFilter.jsp new file mode 100644 index 00000000..77d55892 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testCacheFilter.jsp @@ -0,0 +1,71 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> + + + + + Insert title here + + + + +
+ 登录 + 登录为管理员 +
+ + + + + + + + + diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testCharacterEncodingFilter.jsp b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testCharacterEncodingFilter.jsp new file mode 100644 index 00000000..f9da90a4 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testCharacterEncodingFilter.jsp @@ -0,0 +1,28 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> + + + + + Insert title here + + + +
+您输入了:
+${ param.text }
+
+

+
+ + + diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testExceptionHandlerFilter.jsp b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testExceptionHandlerFilter.jsp new file mode 100644 index 00000000..ef1f3304 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testExceptionHandlerFilter.jsp @@ -0,0 +1,29 @@ +<%@ 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/javaee-filter/src/main/webapp/views/jsp/testGZipFilter.jsp b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testGZipFilter.jsp new file mode 100644 index 00000000..be254535 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testGZipFilter.jsp @@ -0,0 +1,66 @@ +<%@ 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); + } +%> + + + diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testOutputReplaceFilter.jsp b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testOutputReplaceFilter.jsp new file mode 100644 index 00000000..7517a03a --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testOutputReplaceFilter.jsp @@ -0,0 +1,19 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> + + + + + Insert title here + + + +Chna
+
+色情
+赌博
+情色
+
+www.baidu.com.cn
+ + + diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testUploadFilter.jsp b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testUploadFilter.jsp new file mode 100644 index 00000000..d6b602ce --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testUploadFilter.jsp @@ -0,0 +1,43 @@ +<%@ 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()); + } + %>

+ + + +
+ + + diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testWaterMarkFilter.jsp b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testWaterMarkFilter.jsp new file mode 100644 index 00000000..99c5585c --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/jsp/testWaterMarkFilter.jsp @@ -0,0 +1,16 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> + + + + + 图片过滤器示例 + + + + +刷新 +
+${ header } + + + diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/xml/book.xml b/codes/javaee/javaee-filter/src/main/webapp/views/xml/book.xml new file mode 100644 index 00000000..ab4bc9fc --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/xml/book.xml @@ -0,0 +1,6 @@ + + Bruce + Java 编程思想 + 计算机经典书籍系列丛书 + $100.00 + diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/xml/book.xsl b/codes/javaee/javaee-filter/src/main/webapp/views/xml/book.xsl new file mode 100644 index 00000000..409e24e3 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/xml/book.xsl @@ -0,0 +1,40 @@ + + + + + + + + + 图书资料: + + + + + + + + + + + + + + + + + +
作者: + +
书名: + +
类别: + +
定价: + +
+ + +
+
diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/xml/demo.xml b/codes/javaee/javaee-filter/src/main/webapp/views/xml/demo.xml new file mode 100644 index 00000000..82020867 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/xml/demo.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + 你会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/javaee-filter/src/main/webapp/views/xml/messageLog.xsl b/codes/javaee/javaee-filter/src/main/webapp/views/xml/messageLog.xsl new file mode 100644 index 00000000..715ce717 Binary files /dev/null and b/codes/javaee/javaee-filter/src/main/webapp/views/xml/messageLog.xsl differ diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/xml/thinkInJava.xml b/codes/javaee/javaee-filter/src/main/webapp/views/xml/thinkInJava.xml new file mode 100644 index 00000000..e3cb21c7 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/xml/thinkInJava.xml @@ -0,0 +1,7 @@ + + + Bruce + Java 编程思想 + 计算机经典书籍系列丛书 + $100.00 + diff --git a/codes/javaee/javaee-filter/src/main/webapp/views/xml/xml.xsl b/codes/javaee/javaee-filter/src/main/webapp/views/xml/xml.xsl new file mode 100644 index 00000000..3dd7c355 --- /dev/null +++ b/codes/javaee/javaee-filter/src/main/webapp/views/xml/xml.xsl @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/codes/javaee/javaee-filter/src/test/java/io/github/dunwu/javaee/server/FilterDemosBootstrap.java b/codes/javaee/javaee-filter/src/test/java/io/github/dunwu/javaee/server/FilterDemosBootstrap.java new file mode 100644 index 00000000..fe36788e --- /dev/null +++ b/codes/javaee/javaee-filter/src/test/java/io/github/dunwu/javaee/server/FilterDemosBootstrap.java @@ -0,0 +1,35 @@ +package io.github.dunwu.javaee.server; + +import org.eclipse.jetty.server.Server; + +/** + * 快速启动 jetty 服务器,方便测试 + * + * @author Zhang Peng + */ +public class FilterDemosBootstrap { + + // 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/javaee-filter/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java b/codes/javaee/javaee-filter/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java new file mode 100644 index 00000000..35d18b11 --- /dev/null +++ b/codes/javaee/javaee-filter/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java @@ -0,0 +1,131 @@ +package io.github.dunwu.javaee.server; + +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +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("all") +public class JettyFactory { + + public static final int IDE_ECLIPSE = 0; + + public static final int IDE_INTELLIJ = 1; + + public static final String ACTIVE_PROFILE = "spring.profiles.active"; + + public static final String DEFAULT_PROFILE = "spring.profiles.default"; + + public static final String DEVELOPMENT = "development"; + + private static final int PORT = 9527; + + 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", "tiles" }; + + private static final String WINDOWS_WEBDEFAULT_PATH = "jetty/webdefault.xml"; + + public static Server initServer() { + setProfileAsSystemProperty(DEVELOPMENT); + WebAppContext webAppContext = new WebAppContext(); + Server server = new Server(PORT); + server.setHandler(webAppContext); + return server; + } + + /** + * 在Spring启动前,设置profile的环境变量。 + */ + public static void setProfileAsSystemProperty(String profile) { + System.setProperty(ACTIVE_PROFILE, profile); + } + + 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 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. + + List list = new ArrayList<>(); + list.add(".*/[^/]*servlet-api-[^/]*\\.jar$"); + list.add(".*/javax.servlet.jsp.jstl-.*\\.jar$"); + list.add(".*/[^/]*taglibs.*\\.jar$"); + + for (String jarName : jarNames) { + String str = ".*/" + jarName + "-[^/]*\\.jar$"; + list.add(str); + } + + context.setAttribute("org.eclipse.jetty.io.github.dunwu.javaee.server.webapp.ContainerIncludeJarPattern", + StringUtils.join(list, '|')); + } + + 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"); + } + +} diff --git a/codes/javaee/javaee-filter/src/test/resources/jetty/webdefault.xml b/codes/javaee/javaee-filter/src/test/resources/jetty/webdefault.xml new file mode 100644 index 00000000..b991d44c --- /dev/null +++ b/codes/javaee/javaee-filter/src/test/resources/jetty/webdefault.xml @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + 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/javaee-jsp/pom.xml b/codes/javaee/javaee-jsp/pom.xml new file mode 100644 index 00000000..bed08254 --- /dev/null +++ b/codes/javaee/javaee-jsp/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + + + io.github.dunwu.javaee + javaee + 1.0.0 + + + javaee-jsp + 1.0.0 + war + javaee-jsp + JavaEE 学习笔记之 JSP + + + UTF-8 + 1.7 + ${java.version} + ${java.version} + + + + + + ch.qos.logback + logback-classic + + + org.slf4j + jcl-over-slf4j + + + + + commons-fileupload + commons-fileupload + + + commons-io + commons-io + + + org.apache.commons + commons-lang3 + + + + + javax.servlet + javax.servlet-api + provided + + + org.eclipse.jetty + jetty-webapp + test + + + org.eclipse.jetty + jetty-server + test + + + org.eclipse.jetty + jetty-annotations + test + + + org.eclipse.jetty + apache-jsp + test + + + + + junit + junit + test + + + org.assertj + assertj-core + test + + + + diff --git a/codes/javaee/javaee-jsp/src/main/java/com/sun/products/applet/demo/Graph.java b/codes/javaee/javaee-jsp/src/main/java/com/sun/products/applet/demo/Graph.java new file mode 100644 index 00000000..7536980a --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/java/com/sun/products/applet/demo/Graph.java @@ -0,0 +1,486 @@ +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.*; +import java.awt.event.*; +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; + + 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; + + Graph graph; + + int nnodes; + + Node nodes[] = new Node[100]; + + int nedges; + + Edge edges[] = new Edge[200]; + + Thread relaxer; + + boolean stress; + + boolean random; + + Node pick; + + boolean pickfixed; + + Image offscreen; + + Dimension offscreensize; + + Graphics offgraphics; + + GraphPanel(Graph graph) { + this.graph = graph; + addMouseListener(this); + } + + 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; + } + + 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++; + } + + @Override + 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(); + } + + @Override + 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); + } + + 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()); + } + + // 1.1 event handling + @Override + public void mouseClicked(MouseEvent e) { + } + + @Override + 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(); + } + + @Override + 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(); + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } + + @Override + public void mouseDragged(MouseEvent e) { + pick.x = e.getX(); + pick.y = e.getY(); + repaint(); + e.consume(); + } + + @Override + 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"); + + @Override + 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; + } + } + + @Override + public void destroy() { + remove(panel); + remove(controlPanel); + } + + @Override + public void start() { + panel.start(); + } + + @Override + public void stop() { + panel.stop(); + } + + @Override + 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; + } + } + } + } + + @Override + 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; + } + } + + @Override + public String getAppletInfo() { + return "Title: GraphLayout \nAuthor: "; + } + + @Override + 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/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Counter.java b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Counter.java new file mode 100644 index 00000000..2b4108d5 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Counter.java @@ -0,0 +1,15 @@ +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/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Message.java b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Message.java new file mode 100644 index 00000000..a15011f0 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Message.java @@ -0,0 +1,15 @@ +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/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Person.java b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Person.java new file mode 100644 index 00000000..a699495f --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/bean/Person.java @@ -0,0 +1,35 @@ +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/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/IpUtil.java b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/IpUtil.java new file mode 100644 index 00000000..5b3c47f9 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/IpUtil.java @@ -0,0 +1,16 @@ +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/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/IPEntry.java b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/IPEntry.java new file mode 100644 index 00000000..d662262e --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/IPEntry.java @@ -0,0 +1,51 @@ +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/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/IPSeeker.java b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/IPSeeker.java new file mode 100644 index 00000000..06fee4d4 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/IPSeeker.java @@ -0,0 +1,709 @@ +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 { + + 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; + + // ��һģʽʵ�� + private static IPSeeker instance = new IPSeeker(); + + // ������Ϊcache����ѯһ��ipʱ���Ȳ鿴cache���Լ��ٲ���Ҫ���ظ����� + private Hashtable ipCache; + + // ����ļ������� + private RandomAccessFile ipFile; + + // �ڴ�ӳ���ļ� + private MappedByteBuffer mbb; + + // ��ʼ�����Ŀ�ʼ�ͽ����ľ���ƫ�� + 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; + } + } + } + + /** + * ��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; + } + } + + /** + * @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; + } + + /** + * ��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; + } + } + + /** + * ����һ��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; + } + } + + /** + * ��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()); + } + } + + /** + * �ӵ�ǰλ�ö�ȡ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ƫ�ƴ���ȡһ����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ƫ�ƿ�ʼ����������ֽڣ�����һ�������� + * + * @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); + } + } + + /** + * ����һ���ص�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; + } + + /** + * @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λ�ö�ȡ�ĸ��ֽڵ�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; + } + + /** + * ���ڴ�ӳ���ļ��ĵ�ǰλ�ÿ�ʼ��3���ֽڶ�ȡһ��int + * + * @return + */ + private int readInt3() { + return mbb.getInt() & 0x00FFFFFF; + } + + /** + * ���ڴ�ӳ���ļ���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 ""; + } + + /** + * @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); + } + } + + /** + * ����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����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; + } + + /** + * �������������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; + } + } + + /** + * �����Ա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; + } + + /** + * �õ�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; + } + + /** + * ������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�õ������� + * + * @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; + } + } + + 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(); + } + + /** + * ����IP�õ������� + * + * @param ip IP���ַ�����ʽ + * @return �������ַ��� + */ + public String getCountry(String ip) { + return getCountry(Utils.getIpByteArrayFromString(ip)); + } + + /** + * ����IP�õ������� + * + * @param ip IP���ַ�����ʽ + * @return �������ַ��� + */ + public String getArea(String ip) { + return getArea(Utils.getIpByteArrayFromString(ip)); + } + + /** + *
+	 * ������װ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; + } + + } + +} diff --git a/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/QQWry.dat b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/QQWry.dat new file mode 100644 index 00000000..a77baf0a Binary files /dev/null and b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/QQWry.dat differ diff --git a/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/Test.java b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/Test.java new file mode 100644 index 00000000..e0ce37ce --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/Test.java @@ -0,0 +1,35 @@ +package io.github.dunwu.javaee.jsp.util.ip; + +import java.util.List; + +/** + * @author LJ-silver + */ + +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 < a.size(); i++) { + System.out.println(a.get(i).toString()); + } + } else { + System.out.println("usage:java Test ip/address yourIpString/yourAddressString"); + } + } else { + System.out.println("usage:java Test ip/address yourIpString/yourAddressString"); + } + } + +} diff --git a/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/Utils.java b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/Utils.java new file mode 100644 index 00000000..2fc31152 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/java/io/github/dunwu/javaee/jsp/util/ip/Utils.java @@ -0,0 +1,106 @@ +package io.github.dunwu.javaee.jsp.util.ip; + +/* + * Created on 2004-8-4 + * + */ + +import java.io.UnsupportedEncodingException; + +/** + * @author LJ-silver + */ +public class Utils { + + public static void main(String args[]) { + byte[] a = getIpByteArrayFromString(args[0]); + for (int i = 0; i < a.length; i++) + System.out.println(a[i]); + System.out.println(getIpStringFromBytes(a)); + } + + /** + * ��ip���ַ�����ʽ�õ��ֽ�������ʽ + * + * @param ip �ַ�����ʽ��ip + * @return �ֽ�������ʽ��ip + */ + public static byte[] getIpByteArrayFromString(String ip) { + byte[] ret = new byte[4]; + java.util.StringTokenizer st = new java.util.StringTokenizer(ip, "."); + try { + ret[0] = (byte) (Integer.parseInt(st.nextToken()) & 0xFF); + ret[1] = (byte) (Integer.parseInt(st.nextToken()) & 0xFF); + ret[2] = (byte) (Integer.parseInt(st.nextToken()) & 0xFF); + ret[3] = (byte) (Integer.parseInt(st.nextToken()) & 0xFF); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + return ret; + } + + /** + * @param ip ip���ֽ�������ʽ + * @return �ַ�����ʽ��ip + */ + public static String getIpStringFromBytes(byte[] ip) { + StringBuffer sb = new StringBuffer(); + sb.append(ip[0] & 0xFF); + sb.append('.'); + sb.append(ip[1] & 0xFF); + sb.append('.'); + sb.append(ip[2] & 0xFF); + sb.append('.'); + sb.append(ip[3] & 0xFF); + return sb.toString(); + } + + /** + * ��ԭʼ�ַ������б���ת�������ʧ�ܣ�����ԭʼ���ַ��� + * + * @param s ԭʼ�ַ��� + * @param srcEncoding Դ���뷽ʽ + * @param destEncoding Ŀ����뷽ʽ + * @return ת���������ַ�����ʧ�ܷ���ԭʼ�ַ��� + */ + public static String getString(String s, String srcEncoding, String destEncoding) { + try { + return new String(s.getBytes(srcEncoding), destEncoding); + } catch (UnsupportedEncodingException e) { + return s; + } + } + + /** + * ����ij�ֱ��뷽ʽ���ֽ�����ת�����ַ��� + * + * @param b �ֽ����� + * @param encoding ���뷽ʽ + * @return ���encoding��֧�֣�����һ��ȱʡ������ַ��� + */ + public static String getString(byte[] b, String encoding) { + try { + return new String(b, encoding); + } catch (UnsupportedEncodingException e) { + return new String(b); + } + } + + /** + * ����ij�ֱ��뷽ʽ���ֽ�����ת�����ַ��� + * + * @param b �ֽ����� + * @param offset Ҫת������ʼλ�� + * @param len Ҫת���ij��� + * @param encoding ���뷽ʽ + * @return ���encoding��֧�֣�����һ��ȱʡ������ַ��� + */ + public static String getString(byte[] b, int offset, int len, String encoding) { + try { + return new String(b, offset, len, encoding); + } catch (UnsupportedEncodingException e) { + return new String(b, offset, len); + } + } + +} diff --git a/codes/javaee/javaee-jsp/src/main/resources/logback.xml b/codes/javaee/javaee-jsp/src/main/resources/logback.xml new file mode 100644 index 00000000..677498f2 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/resources/logback.xml @@ -0,0 +1,46 @@ + + + + + + + + + + %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/javaee-jsp/src/main/webapp/META-INF/MANIFEST.MF b/codes/javaee/javaee-jsp/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 00000000..254272e1 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/WEB-INF/META-INF/MANIFEST.MF b/codes/javaee/javaee-jsp/src/main/webapp/WEB-INF/META-INF/MANIFEST.MF new file mode 100644 index 00000000..254272e1 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/WEB-INF/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/WEB-INF/resources/jsp/index.jsp b/codes/javaee/javaee-jsp/src/main/webapp/WEB-INF/resources/jsp/index.jsp new file mode 100644 index 00000000..473a1315 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/WEB-INF/resources/jsp/index.jsp @@ -0,0 +1,16 @@ +<%@ 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/javaee-jsp/src/main/webapp/WEB-INF/web.xml b/codes/javaee/javaee-jsp/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..a403cc53 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,25 @@ + + + + + HelloServlet + /examples/configuration.jsp + + message + welcome to jsp + + 1 + + + HelloServlet + /config + /config.jsp + + + + + /WEB-INF/views/jsp/index.jsp + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/break.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/break.jsp new file mode 100644 index 00000000..0c39bb74 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/break.jsp @@ -0,0 +1,26 @@ +<%@ 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/javaee-jsp/src/main/webapp/examples/02.grammar/for.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/for.jsp new file mode 100644 index 00000000..7f172314 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/for.jsp @@ -0,0 +1,54 @@ +<%@ 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/javaee-jsp/src/main/webapp/examples/02.grammar/if.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/if.jsp new file mode 100644 index 00000000..5d5d19ff --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/if.jsp @@ -0,0 +1,68 @@ +<%@ 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/javaee-jsp/src/main/webapp/examples/02.grammar/if2.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/if2.jsp new file mode 100644 index 00000000..ac691dfe --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/if2.jsp @@ -0,0 +1,17 @@ +<%@ 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 { %> +

今天不是周末

+<% } %> + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/scriptlet.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/scriptlet.jsp new file mode 100644 index 00000000..38c13ab0 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/scriptlet.jsp @@ -0,0 +1,16 @@ +<%@ 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/javaee-jsp/src/main/webapp/examples/02.grammar/switch.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/switch.jsp new file mode 100644 index 00000000..435c36b9 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/switch.jsp @@ -0,0 +1,37 @@ +<%@ 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("星期六"); + } +%> + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/while.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/while.jsp new file mode 100644 index 00000000..c35802eb --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/02.grammar/while.jsp @@ -0,0 +1,23 @@ +<%@ 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/javaee-jsp/src/main/webapp/examples/03.directive/foot.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/03.directive/foot.jsp new file mode 100644 index 00000000..62f768cd --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/03.directive/foot.jsp @@ -0,0 +1,13 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" %> + + + + + + +
+ Copyright 2017 &Zhang Peng +
+ + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/03.directive/head.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/03.directive/head.jsp new file mode 100644 index 00000000..e356ddf7 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/03.directive/head.jsp @@ -0,0 +1,20 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" %> + + + JSP 示例 + + + + + + + + + + + + + +
JSP 示例 +
首页资源文档
diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/03.directive/include.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/03.directive/include.jsp new file mode 100644 index 00000000..677945d6 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/03.directive/include.jsp @@ -0,0 +1,18 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" %> + +<%@ include file="head.jsp" %> + +

念奴娇·赤壁怀古

+

宋·苏轼

+

+

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

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

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

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

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

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

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

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

+ + <%@ include file="foot.jsp" %> diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/03.directive/page.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/03.directive/page.jsp new file mode 100644 index 00000000..4eeebb40 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/03.directive/page.jsp @@ -0,0 +1,17 @@ +<%@ 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("你好!"); +%> + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/date.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/date.jsp new file mode 100644 index 00000000..824dce57 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/date.jsp @@ -0,0 +1,4 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> +

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

diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/jspforward.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/jspforward.jsp new file mode 100644 index 00000000..968220e6 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/jspforward.jsp @@ -0,0 +1,23 @@ + + +<% + 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/javaee-jsp/src/main/webapp/examples/04.action/jspinclude.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/jspinclude.jsp new file mode 100644 index 00000000..765997b9 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/jspinclude.jsp @@ -0,0 +1,14 @@ +<%@ 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 示例

+ + + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/jspplugin.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/jspplugin.jsp new file mode 100644 index 00000000..bd6f10db --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/jspplugin.jsp @@ -0,0 +1,33 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" %> + + + plugin + + +
+ + + + + +
+ + + + + + + 您的浏览器不支持 Java Applet + + +
+ + + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/jspuseBean.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/jspuseBean.jsp new file mode 100644 index 00000000..df227703 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/jspuseBean.jsp @@ -0,0 +1,55 @@ +<%@ 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/javaee-jsp/src/main/webapp/examples/04.action/jspuseBean2.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/jspuseBean2.jsp new file mode 100644 index 00000000..d8012add --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/jspuseBean2.jsp @@ -0,0 +1,43 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" %> + + + 计数器 + + +
+ +<%-- 定义一个 session 范围内的计数器,记录个人的访问信息 --%> + + +<%-- 定义一个 application 范围内的计数器,记录所有人的访问信息 --%> + + +
+
+
+ 计数器 + + + + + + + + + +
您的访问次数: + <%-- 获取各人的访问次数 --%> + + 次 +
总共的访问次数: + <%-- 获取所有人的访问次数 --%> + + 次 +
+
+
+
+ + + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/useBean.html b/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/useBean.html new file mode 100644 index 00000000..a83a1cb0 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/04.action/useBean.html @@ -0,0 +1,42 @@ + + + + Java Bean Actions + + +
+ +
+
+
+ 请填写 Person 信息 + + + + + + + + + + + + + + + + + + +
姓名:
年龄:
性别: + Male + Female +
+ +
+
+
+
+ + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/05.implicit_object/error.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/05.implicit_object/error.jsp new file mode 100644 index 00000000..5a9640f9 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/05.implicit_object/error.jsp @@ -0,0 +1,11 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" isErrorPage="true" %> + +JSP + + +<% + out.println("程序拋出了一个异常:" + exception); +%> + + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/05.implicit_object/exception.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/05.implicit_object/exception.jsp new file mode 100644 index 00000000..3edbf87e --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/05.implicit_object/exception.jsp @@ -0,0 +1,14 @@ +<%@ 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/javaee-jsp/src/main/webapp/examples/cookie/readCookie.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/cookie/readCookie.jsp new file mode 100644 index 00000000..03e068a6 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/cookie/readCookie.jsp @@ -0,0 +1,31 @@ +<%@ 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

"); + } +%> + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/cookie/session.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/cookie/session.jsp new file mode 100644 index 00000000..f8adb69e --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/cookie/session.jsp @@ -0,0 +1,71 @@ +<%@ 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); %>
+ + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/cookie/writeCookie.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/cookie/writeCookie.jsp new file mode 100644 index 00000000..330410be --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/cookie/writeCookie.jsp @@ -0,0 +1,41 @@ +<%-- +访问本页面的形式如: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")%> +

  • +
+ + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/language.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/language.jsp new file mode 100644 index 00000000..e625db79 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/language.jsp @@ -0,0 +1,23 @@ +<%@ 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 + "
"); + %> +

+ + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/language2.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/language2.jsp new file mode 100644 index 00000000..9b93661d --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/language2.jsp @@ -0,0 +1,20 @@ +<% + // 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!

+
+ + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/localeCurrency.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/localeCurrency.jsp new file mode 100644 index 00000000..b92f0c0e --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/localeCurrency.jsp @@ -0,0 +1,20 @@ +<%@ 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); %>

+
+ + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/localeDate.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/localeDate.jsp new file mode 100644 index 00000000..916debc2 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/localeDate.jsp @@ -0,0 +1,20 @@ +<%@ 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); %>

+
+ + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/localePercent.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/localePercent.jsp new file mode 100644 index 00000000..926d7766 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/locale/localePercent.jsp @@ -0,0 +1,20 @@ +<%@ 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); %>

+
+ + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/date.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/date.jsp new file mode 100644 index 00000000..c0b67962 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/date.jsp @@ -0,0 +1,24 @@ +<%@ 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) + "

"); +%> + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/checkbox.html b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/checkbox.html new file mode 100644 index 00000000..67be6224 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/checkbox.html @@ -0,0 +1,17 @@ + + + + + Checkbox + + + +
+ 谷歌 + 百度 + 淘宝 + +
+ + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/checkbox.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/checkbox.jsp new file mode 100644 index 00000000..aea6c4a8 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/checkbox.jsp @@ -0,0 +1,23 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" + pageEncoding="UTF-8" %> + + + + + Checkbox + + +

从复选框中读取数据

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

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

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

  • +
+ + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/checkbox2.html b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/checkbox2.html new file mode 100644 index 00000000..efc913fb --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/checkbox2.html @@ -0,0 +1,17 @@ + + + + + Checkbox + + + +
+ 谷歌 + 百度 + 淘宝 + +
+ + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/checkbox2.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/checkbox2.jsp new file mode 100644 index 00000000..3339a2e7 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/checkbox2.jsp @@ -0,0 +1,29 @@ +<%@ 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 + "
+ + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/formGet.html b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/formGet.html new file mode 100644 index 00000000..4f905ebc --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/formGet.html @@ -0,0 +1,17 @@ + + + + + 表单 - Get操作 + + + +
+ 站点名: +
+ 网址: + +
+ + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/formGet.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/formGet.jsp new file mode 100644 index 00000000..095eb1e0 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/formGet.jsp @@ -0,0 +1,20 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" + pageEncoding="UTF-8" %> + + + + + 表单 - Get操作 + + +

使用 GET 方法读取数据

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

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

  • +
+ + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/formPost.html b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/formPost.html new file mode 100644 index 00000000..3bc877ab --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/formPost.html @@ -0,0 +1,17 @@ + + + + + 表单 - Post操作 + + + +
+ 站点名: +
+ 网址: + +
+ + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/formPost.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/formPost.jsp new file mode 100644 index 00000000..e74174a9 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/form/formPost.jsp @@ -0,0 +1,24 @@ +<%@ 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")%> +

  • +
+ + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/hitCount.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/hitCount.jsp new file mode 100644 index 00000000..945dbb66 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/hitCount.jsp @@ -0,0 +1,27 @@ +<%@ 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%> +

+ + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/mail/sendMail.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/mail/sendMail.jsp new file mode 100644 index 00000000..7261e11d --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/mail/sendMail.jsp @@ -0,0 +1,55 @@ +<%@ page import="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"); + %> +

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

页面重定向

+ +<% + // 重定向到新地址 + String site = "http://www.baidu.com"; + response.setStatus(response.SC_MOVED_TEMPORARILY); + response.setHeader("Location", site); +%> + + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/refresh.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/refresh.jsp new file mode 100644 index 00000000..85d34aa9 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/practice/refresh.jsp @@ -0,0 +1,30 @@ +<%@ 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"); +%> + + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/uncheck/01.helloWorld.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/uncheck/01.helloWorld.jsp new file mode 100644 index 00000000..bba59c7f --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/uncheck/01.helloWorld.jsp @@ -0,0 +1,10 @@ + + + First Jsp Programe + + +<% + out.println("Hello World!"); +%> + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/uncheck/01.helloWorld_zh.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/uncheck/01.helloWorld_zh.jsp new file mode 100644 index 00000000..b02b856b --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/uncheck/01.helloWorld_zh.jsp @@ -0,0 +1,13 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" + pageEncoding="UTF-8" %> + + + + 第一个Jsp程序 + + +<% + out.println("你好!"); +%> + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/examples/uncheck/02.life.jsp b/codes/javaee/javaee-jsp/src/main/webapp/examples/uncheck/02.life.jsp new file mode 100644 index 00000000..2e29bad8 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/examples/uncheck/02.life.jsp @@ -0,0 +1,45 @@ +<%@ 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 %> +

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

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

+ + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/images/bg-btn-blue.gif b/codes/javaee/javaee-jsp/src/main/webapp/images/bg-btn-blue.gif new file mode 100644 index 00000000..bc03f1bd Binary files /dev/null and b/codes/javaee/javaee-jsp/src/main/webapp/images/bg-btn-blue.gif differ diff --git a/codes/javaee/javaee-jsp/src/main/webapp/images/mail.gif b/codes/javaee/javaee-jsp/src/main/webapp/images/mail.gif new file mode 100644 index 00000000..0f2b0d76 Binary files /dev/null and b/codes/javaee/javaee-jsp/src/main/webapp/images/mail.gif differ diff --git a/codes/javaee/javaee-jsp/src/main/webapp/images/vertical_line.gif b/codes/javaee/javaee-jsp/src/main/webapp/images/vertical_line.gif new file mode 100644 index 00000000..65f8ee78 Binary files /dev/null and b/codes/javaee/javaee-jsp/src/main/webapp/images/vertical_line.gif differ diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/css/style.css b/codes/javaee/javaee-jsp/src/main/webapp/views/css/style.css new file mode 100644 index 00000000..2ae2e7bd --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/css/style.css @@ -0,0 +1,51 @@ +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: #FFFFFF; + font-weight: bold; + font-size: 11px; + text-align: center; + padding: .17em 0 .2em .17em; + border-style: solid; + border-width: 1px; + border-color: #99CCFF #115599 #115599 #99CCFF; + background: #6699CC url(../images/bg-btn-blue.gif) repeat-x; +} diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action.jsp new file mode 100644 index 00000000..e2353464 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action.jsp @@ -0,0 +1,7 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> + + + +这里是正文 + + \ No newline at end of file diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action/date.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action/date.jsp new file mode 100644 index 00000000..4537afec --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action/date.jsp @@ -0,0 +1,4 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> +

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

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

jsp:forward范例

+ + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action/getProperty.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action/getProperty.jsp new file mode 100644 index 00000000..78e1c59c --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action/getProperty.jsp @@ -0,0 +1,19 @@ +<%@ 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/javaee-jsp/src/main/webapp/views/jsp/action/include.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action/include.jsp new file mode 100644 index 00000000..cdb036a0 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action/include.jsp @@ -0,0 +1,12 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> + + + + + jsp:include范例 + + +

include 动作实例

+ + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action/useBean.html b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action/useBean.html new file mode 100644 index 00000000..e087b551 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action/useBean.html @@ -0,0 +1,38 @@ + + + Java Bean Actions + + + + +
+
+
+
+ 请填写 Person 信息 + + + + + + + + + + + + + + + + + +
姓名:
年龄:
性别:Male Female +
+
+
+
+
+ + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action/useBean.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action/useBean.jsp new file mode 100644 index 00000000..7d7d1850 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/action/useBean.jsp @@ -0,0 +1,53 @@ +<%@ 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/javaee-jsp/src/main/webapp/views/jsp/application.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/application.jsp new file mode 100644 index 00000000..655a26b4 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/application.jsp @@ -0,0 +1,15 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" %> +<% + out.getBufferSize(); + + out.println(request); + + +%> + +JSP Scriptlets + + + + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/break.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/break.jsp new file mode 100644 index 00000000..72ab9510 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/break.jsp @@ -0,0 +1,27 @@ +<%@ 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/javaee-jsp/src/main/webapp/views/jsp/comment.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/comment.jsp new file mode 100644 index 00000000..457d8ad9 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/comment.jsp @@ -0,0 +1,21 @@ +<%@ 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/javaee-jsp/src/main/webapp/views/jsp/counter.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/counter.jsp new file mode 100644 index 00000000..0c7095a8 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/counter.jsp @@ -0,0 +1,43 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" %> + + + 计数器 + + +
+ +<%-- 定义一个 session 范围内的计数器 记录个人访问信息 --%> + + +<%-- 定义一个 application 范围内的计数器 记录所有人的访问信息 --%> + + +
+
+
+ 计数器 + + + + + + + + + +
您的访问次数: + <%-- 获取个人的 访问次数 --%> + + 次 +
总共的访问次数: + <%-- 获取所有人的 访问次数 --%> + + 次 +
+
+
+
+ + + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive.jsp new file mode 100644 index 00000000..21f32e35 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive.jsp @@ -0,0 +1,4 @@ +<%@page contentType="image/jpeg" %> + + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/include/foot.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/include/foot.jsp new file mode 100644 index 00000000..a86f2eb7 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/include/foot.jsp @@ -0,0 +1,13 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" %> + + + + + + +
+ Copyright 2007-2010 ©Helloweenvsfei +
+ + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/include/head.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/include/head.jsp new file mode 100644 index 00000000..479ac0d7 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/include/head.jsp @@ -0,0 +1,23 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" %> + + + JSP指令include范例 + + + + + + + + + + + + + + + + + + +
JSP指令include范例
首页资源文档下载关于邮件社区
diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/include/include.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/include/include.jsp new file mode 100644 index 00000000..addd936c --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/include/include.jsp @@ -0,0 +1,15 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" %> + +<%@ include file="head.jsp" %> +

+

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

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

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

你好啊

+

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

+ + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/page/errorPage.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/page/errorPage.jsp new file mode 100644 index 00000000..a63836ae --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/page/errorPage.jsp @@ -0,0 +1,13 @@ + +<%@ page language="java" errorPage="isErrorPage.jsp" pageEncoding="UTF-8" %> + + + JSP指令page-属性errorPage + + +<% + //这行代码肯定会出错,因为除数是0,一运行就会抛出异常 + int x = 1 / 0; +%> + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/page/isErrorPage.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/page/isErrorPage.jsp new file mode 100644 index 00000000..db80567d --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/directive/page/isErrorPage.jsp @@ -0,0 +1,12 @@ + +<%@ page language="java" pageEncoding="UTF-8" isErrorPage="true" %> + + + JSP + + +<% + out.println("程序抛出了一个异常:" + exception); +%> + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/el.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/el.jsp new file mode 100644 index 00000000..d58a74a1 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/el.jsp @@ -0,0 +1,26 @@ +<%@ page language="java" pageEncoding="UTF-8" %> + +<%! + String ss1 = "ss String"; +%> +<% +%> + + + My JSP 'el.jsp' starting page + + + + + + + + + + +${not (1==2) } + + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/error.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/error.jsp new file mode 100644 index 00000000..ae1fe636 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/error.jsp @@ -0,0 +1,11 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" isErrorPage="true" %> + + + JSP + + +<% + out.println("程序抛出了一个异常:" + exception); +%> + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/exception.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/exception.jsp new file mode 100644 index 00000000..4abf68c2 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/exception.jsp @@ -0,0 +1,14 @@ +<%@ 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/javaee-jsp/src/main/webapp/views/jsp/grammar/for.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/grammar/for.jsp new file mode 100644 index 00000000..860e0467 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/grammar/for.jsp @@ -0,0 +1,53 @@ +<%@ 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 < 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/javaee-jsp/src/main/webapp/views/jsp/grammar/if.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/grammar/if.jsp new file mode 100644 index 00000000..a9f1bfb8 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/grammar/if.jsp @@ -0,0 +1,48 @@ +<%@ 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/javaee-jsp/src/main/webapp/views/jsp/grammar/while.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/grammar/while.jsp new file mode 100644 index 00000000..a8a2ce6e --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/grammar/while.jsp @@ -0,0 +1,23 @@ +<%@ 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/javaee-jsp/src/main/webapp/views/jsp/greeting.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/greeting.jsp new file mode 100644 index 00000000..24ea2f01 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/greeting.jsp @@ -0,0 +1,44 @@ +<%@ page language="java" import="java.util.Calendar" contentType="text/html; charset=utf-8" %> +<%@ page import="java.util.Locale" %> +<% + 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/javaee-jsp/src/main/webapp/views/jsp/index.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/index.jsp new file mode 100644 index 00000000..f0dded67 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/index.jsp @@ -0,0 +1,25 @@ +<%@ page language="java" 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/javaee-jsp/src/main/webapp/views/jsp/life.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/life.jsp new file mode 100644 index 00000000..b80202bf --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/life.jsp @@ -0,0 +1,15 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> +<%! + +%> + + + + + + Insert title here + + + + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/method.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/method.jsp new file mode 100644 index 00000000..bfc8cda9 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/method.jsp @@ -0,0 +1,84 @@ +<%@ 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/javaee-jsp/src/main/webapp/views/jsp/plugin.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/plugin.jsp new file mode 100644 index 00000000..bd6f10db --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/plugin.jsp @@ -0,0 +1,33 @@ +<%@ page language="java" contentType="text/html; charset=utf-8" %> + + + plugin + + +
+ + + + + +
+ + + + + + + 您的浏览器不支持 Java Applet + + +
+ + + + diff --git a/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/return.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/return.jsp new file mode 100644 index 00000000..e064ddf6 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/return.jsp @@ -0,0 +1,27 @@ +<%@ 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/javaee-jsp/src/main/webapp/views/jsp/scriptlet.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/scriptlet.jsp new file mode 100644 index 00000000..5197d1ce --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/scriptlet.jsp @@ -0,0 +1,17 @@ +<%@ 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/javaee-jsp/src/main/webapp/views/jsp/taglib.jsp b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/taglib.jsp new file mode 100644 index 00000000..e0560793 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/main/webapp/views/jsp/taglib.jsp @@ -0,0 +1,14 @@ +<%@ 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/javaee-jsp/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java b/codes/javaee/javaee-jsp/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java new file mode 100644 index 00000000..43d2fe1d --- /dev/null +++ b/codes/javaee/javaee-jsp/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java @@ -0,0 +1,114 @@ +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 { + + public static final int IDE_ECLIPSE = 0; + + public static final int IDE_INTELLIJ = 1; + + 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 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 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, '|')); + } + + 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"); + } + +} diff --git a/codes/javaee/javaee-jsp/src/test/java/io/github/dunwu/javaee/server/Profiles.java b/codes/javaee/javaee-jsp/src/test/java/io/github/dunwu/javaee/server/Profiles.java new file mode 100644 index 00000000..dcfd3e39 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/test/java/io/github/dunwu/javaee/server/Profiles.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * 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/javaee-jsp/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java b/codes/javaee/javaee-jsp/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java new file mode 100644 index 00000000..049c8802 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java @@ -0,0 +1,35 @@ +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/javaee-jsp/src/test/resources/jetty/webdefault.xml b/codes/javaee/javaee-jsp/src/test/resources/jetty/webdefault.xml new file mode 100644 index 00000000..b991d44c --- /dev/null +++ b/codes/javaee/javaee-jsp/src/test/resources/jetty/webdefault.xml @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + 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/javaee-jsp/src/test/resources/logback.xml b/codes/javaee/javaee-jsp/src/test/resources/logback.xml new file mode 100644 index 00000000..5b969d92 --- /dev/null +++ b/codes/javaee/javaee-jsp/src/test/resources/logback.xml @@ -0,0 +1,46 @@ + + + + + + + + + + %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/javaee-jstl/pom.xml b/codes/javaee/javaee-jstl/pom.xml new file mode 100644 index 00000000..74cf36a0 --- /dev/null +++ b/codes/javaee/javaee-jstl/pom.xml @@ -0,0 +1,118 @@ + + + 4.0.0 + + + io.github.dunwu.javaee + javaee + 1.0.0 + + + + io.github.dunwu + javaee-jstl + 1.0.0 + war + + + + + javaee-jstl + JavaEE 学习笔记之 jstl + + + + + + UTF-8 + 1.7 + ${java.version} + ${java.version} + + + + + + ch.qos.logback + logback-classic + + + org.slf4j + jcl-over-slf4j + + + + + + org.apache.commons + commons-lang3 + + + + + + javax.servlet + javax.servlet-api + provided + + + javax.servlet.jsp + javax.servlet.jsp-api + provided + + + javax.servlet + jstl + 1.2 + + + + + + org.eclipse.jetty + jetty-webapp + test + + + org.eclipse.jetty + jetty-annotations + test + + + org.eclipse.jetty + apache-jsp + test + + + org.eclipse.jetty + apache-jstl + test + + + + + + org.assertj + assertj-core + test + + + + + + + xalan + xalan + 2.7.2 + + + xerces + xercesImpl + 2.11.0 + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/java/io/github/dunwu/javaee/bean/Person.java b/codes/javaee/javaee-jstl/src/main/java/io/github/dunwu/javaee/bean/Person.java new file mode 100644 index 00000000..36943d29 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/java/io/github/dunwu/javaee/bean/Person.java @@ -0,0 +1,107 @@ +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/javaee-jstl/src/main/java/io/github/dunwu/javaee/util/Example.java b/codes/javaee/javaee-jstl/src/main/java/io/github/dunwu/javaee/util/Example.java new file mode 100644 index 00000000..70c961ef --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/java/io/github/dunwu/javaee/util/Example.java @@ -0,0 +1,14 @@ +package io.github.dunwu.javaee.util; + +import java.util.ListResourceBundle; + +public class Example extends ListResourceBundle { + + static final Object[][] contents = { { "count.one", "一" }, { "count.two", "二" }, { "count.three", "三" }, }; + + @Override + public Object[][] getContents() { + return contents; + } + +} diff --git a/codes/javaee/javaee-jstl/src/main/java/io/github/dunwu/javaee/util/Example_es_ES.java b/codes/javaee/javaee-jstl/src/main/java/io/github/dunwu/javaee/util/Example_es_ES.java new file mode 100644 index 00000000..7712df98 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/java/io/github/dunwu/javaee/util/Example_es_ES.java @@ -0,0 +1,13 @@ +package io.github.dunwu.javaee.util; + +import java.util.ListResourceBundle; + +public class Example_es_ES extends ListResourceBundle { + + static final Object[][] contents = { { "count.one", "one" }, { "count.two", "two" }, { "count.three", "three" }, }; + + public Object[][] getContents() { + return contents; + } + +} diff --git a/codes/javaee/javaee-jstl/src/main/java/io/github/dunwu/javaee/util/Pagination.java b/codes/javaee/javaee-jstl/src/main/java/io/github/dunwu/javaee/util/Pagination.java new file mode 100644 index 00000000..e31e0526 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/java/io/github/dunwu/javaee/util/Pagination.java @@ -0,0 +1,204 @@ +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(); + } + + /** + * 生成分页信息 包括第一页,上一页,下一页,最后一页等等。 + */ + 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(); + } + + private void calculate() { + pageCount = (recordCount + pageSize - 1) / pageSize; + + firstResult = (pageNum - 1) * pageSize; + } + + 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/javaee-jstl/src/main/resources/init.sql b/codes/javaee/javaee-jstl/src/main/resources/init.sql new file mode 100644 index 00000000..562a8d72 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/resources/init.sql @@ -0,0 +1,45 @@ +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/javaee-jstl/src/main/resources/logback.xml b/codes/javaee/javaee-jstl/src/main/resources/logback.xml new file mode 100644 index 00000000..5985be1e --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/resources/logback.xml @@ -0,0 +1,46 @@ + + + + + + + + + + %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/javaee-jstl/src/main/resources/messages.properties b/codes/javaee/javaee-jstl/src/main/resources/messages.properties new file mode 100644 index 00000000..8530dcc4 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/resources/messages.properties @@ -0,0 +1,2 @@ +prompt.hello=Hello, "{0}". +prompt.greeting=Nice to meet you. diff --git a/codes/javaee/javaee-jstl/src/main/resources/messages_zh_CN.properties b/codes/javaee/javaee-jstl/src/main/resources/messages_zh_CN.properties new file mode 100644 index 00000000..1cec3305 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/resources/messages_zh_CN.properties @@ -0,0 +1,2 @@ +prompt.hello=\u4f60\u597d, "{0}". +prompt.greeting=\u5f88\u9ad8\u5174\u89c1\u5230\u4f60. diff --git a/codes/javaee/javaee-jstl/src/main/resources/sql/create_employees.sql b/codes/javaee/javaee-jstl/src/main/resources/sql/create_employees.sql new file mode 100644 index 00000000..707cd5cb --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/resources/sql/create_employees.sql @@ -0,0 +1,12 @@ +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/javaee-jstl/src/main/resources/sql/create_students.sql b/codes/javaee/javaee-jstl/src/main/resources/sql/create_students.sql new file mode 100644 index 00000000..a7a434d6 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/resources/sql/create_students.sql @@ -0,0 +1,12 @@ +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/javaee-jstl/src/main/webapp/WEB-INF/web.xml b/codes/javaee/javaee-jstl/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..a403cc53 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,25 @@ + + + + + HelloServlet + /examples/configuration.jsp + + message + welcome to jsp + + 1 + + + HelloServlet + /config + /config.jsp + + + + + /WEB-INF/views/jsp/index.jsp + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_catch.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_catch.jsp new file mode 100644 index 00000000..87fa389f --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_catch.jsp @@ -0,0 +1,20 @@ +<%@ 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}

+
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_choose.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_choose.jsp new file mode 100644 index 00000000..898147dc --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_choose.jsp @@ -0,0 +1,23 @@ +<%@ 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 标签实例 + + + +

你的工资为 :

+ + + 太惨了。 + + + 不错的薪水,还能生活。 + + + 什么都没有。 + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_forEach.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_forEach.jsp new file mode 100644 index 00000000..57b8421e --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_forEach.jsp @@ -0,0 +1,13 @@ +<%@ 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

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_forTokens.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_forTokens.jsp new file mode 100644 index 00000000..046f4c49 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_forTokens.jsp @@ -0,0 +1,13 @@ +<%@ 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 标签实例 + + + +

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_if.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_if.jsp new file mode 100644 index 00000000..4abf6c52 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_if.jsp @@ -0,0 +1,14 @@ +<%@ 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 标签实例 + + + + +

我的工资为:

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_import.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_import.jsp new file mode 100644 index 00000000..98562585 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_import.jsp @@ -0,0 +1,12 @@ +<%@ 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 标签实例 + + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_out.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_out.jsp new file mode 100644 index 00000000..ab88b66f --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_out.jsp @@ -0,0 +1,20 @@ +<%@ 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,则输出该默认值
+ + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_param.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_param.jsp new file mode 100644 index 00000000..95438a73 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_param.jsp @@ -0,0 +1,21 @@ +<%@ 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发送两个参数。--%> + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_redirect.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_redirect.jsp new file mode 100644 index 00000000..fbfbf835 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_redirect.jsp @@ -0,0 +1,11 @@ +<%@ 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 标签实例 + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_remove.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_remove.jsp new file mode 100644 index 00000000..5d53732b --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_remove.jsp @@ -0,0 +1,14 @@ +<%@ 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 变量后的值:

+ + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_set.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_set.jsp new file mode 100644 index 00000000..e78c3bca --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_set.jsp @@ -0,0 +1,12 @@ +<%@ 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 标签实例 + + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_url.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_url.jsp new file mode 100644 index 00000000..d0e892cf --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/core/c_url.jsp @@ -0,0 +1,14 @@ +<%@ 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> 标签生成。 + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_bundle.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_bundle.jsp new file mode 100644 index 00000000..b6e3211d --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_bundle.jsp @@ -0,0 +1,18 @@ +<%@ 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/javaee-jstl/src/main/webapp/examples/fmt/fmt_formatDate.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_formatDate.jsp new file mode 100644 index 00000000..958f062c --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_formatDate.jsp @@ -0,0 +1,33 @@ +<%@ 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):

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_formatNumber.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_formatNumber.jsp new file mode 100644 index 00000000..529449bb --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_formatNumber.jsp @@ -0,0 +1,33 @@ +<%@ 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):

+

美元 : + +

+ + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_message.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_message.jsp new file mode 100644 index 00000000..cac1b759 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_message.jsp @@ -0,0 +1,26 @@ +<%@ 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/javaee-jstl/src/main/webapp/examples/fmt/fmt_parseDate.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_parseDate.jsp new file mode 100644 index 00000000..a6d5241c --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_parseDate.jsp @@ -0,0 +1,19 @@ +<%@ 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 标签 + + +

日期解析:

+ + + +

解析后的日期为:

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_parseNumber.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_parseNumber.jsp new file mode 100644 index 00000000..4918be0d --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_parseNumber.jsp @@ -0,0 +1,20 @@ +<%@ 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) :

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_requestEncoding.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_requestEncoding.jsp new file mode 100644 index 00000000..e81f2b3f --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_requestEncoding.jsp @@ -0,0 +1,20 @@ +<%@ 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/javaee-jstl/src/main/webapp/examples/fmt/fmt_setLocale.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_setLocale.jsp new file mode 100644 index 00000000..6941dd40 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_setLocale.jsp @@ -0,0 +1,26 @@ +<%@ 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/javaee-jstl/src/main/webapp/examples/fmt/fmt_setTimeZone.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_setTimeZone.jsp new file mode 100644 index 00000000..e05a7959 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_setTimeZone.jsp @@ -0,0 +1,18 @@ +<%@ 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:

+ + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_timeZone.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_timeZone.jsp new file mode 100644 index 00000000..adb6f13b --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/fmt/fmt_timeZone.jsp @@ -0,0 +1,42 @@ +<%@ 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: + + + +

+
+ + + + + +
+ + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_contains.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_contains.jsp new file mode 100644 index 00000000..9ec50047 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_contains.jsp @@ -0,0 +1,24 @@ +<%@ 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 +

+ + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_containsIgnoreCase.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_containsIgnoreCase.jsp new file mode 100644 index 00000000..512104b7 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_containsIgnoreCase.jsp @@ -0,0 +1,24 @@ +<%@ 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 +

+ + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_endsWith.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_endsWith.jsp new file mode 100644 index 00000000..8ec96d0e --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_endsWith.jsp @@ -0,0 +1,24 @@ +<%@ 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 结尾 +

+ + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_escapeXml.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_escapeXml.jsp new file mode 100644 index 00000000..d9509b06 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_escapeXml.jsp @@ -0,0 +1,23 @@ +<%@ 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}

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_indexOf.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_indexOf.jsp new file mode 100644 index 00000000..d4896f7a --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_indexOf.jsp @@ -0,0 +1,18 @@ +<%@ 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")}

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_join.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_join.jsp new file mode 100644 index 00000000..aba2d96e --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_join.jsp @@ -0,0 +1,18 @@ +<%@ 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}

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_length.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_length.jsp new file mode 100644 index 00000000..0361946a --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_length.jsp @@ -0,0 +1,19 @@ +<%@ 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)}

+ + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_replace.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_replace.jsp new file mode 100644 index 00000000..e5931747 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_replace.jsp @@ -0,0 +1,19 @@ +<%@ 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}

+ + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_split.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_split.jsp new file mode 100644 index 00000000..5ea82273 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_split.jsp @@ -0,0 +1,23 @@ +<%@ 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}

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_startsWith.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_startsWith.jsp new file mode 100644 index 00000000..9879a840 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_startsWith.jsp @@ -0,0 +1,21 @@ +<%@ 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 开头

+
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_substring.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_substring.jsp new file mode 100644 index 00000000..55f74fc4 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_substring.jsp @@ -0,0 +1,17 @@ +<%@ 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}

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_substringAfter.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_substringAfter.jsp new file mode 100644 index 00000000..0ec7b3f4 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_substringAfter.jsp @@ -0,0 +1,17 @@ +<%@ 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}

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_substringBefore.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_substringBefore.jsp new file mode 100644 index 00000000..c6967814 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_substringBefore.jsp @@ -0,0 +1,17 @@ +<%@ 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}

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_toLowerCase.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_toLowerCase.jsp new file mode 100644 index 00000000..804d856d --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_toLowerCase.jsp @@ -0,0 +1,17 @@ +<%@ 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}

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_toUpperCase.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_toUpperCase.jsp new file mode 100644 index 00000000..3ea5988a --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_toUpperCase.jsp @@ -0,0 +1,17 @@ +<%@ 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}

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_trim.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_trim.jsp new file mode 100644 index 00000000..1ac0e961 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/function/fn_trim.jsp @@ -0,0 +1,19 @@ +<%@ 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}

+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_dateParam.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_dateParam.jsp new file mode 100644 index 00000000..a24b0770 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_dateParam.jsp @@ -0,0 +1,52 @@ +<%@ 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
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_param.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_param.jsp new file mode 100644 index 00000000..4c53b6d6 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_param.jsp @@ -0,0 +1,47 @@ +<%@ 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
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_query.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_query.jsp new file mode 100644 index 00000000..41d515fe --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_query.jsp @@ -0,0 +1,40 @@ +<%@ 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
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_setDataSource.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_setDataSource.jsp new file mode 100644 index 00000000..73986187 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_setDataSource.jsp @@ -0,0 +1,18 @@ +<%@ 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 示例 + + + + + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_transaction.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_transaction.jsp new file mode 100644 index 00000000..f792744a --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_transaction.jsp @@ -0,0 +1,60 @@ +<%@ 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
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_update.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_update.jsp new file mode 100644 index 00000000..651ae8d4 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/sql/sql_update.jsp @@ -0,0 +1,44 @@ +<%@ 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
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/bundle.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/bundle.jsp new file mode 100644 index 00000000..77cd39f6 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/bundle.jsp @@ -0,0 +1,41 @@ +<%@ 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 + + + + + + + + + +
+ + + +
+ + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/catch.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/catch.jsp new file mode 100644 index 00000000..4a502a96 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/catch.jsp @@ -0,0 +1,26 @@ +<%@ 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 } + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/choose.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/choose.jsp new file mode 100644 index 00000000..69d457a7 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/choose.jsp @@ -0,0 +1,21 @@ +<%@ 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 标签的输出 + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/contains.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/contains.jsp new file mode 100644 index 00000000..98e1b491 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/contains.jsp @@ -0,0 +1,35 @@ +<%@ 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 操作系统 +。 + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/containsIgnoreCase.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/containsIgnoreCase.jsp new file mode 100644 index 00000000..f63fc4c2 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/containsIgnoreCase.jsp @@ -0,0 +1,33 @@ +<%@ 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 操作系统 +。 + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/dateParam.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/dateParam.jsp new file mode 100644 index 00000000..71223df1 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/dateParam.jsp @@ -0,0 +1,80 @@ +<%@ 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] }
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/endsWith.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/endsWith.jsp new file mode 100644 index 00000000..588a50b6 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/endsWith.jsp @@ -0,0 +1,70 @@ +<%@ 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 数据库脚本文件 + + + +
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/escapeXml.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/escapeXml.jsp new file mode 100644 index 00000000..fb35be08 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/escapeXml.jsp @@ -0,0 +1,21 @@ +<%@ 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) } +
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/fmt.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/fmt.jsp new file mode 100644 index 00000000..c237093e --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/fmt.jsp @@ -0,0 +1,14 @@ +<%@ 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 + + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/fn.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/fn.jsp new file mode 100644 index 00000000..cfa2276c --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/fn.jsp @@ -0,0 +1,14 @@ +<%@ 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 + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/forEach.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/forEach.jsp new file mode 100644 index 00000000..f8eeeb8f --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/forEach.jsp @@ -0,0 +1,76 @@ +<%@ 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 }
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/forEachWithList.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/forEachWithList.jsp new file mode 100644 index 00000000..e4e54bce --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/forEachWithList.jsp @@ -0,0 +1,196 @@ +<%@ page language="java" import="com.helloweenvsfei.jstl.bean.*, java.util.ArrayList" + contentType="text/html; charset=UTF-8" %> +<%@ page import="java.util.List" %> +<%@ 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 }
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/forTokens.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/forTokens.jsp new file mode 100644 index 00000000..f0c9e9cd --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/forTokens.jsp @@ -0,0 +1,39 @@ +<%@ 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 }
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/formatDate.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/formatDate.jsp new file mode 100644 index 00000000..bf224c8d --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/formatDate.jsp @@ -0,0 +1,77 @@ +<%@ 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.lang.reflect.Field" %> +<%@page import="java.util.ArrayList" %> +<%@page import="java.util.List" %> +<%@page import="java.util.Locale" %> + + + + Insert title here + + + + +<% + Field[] field = Locale.class.getFields(); + + List localeList = new ArrayList(); + + for (int i = 0; i < field.length; i++) { + if (field[i].getType().equals(Locale.class)) { + localeList.add((Locale) field[i].get(null)); + } + } + + request.setAttribute("localeList", localeList); +%> + + + + + + + + + + + + + + + + + + + + + + +
LocaleDate and TimeNumbercurrency
${ locale }
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/formatNumber.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/formatNumber.jsp new file mode 100644 index 00000000..55306342 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/formatNumber.jsp @@ -0,0 +1,89 @@ +<%@ 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.lang.reflect.Field" %> +<%@page import="java.util.ArrayList" %> +<%@page import="java.util.List" %> +<%@page import="java.util.Locale" %> + + + + Insert title here + + + + +<% + Field[] field = Locale.class.getFields(); + + List localeList = new ArrayList(); + + for (int i = 0; i < field.length; i++) { + if (field[i].getType().equals(Locale.class)) { + localeList.add((Locale) field[i].get(null)); + } + } + + request.setAttribute("localeList", localeList); + + double[] numbers = {0, 10000, 55.0, -123.2568}; + request.setAttribute("numbers", numbers); +%> + + + +当前格式: + +  + + + ${ locale }  + + + + + + + + + + + + + + + + + + + +
数字原值数字格式货币格式百分数格式
${ number }
+ + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/if.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/if.jsp new file mode 100644 index 00000000..7e8910e1 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/if.jsp @@ -0,0 +1,56 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + + + + + + + +
+ + + 添加操作 + + + + + + + + + +
帐号
真实姓名
+
+ + 修改操作 + + + + + + + + + +
帐号
真实姓名
+
+ + + + + +
+ + +
+ + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/import.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/import.jsp new file mode 100644 index 00000000..d1c07771 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/import.jsp @@ -0,0 +1,25 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + + + Insert title here + + + + + + + +Baidu 的源代码为: + + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/index.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/index.jsp new file mode 100644 index 00000000..938d29b0 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/index.jsp @@ -0,0 +1,16 @@ +<%@ 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 + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/indexOf.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/indexOf.jsp new file mode 100644 index 00000000..7d8293ab --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/indexOf.jsp @@ -0,0 +1,16 @@ +<%@ 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', '.') } + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/join.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/join.jsp new file mode 100644 index 00000000..29971147 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/join.jsp @@ -0,0 +1,20 @@ +<%@ 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, '; ') } + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/length.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/length.jsp new file mode 100644 index 00000000..1972e925 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/length.jsp @@ -0,0 +1,18 @@ +<%@ 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) }
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/out.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/out.jsp new file mode 100644 index 00000000..a72e6d0c --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/out.jsp @@ -0,0 +1,14 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + + + Insert title here + + + +action 参数为: + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/param.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/param.jsp new file mode 100644 index 00000000..e378bd06 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/param.jsp @@ -0,0 +1,17 @@ +<%@ page language="java" contentType="text/html; charset=GBK" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + + + Insert title here + + + + + + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/parse.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/parse.jsp new file mode 100644 index 00000000..301b736e --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/parse.jsp @@ -0,0 +1,51 @@ +<%@ 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 + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/parseDate.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/parseDate.jsp new file mode 100644 index 00000000..56aba204 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/parseDate.jsp @@ -0,0 +1,16 @@ +<%@ 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 + + + + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/parseNumber.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/parseNumber.jsp new file mode 100644 index 00000000..8c6bc209 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/parseNumber.jsp @@ -0,0 +1,16 @@ +<%@ 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 + + + +
+
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/query.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/query.jsp new file mode 100644 index 00000000..a1428b47 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/query.jsp @@ -0,0 +1,57 @@ +<%@ 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'] }
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/queryPagination.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/queryPagination.jsp new file mode 100644 index 00000000..64cde5e5 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/queryPagination.jsp @@ -0,0 +1,79 @@ +<%@ 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 } + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/queryReflect.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/queryReflect.jsp new file mode 100644 index 00000000..43e26dbe --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/queryReflect.jsp @@ -0,0 +1,75 @@ +<%@ 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 }
+
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/redirect.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/redirect.jsp new file mode 100644 index 00000000..8ac64b11 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/redirect.jsp @@ -0,0 +1,14 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + + + Insert title here + + + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/remove.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/remove.jsp new file mode 100644 index 00000000..74a5e512 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/remove.jsp @@ -0,0 +1,21 @@ +<%@ 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 没有被删除' } + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/requestEncoding.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/requestEncoding.jsp new file mode 100644 index 00000000..8c62bad5 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/requestEncoding.jsp @@ -0,0 +1,26 @@ +<%@ 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 + + + + + + +
+ 关键字: +

+ +
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/set.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/set.jsp new file mode 100644 index 00000000..cfe7d922 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/set.jsp @@ -0,0 +1,34 @@ +<%@ 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 } + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/setBundle.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/setBundle.jsp new file mode 100644 index 00000000..a702dd29 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/setBundle.jsp @@ -0,0 +1,23 @@ +<%@ 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 +
+ + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/setDataSource.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/setDataSource.jsp new file mode 100644 index 00000000..bf4fc563 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/setDataSource.jsp @@ -0,0 +1,27 @@ +<%@ 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 } + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/setLocale.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/setLocale.jsp new file mode 100644 index 00000000..b077fe91 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/setLocale.jsp @@ -0,0 +1,68 @@ +<%@ 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 }
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/setTimeZone.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/setTimeZone.jsp new file mode 100644 index 00000000..2e3f0dde --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/setTimeZone.jsp @@ -0,0 +1,43 @@ +<%@ 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 } : + + + +
+
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/sina.xml b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/sina.xml new file mode 100644 index 00000000..772fca53 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/sina.xml @@ -0,0 +1,300 @@ + + + + + + + <![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 + + + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/split.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/split.jsp new file mode 100644 index 00000000..a418e32d --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/split.jsp @@ -0,0 +1,21 @@ +<%@ 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 }
+
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/sql.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/sql.jsp new file mode 100644 index 00000000..cad2d4b0 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/sql.jsp @@ -0,0 +1,16 @@ +<%@ 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 + + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/substring.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/substring.jsp new file mode 100644 index 00000000..d1e5af91 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/substring.jsp @@ -0,0 +1,21 @@ +<%@ 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, '@') } + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/timeZone.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/timeZone.jsp new file mode 100644 index 00000000..e3521846 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/timeZone.jsp @@ -0,0 +1,81 @@ +<%@ 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 }
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/transaction.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/transaction.jsp new file mode 100644 index 00000000..388df0d8 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/transaction.jsp @@ -0,0 +1,91 @@ +<%@ 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] }
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/update.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/update.jsp new file mode 100644 index 00000000..3e03c34e --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/update.jsp @@ -0,0 +1,65 @@ +<%@ 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 }
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/url.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/url.jsp new file mode 100644 index 00000000..099f71ff --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/url.jsp @@ -0,0 +1,15 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> +<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %> + + + + + Insert title here + + + + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_choose.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_choose.jsp new file mode 100644 index 00000000..f2169bc8 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_choose.jsp @@ -0,0 +1,43 @@ +<%@ 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 属性。 + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_forEach.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_forEach.jsp new file mode 100644 index 00000000..fdb7ef5d --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_forEach.jsp @@ -0,0 +1,44 @@ +<%@ 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">  +
+

+ +
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_if.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_if.jsp new file mode 100644 index 00000000..96521079 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_if.jsp @@ -0,0 +1,42 @@ +<%@ 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 不存在。 + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_out.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_out.jsp new file mode 100644 index 00000000..6ba9e20a --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_out.jsp @@ -0,0 +1,25 @@ +<%@ 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:
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_set.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_set.jsp new file mode 100644 index 00000000..5deff358 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_set.jsp @@ -0,0 +1,38 @@ +<%@ 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: + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_transform.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_transform.jsp new file mode 100644 index 00000000..ff902f9a --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/x_transform.jsp @@ -0,0 +1,107 @@ +<%@ 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 + + + + + + + + + + + + + + + + + + + + + + + + +
编号姓名电子邮件描述
+
+
+ +
+ +
+ +
+ + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/xml.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/xml.jsp new file mode 100644 index 00000000..db0f4a94 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/uncheck/xml.jsp @@ -0,0 +1,25 @@ +<%@ 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:
+ + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_choose.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_choose.jsp new file mode 100644 index 00000000..d4261640 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_choose.jsp @@ -0,0 +1,42 @@ +<%@ 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. + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_forEach.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_forEach.jsp new file mode 100644 index 00000000..81dc6762 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_forEach.jsp @@ -0,0 +1,36 @@ +<%@ 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:
  • +
    +
+ + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_if.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_if.jsp new file mode 100644 index 00000000..6222d2f8 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_if.jsp @@ -0,0 +1,40 @@ +<%@ 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 + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_out.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_out.jsp new file mode 100644 index 00000000..11924dbd --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_out.jsp @@ -0,0 +1,35 @@ +<%@ 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: + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_param.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_param.jsp new file mode 100644 index 00000000..e7701603 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_param.jsp @@ -0,0 +1,33 @@ +<%@ 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 + + + + + + + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_parse.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_parse.jsp new file mode 100644 index 00000000..22aca6a5 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_parse.jsp @@ -0,0 +1,22 @@ +<%@ 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: + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_set.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_set.jsp new file mode 100644 index 00000000..2c0617df --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_set.jsp @@ -0,0 +1,31 @@ +<%@ 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: + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_transform.jsp b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_transform.jsp new file mode 100644 index 00000000..c8bc4075 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/examples/xml/x_transform.jsp @@ -0,0 +1,31 @@ +<%@ 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 + + + + + + + + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/xml/books.xml b/codes/javaee/javaee-jstl/src/main/webapp/xml/books.xml new file mode 100644 index 00000000..1ff9cc88 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/xml/books.xml @@ -0,0 +1,12 @@ + + + Padam History + ZARA + 100 + + + Great Mistry + NUHA + 2000 + + diff --git a/codes/javaee/javaee-jstl/src/main/webapp/xml/style.xsl b/codes/javaee/javaee-jstl/src/main/webapp/xml/style.xsl new file mode 100644 index 00000000..728b223b --- /dev/null +++ b/codes/javaee/javaee-jstl/src/main/webapp/xml/style.xsl @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + +
+
diff --git a/codes/javaee/javaee-jstl/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java b/codes/javaee/javaee-jstl/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java new file mode 100644 index 00000000..f4706199 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java @@ -0,0 +1,114 @@ +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 { + + public static final int IDE_ECLIPSE = 0; + + public static final int IDE_INTELLIJ = 1; + + 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 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) { + 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 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, '|')); + } + + 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"); + } + +} diff --git a/codes/javaee/javaee-jstl/src/test/java/io/github/dunwu/javaee/server/Profiles.java b/codes/javaee/javaee-jstl/src/test/java/io/github/dunwu/javaee/server/Profiles.java new file mode 100644 index 00000000..dcfd3e39 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/test/java/io/github/dunwu/javaee/server/Profiles.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * 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/javaee-jstl/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java b/codes/javaee/javaee-jstl/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java new file mode 100644 index 00000000..049c8802 --- /dev/null +++ b/codes/javaee/javaee-jstl/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java @@ -0,0 +1,35 @@ +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/javaee-jstl/src/test/resources/jetty/webdefault.xml b/codes/javaee/javaee-jstl/src/test/resources/jetty/webdefault.xml new file mode 100644 index 00000000..b991d44c --- /dev/null +++ b/codes/javaee/javaee-jstl/src/test/resources/jetty/webdefault.xml @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + 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/javaee-listener/pom.xml b/codes/javaee/javaee-listener/pom.xml new file mode 100644 index 00000000..5e37b2f9 --- /dev/null +++ b/codes/javaee/javaee-listener/pom.xml @@ -0,0 +1,97 @@ + + 4.0.0 + + + io.github.dunwu.javaee + javaee + 1.0.0 + + + io.github.dunwu + javaee-listener + 1.0.0 + war + javaee-listener + JavaEE 学习笔记之 listener + + + UTF-8 + 1.7 + ${java.version} + ${java.version} + + + + + + ch.qos.logback + logback-classic + + + org.slf4j + jcl-over-slf4j + + + + + + commons-fileupload + commons-fileupload + + + commons-io + commons-io + + + org.apache.commons + commons-lang3 + + + + + + javax.servlet + javax.servlet-api + provided + + + javax.servlet.jsp + javax.servlet.jsp-api + provided + + + + + + org.eclipse.jetty + jetty-webapp + test + + + org.eclipse.jetty + jetty-annotations + test + + + org.eclipse.jetty + apache-jsp + test + + + org.eclipse.jetty + apache-jstl + test + + + + + + junit + junit + test + + + + + diff --git a/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/LoginSessionListener.java b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/LoginSessionListener.java new file mode 100644 index 00000000..05906ade --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/LoginSessionListener.java @@ -0,0 +1,89 @@ +package io.github.dunwu.javaee.listener; + +import io.github.dunwu.javaee.listener.bean.PersonInfo; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionAttributeListener; +import javax.servlet.http.HttpSessionBindingEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 单点登录处理类 + * + * @author Zhang Peng + * @since 2017-04-04 + */ +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/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionActivationListener.java b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionActivationListener.java new file mode 100644 index 00000000..647a568e --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionActivationListener.java @@ -0,0 +1,46 @@ +package io.github.dunwu.javaee.listener; + +import java.io.Serializable; +import javax.servlet.http.HttpSessionActivationListener; +import javax.servlet.http.HttpSessionEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * HttpSessionActivationListener 接口的 JavaBean 对象可以感知自己被序列化或反序列化的事件。 + * + * @author Zhang Peng + * @since 2017-04-04 + */ +public class MyHttpSessionActivationListener implements HttpSessionActivationListener, Serializable { + + private static final long serialVersionUID = 1L; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private String name; + + public MyHttpSessionActivationListener(String name) { + this.name = 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; + } + +} diff --git a/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionAttributeListener.java b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionAttributeListener.java new file mode 100644 index 00000000..cc55a3e1 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionAttributeListener.java @@ -0,0 +1,44 @@ +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; + +/** + * HttpSessionAttributeListener 用于监听 HttpSession 中的属性变化 + * + * @author Zhang Peng + * @since 2017-04-04 + */ +public class MyHttpSessionAttributeListener implements HttpSessionAttributeListener { + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + // 添加属性 + @Override + public void attributeAdded(HttpSessionBindingEvent se) { + HttpSession session = se.getSession(); + String name = se.getName(); + logger.info("新建session属性:" + name + ", 值为:" + se.getValue()); + } + + // 删除属性 + @Override + public void attributeRemoved(HttpSessionBindingEvent se) { + HttpSession session = se.getSession(); + String name = se.getName(); + logger.info("删除session属性:" + name + ", 值为:" + se.getValue()); + } + + // 修改属性 + @Override + 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/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionBindingListener.java b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionBindingListener.java new file mode 100644 index 00000000..783712c8 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionBindingListener.java @@ -0,0 +1,28 @@ +package io.github.dunwu.javaee.listener; + +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionBindingListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * HttpSessionBindingListener 接口的 JavaBean 对象可以感知自己被绑定或解绑定到 Session 中的事件。 + * + * @author Zhang Peng + * @since 2017-04-04 + */ +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/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionListener.java b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionListener.java new file mode 100644 index 00000000..0fa16180 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyHttpSessionListener.java @@ -0,0 +1,48 @@ +package io.github.dunwu.javaee.listener; + +import io.github.dunwu.javaee.listener.util.ApplicationConstants; +import java.util.Date; +import javax.servlet.http.HttpSession; +import javax.servlet.http.HttpSessionEvent; +import javax.servlet.http.HttpSessionListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * HttpSessionListener 接口用于监听 HttpSession 对象的创建和销毁。 + * + * @author Zhang Peng + * @since 2017-04-04 + */ +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/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyServletContextAttributeListener.java b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyServletContextAttributeListener.java new file mode 100644 index 00000000..e1d2723e --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyServletContextAttributeListener.java @@ -0,0 +1,33 @@ +package io.github.dunwu.javaee.listener; + +import javax.servlet.ServletContextAttributeEvent; +import javax.servlet.ServletContextAttributeListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ServletContextAttributeListener 用于监听 ServletContext 中的属性变化 + * + * @author Zhang Peng + * @since 2017-04-04 + */ +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/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyServletContextListener.java b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyServletContextListener.java new file mode 100644 index 00000000..2855c52e --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyServletContextListener.java @@ -0,0 +1,38 @@ +package io.github.dunwu.javaee.listener; + +import io.github.dunwu.javaee.listener.util.ApplicationConstants; +import java.util.Date; +import javax.servlet.ServletContext; +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ServletContextListener 接口用于监听 ServletContext 对象的创建和销毁事件。 + * + * @author Zhang Peng + * @since 2017-04-04 + */ +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/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyServletRequestAttributeListener.java b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyServletRequestAttributeListener.java new file mode 100644 index 00000000..1a6fad17 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyServletRequestAttributeListener.java @@ -0,0 +1,33 @@ +package io.github.dunwu.javaee.listener; + +import javax.servlet.ServletRequestAttributeEvent; +import javax.servlet.ServletRequestAttributeListener; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * ServletContextAttributeListener 用于监听 ServletRequest 中的属性变化 + * + * @author Zhang Peng + * @since 2017-04-04 + */ +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/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyServletRequestListener.java b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyServletRequestListener.java new file mode 100644 index 00000000..832850b6 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/MyServletRequestListener.java @@ -0,0 +1,61 @@ +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; + +/** + * ServletRequestListener 接口用于监听 ServletRequest 对象的创建和销毁。 + * + * @author Zhang Peng + * @since 2017-04-04 + */ +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/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/PersonInfoListener.java b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/PersonInfoListener.java new file mode 100644 index 00000000..75d3b6a1 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/PersonInfoListener.java @@ -0,0 +1,78 @@ +package io.github.dunwu.javaee.listener; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.util.Date; +import javax.servlet.http.*; + +/** + * @author Zhang Peng + * @since 2017-04-04 + */ +public class PersonInfoListener implements HttpSessionActivationListener, HttpSessionBindingListener, Serializable { + + private static final long serialVersionUID = -4780592776386225973L; + + private final Logger logger = 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; + } + + // 从硬盘加载后 + @Override + public void sessionDidActivate(HttpSessionEvent se) { + HttpSession session = se.getSession(); + logger.info(this + "已经成功从硬盘中加载。sessionId: " + session.getId()); + } + + // 即将被钝化到硬盘时 + @Override + public void sessionWillPassivate(HttpSessionEvent se) { + HttpSession session = se.getSession(); + logger.info(this + "即将保存到硬盘。sessionId: " + session.getId()); + } + + // 被放进session前 + @Override + public void valueBound(HttpSessionBindingEvent event) { + HttpSession session = event.getSession(); + String name = event.getName(); + logger.info(this + "被绑定到session \"" + session.getId() + "\"的" + name + "属性上"); + + // 记录放到session中的时间 + this.setDateCreated(new Date()); + } + + // 从session中移除后 + @Override + public void valueUnbound(HttpSessionBindingEvent event) { + HttpSession session = event.getSession(); + String name = event.getName(); + logger.info(this + "被从session \"" + session.getId() + "\"的" + name + "属性上移除"); + } + + @Override + public String toString() { + return "PersonInfoListener(" + name + ")"; + } + +} diff --git a/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/bean/PersonInfo.java b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/bean/PersonInfo.java new file mode 100644 index 00000000..360c969e --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/bean/PersonInfo.java @@ -0,0 +1,51 @@ +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 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()); + } + + public String getAccount() { + return account; + } + + public void setAccount(String account) { + this.account = account; + } + +} diff --git a/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/util/ApplicationConstants.java b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/util/ApplicationConstants.java new file mode 100644 index 00000000..f91d75c3 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/java/io/github/dunwu/javaee/listener/util/ApplicationConstants.java @@ -0,0 +1,28 @@ +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/javaee-listener/src/main/resources/logback.xml b/codes/javaee/javaee-listener/src/main/resources/logback.xml new file mode 100644 index 00000000..c05e3f34 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/resources/logback.xml @@ -0,0 +1,46 @@ + + + + + + + + + + %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/javaee-listener/src/main/webapp/META-INF/MANIFEST.MF b/codes/javaee/javaee-listener/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 00000000..254272e1 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/codes/javaee/javaee-listener/src/main/webapp/META-INF/context.xml b/codes/javaee/javaee-listener/src/main/webapp/META-INF/context.xml new file mode 100644 index 00000000..9d357dc7 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/webapp/META-INF/context.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/codes/javaee/javaee-listener/src/main/webapp/WEB-INF/resources/jsp/index.jsp b/codes/javaee/javaee-listener/src/main/webapp/WEB-INF/resources/jsp/index.jsp new file mode 100644 index 00000000..473a1315 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/webapp/WEB-INF/resources/jsp/index.jsp @@ -0,0 +1,16 @@ +<%@ 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/javaee-listener/src/main/webapp/WEB-INF/web.xml b/codes/javaee/javaee-listener/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..b9947a30 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,57 @@ + + + + + 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.MyHttpSessionActivationListener + + + + + io.github.dunwu.javaee.listener.LoginSessionListener + + + + + + io.github.dunwu.javaee.listener.PersonInfoListener + + + + + + index.jsp + + diff --git a/codes/javaee/javaee-listener/src/main/webapp/a.gif b/codes/javaee/javaee-listener/src/main/webapp/a.gif new file mode 100644 index 00000000..6f1cba44 Binary files /dev/null and b/codes/javaee/javaee-listener/src/main/webapp/a.gif differ diff --git a/codes/javaee/javaee-listener/src/main/webapp/aa.html b/codes/javaee/javaee-listener/src/main/webapp/aa.html new file mode 100644 index 00000000..4153961f --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/webapp/aa.html @@ -0,0 +1,17 @@ +

sdfsd

+ +

sadfsdf

+ +
    +
  1. sadf
  2. +
  3. asdfsadf
  4. +
  5. sdf
  6. +
  7. sdfsadfasdf
  8. +
+ + + + + + + diff --git a/codes/javaee/javaee-listener/src/main/webapp/active.jsp b/codes/javaee/javaee-listener/src/main/webapp/active.jsp new file mode 100644 index 00000000..a2bc7909 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/webapp/active.jsp @@ -0,0 +1,24 @@ +<%@ 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/javaee-listener/src/main/webapp/b.gif b/codes/javaee/javaee-listener/src/main/webapp/b.gif new file mode 100644 index 00000000..2996ad0e Binary files /dev/null and b/codes/javaee/javaee-listener/src/main/webapp/b.gif differ diff --git a/codes/javaee/javaee-listener/src/main/webapp/index.jsp b/codes/javaee/javaee-listener/src/main/webapp/index.jsp new file mode 100644 index 00000000..7f55cd70 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/webapp/index.jsp @@ -0,0 +1,22 @@ +<%@ 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/javaee-listener/src/main/webapp/listener.jsp b/codes/javaee/javaee-listener/src/main/webapp/listener.jsp new file mode 100644 index 00000000..c8326ee4 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/webapp/listener.jsp @@ -0,0 +1,91 @@ +<%@ 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/javaee-listener/src/main/webapp/online.jsp b/codes/javaee/javaee-listener/src/main/webapp/online.jsp new file mode 100644 index 00000000..e9219e1c --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/webapp/online.jsp @@ -0,0 +1,55 @@ +<%@ 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/javaee-listener/src/main/webapp/testLoginSessionListener.jsp b/codes/javaee/javaee-listener/src/main/webapp/testLoginSessionListener.jsp new file mode 100644 index 00000000..7ef99320 --- /dev/null +++ b/codes/javaee/javaee-listener/src/main/webapp/testLoginSessionListener.jsp @@ -0,0 +1,70 @@ +<%@ 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/javaee-listener/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java b/codes/javaee/javaee-listener/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java new file mode 100644 index 00000000..35d18b11 --- /dev/null +++ b/codes/javaee/javaee-listener/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java @@ -0,0 +1,131 @@ +package io.github.dunwu.javaee.server; + +import java.util.ArrayList; +import java.util.List; +import org.apache.commons.lang3.StringUtils; +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("all") +public class JettyFactory { + + public static final int IDE_ECLIPSE = 0; + + public static final int IDE_INTELLIJ = 1; + + public static final String ACTIVE_PROFILE = "spring.profiles.active"; + + public static final String DEFAULT_PROFILE = "spring.profiles.default"; + + public static final String DEVELOPMENT = "development"; + + private static final int PORT = 9527; + + 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", "tiles" }; + + private static final String WINDOWS_WEBDEFAULT_PATH = "jetty/webdefault.xml"; + + public static Server initServer() { + setProfileAsSystemProperty(DEVELOPMENT); + WebAppContext webAppContext = new WebAppContext(); + Server server = new Server(PORT); + server.setHandler(webAppContext); + return server; + } + + /** + * 在Spring启动前,设置profile的环境变量。 + */ + public static void setProfileAsSystemProperty(String profile) { + System.setProperty(ACTIVE_PROFILE, profile); + } + + 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 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. + + List list = new ArrayList<>(); + list.add(".*/[^/]*servlet-api-[^/]*\\.jar$"); + list.add(".*/javax.servlet.jsp.jstl-.*\\.jar$"); + list.add(".*/[^/]*taglibs.*\\.jar$"); + + for (String jarName : jarNames) { + String str = ".*/" + jarName + "-[^/]*\\.jar$"; + list.add(str); + } + + context.setAttribute("org.eclipse.jetty.io.github.dunwu.javaee.server.webapp.ContainerIncludeJarPattern", + StringUtils.join(list, '|')); + } + + 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"); + } + +} diff --git a/codes/javaee/javaee-listener/src/test/java/io/github/dunwu/javaee/server/ListenerDemosBootstrap.java b/codes/javaee/javaee-listener/src/test/java/io/github/dunwu/javaee/server/ListenerDemosBootstrap.java new file mode 100644 index 00000000..3ec9a1a0 --- /dev/null +++ b/codes/javaee/javaee-listener/src/test/java/io/github/dunwu/javaee/server/ListenerDemosBootstrap.java @@ -0,0 +1,35 @@ +package io.github.dunwu.javaee.server; + +import org.eclipse.jetty.server.Server; + +/** + * 快速启动 jetty 服务器,方便测试 + * + * @author Zhang Peng + */ +public class ListenerDemosBootstrap { + + // 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/javaee-listener/src/test/resources/jetty/webdefault.xml b/codes/javaee/javaee-listener/src/test/resources/jetty/webdefault.xml new file mode 100644 index 00000000..b991d44c --- /dev/null +++ b/codes/javaee/javaee-listener/src/test/resources/jetty/webdefault.xml @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + 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/javaee-oss/pom.xml b/codes/javaee/javaee-oss/pom.xml new file mode 100644 index 00000000..0d7aba66 --- /dev/null +++ b/codes/javaee/javaee-oss/pom.xml @@ -0,0 +1,149 @@ + + + 4.0.0 + + + io.github.dunwu.javaee + javaee + 1.0.0 + + + io.github.dunwu + javaee-oss + 1.0.0 + jar + javaee-oss + JavaEE 学习笔记之 OSS(Open Source Software) + + + UTF-8 + 1.7 + ${java.version} + ${java.version} + + + + + + ch.qos.logback + logback-classic + + + org.slf4j + jcl-over-slf4j + + + + + + commons-codec + commons-codec + + + org.bouncycastle + bcprov-jdk15on + 1.54 + + + + + + + org.jsoup + jsoup + 1.9.2 + + + + + + com.google.zxing + core + 3.3.0 + + + com.google.zxing + javase + 3.3.0 + + + net.coobird + thumbnailator + + + net.sf.jmimemagic + jmimemagic + 0.1.4 + + + + + + org.apache.activemq + activemq-all + 5.14.1 + + + + + + com.alibaba + fastjson + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + + + + javax.mail + javax.mail-api + + + javax.validation + validation-api + + + + + + org.apache.velocity + velocity + 1.7 + + + + + + junit + junit + test + + + org.assertj + assertj-core + test + + + + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-collections4 + + + + + diff --git a/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/DsaCoder.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/DsaCoder.java new file mode 100644 index 00000000..9934fdb0 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/DsaCoder.java @@ -0,0 +1,81 @@ +package io.github.dunwu.javaee.oss.encode.digest; + +import org.apache.commons.codec.binary.Base64; + +import java.security.*; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; + +/** + * 数字签名算法(Digital Signature Algorithm, DSA)工具类。 DSA是一种数字签名算法。 DSA仅支持SHA系列算法,而JDK仅支持SHA1withDSA。 + * + * @author Zhang Peng + * @since 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(); + } + + private KeyPair initKey() throws Exception { + // 初始化密钥对生成器 + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM); + // 实例化密钥对生成器 + keyPairGen.initialize(KEY_SIZE); + // 实例化密钥对 + return keyPairGen.genKeyPair(); + } + + 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); + } + + 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 byte[] getPrivateKey() { + return keyPair.getPrivate().getEncoded(); + } + + 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); + } + + public byte[] getPublicKey() { + return keyPair.getPublic().getEncoded(); + } + +} diff --git a/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/HmacCoder.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/HmacCoder.java new file mode 100644 index 00000000..c0878d00 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/HmacCoder.java @@ -0,0 +1,38 @@ +package io.github.dunwu.javaee.oss.encode.digest; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.codec.binary.Base64; + +/** + * 消息认证码算法(Message Autherntication Code, MAC)是基于密码的消息摘要算法。 它兼容了MD/SHA的特性,并以此为基础加入了密钥。 + * + * @author Zhang Peng + * @since 2016年7月21日 + */ +public class HmacCoder { + + 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)); + } + + 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); + } + + /** + * JDK支持HmacMD5, HmacSHA1, HmacSHA256, HmacSHA384, HmacSHA512 + */ + public enum HmacTypeEn { + + HmacMD5, HmacSHA1, HmacSHA256, HmacSHA384, HmacSHA512; + } + +} diff --git a/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/MdCoder.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/MdCoder.java new file mode 100644 index 00000000..b466a25e --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/MdCoder.java @@ -0,0 +1,46 @@ +package io.github.dunwu.javaee.oss.encode.digest; + +import java.security.MessageDigest; +import org.apache.commons.codec.binary.Base64; + +/** + * 消息摘要算法(Message Digest, MD)是消息摘要算法。 + * + * @author Zhang Peng + * @since 2016年7月21日 + */ +public class MdCoder { + + 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); + } + + public static byte[] encodeWithBase64(byte[] input, MdTypeEn type) throws Exception { + return Base64.encodeBase64URLSafe(encode(input, type)); + } + + public static byte[] encode(byte[] input, MdTypeEn type) throws Exception { + // 根据类型,初始化消息摘要对象 + MessageDigest md5Digest = MessageDigest.getInstance(type.name()); + + // 更新要计算的内容 + md5Digest.update(input); + + // 完成哈希计算,返回摘要 + return md5Digest.digest(); + } + + /** + * JDK支持MD2和MD5两种MD算法 + */ + public enum MdTypeEn { + + MD2, MD5 + } + +} diff --git a/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/ShaCoder.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/ShaCoder.java new file mode 100644 index 00000000..f22efaf3 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/digest/ShaCoder.java @@ -0,0 +1,56 @@ +package io.github.dunwu.javaee.oss.encode.digest; + +import java.security.MessageDigest; +import org.apache.commons.codec.binary.Base64; + +/** + * 安全散列算法(Secure Hash Algorithm, SHA)是消息摘要算法 + * + * @author Zhang Peng + * @since 2016年7月21日 + */ +public class ShaCoder { + + 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); + } + + public static byte[] encodeWithBase64(byte[] input, ShaTypeEn type) throws Exception { + return Base64.encodeBase64URLSafe(encode(input, type)); + } + + public static byte[] encode(byte[] input, ShaTypeEn type) throws Exception { + // 根据类型,初始化消息摘要对象 + MessageDigest md5Digest = MessageDigest.getInstance(type.getName()); + + // 更新要计算的内容 + md5Digest.update(input); + + // 完成哈希计算,返回摘要 + return md5Digest.digest(); + } + + /** + * 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; + } + } + +} diff --git a/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/AESCoder.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/AESCoder.java new file mode 100644 index 00000000..5880cdd6 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/AESCoder.java @@ -0,0 +1,129 @@ +package io.github.dunwu.javaee.oss.encode.encrypt; + +import org.bouncycastle.util.encoders.Base64; + +import java.security.*; +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; + +/** + * AES安全编码:对称加密算法。DES的替代方案。 + * + * @author Zhang Peng + * @since 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; + } + + /** + * 根据随机数种子生成一个密钥 + * + * @return Key + * @throws NoSuchAlgorithmException + * @author Zhang Peng + * @since 2016年7月14日 + */ + 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(); + } + + public AESCoder(String transformation) throws NoSuchAlgorithmException, NoSuchPaddingException { + this.key = initKey(); + this.cipher = Cipher.getInstance(transformation); + this.transformation = transformation; + } + + 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)); + } + + /** + * 加密 + * + * @param input 明文 + * @return byte[] 密文 + * @throws InvalidKeyException + * @throws IllegalBlockSizeException + * @throws BadPaddingException + * @throws InvalidAlgorithmParameterException + * @author Zhang Peng + * @since 2016年7月20日 + */ + 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); + } + + /** + * 解密 + * + * @param input 密文 + * @return byte[] 明文 + * @throws InvalidKeyException + * @throws IllegalBlockSizeException + * @throws BadPaddingException + * @throws InvalidAlgorithmParameterException + * @author Zhang Peng + * @since 2016年7月20日 + */ + 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); + } + + private byte[] getIV() { + String iv = "0123456789ABCDEF"; // IV length: must be 16 bytes long + return iv.getBytes(); + } + +} diff --git a/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/Base64Demo.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/Base64Demo.java new file mode 100644 index 00000000..7ebb28a4 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/Base64Demo.java @@ -0,0 +1,25 @@ +package io.github.dunwu.javaee.oss.encode.encrypt; + +import java.io.UnsupportedEncodingException; +import org.apache.commons.codec.binary.Base64; + +/** + * Base64编码、解码范例 + * + * @author Zhang Peng + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/DESCoder.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/DESCoder.java new file mode 100644 index 00000000..38795c04 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/DESCoder.java @@ -0,0 +1,130 @@ +package io.github.dunwu.javaee.oss.encode.encrypt; + +import org.bouncycastle.util.encoders.Base64; + +import java.security.*; +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; + +/** + * DES安全编码:是经典的对称加密算法。密钥仅56位,且迭代次数偏少。已被视为并不安全的加密算法。 + * + * @author Zhang Peng + * @since 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; + } + + /** + * 根据随机数种子生成一个密钥 + * + * @return Key + * @throws NoSuchAlgorithmException + * @throws NoSuchProviderException + * @author Zhang Peng + * @since 2016年7月14日 + */ + 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(); + } + + public DESCoder(String transformation) + throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException { + this.key = initKey(); + this.cipher = Cipher.getInstance(transformation); + this.transformation = transformation; + } + + 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)); + } + + /** + * 加密 + * + * @param input 明文 + * @return byte[] 密文 + * @throws InvalidKeyException + * @throws IllegalBlockSizeException + * @throws BadPaddingException + * @throws InvalidAlgorithmParameterException + * @author Zhang Peng + * @since 2016年7月20日 + */ + 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); + } + + /** + * 解密 + * + * @param input 密文 + * @return byte[] 明文 + * @throws InvalidKeyException + * @throws IllegalBlockSizeException + * @throws BadPaddingException + * @throws InvalidAlgorithmParameterException + * @author Zhang Peng + * @since 2016年7月20日 + */ + 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); + } + + private byte[] getIV() { + String iv = "01234567"; // IV length: must be 8 bytes long + return iv.getBytes(); + } + +} diff --git a/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/DESedeCoder.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/DESedeCoder.java new file mode 100644 index 00000000..1af976f8 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/DESedeCoder.java @@ -0,0 +1,73 @@ +package io.github.dunwu.javaee.oss.encode.encrypt; + +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Security; +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import org.apache.commons.codec.binary.Base64; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +/** + * DESede安全编码,DES的升级版,支持更长的密钥,基本算法不变。 + * + * @author Zhang Peng + * @since 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(); + } + + 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)); + } + + 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); + } + +} diff --git a/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/PBECoder.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/PBECoder.java new file mode 100644 index 00000000..c4f22552 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/PBECoder.java @@ -0,0 +1,91 @@ +package io.github.dunwu.javaee.oss.encode.encrypt; + +import java.security.Key; +import java.security.SecureRandom; +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.PBEParameterSpec; +import org.apache.commons.codec.binary.Base64; + +/** + * 基于口令加密(Password Based Encryption, PBE),是一种对称加密算法。 其特点是:口令由用户自己掌管,采用随机数(这里叫做盐)杂凑多重加密等方法保证数据的安全性。 + * PBE没有密钥概念,密钥在其他对称加密算法中是经过计算得出的,PBE则使用口令替代了密钥。 + * + * @author Zhang Peng + * @since 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); + } + + 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 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); + } + + 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/RSACoder.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/RSACoder.java new file mode 100644 index 00000000..085189b0 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/encrypt/RSACoder.java @@ -0,0 +1,142 @@ +package io.github.dunwu.javaee.oss.encode.encrypt; + +import org.apache.commons.codec.binary.Base64; + +import java.security.*; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import javax.crypto.Cipher; + +/** + * RSA安全编码:非对称加密算法。它既可以用来加密、解密,也可以用来做数字签名 + * + * @author Zhang Peng + * @since 2016年7月20日 + */ +public class RSACoder { + + public final static String KEY_ALGORITHM = "RSA"; + + public final static String SIGN_ALGORITHM = "MD5WithRSA"; + + private KeyPair keyPair; + + public RSACoder() throws Exception { + this.keyPair = initKeyPair(); + } + + 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); + } + + 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[] 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[] 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 byte[] getPrivateKey() { + return keyPair.getPrivate().getEncoded(); + } + + 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 enum RsaSignTypeEn { + + MD2WithRSA, + MD5WithRSA, + SHA1WithRSA + } + +} diff --git a/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/DownloadPolicy.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/DownloadPolicy.java new file mode 100644 index 00000000..d5720446 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/DownloadPolicy.java @@ -0,0 +1,111 @@ +package io.github.dunwu.javaee.oss.encode.sample; + +import java.io.Serializable; +import org.apache.commons.lang3.StringUtils; + +/** + * Created by Zhang Peng on 2016/7/22. + */ +public class DownloadPolicy implements Serializable { + + /** + * + */ + private static final long serialVersionUID = 1159078308838844309L; + + /** + * 操作类型 + */ + private final String operate = UploadConstant.TOKEN_DOWNLOAD; + + /** + * 文件ID + */ + private Long fileId; + + /** + * 下载文件所属空间 + */ + private String namespace; + + /** + * 令牌有效的截止时间。用Unix时间表示。单位秒 + */ + private Long deadline; + + /** + * 允许下载文件类型 + */ + private String fType = UploadConstant.SUPPORT_FILE_TYPE; + + /** + * 判断数据是否有效 + * + * @param policy + * @return boolean + * @author Zhang Peng + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/ModifyPolicy.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/ModifyPolicy.java new file mode 100644 index 00000000..0ed0a444 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/ModifyPolicy.java @@ -0,0 +1,95 @@ +package io.github.dunwu.javaee.oss.encode.sample; + +import java.io.Serializable; +import org.apache.commons.lang3.StringUtils; + +/** + * Created by Zhang Peng on 2016/7/22. + */ +public class ModifyPolicy implements Serializable { + + private static final long serialVersionUID = -547821705196510884L; + + /** + * 操作类型 + */ + private final String operate = UploadConstant.TOKEN_MODIFY; + + /** + * 下载文件所属空间 + */ + private String namespace; + + /** + * 令牌有效的截止时间。用Unix时间表示。单位秒 + */ + private Long deadline; + + /** + * 允许下载文件类型 + */ + private String fType = UploadConstant.SUPPORT_FILE_TYPE; + + /** + * 判断数据是否有效 + * + * @param policy + * @return boolean + * @author Zhang Peng + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/TokenUtil.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/TokenUtil.java new file mode 100644 index 00000000..35b5f1ab --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/TokenUtil.java @@ -0,0 +1,158 @@ +package io.github.dunwu.javaee.oss.encode.sample; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import io.github.dunwu.javaee.oss.encode.digest.HmacCoder; +import org.apache.commons.codec.binary.Base64; + +/** + * 令牌工具类 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 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 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; + } + + public static void main(String[] args) throws Exception { + testGetToken(TokenTypeEN.DOWNLOAD.name()); + } + + 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; + } + + 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 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 enum TokenTypeEN { + + UPLOAD, DOWNLOAD, MODIFY + } + +} diff --git a/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/UploadConstant.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/UploadConstant.java new file mode 100644 index 00000000..8c6affbf --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/UploadConstant.java @@ -0,0 +1,35 @@ +package io.github.dunwu.javaee.oss.encode.sample; + +import java.util.HashSet; +import java.util.Set; +import org.apache.commons.collections.CollectionUtils; + +/** + * Created by Zhang Peng 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/UploadPolicy.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/UploadPolicy.java new file mode 100644 index 00000000..2afad08a --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/encode/sample/UploadPolicy.java @@ -0,0 +1,153 @@ +package io.github.dunwu.javaee.oss.encode.sample; + +import java.io.Serializable; +import org.apache.commons.lang3.StringUtils; + +/** + * Created by Zhang Peng on 2016/7/22. + */ +public class UploadPolicy implements Serializable { + + private static final long serialVersionUID = 8289239747395166646L; + + /** + * 操作类型 + */ + private final String operate = UploadConstant.TOKEN_UPLOAD; + + /** + * 上传文件所属空间 + */ + 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; + + /** + * 判断数据是否有效 + * + * @param policy + * @return boolean + * @author Zhang Peng + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/html/CnblogParser.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/html/CnblogParser.java new file mode 100644 index 00000000..c8027e39 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/html/CnblogParser.java @@ -0,0 +1,52 @@ +/** + * The Apache License 2.0 Copyright (c) 2016 Victor Zhang + */ +package io.github.dunwu.javaee.oss.html; + +import java.io.IOException; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +/** + * 博客园博文抓取工具 + * + * @author Victor Zhang + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/html/XiamiParser.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/html/XiamiParser.java new file mode 100644 index 00000000..e5708593 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/html/XiamiParser.java @@ -0,0 +1,109 @@ +/** + * The Apache License 2.0 Copyright (c) 2016 Victor Zhang + */ +package io.github.dunwu.javaee.oss.html; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +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; + +/** + * 获取虾米网我的音乐中所有曲目 + * + * @author Victor Zhang + * @since 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; + + @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; + } + } + + 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 int hashCode() { + return 1; + } + + } + +} diff --git a/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/image/ImageUtil.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/image/ImageUtil.java new file mode 100644 index 00000000..2db945e5 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/image/ImageUtil.java @@ -0,0 +1,142 @@ +package io.github.dunwu.javaee.oss.image; + +import io.github.dunwu.javaee.oss.image.dto.ImageParamDTO; +import net.coobird.thumbnailator.Thumbnails; +import net.coobird.thumbnailator.geometry.Positions; +import net.sf.jmimemagic.*; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.awt.image.BufferedImage; +import java.io.*; +import javax.imageio.ImageIO; + +/** + * 图片工具类 + * + * @author Victor Zhang + * @since 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); + } + + 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()); + } + } + + 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; + } + + /** + * 获取文件的 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/image/QRCodeUtil.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/image/QRCodeUtil.java new file mode 100644 index 00000000..376407f4 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/image/QRCodeUtil.java @@ -0,0 +1,60 @@ +package io.github.dunwu.javaee.oss.image; + +import com.google.zxing.*; +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; +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; + +/** + * 二维码工具类 + * + * @author Victor Zhang + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/image/dto/BarcodeParamDTO.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/image/dto/BarcodeParamDTO.java new file mode 100644 index 00000000..49c97e41 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/image/dto/BarcodeParamDTO.java @@ -0,0 +1,87 @@ +/** + * 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 + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/image/dto/ImageParamDTO.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/image/dto/ImageParamDTO.java new file mode 100644 index 00000000..3e886501 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/image/dto/ImageParamDTO.java @@ -0,0 +1,181 @@ +/** + * The Apache License 2.0 Copyright (c) 2016 Victor Zhang + */ +package io.github.dunwu.javaee.oss.image.dto; + +import java.io.Serializable; +import net.coobird.thumbnailator.geometry.Positions; + +/** + * @author Victor Zhang + * @since 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; // 水印信息 + + /** + * 将位置类型码转换为 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 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; + } + + 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSHelloWorld.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSHelloWorld.java new file mode 100644 index 00000000..7808119c --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSHelloWorld.java @@ -0,0 +1,140 @@ +/** + * The Apache License 2.0 Copyright (c) 2016 Victor Zhang + */ +package io.github.dunwu.javaee.oss.jms; + +import javax.jms.*; +import org.apache.activemq.ActiveMQConnectionFactory; + +/** + * @author Victor Zhang + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSReceiver.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSReceiver.java new file mode 100644 index 00000000..604ccbb8 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSReceiver.java @@ -0,0 +1,62 @@ +/** + * The Apache License 2.0 Copyright (c) 2016 Victor Zhang + */ +package io.github.dunwu.javaee.oss.jms; + +import javax.jms.*; +import org.apache.activemq.ActiveMQConnection; +import org.apache.activemq.ActiveMQConnectionFactory; + +/** + * 消息的消费者 + * + * @author Victor Zhang + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSSender.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSSender.java new file mode 100644 index 00000000..0f11282d --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/jms/JMSSender.java @@ -0,0 +1,77 @@ +package io.github.dunwu.javaee.oss.jms; + +import javax.jms.*; +import org.apache.activemq.ActiveMQConnection; +import org.apache.activemq.ActiveMQConnectionFactory; + +/** + * 消息的生产者 + * + * @author Victor Zhang + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/logging/JclDemo.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/logging/JclDemo.java new file mode 100644 index 00000000..fa75b912 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/logging/JclDemo.java @@ -0,0 +1,29 @@ +/** + * 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 + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/logging/Slf4jDemo.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/logging/Slf4jDemo.java new file mode 100644 index 00000000..6f9ce216 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/logging/Slf4jDemo.java @@ -0,0 +1,28 @@ +/** + * 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 + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/ForwardMail.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/ForwardMail.java new file mode 100644 index 00000000..b4a013be --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/ForwardMail.java @@ -0,0 +1,110 @@ +package io.github.dunwu.javaee.oss.mail; /** + * The Apache License 2.0 Copyright (c) 2016 Zhang Peng + */ + +import java.util.Date; +import java.util.Properties; +import javax.mail.*; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; +import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; + +/** + * @author Zhang Peng + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailConfigDTO.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailConfigDTO.java new file mode 100644 index 00000000..6f001ef6 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailConfigDTO.java @@ -0,0 +1,72 @@ +/** + * The Apache License 2.0 Copyright (c) 2016 Victor Zhang + */ +package io.github.dunwu.javaee.oss.mail; + +/** + * @author Victor Zhang + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailDTO.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailDTO.java new file mode 100644 index 00000000..a032cb45 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailDTO.java @@ -0,0 +1,104 @@ +/** + * 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 + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailUtil.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailUtil.java new file mode 100644 index 00000000..3d4664f8 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/MailUtil.java @@ -0,0 +1,202 @@ +package io.github.dunwu.javaee.oss.mail; + +import org.apache.commons.lang3.StringUtils; + +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; + +/** + * @author Victor Zhang + * @since 2016/12/22. + */ +public class MailUtil { + + private static final String TYPE_TEXT = "text"; + + private static final String TYPE_HTML = "html"; + + private MailConfigDTO configDTO; + + /** + * 以默认配置初始化邮件收发工具 + */ + public MailUtil() { + this.configDTO = initEmailConfig(); + } + + 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; + } + + /** + * 以自定义配置初始化邮件收发工具 + */ + public MailUtil(MailConfigDTO configDTO) { + this.configDTO = 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendAttachmentMail.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendAttachmentMail.java new file mode 100644 index 00000000..9dadc189 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendAttachmentMail.java @@ -0,0 +1,84 @@ +package io.github.dunwu.javaee.oss.mail; /** + * The Apache License 2.0 Copyright (c) 2016 Zhang Peng + */ + +import java.util.Properties; +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; + +/** + * @author Zhang Peng + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendTemplateMail.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendTemplateMail.java new file mode 100644 index 00000000..7e89e9ac --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendTemplateMail.java @@ -0,0 +1,81 @@ +/** + * The Apache License 2.0 Copyright (c) 2016 Victor Zhang + */ +package io.github.dunwu.javaee.oss.mail; + +import io.github.dunwu.javaee.oss.template.VelocityUtil; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import javax.mail.MessagingException; +import org.apache.velocity.VelocityContext; + +/** + * @author Victor Zhang + * @see org.zp.javaee.tools.mail.MailUtil 注意:如果想要成功发送邮件,需要修改JavaParty项目 src\javaee\tools\src\main\resources\mail\mail.properties + * 中的 参数,请根据实际邮箱来配置。 + * @since 2016/12/23. + */ +public class SendTemplateMail { + + private static final String DEFAULT_TO = "xxxxxx@163.com"; + + 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); + } + + 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; + } + + } + +} diff --git a/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendTextMail.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendTextMail.java new file mode 100644 index 00000000..fe4833a4 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/SendTextMail.java @@ -0,0 +1,67 @@ +package io.github.dunwu.javaee.oss.mail; /** + * The Apache License 2.0 Copyright (c) 2016 Zhang Peng + */ + +import java.util.Properties; +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; + +/** + * @author Zhang Peng + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/StoreMail.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/StoreMail.java new file mode 100644 index 00000000..32df1236 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/StoreMail.java @@ -0,0 +1,66 @@ +package io.github.dunwu.javaee.oss.mail; /** + * The Apache License 2.0 Copyright (c) 2016 Zhang Peng + */ + +import java.util.Properties; +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.Store; + +/** + * @author Zhang Peng + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/sendHtmlMail.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/sendHtmlMail.java new file mode 100644 index 00000000..bdceb7a1 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/mail/sendHtmlMail.java @@ -0,0 +1,84 @@ +package io.github.dunwu.javaee.oss.mail; /** + * The Apache License 2.0 Copyright (c) 2016 Zhang Peng + */ + +import java.util.Properties; +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; + +/** + * @author Zhang Peng + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/template/LoadVelocityDemo.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/template/LoadVelocityDemo.java new file mode 100644 index 00000000..4d21f306 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/template/LoadVelocityDemo.java @@ -0,0 +1,94 @@ +/** + * 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 + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityHelloWorld.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityHelloWorld.java new file mode 100644 index 00000000..c5d9b3ee --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityHelloWorld.java @@ -0,0 +1,42 @@ +package io.github.dunwu.javaee.oss.template; /** + * The Apache License 2.0 Copyright (c) 2016 Victor Zhang + */ + +import java.io.StringWriter; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; + +/** + * Velocity 的 HelloWorld 示例 + * + * @author Victor Zhang + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityHelloWorld2.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityHelloWorld2.java new file mode 100644 index 00000000..19398dfb --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityHelloWorld2.java @@ -0,0 +1,47 @@ +package io.github.dunwu.javaee.oss.template; /** + * The Apache License 2.0 Copyright (c) 2016 Victor Zhang + */ + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Properties; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; + +/** + * Velocity 的 HelloWorld 示例 + * + * @author Victor Zhang + * @since 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/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityUtil.java b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityUtil.java new file mode 100644 index 00000000..77945c52 --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/java/io/github/dunwu/javaee/oss/template/VelocityUtil.java @@ -0,0 +1,46 @@ +/** + * 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.Properties; +import org.apache.velocity.Template; +import org.apache.velocity.VelocityContext; +import org.apache.velocity.app.VelocityEngine; + +/** + * @author Victor Zhang + * @since 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/javaee-oss/src/main/resources/html/example.html b/codes/javaee/javaee-oss/src/main/resources/html/example.html new file mode 100644 index 00000000..32c7cf6a --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/resources/html/example.html @@ -0,0 +1,8 @@ + + + 测试html页面 + + +

牛刀小试

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

$hint

+
+
+

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

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

传图片url,显示logo

+ logo +
+
+ $date +
+
+
+ 提示:此邮件由系统自动发送,请勿直接回复 +
+
+
+ + + + + diff --git a/codes/javaee/javaee-oss/src/main/resources/template/velocity.properties b/codes/javaee/javaee-oss/src/main/resources/template/velocity.properties new file mode 100644 index 00000000..c299635c --- /dev/null +++ b/codes/javaee/javaee-oss/src/main/resources/template/velocity.properties @@ -0,0 +1,4 @@ +resource.loader=file +file.resource.loader.path=D:/01_Workspace/Project/zp/java/javaee-tutorial/codes/oss/src/main/resources +input.encoding=utf-8 +output.encoding=utf-8 diff --git a/codes/javaee/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/html/JsoupTest.java b/codes/javaee/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/html/JsoupTest.java new file mode 100644 index 00000000..23503c78 --- /dev/null +++ b/codes/javaee/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/html/JsoupTest.java @@ -0,0 +1,105 @@ +/** + * 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 + * @since 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/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/image/ImageUtilTest.java b/codes/javaee/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/image/ImageUtilTest.java new file mode 100644 index 00000000..29bb1547 --- /dev/null +++ b/codes/javaee/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/image/ImageUtilTest.java @@ -0,0 +1,66 @@ +package io.github.dunwu.javaee.oss.image; + +import io.github.dunwu.javaee.oss.image.dto.ImageParamDTO; +import net.sf.jmimemagic.MagicException; +import net.sf.jmimemagic.MagicMatchNotFoundException; +import net.sf.jmimemagic.MagicParseException; +import org.junit.Assert; +import org.junit.Test; + +import java.awt.image.BufferedImage; +import java.io.*; + +/** + * @author Victor Zhang + * @since 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/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/image/QRCodeUtilTest.java b/codes/javaee/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/image/QRCodeUtilTest.java new file mode 100644 index 00000000..7de69c35 --- /dev/null +++ b/codes/javaee/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/image/QRCodeUtilTest.java @@ -0,0 +1,99 @@ +/** + * The Apache License 2.0 Copyright (c) 2016 Victor Zhang + */ +package io.github.dunwu.javaee.oss.image; + +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; +import io.github.dunwu.javaee.oss.image.dto.BarcodeParamDTO; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.junit.Assert; +import org.junit.Before; +import org.junit.FixMethodOrder; +import org.junit.Test; +import org.junit.runners.MethodSorters; + +/** + * 测试qrcode工具类 + * + * @author Victor Zhang + * @since 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/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/image/ThumbnailatorTest.java b/codes/javaee/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/image/ThumbnailatorTest.java new file mode 100644 index 00000000..727517d8 --- /dev/null +++ b/codes/javaee/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/image/ThumbnailatorTest.java @@ -0,0 +1,141 @@ +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 java.awt.image.BufferedImage; +import java.io.*; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.imageio.ImageIO; +import javax.imageio.stream.FileImageOutputStream; + +/** + * @author Victor Zhang + * @since 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); + } + + private void createFolderIfNotExist(String folderPath) throws IOException { + File f = new File(folderPath); + if (!(f.exists() && f.isDirectory())) { + f.mkdirs(); + } + } + + @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); + } + +} diff --git a/codes/javaee/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/template/VelocityUtilTest.java b/codes/javaee/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/template/VelocityUtilTest.java new file mode 100644 index 00000000..8bae0bc6 --- /dev/null +++ b/codes/javaee/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/template/VelocityUtilTest.java @@ -0,0 +1,23 @@ +/** + * 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 + * @since 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/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/test/JUnitExecTest.java b/codes/javaee/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/test/JUnitExecTest.java new file mode 100644 index 00000000..dfbc7830 --- /dev/null +++ b/codes/javaee/javaee-oss/src/test/java/io/github/dunwu/javaee/oss/test/JUnitExecTest.java @@ -0,0 +1,85 @@ +package io.github.dunwu.javaee.oss.test; + +import org.junit.*; + +/** + * JUnit4开始支持注解,本例展示一个单元测试执行过程中,各个注解的调用顺序 + * + * @author Victor Zhang + * @since 2016/11/18. + */ +public class JUnitExecTest { + + @BeforeClass + public static void beforeClass1() { + System.out.println("@beforeClass1"); + } + + @BeforeClass + public static void beforeClass2() { + System.out.println("@beforeClass2"); + } + + @AfterClass + public static void afterClass1() { + System.out.println("@afterClass1"); + } + + @AfterClass + public static void afterClass2() { + System.out.println("@afterClass2"); + } + + @Before + public void before1() { + System.out.println("@before1"); + } + + @Before + public void before2() { + 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); + Assert.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"); + } + +} diff --git a/codes/javaee/javaee-oss/src/test/resources/html/jsoup-cookbook.html b/codes/javaee/javaee-oss/src/test/resources/html/jsoup-cookbook.html new file mode 100644 index 00000000..e1beb7a3 --- /dev/null +++ b/codes/javaee/javaee-oss/src/test/resources/html/jsoup-cookbook.html @@ -0,0 +1,107 @@ + + + + + + Cookbook: jsoup Java HTML parser + + + + + + + + + + + + + + + diff --git a/codes/javaee/javaee-oss/src/test/resources/images/lion.jpg b/codes/javaee/javaee-oss/src/test/resources/images/lion.jpg new file mode 100644 index 00000000..d10bd212 Binary files /dev/null and b/codes/javaee/javaee-oss/src/test/resources/images/lion.jpg differ diff --git a/codes/javaee/javaee-oss/src/test/resources/images/lion2.jpg b/codes/javaee/javaee-oss/src/test/resources/images/lion2.jpg new file mode 100644 index 00000000..6794cb34 Binary files /dev/null and b/codes/javaee/javaee-oss/src/test/resources/images/lion2.jpg differ diff --git a/codes/javaee/javaee-oss/src/test/resources/images/lion2_watermark.png b/codes/javaee/javaee-oss/src/test/resources/images/lion2_watermark.png new file mode 100644 index 00000000..2de3bb8a Binary files /dev/null and b/codes/javaee/javaee-oss/src/test/resources/images/lion2_watermark.png differ diff --git a/codes/javaee/javaee-oss/src/test/resources/images/wartermark.png b/codes/javaee/javaee-oss/src/test/resources/images/wartermark.png new file mode 100644 index 00000000..675f9a39 Binary files /dev/null and b/codes/javaee/javaee-oss/src/test/resources/images/wartermark.png differ diff --git a/codes/javaee/javaee-oss/src/test/resources/logback.xml b/codes/javaee/javaee-oss/src/test/resources/logback.xml new file mode 100644 index 00000000..1598ec4f --- /dev/null +++ b/codes/javaee/javaee-oss/src/test/resources/logback.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + %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 + + + + + + + + + + + + + + + + + + + + + diff --git a/codes/javaee/javaee-servlet/pom.xml b/codes/javaee/javaee-servlet/pom.xml new file mode 100644 index 00000000..96f53191 --- /dev/null +++ b/codes/javaee/javaee-servlet/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + + io.github.dunwu.javaee + javaee + 1.0.0 + + + io.github.dunwu + javaee-servlet + 1.0.0 + war + javaee-servlet + JavaEE 学习笔记之 servlet + + + UTF-8 + 1.7 + ${java.version} + ${java.version} + 0.4.0 + + + + + + ch.qos.logback + logback-classic + + + + + javax.servlet + javax.servlet-api + + + org.eclipse.jetty + jetty-webapp + test + + + org.eclipse.jetty + jetty-annotations + test + + + org.eclipse.jetty + apache-jsp + test + + + org.eclipse.jetty + apache-jstl + test + + + + + junit + junit + test + + + org.assertj + assertj-core + test + + + + + org.apache.commons + commons-lang3 + + + commons-fileupload + commons-fileupload + + + + diff --git a/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/AnnotationServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/AnnotationServlet.java new file mode 100644 index 00000000..9156ddb6 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/AnnotationServlet.java @@ -0,0 +1,85 @@ +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/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/ContextParamServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/ContextParamServlet.java new file mode 100644 index 00000000..0c4c2732 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/ContextParamServlet.java @@ -0,0 +1,64 @@ +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/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/FirstServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/FirstServlet.java new file mode 100644 index 00000000..36b6dde7 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/FirstServlet.java @@ -0,0 +1,92 @@ +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); + } + + 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(); + } + + /** + * 以 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; + } + +} diff --git a/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/ForwardServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/ForwardServlet.java new file mode 100644 index 00000000..bb47a2ac --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/ForwardServlet.java @@ -0,0 +1,47 @@ +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 doPut(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + this.doGet(request, response); + } + + 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 "); + } + } + +} diff --git a/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/HelloServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/HelloServlet.java new file mode 100644 index 00000000..97d42916 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/HelloServlet.java @@ -0,0 +1,108 @@ +/** + * The Apache License 2.0 Copyright (c) 2016 Zhang Peng + */ +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; + +/** + * @author Zhang Peng + * @since 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 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); + } + + /** + * 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(); + } + + /** + * Initialization of the servlet.
+ * + * @throws ServletException if an error occure + */ + public void init() throws ServletException { + // Put your code here + } + +} diff --git a/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/ImageServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/ImageServlet.java new file mode 100644 index 00000000..1155ece9 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/ImageServlet.java @@ -0,0 +1,94 @@ +package io.github.dunwu.javaee.servlet; + +import java.io.*; +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() + " ... "); + } + + @Override + public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + + this.doGet(request, response); + } + + @Override + 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(); + } + } + } + +} diff --git a/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/InitParamServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/InitParamServlet.java new file mode 100644 index 00000000..e05c2a44 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/InitParamServlet.java @@ -0,0 +1,81 @@ +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 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); + } + + 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(); + } + +} diff --git a/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/InjectionServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/InjectionServlet.java new file mode 100644 index 00000000..66eca83b --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/InjectionServlet.java @@ -0,0 +1,54 @@ +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/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/LifeCycleServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/LifeCycleServlet.java new file mode 100644 index 00000000..f1c82384 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/LifeCycleServlet.java @@ -0,0 +1,138 @@ +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/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/PostServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/PostServlet.java new file mode 100644 index 00000000..33bb6a0c --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/PostServlet.java @@ -0,0 +1,153 @@ +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/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/RedirectServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/RedirectServlet.java new file mode 100644 index 00000000..f751bb3c --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/RedirectServlet.java @@ -0,0 +1,78 @@ +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/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/RequestServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/RequestServlet.java new file mode 100644 index 00000000..177e97af --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/RequestServlet.java @@ -0,0 +1,187 @@ +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()); + + @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(); + } + + /** + * @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 "未知"; + } + + /** + * @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 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 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(", $", ""); + } + +} diff --git a/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/ThreadSafetyServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/ThreadSafetyServlet.java new file mode 100644 index 00000000..3dd596f0 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/ThreadSafetyServlet.java @@ -0,0 +1,34 @@ +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/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/UploadServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/UploadServlet.java new file mode 100644 index 00000000..4c8c1220 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/UploadServlet.java @@ -0,0 +1,175 @@ +package io.github.dunwu.javaee.servlet; + +import org.apache.commons.fileupload.DiskFileUpload; +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.FileUploadException; + +import java.io.*; +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; + +public class UploadServlet extends HttpServlet { + + private static final long serialVersionUID = 7523024737218332088L; + + @Override + public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.setCharacterEncoding("UTF-8"); + response.getWriter().println("请以 POST 方式上传文件"); + } + + @SuppressWarnings("unchecked") + @Override + 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/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/ProgressUploadServlet.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/ProgressUploadServlet.java new file mode 100644 index 00000000..fe42b6ec --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/ProgressUploadServlet.java @@ -0,0 +1,121 @@ +package io.github.dunwu.javaee.servlet.upload; + +import org.apache.commons.fileupload.FileItem; +import org.apache.commons.fileupload.disk.DiskFileItemFactory; +import org.apache.commons.fileupload.servlet.ServletFileUpload; + +import java.io.*; +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; + +public class ProgressUploadServlet extends HttpServlet { + + private static final long serialVersionUID = -4935921396709035718L; + + @Override + 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()); + } + } + + @Override + 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/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/UploadListener.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/UploadListener.java new file mode 100644 index 00000000..3812ae18 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/UploadListener.java @@ -0,0 +1,19 @@ +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/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/UploadStatus.java b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/UploadStatus.java new file mode 100644 index 00000000..0483f070 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/java/io/github/dunwu/javaee/servlet/upload/UploadStatus.java @@ -0,0 +1,45 @@ +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/javaee-servlet/src/main/resources/logback.xml b/codes/javaee/javaee-servlet/src/main/resources/logback.xml new file mode 100644 index 00000000..0062bccc --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/resources/logback.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + %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/javaee-servlet/src/main/webapp/META-INF/MANIFEST.MF b/codes/javaee/javaee-servlet/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 00000000..254272e1 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/codes/javaee/javaee-servlet/src/main/webapp/WEB-INF/notice.html b/codes/javaee/javaee-servlet/src/main/webapp/WEB-INF/notice.html new file mode 100644 index 00000000..aebd0084 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/webapp/WEB-INF/notice.html @@ -0,0 +1,116 @@ + + + + + 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/javaee-servlet/src/main/webapp/WEB-INF/web.xml b/codes/javaee/javaee-servlet/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..38144543 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,216 @@ + + + + + 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/javaee-servlet/src/main/webapp/index.jsp b/codes/javaee/javaee-servlet/src/main/webapp/index.jsp new file mode 100644 index 00000000..b4a2d228 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/main/webapp/index.jsp @@ -0,0 +1,33 @@ +<%@ page contentType="text/html;charset=UTF-8" language="java" %> + + + javaee-web + + +

首页

+

欢迎访问

+

测试链接

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

+
+
+
+
+
+ + diff --git a/codes/javaee/javaee-servlet/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java b/codes/javaee/javaee-servlet/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java new file mode 100644 index 00000000..460b9e3f --- /dev/null +++ b/codes/javaee/javaee-servlet/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java @@ -0,0 +1,114 @@ +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 { + + public static final int IDE_ECLIPSE = 0; + + public static final int IDE_INTELLIJ = 1; + + 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 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 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, '|')); + } + + 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"); + } + +} diff --git a/codes/javaee/javaee-servlet/src/test/java/io/github/dunwu/javaee/server/Profiles.java b/codes/javaee/javaee-servlet/src/test/java/io/github/dunwu/javaee/server/Profiles.java new file mode 100644 index 00000000..dcfd3e39 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/test/java/io/github/dunwu/javaee/server/Profiles.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * 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/javaee-servlet/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java b/codes/javaee/javaee-servlet/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java new file mode 100644 index 00000000..049c8802 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java @@ -0,0 +1,35 @@ +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/javaee-servlet/src/test/resources/jetty/webdefault.xml b/codes/javaee/javaee-servlet/src/test/resources/jetty/webdefault.xml new file mode 100644 index 00000000..b991d44c --- /dev/null +++ b/codes/javaee/javaee-servlet/src/test/resources/jetty/webdefault.xml @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + 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/javaee-servlet/src/test/resources/logback.xml b/codes/javaee/javaee-servlet/src/test/resources/logback.xml new file mode 100644 index 00000000..5b969d92 --- /dev/null +++ b/codes/javaee/javaee-servlet/src/test/resources/logback.xml @@ -0,0 +1,46 @@ + + + + + + + + + + %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/javaee-session/pom.xml b/codes/javaee/javaee-session/pom.xml new file mode 100644 index 00000000..cd0d5743 --- /dev/null +++ b/codes/javaee/javaee-session/pom.xml @@ -0,0 +1,88 @@ + + 4.0.0 + + + io.github.dunwu.javaee + javaee + 1.0.0 + + + io.github.dunwu + javaee-session + 1.0.0 + war + javaee-session + JavaEE 学习笔记之 session + + + UTF-8 + 1.7 + ${java.version} + ${java.version} + + + + + + ch.qos.logback + logback-classic + + + org.slf4j + jcl-over-slf4j + + + + + + org.apache.commons + commons-lang3 + + + + + + javax.servlet + javax.servlet-api + + + + + + org.eclipse.jetty + jetty-webapp + test + + + org.eclipse.jetty + jetty-annotations + test + + + org.eclipse.jetty + apache-jsp + test + + + org.eclipse.jetty + apache-jstl + test + + + + + + junit + junit + test + + + org.assertj + assertj-core + test + + + + + diff --git a/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/AddCookies.java b/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/AddCookies.java new file mode 100644 index 00000000..d30df96a --- /dev/null +++ b/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/AddCookies.java @@ -0,0 +1,68 @@ +/** + * 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 + * @since 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#doPost(HttpServletRequest request, HttpServletResponse response) + */ + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + doGet(request, response); + } + + /** + * @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" + ""); + } + +} diff --git a/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/DeleteCookies.java b/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/DeleteCookies.java new file mode 100644 index 00000000..b3cd3193 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/DeleteCookies.java @@ -0,0 +1,75 @@ +/** + * 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 + * @since 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#doPost(HttpServletRequest request, HttpServletResponse response) + */ + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + doGet(request, response); + } + + /** + * @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(""); + } + +} diff --git a/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/ReadCookies.java b/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/ReadCookies.java new file mode 100644 index 00000000..add3ecab --- /dev/null +++ b/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/ReadCookies.java @@ -0,0 +1,76 @@ +/** + * 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 + * @since 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#doPost(HttpServletRequest request, HttpServletResponse response) + */ + protected void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + doGet(request, response); + } + + /** + * @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(""); + } + +} diff --git a/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/bean/Person.java b/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/bean/Person.java new file mode 100644 index 00000000..6e4d9271 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/bean/Person.java @@ -0,0 +1,65 @@ +/** + * 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 + * @since 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/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/bean/Topic.java b/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/bean/Topic.java new file mode 100644 index 00000000..4e263d60 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/cookie/bean/Topic.java @@ -0,0 +1,42 @@ +/** + * The Apache License 2.0 Copyright (c) 2017 Zhang Peng + */ +package io.github.dunwu.javaee.cookie.bean; + +/** + * @author Zhang Peng + * @since 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/javaee-session/src/main/java/io/github/dunwu/javaee/session/SessionTrackServlet.java b/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/session/SessionTrackServlet.java new file mode 100644 index 00000000..a495546d --- /dev/null +++ b/codes/javaee/javaee-session/src/main/java/io/github/dunwu/javaee/session/SessionTrackServlet.java @@ -0,0 +1,69 @@ +/** + * 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 + * @since 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/javaee-session/src/main/resources/logback.xml b/codes/javaee/javaee-session/src/main/resources/logback.xml new file mode 100644 index 00000000..0e8f7363 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/resources/logback.xml @@ -0,0 +1,45 @@ + + + + + + + + + + %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/javaee-session/src/main/webapp/META-INF/MANIFEST.MF b/codes/javaee/javaee-session/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 00000000..254272e1 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/codes/javaee/javaee-session/src/main/webapp/META-INF/context.xml b/codes/javaee/javaee-session/src/main/webapp/META-INF/context.xml new file mode 100644 index 00000000..8f05edc7 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/META-INF/context.xml @@ -0,0 +1,4 @@ + + + + diff --git a/codes/javaee/javaee-session/src/main/webapp/WEB-INF/web.xml b/codes/javaee/javaee-session/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..83c873a5 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,49 @@ + + + + + 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/javaee-session/src/main/webapp/encodeURL.jsp b/codes/javaee/javaee-session/src/main/webapp/encodeURL.jsp new file mode 100644 index 00000000..c2aeaad0 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/encodeURL.jsp @@ -0,0 +1,15 @@ +<%@ page language="java" pageEncoding="UTF-8" %> + + + + Cookie Encoding + + +">Homepage + +<%= response.encodeURL("index.jsp") %>?c=1&wd=Java + + + + + diff --git a/codes/javaee/javaee-session/src/main/webapp/index.jsp b/codes/javaee/javaee-session/src/main/webapp/index.jsp new file mode 100644 index 00000000..79af5bcc --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/index.jsp @@ -0,0 +1,26 @@ +<%@ page language="java" 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/javaee-session/src/main/webapp/views/css/style.css b/codes/javaee/javaee-session/src/main/webapp/views/css/style.css new file mode 100644 index 00000000..2ae2e7bd --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/css/style.css @@ -0,0 +1,51 @@ +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: #FFFFFF; + font-weight: bold; + font-size: 11px; + text-align: center; + padding: .17em 0 .2em .17em; + border-style: solid; + border-width: 1px; + border-color: #99CCFF #115599 #115599 #99CCFF; + background: #6699CC url(../images/bg-btn-blue.gif) repeat-x; +} diff --git a/codes/javaee/javaee-session/src/main/webapp/views/images/bg-btn-blue.gif b/codes/javaee/javaee-session/src/main/webapp/views/images/bg-btn-blue.gif new file mode 100644 index 00000000..bc03f1bd Binary files /dev/null and b/codes/javaee/javaee-session/src/main/webapp/views/images/bg-btn-blue.gif differ diff --git a/codes/javaee/javaee-session/src/main/webapp/views/images/cookie.gif b/codes/javaee/javaee-session/src/main/webapp/views/images/cookie.gif new file mode 100644 index 00000000..6c6bd580 Binary files /dev/null and b/codes/javaee/javaee-session/src/main/webapp/views/images/cookie.gif differ diff --git a/codes/javaee/javaee-session/src/main/webapp/views/images/errorstate.gif b/codes/javaee/javaee-session/src/main/webapp/views/images/errorstate.gif new file mode 100644 index 00000000..a3b621b5 Binary files /dev/null and b/codes/javaee/javaee-session/src/main/webapp/views/images/errorstate.gif differ diff --git a/codes/javaee/javaee-session/src/main/webapp/views/images/mail.gif b/codes/javaee/javaee-session/src/main/webapp/views/images/mail.gif new file mode 100644 index 00000000..0f2b0d76 Binary files /dev/null and b/codes/javaee/javaee-session/src/main/webapp/views/images/mail.gif differ diff --git a/codes/javaee/javaee-session/src/main/webapp/views/images/vertical_line.gif b/codes/javaee/javaee-session/src/main/webapp/views/images/vertical_line.gif new file mode 100644 index 00000000..65f8ee78 Binary files /dev/null and b/codes/javaee/javaee-session/src/main/webapp/views/images/vertical_line.gif differ diff --git a/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/addCookies.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/addCookies.jsp new file mode 100644 index 00000000..58b5ff9e --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/addCookies.jsp @@ -0,0 +1,16 @@ +<%@ page language="java" pageEncoding="UTF-8" %> + + + + + 添加Cookie + + +
+ 站点名 : +
+ 站点 URL:
+ +
+ + diff --git a/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/base64.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/base64.jsp new file mode 100644 index 00000000..49b525ac --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/base64.jsp @@ -0,0 +1,35 @@ +<%@ 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/javaee-session/src/main/webapp/views/jsp/cookie/base64_decode.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/base64_decode.jsp new file mode 100644 index 00000000..e317b628 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/base64_decode.jsp @@ -0,0 +1,32 @@ +<%@ 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; + } + } + +%> diff --git a/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/cookie.gif b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/cookie.gif new file mode 100644 index 00000000..6c6bd580 Binary files /dev/null and b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/cookie.gif differ diff --git a/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/cookie.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/cookie.jsp new file mode 100644 index 00000000..e6d40bbd --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/cookie.jsp @@ -0,0 +1,81 @@ +<%@ 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/javaee-session/src/main/webapp/views/jsp/cookie/cookieAttribute.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/cookieAttribute.jsp new file mode 100644 index 00000000..2fda1553 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/cookieAttribute.jsp @@ -0,0 +1,30 @@ +<%@ 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/javaee-session/src/main/webapp/views/jsp/cookie/cookieDomain.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/cookieDomain.jsp new file mode 100644 index 00000000..4bfa7b8b --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/cookieDomain.jsp @@ -0,0 +1,27 @@ +<%@ 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/javaee-session/src/main/webapp/views/jsp/cookie/encoding.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/encoding.jsp new file mode 100644 index 00000000..02391beb --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/encoding.jsp @@ -0,0 +1,33 @@ +<%@ 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/javaee-session/src/main/webapp/views/jsp/cookie/history.js b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/history.js new file mode 100644 index 00000000..52dee341 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/history.js @@ -0,0 +1,27 @@ +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/javaee-session/src/main/webapp/views/jsp/cookie/history.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/history.jsp new file mode 100644 index 00000000..55a2886a --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/history.jsp @@ -0,0 +1,64 @@ +<%@ page language="java" pageEncoding="UTF-8" isErrorPage="false" %> +<%! + +%> +<% + + +%> + + + + . + + + +
+
+ 当前有效的 Cookie + +
+
+ 历史记录 +
+ + + + + + + + + + + + + + + + + +
+ 帐号: + + +
+ 密码: + + +
+ 有效期: + + 关闭浏览器即失效
+ 30天内有效
+ 永久有效
+
+ + +
+
+
+
+ + + diff --git a/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/javascript.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/javascript.jsp new file mode 100644 index 00000000..1060d5d5 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/javascript.jsp @@ -0,0 +1,106 @@ +<%@ page language="java" pageEncoding="UTF-8" %> + + + + 欢迎您 + + + + +
+
+ 当前有效的 Cookie +
+ +
+
+ + 欢迎您 + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 读取 Cookie: + + +
+   + +
+ 设置 Cookie: + +
+ Name 属性: + + +
+ Value 属性: + + +
+ + +
+
+
+ + diff --git a/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/login.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/login.jsp new file mode 100644 index 00000000..09f2a0a3 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/login.jsp @@ -0,0 +1,68 @@ +<%@ 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/javaee-session/src/main/webapp/views/jsp/cookie/loginCookie.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/loginCookie.jsp new file mode 100644 index 00000000..00ce6801 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/loginCookie.jsp @@ -0,0 +1,165 @@ +<%@ 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/javaee-session/src/main/webapp/views/jsp/cookie/maxAge.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/maxAge.jsp new file mode 100644 index 00000000..afc2ff95 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/maxAge.jsp @@ -0,0 +1,26 @@ +<%@ 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/javaee-session/src/main/webapp/views/jsp/cookie/setCookie.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/setCookie.jsp new file mode 100644 index 00000000..bf6c1234 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/cookie/setCookie.jsp @@ -0,0 +1,109 @@ +<%@ 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/javaee-session/src/main/webapp/views/jsp/session/session.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/session/session.jsp new file mode 100644 index 00000000..1a547152 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/session/session.jsp @@ -0,0 +1,94 @@ +<%@ 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/javaee-session/src/main/webapp/views/jsp/session/vote.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/session/vote.jsp new file mode 100644 index 00000000..17d7c865 --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/session/vote.jsp @@ -0,0 +1,52 @@ +<%@ page language="java" pageEncoding="UTF-8" %> + + + + 欢迎您 + + + +
+
+ 投票 + + + + + + + + + + + + + + + + + + + + + +
+ 您的姓名: + +
+ +
+ 您的年龄: + +
+ 您的生日: + +
+ + +
+
+
+ + + diff --git a/codes/javaee/javaee-session/src/main/webapp/views/jsp/session/welcome.jsp b/codes/javaee/javaee-session/src/main/webapp/views/jsp/session/welcome.jsp new file mode 100644 index 00000000..5cb7b51b --- /dev/null +++ b/codes/javaee/javaee-session/src/main/webapp/views/jsp/session/welcome.jsp @@ -0,0 +1,63 @@ +<%@ 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/javaee-session/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java b/codes/javaee/javaee-session/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java new file mode 100644 index 00000000..c15c631a --- /dev/null +++ b/codes/javaee/javaee-session/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java @@ -0,0 +1,114 @@ +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 { + + public static final int IDE_ECLIPSE = 0; + + public static final int IDE_INTELLIJ = 1; + + 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 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 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, '|')); + } + + 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"); + } + +} diff --git a/codes/javaee/javaee-session/src/test/java/io/github/dunwu/javaee/server/Profiles.java b/codes/javaee/javaee-session/src/test/java/io/github/dunwu/javaee/server/Profiles.java new file mode 100644 index 00000000..dcfd3e39 --- /dev/null +++ b/codes/javaee/javaee-session/src/test/java/io/github/dunwu/javaee/server/Profiles.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * 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/javaee-session/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java b/codes/javaee/javaee-session/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java new file mode 100644 index 00000000..049c8802 --- /dev/null +++ b/codes/javaee/javaee-session/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java @@ -0,0 +1,35 @@ +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/javaee-session/src/test/resources/jetty/webdefault.xml b/codes/javaee/javaee-session/src/test/resources/jetty/webdefault.xml new file mode 100644 index 00000000..b991d44c --- /dev/null +++ b/codes/javaee/javaee-session/src/test/resources/jetty/webdefault.xml @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + 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/javaee-taglib/pom.xml b/codes/javaee/javaee-taglib/pom.xml new file mode 100644 index 00000000..ab7de1e7 --- /dev/null +++ b/codes/javaee/javaee-taglib/pom.xml @@ -0,0 +1,118 @@ + + + 4.0.0 + + + io.github.dunwu.javaee + javaee + 1.0.0 + + + + io.github.dunwu + javaee-taglib + 1.0.0 + war + + + + + javaee-taglib + JavaEE 学习笔记之 taglib + + + + + + UTF-8 + 1.7 + ${java.version} + ${java.version} + + + + + + ch.qos.logback + logback-classic + + + org.slf4j + jcl-over-slf4j + + + + + + org.apache.commons + commons-lang3 + + + + + + javax.servlet + javax.servlet-api + provided + + + javax.servlet.jsp + javax.servlet.jsp-api + provided + + + + + + org.eclipse.jetty + jetty-webapp + test + + + org.eclipse.jetty + jetty-annotations + test + + + org.eclipse.jetty + apache-jsp + test + + + org.eclipse.jetty + apache-jstl + test + + + + + + junit + junit + test + + + org.assertj + assertj-core + test + + + + + + + xalan + xalan + 2.7.2 + + + xerces + xercesImpl + 2.11.0 + + + + + diff --git a/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/Copyright.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/Copyright.java new file mode 100644 index 00000000..a1208fb2 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/Copyright.java @@ -0,0 +1,62 @@ +/** + * 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 + * @since 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 setParent(Tag parent) { + this.parent = parent; + } + + @Override + public void release() { + } + + @Override + public void setPageContext(PageContext pageContext) { + this.pageContext = pageContext; + } + +} diff --git a/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag.java new file mode 100644 index 00000000..348c1d2e --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag.java @@ -0,0 +1,24 @@ +/** + * The Apache License 2.0 Copyright (c) 2017 Zhang Peng + */ +package io.github.dunwu.javaee.taglib; + +/** + * @author Zhang Peng + * @since 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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag2.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag2.java new file mode 100644 index 00000000..604014b7 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag2.java @@ -0,0 +1,24 @@ +/** + * 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 + * @since 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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag3.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag3.java new file mode 100644 index 00000000..5aeb9a3a --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/HelloTag3.java @@ -0,0 +1,38 @@ +/** + * 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 + * @since 2017/4/3. + */ +public class HelloTag3 extends SimpleTagSupport { + + StringWriter sw = new StringWriter(); + + private String message; + + public void setMessage(String msg) { + this.message = msg; + } + + 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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/bean/Person.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/bean/Person.java new file mode 100644 index 00000000..07868678 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/bean/Person.java @@ -0,0 +1,105 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/function/Function.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/function/Function.java new file mode 100644 index 00000000..9858e9c6 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/function/Function.java @@ -0,0 +1,62 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/AddTag.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/AddTag.java new file mode 100644 index 00000000..a8f23eac --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/AddTag.java @@ -0,0 +1,32 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/Copyright.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/Copyright.java new file mode 100644 index 00000000..649a5aa4 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/Copyright.java @@ -0,0 +1,57 @@ +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 setParent(Tag parent) { + this.parent = parent; + } + + @Override + public void release() { + } + + @Override + public void setPageContext(PageContext pageContext) { + this.pageContext = pageContext; + } + +} + +// end diff --git a/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/Copyright2.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/Copyright2.java new file mode 100644 index 00000000..93e6c32f --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/Copyright2.java @@ -0,0 +1,33 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/DynamicAttributeTag.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/DynamicAttributeTag.java new file mode 100644 index 00000000..69edd78d --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/DynamicAttributeTag.java @@ -0,0 +1,58 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/HelloTag.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/HelloTag.java new file mode 100644 index 00000000..68fe5624 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/HelloTag.java @@ -0,0 +1,32 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/IteratorTag.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/IteratorTag.java new file mode 100644 index 00000000..22eaad19 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/IteratorTag.java @@ -0,0 +1,31 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/LoopTag.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/LoopTag.java new file mode 100644 index 00000000..d89122ff --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/LoopTag.java @@ -0,0 +1,40 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/ToLowerCaseTag.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/ToLowerCaseTag.java new file mode 100644 index 00000000..60e372b3 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/ToLowerCaseTag.java @@ -0,0 +1,23 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/table/Column.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/table/Column.java new file mode 100644 index 00000000..016c7746 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/table/Column.java @@ -0,0 +1,64 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/table/Table.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/table/Table.java new file mode 100644 index 00000000..5b13dd31 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags/table/Table.java @@ -0,0 +1,203 @@ +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; + } + + /** + * 首字母大写 + * + * @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 Object getItems() { + return items; + } + + public void setItems(Object items) { + this.items = items; + } + + 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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/MultiAttributeTag.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/MultiAttributeTag.java new file mode 100644 index 00000000..610efed7 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/MultiAttributeTag.java @@ -0,0 +1,44 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/MultiTag.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/MultiTag.java new file mode 100644 index 00000000..6ec8ca32 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/MultiTag.java @@ -0,0 +1,34 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/RepeatTag.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/RepeatTag.java new file mode 100644 index 00000000..ecd9dd61 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/RepeatTag.java @@ -0,0 +1,17 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/ToUpperCaseTag.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/ToUpperCaseTag.java new file mode 100644 index 00000000..e48cb349 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/tags2/ToUpperCaseTag.java @@ -0,0 +1,28 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/test/Messages.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/test/Messages.java new file mode 100644 index 00000000..624e2da6 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/test/Messages.java @@ -0,0 +1,34 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/test/TestMessage.java b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/test/TestMessage.java new file mode 100644 index 00000000..e97e5ed9 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/test/TestMessage.java @@ -0,0 +1,9 @@ +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/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/test/messages.properties b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/test/messages.properties new file mode 100644 index 00000000..c99d9bfc --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/java/io/github/dunwu/javaee/taglib/test/messages.properties @@ -0,0 +1 @@ +TestMessage.0=test {0}, {1} diff --git a/codes/javaee/javaee-taglib/src/main/resources/copyright.properties b/codes/javaee/javaee-taglib/src/main/resources/copyright.properties new file mode 100644 index 00000000..2a835e8b --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/resources/copyright.properties @@ -0,0 +1 @@ +copyright=©2016-2017, Zhang Peng diff --git a/codes/javaee/javaee-taglib/src/main/resources/logback.xml b/codes/javaee/javaee-taglib/src/main/resources/logback.xml new file mode 100644 index 00000000..112831e0 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/resources/logback.xml @@ -0,0 +1,46 @@ + + + + + + + + + + %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/javaee-taglib/src/main/webapp/WEB-INF/tld/function.tld b/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/tld/function.tld new file mode 100644 index 00000000..4bf0a5e4 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/tld/function.tld @@ -0,0 +1,44 @@ + + + + + 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/javaee-taglib/src/main/webapp/WEB-INF/tld/hello.tld b/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/tld/hello.tld new file mode 100644 index 00000000..2dca9237 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/tld/hello.tld @@ -0,0 +1,10 @@ + + 1.0 + 2.0 + Example TLD + + Hello + io.github.dunwu.javaee.taglib.HelloTag + empty + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/tld/hello2.tld b/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/tld/hello2.tld new file mode 100644 index 00000000..44728ef9 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/tld/hello2.tld @@ -0,0 +1,10 @@ + + 1.0 + 2.0 + Example TLD with Body + + Hello + io.github.dunwu.javaee.taglib.HelloTag2 + scriptless + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/tld/hello3.tld b/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/tld/hello3.tld new file mode 100644 index 00000000..6d995a74 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/tld/hello3.tld @@ -0,0 +1,13 @@ + + 1.0 + 2.0 + Example TLD with Body + + Hello + io.github.dunwu.javaee.taglib.HelloTag3 + scriptless + + message + + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/tld/taglib.tld b/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/tld/taglib.tld new file mode 100644 index 00000000..0874c62f --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/tld/taglib.tld @@ -0,0 +1,149 @@ + + + + + + + 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/javaee-taglib/src/main/webapp/WEB-INF/web.xml b/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..a403cc53 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,25 @@ + + + + + HelloServlet + /examples/configuration.jsp + + message + welcome to jsp + + 1 + + + HelloServlet + /config + /config.jsp + + + + + /WEB-INF/views/jsp/index.jsp + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/dynamic.jsp b/codes/javaee/javaee-taglib/src/main/webapp/dynamic.jsp new file mode 100644 index 00000000..abb85b60 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/dynamic.jsp @@ -0,0 +1,18 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> +<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib" %> + + + + Insert title here + + + + + + + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/function.jsp b/codes/javaee/javaee-taglib/src/main/webapp/function.jsp new file mode 100644 index 00000000..67b405b6 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/function.jsp @@ -0,0 +1,46 @@ +<%@ 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) }
+ + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/hello.jsp b/codes/javaee/javaee-taglib/src/main/webapp/hello.jsp new file mode 100644 index 00000000..2aa13cd9 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/hello.jsp @@ -0,0 +1,15 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> +<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib" %> + + + + Insert title here + + + +
+
+
+ + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/I.png b/codes/javaee/javaee-taglib/src/main/webapp/images/I.png new file mode 100644 index 00000000..e8512fb9 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/I.png differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/L.png b/codes/javaee/javaee-taglib/src/main/webapp/images/L.png new file mode 100644 index 00000000..eb334eda Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/L.png differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/Lminus.png b/codes/javaee/javaee-taglib/src/main/webapp/images/Lminus.png new file mode 100644 index 00000000..f7c43c0a Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/Lminus.png differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/Lplus.png b/codes/javaee/javaee-taglib/src/main/webapp/images/Lplus.png new file mode 100644 index 00000000..848ec2fc Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/Lplus.png differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/T.png b/codes/javaee/javaee-taglib/src/main/webapp/images/T.png new file mode 100644 index 00000000..30173254 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/T.png differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/Thumbs.db b/codes/javaee/javaee-taglib/src/main/webapp/images/Thumbs.db new file mode 100644 index 00000000..afada40f Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/Thumbs.db differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/Tminus.png b/codes/javaee/javaee-taglib/src/main/webapp/images/Tminus.png new file mode 100644 index 00000000..2260e424 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/Tminus.png differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/Tplus.png b/codes/javaee/javaee-taglib/src/main/webapp/images/Tplus.png new file mode 100644 index 00000000..2c8d8f4f Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/Tplus.png differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/asc.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/asc.gif new file mode 100644 index 00000000..ca472c09 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/asc.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/bg-btn-blue.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/bg-btn-blue.gif new file mode 100644 index 00000000..bc03f1bd Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/bg-btn-blue.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/bg_4.jpg b/codes/javaee/javaee-taglib/src/main/webapp/images/bg_4.jpg new file mode 100644 index 00000000..076cdece Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/bg_4.jpg differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/blank.png b/codes/javaee/javaee-taglib/src/main/webapp/images/blank.png new file mode 100644 index 00000000..cee9cd37 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/blank.png differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/bottom-left.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/bottom-left.gif new file mode 100644 index 00000000..8ff0da62 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/bottom-left.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/bottom-right.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/bottom-right.gif new file mode 100644 index 00000000..03a51b0a Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/bottom-right.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/btn-go-dark.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/btn-go-dark.gif new file mode 100644 index 00000000..206207a7 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/btn-go-dark.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/delete.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/delete.gif new file mode 100644 index 00000000..c610608f Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/delete.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/delimiter.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/delimiter.gif new file mode 100644 index 00000000..5bfd67a2 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/delimiter.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/desc.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/desc.gif new file mode 100644 index 00000000..dfab21c1 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/desc.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/edit.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/edit.gif new file mode 100644 index 00000000..25583265 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/edit.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/element.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/element.gif new file mode 100644 index 00000000..b07b87b1 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/element.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/file.png b/codes/javaee/javaee-taglib/src/main/webapp/images/file.png new file mode 100644 index 00000000..a20c6fa0 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/file.png differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/foldericon.png b/codes/javaee/javaee-taglib/src/main/webapp/images/foldericon.png new file mode 100644 index 00000000..2684748b Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/foldericon.png differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/ibm-tab-background.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/ibm-tab-background.gif new file mode 100644 index 00000000..d36dc073 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/ibm-tab-background.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/ibm_logo.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/ibm_logo.gif new file mode 100644 index 00000000..5d1d0477 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/ibm_logo.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/left-nav-corner.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/left-nav-corner.gif new file mode 100644 index 00000000..ae395ea6 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/left-nav-corner.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/leftnav-overview-highlight.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/leftnav-overview-highlight.gif new file mode 100644 index 00000000..2996ad0e Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/leftnav-overview-highlight.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/line01.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/line01.gif new file mode 100644 index 00000000..6b9a6e3f Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/line01.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/masthead-links-gradient.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/masthead-links-gradient.gif new file mode 100644 index 00000000..b20772a4 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/masthead-links-gradient.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/memo.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/memo.gif new file mode 100644 index 00000000..2387e310 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/memo.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/new.png b/codes/javaee/javaee-taglib/src/main/webapp/images/new.png new file mode 100644 index 00000000..a20c6fa0 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/new.png differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/next.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/next.gif new file mode 100644 index 00000000..d0d7d2fe Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/next.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/openfoldericon.png b/codes/javaee/javaee-taglib/src/main/webapp/images/openfoldericon.png new file mode 100644 index 00000000..15fcd567 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/openfoldericon.png differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/pagetools-gradient.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/pagetools-gradient.gif new file mode 100644 index 00000000..7beb3cb9 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/pagetools-gradient.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/pagetools_gradient_a.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/pagetools_gradient_a.gif new file mode 100644 index 00000000..50b38ed0 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/pagetools_gradient_a.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/password.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/password.gif new file mode 100644 index 00000000..8e3d48a6 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/password.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/prev.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/prev.gif new file mode 100644 index 00000000..e7d51fd7 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/prev.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/project.png b/codes/javaee/javaee-taglib/src/main/webapp/images/project.png new file mode 100644 index 00000000..4a773bc3 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/project.png differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/pspbrwse.jbf b/codes/javaee/javaee-taglib/src/main/webapp/images/pspbrwse.jbf new file mode 100644 index 00000000..1485c076 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/pspbrwse.jbf differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/rl-bullet.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/rl-bullet.gif new file mode 100644 index 00000000..60046e74 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/rl-bullet.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/role.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/role.gif new file mode 100644 index 00000000..d28c326d Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/role.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/team.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/team.gif new file mode 100644 index 00000000..81fb7b4a Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/team.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/template-gradient.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/template-gradient.gif new file mode 100644 index 00000000..1bd540ea Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/template-gradient.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/top-content-shadow.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/top-content-shadow.gif new file mode 100644 index 00000000..34035a8a Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/top-content-shadow.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/top-left.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/top-left.gif new file mode 100644 index 00000000..00b8ab41 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/top-left.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/top-right.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/top-right.gif new file mode 100644 index 00000000..ed23cf8e Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/top-right.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/images/trans.gif b/codes/javaee/javaee-taglib/src/main/webapp/images/trans.gif new file mode 100644 index 00000000..6964168b Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/images/trans.gif differ diff --git a/codes/javaee/javaee-taglib/src/main/webapp/index.jsp b/codes/javaee/javaee-taglib/src/main/webapp/index.jsp new file mode 100644 index 00000000..b3346ace --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/index.jsp @@ -0,0 +1,83 @@ +<%@ page language="java" contentType="text/html; charset=gb2312" %> +<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib" %> + + + + Insert title here + + + + + + + + + + + + + + + + + + + + + + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/loop.jsp b/codes/javaee/javaee-taglib/src/main/webapp/loop.jsp new file mode 100644 index 00000000..540e2cb2 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/loop.jsp @@ -0,0 +1,15 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> +<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib" %> + + + + Insert title here + + + +
+Loop. 
+
+ + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/multi.jsp b/codes/javaee/javaee-taglib/src/main/webapp/multi.jsp new file mode 100644 index 00000000..c960cbd1 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/multi.jsp @@ -0,0 +1,13 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> +<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib" %> + + + + Insert title here + + + + + + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/multiAttribute.jsp b/codes/javaee/javaee-taglib/src/main/webapp/multiAttribute.jsp new file mode 100644 index 00000000..f245d55d --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/multiAttribute.jsp @@ -0,0 +1,21 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> +<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib" %> + + + + Insert title here + + + + + + 标签体一, + 标签体二, + + + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/style/style.css b/codes/javaee/javaee-taglib/src/main/webapp/style/style.css new file mode 100644 index 00000000..94ac1b2f --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/style/style.css @@ -0,0 +1,66 @@ +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: #FFFFFF; + 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: #99CCFF #115599 #115599 #99CCFF; + background: #6699CC url(../images/bg-btn-blue.gif) repeat-x; +} diff --git a/codes/javaee/javaee-taglib/src/main/webapp/table.jsp b/codes/javaee/javaee-taglib/src/main/webapp/table.jsp new file mode 100644 index 00000000..ea9c96ef --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/table.jsp @@ -0,0 +1,184 @@ +<%@ page language="java" contentType="text/html; charset=gb2312" %> +<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib" %> +<%@page import="io.github.dunwu.javaee.taglib.bean.Person" %> +<%@page import="java.util.ArrayList" %> +<%@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/javaee-taglib/src/main/webapp/taglib/copyright.jsp b/codes/javaee/javaee-taglib/src/main/webapp/taglib/copyright.jsp new file mode 100644 index 00000000..c8dc90db --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/taglib/copyright.jsp @@ -0,0 +1,13 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> +<%@ taglib uri="http://www.victorzhang.com/tags" prefix="taglib" %> + + + + Insert title here + + + +Hello, taglib + + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/taglib/helloTag.jsp b/codes/javaee/javaee-taglib/src/main/webapp/taglib/helloTag.jsp new file mode 100644 index 00000000..209768fb --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/taglib/helloTag.jsp @@ -0,0 +1,9 @@ +<%@ taglib prefix="ex" uri="/WEB-INF/tld/hello.tld" %> + + + A sample custom tag + + + + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/taglib/helloTag2.jsp b/codes/javaee/javaee-taglib/src/main/webapp/taglib/helloTag2.jsp new file mode 100644 index 00000000..ab30e991 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/taglib/helloTag2.jsp @@ -0,0 +1,11 @@ +<%@ taglib prefix="ex" uri="/WEB-INF/tld/hello2.tld" %> + + + A sample custom tag + + + + This is message body + + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/taglib/helloTag3.jsp b/codes/javaee/javaee-taglib/src/main/webapp/taglib/helloTag3.jsp new file mode 100644 index 00000000..e36e2737 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/taglib/helloTag3.jsp @@ -0,0 +1,9 @@ +<%@ taglib prefix="ex" uri="/WEB-INF/tld/hello3.tld" %> + + + A sample custom tag + + + + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/test.jsp b/codes/javaee/javaee-taglib/src/main/webapp/test.jsp new file mode 100644 index 00000000..8191a043 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/test.jsp @@ -0,0 +1,38 @@ +<%@ page language="java" contentType="text/html; charset=UTF-8" %> +<%@page import="io.github.dunwu.javaee.taglib.bean.Person" %> +<%@page import="java.util.ArrayList" %> +<%@page import="java.util.List" %> +<% + 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/javaee-taglib/src/main/webapp/toLowerCase.jsp b/codes/javaee/javaee-taglib/src/main/webapp/toLowerCase.jsp new file mode 100644 index 00000000..2e7b1f49 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/toLowerCase.jsp @@ -0,0 +1,13 @@ +<%@ 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. + + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/toUpperCase.jsp b/codes/javaee/javaee-taglib/src/main/webapp/toUpperCase.jsp new file mode 100644 index 00000000..c761cf88 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/main/webapp/toUpperCase.jsp @@ -0,0 +1,15 @@ +<%@ 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. + + + + diff --git a/codes/javaee/javaee-taglib/src/main/webapp/vote.gif b/codes/javaee/javaee-taglib/src/main/webapp/vote.gif new file mode 100644 index 00000000..26a5a3e5 Binary files /dev/null and b/codes/javaee/javaee-taglib/src/main/webapp/vote.gif differ diff --git a/codes/javaee/javaee-taglib/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java b/codes/javaee/javaee-taglib/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java new file mode 100644 index 00000000..f4706199 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/test/java/io/github/dunwu/javaee/server/JettyFactory.java @@ -0,0 +1,114 @@ +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 { + + public static final int IDE_ECLIPSE = 0; + + public static final int IDE_INTELLIJ = 1; + + 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 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) { + 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 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, '|')); + } + + 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"); + } + +} diff --git a/codes/javaee/javaee-taglib/src/test/java/io/github/dunwu/javaee/server/Profiles.java b/codes/javaee/javaee-taglib/src/test/java/io/github/dunwu/javaee/server/Profiles.java new file mode 100644 index 00000000..dcfd3e39 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/test/java/io/github/dunwu/javaee/server/Profiles.java @@ -0,0 +1,34 @@ +/******************************************************************************* + * 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/javaee-taglib/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java b/codes/javaee/javaee-taglib/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java new file mode 100644 index 00000000..049c8802 --- /dev/null +++ b/codes/javaee/javaee-taglib/src/test/java/io/github/dunwu/javaee/server/QuickStartServer.java @@ -0,0 +1,35 @@ +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/javaee-taglib/src/test/resources/jetty/webdefault.xml b/codes/javaee/javaee-taglib/src/test/resources/jetty/webdefault.xml new file mode 100644 index 00000000..b991d44c --- /dev/null +++ b/codes/javaee/javaee-taglib/src/test/resources/jetty/webdefault.xml @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + 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/javaee-websocket/pom.xml b/codes/javaee/javaee-websocket/pom.xml new file mode 100644 index 00000000..2e170eb9 --- /dev/null +++ b/codes/javaee/javaee-websocket/pom.xml @@ -0,0 +1,120 @@ + + + 4.0.0 + + + io.github.dunwu.javaee + javaee + 1.0.0 + + + io.github.dunwu + javaee-websocket + 1.0.0 + war + javaee-websocket + JavaEE 学习笔记之 WebSocket + + + UTF-8 + 1.7 + ${java.version} + ${java.version} + + + + + + ch.qos.logback + logback-classic + + + org.slf4j + jcl-over-slf4j + + + + + + javax.servlet + javax.servlet-api + provided + + + javax.servlet.jsp + javax.servlet.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 + + + + + + + + org.apache.tomcat.maven + tomcat7-maven-plugin + 2.2 + + 8089 + /${artifactId} + UTF-8 + + + + org.eclipse.jetty + jetty-maven-plugin + 9.4.18.v20190429 + + + 8089 + + + /${artifactId} + + + + + + + diff --git a/codes/javaee/javaee-websocket/src/main/java/io/github/dunwu/javaee/servlet/SocketServlet.java b/codes/javaee/javaee-websocket/src/main/java/io/github/dunwu/javaee/servlet/SocketServlet.java new file mode 100644 index 00000000..c2730f56 --- /dev/null +++ b/codes/javaee/javaee-websocket/src/main/java/io/github/dunwu/javaee/servlet/SocketServlet.java @@ -0,0 +1,58 @@ +package io.github.dunwu.javaee.servlet; + +import io.github.dunwu.javaee.websocket.WebSocketServer; +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; + +/** + * 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 doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + this.doGet(request, response); + } + + 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 init() throws ServletException { + // Put your code here + } + +} diff --git a/codes/javaee/javaee-websocket/src/main/java/io/github/dunwu/javaee/websocket/WebSocketServer.java b/codes/javaee/javaee-websocket/src/main/java/io/github/dunwu/javaee/websocket/WebSocketServer.java new file mode 100644 index 00000000..d896a8d7 --- /dev/null +++ b/codes/javaee/javaee-websocket/src/main/java/io/github/dunwu/javaee/websocket/WebSocketServer.java @@ -0,0 +1,118 @@ +package io.github.dunwu.javaee.websocket; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +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.*; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; + +/** + * Websocket 消息处理中心 + * + * @author Zhang Peng + * @see https://github.com/jetty-project/embedded-jetty-websocket-examples + */ +@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>(); + + /** + * 发送广播消息 + * + * @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()); + } + } + } + } + } + + @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()); + } + +} diff --git a/codes/javaee/javaee-websocket/src/main/java/io/github/dunwu/javaee/websocket/WebSocketServerConfigurator.java b/codes/javaee/javaee-websocket/src/main/java/io/github/dunwu/javaee/websocket/WebSocketServerConfigurator.java new file mode 100644 index 00000000..4b515645 --- /dev/null +++ b/codes/javaee/javaee-websocket/src/main/java/io/github/dunwu/javaee/websocket/WebSocketServerConfigurator.java @@ -0,0 +1,19 @@ +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/javaee-websocket/src/main/resources/logback.xml b/codes/javaee/javaee-websocket/src/main/resources/logback.xml new file mode 100644 index 00000000..c9733094 --- /dev/null +++ b/codes/javaee/javaee-websocket/src/main/resources/logback.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + %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/javaee-websocket/src/main/webapp/WEB-INF/web.xml b/codes/javaee/javaee-websocket/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..942745f7 --- /dev/null +++ b/codes/javaee/javaee-websocket/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,23 @@ + + + + + SocketServlet + + io.github.dunwu.javaee.servlet.SocketServlet + + + + SocketServlet + /sock/* + + + + index.jsp + + + diff --git a/codes/javaee/javaee-websocket/src/main/webapp/websocketA.html b/codes/javaee/javaee-websocket/src/main/webapp/websocketA.html new file mode 100644 index 00000000..6b15a582 --- /dev/null +++ b/codes/javaee/javaee-websocket/src/main/webapp/websocketA.html @@ -0,0 +1,60 @@ + + + + + webscocket test + + + + + + + + +
+
+ + + diff --git a/codes/javaee/javaee-websocket/src/main/webapp/websocketB.html b/codes/javaee/javaee-websocket/src/main/webapp/websocketB.html new file mode 100644 index 00000000..b9b7b081 --- /dev/null +++ b/codes/javaee/javaee-websocket/src/main/webapp/websocketB.html @@ -0,0 +1,60 @@ + + + + + webscocket test + + + + + + + + +
+
+ + + diff --git a/codes/javaee/javaee-websocket/src/test/java/io/github/dunwu/javaee/server/DevServer.java b/codes/javaee/javaee-websocket/src/test/java/io/github/dunwu/javaee/server/DevServer.java new file mode 100644 index 00000000..1331516a --- /dev/null +++ b/codes/javaee/javaee-websocket/src/test/java/io/github/dunwu/javaee/server/DevServer.java @@ -0,0 +1,137 @@ +package io.github.dunwu.javaee.server; + +import com.google.common.collect.Lists; +import io.github.dunwu.javaee.websocket.WebSocketServer; +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 java.util.ArrayList; + +/** + * DevServer 可以工作在 Eclipse 和 Intellij 中,用来启动 jetty 服务。 Intellij 并不支持jetty,所以要想类似eclipse一样的使用jetty,需要配置webdefault.xml。 + * + * @author Zhang Peng + */ +@SuppressWarnings("unused") +public class DevServer { + + 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 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 void main(String[] args) { + + // 初始化 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); + } + } + + 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, '|')); + } + + 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"); + } + +} diff --git a/codes/javaee/javaee-websocket/src/test/resources/jetty/webdefault.xml b/codes/javaee/javaee-websocket/src/test/resources/jetty/webdefault.xml new file mode 100644 index 00000000..b991d44c --- /dev/null +++ b/codes/javaee/javaee-websocket/src/test/resources/jetty/webdefault.xml @@ -0,0 +1,534 @@ + + + + + + + + + + + + + + + + + + + + + + + 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/javaee-websocket/src/test/resources/logback.xml b/codes/javaee/javaee-websocket/src/test/resources/logback.xml new file mode 100644 index 00000000..6de5c7eb --- /dev/null +++ b/codes/javaee/javaee-websocket/src/test/resources/logback.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + %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/javaee/pom.xml b/codes/javaee/pom.xml new file mode 100644 index 00000000..376eb379 --- /dev/null +++ b/codes/javaee/pom.xml @@ -0,0 +1,111 @@ + + + 4.0.0 + + + io.github.dunwu + dunwu-parent + 0.5.1 + + + io.github.dunwu.javaee + javaee + 1.0.0 + pom + JAVAEE + + + javaee-servlet + javaee-jsp + javaee-session + javaee-filter + javaee-listener + javaee-jstl + javaee-taglib + javaee-oss + javaee-websocket + + + + UTF-8 + 1.6 + ${java.version} + ${java.version} + + + + + + + + 2.9.5 + + + + + + + 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 + + + + + + + diff --git a/codes/javalib/bean/pom.xml b/codes/javalib/bean/pom.xml deleted file mode 100644 index a65806db..00000000 --- a/codes/javalib/bean/pom.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - 4.0.0 - - - - - - io.github.dunwu.javalib - javalib-bean - 1.0.0 - jar - - - - - - - org.projectlombok - lombok - 1.16.8 - - - - - - junit - junit - 4.12 - test - - - - - - - - UTF-8 - 1.8 - ${java.version} - ${java.version} - - - - - - - - - - - true - src/main/resources - - - - - - - - ${project.artifactId} - javalib 之JavaBean库示例集锦 - - - - diff --git a/codes/javalib/bean/src/main/java/io/github/dunwu/javalib/bean/Company.java b/codes/javalib/bean/src/main/java/io/github/dunwu/javalib/bean/Company.java deleted file mode 100644 index 0913e707..00000000 --- a/codes/javalib/bean/src/main/java/io/github/dunwu/javalib/bean/Company.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.dunwu.javalib.bean; - -import java.util.List; - -import lombok.Data; -import lombok.NonNull; - -/** - * Lombok 示例 - * @see http://jnb.ociweb.com/jnb/jnbJan2010.html - * @author Zhang Peng - */ -@Data(staticConstructor = "of") -public class Company { - private final Person founder; - private String name; - protected List employees; -} diff --git a/codes/javalib/bean/src/main/java/io/github/dunwu/javalib/bean/Person.java b/codes/javalib/bean/src/main/java/io/github/dunwu/javalib/bean/Person.java deleted file mode 100644 index aa6f622e..00000000 --- a/codes/javalib/bean/src/main/java/io/github/dunwu/javalib/bean/Person.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.dunwu.javalib.bean; - -import lombok.Data; -import lombok.EqualsAndHashCode; -import lombok.ToString; - -/** - * Lombok 示例 - * @see http://jnb.ociweb.com/jnb/jnbJan2010.html - * @author Zhang Peng - */ -@Data -@ToString(exclude = "age") -@EqualsAndHashCode(exclude = { "age", "sex" }) -public class Person { - private String name; - private Integer age; - private String sex; -} diff --git a/codes/javalib/bean/src/test/java/io/github/dunwu/javalib/bean/LombokTest.java b/codes/javalib/bean/src/test/java/io/github/dunwu/javalib/bean/LombokTest.java deleted file mode 100644 index 9ddaa83f..00000000 --- a/codes/javalib/bean/src/test/java/io/github/dunwu/javalib/bean/LombokTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package io.github.dunwu.javalib.bean; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -import org.junit.Assert; -import org.junit.Test; - -import lombok.Cleanup; - -/** - * Lombok 单元测试 - * @see @see http://jnb.ociweb.com/jnb/jnbJan2010.html - * @author Zhang Peng - */ -public class LombokTest { - @Test - public void testData() { - 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); - - Company company = Company.of(huangshiren); - company.setName("黑心农产品公司"); - company.setEmployees(personList); - - System.out.println("公司名:" + company.getName()); - System.out.println("创始人:" + company.getFounder()); - System.out.println("员工信息"); - company.getEmployees().forEach(person -> { - System.out.println(person.toString()); - }); - } - - @Test - 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 - public void testToString() { - Person person = new Person(); - person.setName("张三"); - person.setAge(20); - person.setSex("男"); - System.out.println(person.toString()); - // output: Person(name=张三, sex=男) - } - - @Test - public void testEqualsAndHashCode() { - 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("男"); - - Assert.assertEquals(person, person2); - Assert.assertNotEquals(person, person3); - } -} diff --git a/codes/javalib/log/log4j-demo/pom.xml b/codes/javalib/log/log4j-demo/pom.xml deleted file mode 100644 index 64aee110..00000000 --- a/codes/javalib/log/log4j-demo/pom.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - 4.0.0 - - - - - - io.github.dunwu - javalib-log-log4j - 1.0.0 - jar - - - - - - - org.slf4j - slf4j-api - 1.7.25 - - - org.slf4j - slf4j-log4j12 - 1.7.25 - - - log4j - log4j - 1.2.17 - - - - - junit - junit - 4.12 - test - - - - - - - UTF-8 - 1.8 - ${java.version} - ${java.version} - 1.2.3 - - - - - - - - - - - true - src/main/resources - - - - - - - - ${project.artifactId} - log4j 示例 - - - - diff --git a/codes/javalib/log/log4j-demo/src/main/resources/log4j.xml b/codes/javalib/log/log4j-demo/src/main/resources/log4j.xml deleted file mode 100644 index a03e0144..00000000 --- a/codes/javalib/log/log4j-demo/src/main/resources/log4j.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/codes/javalib/log/log4j2-demo/pom.xml b/codes/javalib/log/log4j2-demo/pom.xml deleted file mode 100644 index a6f59c93..00000000 --- a/codes/javalib/log/log4j2-demo/pom.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - 4.0.0 - - - - - - io.github.dunwu - javalib-log-log4j2 - 1.0.0 - jar - - - - - - - org.slf4j - slf4j-api - 1.7.25 - - - org.apache.logging.log4j - log4j-slf4j-impl - 2.11.0 - - - org.apache.logging.log4j - log4j-api - 2.11.0 - - - org.apache.logging.log4j - log4j-core - 2.11.0 - - - - - junit - junit - 4.12 - test - - - - - - - UTF-8 - 1.8 - ${java.version} - ${java.version} - - - - - - - - - - - true - src/main/resources - - - - - - - - ${project.artifactId} - log4j2 示例 - - - - diff --git a/codes/javalib/log/log4j2-demo/src/main/resources/log4j2.xml b/codes/javalib/log/log4j2-demo/src/main/resources/log4j2.xml deleted file mode 100644 index f518533a..00000000 --- a/codes/javalib/log/log4j2-demo/src/main/resources/log4j2.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - ${logPattern} - - - - - - - - - - - - - - - - - - ${logPattern} - - - - - - - - - - - - - - - - - - ${logPattern} - - - - - - - - - - - - - - - - - - ${logPattern} - - - - - - - - - - - - - - - - - - ${logPattern} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/codes/javalib/log/logback-demo/pom.xml b/codes/javalib/log/logback-demo/pom.xml deleted file mode 100644 index cc5a1e26..00000000 --- a/codes/javalib/log/logback-demo/pom.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - 4.0.0 - - - - - - io.github.dunwu - javalib-log-logback - 1.0.0 - jar - - - - - - - ch.qos.logback - logback-classic - ${logback.version} - - - ch.qos.logback - logback-access - ${logback.version} - - - - - junit - junit - 4.12 - test - - - - - - - UTF-8 - 1.8 - ${java.version} - ${java.version} - 1.2.3 - - - - - - - - - - - true - src/main/resources - - - - - - - - ${project.artifactId} - logback 示例 - - - - diff --git a/codes/javalib/log/logback-demo/src/main/resources/logback.xml b/codes/javalib/log/logback-demo/src/main/resources/logback.xml deleted file mode 100644 index 71c5dce8..00000000 --- a/codes/javalib/log/logback-demo/src/main/resources/logback.xml +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - 10 - 100 - - - - - - - ${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 - - - - - - ${LOG_PATH}/logs/${FILE_NAME}-warn.%d{yyyy-MM-dd}.log - 30 - - - - - 10MB - - - - WARN - ACCEPT - DENY - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [javalib] [%thread] [%p] %c{36}#%M - %m%n - - - - - - ${LOG_PATH}/logs/${FILE_NAME}-info.%d{yyyy-MM-dd}.log - 30 - - - - - 10MB - - - - INFO - ACCEPT - DENY - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [javalib] [%thread] [%p] %c{36}#%M - %m%n - - - - - - ${LOG_PATH}/logs/${FILE_NAME}-debug.%d{yyyy-MM-dd}.log - 30 - - - - - 10MB - - - - DEBUG - ACCEPT - DENY - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [javalib] [%thread] [%p] %c{36}#%M - %m%n - - - - - - ${LOG_PATH}/logs/${FILE_NAME}-trace.%d{yyyy-MM-dd}.log - 30 - - - - - 10MB - - - - TRACE - ACCEPT - DENY - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [javalib] [%thread] [%p] %c{36}#%M - %m%n - - - - - - - - - - - - - - %d{yyyy-MM-dd HH:mm:ss.SSS} [javalib] [%thread] [%p] %c{36}#%M - %m%n - - - - - - - diff --git a/codes/javalib/log/pom.xml b/codes/javalib/log/pom.xml deleted file mode 100644 index 118bdba0..00000000 --- a/codes/javalib/log/pom.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - 4.0.0 - - - - - - io.github.dunwu - javalib-log - 1.0.0 - pom - - - - - logback-demo - log4j-demo - log4j2-demo - - - - - - UTF-8 - 1.8 - ${java.version} - ${java.version} - - - - - - - ${project.artifactId} - javalib 之日志库示例集锦 - - - - diff --git a/codes/javalib/pom.xml b/codes/javalib/pom.xml deleted file mode 100644 index 195020bc..00000000 --- a/codes/javalib/pom.xml +++ /dev/null @@ -1,75 +0,0 @@ - - - 4.0.0 - - - - - - io.github.dunwu - javalib - 1.0.0 - pom - - - - - bean - log - - - - - org.projectlombok - lombok - 1.16.8 - - - - - - ch.qos.logback - logback-core - 1.2.3 - - - ch.qos.logback - logback-classic - 1.2.3 - - - ch.qos.logback - logback-access - 1.2.3 - - - - - junit - junit - 4.12 - test - - - - - - - UTF-8 - 1.8 - ${java.version} - ${java.version} - - - - - - - - ${project.artifactId} - Java 库使用示例 - - - - diff --git a/codes/javatech/javatech-cache/pom.xml b/codes/javatech/javatech-cache/pom.xml new file mode 100644 index 00000000..4503c60c --- /dev/null +++ b/codes/javatech/javatech-cache/pom.xml @@ -0,0 +1,76 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.2.1.RELEASE + + + io.github.dunwu.javatech + javatech-cache + 1.0.0 + jar + JAVATECH-缓存示例 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-cache + + + net.sf.ehcache + ehcache + + + com.github.ben-manes.caffeine + caffeine + + + net.spy + spymemcached + 2.12.2 + + + com.google.guava + guava + 29.0-jre + + + org.springframework.boot + spring-boot-starter-test + + + org.projectlombok + lombok + + + mysql + mysql-connector-java + + + com.h2database + h2 + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/SpringBootDataCacheApplication.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/SpringBootDataCacheApplication.java new file mode 100644 index 00000000..4a5220d3 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/SpringBootDataCacheApplication.java @@ -0,0 +1,87 @@ +package io.github.dunwu.javatech; + +import io.github.dunwu.javatech.data.User; +import io.github.dunwu.javatech.data.UserDao; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; + +/** + * @author Zhang Peng + * @since 2019-10-14 + */ +@EnableCaching +@SpringBootApplication +public class SpringBootDataCacheApplication implements CommandLineRunner { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private final UserDao userDao; + + public SpringBootDataCacheApplication(UserDao userDao) { + this.userDao = userDao; + } + + public static void main(String[] args) { + SpringApplication.run(SpringBootDataCacheApplication.class, args); + } + + @Override + public void run(String... args) throws Exception { + + if (userDao != null) { + printDataSourceInfo(userDao.getJdbcTemplate()); + log.info("连接数据源成功!"); + } else { + log.error("连接数据源失败!"); + return; + } + + for (int i = 1; i <= 3; i++) { + User user = userDao.queryByName("张三"); + log.info("第 {} 次查询 name = {}", i, user.toString()); + } + + for (int i = 1; i <= 3; i++) { + User user = userDao.queryByName("李四"); + log.info("第 {} 次查询 name = {}", i, user.toString()); + } + + User result = userDao.queryByName("张三"); + result.setAddress("深圳"); + userDao.update(result); + + for (int i = 1; i <= 3; i++) { + User user = userDao.queryByName("张三"); + log.info("第 {} 次查询 name = {}", i, user.toString()); + } + } + + public void printDataSourceInfo(JdbcTemplate jdbcTemplate) throws SQLException { + + DataSource dataSource = jdbcTemplate.getDataSource(); + + Connection connection; + if (dataSource != null) { + connection = dataSource.getConnection(); + } else { + log.error("获取 DataSource 失败"); + return; + } + + if (connection != null) { + log.info("DB URL: {}", connection.getMetaData().getURL()); + } else { + log.error("获取 Connection 失败"); + } + } + +} diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/CaffeineDemo.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/CaffeineDemo.java new file mode 100644 index 00000000..484259a6 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/CaffeineDemo.java @@ -0,0 +1,23 @@ +package io.github.dunwu.javatech.cache; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import java.util.concurrent.TimeUnit; + +/** + * @author Zhang Peng + * @since 2020-07-09 + */ +public class CaffeineDemo { + + public static void main(String[] args) { + Cache cache = Caffeine.newBuilder() + .expireAfterWrite(1, TimeUnit.SECONDS) + .expireAfterAccess(1, TimeUnit.SECONDS) + .maximumSize(10) + .build(); + cache.put("hello", "hello"); + } + +} diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/GuavaCacheDemo.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/GuavaCacheDemo.java new file mode 100644 index 00000000..1cdb585e --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/GuavaCacheDemo.java @@ -0,0 +1,55 @@ +package io.github.dunwu.javatech.cache; + +import com.google.common.cache.*; + +import java.util.concurrent.TimeUnit; + +/** + * @author Zhang Peng + * @since 2020-07-09 + */ +public class GuavaCacheDemo { + + public static void main(String[] args) { + CacheLoader loader = new CacheLoader() { + @Override + public String load(String key) throws Exception { + Thread.sleep(1000); + if ("key".equals(key)) { + return null; + } + System.out.println(key + " is loaded from a cacheLoader!"); + return key + "'s value"; + } + }; + + RemovalListener removalListener = new RemovalListener() { + @Override + public void onRemoval(RemovalNotification removal) { + System.out.println("[" + removal.getKey() + ":" + removal.getValue() + "] is evicted!"); + } + }; + + LoadingCache testCache = CacheBuilder.newBuilder() + .maximumSize(7) + .expireAfterWrite(10, TimeUnit.MINUTES) + .removalListener(removalListener) + .build(loader); + + for (int i = 0; i < 10; i++) { + String key = "key" + i; + String value = "value" + i; + testCache.put(key, value); + System.out.println("[" + key + ":" + value + "] is put into cache!"); + } + + System.out.println(testCache.getIfPresent("key6")); + + try { + System.out.println(testCache.get("key")); + } catch (Exception e) { + e.printStackTrace(); + } + } + +} diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/LRUCache.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/LRUCache.java new file mode 100644 index 00000000..3d38eae3 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/LRUCache.java @@ -0,0 +1,60 @@ +package io.github.dunwu.javatech.cache; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 通过继承 LinkedHashMap 来实现一个简单的 LRUHashMap + * + * 核心思想就是:LRU (最近最少使用)算法 + * + * @author Zhang Peng + * @since 2020-01-18 + */ +class LRUCache extends LinkedHashMap { + + private final int max; + private Object lock; + + public LRUCache(int max) { + //无需扩容 + super((int) (max * 1.4f), 0.75f, true); + this.max = max; + this.lock = new Object(); + } + + /** + * 重写LinkedHashMap的removeEldestEntry方法即可 在Put的时候判断,如果为true,就会删除最老的 + * + * @param eldest + * @return + */ + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > max; + } + + public Object getValue(Object key) { + synchronized (lock) { + return get(key); + } + } + + public void putValue(Object key, Object value) { + synchronized (lock) { + put(key, value); + } + } + + public boolean removeValue(Object key) { + synchronized (lock) { + return remove(key) != null; + } + } + + public boolean removeAll() { + clear(); + return true; + } + +} diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/MemcachedDemo.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/MemcachedDemo.java new file mode 100644 index 00000000..9399b12d --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/cache/MemcachedDemo.java @@ -0,0 +1,294 @@ +package io.github.dunwu.javatech.cache; + +import net.spy.memcached.CASResponse; +import net.spy.memcached.CASValue; +import net.spy.memcached.MemcachedClient; + +import java.net.InetSocketAddress; +import java.util.concurrent.Future; + +/** + * Memcached 客户端连接示例 + * + * @author Zhang Peng + * @since 2020-07-10 + */ +public class MemcachedDemo { + + public static final String URL = "127.0.0.1"; + public static final int PORT = 11211; + + public static void main(String[] args) { + add(); + remove(); + append(); + prepend(); + cas(); + get(); + delete(); + incrAndDecr(); + } + + public static void add() { + try { + + // 连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加数据 + Future fo = mcc.set("MyKey", 900, "Free Education"); + + // 打印状态 + System.out.println("set status:" + fo.get()); + + // 输出 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 添加 + fo = mcc.add("MyKey", 900, "memcached"); + + // 打印状态 + System.out.println("add status:" + fo.get()); + + // 添加新key + fo = mcc.add("codingground", 900, "All Free Compilers"); + + // 打印状态 + System.out.println("add status:" + fo.get()); + + // 输出 + System.out.println("codingground value in cache - " + mcc.get("codingground")); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + + public static void remove() { + + try { + //连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加第一个 key=》value 对 + Future fo = mcc.set("MyKey", 900, "Free Education"); + + // 输出执行 add 方法后的状态 + System.out.println("add status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 添加新的 key + fo = mcc.replace("MyKey", 900, "Largest Tutorials' Library"); + + // 输出执行 set 方法后的状态 + System.out.println("replace status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + + public static void append() { + + try { + + // 连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加数据 + Future fo = mcc.set("MyKey", 900, "Free Education"); + + // 输出执行 set 方法后的状态 + System.out.println("set status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 对存在的key进行数据添加操作 + fo = mcc.append(900, "MyKey", " for All"); + + // 输出执行 set 方法后的状态 + System.out.println("append status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("codingground")); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + + public static void prepend() { + + try { + + // 连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加数据 + Future fo = mcc.set("MyKey", 900, "Education for All"); + + // 输出执行 set 方法后的状态 + System.out.println("set status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 对存在的key进行数据添加操作 + fo = mcc.prepend(900, "MyKey", "Free "); + + // 输出执行 set 方法后的状态 + System.out.println("prepend status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("codingground")); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + + public static void cas() { + + try { + + // 连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加数据 + Future fo = mcc.set("MyKey", 900, "Free Education"); + + // 输出执行 set 方法后的状态 + System.out.println("set status:" + fo.get()); + + // 使用 get 方法获取数据 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 通过 gets 方法获取 CAS token(令牌) + CASValue casValue = mcc.gets("MyKey"); + + // 输出 CAS token(令牌) 值 + System.out.println("CAS token - " + casValue); + + // 尝试使用cas方法来更新数据 + CASResponse casresp = mcc.cas("MyKey", casValue.getCas(), 900, "Largest Tutorials-Library"); + + // 输出 CAS 响应信息 + System.out.println("CAS Response - " + casresp); + + // 输出值 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + + public static void get() { + + try { + + // 连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加数据 + Future fo = mcc.set("MyKey", 900, "Free Education"); + + // 输出执行 set 方法后的状态 + System.out.println("set status:" + fo.get()); + + // 使用 get 方法获取数据 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + + public static void delete() { + + try { + + // 连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加数据 + Future fo = mcc.set("MyKey", 900, "World's largest online tutorials library"); + + // 输出执行 set 方法后的状态 + System.out.println("set status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("MyKey")); + + // 对存在的key进行数据添加操作 + fo = mcc.delete("MyKey"); + + // 输出执行 delete 方法后的状态 + System.out.println("delete status:" + fo.get()); + + // 获取键对应的值 + System.out.println("MyKey value in cache - " + mcc.get("codingground")); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + + public static void incrAndDecr() { + + try { + + // 连接本地的 Memcached 服务 + MemcachedClient mcc = new MemcachedClient(new InetSocketAddress(URL, PORT)); + System.out.println("Connection to server sucessful."); + + // 添加数字值 + Future fo = mcc.set("number", 900, "1000"); + + // 输出执行 set 方法后的状态 + System.out.println("set status:" + fo.get()); + + // 获取键对应的值 + System.out.println("value in cache - " + mcc.get("number")); + + // 自增并输出 + System.out.println("value in cache after increment - " + mcc.incr("number", 111)); + + // 自减并输出 + System.out.println("value in cache after decrement - " + mcc.decr("number", 112)); + + // 关闭连接 + mcc.shutdown(); + } catch (Exception ex) { + System.out.println(ex.getMessage()); + } + } + +} diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/User.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/User.java new file mode 100644 index 00000000..758e3c9a --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/User.java @@ -0,0 +1,38 @@ +package io.github.dunwu.javatech.data; + +import lombok.Data; +import lombok.ToString; + +import java.io.Serializable; + +@Data +@ToString +public class User implements Serializable { + + private static final long serialVersionUID = 4142994984277644695L; + + private Long id; + + private String name; + + private Integer age; + + private String address; + + private String email; + + public User() {} + + public User(Long id, String name) { + this.id = id; + this.name = name; + } + + public User(String name, Integer age, String address, String email) { + this.name = name; + this.age = age; + this.address = address; + this.email = email; + } + +} diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDao.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDao.java new file mode 100644 index 00000000..370147cd --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDao.java @@ -0,0 +1,33 @@ +package io.github.dunwu.javatech.data; + +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.List; + +public interface UserDao { + + void batchInsert(List users); + + Integer count(); + + @CacheEvict(value = "dunwu:users", key = "#name") + int deleteByName(String name); + + void insert(User user); + + List list(); + + @Cacheable(value = "dunwu:users", key = "#name") + User queryByName(String name); + + void recreateTable(); + + @CachePut(value = "dunwu:users", key = "#user.name") + User update(User user); + + JdbcTemplate getJdbcTemplate(); + +} diff --git a/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDaoImpl.java b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDaoImpl.java new file mode 100644 index 00000000..a69ec012 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/java/io/github/dunwu/javatech/data/UserDaoImpl.java @@ -0,0 +1,105 @@ +package io.github.dunwu.javatech.data; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class UserDaoImpl implements UserDao { + + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private final JdbcTemplate jdbcTemplate; + + public UserDaoImpl(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchInsert(List users) { + String sql = "INSERT INTO user(name, age, address, email) VALUES(?, ?, ?, ?)"; + + List params = new ArrayList<>(); + + users.forEach(item -> { + params.add(new Object[] { item.getName(), item.getAge(), item.getAddress(), item.getEmail() }); + }); + jdbcTemplate.batchUpdate(sql, params); + } + + @Override + public Integer count() { + try { + return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM user", Integer.class); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public int deleteByName(String name) { + int result = jdbcTemplate.update("DELETE FROM user WHERE name = ?", name); + log.info("[Delete] name = {}", name); + return result; + } + + @Override + public void insert(User user) { + jdbcTemplate.update("INSERT INTO user(name, age, address, email) VALUES(?, ?, ?, ?)", user.getName(), + user.getAge(), user.getAddress(), user.getEmail()); + } + + @Override + public List list() { + return jdbcTemplate.query("select * from USER", new BeanPropertyRowMapper(User.class)); + } + + @Override + public User queryByName(String name) { + + try { + User user = jdbcTemplate.queryForObject("SELECT * FROM user WHERE name = ?", + new BeanPropertyRowMapper<>(User.class), name); + log.info("[Query] name = {}, result = {}", name, user); + return user; + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void recreateTable() { + jdbcTemplate.execute("DROP TABLE IF EXISTS user"); + + String sqlStatement = + "CREATE TABLE user (\n" + " id BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Id',\n" + + " name VARCHAR(10) NOT NULL DEFAULT '' COMMENT '用户名',\n" + + " age TINYINT(3) NOT NULL DEFAULT 0 COMMENT '年龄',\n" + + " address VARCHAR(32) NOT NULL DEFAULT '' COMMENT '地址',\n" + + " email VARCHAR(32) NOT NULL DEFAULT '' COMMENT '邮件',\n" + " PRIMARY KEY (id)\n" + + ") COMMENT = '用户表';"; + jdbcTemplate.execute(sqlStatement); + } + + @Override + public User update(User user) { + jdbcTemplate.update("UPDATE USER SET name=?, age=?, address=?, email=? WHERE id=?", user.getName(), + user.getAge(), user.getAddress(), user.getEmail(), user.getId()); + return user; + } + + @Override + public JdbcTemplate getJdbcTemplate() { + return jdbcTemplate; + } + +} diff --git a/codes/javatech/javatech-cache/src/main/resources/application.properties b/codes/javatech/javatech-cache/src/main/resources/application.properties new file mode 100644 index 00000000..63306cc8 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/resources/application.properties @@ -0,0 +1,15 @@ +spring.datasource.url = jdbc:mysql://localhost:3306/spring_boot_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false +spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver +spring.datasource.username = root +spring.datasource.password = root +# 强制每次启动使用 sql 初始化数据,本项目仅为了演示方便,真实环境应避免这种模式 +spring.datasource.initialization-mode = ALWAYS +spring.datasource.schema = classpath:sql/schema.sql +spring.datasource.data = classpath:sql/data.sql +#spring.redis.database = 0 +#spring.redis.host = localhost +#spring.redis.port = 6379 +#spring.redis.password = +spring.cache.type = simple +spring.cache.cache-names = dunwu +spring.cache.redis.time-to-live = 60s diff --git a/codes/javatech/javatech-cache/src/main/resources/banner.txt b/codes/javatech/javatech-cache/src/main/resources/banner.txt new file mode 100644 index 00000000..449413d5 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/resources/banner.txt @@ -0,0 +1,12 @@ +${AnsiColor.BRIGHT_YELLOW}${AnsiStyle.BOLD} + ________ ___ ___ ________ ___ __ ___ ___ +|\ ___ \|\ \|\ \|\ ___ \|\ \ |\ \|\ \|\ \ +\ \ \_|\ \ \ \\\ \ \ \\ \ \ \ \ \ \ \ \ \\\ \ + \ \ \ \\ \ \ \\\ \ \ \\ \ \ \ \ __\ \ \ \ \\\ \ + \ \ \_\\ \ \ \\\ \ \ \\ \ \ \ \|\__\_\ \ \ \\\ \ + \ \_______\ \_______\ \__\\ \__\ \____________\ \_______\ + \|_______|\|_______|\|__| \|__|\|____________|\|_______| +${AnsiColor.CYAN}${AnsiStyle.BOLD} +:: Java :: (v${java.version}) +:: Spring Boot :: (v${spring-boot.version}) +${AnsiStyle.NORMAL} diff --git a/codes/javatech/javatech-cache/src/main/resources/logback.xml b/codes/javatech/javatech-cache/src/main/resources/logback.xml new file mode 100644 index 00000000..240ee4c6 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/resources/logback.xml @@ -0,0 +1,15 @@ + + + + + %d{HH:mm:ss.SSS} [%boldYellow(%thread)] [%highlight(%-5level)] %boldGreen(%c{36}.%M) - %boldBlue(%m%n) + + + + + + + + + + diff --git a/codes/javatech/javatech-cache/src/main/resources/sql/data.sql b/codes/javatech/javatech-cache/src/main/resources/sql/data.sql new file mode 100644 index 00000000..694c3472 --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/resources/sql/data.sql @@ -0,0 +1,8 @@ +-- ------------------------------------------- +-- 运行本项目的 DML 脚本 +-- ------------------------------------------- + +INSERT INTO user (name, age, address, email) +VALUES ('张三', 18, '北京', 'xxx@163.com'); +INSERT INTO user (name, age, address, email) +VALUES ('李四', 19, '上海', 'xxx@163.com'); diff --git a/codes/javatech/javatech-cache/src/main/resources/sql/schema.sql b/codes/javatech/javatech-cache/src/main/resources/sql/schema.sql new file mode 100644 index 00000000..247bdc1e --- /dev/null +++ b/codes/javatech/javatech-cache/src/main/resources/sql/schema.sql @@ -0,0 +1,13 @@ +-- ------------------------------------------- +-- 运行本项目的 DDL 脚本 +-- ------------------------------------------- + +-- 创建数据表 user +CREATE TABLE IF NOT EXISTS user ( + id BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Id', + name VARCHAR(10) NOT NULL DEFAULT '' COMMENT '用户名', + age TINYINT(3) NOT NULL DEFAULT 0 COMMENT '年龄', + address VARCHAR(32) NOT NULL DEFAULT '' COMMENT '地址', + email VARCHAR(32) NOT NULL DEFAULT '' COMMENT '邮件', + PRIMARY KEY (id) +) COMMENT = '用户表'; diff --git a/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/EhcacheApiTest.java b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/EhcacheApiTest.java new file mode 100644 index 00000000..d8d2a356 --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/EhcacheApiTest.java @@ -0,0 +1,235 @@ +package io.github.dunwu.javatech.cache; + +import net.sf.ehcache.Cache; +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.Element; +import net.sf.ehcache.config.CacheConfiguration; +import net.sf.ehcache.config.PersistenceConfiguration; +import net.sf.ehcache.store.MemoryStoreEvictionPolicy; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.net.URL; + +/** + * Ehcache API 测试(没有和任何框架集成的情况) + * + * @author Zhang Peng + * @since 2019-09-04 + */ +public class EhcacheApiTest { + + /** + * 使用Ehcache默认配置(classpath下的ehcache.xml)获取单例的CacheManager实例 + */ + @Test + public void operation() { + CacheManager manager = CacheManager.newInstance("src/test/resources/ehcache/ehcache.xml"); + + // 获得Cache的引用 + Cache cache = manager.getCache("users"); + + // 将一个Element添加到Cache + cache.put(new Element("key1", "value1")); + + // 获取Element,Element类支持序列化,所以下面两种方法都可以用 + Element element1 = cache.get("key1"); + // 获取非序列化的值 + System.out.println("key=" + element1.getObjectKey() + ", value=" + element1.getObjectValue()); + // 获取序列化的值 + System.out.println("key=" + element1.getKey() + ", value=" + element1.getValue()); + + // 更新Cache中的Element + cache.put(new Element("key1", "value2")); + Element element2 = cache.get("key1"); + System.out.println("key=" + element2.getObjectKey() + ", value=" + element2.getObjectValue()); + + // 获取Cache的元素数 + System.out.println("cache size:" + cache.getSize()); + + // 获取MemoryStore的元素数 + System.out.println("MemoryStoreSize:" + cache.getMemoryStoreSize()); + + // 获取DiskStore的元素数 + System.out.println("DiskStoreSize:" + cache.getDiskStoreSize()); + + // 移除Element + cache.remove("key1"); + System.out.println("cache size:" + cache.getSize()); + + // 关闭当前CacheManager对象 + manager.shutdown(); + + // 关闭CacheManager单例实例 + CacheManager.getInstance().shutdown(); + } + + /** + * 使用Ehcache默认配置(classpath下的ehcache.xml)获取单例的CacheManager实例 + */ + @Test + public void create01() { + CacheManager cacheManager = CacheManager.create(); + + String[] cacheNames = cacheManager.getCacheNames(); + for (String name : cacheNames) { + System.out.println("name:" + name); + } + + cacheManager.shutdown(); + } + + /** + * 使用Ehcache默认配置(classpath下的ehcache.xml)新建一个单例的CacheManager实例 + */ + @Test + public void create02() { + CacheManager.newInstance(); + + String[] cacheNames = CacheManager.getInstance().getCacheNames(); + for (String name : cacheNames) { + System.out.println("name:" + name); + } + + // 关闭CacheManager单例实例 + CacheManager.getInstance().shutdown(); + } + + /** + * 使用不同的配置文件分别创建一个CacheManager实例 + */ + @Test + public void create03() { + CacheManager manager1 = CacheManager.newInstance("src/test/resources/ehcache/ehcache1.xml"); + CacheManager manager2 = CacheManager.newInstance("src/test/resources/ehcache/ehcache1.xml"); + String[] cacheNamesForManager1 = manager1.getCacheNames(); + String[] cacheNamesForManager2 = manager2.getCacheNames(); + + for (String name : cacheNamesForManager1) { + System.out.println("[ehcache1.xml]name:" + name); + } + + for (String name : cacheNamesForManager2) { + System.out.println("[ehcache2.xml]name:" + name); + } + + manager1.shutdown(); + manager2.shutdown(); + } + + /** + * 基于classpath下的配置文件创建CacheManager实例 + */ + @Test + public void create04() { + URL url = getClass().getResource("/ehcache/ehcache.xml"); + CacheManager manager = CacheManager.newInstance(url); + String[] cacheNames = manager.getCacheNames(); + + for (String name : cacheNames) { + System.out.println("[ehcache.xml]name:" + name); + } + + manager.shutdown(); + } + + /** + * 基于IO流得到配置文件,并创建CacheManager实例 + */ + @Test + public void create05() throws Exception { + InputStream fis = new FileInputStream(new File("src/test/resources/ehcache/ehcache.xml").getAbsolutePath()); + CacheManager manager = CacheManager.newInstance(fis); + fis.close(); + String[] cacheNames = manager.getCacheNames(); + + for (String name : cacheNames) { + System.out.println("[ehcache.xml]name:" + name); + } + + manager.shutdown(); + } + + /** + * 使用默认配置(classpath下的ehcache.xml)添加缓存 + */ + @Test + public void addAndRemove01() { + CacheManager singletonManager = CacheManager.create(); + + // 添加缓存 + singletonManager.addCache("testCache"); + + // 打印配置信息和状态 + Cache test = singletonManager.getCache("testCache"); + System.out.println("cache name:" + test.getCacheConfiguration().getName()); + System.out.println("cache status:" + test.getStatus().toString()); + System.out.println("maxElementsInMemory:" + test.getCacheConfiguration().getMaxElementsInMemory()); + System.out.println("timeToIdleSeconds:" + test.getCacheConfiguration().getTimeToIdleSeconds()); + System.out.println("timeToLiveSeconds:" + test.getCacheConfiguration().getTimeToLiveSeconds()); + + // 删除缓存 + singletonManager.removeCache("testCache"); + System.out.println("cache status:" + test.getStatus().toString()); + + singletonManager.shutdown(); + } + + /** + * 使用自定义配置添加缓存,注意缓存未添加进CacheManager之前并不可用 + */ + @Test + public void addAndRemove02() { + CacheManager singletonManager = CacheManager.create(); + + // 添加缓存 + Cache memoryOnlyCache = new Cache("testCache2", 5000, false, false, 5, 2); + singletonManager.addCache(memoryOnlyCache); + + // 打印配置信息和状态 + Cache test = singletonManager.getCache("testCache2"); + System.out.println("cache name:" + test.getCacheConfiguration().getName()); + System.out.println("cache status:" + test.getStatus().toString()); + System.out.println("maxElementsInMemory:" + test.getCacheConfiguration().getMaxElementsInMemory()); + System.out.println("timeToIdleSeconds:" + test.getCacheConfiguration().getTimeToIdleSeconds()); + System.out.println("timeToLiveSeconds:" + test.getCacheConfiguration().getTimeToLiveSeconds()); + + // 删除缓存 + singletonManager.removeCache("testCache2"); + System.out.println("cache status:" + test.getStatus().toString()); + + singletonManager.shutdown(); + } + + /** + * 使用特定的配置添加缓存 + */ + @Test + public void addAndRemove03() { + CacheManager manager = CacheManager.create(); + + // 添加缓存 + Cache testCache = new Cache(new CacheConfiguration("testCache3", 5000) + .memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LFU).eternal(false).timeToLiveSeconds(60) + .timeToIdleSeconds(30).diskExpiryThreadIntervalSeconds(0) + .persistence(new PersistenceConfiguration().strategy(PersistenceConfiguration.Strategy.LOCALTEMPSWAP))); + manager.addCache(testCache); + + // 打印配置信息和状态 + Cache test = manager.getCache("testCache3"); + System.out.println("cache name:" + test.getCacheConfiguration().getName()); + System.out.println("cache status:" + test.getStatus().toString()); + System.out.println("maxElementsInMemory:" + test.getCacheConfiguration().getMaxElementsInMemory()); + System.out.println("timeToIdleSeconds:" + test.getCacheConfiguration().getTimeToIdleSeconds()); + System.out.println("timeToLiveSeconds:" + test.getCacheConfiguration().getTimeToLiveSeconds()); + + // 删除缓存 + manager.removeCache("testCache3"); + System.out.println("cache status:" + test.getStatus().toString()); + + manager.shutdown(); + } + +} diff --git a/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/LRUCacheTest.java b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/LRUCacheTest.java new file mode 100644 index 00000000..21bfbc81 --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/LRUCacheTest.java @@ -0,0 +1,25 @@ +package io.github.dunwu.javatech.cache; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * @author Zhang Peng + * @since 2020-01-18 + */ +public class LRUCacheTest { + + @Test + public void test() { + LRUCache cache = new LRUCache(2); + Assertions.assertNull(cache.get(2)); + cache.put(2, "B"); + Assertions.assertNull(cache.get(1)); + cache.put(1, "A"); + cache.put(3, "C"); + Assertions.assertEquals("A", cache.get(1)); + Assertions.assertEquals(null, cache.get(2)); + Assertions.assertEquals("C", cache.get(3)); + } + +} diff --git a/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCacheDemo.java b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCacheDemo.java new file mode 100644 index 00000000..837744ab --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCacheDemo.java @@ -0,0 +1,142 @@ +package io.github.dunwu.javatech.cache.spring; + +import io.github.dunwu.javatech.data.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Component; + +/** + * Spring 缓存接口测试类 + * + * @author Zhang Peng + * @since 2019-09-04 + */ +@Component +public class SpringCacheDemo { + + @Autowired + private UserService userService; + + @Autowired + private CacheManager cacheManager; + + /** + * 测试当前真实工作的 CacheManager 是什么 + */ + public void getCacheManager() { + System.out.println("当前 CacheManager 类:" + cacheManager.getClass()); + System.out.println(cacheManager.getCacheNames()); + } + + /** + * 测试@Cacheable + */ + public void testFindUser() throws InterruptedException { + // 设置查询条件 + User user1 = new User(1L, null); + User user2 = new User(2L, null); + User user3 = new User(3L, null); + User user4 = new User(3L, null); + + System.out.println("第一次查询"); + System.out.println(userService.findUser(user1)); + System.out.println(userService.findUser(user2)); + System.out.println(userService.findUser(user3)); + System.out.println(userService.findUser(user4)); + + // 如果缓存有效,应该不会打印 查找数据库 id = %d 成功 这样的信息 + System.out.println("\n第二次查询"); + System.out.println(userService.findUser(user1)); + System.out.println(userService.findUser(user2)); + System.out.println(userService.findUser(user3)); + System.out.println(userService.findUser(user4)); + + // 在classpath:ehcache/ehcache.xml中,设置了userCache的缓存时间为3000 ms, 这里设置等待 + Thread.sleep(3000); + + System.out.println("\n缓存过期,再次查询"); + System.out.println(userService.findUser(user1)); + System.out.println(userService.findUser(user2)); + System.out.println(userService.findUser(user3)); + } + + /** + * 测试@Cacheable设置Spring SpEL条件限制 + */ + public void testFindUserInLimit() throws InterruptedException { + // 设置查询条件 + User user1 = new User(1L, null); + User user2 = new User(2L, null); + User user3 = new User(3L, null); + + System.out.println("第一次查询user info"); + System.out.println(userService.findUserInLimit(user1)); + System.out.println(userService.findUserInLimit(user2)); + System.out.println(userService.findUserInLimit(user3)); + + System.out.println("\n第二次查询user info"); + System.out.println(userService.findUserInLimit(user1)); + System.out.println(userService.findUserInLimit(user2)); + System.out.println(userService.findUserInLimit(user3)); // 超过限制条件,不会从缓存中读数据 + + // 在classpath:ehcache/ehcache.xml中,设置了userCache的缓存时间为3000 ms, 这里设置等待 + Thread.sleep(3000); + + System.out.println("\n缓存过期,再次查询"); + System.out.println(userService.findUserInLimit(user1)); + System.out.println(userService.findUserInLimit(user2)); + System.out.println(userService.findUserInLimit(user3)); + } + + /** + * 测试@CachePut + */ + public void testUpdateUser() { + // 设置查询条件 + User user1 = new User(2L, null); + User user2 = new User(2L, null); + + System.out.println(userService.findUser(user1)); + System.out.println(userService.findUser(user2)); + userService.updateUser(new User(2L, "尼古拉斯.赵四")); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(userService.findUser(user1)); + System.out.println(userService.findUser(user2)); + } + + /** + * 测试@CacheEvict删除指定缓存 + */ + public void testRemoveUser() { + // 设置查询条件 + User user1 = new User(1L, null); + + System.out.println("数据删除前:"); + System.out.println(userService.findUser(user1)); + + userService.removeUser(user1); + System.out.println("数据删除后:"); + System.out.println(userService.findUser(user1)); + } + + /** + * 测试@CacheEvict删除所有缓存 + */ + public void testClear() { + System.out.println("数据清空前:"); + System.out.println(userService.findUser(new User(1L, null))); + System.out.println(userService.findUser(new User(2L, null))); + System.out.println(userService.findUser(new User(3L, null))); + + userService.clear(); + System.out.println("\n数据清空后:"); + System.out.println(userService.findUser(new User(1L, null))); + System.out.println(userService.findUser(new User(2L, null))); + System.out.println(userService.findUser(new User(3L, null))); + } + +} diff --git a/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCaffeineCacheTest.java b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCaffeineCacheTest.java new file mode 100644 index 00000000..ba1445ee --- /dev/null +++ b/codes/javatech/javatech-cache/src/test/java/io/github/dunwu/javatech/cache/spring/SpringCaffeineCacheTest.java @@ -0,0 +1,72 @@ +package io.github.dunwu.javatech.cache.spring; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * 使用 Caffeine 作为 Spring 缓存测试 + *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

访问形式:http://localhost:8080/hello?name=张三 + *

+ * 在本例中,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"); + mav.setViewName(helloService.hello()); return mav; } /** - *

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

访问形式:http://localhost:8080/log + *

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

+ * 访问形式:http://localhost:8080/log */ @ResponseBody @RequestMapping(value = "/log", method = RequestMethod.GET) @@ -47,4 +57,5 @@ public String log() { log.error(msg, "error"); return msg; } + } diff --git a/codes/javatool/server/src/main/java/io/github/dunwu/javatool/controller/IndexController.java b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/controller/IndexController.java similarity index 61% rename from codes/javatool/server/src/main/java/io/github/dunwu/javatool/controller/IndexController.java rename to codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/controller/IndexController.java index 3b19bf95..65549bb7 100644 --- a/codes/javatool/server/src/main/java/io/github/dunwu/javatool/controller/IndexController.java +++ b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/controller/IndexController.java @@ -1,8 +1,7 @@ /** - * The Apache License 2.0 - * Copyright (c) 2016 Zhang Peng + * The Apache License 2.0 Copyright (c) 2016 Zhang Peng */ -package io.github.dunwu.javatool.controller; +package io.github.dunwu.javatech.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @@ -11,13 +10,16 @@ /** * @author Zhang Peng - * @date 2017/4/12. + * @since 2017/4/12. */ @Controller public class IndexController { + /** - *

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

访问形式:http://localhost:8080/ + *

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

+ * 访问形式:http://localhost:8080/ */ @RequestMapping(value = "/", method = RequestMethod.GET) public ModelAndView index() { @@ -25,4 +27,5 @@ public ModelAndView index() { mav.setViewName("index"); return mav; } + } diff --git a/codes/javatool/server/src/main/java/io/github/dunwu/javatool/server/SimpleTomcatServer.java b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/server/SimpleTomcatServer.java similarity index 86% rename from codes/javatool/server/src/main/java/io/github/dunwu/javatool/server/SimpleTomcatServer.java rename to codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/server/SimpleTomcatServer.java index 099d4e43..5741fc81 100644 --- a/codes/javatool/server/src/main/java/io/github/dunwu/javatool/server/SimpleTomcatServer.java +++ b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/server/SimpleTomcatServer.java @@ -1,16 +1,18 @@ -package io.github.dunwu.javatool.server; - -import java.util.Optional; +package io.github.dunwu.javatech.server; import org.apache.catalina.startup.Tomcat; +import java.util.Optional; + /** - * 简单的嵌入式 Tomcat 启动类 - * 启动后可访问 http://localhost:8080/javatool-server/ + * 简单的嵌入式 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 { @@ -29,10 +31,11 @@ public static void main(String[] args) throws Exception { private static String getAbsolutePath() { String path = null; String folderPath = SimpleTomcatServer.class.getProtectionDomain().getCodeSource().getLocation().getPath() - .substring(1); + .substring(1); if (folderPath.indexOf("target") > 0) { path = folderPath.substring(0, folderPath.indexOf("target")); } return path; } + } diff --git a/codes/javatool/server/src/main/java/io/github/dunwu/javatool/server/TomcatServer.java b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/server/TomcatServer.java similarity index 68% rename from codes/javatool/server/src/main/java/io/github/dunwu/javatool/server/TomcatServer.java rename to codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/server/TomcatServer.java index 760f6d83..a18bb192 100644 --- a/codes/javatool/server/src/main/java/io/github/dunwu/javatool/server/TomcatServer.java +++ b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/server/TomcatServer.java @@ -1,6 +1,4 @@ -package io.github.dunwu.javatool.server; - -import java.io.File; +package io.github.dunwu.javatech.server; import org.apache.catalina.Server; import org.apache.catalina.startup.Catalina; @@ -11,18 +9,26 @@ 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_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 { @@ -63,7 +69,6 @@ public static void main(String[] args) throws Exception { 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(); @@ -71,8 +76,7 @@ public static void main(String[] args) throws Exception { private static String getAbsolutePath() { String path = null; - String folderPath = TomcatServer.class.getProtectionDomain().getCodeSource().getLocation() - .getPath(); + 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) { @@ -80,47 +84,52 @@ private static String getAbsolutePath() { } return path; } -} -class ExtendedTomcat extends Tomcat { + static class ExtendedTomcat extends Tomcat { - private Logger log = LoggerFactory.getLogger(this.getClass()); + private static final String RELATIVE_SERVERXML_PATH = "/conf/server.xml"; - private static final String RELATIVE_SERVERXML_PATH = "/conf/server.xml"; + private Logger log = LoggerFactory.getLogger(this.getClass()); - private static class ExtendedCatalina extends Catalina { @Override - public Digester createStartDigester() { - return super.createStartDigester(); + 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 + ")配置是否正确"); + } } - } - @Override - public Server getServer() { - if (server != null) { - return server; - } - // 默认不开启JNDI. 开启时, 注意maven必须添加tomcat-dbcp依赖 - System.setProperty("catalina.useNaming", "false"); - ExtendedCatalina extendedCatalina = new ExtendedCatalina(); - - //覆盖默认的skip和scan jar包配置 - System.setProperty(Constants.SKIP_JARS_PROPERTY,""); - System.setProperty(Constants.SCAN_JARS_PROPERTY,""); - - Digester digester = extendedCatalina.createStartDigester(); - digester.push(extendedCatalina); - try { - server = ((ExtendedCatalina) digester - .parse(new File(System.getProperty("catalina.base") + RELATIVE_SERVERXML_PATH))).getServer(); - // 设置catalina.base和catalna.home - this.initBaseDir(); - return server; - } catch (Exception e) { - log.error("Error while parsing server.xml", e); - throw new RuntimeException("server未创建,请检查server.xml(路径:" + System.getProperty("catalina.base") - + RELATIVE_SERVERXML_PATH + ")配置是否正确"); + private static class ExtendedCatalina extends Catalina { + + @Override + public Digester createStartDigester() { + return super.createStartDigester(); + } + } + } } + + diff --git a/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/service/HelloService.java b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/service/HelloService.java new file mode 100644 index 00000000..75d9b577 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/java/io/github/dunwu/javatech/service/HelloService.java @@ -0,0 +1,15 @@ +package io.github.dunwu.javatech.service; + +import org.springframework.stereotype.Service; + +/** + * @author Zhang Peng + * @date 2022-07-28 + */ +@Service +public class HelloService { + + public String hello() { + return "hello"; + } +} diff --git a/codes/javatool/server/src/main/resources/logback.xml b/codes/javatech/javatech-server/src/main/resources/logback.xml similarity index 86% rename from codes/javatool/server/src/main/resources/logback.xml rename to codes/javatech/javatech-server/src/main/resources/logback.xml index 4c37feab..9884e7be 100644 --- a/codes/javatool/server/src/main/resources/logback.xml +++ b/codes/javatech/javatech-server/src/main/resources/logback.xml @@ -3,7 +3,7 @@ - + @@ -33,14 +33,14 @@ - + - - + + - + diff --git a/codes/javatech/javatech-server/src/main/resources/properties/application-develop.properties b/codes/javatech/javatech-server/src/main/resources/properties/application-develop.properties new file mode 100644 index 00000000..25f09a35 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/resources/properties/application-develop.properties @@ -0,0 +1,11 @@ +# jdbc +jdbc.driver = +jdbc.url = +jdbc.username = +jdbc.password = +# redis +redis.name = +redis.host = +redis.port = +redis.password = +redis.database = diff --git a/codes/javatech/javatech-server/src/main/resources/properties/application-test.properties b/codes/javatech/javatech-server/src/main/resources/properties/application-test.properties new file mode 100644 index 00000000..25f09a35 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/resources/properties/application-test.properties @@ -0,0 +1,11 @@ +# jdbc +jdbc.driver = +jdbc.url = +jdbc.username = +jdbc.password = +# redis +redis.name = +redis.host = +redis.port = +redis.password = +redis.database = diff --git a/codes/javatool/server/src/main/resources/spring/spring-servlet.xml b/codes/javatech/javatech-server/src/main/resources/spring/spring-servlet.xml similarity index 50% rename from codes/javatool/server/src/main/resources/spring/spring-servlet.xml rename to codes/javatech/javatech-server/src/main/resources/spring/spring-servlet.xml index ec3b74c2..6c05fe85 100644 --- a/codes/javatool/server/src/main/resources/spring/spring-servlet.xml +++ b/codes/javatech/javatech-server/src/main/resources/spring/spring-servlet.xml @@ -1,9 +1,9 @@ - - - + + - - + + diff --git a/codes/javatool/server/src/main/resources/tomcat/conf/server.xml b/codes/javatech/javatech-server/src/main/resources/tomcat/conf/server.xml similarity index 57% rename from codes/javatool/server/src/main/resources/tomcat/conf/server.xml rename to codes/javatech/javatech-server/src/main/resources/tomcat/conf/server.xml index 3fc03693..77316698 100644 --- a/codes/javatool/server/src/main/resources/tomcat/conf/server.xml +++ b/codes/javatech/javatech-server/src/main/resources/tomcat/conf/server.xml @@ -1,8 +1,8 @@ - - - + + + @@ -10,22 +10,24 @@ + maxThreads="300" minSpareThreads="25" /> - + - + autoDeploy="false" + deployOnStartup="false" failCtxIfServletStartFails="true"> + + defaultPluggabilityScan="true" tldSkip="*.jar" + tldScan="" defaultTldScan="true" /> diff --git a/codes/javatool/server/src/main/resources/tomcat/conf/web.xml b/codes/javatech/javatech-server/src/main/resources/tomcat/conf/web.xml similarity index 99% rename from codes/javatool/server/src/main/resources/tomcat/conf/web.xml rename to codes/javatech/javatech-server/src/main/resources/tomcat/conf/web.xml index 8e0bed0f..7dc916a5 100644 --- a/codes/javatool/server/src/main/resources/tomcat/conf/web.xml +++ b/codes/javatech/javatech-server/src/main/resources/tomcat/conf/web.xml @@ -15,11 +15,11 @@ See the License for the specific language governing permissions and limitations under the License. --> - + version="3.1"> diff --git a/codes/javatech/javatech-server/src/main/webapp/META-INF/MANIFEST.MF b/codes/javatech/javatech-server/src/main/webapp/META-INF/MANIFEST.MF new file mode 100644 index 00000000..254272e1 --- /dev/null +++ b/codes/javatech/javatech-server/src/main/webapp/META-INF/MANIFEST.MF @@ -0,0 +1,3 @@ +Manifest-Version: 1.0 +Class-Path: + diff --git a/codes/javatool/server/src/main/webapp/WEB-INF/web.xml b/codes/javatech/javatech-server/src/main/webapp/WEB-INF/web.xml similarity index 85% rename from codes/javatool/server/src/main/webapp/WEB-INF/web.xml rename to codes/javatech/javatech-server/src/main/webapp/WEB-INF/web.xml index 4658a980..974a088b 100644 --- a/codes/javatool/server/src/main/webapp/WEB-INF/web.xml +++ b/codes/javatech/javatech-server/src/main/webapp/WEB-INF/web.xml @@ -1,6 +1,6 @@ - + javatool diff --git a/codes/javatool/server/src/main/webapp/views/jsp/hello.jsp b/codes/javatech/javatech-server/src/main/webapp/views/jsp/hello.jsp similarity index 100% rename from codes/javatool/server/src/main/webapp/views/jsp/hello.jsp rename to codes/javatech/javatech-server/src/main/webapp/views/jsp/hello.jsp diff --git a/codes/javatool/server/src/main/webapp/views/jsp/index.jsp b/codes/javatech/javatech-server/src/main/webapp/views/jsp/index.jsp similarity index 100% rename from codes/javatool/server/src/main/webapp/views/jsp/index.jsp rename to codes/javatech/javatech-server/src/main/webapp/views/jsp/index.jsp diff --git a/codes/javatech/javatech-server/src/test/java/io/github/dunwu/javatech/HelloControllerTests.java b/codes/javatech/javatech-server/src/test/java/io/github/dunwu/javatech/HelloControllerTests.java new file mode 100644 index 00000000..946e65de --- /dev/null +++ b/codes/javatech/javatech-server/src/test/java/io/github/dunwu/javatech/HelloControllerTests.java @@ -0,0 +1,28 @@ +package io.github.dunwu.javatech; + +import io.github.dunwu.javatech.service.HelloService; +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * 单元测试 + * @author Zhang Peng + * @date 2022-07-28 + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = {"classpath:spring/spring-servlet.xml"}) +public class HelloControllerTests { + + @Autowired + private HelloService service; + + @Test + public void test() { + String msg = service.hello(); + Assertions.assertThat("hello").isEqualTo(msg); + } +} diff --git a/codes/javatech/pom.xml b/codes/javatech/pom.xml new file mode 100644 index 00000000..8bfcf17b --- /dev/null +++ b/codes/javatech/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + io.github.dunwu.javatech + javatech + 1.0.0 + pom + JAVATECH + Java 技术示例源码 + + + javatech-cache + javatech-lib + javatech-log + javatech-mq + javatech-server + javatech-others + + diff --git a/codes/javatool/examples/pom.xml b/codes/javatool/examples/pom.xml deleted file mode 100644 index 868daaab..00000000 --- a/codes/javatool/examples/pom.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - 4.0.0 - - - - - - javatool-examples - jar - - - - - io.github.dunwu - javatool - 1.0.0 - - - - - log4j - log4j - - - - ch.qos.logback - logback-core - - - ch.qos.logback - logback-classic - - - ch.qos.logback - logback-access - - - net.logstash.logback - logstash-logback-encoder - - - - - - - - - - - ${artifactId} - - - true - src/main/resources - - logback.xml - log4j.xml - - - - - - - - - - ${project.artifactId} - Java 工具使用示例 - - - - diff --git a/codes/javatool/examples/src/main/resources/log4j.xml b/codes/javatool/examples/src/main/resources/log4j.xml deleted file mode 100644 index 8bfd70bc..00000000 --- a/codes/javatool/examples/src/main/resources/log4j.xml +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-spring/README.md b/codes/javatool/javatool-monitor/javatool-zipkin-spring/README.md new file mode 100644 index 00000000..abba6574 --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-spring/README.md @@ -0,0 +1,14 @@ +# 监控工具示例 + +## Zipkin + +> 搬迁 https://github.com/openzipkin/brave-webmvc-example 示例。 +> + +使用方法: + +``` +mvn jetty:run -Pfrontend +mvn jetty:run -Pbackend +``` + diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-spring/pom.xml b/codes/javatool/javatool-monitor/javatool-zipkin-spring/pom.xml new file mode 100644 index 00000000..8ada12b7 --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-spring/pom.xml @@ -0,0 +1,204 @@ + + + 4.0.0 + + io.github.dunwu.javatool + javatool-zipkin-spring + 1.0.0 + war + JAVATOOL-监控-Zipkin(Spring) + + + UTF-8 + 1.7 + 1.7 + + 4.3.26.RELEASE + 2.13.0 + 5.10.1 + + + + + + io.zipkin.brave + brave-bom + ${brave.version} + pom + import + + + + + + + javax.servlet + javax.servlet-api + 3.1.0 + provided + + + org.springframework + spring-webmvc + ${spring.version} + + + org.springframework + spring-web + ${spring.version} + + + org.apache.httpcomponents + httpclient + 4.5.11 + + + + + io.zipkin.brave + brave-instrumentation-spring-webmvc + + + + io.zipkin.brave + brave-instrumentation-httpclient + + + + + org.apache.logging.log4j + log4j-core + ${log4j.version} + + + org.apache.logging.log4j + log4j-jul + ${log4j.version} + + + org.apache.logging.log4j + log4j-jcl + ${log4j.version} + + + org.apache.logging.log4j + log4j-slf4j-impl + ${log4j.version} + + + io.zipkin.brave + brave-context-log4j2 + + + + + io.zipkin.brave + brave-spring-beans + + + io.zipkin.brave + brave + + + io.zipkin.reporter2 + zipkin-sender-okhttp3 + + + + + + + maven-compiler-plugin + 3.8.1 + + + + maven-enforcer-plugin + 3.0.0-M3 + + + enforce-java + + enforce + + + + + + [1.7.0_80,) + + + + + + + + + maven-war-plugin + 3.2.3 + + false + WEB-INF/lib/servlet-api-*.jar + + + + + + + org.eclipse.jetty + jetty-maven-plugin + 9.4.26.v20200117 + + + + + + + + frontend + + + + org.eclipse.jetty + jetty-maven-plugin + + + 8081 + + + + zipkin.service + frontend + + + + + + + + + backend + + + + org.eclipse.jetty + jetty-maven-plugin + + + 9000 + + + + zipkin.service + backend + + + + + + + + + + diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/AppConfiguration.java b/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/AppConfiguration.java new file mode 100644 index 00000000..4f0de553 --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/AppConfiguration.java @@ -0,0 +1,22 @@ +package io.github.dunwu.javatool.monitor.zipkin; + +import org.apache.http.client.HttpClient; +import org.apache.http.impl.client.HttpClients; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** The application is simple, it only uses Web MVC and a {@linkplain RestTemplate}. */ +@EnableWebMvc +public class AppConfiguration { + @Autowired(required = false) + HttpClient httpClient; + + @Bean RestTemplate restTemplate() { + HttpClient httpClient = this.httpClient; + if (httpClient == null) httpClient = HttpClients.createDefault(); + return new RestTemplate(new HttpComponentsClientHttpRequestFactory(httpClient)); + } +} diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Backend.java b/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Backend.java new file mode 100644 index 00000000..9f19662c --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Backend.java @@ -0,0 +1,23 @@ +package io.github.dunwu.javatool.monitor.zipkin; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.util.Date; + +@Configuration +@EnableWebMvc +@RestController +public class Backend { + + @RequestMapping("/api") + public String printDate(@RequestHeader(name = "user-name", required = false) String username) { + if (username != null) { + return new Date().toString() + " " + username; + } + return new Date().toString(); + } +} diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Frontend.java b/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Frontend.java new file mode 100644 index 00000000..20a466dd --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Frontend.java @@ -0,0 +1,21 @@ +package io.github.dunwu.javatool.monitor.zipkin; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@EnableWebMvc +@RestController +@Configuration +@CrossOrigin // So that javascript can be hosted elsewhere +public class Frontend { + @Autowired RestTemplate restTemplate; + + @RequestMapping("/") public String callBackend() { + return restTemplate.getForObject("http://localhost:9000/api", String.class); + } +} diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Initializer.java b/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Initializer.java new file mode 100644 index 00000000..5be07f0f --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Initializer.java @@ -0,0 +1,28 @@ +package io.github.dunwu.javatool.monitor.zipkin; + +import brave.spring.webmvc.DelegatingTracingFilter; +import org.springframework.web.SpringServletContainerInitializer; +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; + +import javax.servlet.Filter; + +/** Indirectly invoked by {@link SpringServletContainerInitializer} in a Servlet 3+ container */ +public class Initializer extends AbstractAnnotationConfigDispatcherServletInitializer { + + @Override protected String[] getServletMappings() { + return new String[] {"/"}; + } + + @Override protected Class[] getRootConfigClasses() { + return new Class[] { TracingConfiguration.class, AppConfiguration.class}; + } + + /** Ensures tracing is setup for all HTTP requests. */ + @Override protected Filter[] getServletFilters() { + return new Filter[] {new DelegatingTracingFilter()}; + } + + @Override protected Class[] getServletConfigClasses() { + return new Class[] { Frontend.class, Backend.class}; + } +} diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/TracingConfiguration.java b/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/TracingConfiguration.java new file mode 100644 index 00000000..81291177 --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/TracingConfiguration.java @@ -0,0 +1,90 @@ +package io.github.dunwu.javatool.monitor.zipkin; + +import brave.CurrentSpanCustomizer; +import brave.SpanCustomizer; +import brave.Tracing; +import brave.context.log4j2.ThreadContextScopeDecorator; +import brave.http.HttpTracing; +import brave.httpclient.TracingHttpClientBuilder; +import brave.propagation.B3Propagation; +import brave.propagation.ExtraFieldPropagation; +import brave.propagation.ThreadLocalCurrentTraceContext; +import brave.spring.webmvc.DelegatingTracingFilter; +import brave.spring.webmvc.SpanCustomizingAsyncHandlerInterceptor; +import org.apache.http.client.HttpClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import zipkin2.Span; +import zipkin2.reporter.AsyncReporter; +import zipkin2.reporter.Sender; +import zipkin2.reporter.okhttp3.OkHttpSender; + +/** + * This adds tracing configuration to any web mvc controllers or rest template clients. + * + *

This is a {@link Initializer#getRootConfigClasses() root config class}, so the + * {@linkplain DelegatingTracingFilter} added in {@link Initializer#getServletFilters()} can wire up properly. + */ +@Configuration +// Importing this class is effectively the same as declaring bean methods +@Import(SpanCustomizingAsyncHandlerInterceptor.class) +public class TracingConfiguration extends WebMvcConfigurerAdapter { + + /** Configuration for how to send spans to Zipkin */ + @Bean + Sender sender() { + return OkHttpSender.create("http://127.0.0.1:9411/api/v2/spans"); + } + + /** Configuration for how to buffer spans into messages for Zipkin */ + @Bean + AsyncReporter spanReporter() { + return AsyncReporter.create(sender()); + } + + /** Controls aspects of tracing such as the service name that shows up in the UI */ + @Bean + Tracing tracing(@Value("${zipkin.service:brave-webmvc-example}") String serviceName) { + return Tracing.newBuilder() + .localServiceName(serviceName) + .propagationFactory(ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "user-name")) + .currentTraceContext(ThreadLocalCurrentTraceContext.newBuilder() + .addScopeDecorator(ThreadContextScopeDecorator.create()) // puts trace IDs into logs + .build() + ) + .spanReporter(spanReporter()).build(); + } + + /** Allows someone to add tags to a span if a trace is in progress. */ + @Bean + SpanCustomizer spanCustomizer(Tracing tracing) { + return CurrentSpanCustomizer.create(tracing); + } + + /** Decides how to name and tag spans. By default they are named the same as the http method. */ + @Bean + HttpTracing httpTracing(Tracing tracing) { + return HttpTracing.create(tracing); + } + + /** adds tracing to any underlying http client calls */ + @Bean + HttpClient httpClient(HttpTracing httpTracing) { + return TracingHttpClientBuilder.create(httpTracing).build(); + } + + @Autowired + SpanCustomizingAsyncHandlerInterceptor serverInterceptor; + + /** adds tracing to the application-defined web controller */ + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(serverInterceptor); + } + +} diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/package-info.java b/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/package-info.java new file mode 100644 index 00000000..2d540cab --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/java/io/github/dunwu/javatool/monitor/zipkin/package-info.java @@ -0,0 +1,9 @@ +/** + * Zipkin 使用示例 + * + * @author Zhang Peng + * @see brave + * @see brave-webmvc-example + * @since 2020-03-05 + */ +package io.github.dunwu.javatool.monitor.zipkin; diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/resources/log4j2.properties b/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/resources/log4j2.properties new file mode 100644 index 00000000..3714a376 --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-spring/src/main/resources/log4j2.properties @@ -0,0 +1,8 @@ +appenders = console +appender.console.type = Console +appender.console.name = STDOUT +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = %d{ABSOLUTE} [%X{traceId}/%X{spanId}] %-5p [%t] %C{2} - %m%n +rootLogger.level = info +rootLogger.appenderRefs = stdout +rootLogger.appenderRef.stdout.ref = STDOUT diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-springboot/README.md b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/README.md new file mode 100644 index 00000000..9057ac27 --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/README.md @@ -0,0 +1,13 @@ +# 监控工具示例 + +## Zipkin + +> 搬迁 https://github.com/openzipkin/brave-webmvc-example 示例。 +> + +使用方法: + +``` +mvn compile exec:java -Dexec.mainClass=io.github.dunwu.javatool.monitor.zipkin.Backend +mvn compile exec:java -Dexec.mainClass=io.github.dunwu.javatool.monitor.zipkin.Frontend +``` diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-springboot/pom.xml b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/pom.xml new file mode 100644 index 00000000..7d99565f --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/pom.xml @@ -0,0 +1,106 @@ + + 4.0.0 + + io.github.dunwu.javatool + javatool-zipkin-springboot + 1.0.0 + jar + JAVATOOL-监控-Zipkin(SpringBoot) + + + UTF-8 + 1.7 + 1.7 + 2.7.2 + 5.10.1 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + io.zipkin.brave + brave-bom + ${brave.version} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.apache.httpcomponents + httpclient + + + + + io.zipkin.brave + brave-instrumentation-spring-webmvc + + + + io.zipkin.brave + brave-instrumentation-httpclient + + + + + io.zipkin.brave + brave-context-slf4j + + + + + io.zipkin.brave + brave + + + io.zipkin.reporter2 + zipkin-sender-okhttp3 + + + + + + + maven-compiler-plugin + 3.8.1 + + + + maven-enforcer-plugin + 3.0.0-M3 + + + enforce-java + + enforce + + + + + + [1.7.0_80,) + + + + + + + + + diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Backend.java b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Backend.java new file mode 100644 index 00000000..7563740b --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Backend.java @@ -0,0 +1,28 @@ +package io.github.dunwu.javatool.monitor.zipkin; + +import java.util.Date; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@EnableAutoConfiguration +@RestController +public class Backend { + + @RequestMapping("/api") + public String printDate(@RequestHeader(name = "user-name", required = false) String username) { + if (username != null) { + return new Date().toString() + " " + username; + } + return new Date().toString(); + } + + public static void main(String[] args) { + SpringApplication.run(Backend.class, + "--spring.application.name=backend", + "--server.port=9000" + ); + } +} diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Frontend.java b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Frontend.java new file mode 100644 index 00000000..a00ce58b --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/java/io/github/dunwu/javatool/monitor/zipkin/Frontend.java @@ -0,0 +1,33 @@ +package io.github.dunwu.javatool.monitor.zipkin; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestTemplate; + +@EnableAutoConfiguration +@RestController +@CrossOrigin // So that javascript can be hosted elsewhere +public class Frontend { + + final RestTemplate restTemplate; + + @Autowired Frontend(RestTemplateBuilder restTemplateBuilder) { + this.restTemplate = restTemplateBuilder.build(); + } + + @RequestMapping("/") public String callBackend() { + return restTemplate.getForObject("http://localhost:9000/api", String.class); + } + + public static void main(String[] args) { + SpringApplication.run(Frontend.class, + "--spring.application.name=frontend", + "--server.port=8081" + ); + } +} diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/java/io/github/dunwu/javatool/monitor/zipkin/TracingConfiguration.java b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/java/io/github/dunwu/javatool/monitor/zipkin/TracingConfiguration.java new file mode 100644 index 00000000..5aef8af0 --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/java/io/github/dunwu/javatool/monitor/zipkin/TracingConfiguration.java @@ -0,0 +1,91 @@ +package io.github.dunwu.javatool.monitor.zipkin; + +import brave.CurrentSpanCustomizer; +import brave.SpanCustomizer; +import brave.Tracing; +import brave.context.slf4j.MDCScopeDecorator; +import brave.http.HttpTracing; +import brave.httpclient.TracingHttpClientBuilder; +import brave.propagation.B3Propagation; +import brave.propagation.ExtraFieldPropagation; +import brave.propagation.ThreadLocalCurrentTraceContext; +import brave.servlet.TracingFilter; +import brave.spring.webmvc.SpanCustomizingAsyncHandlerInterceptor; +import javax.servlet.Filter; +import org.apache.http.impl.client.CloseableHttpClient; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import zipkin2.Span; +import zipkin2.reporter.AsyncReporter; +import zipkin2.reporter.Sender; +import zipkin2.reporter.okhttp3.OkHttpSender; + +/** + * This adds tracing configuration to any web mvc controllers or rest template clients. + */ +@Configuration +// Importing a class is effectively the same as declaring bean methods +@Import(SpanCustomizingAsyncHandlerInterceptor.class) +public class TracingConfiguration extends WebMvcConfigurerAdapter { + + /** Configuration for how to send spans to Zipkin */ + @Bean Sender sender() { + return OkHttpSender.create("http://127.0.0.1:9411/api/v2/spans"); + } + + /** Configuration for how to buffer spans into messages for Zipkin */ + @Bean AsyncReporter spanReporter() { + return AsyncReporter.create(sender()); + } + + /** Controls aspects of tracing such as the service name that shows up in the UI */ + @Bean Tracing tracing(@Value("${spring.application.name}") String serviceName) { + return Tracing.newBuilder() + .localServiceName(serviceName) + .propagationFactory(ExtraFieldPropagation.newFactory(B3Propagation.FACTORY, "user-name")) + .currentTraceContext(ThreadLocalCurrentTraceContext.newBuilder() + .addScopeDecorator(MDCScopeDecorator.create()) // puts trace IDs into logs + .build() + ) + .spanReporter(spanReporter()).build(); + } + + /** Allows someone to add tags to a span if a trace is in progress */ + @Bean SpanCustomizer spanCustomizer(Tracing tracing) { + return CurrentSpanCustomizer.create(tracing); + } + + /** Decides how to name and tag spans. By default they are named the same as the http method */ + @Bean HttpTracing httpTracing(Tracing tracing) { + return HttpTracing.create(tracing); + } + + /** Creates server spans for http requests */ + @Bean Filter tracingFilter(HttpTracing httpTracing) { + return TracingFilter.create(httpTracing); + } + + @Bean RestTemplateCustomizer useTracedHttpClient(HttpTracing httpTracing) { + final CloseableHttpClient httpClient = TracingHttpClientBuilder.create(httpTracing).build(); + return new RestTemplateCustomizer() { + @Override public void customize(RestTemplate restTemplate) { + restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory(httpClient)); + } + }; + } + + @Autowired SpanCustomizingAsyncHandlerInterceptor webMvcTracingCustomizer; + + /** Decorates server spans with application-defined web tags */ + @Override public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(webMvcTracingCustomizer); + } +} diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/resources/META-INF/spring.factories b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..2a115f70 --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration = \ +io.github.dunwu.javatool.monitor.zipkin.TracingConfiguration diff --git a/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/resources/application.properties b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/resources/application.properties new file mode 100644 index 00000000..9981ddf5 --- /dev/null +++ b/codes/javatool/javatool-monitor/javatool-zipkin-springboot/src/main/resources/application.properties @@ -0,0 +1,5 @@ +# spring.application.name and server.port are set in the main methods, +# so not done here +logging.level.root=INFO +# Adds trace and span IDs to logs (when a trace is in progress) +logging.pattern.level=[%X{traceId}/%X{spanId}] %-5p [%t] %C{2} - %m%n diff --git a/codes/javatool/javatool-monitor/pom.xml b/codes/javatool/javatool-monitor/pom.xml new file mode 100644 index 00000000..027eca63 --- /dev/null +++ b/codes/javatool/javatool-monitor/pom.xml @@ -0,0 +1,17 @@ + + + 4.0.0 + + io.github.dunwu.javatool + javatool-monitor + 1.0.0 + pom + JAVATOOL-监控 + + + javatool-zipkin-spring + javatool-zipkin-springboot + + + diff --git a/codes/javatool/pom.xml b/codes/javatool/pom.xml index 16ba52d7..86b22be9 100644 --- a/codes/javatool/pom.xml +++ b/codes/javatool/pom.xml @@ -1,128 +1,16 @@ - - - 4.0.0 - - - - - - io.github.dunwu - javatool - 1.0.0 - - server - examples - - pom - - - - - - junit - junit - 4.12 - test - - - - - - - log4j - log4j - 1.2.17 - - - - ch.qos.logback - logback-core - 1.2.3 - - - ch.qos.logback - logback-classic - 1.2.3 - - - ch.qos.logback - logback-access - 1.2.3 - - - net.logstash.logback - logstash-logback-encoder - 4.11 - - - org.logback-extensions - logback-ext-spring - 0.1.2 - - - org.slf4j - jcl-over-slf4j - 1.7.12 - - - - - - javax.servlet - javax.servlet-api - 3.1.0 - provided - - - javax.servlet.jsp - jsp-api - 2.2 - provided - - - - - - org.springframework - spring-context-support - ${spring.version} - - - org.springframework - spring-webmvc - ${spring.version} - - - - - - - - - UTF-8 - 1.8 - ${java.version} - ${java.version} - - 4.3.13.RELEASE - - - - - - - - - ${project.artifactId} - - - - - - ${project.artifactId} - Java 工具使用示例 - - + + + 4.0.0 + + io.github.dunwu.javatool + javatool + 1.0.0 + pom + JAVATOOL + + + javatool-monitor + diff --git a/codes/javatool/server/pom.xml b/codes/javatool/server/pom.xml deleted file mode 100644 index b731cd62..00000000 --- a/codes/javatool/server/pom.xml +++ /dev/null @@ -1,114 +0,0 @@ - - - 4.0.0 - - - - - - javatool-server - war - - - - - io.github.dunwu - javatool - 1.0.0 - - - - - ch.qos.logback - logback-classic - - - org.logback-extensions - logback-ext-spring - - - org.slf4j - jcl-over-slf4j - - - - - - javax.servlet - javax.servlet-api - provided - - - javax.servlet.jsp - jsp-api - provided - - - - - - org.apache.tomcat.embed - tomcat-embed-core - ${tomcat.version} - - - org.apache.tomcat.embed - tomcat-embed-el - ${tomcat.version} - - - org.apache.tomcat.embed - tomcat-embed-jasper - ${tomcat.version} - - - - - - org.springframework - spring-context-support - - - org.springframework - spring-webmvc - - - - - - - - 8.5.24 - - - - - - - - - - - - - - - - - - ${project.artifactId} - Java工具-服务器 示例 - - - - diff --git a/codes/javatool/server/src/main/resources/properties/application-develop.properties b/codes/javatool/server/src/main/resources/properties/application-develop.properties deleted file mode 100644 index 14cbdb4e..00000000 --- a/codes/javatool/server/src/main/resources/properties/application-develop.properties +++ /dev/null @@ -1,12 +0,0 @@ -# jdbc -jdbc.driver= -jdbc.url= -jdbc.username= -jdbc.password= - -# redis -redis.name= -redis.host= -redis.port= -redis.password= -redis.database= diff --git a/codes/javatool/server/src/main/resources/properties/application-test.properties b/codes/javatool/server/src/main/resources/properties/application-test.properties deleted file mode 100644 index 14cbdb4e..00000000 --- a/codes/javatool/server/src/main/resources/properties/application-test.properties +++ /dev/null @@ -1,12 +0,0 @@ -# jdbc -jdbc.driver= -jdbc.url= -jdbc.username= -jdbc.password= - -# redis -redis.name= -redis.host= -redis.port= -redis.password= -redis.database= diff --git a/codes/trouble-shooting/pom.xml b/codes/trouble-shooting/pom.xml new file mode 100644 index 00000000..54185a78 --- /dev/null +++ b/codes/trouble-shooting/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.2.1.RELEASE + + + io.github.dunwu.trouble + trouble-shooting + jar + 故障诊断 + 故障诊断示例源码 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-starter-test + test + + + io.github.dunwu + dunwu-tool-core + + + + + + + + + + + org.assertj + assertj-core + + + org.projectlombok + lombok + + + mysql + mysql-connector-java + + + org.apache.httpcomponents + httpclient + 4.5.9 + + + org.apache.httpcomponents + fluent-hc + 4.5.9 + + + + + + io.github.dunwu + dunwu-dependencies + 0.5.7 + pom + import + + + + diff --git a/codes/trouble-shooting/src/main/java/io/github/dunwu/trouble/BatchInsertApplication.java b/codes/trouble-shooting/src/main/java/io/github/dunwu/trouble/BatchInsertApplication.java new file mode 100644 index 00000000..22292360 --- /dev/null +++ b/codes/trouble-shooting/src/main/java/io/github/dunwu/trouble/BatchInsertApplication.java @@ -0,0 +1,48 @@ +package io.github.dunwu.trouble; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.concurrent.TimeUnit; + +@SpringBootApplication +@Slf4j +public class BatchInsertApplication implements CommandLineRunner { + + private final JdbcTemplate jdbcTemplate; + + public BatchInsertApplication(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public static void main(String[] args) { + SpringApplication.run(BatchInsertApplication.class, args); + } + + @Override + public void run(String... args) { + + long begin = System.nanoTime(); + String sql = "INSERT INTO `testuser` (`name`) VALUES (?)"; + jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() { + @Override + public void setValues(PreparedStatement preparedStatement, int i) throws SQLException { + preparedStatement.setString(1, "user" + i); + } + + @Override + public int getBatchSize() { + return 100000; + } + }); + long time = System.nanoTime() - begin; + log.info("took : {} ms", TimeUnit.NANOSECONDS.toMillis(time)); + } + +} diff --git a/codes/trouble-shooting/src/main/resources/application.properties b/codes/trouble-shooting/src/main/resources/application.properties new file mode 100644 index 00000000..1737cc99 --- /dev/null +++ b/codes/trouble-shooting/src/main/resources/application.properties @@ -0,0 +1,11 @@ +server.port = 8888 +#server.port = ${random.int[1024,65536]} +#spring.datasource.url = jdbc:mysql://172.22.6.9:3316/trouble_shooting?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +spring.datasource.url = jdbc:mysql://172.22.6.9:3316/trouble_shooting?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true +spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver +spring.datasource.username = root +spring.datasource.password = 604330436 +# 强制每次启动使用 sql 初始化数据,本项目仅为了演示方便,真实环境应避免这种模式 +spring.datasource.initialization-mode = ALWAYS +spring.datasource.schema = classpath:sql/schema.sql +spring.datasource.data = classpath:sql/data.sql diff --git a/codes/trouble-shooting/src/main/resources/banner.txt b/codes/trouble-shooting/src/main/resources/banner.txt new file mode 100644 index 00000000..449413d5 --- /dev/null +++ b/codes/trouble-shooting/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/trouble-shooting/src/main/resources/logback.xml b/codes/trouble-shooting/src/main/resources/logback.xml new file mode 100644 index 00000000..7667f240 --- /dev/null +++ b/codes/trouble-shooting/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + + + %d{HH:mm:ss.SSS} [%boldYellow(%thread)] [%highlight(%-5level)] %boldGreen(%c{36}.%M) - %boldBlue(%m%n) + + + + + + + + diff --git a/codes/trouble-shooting/src/main/resources/sql/data.sql b/codes/trouble-shooting/src/main/resources/sql/data.sql new file mode 100644 index 00000000..733abe41 --- /dev/null +++ b/codes/trouble-shooting/src/main/resources/sql/data.sql @@ -0,0 +1,11 @@ +-- ------------------------------------------------------------------- +-- 运行本项目的初始化 DML 脚本 +-- Mysql 知识点可以参考: +-- https://dunwu.github.io/db-tutorial/#/sql/mysql/README +-- ------------------------------------------------------------------- + +# INSERT INTO user (username, password, email) +# VALUES ('admin', '$2a$10$Y9uV9YjFuNlATDGz5MeTZeuo8LbebbpP6jRgtZYQcgiCZRlf8rJYG', 'admin@xxx.com'); +# +# INSERT INTO user (username, password, email) +# VALUES ('user', '$2a$10$Y9uV9YjFuNlATDGz5MeTZeuo8LbebbpP6jRgtZYQcgiCZRlf8rJYG', 'user@xxx.com'); diff --git a/codes/trouble-shooting/src/main/resources/sql/schema.sql b/codes/trouble-shooting/src/main/resources/sql/schema.sql new file mode 100644 index 00000000..13e82ae2 --- /dev/null +++ b/codes/trouble-shooting/src/main/resources/sql/schema.sql @@ -0,0 +1,15 @@ +-- ------------------------------------------------------------------- +-- 运行本项目的初始化 DDL 脚本 +-- Mysql 知识点可以参考: +-- https://dunwu.github.io/db-tutorial/#/sql/mysql/README +-- ------------------------------------------------------------------- + +-- 强制新建用户表 +DROP TABLE IF EXISTS `testuser`; +CREATE TABLE `testuser` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4; 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..6bba8f05 --- /dev/null +++ b/docs/.vuepress/config.js @@ -0,0 +1,209 @@ +const baiduCode = require('./config/baiduCode.js') // 百度统计hm码 +const htmlModules = require('./config/htmlModules.js') + +module.exports = { + port: '4000', + dest: 'docs/.temp', + base: '/java-tutorial/', // 默认'/'。如果你想将你的网站部署到如 https://foo.github.io/bar/,那么 base 应该被设置成 "/bar/",(否则页面将失去样式等文件) + title: 'JAVA-TUTORIAL', + description: '☕ java-tutorial 是一个 Java 教程,汇集一个老司机在 Java 领域的十年积累。', + theme: 'vdoing', // 使用依赖包主题 + // theme: require.resolve('../../vdoing'), // 使用本地主题 + head: [ + // 注入到页面 中的标签,格式[tagName, { attrName: attrValue }, innerHTML?] + ['link', { rel: 'icon', href: '/img/favicon.ico' }], //favicons,资源放在public文件夹 + ['meta', { name: 'keywords', content: 'vuepress,theme,blog,vdoing' }], + ['meta', { name: 'theme-color', content: '#11a8cd' }], // 移动浏览器主题颜色 + + ['meta', { name: 'wwads-cn-verify', content: 'mxqWx62nfQQ9ocT4e5DzISHzOWyF4s' }], // 广告相关,你可以去掉 + ['script', { src: 'https://cdn.wwads.cn/js/makemoney.js', type: 'text/javascript' }] // 广告相关,你可以去掉 + ], + markdown: { + // lineNumbers: true, + extractHeaders: ['h2', 'h3', 'h4', 'h5', 'h6'], // 提取标题到侧边栏的级别,默认['h2', 'h3'] + externalLinks: { + target: '_blank', + rel: 'noopener noreferrer' + } + }, + // 主题配置 + themeConfig: { + nav: [ + { text: '首页', link: '/' }, + { text: 'JavaEE', link: '/01.Java/02.JavaEE/' }, + { + text: 'Java软件', + link: '/01.Java/11.软件/', + items: [ + { text: 'Java 构建', link: '/01.Java/11.软件/01.构建/' }, + { text: 'Java IDE', link: '/01.Java/11.软件/02.IDE/' }, + { text: 'Java 监控诊断', link: '/01.Java/11.软件/03.监控诊断/' } + ] + }, + { + text: 'Java工具', + link: '/01.Java/12.工具/', + items: [ + { text: 'Java IO 工具', link: '/01.Java/12.工具/01.IO/' }, + { text: 'JavaBean 工具', link: '/01.Java/12.工具/02.JavaBean/' }, + { text: 'Java 模板引擎', link: '/01.Java/12.工具/03.模板引擎/' }, + { text: 'Java 测试工具', link: '/01.Java/12.工具/04.测试/' } + ] + }, + { text: 'Java框架', link: '/01.Java/13.框架/' }, + { text: 'Java中间件', link: '/01.Java/14.中间件/' }, + { + text: '✨ Java系列', + ariaLabel: 'Java', + items: [ + { text: 'Java 教程 📚', link: 'https://dunwu.github.io/java-tutorial/', target: '_blank' }, + { text: 'JavaCore 教程 📚', link: 'https://dunwu.github.io/javacore/', target: '_blank' } + ] + } + ], + sidebarDepth: 2, // 侧边栏显示深度,默认1,最大2(显示到h3标题) + logo: 'https://raw.githubusercontent.com/dunwu/images/master/common/dunwu-logo.png', // 导航栏logo + repo: 'dunwu/java-tutorial', // 导航栏右侧生成Github链接 + searchMaxSuggestions: 10, // 搜索结果显示最大数 + lastUpdated: '上次更新', // 更新的时间,及前缀文字 string | boolean (取值为git提交时间) + + docsDir: 'docs', // 编辑的文件夹 + editLinks: true, // 编辑链接 + editLinkText: '📝 帮助改善此页面!', + + // 以下配置是Vdoing主题改动的和新增的配置 + sidebar: { mode: 'structuring', collapsable: true }, // 侧边栏 'structuring' | { mode: 'structuring', collapsable: + // Boolean} | 'auto' | 自定义 温馨提示:目录页数据依赖于结构化的侧边栏数据,如果你不设置为'structuring',将无法使用目录页 + + sidebarOpen: true, // 初始状态是否打开侧边栏,默认true + updateBar: { + // 最近更新栏 + showToArticle: true // 显示到文章页底部,默认true + // moreArticle: '/archives' // “更多文章”跳转的页面,默认'/archives' + }, + // titleBadge: false, // 文章标题前的图标是否显示,默认true + // titleBadgeIcons: [ // 文章标题前图标的地址,默认主题内置图标 + // '图标地址1', + // '图标地址2' + // ], + // bodyBgImg: [ + // 'https://cdn.jsdelivr.net/gh/xugaoyi/image_store/blog/20200507175828.jpeg', + // 'https://cdn.jsdelivr.net/gh/xugaoyi/image_store/blog/20200507175845.jpeg', + // 'https://cdn.jsdelivr.net/gh/xugaoyi/image_store/blog/20200507175846.jpeg' + // ], // body背景大图,默认无。 单张图片 String || 多张图片 Array, 多张图片时每隔15秒换一张。 + + // categoryText: '随笔', // 碎片化文章(_posts文件夹的文章)预设生成的分类值,默认'随笔' + + // contentBgStyle: 1, + + category: true, // 是否打开分类功能,默认true。 如打开,会做的事情有:1. 自动生成的frontmatter包含分类字段 2.页面中显示与分类相关的信息和模块 3.自动生成分类页面(在@pages文件夹)。如关闭,则反之。 + tag: true, // 是否打开标签功能,默认true。 如打开,会做的事情有:1. 自动生成的frontmatter包含标签字段 2.页面中显示与标签相关的信息和模块 3.自动生成标签页面(在@pages文件夹)。如关闭,则反之。 + archive: true, // 是否打开归档功能,默认true。 如打开,会做的事情有:1.自动生成归档页面(在@pages文件夹)。如关闭,则反之。 + + author: { + // 文章默认的作者信息,可在md文件中单独配置此信息 String | {name: String, href: String} + name: 'dunwu', // 必需 + href: 'https://github.com/dunwu' // 可选的 + }, + social: { + // 社交图标,显示于博主信息栏和页脚栏 + // iconfontCssFile: '//at.alicdn.com/t/font_1678482_u4nrnp8xp6g.css', // 可选,阿里图标库在线css文件地址,对于主题没有的图标可自由添加 + icons: [ + { + iconClass: 'icon-youjian', + title: '发邮件', + link: 'mailto:forbreak@163.com' + }, + { + iconClass: 'icon-github', + title: 'GitHub', + link: 'https://github.com/dunwu' + } + ] + }, + footer: { + // 页脚信息 + createYear: 2019, // 博客创建年份 + copyrightInfo: '钝悟(dunwu) | CC-BY-SA-4.0' // 博客版权信息,支持a标签 + }, + htmlModules + }, + + // 插件 + plugins: [ + [ + require('./plugins/love-me'), + { + // 鼠标点击爱心特效 + color: '#11a8cd', // 爱心颜色,默认随机色 + excludeClassName: 'theme-vdoing-content' // 要排除元素的class, 默认空'' + } + ], + + ['fulltext-search'], // 全文搜索 + + // ['thirdparty-search', { // 可以添加第三方搜索链接的搜索框(原官方搜索框的参数仍可用) + // thirdparty: [ // 可选,默认 [] + // { + // title: '在GitHub中搜索', + // frontUrl: 'https://github.com/search?q=', // 搜索链接的前面部分 + // behindUrl: '' // 搜索链接的后面部分,可选,默认 '' + // }, + // { + // title: '在npm中搜索', + // frontUrl: 'https://www.npmjs.com/search?q=', + // }, + // { + // title: '在Bing中搜索', + // frontUrl: 'https://cn.bing.com/search?q=' + // } + // ] + // }], + + [ + 'one-click-copy', + { + // 代码块复制按钮 + copySelector: ['div[class*="language-"] pre', 'div[class*="aside-code"] aside'], // String or Array + copyMessage: '复制成功', // default is 'Copy successfully and then paste it for use.' + duration: 1000, // prompt message display time. + showInMobile: false // whether to display on the mobile side, default: false. + } + ], + [ + 'demo-block', + { + // demo演示模块 https://github.com/xiguaxigua/vuepress-plugin-demo-block + settings: { + // jsLib: ['http://xxx'], // 在线示例(jsfiddle, codepen)中的js依赖 + // cssLib: ['http://xxx'], // 在线示例中的css依赖 + // vue: 'https://cdn.jsdelivr.net/npm/vue/dist/vue.min.js', // 在线示例中的vue依赖 + jsfiddle: false, // 是否显示 jsfiddle 链接 + codepen: true, // 是否显示 codepen 链接 + horizontal: false // 是否展示为横向样式 + } + } + ], + [ + 'vuepress-plugin-zooming', // 放大图片 + { + selector: '.theme-vdoing-content img:not(.no-zoom)', + options: { + bgColor: 'rgba(0,0,0,0.6)' + } + } + ], + [ + '@vuepress/last-updated', // "上次更新"时间格式 + { + transformer: (timestamp, lang) => { + const dayjs = require('dayjs') // https://day.js.org/ + return dayjs(timestamp).format('YYYY/MM/DD, HH:mm:ss') + } + } + ] + ], + + // 监听文件变化并重新构建 + extraWatchFiles: ['.vuepress/config.js', '.vuepress/config/htmlModules.js'] +} diff --git a/docs/.vuepress/config/baiduCode.js b/docs/.vuepress/config/baiduCode.js new file mode 100644 index 00000000..b0c50903 --- /dev/null +++ b/docs/.vuepress/config/baiduCode.js @@ -0,0 +1 @@ +module.exports = '' diff --git a/docs/.vuepress/config/htmlModules.js b/docs/.vuepress/config/htmlModules.js new file mode 100644 index 00000000..fc0a47eb --- /dev/null +++ b/docs/.vuepress/config/htmlModules.js @@ -0,0 +1,69 @@ +/** 插入自定义html模块 (可用于插入广告模块等) + * { + * homeSidebarB: htmlString, 首页侧边栏底部 + * + * sidebarT: htmlString, 全局左侧边栏顶部 + * sidebarB: htmlString, 全局左侧边栏底部 + * + * pageT: htmlString, 全局页面顶部 + * pageB: htmlString, 全局页面底部 + * pageTshowMode: string, 页面顶部-显示方式:未配置默认全局;'article' => 仅文章页①; 'custom' => 仅自定义页① + * pageBshowMode: string, 页面底部-显示方式:未配置默认全局;'article' => 仅文章页①; 'custom' => 仅自定义页① + * + * windowLB: htmlString, 全局左下角② + * windowRB: htmlString, 全局右下角② + * } + * + * ①注:在.md文件front matter配置`article: false`的页面是自定义页,未配置的默认是文章页(首页除外)。 + * ②注:windowLB 和 windowRB:1.展示区块最大宽高200px*400px。2.请给自定义元素定一个不超过200px*400px的宽高。3.在屏幕宽度小于960px时无论如何都不会显示。 + */ + +module.exports = { + // 万维广告 + // pageT: ` + //

+ // + // `, + windowRB: ` +
+ + `, +} + +// module.exports = { +// homeSidebarB: `
自定义模块测试
`, +// sidebarT: `
自定义模块测试
`, +// sidebarB: `
自定义模块测试
`, +// pageT: `
自定义模块测试
`, +// pageB: `
自定义模块测试
`, +// windowLB: `
自定义模块测试
`, +// windowRB: `
自定义模块测试
`, +// } diff --git a/docs/.vuepress/enhanceApp.js b/docs/.vuepress/enhanceApp.js new file mode 100644 index 00000000..5bfa34f4 --- /dev/null +++ b/docs/.vuepress/enhanceApp.js @@ -0,0 +1,59 @@ +/** + * to主题使用者:你可以去掉本文件的所有代码 + */ +export default ({ + Vue, // VuePress 正在使用的 Vue 构造函数 + options, // 附加到根实例的一些选项 + router, // 当前应用的路由实例 + siteData, // 站点元数据 + isServer // 当前应用配置是处于 服务端渲染 还是 客户端 +}) => { + + // 用于监控在路由变化时检查广告拦截器 (to主题使用者:你可以去掉本文件的所有代码) + if (!isServer) { + router.afterEach(() => { + //check if wwads' fire function was blocked after document is ready with 3s timeout (waiting the ad loading) + docReady(function () { + setTimeout(function () { + if (window._AdBlockInit === undefined) { + ABDetected(); + } + }, 3000); + }); + + // 删除事件改为隐藏事件 + setTimeout(() => { + const pageAD = document.querySelector('.page-wwads'); + if (!pageAD) return; + const btnEl = pageAD.querySelector('.wwads-hide'); + if (btnEl) { + btnEl.onclick = () => { + pageAD.style.display = 'none'; + } + } + // 显示广告模块 + if (pageAD.style.display === 'none') { + pageAD.style.display = 'flex'; + } + }, 900); + }) + } +} + + +function ABDetected() { + const h = ""; + const wwadsEl = document.getElementsByClassName("wwads-cn"); + const wwadsContentEl = document.querySelector('.wwads-content'); + if (wwadsEl[0] && !wwadsContentEl) { + wwadsEl[0].innerHTML = h; + } +}; + +//check document ready +function docReady(t) { + "complete" === document.readyState || + "interactive" === document.readyState + ? setTimeout(t, 1) + : document.addEventListener("DOMContentLoaded", t); +} diff --git a/docs/.vuepress/plugins/love-me/index.js b/docs/.vuepress/plugins/love-me/index.js new file mode 100644 index 00000000..2851beb0 --- /dev/null +++ b/docs/.vuepress/plugins/love-me/index.js @@ -0,0 +1,12 @@ +const path = require('path') +const LoveMyPlugin = (options = {}) => ({ + define() { + const COLOR = + options.color || + 'rgb(' + ~~(255 * Math.random()) + ',' + ~~(255 * Math.random()) + ',' + ~~(255 * Math.random()) + ')' + const EXCLUDECLASS = options.excludeClassName || '' + return { COLOR, EXCLUDECLASS } + }, + enhanceAppFiles: [path.resolve(__dirname, 'love-me.js')] +}) +module.exports = LoveMyPlugin diff --git a/docs/.vuepress/plugins/love-me/love-me.js b/docs/.vuepress/plugins/love-me/love-me.js new file mode 100644 index 00000000..5c0369ac --- /dev/null +++ b/docs/.vuepress/plugins/love-me/love-me.js @@ -0,0 +1,89 @@ +export default () => { + if (typeof window !== 'undefined') { + ;(function (e, t, a) { + function r() { + for (var e = 0; e < s.length; e++) + s[e].alpha <= 0 + ? (t.body.removeChild(s[e].el), s.splice(e, 1)) + : (s[e].y--, + (s[e].scale += 0.004), + (s[e].alpha -= 0.013), + (s[e].el.style.cssText = + 'left:' + + s[e].x + + 'px;top:' + + s[e].y + + 'px;opacity:' + + s[e].alpha + + ';transform:scale(' + + s[e].scale + + ',' + + s[e].scale + + ') rotate(45deg);background:' + + s[e].color + + ';z-index:99999')) + requestAnimationFrame(r) + } + function n() { + var t = 'function' == typeof e.onclick && e.onclick + + e.onclick = function (e) { + // 过滤指定元素 + let mark = true + EXCLUDECLASS && + e.path && + e.path.forEach((item) => { + if (item.nodeType === 1) { + typeof item.className === 'string' && item.className.indexOf(EXCLUDECLASS) > -1 ? (mark = false) : '' + } + }) + + if (mark) { + t && t(), o(e) + } + } + } + function o(e) { + var a = t.createElement('div') + ;(a.className = 'heart'), + s.push({ + el: a, + x: e.clientX - 5, + y: e.clientY - 5, + scale: 1, + alpha: 1, + color: COLOR + }), + t.body.appendChild(a) + } + function i(e) { + var a = t.createElement('style') + a.type = 'text/css' + try { + a.appendChild(t.createTextNode(e)) + } catch (t) { + a.styleSheet.cssText = e + } + t.getElementsByTagName('head')[0].appendChild(a) + } + // function c() { + // return "rgb(" + ~~ (255 * Math.random()) + "," + ~~ (255 * Math.random()) + "," + ~~ (255 * Math.random()) + ")" + // } + var s = [] + ;(e.requestAnimationFrame = + e.requestAnimationFrame || + e.webkitRequestAnimationFrame || + e.mozRequestAnimationFrame || + e.oRequestAnimationFrame || + e.msRequestAnimationFrame || + function (e) { + setTimeout(e, 1e3 / 60) + }), + i( + ".heart{width: 10px;height: 10px;position: fixed;background: #f00;transform: rotate(45deg);-webkit-transform: rotate(45deg);-moz-transform: rotate(45deg);}.heart:after,.heart:before{content: '';width: inherit;height: inherit;background: inherit;border-radius: 50%;-webkit-border-radius: 50%;-moz-border-radius: 50%;position: fixed;}.heart:after{top: -5px;}.heart:before{left: -5px;}" + ), + n(), + r() + })(window, document) + } +} diff --git a/docs/.vuepress/public/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/img/bg.gif b/docs/.vuepress/public/img/bg.gif new file mode 100644 index 00000000..d4bf3c41 Binary files /dev/null and b/docs/.vuepress/public/img/bg.gif differ diff --git a/docs/.vuepress/public/img/dunwu-logo.png b/docs/.vuepress/public/img/dunwu-logo.png new file mode 100644 index 00000000..61570e2a Binary files /dev/null and b/docs/.vuepress/public/img/dunwu-logo.png differ diff --git a/docs/.vuepress/public/img/favicon.ico b/docs/.vuepress/public/img/favicon.ico new file mode 100644 index 00000000..51e9bfa0 Binary files /dev/null and b/docs/.vuepress/public/img/favicon.ico differ diff --git a/docs/.vuepress/public/img/more.png b/docs/.vuepress/public/img/more.png new file mode 100644 index 00000000..830613ba Binary files /dev/null and b/docs/.vuepress/public/img/more.png differ diff --git a/docs/.vuepress/public/img/other.png b/docs/.vuepress/public/img/other.png new file mode 100644 index 00000000..87f80989 Binary files /dev/null and b/docs/.vuepress/public/img/other.png differ diff --git a/docs/.vuepress/public/markmap/01.html b/docs/.vuepress/public/markmap/01.html new file mode 100644 index 00000000..c4e0bdbc --- /dev/null +++ b/docs/.vuepress/public/markmap/01.html @@ -0,0 +1,113 @@ + + + + + + + Markmap + + + + + + + + + diff --git a/docs/.vuepress/styles/index.styl b/docs/.vuepress/styles/index.styl new file mode 100644 index 00000000..3113dd61 --- /dev/null +++ b/docs/.vuepress/styles/index.styl @@ -0,0 +1,93 @@ +.home-wrapper .banner .banner-conent .hero h1{ + font-size 2.8rem!important +} +// 文档中适配 +table + width auto +.page >*:not(.footer),.card-box + box-shadow: none!important + +.page + @media (min-width $contentWidth + 80) + padding-top $navbarHeight!important +.home-wrapper .banner .banner-conent + padding 0 2.9rem + box-sizing border-box +.home-wrapper .banner .slide-banner .slide-banner-wrapper .slide-item a + h2 + margin-top 2rem + font-size 1.2rem!important + p + padding 0 1rem + +// 评论区颜色重置 +.gt-container + .gt-ico-tip + &::after + content: '。( Win + . ) or ( ⌃ + ⌘ + ␣ ) open Emoji' + color: #999 + .gt-meta + border-color var(--borderColor)!important + .gt-comments-null + color var(--textColor) + opacity .5 + .gt-header-textarea + color var(--textColor) + background rgba(180,180,180,0.1)!important + .gt-btn + border-color $accentColor!important + background-color $accentColor!important + .gt-btn-preview + background-color rgba(255,255,255,0)!important + color $accentColor!important + a + color $accentColor!important + .gt-svg svg + fill $accentColor!important + .gt-comment-content,.gt-comment-admin .gt-comment-content + background-color rgba(150,150,150,0.1)!important + &:hover + box-shadow 0 0 25px rgba(150,150,150,.5)!important + .gt-comment-body + color var(--textColor)!important + + +// qq徽章 +.qq + position: relative; +.qq::after + content: "可撩"; + background: $accentColor; + color:#fff; + padding: 0 5px; + border-radius: 10px; + font-size:12px; + position: absolute; + top: -4px; + right: -35px; + transform:scale(0.85); + +// demo模块图标颜色 +body .vuepress-plugin-demo-block__wrapper + &,.vuepress-plugin-demo-block__display + border-color rgba(160,160,160,.3) + .vuepress-plugin-demo-block__footer:hover + .vuepress-plugin-demo-block__expand::before + border-top-color: $accentColor !important; + border-bottom-color: $accentColor !important; + svg + fill: $accentColor !important; + + +// 全文搜索框 +.suggestions + overflow: auto + max-height: calc(100vh - 6rem) + @media (max-width: 719px) { + width: 90vw; + min-width: 90vw!important; + margin-right: -20px; + } + .highlight + color: $accentColor + font-weight: bold diff --git a/docs/.vuepress/styles/palette.styl b/docs/.vuepress/styles/palette.styl new file mode 100644 index 00000000..d98e697a --- /dev/null +++ b/docs/.vuepress/styles/palette.styl @@ -0,0 +1,62 @@ + +// 原主题变量已弃用,以下是vdoing使用的变量,你可以在这个文件内修改它们。 + +//***vdoing主题-变量***// + +// // 颜色 + +// $bannerTextColor = #fff // 首页banner区(博客标题)文本颜色 +// $accentColor = #11A8CD +// $arrowBgColor = #ccc +// $badgeTipColor = #42b983 +// $badgeWarningColor = darken(#ffe564, 35%) +// $badgeErrorColor = #DA5961 + +// // 布局 +// $navbarHeight = 3.6rem +// $sidebarWidth = 18rem +// $contentWidth = 860px +// $homePageWidth = 1100px +// $rightMenuWidth = 230px // 右侧菜单 + +// // 代码块 +// $lineNumbersWrapperWidth = 2.5rem + +// 浅色模式 +.theme-mode-light + --bodyBg: rgba(255,255,255,1) + --mainBg: rgba(255,255,255,1) + --sidebarBg: rgba(255,255,255,.8) + --blurBg: rgba(255,255,255,.9) + --textColor: #004050 + --textLightenColor: #0085AD + --borderColor: rgba(0,0,0,.15) + --codeBg: #f6f6f6 + --codeColor: #525252 + codeThemeLight() + +// 深色模式 +.theme-mode-dark + --bodyBg: rgba(30,30,34,1) + --mainBg: rgba(30,30,34,1) + --sidebarBg: rgba(30,30,34,.8) + --blurBg: rgba(30,30,34,.8) + --textColor: rgb(140,140,150) + --textLightenColor: #0085AD + --borderColor: #2C2C3A + --codeBg: #252526 + --codeColor: #fff + codeThemeDark() + +// 阅读模式 +.theme-mode-read + --bodyBg: rgba(245,245,213,1) + --mainBg: rgba(245,245,213,1) + --sidebarBg: rgba(245,245,213,.8) + --blurBg: rgba(245,245,213,.9) + --textColor: #004050 + --textLightenColor: #0085AD + --borderColor: rgba(0,0,0,.15) + --codeBg: #282c34 + --codeColor: #fff + codeThemeDark() diff --git "a/docs/01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" "b/docs/01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" new file mode 100644 index 00000000..2133814e --- /dev/null +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" @@ -0,0 +1,313 @@ +--- +title: JavaWeb 之 Servlet 指南 +date: 2020-08-24 19:41:46 +order: 01 +categories: + - Java + - JavaEE + - JavaWeb +tags: + - Java + - JavaWeb + - Servlet +permalink: /pages/e98894/ +--- + +# JavaWeb 之 Servlet 指南 + +## JavaWeb 简介 + +### Web 应用程序 + +Web,在英语中 web 即表示网页的意思,它用于表示 Internet 主机上供外界访问的资源。 + +Web 应用程序是一种可以通过 Web 访问的应用程序,程序的最大好处是用户很容易访问应用程序,用户只需要有浏览器即可,不需要再安装其他软件。 + +Internet 上供外界访问的 Web 资源分为: + +- 静态 web 资源:指 web 页面中供人们浏览的数据始终是不变。常见静态资源文件:html、css、各种图片类型(jpg、png) +- 动态 web 资源:指 web 页面中供人们浏览的数据是由程序产生的,不同时间点访问 web 页面看到的内容各不相同。常见动态资源技术:JSP/Servlet、ASP、PHP + +### 常见 Web 服务器 + +- [Tomcat](http://tomcat.apache.org/) +- [Jetty](http://www.eclipse.org/jetty/) +- [Resin](https://caucho.com/) +- [Apache](http://httpd.apache.org/) +- [Nginx](http://nginx.org/en/) +- [WebSphere](https://www.ibm.com/cloud/websphere-application-platform) +- [WebLogic](https://www.oracle.com/middleware/technologies/weblogic.html) +- JBoss + +## Servlet 简介 + +### 什么是 Servlet + +Servlet(Server Applet),即小服务程序或服务连接器。Servlet 是 Java 编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态 Web 内容。 + +- 狭义的 Servlet 是指 Java 实现的一个接口。 +- 广义的 Servlet 是指任何实现了这个 Servlet 接口的类。 + +Servlet 运行于支持 Java 的应用服务器中。从原理上讲,Servlet 可以响应任何类型的请求,但绝大多数情况下 Servlet 只用来扩展基于 HTTP 协议的 Web 服务器。 + +### Servlet 和 CGI 的区别 + +Servlet 技术出现之前,Web 主要使用 CGI 技术。它们的区别如下: + +- Servlet 是基于 Java 编写的,处于服务器进程中,他能够通过多线程方式运行 service() 方法,一个实例可以服务于多个请求,而且一般不会销毁; +- CGI(Common Gateway Interface),即通用网关接口。它会为每个请求产生新的进程,服务完成后销毁,所以效率上低于 Servlet。 + +### Servlet 版本以及主要特性 + +| 版本 | 日期 | JAVA EE/JDK 版本 | 特性 | +| ----------- | ------------- | ------------------ | --------------------------------------------------------------------- | +| Servlet 4.0 | 2017 年 10 月 | JavaEE 8 | HTTP2 | +| Servlet 3.1 | 2013 年 5 月 | JavaEE 7 | 非阻塞 I/O,HTTP 协议升级机制 | +| Servlet 3.0 | 2009 年 12 月 | JavaEE 6, JavaSE 6 | 可插拔性,易于开发,异步 Servlet,安全性,文件上传 | +| Servlet 2.5 | 2005 年 10 月 | JavaEE 5, JavaSE 5 | 依赖 JavaSE 5,支持注解 | +| Servlet 2.4 | 2003 年 11 月 | J2EE 1.4, J2SE 1.3 | web.xml 使用 XML Schema | +| Servlet 2.3 | 2001 年 8 月 | J2EE 1.3, J2SE 1.2 | Filter | +| Servlet 2.2 | 1999 年 8 月 | J2EE 1.2, J2SE 1.2 | 成为 J2EE 标准 | +| Servlet 2.1 | 1998 年 11 月 | 未指定 | First official specification, added RequestDispatcher, ServletContext | +| Servlet 2.0 | | JDK 1.1 | Part of Java Servlet Development Kit 2.0 | +| Servlet 1.0 | 1997 年 6 月 | | | + +### Servlet 任务 + +Servlet 执行以下主要任务: + +- 读取客户端(浏览器)发送的显式的数据。这包括网页上的 HTML 表单,或者也可以是来自 applet 或自定义的 HTTP 客户端程序的表单。 +- 读取客户端(浏览器)发送的隐式的 HTTP 请求数据。这包括 cookies、媒体类型和浏览器能理解的压缩格式等等。 +- 处理数据并生成结果。这个过程可能需要访问数据库,执行 RMI 或 CORBA 调用,调用 Web 服务,或者直接计算得出对应的响应。 +- 发送显式的数据(即文档)到客户端(浏览器)。该文档的格式可以是多种多样的,包括文本文件(HTML 或 XML)、二进制文件(GIF 图像)、Excel 等。 +- 发送隐式的 HTTP 响应到客户端(浏览器)。这包括告诉浏览器或其他客户端被返回的文档类型(例如 HTML),设置 cookies 和缓存参数,以及其他类似的任务。 + +### Servlet 生命周期 + +![img](http://www.runoob.com/wp-content/uploads/2014/07/Servlet-LifeCycle.jpg) + +Servlet 生命周期如下: + +1. **加载** - 第一个到达服务器的 HTTP 请求被委派到 Servlet 容器。容器通过类加载器使用 Servlet 类对应的文件加载 servlet; +2. **初始化** - Servlet 通过调用 **init ()** 方法进行初始化。 +3. **服务** - Servlet 调用 **service()** 方法来处理客户端的请求。 +4. **销毁** - Servlet 通过调用 **destroy()** 方法终止(结束)。 +5. **卸载** - Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。 + +## Servlet API + +### Servlet 包 + +Java Servlet 是运行在带有支持 Java Servlet 规范的解释器的 web 服务器上的 Java 类。 + +Servlet 可以使用 **javax.servlet** 和 **javax.servlet.http** 包创建,它是 Java 企业版的标准组成部分,Java 企业版是支持大型开发项目的 Java 类库的扩展版本。 + +Java Servlet 就像任何其他的 Java 类一样已经被创建和编译。在您安装 Servlet 包并把它们添加到您的计算机上的 Classpath 类路径中之后,您就可以通过 JDK 的 Java 编译器或任何其他编译器来编译 Servlet。 + +### Servlet 接口 + +Servlet 接口定义了下面五个方法: + +```java +public interface Servlet { + void init(ServletConfig var1) throws ServletException; + + ServletConfig getServletConfig(); + + void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException; + + String getServletInfo(); + + void destroy(); +} +``` + +#### 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()` 方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。 + +`service()` 方法有两个参数:`ServletRequest` 和 `ServletResponse`。`ServletRequest` 用来封装请求信息,`ServletResponse` 用来封装响应信息,因此**本质上这两个类是对通信协议的封装。** + +每次服务器接收到一个 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 状态码 + +title: JavaEE Servlet HTTP 状态码 +date: 2017-11-08 +categories: + +- javaee + tags: +- javaee +- servlet +- http + +### 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 状态码和相关的信息列表: + +- `1**`:信息性状态码 +- `2**`:成功状态码 + - 200:请求正常成功 + - 204:指示请求成功但没有返回新信息 + - 206:指示服务器已完成对资源的部分 GET 请求 +- `3**`:重定向状态码 + - 301:永久性重定向 + - 302:临时性重定向 + - 304:服务器端允许请求访问资源,但未满足条件 +- `4**`:客户端错误状态码 + - 400:请求报文中存在语法错误 + - 401:发送的请求需要有通过 HTTP 认证的认证信息 + - 403:对请求资源的访问被服务器拒绝了 + - 404:服务器上无法找到请求的资源 +- `5**`:服务器错误状态码 + - 500:服务器端在执行请求时发生了错误 + - 503:服务器暂时处于超负载或正在进行停机维护,现在无法处理请求 + +### 设置 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 +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 +``` + +## 参考资料 + +- [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) +- [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" "b/docs/01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" new file mode 100644 index 00000000..32b8ab7c --- /dev/null +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" @@ -0,0 +1,1731 @@ +--- +title: JavaWeb 之 Jsp 指南 +date: 2020-02-07 23:04:47 +order: 02 +categories: + - Java + - JavaEE + - JavaWeb +tags: + - Java + - JavaWeb + - JSP +permalink: /pages/8cc787/ +--- + +# JavaWeb 之 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()方法就行了,就像下面这样: + +```java +public void jspInit(){ + // 初始化代码 +} +``` + +一般来讲程序只初始化一次,servlet 也是如此。通常情况下您可以在 jspInit()方法中初始化数据库连接、打开文件和创建查询表。 + +#### JSP 执行 + +这一阶段描述了 JSP 生命周期中一切与请求相关的交互行为,直到被销毁。 + +当 JSP 网页完成初始化后,JSP 引擎将会调用 `_jspService()` 方法。 + +`_jspService()` 方法需要一个 HttpServletRequest 对象和一个 HttpServletResponse 对象作为它的参数,就像下面这样: + +```java +void _jspService(HttpServletRequest request, + HttpServletResponse response) { + // 服务端处理代码 +} +``` + +`_jspService()` 方法在每个 request 中被调用一次并且负责产生与之相对应的 response,并且它还负责产生所有 7 个 HTTP 方法的回应,比如 GET、POST、DELETE 等等。 + +#### JSP 清理 + +JSP 生命周期的销毁阶段描述了当一个 JSP 网页从容器中被移除时所发生的一切。 + +jspDestroy()方法在 JSP 中等价于 servlet 中的销毁方法。当您需要执行任何清理工作时复写 jspDestroy()方法,比如释放数据库连接或者关闭文件夹等等。 + +jspDestroy()方法的格式如下: + +```java +public void jspDestroy() { + // 清理代码 +} +``` + +## 语法 + +### 脚本 + +脚本程序可以包含任意量的 Java 语句、变量、方法或表达式,只要它们在脚本语言中是有效的。 + +脚本程序的语法格式: + +``` +<% 代码片段 %> +``` + +或者,您也可以编写与其等价的 XML 语句,就像下面这样: + +``` + + 代码片段 + +``` + +任何文本、HTML 标签、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 文件头部添加以下代码:`<>` + +``` +<%@ page language="java" contentType="text/html; charset=UTF-8" +pageEncoding="UTF-8"%> +``` + +接下来我们将以上程序修改为: + +``` +<%@ 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 声明的语法格式: + +``` +<%! declaration; [ declaration; ]+ ... %> +``` + +或者,您也可以编写与其等价的 XML 语句,就像下面这样: + +``` + + 代码片段 + +``` + +程序示例: + +``` +<%! 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 注释的语法格式: + +``` +<%@ 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`块,请看下面这个例子: + +``` +<%@ 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(),并且整个都装在脚本程序的标签中,就像下面这样: + +``` +<%@ 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。 + +## 指令 + +JSP 指令用来设置整个 JSP 页面相关的属性,如网页的编码方式和脚本语言。 + +JSP 指令以开`<%@`开始,以`%>`结束。 + +JSP 指令语法格式如下: + +``` +<%@ directive attribute="value" %> +``` + +指令可以有很多个属性,它们以键值对的形式存在,并用逗号隔开。 + +JSP 中的三种指令标签: + +| **指令** | **描述** | +| -------------------- | -------------------------------------------------------- | +| `<%@ page ... %>` | 定义网页依赖属性,比如脚本语言、error 页面、缓存需求等等 | +| `<%@ include ... %>` | 包含其他文件 | +| `<%@ taglib ... %>` | 引入标签库的定义,可以是自定义标签 | + +### Page 指令 + +Page 指令为容器提供当前页面的使用说明。一个 JSP 页面可以包含多个`page`指令。 + +Page 指令的语法格式: + +``` +<%@ page attribute="value" %> +``` + +等价的 XML 格式: + +``` + +``` + +例: + +``` +<%@ 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 指令的语法格式如下: + +``` +<%@ include file="文件相对 url 地址" %> +``` + +**include** 指令中的文件名实际上是一个相对的 URL 地址。 + +如果您没有给文件关联一个路径,JSP 编译器默认在当前路径下寻找。 + +等价的 XML 语法: + +``` + +``` + +### Taglib 指令 + +JSP 允许用户自定义标签,一个自定义标签库就是自定义标签的集合。 + +`taglib`指令引入一个自定义标签集合的定义,包括库路径、自定义标签。 + +`taglib`指令的语法: + +``` +<%@ taglib uri="uri" prefix="prefixOfTag" %> +``` + +uri 属性确定标签库的位置,prefix 属性指定标签库的前缀。 + +等价的 XML 语法: + +``` + +``` + +## 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 文件代码: + +``` +<%@ page language="java" contentType="text/html; charset=UTF-8" +pageEncoding="UTF-8"%> +

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

+``` + +main.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: + +```java +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 + + + + +``` + +你可以对以上实例尝试使用及不使用该动作元素执行结果的区别。 + +## 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 对象的使用方法,不是很重要,所以不常用: + +``` +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 中的相关属性: + +``` +pageContext.removeAttribute("attrName", PAGE_SCOPE); +``` + +### page 对象 + +这个对象就是页面实例的引用。它可以被看做是整个 JSP 页面的代表。 + +`page`对象就是`this`对象的同义词。 + +### exception 对象 + +`exception`对象包装了从先前页面中抛出的异常信息。它通常被用来产生对出错条件的适当响应。 + +## EL 表达式 + +EL 表达式是用`${}`括起来的脚本,用来更方便地读取对象。EL 表达式写在 JSP 的 HTML 代码中,而不能写在`<%`与`%>`引起的 JSP 脚本中。 + +JSP 表达式语言(EL)使得访问存储在 JavaBean 中的数据变得非常简单。JSP EL 既可以用来创建算术表达式也可以用来创建逻辑表达式。在 JSP EL 表达式内可以使用整型数,浮点数,字符串,常量 true、false,还有 null。 + +### 一个简单的语法 + +典型的,当您需要在 JSP 标签中指定一个属性值时,只需要简单地使用字符串即可: + +``` + +``` + +JSP EL 允许您指定一个表达式来表示属性值。一个简单的表达式语法如下: + +``` +${expr} +``` + +其中,expr 指的是表达式。在 JSP EL 中通用的操作符是"."和"[]"。这两个操作符允许您通过内嵌的 JSP 对象访问各种各样的 JavaBean 属性。 + +举例来说,上面的 `` 标签可以使用表达式语言改写成如下形式: + +``` + +``` + +当 JSP 编译器在属性中见到"\${}"格式后,它会产生代码来计算这个表达式,并且产生一个替代品来代替表达式的值。 + +您也可以在标签的模板文本中使用表达式语言。比如 `` 标签简单地将其主体中的文本插入到 JSP 输出中: + +``` + +

Hello JSP!

+
+``` + +现在,在标签主体中使用表达式,就像这样: + +``` + + Box Perimeter is: ${2*box.width + 2*box.height} + +``` + +在 EL 表达式中可以使用圆括号来组织子表达式。比如 `${(1 + 2) _ 3}` 等于 9,但是 `${1 + (2 _ 3)}` 等于 7。 + +想要停用对 EL 表达式的评估的话,需要使用 page 指令将 isELIgnored 属性值设为 true: + +``` +<%@ 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 允许您在表达式中使用函数。这些函数必须被定义在自定义标签库中。函数的使用语法如下: + +``` +${ns:func(param1, param2, ...)} +``` + +ns 指的是命名空间(namespace),func 指的是函数的名称,param1 指的是第一个参数,param2 指的是第二个参数,以此类推。比如,有函数 fn:length,在 JSTL 库中定义,可以像下面这样来获取一个字符串的长度: + +``` +${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 对象传入的查询字符串,就像这样: + +``` +${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 参数: + +``` +<%@ 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 信息头: + +``` +<%@ page import="java.io.*,java.util.*" %> <% String title = "User Agent +Example"; %> + + + <% out.print(title); %> + + +
+

<% out.print(title); %>

+
+
+

${header["user-agent"]}

+
+ + +``` + +运行结果如下: + +![img](http://www.runoob.com/wp-content/uploads/2014/01/jsp-expression-language.jpg) + +header 对象返回单一值,而 headerValues 则返回一个字符串数组。 + +## JSTL + +JSP 标准标签库(JSTL)是一个 JSP 标签集合,它封装了 JSP 应用的通用核心功能。 + +JSTL 支持通用的、结构化的任务,比如迭代,条件判断,XML 文档操作,国际化标签,SQL 标签。 除了这些,它还提供了一个框架来使用集成 JSTL 的自定义标签。 + +根据 JSTL 标签所提供的功能,可以将其分为 5 个类别。 + +- **核心标签** +- **格式化标签** +- **SQL 标签** +- **XML 标签** +- **JSTL 函数** + +### JSTL 库安装 + +Apache Tomcat 安装 JSTL 库步骤如下: + +从 Apache 的标准标签库中下载的二进包(jakarta-taglibs-standard-current.zip)。 + +- 官方下载地址: +- 本站下载地址:[jakarta-taglibs-standard-1.1.2.zip](http://static.runoob.com/download/jakarta-taglibs-standard-1.1.2.tar.gz) + +下载 **jakarta-taglibs-standard-1.1.2.zip** 包并解压,将 **jakarta-taglibs-standard-1.1.2/lib/** 下的两个 jar 文件:**standard.jar** 和 **jstl.jar** 文件拷贝到 **/WEB-INF/lib/** 下。 + +将 tld 下的需要引入的 tld 文件复制到 WEB-INF 目录下。 + +接下来我们在 web.xml 文件中添加以下配置: + +``` + + + + + http://java.sun.com/jsp/jstl/fmt + /WEB-INF/fmt.tld + + + http://java.sun.com/jsp/jstl/fmt-rt + /WEB-INF/fmt-rt.tld + + + http://java.sun.com/jsp/jstl/core + /WEB-INF/c.tld + + + http://java.sun.com/jsp/jstl/core-rt + /WEB-INF/c-rt.tld + + + http://java.sun.com/jsp/jstl/sql + /WEB-INF/sql.tld + + + http://java.sun.com/jsp/jstl/sql-rt + /WEB-INF/sql-rt.tld + + + http://java.sun.com/jsp/jstl/x + /WEB-INF/x.tld + + + http://java.sun.com/jsp/jstl/x-rt + /WEB-INF/x-rt.tld + + + +``` + +使用任何库,你必须在每个 JSP 文件中的头部包含 **``** 标签。 + +### 核心标签 + +核心标签是最常用的 JSTL 标签。引用核心标签库的语法如下: + +``` +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +``` + +| 标签 | 描述 | +| :---------------------------------------------------------------------- |:------------------------------------------------------------------| +| [``](http://www.runoob.com/jsp/jstl-core-out-tag.html) | 用于在 JSP 中显示数据,就像<%= ... > | +| [``](http://www.runoob.com/jsp/jstl-core-set-tag.html) | 用于保存数据 | +| [``](http://www.runoob.com/jsp/jstl-core-remove-tag.html) | 用于删除数据 | +| [``](http://www.runoob.com/jsp/jstl-core-catch-tag.html) | 用来处理产生错误的异常状况,并且将错误信息储存起来 | +| [``](http://www.runoob.com/jsp/jstl-core-if-tag.html) | 与我们在一般程序中用的 if 一样 | +| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | 本身只当做 `` 和 `` 的父标签 | +| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | `` 的子标签,用来判断条件是否成立 | +| [``](http://www.runoob.com/jsp/jstl-core-choose-tag.html) | `` 的子标签,接在 `` 标签后,当 `` 标签判断为 false 时被执行 | +| [``](http://www.runoob.com/jsp/jstl-core-import-tag.html) | 检索一个绝对或相对 URL,然后将其内容暴露给页面 | +| [``](http://www.runoob.com/jsp/jstl-core-foreach-tag.html) | 基础迭代标签,接受多种集合类型 | +| [``](http://www.runoob.com/jsp/jstl-core-foreach-tag.html) | 根据指定的分隔符来分隔内容并迭代输出 | +| [``](http://www.runoob.com/jsp/jstl-core-param-tag.html) | 用来给包含或重定向的页面传递参数 | +| [``](http://www.runoob.com/jsp/jstl-core-redirect-tag.html) | 重定向至一个新的 URL. | +| [``](http://www.runoob.com/jsp/jstl-core-url-tag.html) | 使用可选的查询参数来创造一个 URL | + +### 格式化标签 + +JSTL 格式化标签用来格式化并输出文本、日期、时间、数字。引用格式化标签库的语法如下: + +``` +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +``` + +| 标签 | 描述 | +| :---------------------------------------------------------------------------------------- | :--------------------------------------- | +| [``](http://www.runoob.com/jsp/jstl-format-formatnumber-tag.html) | 使用指定的格式或精度格式化数字 | +| [``](http://www.runoob.com/jsp/jstl-format-parsenumber-tag.html) | 解析一个代表着数字,货币或百分比的字符串 | +| [``](http://www.runoob.com/jsp/jstl-format-formatdate-tag.html) | 使用指定的风格或模式格式化日期和时间 | +| [``](http://www.runoob.com/jsp/jstl-format-parsedate-tag.html) | 解析一个代表着日期或时间的字符串 | +| [``](http://www.runoob.com/jsp/jstl-format-bundle-tag.html) | 绑定资源 | +| [``](http://www.runoob.com/jsp/jstl-format-setlocale-tag.html) | 指定地区 | +| [``](http://www.runoob.com/jsp/jstl-format-setbundle-tag.html) | 绑定资源 | +| [``](http://www.runoob.com/jsp/jstl-format-timezone-tag.html) | 指定时区 | +| [``](http://www.runoob.com/jsp/jstl-format-settimezone-tag.html) | 指定时区 | +| [``](http://www.runoob.com/jsp/jstl-format-message-tag.html) | 显示资源配置文件信息 | +| [``](http://www.runoob.com/jsp/jstl-format-requestencoding-tag.html) | 设置 request 的字符编码 | + +### SQL 标签 + +JSTL SQL 标签库提供了与关系型数据库(Oracle,MySQL,SQL Server 等等)进行交互的标签。引用 SQL 标签库的语法如下: + +``` +<%@ taglib prefix="sql" uri="http://java.sun.com/jsp/jstl/sql" %> +``` + +| 标签 | 描述 | +| :--------------------------------------------------------------------------------- | :--------------------------------------------------------------------------- | +| [``](http://www.runoob.com/jsp/jstl-sql-setdatasource-tag.html) | 指定数据源 | +| [``](http://www.runoob.com/jsp/jstl-sql-query-tag.html) | 运行 SQL 查询语句 | +| [``](http://www.runoob.com/jsp/jstl-sql-update-tag.html) | 运行 SQL 更新语句 | +| [``](http://www.runoob.com/jsp/jstl-sql-param-tag.html) | 将 SQL 语句中的参数设为指定值 | +| [``](http://www.runoob.com/jsp/jstl-sql-dateparam-tag.html) | 将 SQL 语句中的日期参数设为指定的 java.util.Date 对象值 | +| [``](http://www.runoob.com/jsp/jstl-sql-transaction-tag.html) | 在共享数据库连接中提供嵌套的数据库行为元素,将所有语句以一个事务的形式来运行 | + +### XML 标签 + +JSTL XML 标签库提供了创建和操作 XML 文档的标签。引用 XML 标签库的语法如下: + +``` +<%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml" %> +``` + +在使用 xml 标签前,你必须将 XML 和 XPath 的相关包拷贝至你的 `\lib` 下: + +- XercesImpl.jar + + 下载地址: + +- xalan.jar + + 下载地址: + +| 标签 | 描述 | +| :----------------------------------------------------------------------- |:----------------------------------------------| +| [``](http://www.runoob.com/jsp/jstl-xml-out-tag.html) | 与 `<%= ... >`,类似,不过只用于 XPath 表达式 | +| [``](http://www.runoob.com/jsp/jstl-xml-parse-tag.html) | 解析 XML 数据 | +| [``](http://www.runoob.com/jsp/jstl-xml-set-tag.html) | 设置 XPath 表达式 | +| [``](http://www.runoob.com/jsp/jstl-xml-if-tag.html) | 判断 XPath 表达式,若为真,则执行本体中的内容,否则跳过本体 | +| [``](http://www.runoob.com/jsp/jstl-xml-foreach-tag.html) | 迭代 XML 文档中的节点 | +| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | `` 和 `` 的父标签 | +| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | `` 的子标签,用来进行条件判断 | +| [``](http://www.runoob.com/jsp/jstl-xml-choose-tag.html) | `` 的子标签,当 `` 判断为 false 时被执行 | +| [``](http://www.runoob.com/jsp/jstl-xml-transform-tag.html) | 将 XSL 转换应用在 XML 文档中 | +| [``](http://www.runoob.com/jsp/jstl-xml-param-tag.html) | 与 `` 共同使用,用于设置 XSL 样式表 | + +### JSTL 函数 + +JSTL 包含一系列标准函数,大部分是通用的字符串处理函数。引用 JSTL 函数库的语法如下: + +``` +<%@ 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) | 移除首尾的空白符 | + +## Taglib + +### JSP 自定义标签 + +自定义标签是用户定义的 JSP 语言元素。当 JSP 页面包含一个自定义标签时将被转化为 servlet,标签转化为对被 称为 tag handler 的对象的操作,即当 servlet 执行时 Web container 调用那些操作。 + +JSP 标签扩展可以让你创建新的标签并且可以直接插入到一个 JSP 页面。 JSP 2.0 规范中引入 Simple Tag Handlers 来编写这些自定义标记。 + +你可以继承 SimpleTagSupport 类并重写的 doTag()方法来开发一个最简单的自定义标签。 + +### 创建"Hello"标签 + +接下来,我们想创建一个自定义标签叫作,标签格式为: + +``` + +``` + +要创建自定义的 JSP 标签,你首先必须创建处理标签的 Java 类。所以,让我们创建一个 HelloTag 类,如下所示: + +``` +package com.runoob; import javax.servlet.jsp.tagext.*; import +javax.servlet.jsp.*; import java.io.*; public class HelloTag extends +SimpleTagSupport { public void doTag() throws JspException, IOException { +JspWriter out = getJspContext().getOut(); out.println("Hello Custom Tag!"); } } +``` + +以下代码重写了 doTag()方法,方法中使用了 getJspContext()方法来获取当前的 JspContext 对象,并将"Hello Custom Tag!"传递给 JspWriter 对象。 + +编译以上类,并将其复制到环境变量 CLASSPATH 目录中。最后创建如下标签库:`webapps\ROOT\WEB-INF\custom.tld`。 + +``` + + 1.0 + 2.0 + Example TLD + + Hello + com.runoob.HelloTag + empty + + +``` + +接下来,我们就可以在 JSP 文件中使用 Hello 标签: + +``` +<%@ taglib prefix="ex" uri="WEB-INF/custom.tld"%> + + + A sample custom tag + + + + + +``` + +以上程序输出结果为: + +``` +Hello Custom Tag! +``` + +### 访问标签体 + +你可以像标准标签库一样在标签中包含消息内容。如我们要在我们自定义的 Hello 中包含内容,格式如下: + +``` + + This is message body + +``` + +我们可以修改标签处理类文件,代码如下: + +```java +package com.runoob; + +import javax.servlet.jsp.tagext.*; +import javax.servlet.jsp.*; +import java.io.*; + +public class HelloTag extends SimpleTagSupport { + + StringWriter sw = new StringWriter(); + public void doTag() + throws JspException, IOException + { + getJspBody().invoke(sw); + getJspContext().getOut().println(sw.toString()); + } + +} +``` + +接下来我们需要修改 TLD 文件,如下所示: + +``` + + 1.0 + 2.0 + Example TLD with Body + + Hello + com.runoob.HelloTag + scriptless + + +``` + +现在我们可以在 JSP 使用修改后的标签,如下所示: + +``` +<%@ taglib prefix="ex" uri="WEB-INF/custom.tld"%> + + + A sample custom tag + + + + This is message body + + + +``` + +以上程序输出结果如下所示: + +``` +This is message body +``` + +### 自定义标签属性 + +你可以在自定义标准中设置各种属性,要接收属性,值自定义标签类必须实现 setter 方法, JavaBean 中的 setter 方法如下所示: + +```java +package com.runoob; + +import javax.servlet.jsp.tagext.*; +import javax.servlet.jsp.*; +import java.io.*; + +public class HelloTag 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()); + } + } + +} +``` + +属性的名称是"message",所以 setter 方法是的 setMessage()。现在让我们在 TLD 文件中使用的 `` 元素添加此属性: + +``` + + 1.0 + 2.0 + Example TLD with Body + + Hello + com.runoob.HelloTag + scriptless + + message + + + +``` + +现在我们就可以在 JSP 文件中使用 message 属性了,如下所示: + +``` +<%@ taglib prefix="ex" uri="WEB-INF/custom.tld"%> + + + A sample custom tag + + + + + +``` + +以上实例数据输出结果为: + +``` +This is custom tag +``` + +你还可以包含以下属性: + +| 属性 | 描述 | +| :---------- | :------------------------------------------------------- | +| name | 定义属性的名称。每个标签的是属性名称必须是唯一的。 | +| required | 指定属性是否是必须的或者可选的,如果设置为 false 为可选。 | +| rtexprvalue | 声明在运行表达式时,标签属性是否有效。 | +| type | 定义该属性的 Java 类类型 。默认指定为 **String** | +| description | 描述信息 | +| fragment | 如果声明了该属性,属性值将被视为一个 **JspFragment**。 | + +以下是指定相关的属性实例: + +``` +..... + + attribute_name + false + java.util.Date + false + +..... +``` + +如果你使用了两个属性,修改 TLD 文件,如下所示: + +``` +..... + + attribute_name1 + false + java.util.Boolean + false + + + attribute_name2 + true + java.util.Date + +..... +``` \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" "b/docs/01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" new file mode 100644 index 00000000..aea60247 --- /dev/null +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" @@ -0,0 +1,218 @@ +--- +title: JavaWeb 之 Filter 和 Listener +date: 2020-08-24 19:41:46 +order: 03 +categories: + - Java + - JavaEE + - JavaWeb +tags: + - Java + - JavaWeb + - Filter + - Listener +permalink: /pages/82df5f/ +--- + +# JavaWeb 之 Filter 和 Listener + +引入了 Servlet 规范后,你不需要关心 Socket 网络通信、不需要关心 HTTP 协议,也不需要关心你的业务类是如何被实例化和调用的,因为这些都被 Servlet 规范标准化了,你只要关心怎么实现的你的业务逻辑。这对于程序员来说是件好事,但也有不方便的一面。所谓规范就是说大家都要遵守,就会千篇一律,但是如果这个规范不能满足你的业务的个性化需求,就有问题了,因此设计一个规范或者一个中间件,要充分考虑到可扩展性。Servlet 规范提供了两种扩展机制:**Filter**和**Listener**。 + +## Filter + +**Filter 是过滤器,这个接口允许你对请求和响应做一些统一的定制化处理**。 + +Filter 提供了过滤链(Filter Chain)的概念,一个过滤链包括多个 Filter。客户端请求 request 在抵达 Servlet 之前会经过过滤链的所有 Filter,服务器响应 response 从 Servlet 抵达客户端浏览器之前也会经过过滤链的所有 FIlter。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/1559054413341.png) + +### 过滤器方法 + +Filter 接口有三个方法: + +- `init`:初始化 `Filter` +- `destroy`:销毁 `Filter` +- `doFilter`:将请求传给下个 `Filter` 或 `Servlet` + +`init` 和 `destroy` 方法只会被调用一次;`doFilter` 每次有客户端请求都会被调用一次。 + +```java +public interface Filter { + + /** + * web 程序启动时调用此方法, 用于初始化该 Filter + * @param config + * 可以从该参数中获取初始化参数以及ServletContext信息等 + * @throws ServletException + */ + public void init(FilterConfig config) throws ServletException; + + /** + * 客户请求服务器时会经过 + * + * @param request + * 客户请求 + * @param response + * 服务器响应 + * @param chain + * 过滤链, 通过 chain.doFilter(request, response) 将请求传给下个 Filter 或 + * Servlet + * @throws ServletException + * @throws IOException + */ + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws ServletException, IOException; + + /** + * web 程序关闭时调用此方法, 用于销毁一些资源 + */ + public void destroy(); + +} +``` + +### 过滤器配置 + +`Filter` 需要配置在 `web.xml` 中才能生效。一个 `Filter` 需要配置 `` 与 `` 标签。 + +- `` 配置 Filter 名称,实现类以及初始化参数。 +- `` 配置什么规则下使用该 Filter。 +- `` 的 filterName 与 `` 的 filterName 必须匹配。 +- `` 配置 URL 的规则,可以配置多个,可以使用通配符(`*`)。 +- `` 配置到达 Servlet 的方式,有 4 种取值:REQUEST、FORWARD、INCLUDE、ERROR。可以同时配置多个 ``。如果没有配置任何 ``,默认为 REQUEST。 + - REQUEST - 表示仅当直接请求 Servlet 时才生效。 + - FORWARD - 表示仅当某 Servlet 通过 FORWARD 到该 Servlet 时才生效。 + - INCLUDE - JSP 中可以通过 `` 请求某 Servlet。仅在这种情况表有效。 + - ERROR - JSP 中可以通过 `<%@ page errorPage="error.jsp" %>` 指定错误处理页面。仅在这种情况表有效。 + +## Listener + +监听器(`Listener`)用于监听 web 应用程序中的`ServletContext`, `HttpSession`和 `ServletRequest`等域对象的创建与销毁事件,以及监听这些域对象中的属性发生修改的事件。 + +使用 `Listener` 不需要关注该类事件时怎样触发或者怎么调用相应的 `Listener`,只要记住该类事件触发时一定会调用相应的 `Listener`,遵循 Servlet 规范的服务器会自动完成相应工作。 + +### 监听器的分类 + +在 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` 中的事件。 + +- 当对象被绑定到 `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` 对象一起被反序列化回到内存中 + +## Filter 和 Listener + +Filter 和 Listener 的本质区别: + +- **Filter 是干预过程的**,它是过程的一部分,是基于过程行为的。 +- **Listener 是基于状态的**,任何行为改变同一个状态,触发的事件是一致的。 + +## 示例代码 + +- `Filter` 的示例源码:[源码](https://github.com/dunwu/javatech/tree/master/codes/javaee-tutorial/javaee-tutorial-filter) +- `Listener` 的示例源码:[源码](https://github.com/dunwu/javatech/tree/master/codes/javaee-tutorial/javaee-tutorial-listener) + +## 参考资料 + +- [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) +- [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" "b/docs/01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" new file mode 100644 index 00000000..3729f4a6 --- /dev/null +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" @@ -0,0 +1,590 @@ +--- +title: JavaWeb 之 Cookie 和 Session +date: 2020-08-24 19:41:46 +order: 04 +categories: + - Java + - JavaEE + - JavaWeb +tags: + - Java + - JavaWeb + - Cookie + - Session +permalink: /pages/c46bff/ +--- + +# JavaWeb 之 Cookie 和 Session + +## Cookie + +由于 Http 是一种无状态的协议,服务器单从网络连接上无从知道客户身份。 + +会话跟踪是 Web 程序中常用的技术,用来跟踪用户的整个会话。常用会话跟踪技术是 Cookie 与 Session。 + +### 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 +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 到服务器。浏览器的头信息可能如下所示: + +```http +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 实例 + +#### 添加 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 + +```java +<%@ 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); + } + +} +``` + +## 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 实例 + +#### 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。 + +## Cookie vs 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 不支持跨域名。 \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" "b/docs/01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" new file mode 100644 index 00000000..8e803b40 --- /dev/null +++ "b/docs/01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" @@ -0,0 +1,269 @@ +--- +title: JavaWeb 面经 +date: 2020-02-07 23:04:47 +order: 99 +categories: + - Java + - JavaEE + - JavaWeb +tags: + - Java + - JavaWeb + - Servlet +permalink: /pages/e175ce/ +--- + +# JavaWeb 面经 + +## Servlet + +### 什么是 Servlet + +Servlet(Server Applet),即小服务程序或服务连接器。Servlet 是 Java 编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态 Web 内容。 + +- 狭义的 Servlet 是指 Java 实现的一个接口。 +- 广义的 Servlet 是指任何实现了这个 Servlet 接口的类。 + +Servlet 运行于支持 Java 的应用服务器中。从原理上讲,Servlet 可以响应任何类型的请求,但绝大多数情况下 Servlet 只用来扩展基于 HTTP 协议的 Web 服务器。 + +### Servlet 和 CGI 的区别 + +Servlet 技术出现之前,Web 主要使用 CGI 技术。它们的区别如下: + +- Servlet 是基于 Java 编写的,处于服务器进程中,他能够通过多线程方式运行 service() 方法,一个实例可以服务于多个请求,而且一般不会销毁; +- CGI(Common Gateway Interface),即通用网关接口。它会为每个请求产生新的进程,服务完成后销毁,所以效率上低于 Servlet。 + +### Servlet 版本以及主要特性 + +| 版本 | 日期 | JAVA EE/JDK 版本 | 特性 | +| ----------- | ------------- | ------------------ | --------------------------------------------------------------------- | +| Servlet 4.0 | 2017 年 10 月 | JavaEE 8 | HTTP2 | +| Servlet 3.1 | 2013 年 5 月 | JavaEE 7 | 非阻塞 I/O,HTTP 协议升级机制 | +| Servlet 3.0 | 2009 年 12 月 | JavaEE 6, JavaSE 6 | 可插拔性,易于开发,异步 Servlet,安全性,文件上传 | +| Servlet 2.5 | 2005 年 10 月 | JavaEE 5, JavaSE 5 | 依赖 JavaSE 5,支持注解 | +| Servlet 2.4 | 2003 年 11 月 | J2EE 1.4, J2SE 1.3 | web.xml 使用 XML Schema | +| Servlet 2.3 | 2001 年 8 月 | J2EE 1.3, J2SE 1.2 | Filter | +| Servlet 2.2 | 1999 年 8 月 | J2EE 1.2, J2SE 1.2 | 成为 J2EE 标准 | +| Servlet 2.1 | 1998 年 11 月 | 未指定 | First official specification, added RequestDispatcher, ServletContext | +| Servlet 2.0 | | JDK 1.1 | Part of Java Servlet Development Kit 2.0 | +| Servlet 1.0 | 1997 年 6 月 | | | + +### Servlet 和 JSP 的区别 + +1. Servlet 是一个运行在服务器上的 Java 类,依靠服务器支持向浏览器传输数据。 +2. **JSP 本质上就是 Servlet**,每次运行的时候 JSP 都会被编译成 .java 文件,然后再被编译成 .class 文件。 +3. 有了 JSP,Servlet 不再负责动态生成页面,转而去负责控制程序逻辑的作用,控制 JSP 与 JavaBean 之间的流转。 +4. JSP 侧重于视图,而 Servlet 侧重于控制逻辑,在 MVC 架构模式中,JSP 适合充当视图 View,Servlet 适合充当控制器 Controller。 + +### 简述 Servlet 生命周期 + +![img](http://www.runoob.com/wp-content/uploads/2014/07/Servlet-LifeCycle.jpg) + +Servlet 生命周期如下: + +1. **加载** - 第一个到达服务器的 HTTP 请求被委派到 Servlet 容器。容器通过类加载器使用 Servlet 类对应的文件加载 servlet; +2. **初始化** - Servlet 通过调用 **init ()** 方法进行初始化。 +3. **服务** - Servlet 调用 **service()** 方法来处理客户端的请求。 +4. **销毁** - Servlet 通过调用 **destroy()** 方法终止(结束)。 +5. **卸载** - Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。 + +### 如何现实 servlet 的单线程模式 + +```java +<%@ page isThreadSafe="false" %> +``` + +### Servlet 中如何获取用户提交的查询参数或者表单数据 + +- HttpServletRequest 的 getParameter() 方法。 +- HttpServletRequest 的 getParameterValues() 方法。 +- HttpServletRequest 的 getParameterMap() 方法。 + +### request 的主要方法 + +- setAttribute(String name,Object):设置名字为 name 的 request 的参数值 +- getAttribute(String name):返回由 name 指定的属性值 +- getAttributeNames():返回 request 对象所有属性的名字集合,结果是一个枚举的实例 +- getCookies():返回客户端的所有 Cookie 对象,结果是一个 Cookie 数组 +- getCharacterEncoding():返回请求中的字符编码方式 +- getContentLength():返回请求的 Body 的长度 +- getHeader(String name):获得 HTTP 协议定义的文件头信息 +- getHeaders(String name):返回指定名字的 request Header 的所有值,结果是一个枚举的实例 +- getHeaderNames():返回所以 request Header 的名字,结果是一个枚举的实例 +- getInputStream():返回请求的输入流,用于获得请求中的数据 getMethod():获得客户端向服务器端传送数据的方法 +- getParameter(String name):获得客户端传送给服务器端的有 name 指定的参数值 +- getParameterNames():获得客户端传送给服务器端的所有参数的名字,结果是一个枚举的实例 +- getParameterValues(String name):获得有 name 指定的参数的所有值 +- getProtocol():获取客户端向服务器端传送数据所依据的协议名称 +- getQueryString():获得查询字符串 +- getRequestURI():获取发出请求字符串的客户端地址 +- getRemoteAddr():获取客户端的 IP 地址 +- getRemoteHost():获取客户端的名字 +- getSession([Boolean create]):返回和请求相关 +- Session getServerName():获取服务器的名字 +- getServletPath():获取客户端所请求的脚本文件的路径 +- getServerPort():获取服务器的端口号 +- removeAttribute(String name):删除请求中的一个属性 + +## JSP + +### JSP 的内置对象 + +1. **request**:包含**客户端请求的信息**; +2. **response**:包含**服务器传回客户端的响应信息**; +3. **session**:主要用来**区分每个用户信息和会话状态**; +4. **pageContext**:管理**页面属性**; +5. **application**:服务器启动时创建,服务器关闭时停止,**保存所有应用系统中的共有数据**,一个共享的内置对象(即一个容器中的多个用户共享一个 application 对象); +6. **out**:向客户端**输出数据**; +7. **config**:代码片段配置对象,用于**初始化 Servlet 的配置参数**; +8. **page**:指**网页本身**; +9. **exception**:处理 JSP 文件执行时发生的错误和异常,只要在**错误页面**里才能使用。 + +### JSP 的作用域 + +1. **page**:一个页面; +2. **request**:一次请求; +3. **session**:一次会话; +4. **application**:服务器从启动到停止。 + +### JSP 中 7 个动作指令和作用 + +1. **jsp:forward** - 执行页面转向,把请求转发到下一个页面; +2. **jsp:param** - 用于传递参数,必须与其他支持参数的标签一起使用; +3. **jsp:include** - 用于**动态引入一个 JSP 页面**; +4. **jsp:plugin** - 用于**下载 JavaBean 或 Applet 到客户端执行**; +5. **jsp:useBean** - 寻求或者实例化一个 JavaBean; +6. **jsp:setProperty** - 设置 JavaBean 的属性值; +7. **jsp:getProperty** - 获取 JavaBean 的属性值。 + +### JSP 中动态 INCLUDE 和静态 INCLUDE 有什么区别 + +- **静态 INCLUDE**:用 include 伪码实现,**不会检查所含文件的变化**,适用于包含**静态页面<%@ include file="页面名称.html" %>**。**先合并再编译**。 +- **动态 INCLUDE**:用 jsp:include 动作实现 **** 它总是**会检查文件中的变化**,适用于包含**动态页面**,并且可以**带参数**。**先编译再合并**。 + +## 原理 + +### 请求转发(forward)和重定向(redirect)的区别 + +- 效率上 + - 转发(forward) > 重定向(redirect) +- 显示上 + - 重定向(redirect):显示新的 URL + - 转发(forward):地址栏不变 +- 数据上 + - 转发(forward):可以共享 request 里面的数据 + - 重定向(redirect):不能 +- 请求次数 + - 重定向(redirect)是两次 + - 转发(forward)是一次 + +### get 请求和 post 请求的区别 + +![img](https://upload-images.jianshu.io/upload_images/7779232-5be5ae990207f9d2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/814/format/webp) + +- GET: + - 从服务器上获取数据,一般不能使用在写操作接口 + - 由 URL 所限制,GET 方式传输的数据大小有所限制,传送的数据量不超过 2KB + - 请求的数据会附加在 URL 之后,以?分隔 URL 和传输数据,多个参数用&连接 + - 安全性差 +- POST: + - 向服务器提交数据,一般处理写业务 + - POST 方式传送的数据量比较大,一般被默认为没有限制 + - 安全性高 + - 请的求的数据内容放置在 HTML HEADER 中 + +### 用户在浏览器中输入 URL 之后,发什么了什么?写出请求和响应的流程 + +1. 域名解析 +2. TCP 三次握手 +3. 浏览器向服务器发送 http 请求 +4. 浏览器发送请求头信息 +5. 服务器处理请求 +6. 服务器做出应答 +7. 服务器发送应答头信息 +8. 服务器发送数据 +9. TCP 连接关闭 + +### 什么是 Web Service? + +1. WebService 就是一个应用程序,它向外界暴露出一个能够通过 Web 进行调用的 API。 +2. 它是基于 HTTP 协议传输数据,这使得运行在不同机上的不同应用程序,无须借助附加的、专门的第三方 软件或硬件,就可以相互交换数据或集成。 + +### 会话跟踪技术有哪些? + +由于 HTTP 协议本身是无状态的,服务器为了区分不同的用户,就需要对用户会话进行跟踪,简单的说就是为用户进行登记,为用户分配唯一的 ID,下一次用户在请求中包含此 ID,服务器根据此判断到底是哪一个用户。 + +- URL 重写:在 URL 中添加会话信息作为请求的参数,或者将唯一的会话 ID 添加到 URL 结尾,以表示一个会话。设置表单隐藏域:将和会话跟踪相关的字段添加到隐藏域中,这些信息不会在浏览器显示,但是提交表单时会提交给服务器。 +- cookie:cookie 有两种: + - 一种是基于窗口的,浏览器关闭后,cookie 就没有了; + - 另一种是将信息存储在一个临时文件中,并设置其有效路径和最大存活时间。当用户通过浏览器和服务器建立一次会话后,会话 ID 就会随相应信息储存在基于窗口的 cookie 中,那就意味着只要浏览器没有关闭,会话没有超时,下一次请求时这个会话 ID 又会提交给服务器,让服务器识别用户身份。 + - 在使用 cookie 时要注意几点: + - 首先不要在 cookie 中存放敏 感信息; + - 其次 cookie 存储的数据量有限(4k),不能将过多的内容存储 cookie 中; + - 再者浏览器通常只允许一个站点最多存放 20 个 cookie。 + - 当然,和用户会话相关的其他信息(除了会话 ID)也可以存在 cookie 方便进行会话 跟踪; +- HttpSession:在所有会话跟踪技术中,HttpSession 对象是最强大也是功能最多的。当一个用户第一次访问某个网站时会自动创建 HttpSession,每个用户可以访问他自己的 HttpSession。可以通过 HttpServletRequest 对象的 getSession 方法获得 HttpSession,通过 HttpSession 的 setAttribute 方法可以将一个值放在 HttpSession 中,通过调用 HttpSession 对象的 getAttribute 方法,同时传入属性名就可以获取保存在 HttpSession 中的对象。 + - 与上面三种方式不同的是,HttpSession 放在服务器的内存中,因此不要将过大的对象放在里面,即使目前的 Servlet 容器可以在内存将满时将 HttpSession 中的对象移到其他存储设备中,但是这样势必影响性能。 + - 添加到 HttpSession 中 的值可以是任意 Java 对象,这个对象最好实现了 Serializable 接口,这样 Servlet 容器在必要的时候可以将其序列 化到文件中,否则在序列化时就会出现异常。 + +### 响应结果状态码有哪些,并给出中文含义? + +- `1**`:信息性状态码 +- `2**`:成功状态码 + - 200:请求正常成功 + - 204:指示请求成功但没有返回新信息 + - 206:指示服务器已完成对资源的部分 GET 请求 +- `3**`:重定向状态码 + - 301:永久性重定向 + - 302:临时性重定向 + - 304:服务器端允许请求访问资源,但未满足条件 +- `4**`:客户端错误状态码 + - 400:请求报文中存在语法错误 + - 401:发送的请求需要有通过 HTTP 认证的认证信息 + - 403:对请求资源的访问被服务器拒绝了 + - 404:服务器上无法找到请求的资源 +- `5**`:服务器错误状态码 + - 500:服务器端在执行请求时发生了错误 + - 503:服务器暂时处于超负载或正在进行停机维护,现在无法处理请求 + +### XML 文档定义有几种形式?它们之间有何本质区别?解析 XML 文档有哪几种方式? + +(1)XML 文档有两种约束方式: + +1. DTD 约束 +2. Schema 约束 + +(2)XML 文档区别: +1 DTD 不符合 XML 的语法结构,schema 符合 XML 的语法结构; +2 DTD 的约束扩展性比较差,XML 文档只能引入一个 DTD 的文件。schema 可以引入多个文件; +3 DTD 不支持名称空间(理解包结构),schema 支持名称空间; +4 DTD 支持数据比较少,schema 支持更多的数据类型; + +(3)解析方式主要有三种: + +- DOM 解析: + - (a)加载整个 xml 的文档到内存中,形成树状结构,生成对象; + - (b)容易产生内存溢出; + - (c)可以做增删改 +- SAX 解析 + - (a)边读边解析; + - (b)不可以做增删改 +- DOM4J 解析(hibernate 底层采用) + - (a)可让 SAX 解析也产生树状结构。 + - (b)主要 api 开发步骤: + - 1)SAXReader.read(xxx.xml)代表解析 xml 的文档,返回对象是 Document; + - 2)Document.getRootElement(),返回的是文档的根节点,是 Element 对象; + - 3)Element: + - .element(...)-- 获得指定名称第一个子元素。可以不指定名称; + - .elements(...)-- 获得指定名称的所有子元素。可以不指定名称; + - .getText()-- 获得当前元素的文本内容; + - .elementText(...)-- 获得指定名称子元素的文本值 + - .addElement()-- 添加子节点 + - .setText()-- 设置子标签内容 + - 4)XMLWriter.write("..")-- 写出 + - 5)XMLWriter.close()-- 关闭输出流 + +## 参考资料 + +- https://blog.csdn.net/YM_IlY/article/details/81266959 +- https://www.jianshu.com/p/f073dde56262 \ No newline at end of file diff --git a/docs/01.Java/02.JavaEE/01.JavaWeb/README.md b/docs/01.Java/02.JavaEE/01.JavaWeb/README.md new file mode 100644 index 00000000..56a9b42a --- /dev/null +++ b/docs/01.Java/02.JavaEE/01.JavaWeb/README.md @@ -0,0 +1,34 @@ +--- +title: JavaWeb +date: 2020-02-07 23:04:47 +categories: + - Java + - JavaEE + - JavaWeb +tags: + - JavaWeb +permalink: /pages/50f49f/ +hidden: true +index: false +--- + +# ☕ JavaWeb + +## 知识点 + +- [JavaWeb 之 Servlet 指南](01.JavaWeb之Servlet指南.md) +- [JavaWeb 之 Jsp 指南](01.JavaWeb之Servlet指南.md) +- [JavaWeb 之 Filter 和 Listener](03.JavaWeb之Filter和Listener.md) +- [JavaWeb 之 Cookie 和 Session](04.JavaWeb之Cookie和Session.md) +- [JavaWeb 面经](99.JavaWeb面经.md) + +## 学习资料 + +- **书籍** + - [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) + - [Head First Servlets & JSP](https://book.douban.com/subject/1942934/) +- **教程** + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) + - [Servlet 教程](https://www.runoob.com/servlet/servlet-tutorial.html) + - [博客园孤傲苍狼 JavaWeb 学习总结](https://www.cnblogs.com/xdp-gacl/tag/JavaWeb%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/) + - [JSP 教程](https://www.runoob.com/jsp/jsp-tutorial.html) \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" new file mode 100644 index 00000000..05822b5e --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -0,0 +1,886 @@ +--- +title: Tomcat 快速入门 +date: 2022-02-17 22:34:30 +order: 01 +categories: + - Java + - JavaEE + - 服务器 + - Tomcat +tags: + - Java + - JavaWeb + - 服务器 + - Tomcat +permalink: /pages/4a4c02/ +--- + +# Tomcat 快速入门 + +> 🎁 版本说明 +> +> 当前最新版本:Tomcat 8.5.24 +> +> 环境要求:JDK7+ + +## 1. Tomcat 简介 + +### 1.1. Tomcat 是什么 + +Tomcat 是由 Apache 开发的一个 Servlet 容器,实现了对 Servlet 和 JSP 的支持,并提供了作为 Web 服务器的一些特有功能,如 Tomcat 管理和控制平台、安全域管理和 Tomcat 阀等。 + +由于 Tomcat 本身也内含了一个 HTTP 服务器,它也可以被视作一个单独的 Web 服务器。但是,不能将 Tomcat 和 Apache HTTP 服务器混淆,Apache HTTP 服务器是一个用 C 语言实现的 HTTP Web 服务器;这两个 HTTP web server 不是捆绑在一起的。Tomcat 包含了一个配置管理工具,也可以通过编辑 XML 格式的配置文件来进行配置。 + +### 1.2. Tomcat 重要目录 + +- **/bin** - Tomcat 脚本存放目录(如启动、关闭脚本)。 `*.sh` 文件用于 Unix 系统; `*.bat` 文件用于 Windows 系统。 +- **/conf** - Tomcat 配置文件目录。 +- **/logs** - Tomcat 默认日志目录。 +- **/webapps** - webapp 运行的目录。 + +### 1.3. web 工程发布目录结构 + +一般 web 项目路径结构 + +``` +|-- webapp # 站点根目录 + |-- META-INF # META-INF 目录 + | `-- MANIFEST.MF # 配置清单文件 + |-- WEB-INF # WEB-INF 目录 + | |-- classes # class文件目录 + | | |-- *.class # 程序需要的 class 文件 + | | `-- *.xml # 程序需要的 xml 文件 + | |-- lib # 库文件夹 + | | `-- *.jar # 程序需要的 jar 包 + | `-- web.xml # Web应用程序的部署描述文件 + |-- # 自定义的目录 + |-- # 自定义的资源文件 +``` + +- `webapp`:工程发布文件夹。其实每个 war 包都可以视为 webapp 的压缩包。 + +- `META-INF`:META-INF 目录用于存放工程自身相关的一些信息,元文件信息,通常由开发工具,环境自动生成。 + +- `WEB-INF`:Java web 应用的安全目录。所谓安全就是客户端无法访问,只有服务端可以访问的目录。 +- `/WEB-INF/classes`:存放程序所需要的所有 Java class 文件。 + +- `/WEB-INF/lib`:存放程序所需要的所有 jar 文件。 + +- `/WEB-INF/web.xml`:web 应用的部署配置文件。它是工程中最重要的配置文件,它描述了 servlet 和组成应用的其它组件,以及应用初始化参数、安全管理约束等。 + +### 1.4. Tomcat 功能 + +Tomcat 支持的 I/O 模型有: + +- NIO:非阻塞 I/O,采用 Java NIO 类库实现。 +- NIO2:异步 I/O,采用 JDK 7 最新的 NIO2 类库实现。 +- APR:采用 Apache 可移植运行库实现,是 C/C++ 编写的本地库。 + +Tomcat 支持的应用层协议有: + +- HTTP/1.1:这是大部分 Web 应用采用的访问协议。 +- AJP:用于和 Web 服务器集成(如 Apache)。 +- HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。 + +## 2. Tomcat 入门 + +### 2.1. 安装 + +**前提条件** + +Tomcat 8.5 要求 JDK 版本为 1.7 以上。 + +进入 [Tomcat 官方下载地址](https://tomcat.apache.org/download-80.cgi) 选择合适版本下载,并解压到本地。 + +**Windows** + +添加环境变量 `CATALINA_HOME` ,值为 Tomcat 的安装路径。 + +进入安装目录下的 bin 目录,运行 startup.bat 文件,启动 Tomcat + +**Linux / Unix** + +下面的示例以 8.5.24 版本为例,包含了下载、解压、启动操作。 + +```bash +# 下载解压到本地 +wget http://mirrors.hust.edu.cn/apache/tomcat/tomcat-8/v8.5.24/bin/apache-tomcat-8.5.24.tar.gz +tar -zxf apache-tomcat-8.5.24.tar.gz +# 启动 Tomcat +./apache-tomcat-8.5.24/bin/startup.sh +``` + +启动后,访问 `http://localhost:8080` ,可以看到 Tomcat 安装成功的测试页面。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/tools/tomcat/tomcat.png) + +### 2.2. 配置 + +本节将列举一些重要、常见的配置项。详细的 Tomcat8 配置可以参考 [Tomcat 8 配置官方参考文档](http://tomcat.apache.org/tomcat-8.5-doc/config/index.html) 。 + +#### 2.2.1. Server + +> Server 元素表示整个 Catalina servlet 容器。 +> +> 因此,它必须是 `conf/server.xml` 配置文件中的根元素。它的属性代表了整个 servlet 容器的特性。 + +**属性表** + +| 属性 | 描述 | 备注 | +| --------- | ------------------------------------------------------------------------ | -------------------------------------------- | +| className | 这个类必须实现 org.apache.catalina.Server 接口。 | 默认 org.apache.catalina.core.StandardServer | +| address | 服务器等待关机命令的 TCP / IP 地址。如果没有指定地址,则使用 localhost。 | | +| port | 服务器等待关机命令的 TCP / IP 端口号。设置为-1 以禁用关闭端口。 | | +| shutdown | 必须通过 TCP / IP 连接接收到指定端口号的命令字符串,以关闭 Tomcat。 | | + +#### 2.2.2. Service + +> Service 元素表示一个或多个连接器组件的组合,这些组件共享一个用于处理传入请求的引擎组件。Server 中可以有多个 Service。 + +**属性表** + +| 属性 | 描述 | 备注 | +| --------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------- | +| className | 这个类必须实现`org.apache.catalina.Service`接口。 | 默认 `org.apache.catalina.core.StandardService` | +| name | 此服务的显示名称,如果您使用标准 Catalina 组件,将包含在日志消息中。与特定服务器关联的每个服务的名称必须是唯一的。 | | + +**实例 - `conf/server.xml` 配置文件示例** + +```xml + + + + ... + + +``` + +#### 2.2.3. Executor + +> Executor 表示可以在 Tomcat 中的组件之间共享的线程池。 + +**属性表** + +| 属性 | 描述 | 备注 | +| --------------- | ---------------------------------------------------------------- | ------------------------------------------------------ | +| className | 这个类必须实现`org.apache.catalina.Executor`接口。 | 默认 `org.apache.catalina.core.StandardThreadExecutor` | +| name | 线程池名称。 | 要求唯一, 供 Connector 元素的 executor 属性使用 | +| namePrefix | 线程名称前缀。 | | +| maxThreads | 最大活跃线程数。 | 默认 200 | +| minSpareThreads | 最小活跃线程数。 | 默认 25 | +| maxIdleTime | 当前活跃线程大于 minSpareThreads 时,空闲线程关闭的等待最大时间。 | 默认 60000ms | +| maxQueueSize | 线程池满情况下的请求排队大小。 | 默认 Integer.MAX_VALUE | + +```xml + + + +``` + +#### 2.2.4. Connector + +> Connector 代表连接组件。Tomcat 支持三种协议:HTTP/1.1、HTTP/2.0、AJP。 + +**属性表** + +| 属性 | 说明 | 备注 | +| --------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| asyncTimeout | Servlet3.0 规范中的异步请求超时 | 默认 30s | +| port | 请求连接的 TCP Port | 设置为 0,则会随机选取一个未占用的端口号 | +| protocol | 协议. 一般情况下设置为 HTTP/1.1,这种情况下连接模型会在 NIO 和 APR/native 中自动根据配置选择 | | +| URIEncoding | 对 URI 的编码方式. | 如果设置系统变量 org.apache.catalina.STRICT_SERVLET_COMPLIANCE 为 true,使用 ISO-8859-1 编码;如果未设置此系统变量且未设置此属性, 使用 UTF-8 编码 | +| useBodyEncodingForURI | 是否采用指定的 contentType 而不是 URIEncoding 来编码 URI 中的请求参数 | | + +以下属性在标准的 Connector(NIO, NIO2 和 APR/native)中有效: + +| 属性 | 说明 | 备注 | +| ----------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| acceptCount | 当最大请求连接 maxConnections 满时的最大排队大小 | 默认 100,注意此属性和 Executor 中属性 maxQueueSize 的区别.这个指的是请求连接满时的堆栈大小,Executor 的 maxQueueSize 指的是处理线程满时的堆栈大小 | +| connectionTimeout | 请求连接超时 | 默认 60000ms | +| executor | 指定配置的线程池名称 | | +| keepAliveTimeout | keeAlive 超时时间 | 默认值为 connectionTimeout 配置值.-1 表示不超时 | +| maxConnections | 最大连接数 | 连接满时后续连接放入最大为 acceptCount 的队列中. 对 NIO 和 NIO2 连接,默认值为 10000;对 APR/native,默认值为 8192 | +| maxThreads | 如果指定了 Executor, 此属性忽略;否则为 Connector 创建的内部线程池最大值 | 默认 200 | +| minSpareThreads | 如果指定了 Executor, 此属性忽略;否则为 Connector 创建线程池的最小活跃线程数 | 默认 10 | +| processorCache | 协议处理器缓存 Processor 对象的大小 | -1 表示不限制.当不使用 servlet3.0 的异步处理情况下: 如果配置 Executor,配置为 Executor 的 maxThreads;否则配置为 Connnector 的 maxThreads. 如果使用 Serlvet3.0 异步处理, 取 maxThreads 和 maxConnections 的最大值 | + +#### 2.2.5. Context + +> Context 元素表示一个 Web 应用程序,它在特定的虚拟主机中运行。每个 Web 应用程序都基于 Web 应用程序存档(WAR)文件,或者包含相应的解包内容的相应目录,如 Servlet 规范中所述。 + +**属性表** + +| 属性 | 说明 | 备注 | +| -------------------------- | --------------------------------------------------------------------------- | ---------------------------------------------------- | +| altDDName | web.xml 部署描述符路径 | 默认 /WEB-INF/web.xml | +| docBase | Context 的 Root 路径 | 和 Host 的 appBase 相结合, 可确定 web 应用的实际目录 | +| failCtxIfServletStartFails | 同 Host 中的 failCtxIfServletStartFails, 只对当前 Context 有效 | 默认为 false | +| logEffectiveWebXml | 是否日志打印 web.xml 内容(web.xml 由默认的 web.xml 和应用中的 web.xml 组成) | 默认为 false | +| path | web 应用的 context path | 如果为根路径,则配置为空字符串(""), 不能不配置 | +| privileged | 是否使用 Tomcat 提供的 manager servlet | | +| reloadable | /WEB-INF/classes/ 和/WEB-INF/lib/ 目录中 class 文件发生变化是否自动重新加载 | 默认为 false | +| swallowOutput | true 情况下, System.out 和 System.err 输出将被定向到 web 应用日志中 | 默认为 false | + +#### 2.2.6. Engine + +> Engine 元素表示与特定的 Catalina 服务相关联的整个请求处理机器。它接收并处理来自一个或多个连接器的所有请求,并将完成的响应返回给连接器,以便最终传输回客户端。 + +**属性表** + +| 属性 | 描述 | 备注 | +| ----------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | +| defaultHost | 默认主机名,用于标识将处理指向此服务器上主机名称但未在此配置文件中配置的请求的主机。 | 这个名字必须匹配其中一个嵌套的主机元素的名字属性。 | +| name | 此引擎的逻辑名称,用于日志和错误消息。 | 在同一服务器中使用多个服务元素时,每个引擎必须分配一个唯一的名称。 | + +#### 2.2.7. Host + +> Host 元素表示一个虚拟主机,它是一个服务器的网络名称(如“www.mycompany.com”)与运行 Tomcat 的特定服务器的关联。 + +**属性表** + +| 属性 | 说明 | 备注 | +| -------------------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------- | +| name | 名称 | 用于日志输出 | +| appBase | 虚拟主机对应的应用基础路径 | 可以是个绝对路径, 或\${CATALINA_BASE}相对路径 | +| xmlBase | 虚拟主机 XML 基础路径,里面应该有 Context xml 配置文件 | 可以是个绝对路径, 或\${CATALINA_BASE}相对路径 | +| createDirs | 当 appBase 和 xmlBase 不存在时,是否创建目录 | 默认为 true | +| autoDeploy | 是否周期性的检查 appBase 和 xmlBase 并 deploy web 应用和 context 描述符 | 默认为 true | +| deployIgnore | 忽略 deploy 的正则 | | +| deployOnStartup | Tomcat 启动时是否自动 deploy | 默认为 true | +| failCtxIfServletStartFails | 配置为 true 情况下,任何 load-on-startup >=0 的 servlet 启动失败,则其对应的 Contxt 也启动失败 | 默认为 false | + +#### 2.2.8. Cluster + +由于在实际开发中,我从未用过 Tomcat 集群配置,所以没研究。 + +### 2.3. 启动 + +#### 2.3.1. 部署方式 + +这种方式要求本地必须安装 Tomcat 。 + +将打包好的 war 包放在 Tomcat 安装目录下的 `webapps` 目录下,然后在 bin 目录下执行 `startup.bat` 或 `startup.sh` ,Tomcat 会自动解压 `webapps` 目录下的 war 包。 + +成功后,可以访问 `http://localhost:8080/xxx` (xxx 是 war 包文件名)。 + +> **注意** +> +> 以上步骤是最简单的示例。步骤中的 war 包解压路径、启动端口以及一些更多的功能都可以修改配置文件来定制 (主要是 `server.xml` 或 `context.xml` 文件)。 + +#### 2.3.2. 嵌入式 + +##### 2.3.2.1. API 方式 + +在 pom.xml 中添加依赖 + +```xml + + org.apache.tomcat.embed + tomcat-embed-core + 8.5.24 + +``` + +添加 SimpleEmbedTomcatServer.java 文件,内容如下: + +```java +import java.util.Optional; +import org.apache.catalina.startup.Tomcat; + +public class SimpleTomcatServer { + private static final int PORT = 8080; + private static final String CONTEXT_PATH = "/javatool-server"; + + public static void main(String[] args) throws Exception { + // 设定 profile + Optional profile = Optional.ofNullable(System.getProperty("spring.profiles.active")); + System.setProperty("spring.profiles.active", profile.orElse("develop")); + + Tomcat tomcat = new Tomcat(); + tomcat.setPort(PORT); + tomcat.getHost().setAppBase("."); + tomcat.addWebapp(CONTEXT_PATH, getAbsolutePath() + "src/main/webapp"); + tomcat.start(); + tomcat.getServer().await(); + } + + private static String getAbsolutePath() { + String path = null; + String folderPath = SimpleEmbedTomcatServer.class.getProtectionDomain().getCodeSource().getLocation().getPath() + .substring(1); + if (folderPath.indexOf("target") > 0) { + path = folderPath.substring(0, folderPath.indexOf("target")); + } + return path; + } +} +``` + +成功后,可以访问 `http://localhost:8080/javatool-server` 。 + +> **说明** +> +> 本示例是使用 `org.apache.tomcat.embed` 启动嵌入式 Tomcat 的最简示例。 +> +> 这个示例中使用的是 Tomcat 默认的配置,但通常,我们需要对 Tomcat 配置进行一些定制和调优。为了加载配置文件,启动类就要稍微再复杂一些。这里不想再贴代码,有兴趣的同学可以参考: +> +> [**示例项目**](https://github.com/dunwu/JavaStack/tree/master/codes/javatool/server) + +##### 2.3.2.2. 使用 maven 插件启动(不推荐) + +不推荐理由:这种方式启动 maven 虽然最简单,但是有一个很大的问题是,真的很久很久没发布新版本了(最新版本发布时间:2013-11-11)。且貌似只能找到 Tomcat6 、Tomcat7 插件。 + +**使用方法** + +在 pom.xml 中引入插件 + +```xml + + org.apache.tomcat.maven + tomcat7-maven-plugin + 2.2 + + 8080 + /${project.artifactId} + UTF-8 + + +``` + +运行 `mvn tomcat7:run` 命令,启动 Tomcat。 + +成功后,可以访问 `http://localhost:8080/xxx` (xxx 是 \${project.artifactId} 指定的项目名)。 + +#### 2.3.3. IDE 插件 + +常见 Java IDE 一般都有对 Tomcat 的支持。 + +以 Intellij IDEA 为例,提供了 **Tomcat and TomEE Integration** 插件(一般默认会安装)。 + +**使用步骤** + +- 点击 Run/Debug Configurations > New Tomcat Server > local ,打开 Tomcat 配置页面。 +- 点击 Confiure... 按钮,设置 Tomcat 安装路径。 +- 点击 Deployment 标签页,设置要启动的应用。 +- 设置启动应用的端口、JVM 参数、启动浏览器等。 +- 成功后,可以访问 `http://localhost:8080/`(当然,你也可以在 url 中设置上下文名称)。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/tools/tomcat/tomcat-intellij-run-config.png) + +> **说明** +> +> 个人认为这个插件不如 Eclipse 的 Tomcat 插件好用,Eclipse 的 Tomcat 插件支持对 Tomcat xml 配置文件进行配置。而这里,你只能自己去 Tomcat 安装路径下修改配置文件。 + +文中的嵌入式启动示例可以参考[**我的示例项目**](https://github.com/dunwu/JavaStack/tree/master/codes/javatool/server) + +## 3. Tomcat 架构 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201113193431.png) + +Tomcat 要实现 2 个核心功能: + +- **处理 Socket 连接**,负责网络字节流与 Request 和 Response 对象的转化。 +- **加载和管理 Servlet**,以及**处理具体的 Request 请求**。 + +为此,Tomcat 设计了两个核心组件: + +- **连接器(Connector)**:负责和外部通信 +- **容器(Container)**:负责内部处理 + +### 3.1. Service + +Tomcat 支持的 I/O 模型有: + +- NIO:非阻塞 I/O,采用 Java NIO 类库实现。 +- NIO2:异步 I/O,采用 JDK 7 最新的 NIO2 类库实现。 +- APR:采用 Apache 可移植运行库实现,是 C/C++ 编写的本地库。 + +Tomcat 支持的应用层协议有: + +- HTTP/1.1:这是大部分 Web 应用采用的访问协议。 +- AJP:用于和 Web 服务器集成(如 Apache)。 +- HTTP/2:HTTP 2.0 大幅度的提升了 Web 性能。 + +Tomcat 支持多种 I/O 模型和应用层协议。为了实现这点,一个容器可能对接多个连接器。但是,单独的连接器或容器都不能对外提供服务,需要把它们组装起来才能工作,组装后这个整体叫作 Service 组件。Tomcat 内可能有多个 Service,通过在 Tomcat 中配置多个 Service,可以实现通过不同的端口号来访问同一台机器上部署的不同应用。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201111093124.png) + +**一个 Tomcat 实例有一个或多个 Service;一个 Service 有多个 Connector 和 Container**。Connector 和 Container 之间通过标准的 ServletRequest 和 ServletResponse 通信。 + +### 3.2. 连接器 + +连接器对 Servlet 容器屏蔽了协议及 I/O 模型等的区别,无论是 HTTP 还是 AJP,在容器中获取到的都是一个标准的 ServletRequest 对象。 + +连接器的主要功能是: + +- 网络通信 +- 应用层协议解析 +- Tomcat Request/Response 与 ServletRequest/ServletResponse 的转化 + +Tomcat 设计了 3 个组件来实现这 3 个功能,分别是 **`EndPoint`**、**`Processor`** 和 **`Adapter`**。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201111101440.png) + +组件间通过抽象接口交互。这样做还有一个好处是**封装变化。**这是面向对象设计的精髓,将系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。网络通信的 I/O 模型是变化的,可能是非阻塞 I/O、异步 I/O 或者 APR。应用层协议也是变化的,可能是 HTTP、HTTPS、AJP。浏览器端发送的请求信息也是变化的。但是整体的处理逻辑是不变的,EndPoint 负责提供字节流给 Processor,Processor 负责提供 Tomcat Request 对象给 Adapter,Adapter 负责提供 ServletRequest 对象给容器。 + +如果要支持新的 I/O 方案、新的应用层协议,只需要实现相关的具体子类,上层通用的处理逻辑是不变的。由于 I/O 模型和应用层协议可以自由组合,比如 NIO + HTTP 或者 NIO2 + AJP。Tomcat 的设计者将网络通信和应用层协议解析放在一起考虑,设计了一个叫 ProtocolHandler 的接口来封装这两种变化点。各种协议和通信模型的组合有相应的具体实现类。比如:Http11NioProtocol 和 AjpNioProtocol。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201027091819.png) + +#### 3.2.1. ProtocolHandler 组件 + +**连接器用 ProtocolHandler 接口来封装通信协议和 I/O 模型的差异**。ProtocolHandler 内部又分为 EndPoint 和 Processor 模块,EndPoint 负责底层 Socket 通信,Proccesor 负责应用层协议解析。 + +##### 3.2.1.1. EndPoint + +EndPoint 是通信端点,即通信监听的接口,是具体的 Socket 接收和发送处理器,是对传输层的抽象,因此 EndPoint 是用来实现 TCP/IP 协议的。 + +EndPoint 是一个接口,对应的抽象实现类是 AbstractEndpoint,而 AbstractEndpoint 的具体子类,比如在 NioEndpoint 和 Nio2Endpoint 中,有两个重要的子组件:Acceptor 和 SocketProcessor。 + +其中 Acceptor 用于监听 Socket 连接请求。SocketProcessor 用于处理接收到的 Socket 请求,它实现 Runnable 接口,在 Run 方法里调用协议处理组件 Processor 进行处理。为了提高处理能力,SocketProcessor 被提交到线程池来执行。而这个线程池叫作执行器(Executor)。 + +##### 3.2.1.2. Processor + +如果说 EndPoint 是用来实现 TCP/IP 协议的,那么 Processor 用来实现 HTTP 协议,Processor 接收来自 EndPoint 的 Socket,读取字节流解析成 Tomcat Request 和 Response 对象,并通过 Adapter 将其提交到容器处理,Processor 是对应用层协议的抽象。 + +Processor 是一个接口,定义了请求的处理等方法。它的抽象实现类 AbstractProcessor 对一些协议共有的属性进行封装,没有对方法进行实现。具体的实现有 AJPProcessor、HTTP11Processor 等,这些具体实现类实现了特定协议的解析方法和请求处理方式。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201113185929.png) + +从图中我们看到,EndPoint 接收到 Socket 连接后,生成一个 SocketProcessor 任务提交到线程池去处理,SocketProcessor 的 Run 方法会调用 Processor 组件去解析应用层协议,Processor 通过解析生成 Request 对象后,会调用 Adapter 的 Service 方法。 + +#### 3.2.2. Adapter + +**连接器通过适配器 Adapter 调用容器**。 + +由于协议不同,客户端发过来的请求信息也不尽相同,Tomcat 定义了自己的 Request 类来适配这些请求信息。 + +ProtocolHandler 接口负责解析请求并生成 Tomcat Request 类。但是这个 Request 对象不是标准的 ServletRequest,也就意味着,不能用 Tomcat Request 作为参数来调用容器。Tomcat 的解决方案是引入 CoyoteAdapter,这是适配器模式的经典运用,连接器调用 CoyoteAdapter 的 Sevice 方法,传入的是 Tomcat Request 对象,CoyoteAdapter 负责将 Tomcat Request 转成 ServletRequest,再调用容器的 Service 方法。 + +### 3.3. 容器 + +Tomcat 设计了 4 种容器,分别是 Engine、Host、Context 和 Wrapper。 + +- **Engine** - Servlet 的顶层容器,包含一 个或多个 Host 子容器; +- **Host** - 虚拟主机,负责 web 应用的部署和 Context 的创建; +- **Context** - Web 应用上下文,包含多个 Wrapper,负责 web 配置的解析、管理所有的 Web 资源; +- **Wrapper** - 最底层的容器,是对 Servlet 的封装,负责 Servlet 实例的创 建、执行和销毁。 + +#### 3.3.1. 请求分发 Servlet 过程 + +Tomcat 是怎么确定请求是由哪个 Wrapper 容器里的 Servlet 来处理的呢?答案是,Tomcat 是用 Mapper 组件来完成这个任务的。 + +举例来说,假如有一个网购系统,有面向网站管理人员的后台管理系统,还有面向终端客户的在线购物系统。这两个系统跑在同一个 Tomcat 上,为了隔离它们的访问域名,配置了两个虚拟域名:`manage.shopping.com`和`user.shopping.com`,网站管理人员通过`manage.shopping.com`域名访问 Tomcat 去管理用户和商品,而用户管理和商品管理是两个单独的 Web 应用。终端客户通过`user.shopping.com`域名去搜索商品和下订单,搜索功能和订单管理也是两个独立的 Web 应用。如下所示,演示了 url 应声 Servlet 的处理流程。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201113192022.jpg) + +假如有用户访问一个 URL,比如图中的`http://user.shopping.com:8080/order/buy`,Tomcat 如何将这个 URL 定位到一个 Servlet 呢? + +1. **首先,根据协议和端口号选定 Service 和 Engine。** +2. **然后,根据域名选定 Host。** +3. **之后,根据 URL 路径找到 Context 组件。** +4. **最后,根据 URL 路径找到 Wrapper(Servlet)。** + +这个路由分发过程具体是怎么实现的呢?答案是使用 Pipeline-Valve 管道。 + +#### 3.3.2. Pipeline-Value + +Pipeline 可以理解为现实中的管道,Valve 为管道中的阀门,Request 和 Response 对象在管道中经过各个阀门的处理和控制。 + +Pipeline-Valve 是责任链模式,责任链模式是指在一个请求处理的过程中有很多处理者依次对请求进行处理,每个处理者负责做自己相应的处理,处理完之后将再调用下一个处理者继续处理。Valve 表示一个处理点,比如权限认证和记录日志。 + +先来了解一下 Valve 和 Pipeline 接口的设计: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/tools/tomcat/Pipeline与Valve.png) + +- 每一个容器都有一个 Pipeline 对象,只要触发这个 Pipeline 的第一个 Valve,这个容器里 Pipeline 中的 Valve 就都会被调用到。但是,不同容器的 Pipeline 是怎么链式触发的呢,比如 Engine 中 Pipeline 需要调用下层容器 Host 中的 Pipeline。 +- 这是因为 Pipeline 中还有个 getBasic 方法。这个 BasicValve 处于 Valve 链表的末端,它是 Pipeline 中必不可少的一个 Valve,负责调用下层容器的 Pipeline 里的第一个 Valve。 +- Pipeline 中有 addValve 方法。Pipeline 中维护了 Valve 链表,Valve 可以插入到 Pipeline 中,对请求做某些处理。我们还发现 Pipeline 中没有 invoke 方法,因为整个调用链的触发是 Valve 来完成的,Valve 完成自己的处理后,调用 `getNext.invoke()` 来触发下一个 Valve 调用。 +- Valve 中主要的三个方法:`setNext`、`getNext`、`invoke`。Valve 之间的关系是单向链式结构,本身 `invoke` 方法中会调用下一个 Valve 的 `invoke` 方法。 +- 各层容器对应的 basic valve 分别是 `StandardEngineValve`、`StandardHostValve`、 `StandardContextValve`、`StandardWrapperValve`。 +- 由于 Valve 是一个处理点,因此 invoke 方法就是来处理请求的。注意到 Valve 中有 getNext 和 setNext 方法,因此我们大概可以猜到有一个链表将 Valve 链起来了。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/tools/tomcat/请求处理过程.png) + +整个调用过程由连接器中的 Adapter 触发的,它会调用 Engine 的第一个 Valve: + +```java +connector.getService().getContainer().getPipeline().getFirst().invoke(request, response); +``` + +## 4. Tomcat 生命周期 + +### 4.1. Tomcat 的启动过程 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201118145455.png) + +1. Tomcat 是一个 Java 程序,它的运行从执行 `startup.sh` 脚本开始。`startup.sh` 会启动一个 JVM 来运行 Tomcat 的启动类 `Bootstrap`。 +2. `Bootstrap` 会初始化 Tomcat 的类加载器并实例化 `Catalina`。 +3. `Catalina` 会通过 Digester 解析 `server.xml`,根据其中的配置信息来创建相应组件,并调用 `Server` 的 `start` 方法。 +4. `Server` 负责管理 `Service` 组件,它会调用 `Service` 的 `start` 方法。 +5. `Service` 负责管理 `Connector` 和顶层容器 `Engine`,它会调用 `Connector` 和 `Engine` 的 `start` 方法。 + +#### 4.1.1. Catalina 组件 + +Catalina 的职责就是解析 server.xml 配置,并据此实例化 Server。接下来,调用 Server 组件的 init 方法和 start 方法,将 Tomcat 启动起来。 + +Catalina 还需要处理各种“异常”情况,比如当我们通过“Ctrl + C”关闭 Tomcat 时,Tomcat 将如何优雅的停止并且清理资源呢?因此 Catalina 在 JVM 中注册一个“关闭钩子”。 + +```java +public void start() { + //1. 如果持有的 Server 实例为空,就解析 server.xml 创建出来 + if (getServer() == null) { + load(); + } + + //2. 如果创建失败,报错退出 + if (getServer() == null) { + log.fatal(sm.getString("catalina.noServer")); + return; + } + + //3. 启动 Server + try { + getServer().start(); + } catch (LifecycleException e) { + return; + } + + // 创建并注册关闭钩子 + if (useShutdownHook) { + if (shutdownHook == null) { + shutdownHook = new CatalinaShutdownHook(); + } + Runtime.getRuntime().addShutdownHook(shutdownHook); + } + + // 用 await 方法监听停止请求 + if (await) { + await(); + stop(); + } +} +``` + +为什么需要关闭钩子? + +如果我们需要在 JVM 关闭时做一些清理工作,比如将缓存数据刷到磁盘上,或者清理一些临时文件,可以向 JVM 注册一个“关闭钩子”。“关闭钩子”其实就是一个线程,JVM 在停止之前会尝试执行这个线程的 `run` 方法。 + +Tomcat 的“关闭钩子”—— `CatalinaShutdownHook` 做了些什么呢? + +```java +protected class CatalinaShutdownHook extends Thread { + + @Override + public void run() { + try { + if (getServer() != null) { + Catalina.this.stop(); + } + } catch (Throwable ex) { + ... + } + } +} +``` + +Tomcat 的“关闭钩子”实际上就执行了 `Server` 的 `stop` 方法,`Server` 的 `stop` 方法会释放和清理所有的资源。 + +#### 4.1.2. Server 组件 + +Server 组件的具体实现类是 StandardServer,Server 继承了 LifeCycleBase,它的生命周期被统一管理,并且它的子组件是 Service,因此它还需要管理 Service 的生命周期,也就是说在启动时调用 Service 组件的启动方法,在停止时调用它们的停止方法。Server 在内部维护了若干 Service 组件,它是以数组来保存的。 + +```java +@Override +public void addService(Service service) { + + service.setServer(this); + + synchronized (servicesLock) { + // 创建一个长度 +1 的新数组 + Service results[] = new Service[services.length + 1]; + + // 将老的数据复制过去 + System.arraycopy(services, 0, results, 0, services.length); + results[services.length] = service; + services = results; + + // 启动 Service 组件 + if (getState().isAvailable()) { + try { + service.start(); + } catch (LifecycleException e) { + // Ignore + } + } + + // 触发监听事件 + support.firePropertyChange("service", null, service); + } + +} +``` + +Server 并没有一开始就分配一个很长的数组,而是在添加的过程中动态地扩展数组长度,当添加一个新的 Service 实例时,会创建一个新数组并把原来数组内容复制到新数组,这样做的目的其实是为了节省内存空间。 + +除此之外,Server 组件还有一个重要的任务是启动一个 Socket 来监听停止端口,这就是为什么你能通过 shutdown 命令来关闭 Tomcat。不知道你留意到没有,上面 Caralina 的启动方法的最后一行代码就是调用了 Server 的 await 方法。 + +在 await 方法里会创建一个 Socket 监听 8005 端口,并在一个死循环里接收 Socket 上的连接请求,如果有新的连接到来就建立连接,然后从 Socket 中读取数据;如果读到的数据是停止命令“SHUTDOWN”,就退出循环,进入 stop 流程。 + +#### 4.1.3. Service 组件 + +Service 组件的具体实现类是 StandardService。 + +【源码】StandardService 源码定义 + +```java +public class StandardService extends LifecycleBase implements Service { + // 名字 + private String name = null; + + //Server 实例 + private Server server = null; + + // 连接器数组 + protected Connector connectors[] = new Connector[0]; + private final Object connectorsLock = new Object(); + + // 对应的 Engine 容器 + private Engine engine = null; + + // 映射器及其监听器 + protected final Mapper mapper = new Mapper(); + protected final MapperListener mapperListener = new MapperListener(this); + + // ... +} +``` + +StandardService 继承了 LifecycleBase 抽象类。 + +StandardService 维护了一个 MapperListener 用于支持 Tomcat 热部署。当 Web 应用的部署发生变化时,Mapper 中的映射信息也要跟着变化,MapperListener 就是一个监听器,它监听容器的变化,并把信息更新到 Mapper 中,这是典型的观察者模式。 + +作为“管理”角色的组件,最重要的是维护其他组件的生命周期。此外在启动各种组件时,要注意它们的依赖关系,也就是说,要注意启动的顺序。 + +```java +protected void startInternal() throws LifecycleException { + + //1. 触发启动监听器 + setState(LifecycleState.STARTING); + + //2. 先启动 Engine,Engine 会启动它子容器 + if (engine != null) { + synchronized (engine) { + engine.start(); + } + } + + //3. 再启动 Mapper 监听器 + mapperListener.start(); + + //4. 最后启动连接器,连接器会启动它子组件,比如 Endpoint + synchronized (connectorsLock) { + for (Connector connector: connectors) { + if (connector.getState() != LifecycleState.FAILED) { + connector.start(); + } + } + } +} +``` + +从启动方法可以看到,Service 先启动了 Engine 组件,再启动 Mapper 监听器,最后才是启动连接器。这很好理解,因为内层组件启动好了才能对外提供服务,才能启动外层的连接器组件。而 Mapper 也依赖容器组件,容器组件启动好了才能监听它们的变化,因此 Mapper 和 MapperListener 在容器组件之后启动。组件停止的顺序跟启动顺序正好相反的,也是基于它们的依赖关系。 + +#### 4.1.4. Engine 组件 + +Engine 本质是一个容器,因此它继承了 ContainerBase 基类,并且实现了 Engine 接口。 + +### 4.2. Web 应用的部署方式 + +注:catalina.home:安装目录;catalina.base:工作目录;默认值 user.dir + +- Server.xml 配置 Host 元素,指定 appBase 属性,默认\$catalina.base/webapps/ +- Server.xml 配置 Context 元素,指定 docBase,元素,指定 web 应用的路径 +- 自定义配置:在\$catalina.base/EngineName/HostName/XXX.xml 配置 Context 元素 + +HostConfig 监听了 StandardHost 容器的事件,在 start 方法中解析上述配置文件: + +- 扫描 appbase 路径下的所有文件夹和 war 包,解析各个应用的 META-INF/context.xml,并 创建 StandardContext,并将 Context 加入到 Host 的子容器中。 +- 解析\$catalina.base/EngineName/HostName/下的所有 Context 配置,找到相应 web 应 用的位置,解析各个应用的 META-INF/context.xml,并创建 StandardContext,并将 Context 加入到 Host 的子容器中。 + +注: + +- HostConfig 并没有实际解析 Context.xml,而是在 ContextConfig 中进行的。 +- HostConfig 中会定期检查 watched 资源文件(context.xml 配置文件) + +ContextConfig 解析 context.xml 顺序: + +- 先解析全局的配置 config/context.xml +- 然后解析 Host 的默认配置 EngineName/HostName/context.xml.default +- 最后解析应用的 META-INF/context.xml + +ContextConfig 解析 web.xml 顺序: + +- 先解析全局的配置 config/web.xml +- 然后解析 Host 的默认配置 EngineName/HostName/web.xml.default 接着解析应用的 MEB-INF/web.xml +- 扫描应用 WEB-INF/lib/下的 jar 文件,解析其中的 META-INF/web-fragment.xml 最后合并 xml 封装成 WebXml,并设置 Context + +注: + +- 扫描 web 应用和 jar 中的注解(Filter、Listener、Servlet)就是上述步骤中进行的。 +- 容器的定期执行:backgroundProcess,由 ContainerBase 来实现的,并且只有在顶层容器 中才会开启线程。(backgroundProcessorDelay=10 标志位来控制) + +### 4.3. LifeCycle + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201118105012.png) + +#### 4.3.1. 请求处理过程 + +
+ +
+ +1. 根据 server.xml 配置的指定的 connector 以及端口监听 http、或者 ajp 请求 +2. 请求到来时建立连接,解析请求参数,创建 Request 和 Response 对象,调用顶层容器 pipeline 的 invoke 方法 +3. 容器之间层层调用,最终调用业务 servlet 的 service 方法 +4. Connector 将 response 流中的数据写到 socket 中 + +### 4.4. Connector 流程 + +
+ +
+ +#### 4.4.1. 阻塞 IO + +
+ +
+ +#### 4.4.2. 非阻塞 IO + +
+ +
+ +#### 4.4.3. IO 多路复用 + +
+ +
+ +阻塞与非阻塞的区别在于进行读操作和写操作的系统调用时,如果此时内核态没有数据可读或者没有缓冲空间可写时,是否阻塞。 + +IO 多路复用的好处在于可同时监听多个 socket 的可读和可写事件,这样就能使得应用可以同时监听多个 socket,释放了应用线程资源。 + +#### 4.4.4. Tomcat 各类 Connector 对比 + +
+ +
+ +- JIO:用 java.io 编写的 TCP 模块,阻塞 IO +- NIO:用 java.nio 编写的 TCP 模块,非阻塞 IO,(IO 多路复用) +- APR:全称 Apache Portable Runtime,使用 JNI 的方式来进行读取文件以及进行网络传输 + +Apache Portable Runtime 是一个高度可移植的库,它是 Apache HTTP Server 2.x 的核心。 APR 具有许多用途,包括访问高级 IO 功能(如 sendfile,epoll 和 OpenSSL),操作系统级功能(随机数生成,系统状态等)和本地进程处理(共享内存,NT 管道和 Unix 套接字)。 + +表格中字段含义说明: + +- Support Polling - 是否支持基于 IO 多路复用的 socket 事件轮询 +- Polling Size - 轮询的最大连接数 +- Wait for next Request - 在等待下一个请求时,处理线程是否释放,BIO 是没有释放的,所以在 keep-alive=true 的情况下处理的并发连接数有限 +- Read Request Headers - 由于 request header 数据较少,可以由容器提前解析完毕,不需要阻塞 +- Read Request Body - 读取 request body 的数据是应用业务逻辑的事情,同时 Servlet 的限制,是需要阻塞读取的 +- Write Response - 跟读取 request body 的逻辑类似,同样需要阻塞写 + +**NIO 处理相关类** + +
+ +
+ +Poller 线程从 EventQueue 获取 PollerEvent,并执行 PollerEvent 的 run 方法,调用 Selector 的 select 方法,如果有可读的 Socket 则创建 Http11NioProcessor,放入到线程池中执行; + +CoyoteAdapter 是 Connector 到 Container 的适配器,Http11NioProcessor 调用其提供的 service 方法,内部创建 Request 和 Response 对象,并调用最顶层容器的 Pipeline 中的第一个 Valve 的 invoke 方法 + +Mapper 主要处理 http url 到 servlet 的映射规则的解析,对外提供 map 方法 + +### 4.5. Comet + +Comet 是一种用于 web 的推送技术,能使服务器实时地将更新的信息传送到客户端,而无须客户端发出请求 +在 WebSocket 出来之前,如果不适用 comet,只能通过浏览器端轮询 Server 来模拟实现服务器端推送。 +Comet 支持 servlet 异步处理 IO,当连接上数据可读时触发事件,并异步写数据(阻塞) + +Tomcat 要实现 Comet,只需继承 HttpServlet 同时,实现 CometProcessor 接口 + +- Begin:新的请求连接接入调用,可进行与 Request 和 Response 相关的对象初始化操作,并保存 response 对象,用于后续写入数据 +- Read:请求连接有数据可读时调用 +- End:当数据可用时,如果读取到文件结束或者 response 被关闭时则被调用 +- Error:在连接上发生异常时调用,数据读取异常、连接断开、处理异常、socket 超时 + +Note: + +- Read:在 post 请求有数据,但在 begin 事件中没有处理,则会调用 read,如果 read 没有读取数据,在会触发 Error 回调,关闭 socket +- End:当 socket 超时,并且 response 被关闭时也会调用;server 被关闭时调用 +- Error:除了 socket 超时不会关闭 socket,其他都会关闭 socket +- End 和 Error 时间触发时应关闭当前 comet 会话,即调用 CometEvent 的 close 方法 + Note:在事件触发时要做好线程安全的操作 + +### 4.6. 异步 Servlet + +
+ +
+ +传统流程: + +- 首先,Servlet 接收到请求之后,request 数据解析; +- 接着,调用业务接口的某些方法,以完成业务处理; +- 最后,根据处理的结果提交响应,Servlet 线程结束 + +
+ +
+ +异步处理流程: + +- 客户端发送一个请求 +- Servlet 容器分配一个线程来处理容器中的一个 servlet +- servlet 调用 request.startAsync(),保存 AsyncContext, 然后返回 +- 任何方式存在的容器线程都将退出,但是 response 仍然保持开放 +- 业务线程使用保存的 AsyncContext 来完成响应(线程池) +- 客户端收到响应 + +Servlet 线程将请求转交给一个异步线程来执行业务处理,线程本身返回至容器,此时 Servlet 还没有生成响应数据,异步线程处理完业务以后,可以直接生成响应数据(异步线程拥有 ServletRequest 和 ServletResponse 对象的引用) + +**为什么 web 应用中支持异步?** + +推出异步,主要是针对那些比较耗时的请求:比如一次缓慢的数据库查询,一次外部 REST API 调用, 或者是其他一些 I/O 密集型操作。这种耗时的请求会很快的耗光 Servlet 容器的线程池,继而影响可扩展性。 + +Note:从客户端的角度来看,request 仍然像任何其他的 HTTP 的 request-response 交互一样,只是耗费了更长的时间而已 + +**异步事件监听** + +- onStartAsync:Request 调用 startAsync 方法时触发 +- onComplete:syncContext 调用 complete 方法时触发 +- onError:处理请求的过程出现异常时触发 +- onTimeout:socket 超时触发 + +Note : +onError/ onTimeout 触发后,会紧接着回调 onComplete +onComplete 执行后,就不可再操作 request 和 response + +## 5. 参考资料 + +- **官方** + + - [Tomcat 官方网站](http://tomcat.apache.org/) + - [Tomcat Wiki](http://wiki.apache.org/tomcat/FrontPage) + - [Tomee 官方网站](http://tomee.apache.org/) + +- **文章** + - [Creating a Web App with Bootstrap and Tomcat Embedded](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/basic_app_embedded_tomcat/basic_app-tomcat-embedded.html) + - [Tomcat 组成与工作原理](https://juejin.im/post/58eb5fdda0bb9f00692a78fc) + - [Tomcat 工作原理](https://www.ibm.com/developerworks/cn/java/j-lo-tomcat1/index.html) + - [Tomcat 设计模式分析](https://www.ibm.com/developerworks/cn/java/j-lo-tomcat2/index.html?ca=drs-) \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" new file mode 100644 index 00000000..fbf9fe8e --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" @@ -0,0 +1,523 @@ +--- +title: Tomcat连接器 +date: 2022-02-17 22:34:30 +order: 02 +categories: + - Java + - JavaEE + - 服务器 + - Tomcat +tags: + - Java + - JavaWeb + - 服务器 + - Tomcat +permalink: /pages/13f070/ +--- + +# Tomcat 连接器 + +## 1. NioEndpoint 组件 + +Tomcat 的 NioEndPoint 组件利用 Java NIO 实现了 I/O 多路复用模型。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127094302.jpg) + +NioEndPoint 子组件功能简介: + +- `LimitLatch` 是连接控制器,负责控制最大连接数。NIO 模式下默认是 10000,达到这个阈值后,连接请求被拒绝。 +- `Acceptor` 负责监听连接请求。`Acceptor` 运行在一个单独的线程里,它在一个死循环里调用 accept 方法来接收新连接,一旦有新的连接请求到来,accept 方法返回一个 `Channel` 对象,接着把 `Channel` 对象交给 `Poller` 去处理。 +- `Poller` 的本质是一个 `Selector`,也运行在单独线程里。`Poller` 内部维护一个 `Channel` 数组,它在一个死循环里不断检测 `Channel` 的数据就绪状态,一旦有 `Channel` 可读,就生成一个 `SocketProcessor` 任务对象扔给 `Executor` 去处理。 +- `Executor` 就是线程池,负责运行 `SocketProcessor` 任务类,`SocketProcessor` 的 run 方法会调用 `Http11Processor` 来读取和解析请求数据。我们知道,`Http11Processor` 是应用层协议的封装,它会调用容器获得响应,再把响应通过 `Channel` 写出。 + +NioEndpoint 如何实现高并发的呢? + +要实现高并发需要合理设计线程模型充分利用 CPU 资源,尽量不要让线程阻塞;另外,就是有多少任务,就用相应规模的线程数去处理。 + +NioEndpoint 要完成三件事情:接收连接、检测 I/O 事件以及处理请求,那么最核心的就是把这三件事情分开,用不同规模的线程去处理,比如用专门的线程组去跑 Acceptor,并且 Acceptor 的个数可以配置;用专门的线程组去跑 Poller,Poller 的个数也可以配置;最后具体任务的执行也由专门的线程池来处理,也可以配置线程池的大小。 + +### 1.1. LimitLatch + +`LimitLatch` 用来控制连接个数,当连接数到达最大时阻塞线程,直到后续组件处理完一个连接后将连接数减 1。请你注意到达最大连接数后操作系统底层还是会接收客户端连接,但用户层已经不再接收。 + +```java +public class LimitLatch { + private class Sync extends AbstractQueuedSynchronizer { + + @Override + protected int tryAcquireShared() { + long newCount = count.incrementAndGet(); + if (newCount > limit) { + count.decrementAndGet(); + return -1; + } else { + return 1; + } + } + + @Override + protected boolean tryReleaseShared(int arg) { + count.decrementAndGet(); + return true; + } + } + + private final Sync sync; + private final AtomicLong count; + private volatile long limit; + + // 线程调用这个方法来获得接收新连接的许可,线程可能被阻塞 + public void countUpOrAwait() throws InterruptedException { + sync.acquireSharedInterruptibly(1); + } + + // 调用这个方法来释放一个连接许可,那么前面阻塞的线程可能被唤醒 + public long countDown() { + sync.releaseShared(0); + long result = getCount(); + return result; + } +} +``` + +LimitLatch 内步定义了内部类 Sync,而 Sync 扩展了 AQS,AQS 是 Java 并发包中的一个核心类,它在内部维护一个状态和一个线程队列,可以用来**控制线程什么时候挂起,什么时候唤醒**。我们可以扩展它来实现自己的同步器,实际上 Java 并发包里的锁和条件变量等等都是通过 AQS 来实现的,而这里的 LimitLatch 也不例外。 + +理解源码要点: + +- 用户线程通过调用 LimitLatch 的 countUpOrAwait 方法来拿到锁,如果暂时无法获取,这个线程会被阻塞到 AQS 的队列中。那 AQS 怎么知道是阻塞还是不阻塞用户线程呢?其实这是由 AQS 的使用者来决定的,也就是内部类 Sync 来决定的,因为 Sync 类重写了 AQS 的**tryAcquireShared() 方法**。它的实现逻辑是如果当前连接数 count 小于 limit,线程能获取锁,返回 1,否则返回 -1。 +- 如何用户线程被阻塞到了 AQS 的队列,那什么时候唤醒呢?同样是由 Sync 内部类决定,Sync 重写了 AQS 的**releaseShared() 方法**,其实就是当一个连接请求处理完了,这时又可以接收一个新连接了,这样前面阻塞的线程将会被唤醒。 + +### 1.2. Acceptor + +Acceptor 实现了 Runnable 接口,因此可以跑在单独线程里。一个端口号只能对应一个 ServerSocketChannel,因此这个 ServerSocketChannel 是在多个 Acceptor 线程之间共享的,它是 Endpoint 的属性,由 Endpoint 完成初始化和端口绑定。 + +``` +serverSock = ServerSocketChannel.open(); +serverSock.socket().bind(addr,getAcceptCount()); +serverSock.configureBlocking(true); +``` + +- bind 方法的第二个参数表示操作系统的等待队列长度,我在上面提到,当应用层面的连接数到达最大值时,操作系统可以继续接收连接,那么操作系统能继续接收的最大连接数就是这个队列长度,可以通过 acceptCount 参数配置,默认是 100。 +- ServerSocketChannel 被设置成阻塞模式,也就是说它是以阻塞的方式接收连接的。ServerSocketChannel 通过 accept() 接受新的连接,accept() 方法返回获得 SocketChannel 对象,然后将 SocketChannel 对象封装在一个 PollerEvent 对象中,并将 PollerEvent 对象压入 Poller 的 Queue 里,这是个典型的生产者 - 消费者模式,Acceptor 与 Poller 线程之间通过 Queue 通信。 + +### 1.3. Poller + +`Poller` 本质是一个 `Selector`,它内部维护一个 `Queue`。 + +``` +private final SynchronizedQueue events = new SynchronizedQueue<>(); +``` + +`SynchronizedQueue` 的核心方法都使用了 `Synchronized` 关键字进行修饰,用来保证同一时刻只有一个线程进行读写。 + +使用 `SynchronizedQueue`,意味着同一时刻只有一个 `Acceptor` 线程对队列进行读写;同时有多个 `Poller` 线程在运行,每个 `Poller` 线程都有自己的队列。每个 `Poller` 线程可能同时被多个 `Acceptor` 线程调用来注册 `PollerEvent`。同样 `Poller` 的个数可以通过 pollers 参数配置。 + +`Poller` 不断的通过内部的 `Selector` 对象向内核查询 `Channel` 的状态,一旦可读就生成任务类 `SocketProcessor` 交给 `Executor` 去处理。`Poller` 的另一个重要任务是循环遍历检查自己所管理的 `SocketChannel` 是否已经超时,如果有超时就关闭这个 `SocketChannel`。 + +### 1.4. SocketProcessor + +我们知道,`Poller` 会创建 `SocketProcessor` 任务类交给线程池处理,而 `SocketProcessor` 实现了 `Runnable` 接口,用来定义 `Executor` 中线程所执行的任务,主要就是调用 `Http11Processor` 组件来处理请求。`Http11Processor` 读取 `Channel` 的数据来生成 `ServletRequest` 对象,这里请你注意: + +`Http11Processor` 并不是直接读取 `Channel` 的。这是因为 Tomcat 支持同步非阻塞 I/O 模型和异步 I/O 模型,在 Java API 中,相应的 Channel 类也是不一样的,比如有 `AsynchronousSocketChannel` 和 `SocketChannel`,为了对 `Http11Processor` 屏蔽这些差异,Tomcat 设计了一个包装类叫作 `SocketWrapper`,`Http11Processor` 只调用 `SocketWrapper` 的方法去读写数据。 + +## 2. Nio2Endpoint 组件 + +Nio2Endpoint 工作流程跟 NioEndpoint 较为相似。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127143839.jpg) + +Nio2Endpoint 子组件功能说明: + +- `LimitLatch` 是连接控制器,它负责控制最大连接数。 +- `Nio2Acceptor` 扩展了 `Acceptor`,用异步 I/O 的方式来接收连接,跑在一个单独的线程里,也是一个线程组。`Nio2Acceptor` 接收新的连接后,得到一个 `AsynchronousSocketChannel`,`Nio2Acceptor` 把 `AsynchronousSocketChannel` 封装成一个 `Nio2SocketWrapper`,并创建一个 `SocketProcessor` 任务类交给线程池处理,并且 `SocketProcessor` 持有 `Nio2SocketWrapper` 对象。 +- `Executor` 在执行 `SocketProcessor` 时,`SocketProcessor` 的 run 方法会调用 `Http11Processor` 来处理请求,`Http11Processor` 会通过 `Nio2SocketWrapper` 读取和解析请求数据,请求经过容器处理后,再把响应通过 `Nio2SocketWrapper` 写出。 + +Nio2Endpoint 跟 NioEndpoint 的一个明显不同点是,**Nio2Endpoint 中没有 Poller 组件,也就是没有 Selector。这是为什么呢?因为在异步 I/O 模式下,Selector 的工作交给内核来做了。** + +### 2.1. Nio2Acceptor + +和 `NioEndpint` 一样,`Nio2Endpoint` 的基本思路是用 `LimitLatch` 组件来控制连接数。 + +但是 `Nio2Acceptor` 的监听连接的过程不是在一个死循环里不断的调 accept 方法,而是通过回调函数来完成的。我们来看看它的连接监听方法: + +``` +serverSock.accept(null, this); +``` + +其实就是调用了 accept 方法,注意它的第二个参数是 this,表明 `Nio2Acceptor` 自己就是处理连接的回调类,因此 `Nio2Acceptor` 实现了 `CompletionHandler` 接口。那么它是如何实现 `CompletionHandler` 接口的呢? + +```java +protected class Nio2Acceptor extends Acceptor + implements CompletionHandler { + + @Override + public void completed(AsynchronousSocketChannel socket, + Void attachment) { + + if (isRunning() && !isPaused()) { + if (getMaxConnections() == -1) { + // 如果没有连接限制,继续接收新的连接 + serverSock.accept(null, this); + } else { + // 如果有连接限制,就在线程池里跑 Run 方法,Run 方法会检查连接数 + getExecutor().execute(this); + } + // 处理请求 + if (!setSocketOptions(socket)) { + closeSocket(socket); + } + } + } +} +``` + +可以看到 `CompletionHandler` 的两个模板参数分别是 `AsynchronousServerSocketChannel` 和 Void,我在前面说过第一个参数就是 `accept` 方法的返回值,第二个参数是附件类,由用户自己决定,这里为 Void。`completed` 方法的处理逻辑比较简单: + +- 如果没有连接限制,继续在本线程中调用 `accept` 方法接收新的连接。 +- 如果有连接限制,就在线程池里跑 `run` 方法去接收新的连接。那为什么要跑 `run` 方法呢,因为在 `run` 方法里会检查连接数,当连接达到最大数时,线程可能会被 `LimitLatch` 阻塞。为什么要放在线程池里跑呢?这是因为如果放在当前线程里执行,`completed` 方法可能被阻塞,会导致这个回调方法一直不返回。 + +接着 `completed` 方法会调用 `setSocketOptions` 方法,在这个方法里,会创建 `Nio2SocketWrapper` 和 `SocketProcessor`,并交给线程池处理。 + +### 2.2. Nio2SocketWrapper + +`Nio2SocketWrapper` 的主要作用是封装 Channel,并提供接口给 `Http11Processor` 读写数据。讲到这里你是不是有个疑问:`Http11Processor` 是不能阻塞等待数据的,按照异步 I/O 的套路,`Http11Processor` 在调用 `Nio2SocketWrapper` 的 read 方法时需要注册回调类,read 调用会立即返回,问题是立即返回后 `Http11Processor` 还没有读到数据, 怎么办呢?这个请求的处理不就失败了吗? + +为了解决这个问题,`Http11Processor` 是通过 2 次 read 调用来完成数据读取操作的。 + +- 第一次 read 调用:连接刚刚建立好后,`Acceptor` 创建 `SocketProcessor` 任务类交给线程池去处理,`Http11Processor` 在处理请求的过程中,会调用 `Nio2SocketWrapper` 的 read 方法发出第一次读请求,同时注册了回调类 `readCompletionHandler`,因为数据没读到,`Http11Processor` 把当前的 `Nio2SocketWrapper` 标记为数据不完整。**接着 `SocketProcessor` 线程被回收,`Http11Processor` 并没有阻塞等待数据**。这里请注意,`Http11Processor` 维护了一个 `Nio2SocketWrapper` 列表,也就是维护了连接的状态。 +- 第二次 read 调用:当数据到达后,内核已经把数据拷贝到 `Http11Processor` 指定的 Buffer 里,同时回调类 `readCompletionHandler` 被调用,在这个回调处理方法里会**重新创建一个新的 `SocketProcessor` 任务来继续处理这个连接**,而这个新的 `SocketProcessor` 任务类持有原来那个 `Nio2SocketWrapper`,这一次 `Http11Processor` 可以通过 `Nio2SocketWrapper` 读取数据了,因为数据已经到了应用层的 Buffer。 + +这个回调类 `readCompletionHandler` 的源码如下,最关键的一点是,**`Nio2SocketWrapper` 是作为附件类来传递的**,这样在回调函数里能拿到所有的上下文。 + +``` +this.readCompletionHandler = new CompletionHandler>() { + public void completed(Integer nBytes, SocketWrapperBase attachment) { + ... + // 通过附件类 SocketWrapper 拿到所有的上下文 + Nio2SocketWrapper.this.getEndpoint().processSocket(attachment, SocketEvent.OPEN_READ, false); + } + + public void failed(Throwable exc, SocketWrapperBase attachment) { + ... + } +} +``` + +## 3. AprEndpoint 组件 + +我们在使用 Tomcat 时,可能会在启动日志里看到这样的提示信息: + +> The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: \*\*\* + +这句话的意思就是推荐你去安装 APR 库,可以提高系统性能。 + +APR(Apache Portable Runtime Libraries)是 Apache 可移植运行时库,它是用 C 语言实现的,其目的是向上层应用程序提供一个跨平台的操作系统接口库。Tomcat 可以用它来处理包括文件和网络 I/O,从而提升性能。Tomcat 支持的连接器有 NIO、NIO.2 和 APR。跟 NioEndpoint 一样,AprEndpoint 也实现了非阻塞 I/O,它们的区别是:NioEndpoint 通过调用 Java 的 NIO API 来实现非阻塞 I/O,而 AprEndpoint 是通过 JNI 调用 APR 本地库而实现非阻塞 I/O 的。 + +同样是非阻塞 I/O,为什么 Tomcat 会提示使用 APR 本地库的性能会更好呢?这是因为在某些场景下,比如需要频繁与操作系统进行交互,Socket 网络通信就是这样一个场景,特别是如果你的 Web 应用使用了 TLS 来加密传输,我们知道 TLS 协议在握手过程中有多次网络交互,在这种情况下 Java 跟 C 语言程序相比还是有一定的差距,而这正是 APR 的强项。 + +Tomcat 本身是 Java 编写的,为了调用 C 语言编写的 APR,需要通过 JNI 方式来调用。JNI(Java Native Interface) 是 JDK 提供的一个编程接口,它允许 Java 程序调用其他语言编写的程序或者代码库,其实 JDK 本身的实现也大量用到 JNI 技术来调用本地 C 程序库。 + +### 3.1. AprEndpoint 工作流程 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127145740.jpg) + +#### 3.1.1. Acceptor + +Accpetor 的功能就是监听连接,接收并建立连接。它的本质就是调用了四个操作系统 API:socket、bind、listen 和 accept。那 Java 语言如何直接调用 C 语言 API 呢?答案就是通过 JNI。具体来说就是两步:先封装一个 Java 类,在里面定义一堆用**native 关键字**修饰的方法,像下面这样。 + +```java +public class Socket { + ... + // 用 native 修饰这个方法,表明这个函数是 C 语言实现 + public static native long create(int family, int type, + int protocol, long cont) + + public static native int bind(long sock, long sa); + + public static native int listen(long sock, int backlog); + + public static native long accept(long sock) +} +``` + +接着用 C 代码实现这些方法,比如 bind 函数就是这样实现的: + +```java +// 注意函数的名字要符合 JNI 规范的要求 +JNIEXPORT jint JNICALL +Java_org_apache_tomcat_jni_Socket_bind(JNIEnv *e, jlong sock,jlong sa) + { + jint rv = APR_SUCCESS; + tcn_socket_t *s = (tcn_socket_t *)sock; + apr_sockaddr_t *a = (apr_sockaddr_t *) sa; + + // 调用 APR 库自己实现的 bind 函数 + rv = (jint)apr_socket_bind(s->sock, a); + return rv; + } +``` + +专栏里我就不展开 JNI 的细节了,你可以[扩展阅读](http://jnicookbook.owsiak.org/contents/)获得更多信息和例子。我们要注意的是函数名字要符合 JNI 的规范,以及 Java 和 C 语言如何互相传递参数,比如在 C 语言有指针,Java 没有指针的概念,所以在 Java 中用 long 类型来表示指针。AprEndpoint 的 Acceptor 组件就是调用了 APR 实现的四个 API。 + +#### 3.1.2. Poller + +Acceptor 接收到一个新的 Socket 连接后,按照 NioEndpoint 的实现,它会把这个 Socket 交给 Poller 去查询 I/O 事件。AprEndpoint 也是这样做的,不过 AprEndpoint 的 Poller 并不是调用 Java NIO 里的 Selector 来查询 Socket 的状态,而是通过 JNI 调用 APR 中的 poll 方法,而 APR 又是调用了操作系统的 epoll API 来实现的。 + +这里有个特别的地方是在 AprEndpoint 中,我们可以配置一个叫`deferAccept`的参数,它对应的是 TCP 协议中的`TCP_DEFER_ACCEPT`,设置这个参数后,当 TCP 客户端有新的连接请求到达时,TCP 服务端先不建立连接,而是再等等,直到客户端有请求数据发过来时再建立连接。这样的好处是服务端不需要用 Selector 去反复查询请求数据是否就绪。 + +这是一种 TCP 协议层的优化,不是每个操作系统内核都支持,因为 Java 作为一种跨平台语言,需要屏蔽各种操作系统的差异,因此并没有把这个参数提供给用户;但是对于 APR 来说,它的目的就是尽可能提升性能,因此它向用户暴露了这个参数。 + +### 3.2. APR 提升性能的秘密 + +APR 连接器之所以能提高 Tomcat 的性能,除了 APR 本身是 C 程序库之外,还有哪些提速的秘密呢? + +**JVM 堆 VS 本地内存** + +我们知道 Java 的类实例一般在 JVM 堆上分配,而 Java 是通过 JNI 调用 C 代码来实现 Socket 通信的,那么 C 代码在运行过程中需要的内存又是从哪里分配的呢?C 代码能否直接操作 Java 堆? + +为了回答这些问题,我先来说说 JVM 和用户进程的关系。如果你想运行一个 Java 类文件,可以用下面的 Java 命令来执行。 + +``` +java my.class +``` + +这个命令行中的`java`其实是**一个可执行程序,这个程序会创建 JVM 来加载和运行你的 Java 类**。操作系统会创建一个进程来执行这个`java`可执行程序,而每个进程都有自己的虚拟地址空间,JVM 用到的内存(包括堆、栈和方法区)就是从进程的虚拟地址空间上分配的。请你注意的是,JVM 内存只是进程空间的一部分,除此之外进程空间内还有代码段、数据段、内存映射区、内核空间等。从 JVM 的角度看,JVM 内存之外的部分叫作本地内存,C 程序代码在运行过程中用到的内存就是本地内存中分配的。下面我们通过一张图来理解一下。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127150729.jpg) + +Tomcat 的 Endpoint 组件在接收网络数据时需要预先分配好一块 Buffer,所谓的 Buffer 就是字节数组`byte[]`,Java 通过 JNI 调用把这块 Buffer 的地址传给 C 代码,C 代码通过操作系统 API 读取 Socket 并把数据填充到这块 Buffer。Java NIO API 提供了两种 Buffer 来接收数据:HeapByteBuffer 和 DirectByteBuffer,下面的代码演示了如何创建两种 Buffer。 + +``` +// 分配 HeapByteBuffer +ByteBuffer buf = ByteBuffer.allocate(1024); + +// 分配 DirectByteBuffer +ByteBuffer buf = ByteBuffer.allocateDirect(1024); +``` + +创建好 Buffer 后直接传给 Channel 的 read 或者 write 函数,最终这块 Buffer 会通过 JNI 调用传递给 C 程序。 + +``` +// 将 buf 作为 read 函数的参数 +int bytesRead = socketChannel.read(buf); +``` + +那 HeapByteBuffer 和 DirectByteBuffer 有什么区别呢?HeapByteBuffer 对象本身在 JVM 堆上分配,并且它持有的字节数组`byte[]`也是在 JVM 堆上分配。但是如果用**HeapByteBuffer**来接收网络数据,**需要把数据从内核先拷贝到一个临时的本地内存,再从临时本地内存拷贝到 JVM 堆**,而不是直接从内核拷贝到 JVM 堆上。这是为什么呢?这是因为数据从内核拷贝到 JVM 堆的过程中,JVM 可能会发生 GC,GC 过程中对象可能会被移动,也就是说 JVM 堆上的字节数组可能会被移动,这样的话 Buffer 地址就失效了。如果这中间经过本地内存中转,从本地内存到 JVM 堆的拷贝过程中 JVM 可以保证不做 GC。 + +如果使用 HeapByteBuffer,你会发现 JVM 堆和内核之间多了一层中转,而 DirectByteBuffer 用来解决这个问题,DirectByteBuffer 对象本身在 JVM 堆上,但是它持有的字节数组不是从 JVM 堆上分配的,而是从本地内存分配的。DirectByteBuffer 对象中有个 long 类型字段 address,记录着本地内存的地址,这样在接收数据的时候,直接把这个本地内存地址传递给 C 程序,C 程序会将网络数据从内核拷贝到这个本地内存,JVM 可以直接读取这个本地内存,这种方式比 HeapByteBuffer 少了一次拷贝,因此一般来说它的速度会比 HeapByteBuffer 快好几倍。你可以通过上面的图加深理解。 + +Tomcat 中的 AprEndpoint 就是通过 DirectByteBuffer 来接收数据的,而 NioEndpoint 和 Nio2Endpoint 是通过 HeapByteBuffer 来接收数据的。你可能会问,NioEndpoint 和 Nio2Endpoint 为什么不用 DirectByteBuffer 呢?这是因为本地内存不好管理,发生内存泄漏难以定位,从稳定性考虑,NioEndpoint 和 Nio2Endpoint 没有去冒这个险。 + +#### 3.2.1. sendfile + +我们再来考虑另一个网络通信的场景,也就是静态文件的处理。浏览器通过 Tomcat 来获取一个 HTML 文件,而 Tomcat 的处理逻辑无非是两步: + +1. 从磁盘读取 HTML 到内存。 +2. 将这段内存的内容通过 Socket 发送出去。 + +但是在传统方式下,有很多次的内存拷贝: + +- 读取文件时,首先是内核把文件内容读取到内核缓冲区。 +- 如果使用 HeapByteBuffer,文件数据从内核到 JVM 堆内存需要经过本地内存中转。 +- 同样在将文件内容推入网络时,从 JVM 堆到内核缓冲区需要经过本地内存中转。 +- 最后还需要把文件从内核缓冲区拷贝到网卡缓冲区。 + +从下面的图你会发现这个过程有 6 次内存拷贝,并且 read 和 write 等系统调用将导致进程从用户态到内核态的切换,会耗费大量的 CPU 和内存资源。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127151041.jpg) + +而 Tomcat 的 AprEndpoint 通过操作系统层面的 sendfile 特性解决了这个问题,sendfile 系统调用方式非常简洁。 + +``` +sendfile(socket, file, len); +``` + +它带有两个关键参数:Socket 和文件句柄。将文件从磁盘写入 Socket 的过程只有两步: + +第一步:将文件内容读取到内核缓冲区。 + +第二步:数据并没有从内核缓冲区复制到 Socket 关联的缓冲区,只有记录数据位置和长度的描述符被添加到 Socket 缓冲区中;接着把数据直接从内核缓冲区传递给网卡。这个过程你可以看下面的图。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127151155.jpg) + +## 4. Executor 组件 + +为了提高处理能力和并发度,Web 容器一般会把处理请求的工作放到线程池里来执行,Tomcat 扩展了原生的 Java 线程池,来满足 Web 容器高并发的需求。 + +### 4.1. Tomcat 定制线程池 + +Tomcat 的线程池也是一个定制版的 ThreadPoolExecutor。Tomcat 传入的参数是这样的: + +``` +// 定制版的任务队列 +taskqueue = new TaskQueue(maxQueueSize); + +// 定制版的线程工厂 +TaskThreadFactory tf = new TaskThreadFactory(namePrefix,daemon,getThreadPriority()); + +// 定制版的线程池 +executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), maxIdleTime, TimeUnit.MILLISECONDS,taskqueue, tf); +``` + +其中的两个关键点: + +- Tomcat 有自己的定制版任务队列和线程工厂,并且可以限制任务队列的长度,它的最大长度是 maxQueueSize。 +- Tomcat 对线程数也有限制,设置了核心线程数(minSpareThreads)和最大线程池数(maxThreads)。 + +除了资源限制以外,Tomcat 线程池还定制自己的任务处理流程。我们知道 Java 原生线程池的任务处理逻辑比较简单: + +1. 前 corePoolSize 个任务时,来一个任务就创建一个新线程。 +2. 后面再来任务,就把任务添加到任务队列里让所有的线程去抢,如果队列满了就创建临时线程。 +3. 如果总线程数达到 maximumPoolSize,**执行拒绝策略。** + +Tomcat 线程池扩展了原生的 ThreadPoolExecutor,通过重写 execute 方法实现了自己的任务处理逻辑: + +1. 前 corePoolSize 个任务时,来一个任务就创建一个新线程。 +2. 再来任务的话,就把任务添加到任务队列里让所有的线程去抢,如果队列满了就创建临时线程。 +3. 如果总线程数达到 maximumPoolSize,**则继续尝试把任务添加到任务队列中去。** +4. **如果缓冲队列也满了,插入失败,执行拒绝策略。** + +观察 Tomcat 线程池和 Java 原生线程池的区别,其实就是在第 3 步,Tomcat 在线程总数达到最大数时,不是立即执行拒绝策略,而是再尝试向任务队列添加任务,添加失败后再执行拒绝策略。那具体如何实现呢,其实很简单,我们来看一下 Tomcat 线程池的 execute 方法的核心代码。 + +``` +public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor { + + ... + + public void execute(Runnable command, long timeout, TimeUnit unit) { + submittedCount.incrementAndGet(); + try { + // 调用 Java 原生线程池的 execute 去执行任务 + super.execute(command); + } catch (RejectedExecutionException rx) { + // 如果总线程数达到 maximumPoolSize,Java 原生线程池执行拒绝策略 + if (super.getQueue() instanceof TaskQueue) { + final TaskQueue queue = (TaskQueue)super.getQueue(); + try { + // 继续尝试把任务放到任务队列中去 + if (!queue.force(command, timeout, unit)) { + submittedCount.decrementAndGet(); + // 如果缓冲队列也满了,插入失败,执行拒绝策略。 + throw new RejectedExecutionException("..."); + } + } + } + } +} +``` + +从这个方法你可以看到,Tomcat 线程池的 execute 方法会调用 Java 原生线程池的 execute 去执行任务,如果总线程数达到 maximumPoolSize,Java 原生线程池的 execute 方法会抛出 RejectedExecutionException 异常,但是这个异常会被 Tomcat 线程池的 execute 方法捕获到,并继续尝试把这个任务放到任务队列中去;如果任务队列也满了,再执行拒绝策略。 + +### 4.2. Tomcat 定制任务队列 + +细心的你有没有发现,在 Tomcat 线程池的 execute 方法最开始有这么一行: + +``` +submittedCount.incrementAndGet(); +``` + +这行代码的意思把 submittedCount 这个原子变量加一,并且在任务执行失败,抛出拒绝异常时,将这个原子变量减一: + +``` +submittedCount.decrementAndGet(); +``` + +其实 Tomcat 线程池是用这个变量 submittedCount 来维护已经提交到了线程池,但是还没有执行完的任务个数。Tomcat 为什么要维护这个变量呢?这跟 Tomcat 的定制版的任务队列有关。Tomcat 的任务队列 TaskQueue 扩展了 Java 中的 LinkedBlockingQueue,我们知道 LinkedBlockingQueue 默认情况下长度是没有限制的,除非给它一个 capacity。因此 Tomcat 给了它一个 capacity,TaskQueue 的构造函数中有个整型的参数 capacity,TaskQueue 将 capacity 传给父类 LinkedBlockingQueue 的构造函数。 + +```java +public class TaskQueue extends LinkedBlockingQueue { + + public TaskQueue(int capacity) { + super(capacity); + } + ... +} +``` + +这个 capacity 参数是通过 Tomcat 的 maxQueueSize 参数来设置的,但问题是默认情况下 maxQueueSize 的值是`Integer.MAX_VALUE`,等于没有限制,这样就带来一个问题:当前线程数达到核心线程数之后,再来任务的话线程池会把任务添加到任务队列,并且总是会成功,这样永远不会有机会创建新线程了。 + +为了解决这个问题,TaskQueue 重写了 LinkedBlockingQueue 的 offer 方法,在合适的时机返回 false,返回 false 表示任务添加失败,这时线程池会创建新的线程。那什么是合适的时机呢?请看下面 offer 方法的核心源码: + +```java +public class TaskQueue extends LinkedBlockingQueue { + + ... + @Override + // 线程池调用任务队列的方法时,当前线程数肯定已经大于核心线程数了 + public boolean offer(Runnable o) { + + // 如果线程数已经到了最大值,不能创建新线程了,只能把任务添加到任务队列。 + if (parent.getPoolSize() == parent.getMaximumPoolSize()) + return super.offer(o); + + // 执行到这里,表明当前线程数大于核心线程数,并且小于最大线程数。 + // 表明是可以创建新线程的,那到底要不要创建呢?分两种情况: + + //1. 如果已提交的任务数小于当前线程数,表示还有空闲线程,无需创建新线程 + if (parent.getSubmittedCount()<=(parent.getPoolSize())) + return super.offer(o); + + //2. 如果已提交的任务数大于当前线程数,线程不够用了,返回 false 去创建新线程 + if (parent.getPoolSize()> clazzes, ServletContext ctx) throws ServletException { + ... + } +} +``` + +一旦定义好了 SCI,Tomcat 在启动阶段扫描类时,会将 HandlesTypes 注解中指定的类都扫描出来,作为 SCI 的 onStartup 方法的参数,并调用 SCI 的 onStartup 方法。注意到 WsSci 的 HandlesTypes 注解中定义了`ServerEndpoint.class`、`ServerApplicationConfig.class`和`Endpoint.class`,因此在 Tomcat 的启动阶段会将这些类的类实例(注意不是对象实例)传递给 WsSci 的 onStartup 方法。那么 WsSci 的 onStartup 方法又做了什么事呢? + +它会构造一个 WebSocketContainer 实例,你可以把 WebSocketContainer 理解成一个专门处理 WebSocket 请求的**Endpoint 容器**。也就是说 Tomcat 会把扫描到的 Endpoint 子类和添加了注解`@ServerEndpoint`的类注册到这个容器中,并且这个容器还维护了 URL 到 Endpoint 的映射关系,这样通过请求 URL 就能找到具体的 Endpoint 来处理 WebSocket 请求。 + +### 5.2. WebSocket 请求处理 + +Tomcat 用 ProtocolHandler 组件屏蔽应用层协议的差异,其中 ProtocolHandler 中有两个关键组件:Endpoint 和 Processor。需要注意,这里的 Endpoint 跟上文提到的 WebSocket 中的 Endpoint 完全是两回事,连接器中的 Endpoint 组件用来处理 I/O 通信。WebSocket 本质就是一个应用层协议,因此不能用 HttpProcessor 来处理 WebSocket 请求,而要用专门 Processor 来处理,而在 Tomcat 中这样的 Processor 叫作 UpgradeProcessor。 + +为什么叫 Upgrade Processor 呢?这是因为 Tomcat 是将 HTTP 协议升级成 WebSocket 协议的。 + +WebSocket 是通过 HTTP 协议来进行握手的,因此当 WebSocket 的握手请求到来时,HttpProtocolHandler 首先接收到这个请求,在处理这个 HTTP 请求时,Tomcat 通过一个特殊的 Filter 判断该当前 HTTP 请求是否是一个 WebSocket Upgrade 请求(即包含`Upgrade: websocket`的 HTTP 头信息),如果是,则在 HTTP 响应里添加 WebSocket 相关的响应头信息,并进行协议升级。具体来说就是用 UpgradeProtocolHandler 替换当前的 HttpProtocolHandler,相应的,把当前 Socket 的 Processor 替换成 UpgradeProcessor,同时 Tomcat 会创建 WebSocket Session 实例和 Endpoint 实例,并跟当前的 WebSocket 连接一一对应起来。这个 WebSocket 连接不会立即关闭,并且在请求处理中,不再使用原有的 HttpProcessor,而是用专门的 UpgradeProcessor,UpgradeProcessor 最终会调用相应的 Endpoint 实例来处理请求。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127153521.jpg) + +你可以看到,Tomcat 对 WebSocket 请求的处理没有经过 Servlet 容器,而是通过 UpgradeProcessor 组件直接把请求发到 ServerEndpoint 实例,并且 Tomcat 的 WebSocket 实现不需要关注具体 I/O 模型的细节,从而实现了与具体 I/O 方式的解耦。 + +## 6. 参考资料 + +- **官方** + - [Tomcat 官方网站](http://tomcat.apache.org/) + - [Tomcat Wiki](http://wiki.apache.org/tomcat/FrontPage) + - [Tomee 官方网站](http://tomee.apache.org/) +- **教程** + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" new file mode 100644 index 00000000..0f71d9ce --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" @@ -0,0 +1,728 @@ +--- +title: Tomcat容器 +date: 2022-02-17 22:34:30 +order: 03 +categories: + - Java + - JavaEE + - 服务器 + - Tomcat +tags: + - Java + - JavaWeb + - 服务器 + - Tomcat +permalink: /pages/d5076a/ +--- + +# Tomcat 容器 + +## Tomcat 实现热部署和热加载 + +- 热加载的实现方式是 Web 容器启动一个后台线程,定期检测类文件的变化,如果有变化,就重新加载类,在这个过程中不会清空 Session ,一般用在开发环境。 +- 热部署原理类似,也是由后台线程定时检测 Web 应用的变化,但它会重新加载整个 Web 应用。这种方式会清空 Session,比热加载更加干净、彻底,一般用在生产环境。 + +Tomcat 通过开启后台线程,使得各个层次的容器组件都有机会完成一些周期性任务。Tomcat 是基于 ScheduledThreadPoolExecutor 实现周期性任务的: + +```java +bgFuture = exec.scheduleWithFixedDelay( + new ContainerBackgroundProcessor(),// 要执行的 Runnable + backgroundProcessorDelay, // 第一次执行延迟多久 + backgroundProcessorDelay, // 之后每次执行间隔多久 + TimeUnit.SECONDS); // 时间单位 +``` + +第一个参数就是要周期性执行的任务类 ContainerBackgroundProcessor,它是一个 Runnable,同时也是 ContainerBase 的内部类,ContainerBase 是所有容器组件的基类,我们来回忆一下容器组件有哪些,有 Engine、Host、Context 和 Wrapper 等,它们具有父子关系。 + +### ContainerBackgroundProcessor 实现 + +我们接来看 ContainerBackgroundProcessor 具体是如何实现的。 + +```java +protected class ContainerBackgroundProcessor implements Runnable { + + @Override + public void run() { + // 请注意这里传入的参数是 " 宿主类 " 的实例 + processChildren(ContainerBase.this); + } + + protected void processChildren(Container container) { + try { + //1. 调用当前容器的 backgroundProcess 方法。 + container.backgroundProcess(); + + //2. 遍历所有的子容器,递归调用 processChildren, + // 这样当前容器的子孙都会被处理 + Container[] children = container.findChildren(); + for (int i = 0; i < children.length; i++) { + // 这里请你注意,容器基类有个变量叫做 backgroundProcessorDelay,如果大于 0,表明子容器有自己的后台线程,无需父容器来调用它的 processChildren 方法。 + if (children[i].getBackgroundProcessorDelay() <= 0) { + processChildren(children[i]); + } + } + } catch (Throwable t) { ... } +``` + +上面的代码逻辑也是比较清晰的,首先 ContainerBackgroundProcessor 是一个 Runnable,它需要实现 run 方法,它的 run 很简单,就是调用了 processChildren 方法。这里有个小技巧,它把“宿主类”,也就是**ContainerBase 的类实例当成参数传给了 run 方法**。 + +而在 processChildren 方法里,就做了两步:调用当前容器的 backgroundProcess 方法,以及递归调用子孙的 backgroundProcess 方法。请你注意 backgroundProcess 是 Container 接口中的方法,也就是说所有类型的容器都可以实现这个方法,在这个方法里完成需要周期性执行的任务。 + +这样的设计意味着什么呢?我们只需要在顶层容器,也就是 Engine 容器中启动一个后台线程,那么这个线程**不但会执行 Engine 容器的周期性任务,它还会执行所有子容器的周期性任务**。 + +### backgroundProcess 方法 + +上述代码都是在基类 ContainerBase 中实现的,那具体容器类需要做什么呢?其实很简单,如果有周期性任务要执行,就实现 backgroundProcess 方法;如果没有,就重用基类 ContainerBase 的方法。ContainerBase 的 backgroundProcess 方法实现如下: + +```java +public void backgroundProcess() { + + //1. 执行容器中 Cluster 组件的周期性任务 + Cluster cluster = getClusterInternal(); + if (cluster != null) { + cluster.backgroundProcess(); + } + + //2. 执行容器中 Realm 组件的周期性任务 + Realm realm = getRealmInternal(); + if (realm != null) { + realm.backgroundProcess(); + } + + //3. 执行容器中 Valve 组件的周期性任务 + Valve current = pipeline.getFirst(); + while (current != null) { + current.backgroundProcess(); + current = current.getNext(); + } + + //4. 触发容器的 " 周期事件 ",Host 容器的监听器 HostConfig 就靠它来调用 + fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null); +} +``` + +从上面的代码可以看到,不仅每个容器可以有周期性任务,每个容器中的其他通用组件,比如跟集群管理有关的 Cluster 组件、跟安全管理有关的 Realm 组件都可以有自己的周期性任务。 + +我在前面的专栏里提到过,容器之间的链式调用是通过 Pipeline-Valve 机制来实现的,从上面的代码你可以看到容器中的 Valve 也可以有周期性任务,并且被 ContainerBase 统一处理。 + +请你特别注意的是,在 backgroundProcess 方法的最后,还触发了容器的“周期事件”。我们知道容器的生命周期事件有初始化、启动和停止等,那“周期事件”又是什么呢?它跟生命周期事件一样,是一种扩展机制,你可以这样理解: + +又一段时间过去了,容器还活着,你想做点什么吗?如果你想做点什么,就创建一个监听器来监听这个“周期事件”,事件到了我负责调用你的方法。 + +总之,有了 ContainerBase 中的后台线程和 backgroundProcess 方法,各种子容器和通用组件不需要各自弄一个后台线程来处理周期性任务,这样的设计显得优雅和整洁。 + +### Tomcat 热加载 + +有了 ContainerBase 的周期性任务处理“框架”,作为具体容器子类,只需要实现自己的周期性任务就行。而 Tomcat 的热加载,就是在 Context 容器中实现的。Context 容器的 backgroundProcess 方法是这样实现的: + +```java +public void backgroundProcess() { + + //WebappLoader 周期性的检查 WEB-INF/classes 和 WEB-INF/lib 目录下的类文件 + Loader loader = getLoader(); + if (loader != null) { + loader.backgroundProcess(); + } + + //Session 管理器周期性的检查是否有过期的 Session + Manager manager = getManager(); + if (manager != null) { + manager.backgroundProcess(); + } + + // 周期性的检查静态资源是否有变化 + WebResourceRoot resources = getResources(); + if (resources != null) { + resources.backgroundProcess(); + } + + // 调用父类 ContainerBase 的 backgroundProcess 方法 + super.backgroundProcess(); +} +``` + +从上面的代码我们看到 Context 容器通过 WebappLoader 来检查类文件是否有更新,通过 Session 管理器来检查是否有 Session 过期,并且通过资源管理器来检查静态资源是否有更新,最后还调用了父类 ContainerBase 的 backgroundProcess 方法。 + +这里我们要重点关注,WebappLoader 是如何实现热加载的,它主要是调用了 Context 容器的 reload 方法,而 Context 的 reload 方法比较复杂,总结起来,主要完成了下面这些任务: + +1. 停止和销毁 Context 容器及其所有子容器,子容器其实就是 Wrapper,也就是说 Wrapper 里面 Servlet 实例也被销毁了。 +2. 停止和销毁 Context 容器关联的 Listener 和 Filter。 +3. 停止和销毁 Context 下的 Pipeline 和各种 Valve。 +4. 停止和销毁 Context 的类加载器,以及类加载器加载的类文件资源。 +5. 启动 Context 容器,在这个过程中会重新创建前面四步被销毁的资源。 + +在这个过程中,类加载器发挥着关键作用。一个 Context 容器对应一个类加载器,类加载器在销毁的过程中会把它加载的所有类也全部销毁。Context 容器在启动过程中,会创建一个新的类加载器来加载新的类文件。 + +在 Context 的 reload 方法里,并没有调用 Session 管理器的 distroy 方法,也就是说这个 Context 关联的 Session 是没有销毁的。你还需要注意的是,Tomcat 的热加载默认是关闭的,你需要在 conf 目录下的 Context.xml 文件中设置 reloadable 参数来开启这个功能,像下面这样: + +``` + +``` + +### Tomcat 热部署 + +我们再来看看热部署,热部署跟热加载的本质区别是,热部署会重新部署 Web 应用,原来的 Context 对象会整个被销毁掉,因此这个 Context 所关联的一切资源都会被销毁,包括 Session。 + +那么 Tomcat 热部署又是由哪个容器来实现的呢?应该不是由 Context,因为热部署过程中 Context 容器被销毁了,那么这个重担就落在 Host 身上了,因为它是 Context 的父容器。 + +跟 Context 不一样,Host 容器并没有在 backgroundProcess 方法中实现周期性检测的任务,而是通过监听器 HostConfig 来实现的,HostConfig 就是前面提到的“周期事件”的监听器,那“周期事件”达到时,HostConfig 会做什么事呢? + +```java +public void lifecycleEvent(LifecycleEvent event) { + // 执行 check 方法。 + if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) { + check(); + } +} +``` + +它执行了 check 方法,我们接着来看 check 方法里做了什么。 + +```java +protected void check() { + + if (host.getAutoDeploy()) { + // 检查这个 Host 下所有已经部署的 Web 应用 + DeployedApplication[] apps = + deployed.values().toArray(new DeployedApplication[0]); + + for (int i = 0; i < apps.length; i++) { + // 检查 Web 应用目录是否有变化 + checkResources(apps[i], false); + } + + // 执行部署 + deployApps(); + } +} +``` + +其实 HostConfig 会检查 webapps 目录下的所有 Web 应用: + +- 如果原来 Web 应用目录被删掉了,就把相应 Context 容器整个销毁掉。 +- 是否有新的 Web 应用目录放进来了,或者有新的 WAR 包放进来了,就部署相应的 Web 应用。 + +因此 HostConfig 做的事情都是比较“宏观”的,它不会去检查具体类文件或者资源文件是否有变化,而是检查 Web 应用目录级别的变化。 + +## Tomcat 的类加载机制 + +Tomcat 的自定义类加载器 `WebAppClassLoader` 打破了双亲委派机制,它**首先自己尝试去加载某个类,如果找不到再代理给父类加载器**,其目的是优先加载 Web 应用自己定义的类。具体实现就是重写 ClassLoader 的两个方法:findClass 和 loadClass。 + +### findClass 方法 + +我们先来看看 findClass 方法的实现,为了方便理解和阅读,我去掉了一些细节: + +```java +public Class findClass(String name) throws ClassNotFoundException { + ... + + Class clazz = null; + try { + //1. 先在 Web 应用目录下查找类 + clazz = findClassInternal(name); + } catch (RuntimeException e) { + throw e; + } + + if (clazz == null) { + try { + //2. 如果在本地目录没有找到,交给父加载器去查找 + clazz = super.findClass(name); + } catch (RuntimeException e) { + throw e; + } + + //3. 如果父类也没找到,抛出 ClassNotFoundException + if (clazz == null) { + throw new ClassNotFoundException(name); + } + + return clazz; +} +``` + +在 findClass 方法里,主要有三个步骤: + +1. 先在 Web 应用本地目录下查找要加载的类。 +2. 如果没有找到,交给父加载器去查找,它的父加载器就是上面提到的系统类加载器 AppClassLoader。 +3. 如何父加载器也没找到这个类,抛出 ClassNotFound 异常。 + +### loadClass 方法 + +接着我们再来看 Tomcat 类加载器的 loadClass 方法的实现,同样我也去掉了一些细节: + +```java +public Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + + synchronized (getClassLoadingLock(name)) { + + Class clazz = null; + + //1. 先在本地 cache 查找该类是否已经加载过 + clazz = findLoadedClass0(name); + if (clazz != null) { + if (resolve) + resolveClass(clazz); + return clazz; + } + + //2. 从系统类加载器的 cache 中查找是否加载过 + clazz = findLoadedClass(name); + if (clazz != null) { + if (resolve) + resolveClass(clazz); + return clazz; + } + + // 3. 尝试用 ExtClassLoader 类加载器类加载,为什么? + ClassLoader javaseLoader = getJavaseClassLoader(); + try { + clazz = javaseLoader.loadClass(name); + if (clazz != null) { + if (resolve) + resolveClass(clazz); + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + + // 4. 尝试在本地目录搜索 class 并加载 + try { + clazz = findClass(name); + if (clazz != null) { + if (resolve) + resolveClass(clazz); + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + + // 5. 尝试用系统类加载器 (也就是 AppClassLoader) 来加载 + try { + clazz = Class.forName(name, false, parent); + if (clazz != null) { + if (resolve) + resolveClass(clazz); + return clazz; + } + } catch (ClassNotFoundException e) { + // Ignore + } + } + + //6. 上述过程都加载失败,抛出异常 + throw new ClassNotFoundException(name); +} +``` + +loadClass 方法稍微复杂一点,主要有六个步骤: + +1. 先在本地 Cache 查找该类是否已经加载过,也就是说 Tomcat 的类加载器是否已经加载过这个类。 +2. 如果 Tomcat 类加载器没有加载过这个类,再看看系统类加载器是否加载过。 +3. 如果都没有,就让**ExtClassLoader**去加载,这一步比较关键,目的**防止 Web 应用自己的类覆盖 JRE 的核心类**。因为 Tomcat 需要打破双亲委派机制,假如 Web 应用里自定义了一个叫 Object 的类,如果先加载这个 Object 类,就会覆盖 JRE 里面的那个 Object 类,这就是为什么 Tomcat 的类加载器会优先尝试用 ExtClassLoader 去加载,因为 ExtClassLoader 会委托给 BootstrapClassLoader 去加载,BootstrapClassLoader 发现自己已经加载了 Object 类,直接返回给 Tomcat 的类加载器,这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。 +4. 如果 ExtClassLoader 加载器加载失败,也就是说 JRE 核心类中没有这类,那么就在本地 Web 应用目录下查找并加载。 +5. 如果本地目录下没有这个类,说明不是 Web 应用自己定义的类,那么由系统类加载器去加载。这里请你注意,Web 应用是通过`Class.forName`调用交给系统类加载器的,因为`Class.forName`的默认加载器就是系统类加载器。 +6. 如果上述加载过程全部失败,抛出 ClassNotFound 异常。 + +从上面的过程我们可以看到,Tomcat 的类加载器打破了双亲委派机制,没有一上来就直接委托给父加载器,而是先在本地目录下加载,为了避免本地目录下的类覆盖 JRE 的核心类,先尝试用 JVM 扩展类加载器 ExtClassLoader 去加载。那为什么不先用系统类加载器 AppClassLoader 去加载?很显然,如果是这样的话,那就变成双亲委派机制了,这就是 Tomcat 类加载器的巧妙之处。 + +### Tomcat 实现应用隔离 + +Tomcat 作为 Web 容器,需要解决以下问题: + +1. 如果在 Tomcat 中运行了两个 Web 应用程序,两个 Web 应用中有同名的 Servlet,但是功能不同,Tomcat 需要同时加载和管理这两个同名的 Servlet 类,保证它们不会冲突,因此 Web 应用之间的类需要隔离。 +2. 两个 Web 应用都依赖同一个第三方的 JAR 包,比如 Spring,那 Spring 的 JAR 包被加载到内存后,Tomcat 要保证这两个 Web 应用能够共享,也就是说 Spring 的 JAR 包只被加载一次,否则随着依赖的第三方 JAR 包增多,JVM 的内存会膨胀。 +3. 需要隔离 Tomcat 本身的类和 Web 应用的类。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201130141536.png) + +#### WebAppClassLoader + +针对第一个问题: + +如果使用 JVM 默认 AppClassLoader 来加载 Web 应用,AppClassLoader 只能加载一个 Servlet 类,在加载第二个同名 Servlet 类时,AppClassLoader 会返回第一个 Servlet 类的 Class 实例,这是因为在 AppClassLoader 看来,同名的 Servlet 类只被加载一次。 + +Tomcat 的解决方案是自定义一个类加载器 WebAppClassLoader, 并且给每个 Web 应用创建一个类加载器实例。我们知道,Context 容器组件对应一个 Web 应用,因此,每个 Context 容器负责创建和维护一个 WebAppClassLoader 加载器实例。这背后的原理是,**不同的加载器实例加载的类被认为是不同的类**,即使它们的类名相同。这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间,每一个 Web 应用都有自己的类空间,Web 应用之间通过各自的类加载器互相隔离。 + +#### SharedClassLoader + +针对第二个问题: + +本质需求是两个 Web 应用之间怎么共享库类,并且不能重复加载相同的类。我们知道,在双亲委派机制里,各个子加载器都可以通过父加载器去加载类,那么把需要共享的类放到父加载器的加载路径下不就行了吗,应用程序也正是通过这种方式共享 JRE 的核心类。因此 Tomcat 的设计者又加了一个类加载器 SharedClassLoader,作为 WebAppClassLoader 的父加载器,专门来加载 Web 应用之间共享的类。如果 WebAppClassLoader 自己没有加载到某个类,就会委托父加载器 SharedClassLoader 去加载这个类,SharedClassLoader 会在指定目录下加载共享类,之后返回给 WebAppClassLoader,这样共享的问题就解决了。 + +#### CatalinaClassloader + +如何隔离 Tomcat 本身的类和 Web 应用的类? + +要共享可以通过父子关系,要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的,它们可能拥有同一个父加载器,但是两个兄弟类加载器加载的类是隔离的。基于此 Tomcat 又设计一个类加载器 CatalinaClassloader,专门来加载 Tomcat 自身的类。这样设计有个问题,那 Tomcat 和各 Web 应用之间需要共享一些类时该怎么办呢? + +#### CommonClassLoader + +老办法,还是再增加一个 CommonClassLoader,作为 CatalinaClassloader 和 SharedClassLoader 的父加载器。CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。 + +## Tomcat 实现 Servlet 规范 + +Servlet 容器最重要的任务就是创建 Servlet 的实例并且调用 Servlet。 + +一个 Web 应用里往往有多个 Servlet,而在 Tomcat 中一个 Web 应用对应一个 Context 容器,也就是说一个 Context 容器需要管理多个 Servlet 实例。但 Context 容器并不直接持有 Servlet 实例,而是通过子容器 Wrapper 来管理 Servlet,你可以把 Wrapper 容器看作是 Servlet 的包装。 + +为什么需要 Wrapper 呢?Context 容器直接维护一个 Servlet 数组不就行了吗?这是因为 Servlet 不仅仅是一个类实例,它还有相关的配置信息,比如它的 URL 映射、它的初始化参数,因此设计出了一个包装器,把 Servlet 本身和它相关的数据包起来,没错,这就是面向对象的思想。 + +除此以外,Servlet 规范中还有两个重要特性:Listener 和 Filter,Tomcat 也需要创建它们的实例,并在合适的时机去调用它们的方法。 + +### Servlet 管理 + +Tomcat 是用 Wrapper 容器来管理 Servlet 的,那 Wrapper 容器具体长什么样子呢?我们先来看看它里面有哪些关键的成员变量: + +```java +protected volatile Servlet instance = null; +``` + +它拥有一个 Servlet 实例,并且 Wrapper 通过 loadServlet 方法来实例化 Servlet。为了方便你阅读,我简化了代码: + +```java +public synchronized Servlet loadServlet() throws ServletException { + Servlet servlet; + + //1. 创建一个 Servlet 实例 + servlet = (Servlet) instanceManager.newInstance(servletClass); + + //2. 调用了 Servlet 的 init 方法,这是 Servlet 规范要求的 + initServlet(servlet); + + return servlet; +} +``` + +其实 loadServlet 主要做了两件事:创建 Servlet 的实例,并且调用 Servlet 的 init 方法,因为这是 Servlet 规范要求的。 + +那接下来的问题是,什么时候会调到这个 loadServlet 方法呢?为了加快系统的启动速度,我们往往会采取资源延迟加载的策略,Tomcat 也不例外,默认情况下 Tomcat 在启动时不会加载你的 Servlet,除非你把 Servlet 的`loadOnStartup`参数设置为`true`。 + +这里还需要你注意的是,虽然 Tomcat 在启动时不会创建 Servlet 实例,但是会创建 Wrapper 容器,就好比尽管枪里面还没有子弹,先把枪造出来。那子弹什么时候造呢?是真正需要开枪的时候,也就是说有请求来访问某个 Servlet 时,这个 Servlet 的实例才会被创建。 + +那 Servlet 是被谁调用的呢?我们回忆一下专栏前面提到过 Tomcat 的 Pipeline-Valve 机制,每个容器组件都有自己的 Pipeline,每个 Pipeline 中有一个 Valve 链,并且每个容器组件有一个 BasicValve(基础阀)。Wrapper 作为一个容器组件,它也有自己的 Pipeline 和 BasicValve,Wrapper 的 BasicValve 叫 **StandardWrapperValve**。 + +你可以想到,当请求到来时,Context 容器的 BasicValve 会调用 Wrapper 容器中 Pipeline 中的第一个 Valve,然后会调用到 StandardWrapperValve。我们先来看看它的 invoke 方法是如何实现的,同样为了方便你阅读,我简化了代码: + +```java +public final void invoke(Request request, Response response) { + + //1. 实例化 Servlet + servlet = wrapper.allocate(); + + //2. 给当前请求创建一个 Filter 链 + ApplicationFilterChain filterChain = + ApplicationFilterFactory.createFilterChain(request, wrapper, servlet); + + //3. 调用这个 Filter 链,Filter 链中的最后一个 Filter 会调用 Servlet + filterChain.doFilter(request.getRequest(), response.getResponse()); + +} +``` + +StandardWrapperValve 的 invoke 方法比较复杂,去掉其他异常处理的一些细节,本质上就是三步: + +- 第一步,创建 Servlet 实例; +- 第二步,给当前请求创建一个 Filter 链; +- 第三步,调用这个 Filter 链。 + +你可能会问,为什么需要给每个请求创建一个 Filter 链?这是因为每个请求的请求路径都不一样,而 Filter 都有相应的路径映射,因此不是所有的 Filter 都需要来处理当前的请求,我们需要根据请求的路径来选择特定的一些 Filter 来处理。 + +第二个问题是,为什么没有看到调到 Servlet 的 service 方法?这是因为 Filter 链的 doFilter 方法会负责调用 Servlet,具体来说就是 Filter 链中的最后一个 Filter 会负责调用 Servlet。 + +接下来我们来看 Filter 的实现原理。 + +### Filter 管理 + +我们知道,跟 Servlet 一样,Filter 也可以在`web.xml`文件里进行配置,不同的是,Filter 的作用域是整个 Web 应用,因此 Filter 的实例是在 Context 容器中进行管理的,Context 容器用 Map 集合来保存 Filter。 + +```java +private Map filterDefs = new HashMap<>(); +``` + +那上面提到的 Filter 链又是什么呢?Filter 链的存活期很短,它是跟每个请求对应的。一个新的请求来了,就动态创建一个 FIlter 链,请求处理完了,Filter 链也就被回收了。理解它的原理也非常关键,我们还是来看看源码: + +```java +public final class ApplicationFilterChain implements FilterChain { + + //Filter 链中有 Filter 数组,这个好理解 + private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0]; + + //Filter 链中的当前的调用位置 + private int pos = 0; + + // 总共有多少了 Filter + private int n = 0; + + // 每个 Filter 链对应一个 Servlet,也就是它要调用的 Servlet + private Servlet servlet = null; + + public void doFilter(ServletRequest req, ServletResponse res) { + internalDoFilter(request,response); + } + + private void internalDoFilter(ServletRequest req, + ServletResponse res){ + + // 每个 Filter 链在内部维护了一个 Filter 数组 + if (pos < n) { + ApplicationFilterConfig filterConfig = filters[pos++]; + Filter filter = filterConfig.getFilter(); + + filter.doFilter(request, response, this); + return; + } + + servlet.service(request, response); + +} +``` + +从 ApplicationFilterChain 的源码我们可以看到几个关键信息: + +- Filter 链中除了有 Filter 对象的数组,还有一个整数变量 pos,这个变量用来记录当前被调用的 Filter 在数组中的位置。 +- Filter 链中有个 Servlet 实例,这个好理解,因为上面提到了,每个 Filter 链最后都会调到一个 Servlet。 +- Filter 链本身也实现了 doFilter 方法,直接调用了一个内部方法 internalDoFilter。 +- internalDoFilter 方法的实现比较有意思,它做了一个判断,如果当前 Filter 的位置小于 Filter 数组的长度,也就是说 Filter 还没调完,就从 Filter 数组拿下一个 Filter,调用它的 doFilter 方法。否则,意味着所有 Filter 都调到了,就调用 Servlet 的 service 方法。 + +但问题是,方法体里没看到循环,谁在不停地调用 Filter 链的 doFIlter 方法呢?Filter 是怎么依次调到的呢? + +答案是**Filter 本身的 doFilter 方法会调用 Filter 链的 doFilter 方法**,我们还是来看看代码就明白了: + +```java +public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain){ + + ... + + // 调用 Filter 的方法 + chain.doFilter(request, response); + + } +``` + +注意 Filter 的 doFilter 方法有个关键参数 FilterChain,就是 Filter 链。并且每个 Filter 在实现 doFilter 时,必须要调用 Filter 链的 doFilter 方法,而 Filter 链中保存当前 FIlter 的位置,会调用下一个 FIlter 的 doFilter 方法,这样链式调用就完成了。 + +Filter 链跟 Tomcat 的 Pipeline-Valve 本质都是责任链模式,但是在具体实现上稍有不同,你可以细细体会一下。 + +### Listener 管理 + +我们接着聊 Servlet 规范里 Listener。跟 Filter 一样,Listener 也是一种扩展机制,你可以监听容器内部发生的事件,主要有两类事件: + +- 第一类是生命状态的变化,比如 Context 容器启动和停止、Session 的创建和销毁。 +- 第二类是属性的变化,比如 Context 容器某个属性值变了、Session 的某个属性值变了以及新的请求来了等。 + +我们可以在`web.xml`配置或者通过注解的方式来添加监听器,在监听器里实现我们的业务逻辑。对于 Tomcat 来说,它需要读取配置文件,拿到监听器类的名字,实例化这些类,并且在合适的时机调用这些监听器的方法。 + +Tomcat 是通过 Context 容器来管理这些监听器的。Context 容器将两类事件分开来管理,分别用不同的集合来存放不同类型事件的监听器: + +```java +// 监听属性值变化的监听器 +private List applicationEventListenersList = new CopyOnWriteArrayList<>(); + +// 监听生命事件的监听器 +private Object applicationLifecycleListenersObjects[] = new Object[0]; +``` + +剩下的事情就是触发监听器了,比如在 Context 容器的启动方法里,就触发了所有的 ServletContextListener: + +```java +//1. 拿到所有的生命周期监听器 +Object instances[] = getApplicationLifecycleListeners(); + +for (int i = 0; i < instances.length; i++) { + //2. 判断 Listener 的类型是不是 ServletContextListener + if (!(instances[i] instanceof ServletContextListener)) + continue; + + //3. 触发 Listener 的方法 + ServletContextListener lr = (ServletContextListener) instances[i]; + lr.contextInitialized(event); +} +``` + +需要注意的是,这里的 ServletContextListener 接口是一种留给用户的扩展机制,用户可以实现这个接口来定义自己的监听器,监听 Context 容器的启停事件。Spring 就是这么做的。ServletContextListener 跟 Tomcat 自己的生命周期事件 LifecycleListener 是不同的。LifecycleListener 定义在生命周期管理组件中,由基类 LifeCycleBase 统一管理。 + +## Tomcat 支持异步 Servlet + +### 异步示例 + +```java +@WebServlet(urlPatterns = {"/async"}, asyncSupported = true) +public class AsyncServlet extends HttpServlet { + + //Web 应用线程池,用来处理异步 Servlet + ExecutorService executor = Executors.newSingleThreadExecutor(); + + public void service(HttpServletRequest req, HttpServletResponse resp) { + //1. 调用 startAsync 或者异步上下文 + final AsyncContext ctx = req.startAsync(); + + // 用线程池来执行耗时操作 + executor.execute(new Runnable() { + + @Override + public void run() { + + // 在这里做耗时的操作 + try { + ctx.getResponse().getWriter().println("Handling Async Servlet"); + } catch (IOException e) {} + + //3. 异步 Servlet 处理完了调用异步上下文的 complete 方法 + ctx.complete(); + } + + }); + } +} +``` + +有三个要点: + +1. 通过注解的方式来注册 Servlet,除了 @WebServlet 注解,还需要加上 asyncSupported=true 的属性,表明当前的 Servlet 是一个异步 Servlet。 +2. Web 应用程序需要调用 Request 对象的 startAsync 方法来拿到一个异步上下文 AsyncContext。这个上下文保存了请求和响应对象。 +3. Web 应用需要开启一个新线程来处理耗时的操作,处理完成后需要调用 AsyncContext 的 complete 方法。目的是告诉 Tomcat,请求已经处理完成。 + +这里请你注意,虽然异步 Servlet 允许用更长的时间来处理请求,但是也有超时限制的,默认是 30 秒,如果 30 秒内请求还没处理完,Tomcat 会触发超时机制,向浏览器返回超时错误,如果这个时候你的 Web 应用再调用`ctx.complete`方法,会得到一个 IllegalStateException 异常。 + +### 异步 Servlet 原理 + +通过上面的例子,相信你对 Servlet 的异步实现有了基本的理解。要理解 Tomcat 在这个过程都做了什么事情,关键就是要弄清楚`req.startAsync`方法和`ctx.complete`方法都做了什么。 + +#### startAsync 方法 + +startAsync 方法其实就是创建了一个异步上下文 AsyncContext 对象,AsyncContext 对象的作用是保存请求的中间信息,比如 Request 和 Response 对象等上下文信息。你来思考一下为什么需要保存这些信息呢? + +这是因为 Tomcat 的工作线程在`Request.startAsync`调用之后,就直接结束回到线程池中了,线程本身不会保存任何信息。也就是说一个请求到服务端,执行到一半,你的 Web 应用正在处理,这个时候 Tomcat 的工作线程没了,这就需要有个缓存能够保存原始的 Request 和 Response 对象,而这个缓存就是 AsyncContext。 + +有了 AsyncContext,你的 Web 应用通过它拿到 request 和 response 对象,拿到 Request 对象后就可以读取请求信息,请求处理完了还需要通过 Response 对象将 HTTP 响应发送给浏览器。 + +除了创建 AsyncContext 对象,startAsync 还需要完成一个关键任务,那就是告诉 Tomcat 当前的 Servlet 处理方法返回时,不要把响应发到浏览器,因为这个时候,响应还没生成呢;并且不能把 Request 对象和 Response 对象销毁,因为后面 Web 应用还要用呢。 + +在 Tomcat 中,负责 flush 响应数据的是 CoyoteAdaptor,它还会销毁 Request 对象和 Response 对象,因此需要通过某种机制通知 CoyoteAdaptor,具体来说是通过下面这行代码: + +```java +this.request.getCoyoteRequest().action(ActionCode.ASYNC_START, this); +``` + +你可以把它理解为一个 Callback,在这个 action 方法里设置了 Request 对象的状态,设置它为一个异步 Servlet 请求。 + +我们知道连接器是调用 CoyoteAdapter 的 service 方法来处理请求的,而 CoyoteAdapter 会调用容器的 service 方法,当容器的 service 方法返回时,CoyoteAdapter 判断当前的请求是不是异步 Servlet 请求,如果是,就不会销毁 Request 和 Response 对象,也不会把响应信息发到浏览器。你可以通过下面的代码理解一下,这是 CoyoteAdapter 的 service 方法,我对它进行了简化: + +```java +public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) { + + // 调用容器的 service 方法处理请求 + connector.getService().getContainer().getPipeline(). + getFirst().invoke(request, response); + + // 如果是异步 Servlet 请求,仅仅设置一个标志, + // 否则说明是同步 Servlet 请求,就将响应数据刷到浏览器 + if (request.isAsync()) { + async = true; + } else { + request.finishRequest(); + response.finishResponse(); + } + + // 如果不是异步 Servlet 请求,就销毁 Request 对象和 Response 对象 + if (!async) { + request.recycle(); + response.recycle(); + } +} +``` + +接下来,当 CoyoteAdaptor 的 service 方法返回到 ProtocolHandler 组件时,ProtocolHandler 判断返回值,如果当前请求是一个异步 Servlet 请求,它会把当前 Socket 的协议处理者 Processor 缓存起来,将 SocketWrapper 对象和相应的 Processor 存到一个 Map 数据结构里。 + +```java +private final Map connections = new ConcurrentHashMap<>(); +``` + +之所以要缓存是因为这个请求接下来还要接着处理,还是由原来的 Processor 来处理,通过 SocketWrapper 就能从 Map 里找到相应的 Processor。 + +#### complete 方法 + +接着我们再来看关键的`ctx.complete`方法,当请求处理完成时,Web 应用调用这个方法。那么这个方法做了些什么事情呢?最重要的就是把响应数据发送到浏览器。 + +这件事情不能由 Web 应用线程来做,也就是说`ctx.complete`方法不能直接把响应数据发送到浏览器,因为这件事情应该由 Tomcat 线程来做,但具体怎么做呢? + +我们知道,连接器中的 Endpoint 组件检测到有请求数据达到时,会创建一个 SocketProcessor 对象交给线程池去处理,因此 Endpoint 的通信处理和具体请求处理在两个线程里运行。 + +在异步 Servlet 的场景里,Web 应用通过调用`ctx.complete`方法时,也可以生成一个新的 SocketProcessor 任务类,交给线程池处理。对于异步 Servlet 请求来说,相应的 Socket 和协议处理组件 Processor 都被缓存起来了,并且这些对象都可以通过 Request 对象拿到。 + +讲到这里,你可能已经猜到`ctx.complete`是如何实现的了: + +```java +public void complete() { + // 检查状态合法性,我们先忽略这句 + check(); + + // 调用 Request 对象的 action 方法,其实就是通知连接器,这个异步请求处理完了 +request.getCoyoteRequest().action(ActionCode.ASYNC_COMPLETE, null); + +} +``` + +我们可以看到 complete 方法调用了 Request 对象的 action 方法。而在 action 方法里,则是调用了 Processor 的 processSocketEvent 方法,并且传入了操作码 OPEN_READ。 + +```java +case ASYNC_COMPLETE: { + clearDispatches(); + if (asyncStateMachine.asyncComplete()) { + processSocketEvent(SocketEvent.OPEN_READ, true); + } + break; +} +``` + +我们接着看 processSocketEvent 方法,它调用 SocketWrapper 的 processSocket 方法: + +```java +protected void processSocketEvent(SocketEvent event, boolean dispatch) { + SocketWrapperBase socketWrapper = getSocketWrapper(); + if (socketWrapper != null) { + socketWrapper.processSocket(event, dispatch); + } +} +``` + +而 SocketWrapper 的 processSocket 方法会创建 SocketProcessor 任务类,并通过 Tomcat 线程池来处理: + +```java +public boolean processSocket(SocketWrapperBase socketWrapper, + SocketEvent event, boolean dispatch) { + + if (socketWrapper == null) { + return false; + } + + SocketProcessorBase sc = processorCache.pop(); + if (sc == null) { + sc = createSocketProcessor(socketWrapper, event); + } else { + sc.reset(socketWrapper, event); + } + // 线程池运行 + Executor executor = getExecutor(); + if (dispatch && executor != null) { + executor.execute(sc); + } else { + sc.run(); + } +} +``` + +请你注意 createSocketProcessor 函数的第二个参数是 SocketEvent,这里我们传入的是 OPEN_READ。通过这个参数,我们就能控制 SocketProcessor 的行为,因为我们不需要再把请求发送到容器进行处理,只需要向浏览器端发送数据,并且重新在这个 Socket 上监听新的请求就行了。 + +## 参考资料 + +- **官方** + - [Tomcat 官方网站](http://tomcat.apache.org/) + - [Tomcat Wiki](http://wiki.apache.org/tomcat/FrontPage) + - [Tomee 官方网站](http://tomee.apache.org/) +- **教程** + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" new file mode 100644 index 00000000..3beb96e8 --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" @@ -0,0 +1,156 @@ +--- +title: Tomcat优化 +date: 2022-02-17 22:34:30 +order: 04 +categories: + - Java + - JavaEE + - 服务器 + - Tomcat +tags: + - Java + - JavaWeb + - 服务器 + - Tomcat +permalink: /pages/f9e1e6/ +--- + +# Tomcat 优化 + +## Tomcat 启动优化 + +如果 Tomcat 启动比较慢,可以考虑一些优化点 + +### 清理 Tomcat + +- **清理不必要的 Web 应用**:首先我们要做的是删除掉 webapps 文件夹下不需要的工程,一般是 host-manager、example、doc 等这些默认的工程,可能还有以前添加的但现在用不着的工程,最好把这些全都删除掉。 +- **清理 XML 配置文件**:Tomcat 在启动时会解析所有的 XML 配置文件,解析 XML 较为耗时,所以应该尽量保持配置文件的简洁。 +- **清理 JAR 文件**:JVM 的类加载器在加载类时,需要查找每一个 JAR 文件,去找到所需要的类。如果删除了不需要的 JAR 文件,查找的速度就会快一些。这里请注意:**Web 应用中的 lib 目录下不应该出现 Servlet API 或者 Tomcat 自身的 JAR**,这些 JAR 由 Tomcat 负责提供。 +- **清理其他文件**:及时清理日志,删除 logs 文件夹下不需要的日志文件。同样还有 work 文件夹下的 catalina 文件夹,它其实是 Tomcat 把 JSP 转换为 Class 文件的工作目录。有时候我们也许会遇到修改了代码,重启了 Tomcat,但是仍没效果,这时候便可以删除掉这个文件夹,Tomcat 下次启动的时候会重新生成。 + +### 禁止 Tomcat TLD 扫描 + +Tomcat 为了支持 JSP,在应用启动的时候会扫描 JAR 包里面的 TLD 文件,加载里面定义的标签库。所以在 Tomcat 的启动日志里,你可能会碰到这种提示: + +> At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time. + +Tomcat 的意思是,我扫描了你 Web 应用下的 JAR 包,发现 JAR 包里没有 TLD 文件。我建议配置一下 Tomcat 不要去扫描这些 JAR 包,这样可以提高 Tomcat 的启动速度,并节省 JSP 编译时间。 + +如何配置不去扫描这些 JAR 包呢,这里分两种情况: + +- 如果你的项目没有使用 JSP 作为 Web 页面模板,而是使用 Velocity 之类的模板引擎,你完全可以把 TLD 扫描禁止掉。方法是,找到 Tomcat 的`conf/`目录下的`context.xml`文件,在这个文件里 Context 标签下,加上**JarScanner**和**JarScanFilter**子标签,像下面这样。 + + ```xml + + + + + + ``` + +- 如果你的项目使用了 JSP 作为 Web 页面模块,意味着 TLD 扫描无法避免,但是我们可以通过配置来告诉 Tomcat,只扫描那些包含 TLD 文件的 JAR 包。方法是,找到 Tomcat 的`conf/`目录下的`catalina.properties`文件,在这个文件里的 jarsToSkip 配置项中,加上你的 JAR 包。 + + ``` + tomcat.util.scan.StandardJarScanFilter.jarsToSkip=xxx.jar + ``` + +### 关闭 WebSocket 支持 + +Tomcat 会扫描 WebSocket 注解的 API 实现,比如 `@ServerEndpoint` 注解的类。如果不需要使用 WebSockets 就可以关闭它。具体方法是,找到 Tomcat 的 `conf/` 目录下的 `context.xml` 文件,给 `Context` 标签加一个 **`containerSciFilter`** 的属性: + +```xml + +... + +``` + +更进一步,如果你不需要 WebSockets 这个功能,你可以把 Tomcat `lib` 目录下的 `websocket-api.jar` 和 `tomcat-websocket.jar` 这两个 JAR 文件删除掉,进一步提高性能。 + +### 关闭 JSP 支持 + +如果不需要使用 JSP,可以关闭 JSP 功能: + +```xml + +... + +``` + +如果要同时关闭 WebSocket 和 Jsp,可以这样配置: + +```xml + +... + +``` + +### 禁止扫描 Servlet 注解 + +Servlet 3.0 引入了注解 Servlet,Tomcat 为了支持这个特性,会在 Web 应用启动时扫描你的类文件,因此如果你没有使用 Servlet 注解这个功能,可以告诉 Tomcat 不要去扫描 Servlet 注解。具体配置方法是,在你的 Web 应用的`web.xml`文件中,设置``元素的属性`metadata-complete="true"`,像下面这样。 + +```xml + + +``` + +`metadata-complete` 的意思是,`web.xml` 里配置的 Servlet 是完整的,不需要再去库类中找 Servlet 的定义。 + +### 配置 Web-Fragment 扫描 + +Servlet 3.0 还引入了“Web 模块部署描述符片段”的 `web-fragment.xml`,这是一个部署描述文件,可以完成 `web.xml` 的配置功能。而这个 `web-fragment.xml` 文件必须存放在 JAR 文件的 `META-INF` 目录下,而 JAR 包通常放在 `WEB-INF/lib` 目录下,因此 Tomcat 需要对 JAR 文件进行扫描才能支持这个功能。 + +可以通过配置 `web.xml` 里面的 `` 元素直接指定了哪些 JAR 包需要扫描 `web fragment`,如果 `` 元素是空的, 则表示不需要扫描,像下面这样。 + +```xml + +... + +... + +``` + +### 随机数熵源优化 + +Tomcat 7 以上的版本依赖 Java 的 SecureRandom 类来生成随机数,比如 Session ID。而 JVM 默认使用阻塞式熵源(`/dev/random`), 在某些情况下就会导致 Tomcat 启动变慢。当阻塞时间较长时, 你会看到这样一条警告日志: + +``` + org.apache.catalina.util.SessionIdGenerator createSecureRandom +INFO: Creation of SecureRandom instance for session ID generation using [SHA1PRNG] took [8152] milliseconds. +``` + +解决方案是通过设置,让 JVM 使用非阻塞式的熵源。 + +我们可以设置 JVM 的参数: + +``` +-Djava.security.egd=file:/dev/./urandom +``` + +或者是设置 `java.security` 文件,位于 `$JAVA_HOME/jre/lib/security` 目录之下: `securerandom.source=file:/dev/./urandom` + +这里请你注意,`/dev/./urandom` 中间有个 `./` 的原因是 Oracle JRE 中的 Bug,Java 8 里面的 SecureRandom 类已经修正这个 Bug。 阻塞式的熵源(`/dev/random`)安全性较高, 非阻塞式的熵源(`/dev/./urandom`)安全性会低一些,因为如果你对随机数的要求比较高, 可以考虑使用硬件方式生成熵源。 + +### 并行启动多个 Web 应用 + +Tomcat 启动的时候,默认情况下 Web 应用都是一个一个启动的,等所有 Web 应用全部启动完成,Tomcat 才算启动完毕。如果在一个 Tomcat 下有多个 Web 应用,为了优化启动速度,你可以配置多个应用程序并行启动,可以通过修改 `server.xml` 中 Host 元素的 `startStopThreads` 属性来完成。`startStopThreads` 的值表示你想用多少个线程来启动你的 Web 应用,如果设成 0 表示你要并行启动 Web 应用,像下面这样的配置。 + +```xml + + ... + + ... + + ... + +``` + +需要注意的是,Engine 元素里也配置了这个参数,这意味着如果你的 Tomcat 配置了多个 Host(虚拟主机),Tomcat 会以并行的方式启动多个 Host。 + +## 参考资料 + +- **官方** + - [Tomcat 官方网站](http://tomcat.apache.org/) + - [Tomcat Wiki](http://wiki.apache.org/tomcat/FrontPage) + - [Tomee 官方网站](http://tomee.apache.org/) +- **教程** + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" new file mode 100644 index 00000000..a841530f --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" @@ -0,0 +1,35 @@ +--- +title: Tomcat 和 Jetty +date: 2022-02-17 22:34:30 +order: 05 +categories: + - Java + - JavaEE + - 服务器 + - Tomcat +tags: + - Java + - JavaWeb + - 服务器 + - Tomcat + - Jetty +permalink: /pages/f37326/ +--- + +## Tomcat 和 Jetty + +Web 容器 Tomcat 或 Jetty,作为重要的系统中间件,连接着浏览器和你的 Web 应用,并且支撑着 Web 程序的运行,可以说,**弄懂了 Tomcat 和 Jetty 的原理,Java Web 开发对你来说就毫无秘密可言**。 + +## Web 容器 + +早期的 Web 应用主要用于浏览新闻等静态页面,HTTP 服务器(比如 Apache、Nginx)向浏览器返回静态 HTML,浏览器负责解析 HTML,将结果呈现给用户。 + +随着互联网的发展,我们已经不满足于仅仅浏览静态页面,还希望通过一些交互操作,来获取动态结果,因此也就需要一些扩展机制能够让 HTTP 服务器调用服务端程序。 + +于是 Sun 公司推出了 Servlet 技术。你可以把 Servlet 简单理解为运行在服务端的 Java 小程序,但是 Servlet 没有 main 方法,不能独立运行,因此必须把它部署到 Servlet 容器中,由容器来实例化并调用 Servlet。 + +而 Tomcat 和 Jetty 就是一个 Servlet 容器。为了方便使用,它们也具有 HTTP 服务器的功能,因此**Tomcat 或者 Jetty 就是一个“HTTP 服务器 + Servlet 容器”,我们也叫它们 Web 容器。** + +其他应用服务器比如 JBoss 和 WebLogic,它们不仅仅有 Servlet 容器的功能,也包含 EJB 容器,是完整的 Java EE 应用服务器。从这个角度看,Tomcat 和 Jetty 算是一个轻量级的应用服务器。 + +在微服务架构日渐流行的今天,开发人员更喜欢稳定的、轻量级的应用服务器,并且应用程序用内嵌的方式来运行 Servlet 容器也逐渐流行起来。之所以选择轻量级,是因为在微服务架构下,我们把一个大而全的单体应用,拆分成一个个功能单一的微服务,在这个过程中,服务的数量必然要增加,但为了减少资源的消耗,并且降低部署的成本,我们希望运行服务的 Web 容器也是轻量级的,Web 容器本身应该消耗较少的内存和 CPU 资源,并且由应用本身来启动一个嵌入式的 Web 容器,而不是通过 Web 容器来部署和启动应用,这样可以降低应用部署的复杂度。 \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" new file mode 100644 index 00000000..6204b0c8 --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" @@ -0,0 +1,36 @@ +--- +title: Tomcat 教程 +date: 2022-02-18 08:53:11 +categories: + - Java + - JavaEE + - 服务器 + - Tomcat +tags: + - Java + - JavaWeb + - 服务器 + - Tomcat +permalink: /pages/33e817/ +hidden: true +index: false +--- + +# Tomcat 教程 + +## 📖 内容 + +- [Tomcat 快速入门](01.Tomcat快速入门.md) +- [Tomcat 连接器](02.Tomcat连接器.md) +- [Tomcat 容器](03.Tomcat容器.md) +- [Tomcat 优化](04.Tomcat优化.md) +- [Tomcat 和 Jetty](05.Tomcat和Jetty.md) + +## 📚 资料 + +- [Tomcat 官网](http://tomcat.apache.org/) +- [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" new file mode 100644 index 00000000..db02b55c --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" @@ -0,0 +1,677 @@ +--- +title: Jetty 快速入门 +date: 2022-02-17 22:34:30 +order: 02 +categories: + - Java + - JavaEE + - 服务器 +tags: + - Java + - JavaWeb + - 服务器 + - Jetty +permalink: /pages/ec364e/ +--- + +# Jetty 快速入门 + +## Jetty 简介 + +**jetty 是什么?** + +jetty 是轻量级的 web 服务器和 servlet 引擎。 + +它的最大特点是:可以很方便的作为**嵌入式服务器**。 + +它是 eclipse 的一个开源项目。不用怀疑,就是你常用的那个 eclipse。 + +它是使用 Java 开发的,所以天然对 Java 支持良好。 + +[官方网址](http://www.eclipse.org/jetty/index.html) + +[github 源码地址](https://github.com/eclipse/jetty.project) + +**什么是嵌入式服务器?** + +以 jetty 来说明,就是只要引入 jetty 的 jar 包,可以通过直接调用其 API 的方式来启动 web 服务。 + +用过 Tomcat、Resin 等服务器的朋友想必不会陌生那一套安装、配置、部署的流程吧,还是挺繁琐的。使用 jetty,就不需要这些过程了。 + +jetty 非常适用于项目的开发、测试,因为非常快捷。如果想用于生产环境,则需要谨慎考虑,它不一定能像成熟的 Tomcat、Resin 等服务器一样支持企业级 Java EE 的需要。 + +## Jetty 的使用 + +我觉得嵌入式启动方式的一个好处在于:可以直接运行项目,无需每次部署都得再配置服务器。 + +jetty 的嵌入式启动使用有两种方式: + +API 方式 + +maven 插件方式 + +### API 方式 + +添加 maven 依赖 + +```xml + + org.eclipse.jetty + jetty-webapp + 9.3.2.v20150730 + test + + + org.eclipse.jetty + jetty-annotations + 9.3.2.v20150730 + test + + + org.eclipse.jetty + apache-jsp + 9.3.2.v20150730 + test + + + org.eclipse.jetty + apache-jstl + 9.3.2.v20150730 + test + +``` + +官方的启动代码 + +```java +public class SplitFileServer +{ + public static void main( String[] args ) throws Exception + { + // 创建Server对象,并绑定端口 + Server server = new Server(); + ServerConnector connector = new ServerConnector(server); + connector.setPort(8090); + server.setConnectors(new Connector[] { connector }); + + // 创建上下文句柄,绑定上下文路径。这样启动后的url就会是:http://host:port/context + ResourceHandler rh0 = new ResourceHandler(); + ContextHandler context0 = new ContextHandler(); + context0.setContextPath("/"); + + // 绑定测试资源目录(在本例的配置目录dir0的路径是src/test/resources/dir0) + File dir0 = MavenTestingUtils.getTestResourceDir("dir0"); + context0.setBaseResource(Resource.newResource(dir0)); + context0.setHandler(rh0); + + // 和上面的例子一样 + ResourceHandler rh1 = new ResourceHandler(); + ContextHandler context1 = new ContextHandler(); + context1.setContextPath("/"); + File dir1 = MavenTestingUtils.getTestResourceDir("dir1"); + context1.setBaseResource(Resource.newResource(dir1)); + context1.setHandler(rh1); + + // 绑定两个资源句柄 + ContextHandlerCollection contexts = new ContextHandlerCollection(); + contexts.setHandlers(new Handler[] { context0, context1 }); + server.setHandler(contexts); + + // 启动 + server.start(); + + // 打印dump时的信息 + System.out.println(server.dump()); + + // join当前线程 + server.join(); + } +} +``` + +直接运行 Main 方法,就可以启动 web 服务。 + +**_注:以上代码在 eclipse 中运行没有问题,如果想在 Intellij 中运行还需要为它指定配置文件。_** + +如果想了解在 Eclipse 和 Intellij 都能运行的通用方法可以参考我的 github 代码示例。 + +我的实现也是参考 springside 的方式。 + +代码行数有点多,不在这里贴代码了。 + +[完整参考代码](https://github.com/dunwu/spring-notes) + +### Maven 插件方式 + +如果你熟悉 maven,那么实在太简单了 + +**_注: Maven 版本必须在 3.3 及以上版本。_** + +(1) 添加 maven 插件 + +```xml + + org.eclipse.jetty + jetty-maven-plugin + 9.3.12.v20160915 + +``` + +(2) 执行 maven 命令: + +``` +mvn jetty:run +``` + +讲真,就是这么简单。jetty 默认会为你创建一个 web 服务,地址为 127.0.0.1:8080。 + +当然,你也可以在插件中配置你的 webapp 环境 + +```xml + + org.eclipse.jetty + jetty-maven-plugin + 9.3.12.v20160915 + + + ${project.basedir}/src/staticfiles + + + + / + ${project.basedir}/src/over/here/web.xml + ${project.basedir}/src/over/here/jetty-env.xml + + + + ${project.basedir}/somewhere/else + + + **/Foo.class + + + + src/mydir + src/myfile.txt + + + + + + src/other-resources + + **/*.xml + **/*.properties + + + **/myspecial.xml + **/myspecial.properties + + + + + +``` + +官方给的 jetty-env.xml 范例 + +```xml + + + + + + + + gargle + 100 + true + + + + + wiggle + 55.0 + true + + + + + jdbc/mydatasource99 + + + org.apache.derby.jdbc.EmbeddedXADataSource + databaseName=testdb99;createDatabase=create + mydatasource99 + + + + + +``` + +## Jetty 的架构 + +### Jetty 架构简介 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201127154145.jpg) + +Jetty Server 就是由多个 Connector(连接器)、多个 Handler(处理器),以及一个线程池组成。 + +跟 Tomcat 一样,Jetty 也有 HTTP 服务器和 Servlet 容器的功能,因此 Jetty 中的 Connector 组件和 Handler 组件分别来实现这两个功能,而这两个组件工作时所需要的线程资源都直接从一个全局线程池 ThreadPool 中获取。 + +Jetty Server 可以有多个 Connector 在不同的端口上监听客户请求,而对于请求处理的 Handler 组件,也可以根据具体场景使用不同的 Handler。这样的设计提高了 Jetty 的灵活性,需要支持 Servlet,则可以使用 ServletHandler;需要支持 Session,则再增加一个 SessionHandler。也就是说我们可以不使用 Servlet 或者 Session,只要不配置这个 Handler 就行了。 + +为了启动和协调上面的核心组件工作,Jetty 提供了一个 Server 类来做这个事情,它负责创建并初始化 Connector、Handler、ThreadPool 组件,然后调用 start 方法启动它们。 + +### Jetty 和 Tomcat 架构区别 + +对比一下 Tomcat 的整体架构图,你会发现 Tomcat 在整体上跟 Jetty 很相似,它们的第一个区别是 Jetty 中没有 Service 的概念,Tomcat 中的 Service 包装了多个连接器和一个容器组件,一个 Tomcat 实例可以配置多个 Service,不同的 Service 通过不同的连接器监听不同的端口;而 Jetty 中 Connector 是被所有 Handler 共享的。 + +第二个区别是,在 Tomcat 中每个连接器都有自己的线程池,而在 Jetty 中所有的 Connector 共享一个全局的线程池。 + +### Connector 组件 + +跟 Tomcat 一样,Connector 的主要功能是对 I/O 模型和应用层协议的封装。I/O 模型方面,最新的 Jetty 9 版本只支持 NIO,因此 Jetty 的 Connector 设计有明显的 Java NIO 通信模型的痕迹。至于应用层协议方面,跟 Tomcat 的 Processor 一样,Jetty 抽象出了 Connection 组件来封装应用层协议的差异。 + +服务端在 NIO 通信上主要完成了三件事情:**监听连接、I/O 事件查询以及数据读写**。因此 Jetty 设计了**Acceptor、SelectorManager 和 Connection 来分别做这三件事情** + +#### Acceptor + +**Acceptor 用于接受请求**。跟 Tomcat 一样,Jetty 也有独立的 Acceptor 线程组用于处理连接请求。在 `Connector` 的实现类 `ServerConnector` 中,有一个 `_acceptors` 的数组,在 Connector 启动的时候, 会根据 `_acceptors` 数组的长度创建对应数量的 Acceptor,而 Acceptor 的个数可以配置。 + +```java +for (int i = 0; i < _acceptors.length; i++) +{ + Acceptor a = new Acceptor(i); + getExecutor().execute(a); +} +``` + +`Acceptor` 是 `ServerConnector` 中的一个内部类,同时也是一个 `Runnable`,`Acceptor` 线程是通过 `getExecutor()` 得到的线程池来执行的,前面提到这是一个全局的线程池。 + +`Acceptor` 通过阻塞的方式来接受连接,这一点跟 Tomcat 也是一样的。 + +```java +public void accept(int acceptorID) throws IOException +{ + ServerSocketChannel serverChannel = _acceptChannel; + if (serverChannel != null && serverChannel.isOpen()) + { + // 这里是阻塞的 + SocketChannel channel = serverChannel.accept(); + // 执行到这里时说明有请求进来了 + accepted(channel); + } +} +``` + +接受连接成功后会调用 `accepted()` 函数,`accepted()` 函数中会将 `SocketChannel` 设置为非阻塞模式,然后交给 `Selector` 去处理,因此这也就到了 `Selector` 的地界了。 + +```java +private void accepted(SocketChannel channel) throws IOException +{ + channel.configureBlocking(false); + Socket socket = channel.socket(); + configure(socket); + // _manager 是 SelectorManager 实例,里面管理了所有的 Selector 实例 + _manager.accept(channel); +} +``` + +**SelectorManager** + +**Jetty 的 `Selector` 由 `SelectorManager` 类管理**,而被管理的 `Selector` 叫作 `ManagedSelector`。`SelectorManager` 内部有一个 `ManagedSelector` 数组,真正干活的是 `ManagedSelector`。咱们接着上面分析,看看在 `SelectorManager` 在 `accept` 方法里做了什么。 + +```java +public void accept(SelectableChannel channel, Object attachment) +{ + // 选择一个 ManagedSelector 来处理 Channel + final ManagedSelector selector = chooseSelector(); + // 提交一个任务 Accept 给 ManagedSelector + selector.submit(selector.new Accept(channel, attachment)); +} +``` + +SelectorManager 从本身的 Selector 数组中选择一个 Selector 来处理这个 Channel,并创建一个任务 Accept 交给 ManagedSelector,ManagedSelector 在处理这个任务主要做了两步: + +第一步,调用 Selector 的 register 方法把 Channel 注册到 Selector 上,拿到一个 SelectionKey。 + +``` + _key = _channel.register(selector, SelectionKey.OP_ACCEPT, this); +``` + +第二步,创建一个 EndPoint 和 Connection,并跟这个 SelectionKey(Channel)绑在一起: + +```java +private void createEndPoint(SelectableChannel channel, SelectionKey selectionKey) throws IOException +{ + //1. 创建 Endpoint + EndPoint endPoint = _selectorManager.newEndPoint(channel, this, selectionKey); + + //2. 创建 Connection + Connection connection = _selectorManager.newConnection(channel, endPoint, selectionKey.attachment()); + + //3. 把 Endpoint、Connection 和 SelectionKey 绑在一起 + endPoint.setConnection(connection); + selectionKey.attach(endPoint); + +} +``` + +这里需要你特别注意的是,ManagedSelector 并没有直接调用 EndPoint 的方法去处理数据,而是通过调用 EndPoint 的方法**返回一个 Runnable,然后把这个 Runnable 扔给线程池执行**,所以你能猜到,这个 Runnable 才会去真正读数据和处理请求。 + +**Connection** + +这个 Runnable 是 EndPoint 的一个内部类,它会调用 Connection 的回调方法来处理请求。Jetty 的 Connection 组件类比就是 Tomcat 的 Processor,负责具体协议的解析,得到 Request 对象,并调用 Handler 容器进行处理。下面我简单介绍一下它的具体实现类 HttpConnection 对请求和响应的处理过程。 + +**请求处理**:HttpConnection 并不会主动向 EndPoint 读取数据,而是向在 EndPoint 中注册一堆回调方法: + +``` +getEndPoint().fillInterested(_readCallback); +``` + +这段代码就是告诉 EndPoint,数据到了你就调我这些回调方法 \_readCallback 吧,有点异步 I/O 的感觉,也就是说 Jetty 在应用层面模拟了异步 I/O 模型。 + +而在回调方法 \_readCallback 里,会调用 EndPoint 的接口去读数据,读完后让 HTTP 解析器去解析字节流,HTTP 解析器会将解析后的数据,包括请求行、请求头相关信息存到 Request 对象里。 + +**响应处理**:Connection 调用 Handler 进行业务处理,Handler 会通过 Response 对象来操作响应流,向流里面写入数据,HttpConnection 再通过 EndPoint 把数据写到 Channel,这样一次响应就完成了。 + +到此你应该了解了 Connector 的工作原理,下面我画张图再来回顾一下 Connector 的工作流程。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201118175805.jpg) + +1. Acceptor 监听连接请求,当有连接请求到达时就接受连接,一个连接对应一个 Channel,Acceptor 将 Channel 交给 ManagedSelector 来处理。 + +2. ManagedSelector 把 Channel 注册到 Selector 上,并创建一个 EndPoint 和 Connection 跟这个 Channel 绑定,接着就不断地检测 I/O 事件。 + +3. I/O 事件到了就调用 EndPoint 的方法拿到一个 Runnable,并扔给线程池执行。 + +4. 线程池中调度某个线程执行 Runnable。 + +5. Runnable 执行时,调用回调函数,这个回调函数是 Connection 注册到 EndPoint 中的。 + +6. 回调函数内部实现,其实就是调用 EndPoint 的接口方法来读数据。 + +7. Connection 解析读到的数据,生成请求对象并交给 Handler 组件去处理。 + +### Handler 组件 + +Jetty 的 Handler 设计是它的一大特色,Jetty 本质就是一个 Handler 管理器,Jetty 本身就提供了一些默认 Handler 来实现 Servlet 容器的功能,你也可以定义自己的 Handler 来添加到 Jetty 中,这体现了“**微内核 + 插件**”的设计思想。 + +**Handler 就是一个接口,它有一堆实现类**,Jetty 的 Connector 组件调用这些接口来处理 Servlet 请求。 + +```java +public interface Handler extends LifeCycle, Destroyable +{ + // 处理请求的方法 + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) + throws IOException, ServletException; + + // 每个 Handler 都关联一个 Server 组件,被 Server 管理 + public void setServer(Server server); + public Server getServer(); + + // 销毁方法相关的资源 + public void destroy(); +} +``` + +方法说明: + +- `Handler` 的 `handle` 方法跟 Tomcat 容器组件的 service 方法一样,它有 `ServletRequest` 和 `ServeletResponse` 两个参数。 +- 因为任何一个 `Handler` 都需要关联一个 `Server` 组件,`Handler` 需要被 `Server` 组件来管理。`Handler` 通过 `setServer` 和 `getServer` 方法绑定 `Server`。 +- `Handler` 会加载一些资源到内存,因此通过设置 `destroy` 方法来销毁。 + +#### Handler 继承关系 + +Handler 只是一个接口,完成具体功能的还是它的子类。那么 Handler 有哪些子类呢?它们的继承关系又是怎样的?这些子类是如何实现 Servlet 容器功能的呢? + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201118181025.png) + +在 AbstractHandler 之下有 AbstractHandlerContainer,为什么需要这个类呢?这其实是个过渡,为了实现链式调用,一个 Handler 内部必然要有其他 Handler 的引用,所以这个类的名字里才有 Container,意思就是这样的 Handler 里包含了其他 Handler 的引用。 + +HandlerWrapper 和 HandlerCollection 都是 Handler,但是这些 Handler 里还包括其他 Handler 的引用。不同的是,HandlerWrapper 只包含一个其他 Handler 的引用,而 HandlerCollection 中有一个 Handler 数组的引用。 + +HandlerWrapper 有两个子类:Server 和 ScopedHandler。 + +- Server 比较好理解,它本身是 Handler 模块的入口,必然要将请求传递给其他 Handler 来处理,为了触发其他 Handler 的调用,所以它是一个 HandlerWrapper。 +- ScopedHandler 也是一个比较重要的 Handler,实现了“具有上下文信息”的责任链调用。为什么我要强调“具有上下文信息”呢?那是因为 Servlet 规范规定 Servlet 在执行过程中是有上下文的。那么这些 Handler 在执行过程中如何访问这个上下文呢?这个上下文又存在什么地方呢?答案就是通过 ScopedHandler 来实现的。 + +HandlerCollection 其实维护了一个 Handler 数组。这是为了同时支持多个 Web 应用,如果每个 Web 应用有一个 Handler 入口,那么多个 Web 应用的 Handler 就成了一个数组,比如 Server 中就有一个 HandlerCollection,Server 会根据用户请求的 URL 从数组中选取相应的 Handler 来处理,就是选择特定的 Web 应用来处理请求。 + +Handler 可以分成三种类型: + +- 第一种是**协调 Handler**,这种 Handler 负责将请求路由到一组 Handler 中去,比如 HandlerCollection,它内部持有一个 Handler 数组,当请求到来时,它负责将请求转发到数组中的某一个 Handler。 +- 第二种是**过滤器 Handler**,这种 Handler 自己会处理请求,处理完了后再把请求转发到下一个 Handler,比如图上的 HandlerWrapper,它内部持有下一个 Handler 的引用。需要注意的是,所有继承了 HandlerWrapper 的 Handler 都具有了过滤器 Handler 的特征,比如 ContextHandler、SessionHandler 和 WebAppContext 等。 +- 第三种是**内容 Handler**,说白了就是这些 Handler 会真正调用 Servlet 来处理请求,生成响应的内容,比如 ServletHandler。如果浏览器请求的是一个静态资源,也有相应的 ResourceHandler 来处理这个请求,返回静态页面。 + +#### 实现 Servlet 规范 + +ServletHandler、ContextHandler 以及 WebAppContext 等,它们实现了 Servlet 规范。 + +Servlet 规范中有 Context、Servlet、Filter、Listener 和 Session 等,Jetty 要支持 Servlet 规范,就需要有相应的 Handler 来分别实现这些功能。因此,Jetty 设计了 3 个组件:ContextHandler、ServletHandler 和 SessionHandler 来实现 Servle 规范中规定的功能,而**WebAppContext 本身就是一个 ContextHandler**,另外它还负责管理 ServletHandler 和 SessionHandler。 + +ContextHandler 会创建并初始化 Servlet 规范里的 ServletContext 对象,同时 ContextHandler 还包含了一组能够让你的 Web 应用运行起来的 Handler,可以这样理解,Context 本身也是一种 Handler,它里面包含了其他的 Handler,这些 Handler 能处理某个特定 URL 下的请求。比如,ContextHandler 包含了一个或者多个 ServletHandler。 + +ServletHandler 实现了 Servlet 规范中的 Servlet、Filter 和 Listener 的功能。ServletHandler 依赖 FilterHolder、ServletHolder、ServletMapping、FilterMapping 这四大组件。FilterHolder 和 ServletHolder 分别是 Filter 和 Servlet 的包装类,每一个 Servlet 与路径的映射会被封装成 ServletMapping,而 Filter 与拦截 URL 的映射会被封装成 FilterMapping。 + +SessionHandler 用来管理 Session。除此之外 WebAppContext 还有一些通用功能的 Handler,比如 SecurityHandler 和 GzipHandler,同样从名字可以知道这些 Handler 的功能分别是安全控制和压缩 / 解压缩。 + +WebAppContext 会将这些 Handler 构建成一个执行链,通过这个链会最终调用到我们的业务 Servlet。 + +## Jetty 的线程策略 + +### 传统 Selector 编程模型 + +常规的 NIO 编程思路是,将 I/O 事件的侦测和请求的处理分别用不同的线程处理。具体过程是: + +启动一个线程,在一个死循环里不断地调用 select 方法,检测 Channel 的 I/O 状态,一旦 I/O 事件达到,比如数据就绪,就把该 I/O 事件以及一些数据包装成一个 Runnable,将 Runnable 放到新线程中去处理。 + +在这个过程中按照职责划分,有两个线程在干活,一个是 I/O 事件检测线程,另一个是 I/O 事件处理线程。这样的好处是它们互不干扰和阻塞对方。 + +### Jetty 的 Selector 编程模型 + +将 I/O 事件检测和业务处理这两种工作分开的思路也有缺点:当 Selector 检测读就绪事件时,数据已经被拷贝到内核中的缓存了,同时 CPU 的缓存中也有这些数据了,我们知道 CPU 本身的缓存比内存快多了,这时当应用程序去读取这些数据时,如果用另一个线程去读,很有可能这个读线程使用另一个 CPU 核,而不是之前那个检测数据就绪的 CPU 核,这样 CPU 缓存中的数据就用不上了,并且线程切换也需要开销。 + +因此 Jetty 的 Connector 做了一个大胆尝试,那就是**把 I/O 事件的生产和消费放到同一个线程来处理**,如果这两个任务由同一个线程来执行,如果执行过程中线程不阻塞,操作系统会用同一个 CPU 核来执行这两个任务,这样就能利用 CPU 缓存了。 + +#### ManagedSelector + +ManagedSelector 的本质就是一个 Selector,负责 I/O 事件的检测和分发。为了方便使用,Jetty 在 Java 原生的 Selector 上做了一些扩展,就变成了 ManagedSelector,我们先来看看它有哪些成员变量: + +```java +public class ManagedSelector extends ContainerLifeCycle implements Dumpable +{ + // 原子变量,表明当前的 ManagedSelector 是否已经启动 + private final AtomicBoolean _started = new AtomicBoolean(false); + + // 表明是否阻塞在 select 调用上 + private boolean _selecting = false; + + // 管理器的引用,SelectorManager 管理若干 ManagedSelector 的生命周期 + private final SelectorManager _selectorManager; + + //ManagedSelector 不止一个,为它们每人分配一个 id + private final int _id; + + // 关键的执行策略,生产者和消费者是否在同一个线程处理由它决定 + private final ExecutionStrategy _strategy; + + //Java 原生的 Selector + private Selector _selector; + + //"Selector 更新任务 " 队列 + private Deque _updates = new ArrayDeque<>(); + private Deque _updateable = new ArrayDeque<>(); + + ... +} +``` + +这些成员变量中其他的都好理解,就是“Selector 更新任务”队列`_updates`和执行策略`_strategy`可能不是很直观。 + +#### SelectorUpdate 接口 + +为什么需要一个“Selector 更新任务”队列呢,对于 Selector 的用户来说,我们对 Selector 的操作无非是将 Channel 注册到 Selector 或者告诉 Selector 我对什么 I/O 事件感兴趣,那么这些操作其实就是对 Selector 状态的更新,Jetty 把这些操作抽象成 SelectorUpdate 接口。 + +``` +/** + * A selector update to be done when the selector has been woken. + */ +public interface SelectorUpdate +{ + void update(Selector selector); +} +``` + +这意味着如果你不能直接操作 ManageSelector 中的 Selector,而是需要向 ManagedSelector 提交一个任务类,这个类需要实现 SelectorUpdate 接口 update 方法,在 update 方法里定义你想要对 ManagedSelector 做的操作。 + +比如 Connector 中 Endpoint 组件对读就绪事件感兴趣,它就向 ManagedSelector 提交了一个内部任务类 ManagedSelector.SelectorUpdate: + +``` +_selector.submit(_updateKeyAction); +``` + +这个`_updateKeyAction`就是一个 SelectorUpdate 实例,它的 update 方法实现如下: + +``` +private final ManagedSelector.SelectorUpdate _updateKeyAction = new ManagedSelector.SelectorUpdate() +{ + @Override + public void update(Selector selector) + { + // 这里的 updateKey 其实就是调用了 SelectionKey.interestOps(OP_READ); + updateKey(); + } +}; +``` + +我们看到在 update 方法里,调用了 SelectionKey 类的 interestOps 方法,传入的参数是`OP_READ`,意思是现在我对这个 Channel 上的读就绪事件感兴趣了。 + +那谁来负责执行这些 update 方法呢,答案是 ManagedSelector 自己,它在一个死循环里拉取这些 SelectorUpdate 任务类逐个执行。 + +#### Selectable 接口 + +那 I/O 事件到达时,ManagedSelector 怎么知道应该调哪个函数来处理呢?其实也是通过一个任务类接口,这个接口就是 Selectable,它返回一个 Runnable,这个 Runnable 其实就是 I/O 事件就绪时相应的处理逻辑。 + +``` +public interface Selectable +{ + // 当某一个 Channel 的 I/O 事件就绪后,ManagedSelector 会调用的回调函数 + Runnable onSelected(); + + // 当所有事件处理完了之后 ManagedSelector 会调的回调函数,我们先忽略。 + void updateKey(); +} +``` + +ManagedSelector 在检测到某个 Channel 上的 I/O 事件就绪时,也就是说这个 Channel 被选中了,ManagedSelector 调用这个 Channel 所绑定的附件类的 onSelected 方法来拿到一个 Runnable。 + +这句话有点绕,其实就是 ManagedSelector 的使用者,比如 Endpoint 组件在向 ManagedSelector 注册读就绪事件时,同时也要告诉 ManagedSelector 在事件就绪时执行什么任务,具体来说就是传入一个附件类,这个附件类需要实现 Selectable 接口。ManagedSelector 通过调用这个 onSelected 拿到一个 Runnable,然后把 Runnable 扔给线程池去执行。 + +那 Endpoint 的 onSelected 是如何实现的呢? + +``` +@Override +public Runnable onSelected() +{ + int readyOps = _key.readyOps(); + + boolean fillable = (readyOps & SelectionKey.OP_READ) != 0; + boolean flushable = (readyOps & SelectionKey.OP_WRITE) != 0; + + // return task to complete the job + Runnable task= fillable + ? (flushable + ? _runCompleteWriteFillable + : _runFillable) + : (flushable + ? _runCompleteWrite + : null); + + return task; +} +``` + +上面的代码逻辑很简单,就是读事件到了就读,写事件到了就写。 + +#### ExecutionStrategy + +铺垫了这么多,终于要上主菜了。前面我主要介绍了 ManagedSelector 的使用者如何跟 ManagedSelector 交互,也就是如何注册 Channel 以及 I/O 事件,提供什么样的处理类来处理 I/O 事件,接下来我们来看看 ManagedSelector 是如何统一管理和维护用户注册的 Channel 集合。再回到今天开始的讨论,ManagedSelector 将 I/O 事件的生产和消费看作是生产者消费者模式,为了充分利用 CPU 缓存,生产和消费尽量放到同一个线程处理,那这是如何实现的呢?Jetty 定义了 ExecutionStrategy 接口: + +``` +public interface ExecutionStrategy +{ + // 只在 HTTP2 中用到,简单起见,我们先忽略这个方法。 + public void dispatch(); + + // 实现具体执行策略,任务生产出来后可能由当前线程执行,也可能由新线程来执行 + public void produce(); + + // 任务的生产委托给 Producer 内部接口, + public interface Producer + { + // 生产一个 Runnable(任务) + Runnable produce(); + } +} +``` + +我们看到 ExecutionStrategy 接口比较简单,它将具体任务的生产委托内部接口 Producer,而在自己的 produce 方法里来实现具体执行逻辑,**也就是生产出来的任务要么由当前线程执行,要么放到新线程中执行**。Jetty 提供了一些具体策略实现类:ProduceConsume、ProduceExecuteConsume、ExecuteProduceConsume 和 EatWhatYouKill。它们的区别是: + +- ProduceConsume:任务生产者自己依次生产和执行任务,对应到 NIO 通信模型就是用一个线程来侦测和处理一个 ManagedSelector 上所有的 I/O 事件,后面的 I/O 事件要等待前面的 I/O 事件处理完,效率明显不高。通过图来理解,图中绿色表示生产一个任务,蓝色表示执行这个任务。 + +![img](data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAAvUAAAA1CAYAAADBNRQQAAAUuUlEQVR42u2dCXgV5dXHyUIWQAJJQEF2P7e6VgG1qIUY9iqI8hFQaB/7tdbuaqmtC1pEUZa6VEVFK2KlSkEp2ycoi6IIQsUgAgkEEnYIYctCCLnz9py5Z8Kb4d4scO/NzJ3/eZ7/c+fOzL25mXPueX9z5sx7GzWCwWAwGAwGg8FgMFjUWgwpVlOcpngHKq4Oive4Ah2PuAA+dvJnj4dvoTOIb+QvKFz+ig2yjFhzrr8sxQD1YF4CeuvLnUBKJCWRkh2qJJsSteVkqJoStWOky+nHq0kNvodfoUC5INH2HPkLCnUuTQySSxFrzvQX80xjYRuAPcwzQG/BPH8RmpFSSC1JqaQ0UrqDlCZKFbXUlKptd9rnbohjZB0X9mdzES+3EDnZv2kBfAnfQsFyQAvbYyryFxQif+kx1kLLoym2eEOsOctf7J9zNMAH2MOi3qwKfYJURlOe29xfOV2TN5K+668mZvdTz37dRz39VR81YlolZNPwv1eQTqisN46rYa8XK/JvG7f4dtL6fmrCur5q/Jre8CVUS3yXqWGvHeP4bo/8BUUo1tog1twj8ldTG9jDYFEL9fFyJstntee6JVFN2uCHPk5SY1fcgoEnyECU9Wa5yppaqoZOOcyJ7SK3+HZidl/1zNre6skvMuFLqMb45hPWO18p4vi+DPkLilCsXYRYcxXUt5TCZWMpZMJgUQv1CdJy04rUyS2JiisPz6ztY0LfY5/0xMATbCDiKj1Vlu54aT8ntq6uGITW+6tK477srR5f2gu+hGqI7zL1v68eUXe8uIfjuwfyFxShWOuKWHMV1J8rbVNJAvUxaMOBRaPFySUprtK3JV3iikRlXk70Vx6e+DRD/WnBzRh4ahiIuEo/5IVdnNh+6J5BqLdZVXr4IwAPFCy+T5hXobhyevtzBRzf/ZC/oAjFWi/EmqugvoNU65OlOwFQD4taqE+SYOegv9ItiWrCN33VU6t6qzFLMtToORgUg0K92XpziAaifE5s/d0B9f5e+ieWZ6iH5sG3UHDQGja1RN358gE1eHIex/cQ5C8oQrHWH7HmKqjvIjfSWi04gHpYVJrVT893i3ciXeuGRGX1CHJ7Bl9OfHDWjRh4aqwuFdJAtI0T2yC3VJaeWp1ptt4AeKBgsnqcubVs0KQtHN8jkL+gCMXaYMSaq6D+QlJruWEWUA+LeqhPkzPZbu5KVJnq0cU91QMze2DgiZJKpr2y9IcPboIvoVpAa5+6bUIOx/dI5C8oQrF2B2LNVVB/sUB9M0A9zAtQz5elLiB1d1OienJlpnpkUU91//sYFGuD+kGTtnJiG+oqqOfK0mxUMaFaQOtvewm0NnN8j0L+giIUa0MRa66C+kvkZllAPQxQj0QFqAfUQ4B65C/EGqAeUA+DAeqRqAD1gHoIUI/8BahHrAHqYTBAPRIVoB6CAPUQoB6xBqiHwQD1gHpAPQSoR/6CAPWAekA9DFCPRAWoB9RDgHrkL0A9Yg1QD4MB6pGoAPWAeghQj/wFqEesAephMGdC/e6yTcpuJ3ylanfpJrV0/2sBXzNn51i1q3Sj+sf237kuUeUcME77fw3DUEWlSq3KN9SDc3ynveanMyrVd/sMNX2N4Tqor49/n9/8I/V54TuqsHy7Kq8sNX08d9fTgPowxda4xf64KjlhqJ2HDfXhep8aNR1QH674fiV3mPr2yCJ1rKJQHa8sVttK1qoZ2+/3HGjVN07fXGWo/EOGOn7SUNvp8bUvfZ6E+jMZK1n/3vWkmUuX7X/dkbF2JmMi6+fvVapv9xpq7U4DUB8Ji4mJKYiPj1c16Cva7be8TI99nPCZY2Nj/yKfp6ttUzKtX06Po8Hz4YH6bSVr1Iajn5jKOfY5QV2JuX7VwZnVoI8TVOnJw+a2fxX82bVQ/5+dSi3bapj6LM9QBYf96w9RImOIt/bnhLZ+jz/RLdrsXqivi3+/PbLYXFdQkq3WHZ6viiuKzOdzdz0FqA9xbI3/xGcOnPuOKbVwk6Fy5bUrthmA+jDEN+eu/ce3mus20/bc4pWq0qggKDuupuXd60mor0uc/vNr/7q8g4ZanGOow2X+XPjWV4Znob4uudTSq1tGqLKTR8ztXx+a62ior+uYaGllvn87xwSgPjKA/HJcXNxMFgH+BgH5NdY62v60C6CenXcFfd6PeD3/T+E4/+E/HURxDlSsBHcTUivS/5Cu++ums0tU9orVW3k/M6GDKxE8IC6lKgMPgrq5Geofnl+9+nAXaUuhf9ukZT71m1mVqrSiegXDzVBfm3+t5wz01j5/3/Z/tM5nVqIA9aGLLV63rchQJ32G+uVM/z53v31qn1/9C1Af6vjmK05sa4o+qNrnswPTzHWfF073JNTXFqc/fqdSlVN1fs9RVXUF6fcf+MztXLn3KtTXFmv6Nj55PFpxwBVQX5e8Zen5z3zqBMXGkePhgfrhb52kcbCC/XUp6TzSOaQEh3NZIIXtBOQxhmKC43ts6x0N9XQykqtfXQgD1MdoUG85p7GmBAeKP1eSnLnyGeyFFtSfCdgHS1Qsq1L7+taR6sOdf6mqTljrownqWUty/dte/sKnfvF+pVq6xTC1eb8RdVBv9+/CPZPM5SX7Xq22T8nJQ+aVGUB96GKLYYlt477q8bRgo3+fx//fB6gPcXyvLHzXrKj+M//Bqu2zd4wxt688OANQHyBO/zjXZ7aGzfnWVw30uNix8wigPlisWesW7J5oFsL46rYbod4eD9Y6HhuPlRvq3bWGWZwINdSbQH8K6r9HakNqId0JTuayxtJFEa+BftiuLNQI9bR+FGkOQXQRPS6k9bdqoP066X1azKTtObT/57Ipg5ZX0Lqj3MoT4L2H8HraXsL7yPteqm1Pp/edTtsO8t+l5ddIE22V+kdp3V/ptR+GAep1oI8XhyQKMCdLJbypQ9VMgrwt6WLSDZM3+qG+vmAfLFFN2ZKlfEalWYF4MWdwtW3bS7+Oykr9Qf9VVPXQ3Orbpqz0RR3U2/3LsLOCKpbT8u6r2mfq1lHma/eW5QLqQxhbfCmb2xomL6++n3Xy+OtZqNSHM3+xGL52lK0399FB3+uV+mA5kMUno++t879WB32vQ32gWOP44vs2+GSSr3i6tVIfKB7W7PDfW8FXF0MN9TrQs+/IX5eT2pPSpFrvZC5LFn5M1AA/bGBfI9QTVB+WdpwxtHyAQVvOPhoxxNPzPaRiWv6M9hvLlX167qP3W0TLv6B1MwTGH5D3vZ63kzbS8oO8npYL+aSgqimceuQF1N/hXnmuypPKg/TUDwgT1Mdq1Xl2hnKpbuAkYIH9mSQqvhQ9d9d4U4v3vkD9p3nm+q3Fq097TTRAPfcuz/3OZ4p7RfmmILaPc04H92iA+vr41+pB5t5RtsV7XwLUhym2LL0iMZa9R0X9cQoH1NcnvjceXVbVUsfVVK/eKFufOOX2i0qf/3V88+RdHr5Rti6xxrnzYPkOgvzbXAP1dYmHl1f46ATGqDoBCAvUE9DzGJj1xnH215Uu5jI72EcO6rntRavMjxWw/pEF9fJ8jHYT7jfcpy9AbBr3vdO6Q1JF7k/v86o4xHrfafI+fLZ1vfzdf+jdN3zy0ABQHy9nVk1dHDw3ni3UBzKu0HK1NhqhPpBxchoToPUhGqC+Pv6dsmW4yi9dZ+6z6dintG4goD5MsWXdjMhVvkKqilk99oD68MX3wj2TzVlwuK2Mb2KcteNhz89+U1uccivOPII9hjiGutnZ3m2/qS3WFu193vw+v5c/uureJDdAfW3xwPf6cDsWx4H12nBBPfsta2op++v7LuaypAaDeoZmDc6Hyr4/sUF9C9mlKcG3wVV4guy/WaJ9Vsl+18h+7fjv0bYJ9LiA9q+U7Sn0/KeyPNjWU/9mhKFer9LzyUZLUmvp4Wonl306OEjtNHWQm2T5xOmmSev7mWDFYH8miWo13bnPFVnW/N3Pqre333faTT/RBPVjF/mnqmRxsuLpKtm4V5AvM0cb1NfVvzPy7zf7Q3lQ+qpodtAYANSffWzd826lWrfbP3jya++b6Y3jFA6or0/+OtVe9mOz57mg5BtPQn19cqAlbrng2cA4P/CUhl6E+tpijU8WeQrL2TseM8WQz7aleJX53KlQX1s88D0/fHMstw2O+7jSFM/eVUzbeZl77UMD9f4qPY+BwpMXkToJjzmZy9oIP6YKT1r3AFj99Q1zo2wgqOfWG+01raRlZz1B9mS7ZIrFIQzxpGxa9yy9192k+RbUk+6T5b62z/N4A0E9H/zm0rfFN5+erwVOR4epvaij3CR7FUP9xOyzg/r6zNccjT31rFy50/+hedHfUx9Ib2/7pdkHyoMS30SIH58KX2yNeucUHM3K9ven4senwhff31HLTWF5gXkVSl/Pv8nAYF+Xk1cv3Cirx+mL1Gqxg6Y15Ef7lSW2ict8noT62mKNf+OjJnNarNU1Hj7JNWr8v5771BdaqCe/kb+ulWktO2v841Quayv8mCY8mSxt7I6E+qO2OfAPajfMWnYL6Xdc9aZtn3KfvrS1WO87T4P6DIH0cfob0D7LGrBS30wq9a1kCqXzbZVxp+h87aTjglBV6gH1p+705znEvQb1L+UOoZluikygfzPvHvyibJhjq2pWiRU+zx2nhoB66zcY5u+eULXujbyfmCdVxRUHcaNsgDh96mN/3ltVYAR8/SMLAPWBxG03Mwv+VCVrZjGe3pKfu3H2G46H33/oU098VF083SlX6nn53vfCXqlv53AuO0/4saUbKvVHba97RCB7PB903pf2KZUfiOL3+Jgr9Vyhp6eX8d+h5xXyt3pI//wGUhkt38sQz7PcWFNXNkBPfYLcVX2OtBmliXNaO1DpcjbYRs4Q+fj2MHvqAfVnlcDmbvCdNn2XV6CepyxlKyrfSVXNJdWUfXghoD6EscVV+TKaFvBEpaGWbz1dwX7BEVB/5vH9Lv36NQM8V1HXHppj/u7GgePbzdd+QTOUAOoDxylPXcnHjX81lG/mtn5wiHupR06vBNTXQW6d/aamMTEiPfVvlLG/riZ1EWg+1+Fclq4BfRMpase7BeobE2A/Q+tPWCDON8rKP8fWjbbtsrZJG85EWd4p+3Sh56u1fXK4VacBZ79JlDOrpuKU5nJVwYlqIQHUTqYJvcEC+lBNaelFqJ/6pT+B8cDlNaj/Dw04wYxbcgD1oYutMQt9NV7G5t5WQH3o89c86n+2fg2bjX9N1g/0AwH1QXIgV2etqVYt48q9W27oBtSHZ0wMN9Rrs99cIcXL1gLMTuayc6TjI1mb1jIu3D9CFWprLFDZuoZtbbR1neWMS7d0Wd8QpoO9VbHX56p3opLkxCNFji1fmrqegb6+VfpIK1oGRTdBfaQEqIcaAurPRNx2w78C+sLmW5G/6ii+eXL0v33mzd0ujzXH51Kvx5oO9tKJ0E5uPm3mcC6z5qhPsFXoYxvBGgTs9V+VjdfU2CHSP5M1BWdr6xdlnQ70gHpAPQQ5AeqRvzwda4B6F4G9Vhhurk0R6VQui9cYEkDvALB3k+Ll7DBdbpbtXt+2GyQqQD2gHgLUI38B6hFrTgV7mfnmXKnSN3Yhq8UAr2F1sdOgHokKUA+ohwD1yF8QoD5aVAPUw2CAeiQqQD2gHgLUI38B6hFrgHoYDFCPRAWoB9RDgHrkL0A9Yg1QD4MB6pGoAPUQQAtQDwHqEWuAehgMUA+oB9RDgHrkLwhQD6iHwQD1SFSAekA9BKhH/gLUI9YA9TAYoB6JClAPqIcA9RCgHrEGqIfBAPUYFAH1EEALUA8B6hFrgHoYoB5QD6gH1EOAeuQvCFAPqIfBXA/1aaQupG5uSlTjvsxUjy7uqR6YiUGxNqgfPDmPE9sQV0H9kgz1hw9ugi+hmkHrpX0EWjkc3yORv6AIxdodiDVXQf3FpNaAephXoD6V1Il0jbsSVW/1GKq5NUJ91tRSdecrhQT12zixDXID1E/MJqhfnakeX9pLjZ5zM3wJ1QJa++lK1BaO7+HIX1CEYm0QYs1VUH+hQH1TQD0smi2OlERqSepAutINicpezQX4BYP6ChPqh045pG5/Lp8TWz/H+3ajH+rHr+mtnlieoR6aB99C0XMlCvkramKtP2LNVVDfRdqMmwDqYdEO9YmkFFJb6Tu7jtSbNJiURbqbe1UdpJGku+SzDeWBXD7r7aQ7Zf1dst8oj4r/9+FyPG4j9SX1Iv2QdAsPSJp/nXasRkrM8ecaIf9HlmiEx/0KVc8Bw7gNQmKcY7ofaQBXUSX2hyN/QSHwV5b45lYZGzmX9iRlkgaKDzFWOstfQyQf/IB0Oam9FC+TpUMBUA+LSoslJUifWbq04FwuYM9Jq48MlgMdJGvw7itJNUNTpnxma3Af6FENkOPDAH8z6QZSV1F3Ug8ZmKxj5bTPPkDzs64BmgZCno7vfgJYGRLjP5C4vlFOXjNlH+Qv6Gz91Uf8c5OMjVYuvU7iLQNjpeP81UvGvSsb+ScB4Ztkm0sRMw5QD4tmqLf66lMk8DtKxZ6/DNdoIOgUdRNdK5/vatJV8niNrLf26e5RWcfn++LH75EuEvENQ5fZ/NvNof9DV+2xu+1zetm/XpcVE9b3/wopRlwmukpi/1rkLyhEufRqibFLbLn0cs13iDXn+Iv/z0tJnUltpEpvtd7EAf1g0Qz1cVKtbyJgny5fgvZSue/iUHUWdZITkY6y7OTPHOnj00n8eL60V50nj+00/3Z22Oe+wKZA2+BfyPr+8/e+g8SzFdfttXyA/AWFwlcdJb7ayvjYRsulHRBrjvt/O4h/WpFaNPLfIJsoRcxYoB8sWi1Gq9YnSMW+mVym4jNbnhUnzaFK1R5Tbesgv1pKQksRnzaX5RYipx+vdJvStEcISpUYt+K8pS22kb+gUMaZnktTNGGsdKa/UoRnkm1Aj7YbmCfAPk6De/4CJMmXwYlKsn0+J3/Whpbly0RNCQGOodPUBL6D6pELkrRYT0L+gsKUS/Ucmoix0vH+StBabgD0ME+BvQX3FuDrineQ4uqheI8q0LGI1R5jXeTfePgWqkcOQP6CwumvWFsuRay5w18AehgMBoPBYDAYDOYs+y+rVzUaolvqoQAAAABJRU5ErkJggg==) + +- ProduceExecuteConsume:任务生产者开启新线程来运行任务,这是典型的 I/O 事件侦测和处理用不同的线程来处理,缺点是不能利用 CPU 缓存,并且线程切换成本高。同样我们通过一张图来理解,图中的棕色表示线程切换。 + +![img](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAucAAAEHCAYAAAANq+jXAAAw4ElEQVR42u2dCZgU5bnve/aFGRhgANlhUBHEDUFRE5OggqCIAWVRNHn03KMm95rFx+O9yRVPuEZFwMQoKhCXaGLikqMHl4hLcIvBJUYMLiAiiAKyyirgdNd936634KOZHh2mu6e6+/d7nv/T1VXVVd90Vff86uu3qiIRAAAAAAAACD0FksIkKQphkrWRttP2r9v2IAV8/AEAACCMYh5ITLGkRFJqKQthgraVJAltp+2Ntb04QdoLkHQAAAAIm5gXm7yUS1pJqiTVktYhTbW1Udta6aSKttP2Rtqu7a4wYS9JEHQAAACAFpfzQMxVyqt+9f4ILxty03sjvBnvjPCmLzrdu/Gt4Z60vZa20/amxCS91D4D9J4DAABAi1NoYq69iNrr2S5bxGrGu74gTls43LvhjWEqWj1pO21vopy3NkEvidB7DgAAACGScy1LaCvpkm2SOPXNYd4vF5ymojWAttP2Jsp5h4hf6qIHp0XIOQAAAIRBzvVnfe017yipyyZJnLbw9Hjv7ZSXT1HROpG20/Ymynk3OyitsINU5BwAAABaFO0t1F7DNpIukn5ZI4nvjIiXVlz32jDvP18YqqJ1Gm2n7U2U87qI33uuJ4kGpS0AAAAALSrneiJojaSH5MhskkQ9IfGXr57qTX4uLomjaDttb6Kc95UcFPFLW5BzAAAACI2ct5P0lAzMNkm89u+neZOf/baK1ljaTtubKOf9I/4vRirnpcg5AAAAhEnOe0mOzUZJvNqXxHG0nbY3Uc4HSLpG/Ku2IOcAAAAQGjlvL+ktGZR9kniqd/Uz31HRGk/baTtyDgAAAMg5kkjbkXMAAAAA5BzBpe3IOQAAACDnSCJtR84BAAAAkHMEl7Yj5wAAAICcI4m0HTkHAAAAQM4RXNqOnAMAAABy/jXz6Y73vER2Rbd7n25/z/vrZ7MafM2jK6d4n2x/1/v9Rz9qUUlsStt//f6Z3svr7vPW7fzI21m/Pd7+uZ9clxVtv23JeO9fn8/ztuxe531Rv9Vbtu0N7/6PfpIVbXfz35/8v/j7Pv+z2cg5AADkHwUFBSuKi4u9RvKazHa5DsvjsDC0ubCw8BfWnkEJkypk/PPyeGU63qoszH5yftN7zROtZdte9xZtfjaexVteFoHdFh+/YP2D+wiuCtb2LzfFpz204v+EQs6/Ttv/9fnT8XErti30/rnpcW/r7g3x53M/+WWo267v+WdfLI2Pe1+mL9n6ilcf2y0y/IV3z4eXhP59D3LHB+d5O778PD79zY1zD2i9uo9rksh5ASGEEJLipFx0ZxYVFT2oEVFfZEL+ejBOpl+XBXKub8wR0t6ndLz+TWmU80JLUUKKQ5SgTSWSSkmtpC4Vcp7YE3v3h//Di8Vi8R5RFcS/Sm+nSqFLWOT8q9oePFcxD+a5a9m/ybhovLc3zG3X3n3l9Q3/tWeeF9feEx/38rp7Q912d5oeVGzevTZVcn6EpJukxg5Si0P4WSWEEJIdSfS+Qidp5WqVW5HcixLGh1rO5aBiidvbnwY5L3CEvNikt8z+4WsqQppK6zU8SHKI5LgZ7/rikirR0gS9y7OXXuA9svIXe3pJg/FhlfPEtj+5anp8+Lk1d+wzz7YvN8Z/BQhz219Z94d4j/Qfl1+xZ/qfP54cn/7K+vtD3fZg3BOfTosf2OmvLgcq57pv6z6uf4e0/ShJTzs4rQ7x55QQQkj2pNwcsMSR9rRKeqNyLuMvlDwqMrxBHp+U8aMcYZ4teUAGT5Xpi2X+l23SUBl+ScZt1hKZBpY9RsfL9G06jy23nzO9VpZ7r0xbr+uV4VmSaQk95/9Xxt0kr30kDXKeKOZltnGqTHzbWM9c2BK0q9Z6D/tKjldpORBBTyZat38wwYvG6uM9ob9ZfPY+0z7a/mao5Tyx7Sq2L0kv8z0fXrZnnjlLL4y/dvWOJaFue+LrVHo/3vF2fB5X2MPadm2v1snrQYb+WnEgcu6K+fRFp2vbj5H0kXS20q4aQgghpJlu1docsNKR9KIWk3OR401W5jJZhteqMFstZ0RlXJ6vkmyV4Rdlvina0y7Po7K8eTJ8qYy736T6p7bcITpd8q4MX6HjZXidyn2wYq0hN+G+T2vJtZdcsjNJzfnINMp5IOatJF6W5sTpb58el5cDlXMtlZj7yfXxPL36Zqlz/jA+funWV/d7TdjkvCltD+q4tV5aeXr1rVnT9nc3z99TUqS90dnwvuv7vH7nxyLrZzVbzlXMpy0crm0fnMWfVUIIIdmRQNALnXObMifnWk7i9JRPMUE+M5Bzez7ZOdn0La1jtyOKOFoXLuM22lHHCFnOHfJ4pLPce2w5+jP0EFvv792qFj0IyKCcFzr12xV2xJStO9A3VFpUXlRimnvljQDtVdYe5rDLeVPafvsHE73l2/8Zn+e9LS/IuDOypu1PrpoRv2qLluLoyZUPf/yzULd93upfx3vR/7T8yj11/gci53t6zeUA9MZ/xuX8eP5pEEIISXMqraO6qEXkXOXXkexzbd7vJ8h5jc3SSiQ6pr3iIsu3BJF5Fth8A22+bro+mXajPD4h89fb9Dby/GIbPjuh5vzOFpDzUus117+vk6S7nWSptdyHhTCHWtv0sb/9xH+yildz5PxVucKG9iJrHv90qve7jy7b76S+sMr51237/ct/Eq+JVmF8bcOfk/59YWy7mzlLvxev4V6x7a1Qt10PIvTSiX/++Op4VNaVD7YuiD9vqpxPW3i6N/Ufw7TtJ1jdeT8r6zo0pJ9VQggh4U/wf6TOHFDP52tnnc1lVmHRInI+rDE515IW5zUdrBTmbZHlGYmxWtAxKuOShTJuqixrkuTxQM4ll9nw8IT2XIOct4ycN+W62WGvOW8ov1v2g3jtswqjnlDZ0jfy+bptf0dKWdbtXBHv8XfH6/XaVdC/7gFGS7RdryffGF+77cg5IYSQzMh5j2yS8837FGtLTbpzYmjAKZIf6dmuMu0FrWO3PypY7mOOnA812b7WXYDMM78Fy1ra2EmWnU3Qe9mG6hOiaHt6O23ra6VDzS5ryWU5v3XJGLkyy4a4mN/54UWhuMvm1217cH32xz+9cc+43374/Xjv/9bd60Pddi1neXDF/96T4Ko5ellFfd6MspYhEf9a54c4n4mwfVYJIYRkR+rMq9T9umgntDlhK+vAzQ45F35usny9lrHovDLPdrtRkC7jGe051x5zeXq4rkee77Z1nWT15YskO2T4EpVxvSpLcMnEFjwhtI1d/aGjHTl1tg0VlnR22tXVpETf35OCE0KR8/2jl4FUNuxcKT3Rz+2ThZueDHXb/yB3YVUR117oNzY+Gr/e/NovPoq/9m9yBZSw/2LhJsUnhGpvR0/7HHQO4WeVEEJIdiRwq46RvVcA017z8si+V2wJvZyXiCjfION3BUKtJ4TaH6YMlmmfBNOsvGWaDa+0eerk+avOPIu1BKYF5DzZpRTdS+y0DUnc9tTaUZ72ng8Jes1TdSnFXJLzf4gMJkNLXcIuuI9JPXdwV1ZF7w7qi/kZeSXnjVxKsW0IP6uEEEKyI8H/D/dSiuUJJ4OmRc7TRYnVfXZsZFpnZ1xv6+lyqbXxLUFO3YToQG9ElMmkUhLzre1azqJ34bz5/VF59b5zEyJCCCGRzN2EKChlSftNiOCrBb3QEfWiSHhvBx7UyVdaXZTWSg0Obm2O4NL2XJRzV9Cl7UdE/Btw1diXKbefJoQQ0ly3cpN1veXQ8hSZlLS3Xx0GIYm0PdflPIidDNrVfj0qpVcDAAAAkHMEl7Yj5wAAAADIOW1HzpFzAAAAQM4RXNqOnAMAAAAg57QdOUfOAQAAADlHEmk7cg4AAACAnNN22o6cAwAAAHKOJNJ25BwAAADggOX82CyWxHG0nbYj5wAAAJALct5O0ksyMPsk8TTv6me/raJ1Dm2n7U2U88ORcwAAAAijnLeV9JQcnW2S+MsFp3mTfUn8Lm2n7U2U836SzpJq5BwAAADCIudlkjaSbpIB2SSJ0xYO9657bZh3zfx4ecUZtJ22N1HOD5V0klQh5wAAABAmOdef9Q8yWRkkOUUyWjJOcp7kghBlkmSi5FzJKMlwyVDJd6zdIyVjJONpO21PaPtYyQjJNyVHRfxSrlpJpaQEOQcAAIAwyLlKSauIf1JoD0l/yWDJyZLTJKdLzghRRpoYnmptPMHaqwcVx0u+YcI4jLbT9oS268HESZJjJIdIukhqJBWSYuQcAAAAWppCkxKtO9fSlo4Rv/a8r+QIyUATsONClkEmWEfawcRh1matIdYrcBxF22l7A20/OuKfBHpwxD8RVA9ItaSlzA5UC/hKAAAAgJaW86D3vMIEXX/m1x5F7UXXyyv2MZkJS7Q9ddY2bWM3a29XS3c7wKDttD2x7b2s3VrCpVcoqrYD06DXHDkHAACAFqUgsrf3vNQEvcokXa/g0t5kXdMhRKk1uWpn7axx0tbG03ba3lDbtZ16jkUrE/MSO0ClpAUAAABCQWFkbw+6SnqZSYumMsSpsMegrWU2roK20/ZG2l5hbS61/T0Qc3rNAQAAIDQUJEh6IOphj9vWxEfaTtsba3uRs88DAAAAhFLQCcnHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZAEFlsIkKWpmki0rlevIhSR739ke7BcNpYCvLgAAgNwWcxWBYkmJpdRS1syUJqSkgXFleR73vXC3QUkD7xnbIz/3jRLbN1xpR9ABAAByTMoL7B99IGjlkkpJtaV1ClLtpJWTVK4jFxK8P1UW971qnebtUcX2yIp9o8KEvQRBBwAAyE05L7R/9CrlrdbMO9zLRMbP2eZN+O0X3sS7dnkT7/7SO++e+ryOvgfx3LU7/r7o+yPbo5btQRqKSXog6Mg5AABADsl5sfXEac9pu0zJ4Lg7PvcmzNnuTbhzZ1xIkXNfzvX90PdF3x/ZHr3ZHiSJnFeboBfbATYAAADkgJwHveZaxtJO0iVTMjj21s+8cbM2W28tMrhPr/msLfH3R7bHUWwPkkTOa+2AutTpPacHHQAAIMvlvMj+uWsvXCdJXaZkcMzNK71zb98Y762llCKQ810ixzvkfdkk788nKmDfYHuQJHLeRVIT8cvRipFzAACA3JFz/efeRtJV0j9TMnj2jGXeOTPX+rXOWkqBnMelWN+Pc27b4H33V8tVwEawPUgSOe8paW+/eiHnAAAAOSbnbe2ffcbKKEZP/0BKKdZ442dvRQYT5VwkefT0pSpg32V7kCRy3kfSweS8BDkHAADIHTnXk8q03ry3ZGCmZPCsG9/3xt6yOl5fjQw6ci5yrPXfo6ctUQEbx/YgSeT8EEnHiH95ReQcAAAgh+Rce9705/E6yaBMyeCoqe95Y3+zyj8JERncK+fxk0HXiCwvVgGbyPYgSeS8b8Q/T6QKOQcAAEDOmy+DNyCDoZJztgdyDgAAAMg5MoicE+QcAAAAkHPknO2BnAMAAAByjgwi5wQ5BwAAAOQcOWd7IOcAAACAnCODyDlBzgEAAAA5R87ZHsg5AAAAIOdhksHFa2NeIrFYzNuw3fMWLI95Vzwa3e81F99f772zJubd+3oMOQ9hmrpN71wQ85ZvjHlffBnzPpLHWX+PIucAAGBWVFCwori42Gskr8lsl+uwPA4LQ5sLCwt/Ye0Z5IweXFRUNF/+ns8li2We6+wfRSoFkqQveSfn/1jpefOXxuJ58cOYt2KTP36jCJ3KeDC/it3bq3zhm/c+ch5mOf862/SPb/rjPlwf855eHPM27fC37d2vxXJNzgkhJN+SMtGdKVL7oEakdpEJ+evBOJPcsMt5nbR9l2S5TpN2P63TZfimNMh5oaUoIcWkSWno/WuVT3L+s8f37S09X/LBOn/a9PlR7389XO9t371vjyxyHm45/6pt+r376r2d0lu+arPnXXivP8+P/ysan6496Vku59WSUr4bCSF54i1FjhOm9RfDq1VqRW4vShgfajmX5zfb88HBLCLq70i2pOjNKnCEvNh6h8ok5ZYKcsApd97L1pIOkj75KOea55b402b+Lepd+kC999cPYvG8/1kMOc9COU/cpv8xN+pt2xXzHv1XdB+B14OwlZ9nrZwfJuksaWOfab4bCSH54i4lliJH0jMr5zL+QsmjIr0b5PFJGT/KEebZkgdk8FQtK5H5X7ZJQ2X4JRm3WUtkGlj2GB0v07fpPLbcfs70WlnuvTJtva5XhmdJprlyLsOvyLS1CQL/R5unJsViXmYbpspkso2tgzQ9bew9rLb3s631wB2sB1qrn+qfdz3n67f5PeRXzd132u2vRJHzLO05T7ZNNdqT/qd/+q91hT3L5Fy/r7tI2jmfZb4bCSG57i5VTodEWgW9UTkXAd5kZS6TVYZVmO1nTBXkl+X5KslWGX5R5puiPe3yPCrLmyfDl8q4+02Yf2rLHaLTJe/K8BU6XobXqdwHK5b5n7cSlfvk6ZUybYlkZ0JZy2mSU5z2lss8KyWrU9hrHoi5ll54JG3Rf/KHSo7LdTl/aVnMm/tONB6tPdaTB5VnFu8v4Mh5dsh5U7aplrnUR/3X6Umj52fvCaH9Jd0lHfn+IoTkacrMEzMv51pO4vRMTzFBPjOQc3s+2TnZ9C2tY7ejiTiy7Kdk3EY74hghy7lDHo90lnuPLUd7YIbYen/vdorrQUADJ4QGVMo6HrG/4+JUVNFY+0vsCKk1O2Fa091+Jj9+1V/6583VWgL05MDJf4ki5zlwtZav2qZa4vKYSPyyDTEvKld2+fPCrC1rOVzS00pb+A4jhORjKpxzbjIr5/I40pHsc23e7yfIeVBG0kokOqa94iLXtwSReRbYfANtvm66Ppl2ozw+IfPX2/Q2Ktc2fHZCycqdSeR8oK5Pl+H0zqdKzkut17zGSi+624mLh5hMkqbnUKsv13/sXXVfsOG8kPMp8/xLJGp++FB9/DKJypadsXi5A3KefXLelG0aZNLv6uNX49FLL/77n7JSzgdIettnuCvfjYSQHE5f+26rs++6TlaS28o8sbgl5HxYY3KuJS3OazpYKczbItMzEmNSNkZFWrJQxk2VZU2SPB7IueQyGx6e0J5rGpDz07XcRcteZPjEVJ5/ipwj55k6eXCJXdnjqseoOc+VE0Ldbfqbl6Lex3J5RX105wkurzhtfhQ5J4QQ5Dylcr55n2JtqUl3TgwN0NrwH2lduEx7QevYrVYnWO5jjpwPtbKWa90F6PXME+T8ZFnOF3qyasS/VnYkDXIelLVou2rtJ1zdML1sI/UhTUqd/UPvYf/QD3Le07yV8+DKHtc/i5zn2tVadJv+8hl/Oy5YEWvw9T9/IprNZS1dEj7HfDcSQnLRXXrZd1xn88HW5p4tVtbSJDkXfm5yfb2WnOi8Ms92PcnTlvGM9pxrj7l9wV8uz3fbuk6y+vJFkh0yfInKuF67PLhBUiDn8vqHbD0zdV1urLY91SeEqqC3txOggn9GXUiT0tneu062c+uVHjqYqMdPCM1HOZ+7KLrnsnvIeW7IubtNtYRFL5moJSxvrIx5t8l2fWW5/1qtPb/g3qw/IbQ9342EkDxwl+D7ro15YcudEHoAcl4ignyD3iAoEGo9IdT+KGWwTPskmGblLdNseKXNUyfPX3Xm0bt/TnXkvEhPME12h1NnXc2V82SXUnQvr9OWfO24lySqtrS1nV5/Msq7Sylq5vzdFzkVNuQ8N+Q8cZv++JHonuvWB2hP+g8erM+1Syny3UgIycVk/FKK6aLEvsA7NjKtszMuqF90qbXxLQE3Icr8TYhyVs7DHuQ8M9GTRq/876h30R+y+++IcBMiQgg3Ico6Mc8VAkEvdESd21On7la4JfbTUK3VdA1CBpFztkdWyHlf+8Wr2qm55LuREJLr3lLkOGEBYg65eOBTZCdUtEfOkXO2R9bJeSf7ibeEf1IAAADIOTKInBPkHAAAAJBz5JztgZwj5wAAAMg5MoicE+QcAAAAkHPknO1BkHMAAADkHBlEzglyDgAAAMg5cs72IMg5AAAAco4MIudIMHIOAAAAyDlyjpwT5BwAACB/5VxvidtO0ltybMZkcKrI4C2rkcFEOZ+9VeT8M2/0tCUqYOPZHiSJnB8q6Rjx7/CLnAMAAOSgnPeUHJMpGdSeYZVB7SlGBhuQ8+lLVcDOYXuQJHJ+MHIOAACQm3JeLqmRdJcMyJQMqnyqhKqMIoOOnM/Z5p1z2zrv7BnLVMBGsT1IEjnXX7q0HK0SOQcAAMgtOS+TtJZ0jvg/lQ+WnCI5SzJOcr7kgmZEXz9Be4EloyVnSE6XjLTn59j085q5nmzOJMlEe7/1PRkhOU0yVDLM3rMxkvFp2B5n2vY4w56fa205P4+3R9j2Dd3u37V94URJf0k3SduI/8tXMXIOAACQG3JeKCmN+CeV1Ub80hb9x3+c5GTJqSaKI03eDiQjTP50Wd+SfENykj1+y8YPdwQxHzPC3gN9L75t783xlhMk35R8x4Q9ldsjWNc3bB3ftvGn23xnkFDsG8Ns+w+RHBnxT97uZAfVZXaQDQAAADki59rrpr1vbSJ+DasKul4J4gjJwIjfk35cM6KvH2TLOtIywJZ/lI0flIL1ZHPc9+goe3/0IKmfPep7dbTk2BRvj6MStsfRzjbP5+0Rtn3jWNtW/UzM9Vcu7TWvtIPrQr7OAAAAckPOg9KWUvtHr4LeQdJF0iPi17X2sRx8gOljQqHL6hXxa9t72EFAT5tW18x1ZHuC96iXvSf6HmnZQteE9ysd26OHs/xebI9Q7hu9bft0tQNoFXM9EbTMDq6RcwAAgBwi6D1XQdce9CqTdL2CS3uT9eak1tLe0s7koq0Nt3Pm6ZDHqXXeD31vapy0dd6/dG+P9myP0O4bui9U2+fUFXNqzQEAAHJMzjXag15ikl5uAqCpTGEqEpabjnXkQsoTUpHB7cH7H85U2L5QZp/TYuezCwAAADlGQYKkBylOcYqSPJKvfp/YHiTYD4LPKj3mAAAAOS7ohJDsCQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5CQFlsIkKWpmki0rlesg2Z9k+18BH1EAAADIRzFXQSqWlFhKLWXNTGkDKUvh8kn2x903Smw/LELQAQAAIN+kvMAkKJDxckmlpNrSOgWpThhO1XJJbkX3iyrb/8ptfyxG0AEAACCf5LzQxFxlqNWaeYd7mcj4Odu8Cb/9wpt41y5v4t1feufdU0/IPtH9MeL3qhcj5wAAAJAvcl5sAqQ9lu0yJefj7vjcmzBnuzfhzp0i6LuRUdKQnLc1Qdce9KDEBQAAACBn5TzoNdcygnaSLpmS87G3fuaNm7XZes+Rc9KgnHeW1EgqInt7zwEAAAByVs61N1J7JbXWt5OkLlNyPubmld65t2+M955T2kKSyHlvSYeI33tegpwDAABAPsi51pq3kXSV9M+UnJ89Y5l3zsy1fu25lrYg52R/Oe8b8XvPW9tBJHIOAAAAeSHnWtvbU3JUpuR89PQPpLRljTd+9lbknCST8wGSbnbwWIacAwAAQD7Iudbzar25lhAMzJScn3Xj+97YW1Z742dtQc5JMjk/WtI94tedl9v+CgAAAJDTcq4ng7aX1EkGZUrOR019zxv7m1X+SaHIOWlYzo+J+L/otEXOAQAAADlPp5zfgJyTr5Tzgcg5AAAAIOfIOUHOAQAAAJBzQpBzAAAAQM6Rc4KcAwAAACDnyDlBzgEAAAA5R84Jcg4AAACAnCPnBDkHAAAAQM4znsVrY14isVjM27Dd8xYsj3lXPBrdZ/5rn6733lkT87btinkrN8W8R96Oehfei5wj5wAAuWIgBQUriouLvUbymsx2uQ7L47AwtLmwsPAX1p5BwbiioqIJ8re8JdkuWSTP/y0NskbyK8h5BuX8Hys9b/7SWDwvfhjzVmzyx28USb/4fn/e65+NxsV9zRbPe/K9mLfEXvvSslheibnuD0nknM8tIYS0XFImujNFZB/UqNSakL8ejJPp12WBnI/Q59L+N7W98rhMn0v7z0+DnBdaihJSTLI+idu0RNJKUoucp1/Of/b4vj3k50s+WOdPmz7fn7ZsQ8z7MhrzfvCgP8+k3+2d54cP5aWc95K0s4PIEr6TCCEko55Q6KQgXd57tUntRQnjQy3n0t6/ipBvsx4kpb/9HX9OoZgXOv/w9J9gmfVWaSpITqXc2b6tJR0kfSSDkfPMybnmuSX+tJl/i3rfu68+Pvzumn17yZ9415/nmr9E80bMJ961O/j+q7P9s7UJOp9fQghJvyMEnlDqSHvaBL1ROZfxF0oeFRHeII9PyvhRjjDPljwgg6fK9MUy/8s2aagMvyTjNmuJTAPLHqPjVa51HltuP2d6rSz3Xpm2Xtcrw7Mk01w5l+G/ybi7ndeUy7y7ZVkPp0HMy2zjVNk/xDaSGpJT0e1abdEDvoMkhyDnme85X7/Nr0G/am40Xtryxzdj3ozn953v/c/81//Ph/NAyk3MJ961S7//jrP9srOVXtXwfUQIIRn1hEoT9ZJ0Cnqjci7Cu8nKXCbL8FoVZjtqUEF+WZ6vkmyV4Rdlvina0y7Po7K8eTJ8qYy736T6p7bcITpd8q4MX6HjZXidyn2wYpn/eX2NvPY+eXqlTFsi2ZlYc25oCcK3tcdc55HhkSmU80DMdR0eyat0lfRVGULO0yfnWjc+951oPE8v9k8IVZ5ZnLye/LZXovF5Fq7y8qPH3ORc9wnZH4+XHMbnkxBCWjyuoGdWzrWcxOkpn2KCfGYg5/Z8snOyqZ6guSjiXEVAlv2UjNtoPc8jZDl3yOORznLvseXoEckQW+/v3aoWPQhIIuffCk5k1Xp5e7OaXUXj1B9X2NESO2J+pYdJ0PHIeWau1hKwaYfnTU5SrqK96Hpy6DrpXQ9q0PNDznd5E367Q/fLEySH8/kkhJAWT6VT4lKY6ZrzkY5kn2vzfj9BzmuCXmyR6Jj2iotc3xJE5llg8w20+brp+mTajfL4hMxfb9PbyPOLbfjshJrzO5PIub4xddom7cGXx2dTKOel1muuf18nSXer9zzExI3kRg61+nK9CkY32849rdQKOU+jnE+ZVx8vXdHoyZ33vu6P37IzFq83D+a/6A/13j8/9eVdX3vZg/lzEmiCnJ8oOcL22YMth9ivPHyWCSEk/Z5wkJ2UX2Udwi0i58Mak3MVYuc1HawU5m2R6RmJsT9sjMq4ZKGMmyrLmiR5PJBzyWU2PDyhPdc4cq61lv8hOSVB4GdryYxNR84Jcp6FJ4QusSuxXPWYP+1CkfS3V/nXQX94YSx+tZZ8ukILck4IIch5U+V88z7F2lKT7pwYGqAS/SP9I2TaC1rHbrXcwXIfc+R8qJW1XOsuQOaZ78h5vIfersfe0DwHpbisRdtVaydhqbj1MknvQ7I+uh172weuq+07XShrafmrtej1zfe5estL0by6rvlXlLX0se+hnnwfEUJI2j2hl3mB+kEH67StNJcNv5wLPze5vl7LWKzcZLue5GnLeEZ7zrXH3P7JXK5XWbF1nWT15YskO2T4EpVxWdZNQV15ZO+lFB9WQderusjT0fJ4s7VtfppOCG1jPfIdTeA620Yi2Z+D7JeRWmcbc0JoC8n53EXRPZdS1F7yHbtj3q76mPf80v2TeCfRPDohtKftu534PiKEkLSms+MJwVWyqiL7X7El1HJeIqJ8g4zf5Zyo+ZQJjzJYpn0STLPylmk2vNLmqZPnrzrzLNYSmISa8w56iUdnnpg8f8iOaFIl58kupeheWqctyfokXkqxXYRLKbaYnM/5uy/nryyPeZOfjHqNoTXrXEpxz6UU+SwTQkjq/SDZpRTdk0HTdjOiVFNiNbsdG5nW2RnX23orXWptfGNUWw98dYrbz02IuAkRNyEiYbwJUZ8INyEihJCWvAlR4jXOs0bOc4FA0AsdUec22bl9a94SK2OqtTqzQcg5CYugR/wrXvWyX3cqnX8QfCcRQkhmPKHI8ULEHCADB2NFJj3tkXMStpic97SfW8sjzv0kAAAAAJBz5Jwg5wAAAADIOUHOkXMAAABAzpFzgpwDAAAAIOcEOUfOAQAAADlHzglyDgAAAICcE4KcAwAAAHKOnBPkHAAAAAA5R85JEjk/BjkHAACAfJNzvVWv3oGxt+TYjMn5VJHzW1Yj56QxOT9a0sPkvAw5BwAAgHySc+2hPCZTcn7WjYvjcj5+1hbknCST8yMl3SU1yDkAAADki5yXm/yoBA3IlJyPnr7UG3vrZ9742VuRc5JMzvtLukham5wX8rEFAACAXJfzMpOfzpJDJYMlp0jOkoyTnC+5oBmZZMuYaMs7VzLWhsfbtEnNXAfJ/pxn+8YoyXckx0oOlnSSVEtKkXMAAADIdTkvNOmpktRG/NIW7a08TnKy5FTJCMlIyRkHmJGW0yWn2TI1wyTDneU3Zx0kuxPsH7pffFMySHJYxP81R0uu9KTlEuQcAAAA8kHOiyN+3XkbSUcT9L6SIyL+pewGm6w3N7qcY+1xkCVY9vEpWgfJ3ui+cIztd/oLjp4I2iGyt6SlGDkHAACAXJfzoLRFe88rTdBViLqYHOkVXPpYDm5G+jipsyQut7nrINmdOtvfdL/TEqtaE3M9cCyx/RQ5BwAAgJwn6D0vNRGqMknXcoL2JuvNSW2ScW46kLxOsB/o/tbWpFz3w3LbLwMxL+DjCgAAAPkg54UmQCUmQ+Um6prKFKdVwiMhbsodKS92xJxecwAAAMgbChIkPUhxGlLiDKdrHSS74wo5PeYAAACQt4JOSBgDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkAcUSAqTpKiZSba8VK8njEn2nhZYAAAAAAAaFPNAKIslJZJSS1kzEyynJGG4JMXrCWPcv7U4QdoRdAAAAABIKubFJpPlklaSKkm1pHUKU23LdZefjvWEKcHfWGnCHog6gg4AAAAA+8l5IOYq5VVr5h3uZSLj52zzJvz2C2/iXbu9iXd/6Z13T31exCS9NEHQAQAAAADicqiSqD262rvbLlNyPu6Oz70Jc7Z7E+7cmW9y3sYR9KDEBQAAAABgj5yrLLaVdMmUnI+99TNv/Kwt1nu+K5/kvFPEL3cpj+ztPQcAAAAAiIuh9uBqr3lHSV2m5HzMzZ94596+UeR8R77JeXdJezsgKkHOAQAAACBAyyq0pEVLLbpI+mVKzr/7q+XeObeti9ee55mc97EDoSrkHAAAAAAS5VzLK2okPSRHZkrOR09f6p0zc61/YqjUneeRnPeVdI74V3EJ6s4BAAAAAPZcpaWdpKdkYMbkfNoSqTtf49ed55ec94/4v1Jo3XkZcg4AAAAADcl5L8mxmZLzs25cnK9yfrika8QvJULOAQAAAGA/OdcTFHtLBiHnaZfzAcg5AAAAACDnyDkAAAAAIOfIOXIOAAAAAMg5cg4AAAAAyDlyjpwDAAAAAHKOnAMAAAAAco6cI+cAAAAAgJxbFq+NeYnEYjFvw3bPW7A85l3xaLTB1/37n+q9f62OeW+sjCHnAJB7FBQUrCguLvYayWsy2+U6LI/DwtDmwsLCX1h7BjUwuaNMe0nm+X2q3ypCCGnB5Kyc/2Ol581fGovnxQ9j3opN/viNIukX37//615Z7k/ftMNrCTlnXySEuEmL6M4sKip6UCOivsiE/PVgnEy/LpvkXNr8qE6Tv+WtNMl5oaUoIcWEEJLiuN8xpZJKSa2kLpfk/GeP79tDfr7kg3X+tOnz95326xej3q4vY97nX2RUzmvswKiY731C8vp7uDAhBZnw3qtVbEVwL0oYnxVyLu2+UKR8h2RNiuW8wBFy3VAl1otSbqkghJA0ptzEvFrSSdJHMjhX5Vzz3BJ/2sy/7Z126QP13padMe8Pb8S8ZRtiaZXziXd/qf9jjpB0l7STtHK+8/neJyT/voPLzf1KHXHPiKQ3Kucqv9ozLeK7QR6flPGjHGGeLXlABk+V6Ytl/pdt0lAtM5Fxm7VEpoFlj9HxMn2bzmPL7edMr5Xl3ivT1ut6ZXiWZFoDct5dpn8ujz+WxzdTKOeJYl5mG6pK0tp+7qwhhJA0prU9qiR2kRwqOT6Xe87Xb/Nr0K+au3fa6x/HvI82xrxJv6tPq5yrmE+8a7f+jzlS0kPSwb7rW/O9T0hefw9XOwfqpY6gt5yci/BusjKXyTK8VoXZGhdRGZfnqyRbZfhFmW+K9rTL86gsb54MXyrj7jep/qktd4hOl7wrw1foeBlep3IfrFjmf15fI6+9T55eKdOWSHYmyHmBrOMZOyAoTJOcB2KuG8UjhJAWTF/9/swVOX9pWcyb+040nqcX+yeEKs8s3nvC58yXol5UThYNRD5Dcn6UpJfkIPY5QkhCMibojcq5lpM4PeVTTJDPDOTcnk/eY7UiyFrHHnHOcJdlPyXjNlrP8whZzh3WOxEs9x5bjh6dDLH1uid3qnyvSpDzH2o5izweYutNpZwHveYl1mPemh2SENLCOVxyQi5erSVAxXvyX3wR/+FD9d62XTHvsXf29qKnVc5FzCfetUvf56OthKgr+xwhJCGtnHNRWk7O5XGkI9nn2rzfT5DzGpullQhyTHvFRa5vCSLzLLD5Btp83XR9Mu1GeXxC5q+36W3k+cU2fHZCzfmdjpz3ltdsl8efOAcF6ZDzUtsQNVbz2d1OytIDgsMIISRNOdQEUb9vett3jp6oeGKuyPmUefXxq7JoVMTvfd0fr/Xl37uv3nvi3Vj8JNAZz0e9a5+pj2fNFs/bKtN1WGvR0yznWtrSzR6DbdCXfZOQvPse7mmlhbXWiVxhnbdFLSnnwxqTcy1pcV7TwUph3haZnpEY+yPHqIxLFsq4qbKsSZLHAzmXXGbDwxPac40j52fZepZayUu87MWyxNaDnBNCkPMsOiF0iV2t5arHot6zS5L3sCu/eiGKnBNC0v09fHCCnLfOFjnfvE+xttSkOyeGBpwi+ZH+FCDTXtA6dqvlDpb7mCPnQ62s5Vp3ATLPfEfO+2q5jRtZ5mq9YouV4XRIcVlLG9sonU3Qe9k/zT6EEJLiBELewxJ0CuRUWUtjV2u5/tmo9+NHot5/PrVvVm32e851+JI/pb3nvJt953dN+N7nu5+Q3P8Odr+Hu1oHbVsrz3YvsZodci783OT6ei1j0Xm1BEVP8rRlPKM959pjbv9sLpfnu21dJ1l9+SKrJ79EZVyWdVNwg6RIwzchimTghFAVdL0RSEc7UaizHUkRQkiqc5D9MwjS03pyhuSynM9dFN3vUopuWuCE0FoL3/uE5O/3cK2JuZa0VIbihNADkPMSkekbZPyuQKj1hFD7clMGy7RPgmlW3jLNhlfaPHXy/FVnnsVaAtMCcp7sUoruZXbaEkJIipN3l1LUzPm7L+d6N9AWlPOvcylF9lFCcvv7N0ibyN5LKVY4Yl4UyeANiVKJloT0c6S8oWmdnXG97acDl1ob3xJwEyJCCDchSrGchzlJbkJUwfc+IXl/I6JS88CM3YQIvlrQCx1R5xbOhJBM3Tq61AS91mogByHn6U3EP/G2m/WalSdsD/ZNQvLve7gowQURcwCAPKbYBLG9/YqInGdGzrvaz9llEeeeHQAAAACAnCPnyDkAAAAAIOfIOXIOAAAAAMg5cg4AAAAAyDlyjpwDAAAAAHKOnAMAAAAAco6cI+cAAAAAgJwj5wAAAACQpXJ+LHKOnAMAAABAy8q53kq+l2RgpuR89LQlvpzP3ppvcn44cg4AAAAAjcl5W0lPydEZk/PpS0XOP4vL+cS7duWTnPeTdJG0lpQi5wAAAAAQoGKovbfai9tNMiBTcn72jGXeOTPXeuPnbMs3OT9E0klSjZwDAAAAQENyrr24B0kOlQySnCIZLRknOU9yQTNyvmS8ZIxklOR0yTDJcHs+1qY3dz1hyyTJBPu7R0i+KTky4pcP1UpaSUokheyGAAAAABDIeYmJop4U2kPSXzJYcrLkNJPpMw4wI01MVcSH2jJPlJxgjyfb+GHNXE8Y4/7d+rceHfF7zbWkpUZSgZwDAAAAgIuKYVB3rqUtHSN+7XlfyRGSgSbqxzUj+vpjTU6153iAHQAMsOdH2/TmrieMGWR/n54E2ifinwiqB0FVkb0lLcg5AAAAAOyR86D3vMIEXUsutHdXe9F7m1QefIDpY6mL+OUcuszuTnra+LpmrieMcf9urefXOnO9Kk61HQwVI+cAAAAA4FIQ2dt7XmqCXmWSrldwaW+yrunQjLS3tLPlBmlnSdV6wpZa+9u0jEXr+luZmJc4Yl7AbggAAAAAAYWRvT3oKullJpCayhSlwnkMll3hpDKHE/zNZXYAVIyYAwAAAEBjFCRIeiDqqYy73MTHXE+RI+SBlCPmAAAAANCooJPMBQCgyfx/I9Z+uHcWfZsAAAAASUVORK5CYII=) + +- ExecuteProduceConsume:任务生产者自己运行任务,但是该策略可能会新建一个新线程以继续生产和执行任务。这种策略也被称为“吃掉你杀的猎物”,它来自狩猎伦理,认为一个人不应该杀死他不吃掉的东西,对应线程来说,不应该生成自己不打算运行的任务。它的优点是能利用 CPU 缓存,但是潜在的问题是如果处理 I/O 事件的业务代码执行时间过长,会导致线程大量阻塞和线程饥饿。 + +![img](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAucAAAEHCAYAAAANq+jXAAAyNUlEQVR42u2dCZgU9Z33e+7hHI7hFORSETXEIKhrjEFuUBPXXeOFJo9uNnHzvskq67pvEjEhXoCYmMRN1DUiJiSiRhbRKKAoEAQxCoRrkFMuueUYLqf7//5+Vb+CmmYGR5ijavrzeZ7v09VV1dX/6a6p/tS/f/XvRAIAAAAAAAAiT5Yku5LkRDCVtTMu7a/L1ynb3u8sdnsAAACA6Ip5IHS5kjxJvqUgggnalmftDdocTlTbXlevU/Ba5SDoAAAAANEX81wTuUJJI0ljSRNJ04imibVT0zCUxjFoe22/To3ttSm09ziQdAQdAAAAIGJyHoi5ilvjn68Y6qKeR5b7Gbd0qHt48RA3ZuFgJ21veeP4MkdOHDuZKbD3PBs5BwAAAIgO2SZpKmvau9oiLnI+bpkv52MXDXYPvTdIpbMT8l0lOW9ugp6fONZ7DgAAAAARkvOGJm3t4yTn2ms++v1B7v55A1U6z0O+qyTnbSVFkgYJvw49m38DAAAAgOjIufagaq95a0nX2Mi512s+RHrNB7pRc/qrdF6CfFdJzjtLihPHes+RcwAAAICIoGUNWtKiPantJT3iJOdaa/7Au4PcT97up9I5EPmukpyflfB7z5vYe5/DvwEAAABAdORcLwRtJjld0jNucq4lLSPf8OT8KuS7SnJ+ruQ0OyFDzgEAAAAiKOctJJ0kveIg5169+RIZpeWDwe6+dwa6e2b0Vem8Bvmukpz3lHS0E7JC5BwAAAAgmnLeWXJBLOV8+uUqnd9Avqsk519M+N+SIOcAAAAAEZXzlpIukt7xk/MBgZxfh3xXSc7PNzlvjpwDAAAAIOfIOXIOAAAAAMg5Qc4BAAAAkHPkHDkHAAAAAOScIOcAAAAAyDlyjpwDAAAAAHJOkHMAAACADJbzTQeWu3QOJ0vdptLl7s2tj1f4mMkbRrmNpcvc79f+IHZyXrItddzfm0ql3M5S5+atS7kRk5PHPea2iWVu6ccpN2FBCjkHAACAkyMrK2t9bm6uO0HeldW+r9NyOygKbc7Ozv6ptad32qIGMv8tub2rJl6qGOY4OX9k+anJ+Zr9C9ySPTO8lOyd4w6V7ffmz9sx6ei6v1hxpfvfjT9zpZ/u9pY9v/7/xVbO/7bBuZmrUl5mrU659bv9+btE0lXGg/VV1hdv9iX+9RWRk/MsQgghhNRYql10H8vJyZmkEVFfYkK+IJgnyx+IgZzrC/MFae9rOl//phqU82xLTlpyI5SgTXmShpJiSdfqkPOJa+8oN//p1d/2epS1F12l/M2tT7iy1JFyPc5xlvMfTi3fQ36T5MPt/rKHZybd/32hzJUeKd/LXsdy/qWE/2uwLe29z4vg/kkIIYTEKenOlx1KVk177z0qtyK5t6bNj7Scy0nFynBvfw3IeVZIyHNNeAqsZ1LTIKJROWsqaSs5U3Khiu/JCHplcq7Zd2Snt+yJVTe7lzb89GjPejC/Psm55o2V/rLH/pp0332uzL35YcrLiq2pKMi5/k+cYe95M0mjCO+fhBBCSJwSeJ86YH5I3ANRr305l/m3SCaLDO+U21dl/lUhYX5C8pxMDpDlJbL+HFvUT6Zny7w9WiJTwbav0fmyfL+uY9vtEVpeLNudIMt26PPK9OOSsWk95z+WeY/IY1+qATlPF/MCe4Mam/gWmQRFLUG7tNe8g6S75KJxS335/byCXpmc/+bD610yVeb1nv+y5Opyy9aWvl8ve853+JU87u4p5Zf9Zm4yCnJ+keRsK21pLWkR0f2TEEIIiVuKzP0amwsWhL6hrhs5FznebWUuI2V6mwqznTkkVMbl/mbJPpmeJeuN0p52uZ+U7b0u09+VeRNNqu+07V6syyXLZHqEzpfp7Sr3wRNrDbkJ97NaS6695JJDldScD6tBOQ/EXHsiXUxzycOLhzgV9JOV81nbxrspGx/0Mm3Lo27rwdXe/FX75h/3mPog57PXpNyUpUkv00r8C0KV6SXHC3hE5JwQQgghtZcCc8QaK3E5oZxrOUmop3yUCfKVgZzb/ZGhi00Xah17IjRihNaFy7xddtYxVLbzW7ntGdrueNtOE5V3e97fh6ta9CSgFuU8O1S/3cDOmOK6A106dtFgT3xVgE91tJaALQdWuidX3VIv5bwidh9wbuRfksg5IYQQQsLXd9W+nKv8hiT7Wlv3W2ly3sxWaSQSndJecZHlXwWRdebZer1svQ76fLJsjNy+IuuX2fIiuX+bTV+dVnP+VB3Ieb71muvf10bS0S6yPNPKCKKWs6xtenuOXSh42ZiFpybn82VUlmlbfu1l6qbR7pm1t3sXglb0mPog56Ne94dI1Hzv+TJvmERl76GU++azkZTzy620RUdtOddKxM4mhBBCyCmlu3lVV3NAdcHm5ob5Ndl7XuULQiuScy1pCT2mlZXCLBZZHpceWd5N681VxiWLZN5o2dZwydRAziW32/TgtPbci5zXjZxXdEFoZamPNeealTZay90vJ5FzQgghBDkviLKc7ylXrC016aELQwP6S36gV7vKsre1jt3+qGC7L4fkvJ/J9n3hDcg6M+uwrKXILrJsZ29OZ3ujukUo2p4uobZ1t9KhUy5rQc6Pjdby4IxIyvlX7f/iPDsxOyNi+yYhhBASt3S1dDb3a2cuWGRlLfmhi0KjLefCj0yWH9QyFl1X1im1HwrSbUzXnnPtMbdevu/L/SP2XF+2+vIlkgMy/R2VDh2VJRgysQ4vCNU3Q8eR1tEw2tqb1D5CaRdq12km6vr6fjm4IBQ5P3k5n7IkeXQoxQjK+aV2IqZS3sne//aEEEIIqRa3am0OWBTqNc+rswtCT0LO80SUH5L5hwOh1gtC7Q9T+siyjcEyK28Za9MbbJ2ucn9+aJ0SLYGpAzmvbCjF8BA7zSOScHuK7SxPe88vDnrNq2soxUyU8yff8SV87rpUXIZSbE4IIYSQU/aq8FCK2mNemKiFoRRrijyrfW19gmXtQvO6WI9fmGKbXxfUqx8hOtkfIqqtREHO45oEP0JECCGE1KsfIYLPFvTskKjnJKL70+hBnbwKeiurleqjUh5lMUfOT1nOv2Q1cS0Tx4Z34qeXCSGEkFP3qnDCTpiFJkNVybEzvJb2rUPvKEs5cl4tcn6+lbQ0t/c+h38DAAAAAOQcOUfOAQAAAAA5R86RcwAAAADkHDlHzgEAAAAAOSfIOQAAAAByjpwj5wAAAACAnBPkHAAAAAA5R86RcwAAAAA4RTm/IMZy/g3kGzkHAAAAqA9y3iLh/2pkr1jK+Yy+Kp3/jHxXSc57Sjoi5wAAAADRlXMVtU6S8+Mm5/fPG+hG+nL+j8h3leT8PEkHSTPkHAAAACB6cl4gKTJhOy8ucj5u6VA3ZqHI+fwB7t6ZXlnLFch3leT8bEl7SVN775FzAAAAgIjJuYpaW8lZkt6S/pKvS74huVFyc4QyXHKD5FrJVZLBkn6Sy63dQyVXS66LYNtr+3XSv/+f9cRF8lXJlyTdJK0ljSX5yDkAAABAtOQ8T9Io4V8UqhcKniPpI7lMMlAyxOQuKhlmQt7fhPMSa6+eVFwoudREPWj7sIi1vy5eJ31NLkj4veb6DYleY9DQ3vts/g0AAAAAooGKWW7Crz3W0hbtUdXa8+6SL0h6mfheGLGoiGsvcE87mehu4tkj4ddUfzHCba+L10lfkzMT/oWgxZIm9p7nIucAAAAA0ZLzoPe8gQm6ypvWJGsvug6vqGUQZ0Qo2p6u1jZto/YEnxZKRzvBCNoetfbX5uuk6WyviZYt6bcjTe29zrP3HjkHAAAAiAhZiWO95/kmbY1N0pubzBVbWkUo2p4WFm1ns1Ca2/yWEWx3XbxOLe010fdUy5cKEXMAAACA6JKdONaDrpJeYAKnaRjhNLAUpqVBKA3J0deiwE7AckNijpwDAAAARJCsNEkPRD3qqaitcWp/bb9OSDkAAABAjASdZE4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMoAsSXYlyYlgKmtf1Nudqals38riXw8AAACgYjEPRCpXkifJtxREMPlpyatgXgGJ3PuVZ/tXWNoRdAAAAIAKxDzXBKpQ0kjSWNJE0jTCaWLtDNraJAZtztQ0sf2qgQl7HoIOAAAAcLycB2KuUt745yuGuqjnkeVD3bilQ93Di4e4MQsHu4feG+RuHF9GYhST9EDQkXMAAACAhN9rqWKuPZna+9wiDnI+bpmI+ZIhbuwiX8zve2cAwhs/OW9igp5r+yEAAAAAcm5y1FDSXNI+NnIuveaj31cxH+h+8lY/hDd+cl5sJ4RaShX0ntODDgAAABkv5/kmSa0lXeMi52MXDZFe84Fu1Oz+7oev9UV44yfn7SXNEn45VS5yDgAAAOD3WGpJS5HJUo9YyPnSoV5Jy/3zB7h7Z17u7p7yVYQ3fnLeSdIy4X9rg5wDAAAAmJxrz6X2YJ4u6RkXOdcLQbWkZeSMvu4//vwVhDd+ct5N0srkPA85BwAAADgm5y0Sfk9mrzjIuV4MOuaDwd6FoPdMv9zd+fylCG/85PzMhF9K1Qg5BwAAADhezjtLLoibnP94Wl93x3NfRnjjJ+fdJW0S/vUOyDkAAABASM619reLpHec5Pxncwe4H72OnCPnAAAAAMg5ck6QcwAAAADkHDlHzgEAAACQc+ScIOcAAAAAyDlyjpwDAAAAIOfIOUHOAQAAAJBz5Bw5BwAAAEDOP3c2HVju0jmcLHWbSpe7N7c+Xm7d/155nfv7J6+7vUe2u4Nl+9ya/e+5iWvvyDg5L9mWOu41S6VSbmepc/PWpdyIycly6z81L+XW7Uq5g5+m3Fq5ffydJHIOAADlycrKWp+bm+tOkHdlte/rtNwOikKbs7Ozf2rt6R2a3ScnJ2em/D2fSEpknQfsA6PaXipCIphql/M1+xe4JXtmeCnZO8cdKtvvzZ+3Y5K33i9WXOm2HlzlzVshy1fum+vKUkdE5A+68au/k5Fy/rcNzs1clfIya3XKrd/tz98lkn7bRH/dP77vz1u9I+WmlaTc7gO+zD/9bqq+yTkhhGRKakx0HxOpnaQRqV1iQr4gmGeSG3U57yptPyxZp8uk3dN0uUw/UgNynm3JSUsuIbWQiva7BtUp5+k94E+v/rbXG6y96CrmUzY+4K23YOefj64za9t4b96c7RMyUs5/OLV8D/hNkg+3+8senpl033y2zB2S3vLNe5y7ZYK/zr//Oekt1570mMr52ZK2kqaSAo6FhJB6/nmbE3LAWv2m8B6VWpHbW9PmR1rO5f6jdr9PsIqI+lLJ3mp68bJCQp5rvUQF1mNZaHJESG2mMLQPas9lKz1JrQk51+w7stNb9sSqm93c7X/wetP/uG7E0eUvfjTSWz53x0Tk3PLGSn/ZY39Nuv+cknT7D6fc5L8nywl86ZGU2/BJbOX8HMlpkha2D3IsJITU98/cPEtOSNLrVs5l/i2SySK9O+X2VZl/VUiYn5A8J5MDtKxE1p9ji/rJ9GyZt0dLZCrY9jU6X5bv13Vsuz1Cy4tluxNk2Q59Xpl+XDI2LOcyPVeWbUsT+D/aOs2qWcwL7I1qbD1GRfYchNRWimzfa2L7YZGVF3TTE9RHllevnP/mw+tdMlXm9Z7/suTq4x6nwv7RgcXeOmFhz/Se8x1+NZC7e8rx4q496X/6wH9sWNhjJufnSTrZvteCYyEhpJ5/5jY2/yuobUE/oZyLAO+2MpeRKsMqzDKdb4I8R+5vluyT6Vmy3ijtaZf7Sdne6zL9XZk30YT5TtvuxbpcskymR+h8md6uch88saz/lpWoPCt375JlKyWH0spaBkr6h9pbKOtskGypxl7zQMwbSRwhEUt7yZmSC09VzrVEZcrGB71M2/Ko1Jev9uav2jf/uMcs2zPz6IWQr2wam7EXhM5ek3JTlia9aD25XhCqTC85vldcy1zKkv7j9KLRm+J7Qej5/N8RQjI0BeaFdS/nWk4S6pkeZYJ8ZSDndn9k6GLThVrHbmcXHrLt12TeLjsDGSrb+a3c9gxtd7xtR3sFL7bn/X24U1xPAiq4IDSgoTzHS/Z33FYdVTTW/jw7Y2rKTkkimI52gd5F45ZV32gtAVsOrHRPrrrluMe8unmcN2pL6ae73YFPP3EvfPTDjB+tJUAv+Bz5l+N7xbXE5WWR+DU7U/JtQ8q9uCi2ZS29+L8jhGRoGljndE6dy7ncDgtJ9rW27rfS5DwoI2kkEp3SXnGR618FkXXm2Xq9bL0O+nyybIzcviLrl9nyIpVrm746rWTlqUrkvJc+n24j1DtfXXKeb73mzexr3I5W43umXRhFSG3lLCth6WQ1v+1tulrkfL6MyjJty6+9TN002j2z9nbvQtATPfbJVd/0RmxZv39hRsr5qNfLvFFZNN97vsxNWODP33so5ZWwVPTY4c+UucWb/aEX//VPsZRzPf6ea/vdWRwLCSH1MN3t2NbVvE/9r7n5YL71nte5nA86kZxrSUvoMa2sFGaxyPS49JhcXKMiLVkk80bLtoZLpgZyLrndpgentefeCuR8iJa7aNmLTF9SndefIuckk+T8s8YrXyqlLNsPrZda9BvKzd9+aK0n6J8l8plyQehKG63l7peT7pezk+4jGV5Rb8PrBMMrjp2ZRM4JIQQ5rxE531OuWFtq0kMXhgZobfgPtC5clr2tdexWuxNs9+WQnPezspb7whvQ8czT5Pwy2c5BvVhVy1qq+TVJL2vRdhVL2tkb1dnetG6E1EJ0X9MhE083MW8bOlmsFTn/+yfTvPWmbhpzdN7/rP6W1wO878gOLghNG63lwRlJd/90f9jEeetTFT7+R6/EUs4vsIv3u9k+ybGQEFIfP3M722dsO/O/puaakSlr+VxyLvzI5PpBLTnRdWWdUr3I07YxXXvOtcfcemC+L/eP2HN92erLl0gOyPR3VMZ17PLgB5ICOZfHP2/P85g+VzhW217dF4SqoOu40q1NjtpZ7yUhNZ12ISHXg0QL2xdPCy4IrWk5/8PaH3gifqis1L23a7L8eugTbtvBtd5j/yrDLCLnfqYsSR4dSlFLWHTIRH3d3tuQcv89N+nmrvMfq7XnN0+I7QWhZ9iJYnuOhYSQevyZ29o+a4vMA6NzQehJyHmeCPJD+gNBgVDrBaH2Ryp9ZNnGYJmVt4y16Q22Tle5Pz+0jv765+iQnOfoBaaV/cJp6LlOVc4rG0oxPNxOc0JqOOGhnZpYiuzgoaJU7UMpVpSXpRZdLwIN0F8H9cX8CuTc8uQ7vpyrhHs/OvRS0q3YWv4CUu1J/7dJZfVlKEWOhYSQ+pg6H0qxpsizrz9bn2BZu9C8LtYTGKbY5tcF/AgRicuPEJ20nJ9MtJxFf0H00RVXfa7H1Rc5P5noRaN3/W/S3fqHeP8dCX6EiBDCjxDFVszrC4GgZ4dEnZ+rJnX9k8LBtRDByWvv2hLzU0kmy3l9iV0s1dZ6lAo4FhJC6vnnbU7IAbMQcwCojBw7o2+JnJNalvPuVtLS2E4S+bACAAAA5Bw5J8g5AAAAAHKOnCPnyDkAAAAAck6QcwAAAADkHDknyDkAAAAAck6QcwAAAADkHDknyDkAAAAAck6QcwAAAID4yvkFyDlBzgEAAADqVs71J9Q7S3rFSc7ve2eA+/G0vu7OSch5DOX8LElrSSPkHAAAAKC8nDeXdJKcHy85H+jumdHXjXjxUoQ3fnJ+BnIOAAAAcLycF0iKJB0k58VBzsctHerGLBzs7p830N375uXursmXIbzxk3Mto9JyqobIOQAAAEB5OW8qaZvwSw16S/pLvi75huRGyc0RynBr03WSf5JcLfmatfefQm0eHrF2Z2qG23v1j5JBkksk59jJoH5j00CSi5wDAAAA+HKuvZZaWqC9mKebOPWRXCYZKBkiuSJCGSYZaqI3QHK5pK+lf6jNwyLW7kxN8F7p+3SxpKeka8K/GLSpnRzm8K8IAAAAkEhkJ/xeS60719IWrQHW2nMdSeMLkl4m6hdGLNqmC6x950u+aLe9bH6fiLY7ExO8V/oe9TAxb5fwe821pCXf9kMAAAAA5DxxrPe8gQl6saR9wu9F17rgbgn/4r2opJulq7Wvi51QdLZ5QaLW7kxNt9B7dJqdAKqY67c1BXZyiJwDAAAAJPwa36D3PN8EvbFJugpUS5N1TasIpTjUtpZpKY5omzM5+l7ocJ3NJE1sPwuLObXmAAAAAEZ24lgPeq5JU6GlYQzSICbtzPQ0sH1K96+8kJjTaw4AAACQRlaapAeiHvXEpZ3k2PuVE9rX6DEHAAAAOIGgE1KbAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAOifLkl1JciKS7M+ZHJLxqWzfyOLfHgAAAKIu5iozuZI8S76lICLJDyVoY7i94XYXkIxP+v6SmybsCDoAAABETsqzTFgCqS2UNJQ0sTSNYLRdjS0NLY3sfpTbTepuX2lo+3a+STqCDgAAAJGU82wTcxWXRj9fMdRFOY8s9zNu6VD38OIhbszCwU7aXXzj+DJHyGfFTuIKQoKOnAMAAECk5DzXZEV7F1vEQc7HLfPlfOyiwe6h9wapcHVGPEkV5by5Cbr2oAclLgAAAACRkPOg11y/8m8haR8XOdde89HvD3L3zxuowtUT8SRVlPN2kmaSBoljvecAAAAAkZBz7TnUHkSty20j6RoLOfd6zYdIr/lAN2pOfxWuSxFPUkU57yJplfB7z/OQcwAAAIianGuteZHkNMk5cZFzrTV/4N1B7idv91PhGox4kirKefeE33ve1E5MkXMAAACInJxrHW4nyRfjJOda0jLyDU/Ov454kirK+XmSDnZCWoCcAwAAQNTkXGtvtd5cv+7vFXU59+rNl8goLR8Mdve9M9DdM6OvCtc/I56kinJ+vqRjwq87L7T/AQAAAIDIyLleDNpS0lXSO3ZyPv1yFa7rEE9SRTn/UsL/lqg5cg4AAADIebXK+YBAzq9HPEkV5bwXcg4AAADIOXJOkHMAAAAA5JwQ5BwAAACQc+ScIOcAAAAAyDkhyDkAAAAg58g5Qc4BAAAAkHNCkHMAAABAziWbDix36RxOlrpNpcvdm1sfL7fuL1Zc6eZsf9ZtP7TWHSordRtLl7kpGx9Azj9HSraljnu9U6mU21nq3Lx1KTdicrLc+vdNK3NLP065/YdTbsPulHtpcdLdMgE5R84BAOqb3WRlrc/NzXUnyLuy2vd1Wm4HRaHN2dnZP7X29A7m5eTkXC9/y0JJqWSJ3P+XGhBBEr1Uu5yv2b/ALdkzw0vJ3jki3/u9+fN2TDq67t8/mebNW79/kftg91S378hO7/6Ujfcj559Tzv+2wbmZq1JeZq1OufW7/fm7RNJvm+iv++CMpCfuH+917tXlKbfSHjt7TQo59/8HOBYQQkjdp9pE9zER2UkalVoT8gXBPFn+QAzkfKjel/a/r+2V2zV6X9p/Uw3IebYlJy25pFZS0eveSFJcXXI+ce0d5eY/vfrbnhhqL7r2mAf3VcyDdX635l9kXtLrZUfOP5+c/3Bq+R7ymyQfbveXPTzTX7ZmZ8p9mky5f5vkrzP8mWPrfO/5jJbzBpX8X3BMIoSQmneQ7FCyasp77zGpvTVtfqTlXNr7pgj5fvuwUs6xv+PFahTz7NAHXp6kwHqtCu0DktRuwq99kaS15AxJn0eWD3Wa6pJzTdAz/sSqm92rmx/2pt/4+Lfl1tn/6S5X+ulu5PwU5Vzzxkp/2WN/TbpvPlvmTS/7uHwv+SvL/HXu/UsyE+W8s31b1MiORQUciwghpFb9Q4+7+SFprzFBP6Gcy/xbJJNFhHfK7asy/6qQMD8heU4mB8jyEll/ji3qJ9OzZd4eLZGpYNvX6HyVa13HttsjtLxYtjtBlu3Q55XpxyVjw3Iu03+VeU+HHlMo6x6Rbb1QA2JeYG9OY0lTE8NmpNZTZK+/poWkneQsyYUqv9Up57/58HqXTJV5veW/LLna/XHdCDd7+wQ3fvXtR9d5ctUt3mO3HFiJnFdDz/kOv5LI3T0l6ZW2/PH9lBv3Vvn1Vmz1H/9/XsiM1+uGpz/1IvvKBZIukla2/zfieEQIIbUaPd42sbLaQuu0rTFBP6Gci/DutjKXkTK9TYXZzhpUkOfI/c2SfTI9S9YbpT3tcj8p23tdpr8r8yaaVN9p271Yl0uWyfQInS/T21XugyeW9d/Sx8hjn5W7d8mylZJD6TXnhn5I9dUec11HpodVo5wHYq7P4Ugkc7bkonFLfTn/vIIeyPmsbeOldvxBL9O2POq2HlztzV+1b36Fj9NSF61TV6Zt+TVy/jnlXOvGpyxNeplW4l8Qqkwvqbye/L/nJr11Fm12GfN6pcm5lnC1sRNT/vcJIaRuExb02pVzLScJ9ZSPMkG+MpBzuz8ydLGpXqC5JBEaTUC2/ZrM22U9PUNlO7+V256h7Y637egZycX2vL8PV7XoSUAlcv7V4EJWrZe3F+uUq2is/XnWY96UnTCyOUf3GZXzk+k9r2i0lgDtEdfe8fTH/ObDG9y60g+8dZbvfVvmXYGcn8JoLQG7Dzg3spJyFe1F128xtkvvelCDnmFy3ttKuNpZ7zn/+4QQUrdpGCpxya7tmvNhIcm+1tb9VpqcNwt6sUWiU9orLnL9qyCyzjxbr5et10GfT5aNkdtXZP0yW14k92+z6avTas6fqkTO9YXpqm3SHny5nVGNcp5vvebNrMeqo/VenWk9tqT20t1e965We6tf8XeTnCf5BxXfU5Hz+TIqi/aAa6ZuGu2eWXu71zuevv7EdXd4tegqiu/ufLHCdZDzz5bzUa+XeaUrGr24c8ICf/7eQymv3jxY/9Y/lLkPNvnyro+9fVJmvV4VyPlpkrYm6eHjUXeOEYQQUmM5y5yjkx2DW1iHc2FdyfmgE8m5CnHoMa2sFGaxyPS49Ngfdo3KuGSRzBst2xoumRrIueR2mx6c1p57Q3KuF0X9p6R/msA/oSUzthw5r3+pMTmv6ILQ9Dyz5t/cwbJ93gWgL340kh8hquYLQlfaSCx3v+wvu0UkffFmfxz0FxalvNFaMu31Qs4JIQQ5Pxk531OuWFtq0kMXhgaoRP9A/whZ9rbWsVstd7Ddl0Ny3s/KWu4Lb0DWmRmSc6+H3sZjr2idttVc1qLtKg59IHa2D8VupFbSNSTlp+s3L3bbxcpaalzOf73yGhmZZacn5k+tvpVfCK3B0Vp0fPNyo7fMTmaclFehrKU1xyNCCKk1Bwn8o70dg5tZWUtBLORc+JHJ9YNaxmLlJqV6kadtY7r2nGuPudw9V59HR1mx5/qy1ZcvkRyQ6e/oh5Js65GgrjxxbCjFF1TQdVQXuft1uX3U2jazhi4ILbIe+dahnqv2pNbSzl731vaP0c7+UY5eEFqTcv7Shp966+08tMEt3fNGuSza/SpyXg1yPmVJ8uhQitpLfuCIjDNflnJvrTo+6b8kmoEXhDa341ErjkeEEFIr/tHGjrvNQr3m4RFbIi3neSLKD8n8w6ELNV8zqVL6yLKNwTIrbxlr0xtsna5yf35onRItgUmrOW+lQzyG1knJ/eftw6q65LyyoRTDQ+s0JzWe8FCKRaETpfb2VdNFgZjXlJz/bdeUSi9k1FIX5PzU5fzJd3w5n7su5Ua+mnQnQmvWGUrxuKEUOVYQQkjN+EdFQymGLwatsR8jqm70bKJHSMorWtYuNK+L1VOGKbb5J6KJ9cA3qeb28yNE0f8RojaJU/wRotoKck6q8UeIGif4ESJCCKnLHyFKH+M8NnJeHwgEPTsk6vxMdt3/fG6e9RwWWz1Y7yiLOXJOqknOO1lPToNExT8pzXGCEEJq3kFyQl6ImAPYP0GOfa3U0upwe0dZzJFzUs1yXpgI/Y4EAAAAAHKOnBPkHAAAAAA5J8g5cg4AAADIOXJOkHMAAAAA5Jwg58g5AAAAIOfIOUHOAQAAAJBzQpBzAAAAQM6Rc4KcAwAAAGScnF+HeJIqyvmXkHMAAACIspzrryS2kHSRXBA7OZ/RV4XrWsSTVFHOz5ecbnJegJwDAABAVOVcexO/FCc5v3/eQDfSl/NrEE9SRTnvKekoaYacAwAAQBTlvNBERYXlvDjI+bilQ92YhSLn8we4e2d6ZS1XIJ6kinJ+jqS9pKnJeTaHAgAAAIiSnBeYqLSTnCXpI+kv+ZrkG5KbJDdHJMMlN0iutfYNkfSzDJAMk1wtuU5yY4TaTeomN9q+cpXkcskFkjMkbSRNJPnIOQAAAERJzrNNUBpLihN+aYv2LF4oucyEd6hJ7xV1nGGWwXby8FXJJdZWPaG4SHKpSdhAE/cotJvU3f4yxPbhr0h6S85O+N8QaRmXXgidh5wDAABA1OQ8N+HXnRdJWpugd5d8IeEPO9fHBDgqUcnSETd62onE2ZYekvMkX4xou0ntp4/tK7ov67dCeiFoq8SxkpZc5BwAAACiJOdBaYv2njc0QVd5aW8ioyO4dLOcEYFoO7pau/QkooPlNMvpNj9q7SZ1k2Bf0f1Cy7aKTcz1ZDTP9n3kHAAAACJF0Hueb9LS2CRdv/pvabIelRRbtF3NLc1CaR5qd3HE2k7qdl9pavt2oe3rgZhncQgAAACAqMl5tslKnolLoYm6pmEE08DaWBhqa7jNUW03qZsUhqQ8NyTm9JoDAABAJMlKk/QguRFOejvj0m5Sd/tLdij0mAMAAEDkBZ2QTAkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAhMmSZFeSnAimsrZFuc3V9TenJ8sCAAAAAPVIzAMJzJXkSfItBRFMftp0fiXL6lPy7X3Js/coLO0IOgAAAEA9E/NcE8AGkkaSxpKmEU1RhNtWk2li70tDE/ZA1BF0AAAAgHoi5zkmeYUqfj9fMdRFOY8sH+rGLR3qHl4yxI1ZONg99N4gd+P4soyMSXp+mqADAAAAQEzJNrHTXljtkW0ZdTkft8wX87GLfDG/752BmSznRSFBD0pcAAAAACDGcp5ngtdCclpc5Hz0+4Pc/fMGup+83S+T5bxNwi93KUwc6z0HAAAAgBjLufa6NjHROyMOcj520RCv13zUnP7ux9P6ZrKcd5S0tJOrPOQcAAAAIN5oKYSWtDSTdJCcG3k5XzrUK2l54N1BXq/5f029LJPlvJukdcIvSULOAQAAAOqBnGtJhJa0dJKcHwc51wtB758/wI18o5+7a3JGy3l3SbuE/81HUHcOAAAAADFF65R16EQtjegi6R0XOdcLQUfO6OtGvHhpJsv5OZL2Cb/uvAA5BwAAAKg/ct5VcmGc5PwelfMXMlrOz5WclvBHbUHOAQAAAOqJnBcn/PrlGMn5AHfP9Mvdnc9ntJyfh5wDAAAAIOfIOXIOAAAAAMg5co6cAwAAACDnyDlyDgAAAADIOXKOnAMAAAAg58g5cg4AAAAAyDlyjpwDAAAAIOcnJc2bDix36RxOlrpNpcvdm1sfr/Rx/7vxZ25j6TI3c+sTkZPzkm2p4/6mVCrldpY6N29dyo2YnKzwcf/6pzL39y0p996GFHIOAPWTrKys9bm5ue4EeVdW+75Oy+2gKLQ5Ozv7p9ae3hUsbi3LZss6v6/ul4oQQj5Hql3O1+xf4JbsmeGlZO8cd6hsvzd/3o5Jxz3mtx/e6A58+om3/P1dUyIr53/b4NzMVSkvs1an3Prd/vxdIum3TTz+cXPX+ct3H3A11rYbnv60MjlnvyaEBKlR0X0sJydnkkZEfYkJ+YJgnix/IE5yLm2erMvkb1lYQ3KebclJSy4hJKOTfkzIkzSsTjmfuPaOcvOfXv1tr7dZe9F/seLKcstW7pvr9hzZFnk5/+HU8j3kN0k+3O4ve3hm+WW/mJV0hz9NuU8O1pqcd5A0kxTa+8nxnpDMPKZnpyWrNr33HhVbEdxb0+bHQs6l3beIlB+QfFzNcp4VEvJcO0gX2AG70HrHCCEkSIHdNtFv8yRnqJw/srx65Vyz78hOb9kTq24+Ou+VTWNdWeqIV9YSNznXvLHSX/bYX48t++5zZW7voZT7w3spt2ZnqsbkXMX8ht8d0c+XnpLTJS0lje1Ei+M9IZmVwPP0mJ4fEvdalfQTyrnKr/ZMi/julNtXZf5VIWF+QvKcTA6Q5SWy/hxb1E/LTGTeHi2RqWDb1+h8Wb5f17Ht9ggtL5btTpBlO/R5ZfpxydgK5LyjLP9Ebv9dbt+vRjlPF/PgQ1cP1k3tK89mhBBiaWpS3sTut5WcKbmouuX8Nx9e75KpMq/3/JclV3vzVNIPlu1zc7f/wf1uzb/Esud8h1+t4+6ecmzZgo9Sbu2ulBv+TFmNyXkg5jf87rB+vnxR0kXSRtLC3kuO94Rk7jG9kYl6fkjQ617ORXh3W5nLSJnepsJsjUyojMv9zZJ9Mj1L1hulPe1yPynbe12mvyvzJppU32nbvViXS5bJ9AidL9PbVe6DJ5b139LHyGOflbt3ybKVkkNpcp4lzzHdTgiya0jOAzHXN8cRQkgVo6UR3fV4N27Zqcn5rG3j3ZSND3qZtuVRt/Xgam/+qn3zj66rdek7Dn0ksv61WMj57DUpN2Vp0su0Ev+CUGV6ybELPh+bnZSTkNRRka9pOb/+qUP6vp1v5Ujsw4SQcGpd0E8o51pOEuopH2WCfGUg53Z/5FGrFUHWOvZE6Cp32fZrMm+X9TwPle381r4+DLY73rajZykX2/OGL+5U+d6cJuff03IW651KVLOcZ4fqRxvYGRQ7JyGkqjndvg38h1OV84rYcmCle3LVLd56r2/5hdeL/qd1d3n34yDnFaHiPfIvvoh/7/kyt/9wyr289Fgveo3JufWaX/8/B/V9u0ByFvsvISQtQQ96NORcboeFJPtaW/dbaXLezFZpJIKc0l5xketfBZF15tl6vWy9Dvp8smyM3L4i65fZ8iK5f5tNX51Wc/5USM67yGNK5faO0ElBTch5vr0hzexrzo6SrnZCcDYhhJjMaX15ZztGdLDpcwI519KWz1veEsj5fBmVZdqWX3uZumm0e2bt7eUuBC39dLc3dOKLH93jRWVd+XDfPO9+FOV81Otl3qgsGhXxCQv8+Vpf/s1ny9wry1LeRaDj3kq6+6aXefl4r3P7ZLlOay169cv5geDzpYf1nne12zPtWxD2dUIy55iu//udJO0T/gX+TayzNrhIvM7lfNCJ5FxLWkKPaWWlMItFpselx/7Ya1TGJYtk3mjZ1nDJ1EDOJbfb9OC09twbkvOv2fOsspIXr+zFstKeBzknhMReziu6IDScQ2Wl7kSkj+gS1QtCV9poLXe/nHQzVqZO+Df9/O0kck4IqeljeljOm8ZNzveUK9aWmvTQhaEB/SU/0K8EZNnbWsdutdzBdl8OyXk/K2u5L7wBWWdmSM67a7lNOLLNLTpii5XhtKrmspYie3Pa2Ydv59CBmxCSuelqFxDqQfw0O5C3s/s9akPOtZxl0vr/OppXNz/sPU6HVdT7cRut5cEZSffvLyXdT14rn817/J5znf7On2q0rKW7HeM7WYLjPcd8Qur/8Tw4pp9ux3TtmG1uZdlBWUv85Fz4kcn1g1rGoutqCYpe5GnbmK4959pjLnfP1eeR+0fsub5s9eVLrJ78Oyrjsq1Hgh9ISlT8I0SJWrggVAVdh9fSIdLa2gdwe0JIRqedpY2dwBdb50AH64E55QtCP0vO0xPH0Vo0U5YkjxtKMZxaviC0gx3n23K8JyTj0jZ0TG9uJS0NI3VB6EnIeZ7I9EMy/3Ag1HpBqEmt0keWbQyWWXnLWJveYOt0lfvzQ+uUaAlMHch5ZUMphofbaU4IyegEw+01seNDc5M5/Wr0opoY57w+yvmT7/hyrr8GWhdyXsWhFNnfCanfx/Jmacf0RuZ/+aFe81r/QaLqJM++1m19gmXtQvO62FcIYYptfl3AjxARQk72R4jaWM3iSct5baU25TyqqeRHiILeMo73hGTuDxHlm//V+o8QwWcLenZI1PkpZ0JIZT/3nG9C18rKIy6Mspgj5+UFXd6vL1hJSzP7YM7jeE9IRh/Ts9OCmAMAxIxc63UpRs7jF3m/zrNvcIvsm5AcdmkAAAAA5Bw5R84BAAAAADlHzpFzAAAAAOQcOUfOAQAAAAA5R86RcwAAAADkHDlHzgEAAAAAOUfOkXMAAAAA5Bw5R84BAAAAADn/bDkf8QJyjpwDAAAA1C85159/7yrpEx85H+jumdHXjXgxo+X8XOQcAAAAoP7JeQtJZ0mvuMj5/fMGupEi5//x569kspz3kLSXNJXkI+cAAAAA8UZlrlDSXNJR0jMOcj520WD3wLuD3L0zL3f/OSWj5fxMSRtJE+QcAAAAoH7IuZZDaFmE9sB2l1woGSi5WnKd5CbJzRHMcGvbjZIbbHp4aPktEW33yf6t10uukQyVfEVPpBL+tx16vUAjSZ4km10aAAAAIN5yrj2ujU3yVPa0jvkiyVclg0wGr4hYhtntUJsOboeFll8ZwXafbPTvGyzpJ7lEcn7C7zXXE6pmCb80CTkHAAAAiDkqc0HdufaetzFBPzvh98xeIOmT8HvTo5SL7PbiCuZdZPMvimC7TyW9Tcr15ElH1tELQVvaiVVQ0oKcAwAAAMRczoPe84Ym6K1M/DpJupgInhHhnJmWM0K39SX6HnS1E6cOdhKlF/FqrXmhnWAh5wAAAAAxJytxrPdcBV170BubpKv8ac9ssQl7VBK0p3XotnXofvp69SXF9n5oGYuOztLIxDwvJOZZ7NIAAAAA8SY7cawHXUWvwKRPRb1hRNOogvuNKllWn9LA3psCO5nKRcwBAAAA6h9ZaZKeY+IXl+Sl3dbn5ISEPJByxBwAAACgHgo6iWcAAE6K/w9cZlqkCNlgBQAAAABJRU5ErkJggg==) + +- EatWhatYouKill:这是 Jetty 对 ExecuteProduceConsume 策略的改良,在线程池线程充足的情况下等同于 ExecuteProduceConsume;当系统比较忙线程不够时,切换成 ProduceExecuteConsume 策略。为什么要这么做呢,原因是 ExecuteProduceConsume 是在同一线程执行 I/O 事件的生产和消费,它使用的线程来自 Jetty 全局的线程池,这些线程有可能被业务代码阻塞,如果阻塞得多了,全局线程池中的线程自然就不够用了,最坏的情况是连 I/O 事件的侦测都没有线程可用了,会导致 Connector 拒绝浏览器请求。于是 Jetty 做了一个优化,在低线程情况下,就执行 ProduceExecuteConsume 策略,I/O 侦测用专门的线程处理,I/O 事件的处理扔给线程池处理,其实就是放到线程池的队列里慢慢处理。 + +分析了这几种线程策略,我们再来看看 Jetty 是如何实现 ExecutionStrategy 接口的。答案其实就是实现 produce 接口生产任务,一旦任务生产出来,ExecutionStrategy 会负责执行这个任务。 + +``` +private class SelectorProducer implements ExecutionStrategy.Producer +{ + private Set _keys = Collections.emptySet(); + private Iterator _cursor = Collections.emptyIterator(); + + @Override + public Runnable produce() + { + while (true) + { + // 如何 Channel 集合中有 I/O 事件就绪,调用前面提到的 Selectable 接口获取 Runnable, 直接返回给 ExecutionStrategy 去处理 + Runnable task = processSelected(); + if (task != null) + return task; + + // 如果没有 I/O 事件就绪,就干点杂活,看看有没有客户提交了更新 Selector 的任务,就是上面提到的 SelectorUpdate 任务类。 + processUpdates(); + updateKeys(); + + // 继续执行 select 方法,侦测 I/O 就绪事件 + if (!select()) + return null; + } + } + } +``` + +SelectorProducer 是 ManagedSelector 的内部类,SelectorProducer 实现了 ExecutionStrategy 中的 Producer 接口中的 produce 方法,需要向 ExecutionStrategy 返回一个 Runnable。在这个方法里 SelectorProducer 主要干了三件事情 + +1. 如果 Channel 集合中有 I/O 事件就绪,调用前面提到的 Selectable 接口获取 Runnable,直接返回给 ExecutionStrategy 去处理。 +2. 如果没有 I/O 事件就绪,就干点杂活,看看有没有客户提交了更新 Selector 上事件注册的任务,也就是上面提到的 SelectorUpdate 任务类。 +3. 干完杂活继续执行 select 方法,侦测 I/O 就绪事件。 + +## 参考资料 + +- [Jetty 官方网址](http://www.eclipse.org/jetty/index.html) +- [Jetty Github](https://github.com/eclipse/jetty.project) +- [Jetty wiki](http://wiki.eclipse.org/Jetty/Reference/jetty-env.xml) \ No newline at end of file diff --git "a/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" new file mode 100644 index 00000000..9e9a4473 --- /dev/null +++ "b/docs/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" @@ -0,0 +1,39 @@ +--- +title: Java 服务器 +date: 2022-02-17 22:34:30 +categories: + - Java + - JavaEE + - 服务器 +tags: + - Java + - JavaWeb + - 服务器 +permalink: /pages/e3f3f3/ +hidden: true +index: false +--- + +# Java 服务器 + +## 📖 内容 + +- [Tomcat 快速入门](01.Tomcat/01.Tomcat快速入门.md) +- [Tomcat 连接器](01.Tomcat/02.Tomcat连接器.md) +- [Tomcat 容器](01.Tomcat/03.Tomcat容器.md) +- [Tomcat 优化](01.Tomcat/04.Tomcat优化.md) +- [Tomcat 和 Jetty](01.Tomcat/05.Tomcat和Jetty.md) +- [Jetty](02.Jetty.md) + +## 📚 资料 + +- [Tomcat 官网](http://tomcat.apache.org/) +- [Jetty 官网](http://www.eclipse.org/jetty/index.html) +- [Jetty Github](https://github.com/eclipse/jetty.project) +- [Nginx 官网](https://www.nginx.com/) +- [Nginx 的中文维基](http://tool.oschina.net/apidocs/apidoc?api=nginx-zh) +- [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git a/docs/01.Java/02.JavaEE/README.md b/docs/01.Java/02.JavaEE/README.md new file mode 100644 index 00000000..846c3677 --- /dev/null +++ b/docs/01.Java/02.JavaEE/README.md @@ -0,0 +1,61 @@ +--- +title: JavaEE +date: 2022-02-18 08:53:11 +categories: + - Java + - JavaEE +tags: + - Java + - JavaEE +permalink: /pages/80a822/ +hidden: true +index: false +--- + +# JavaEE + +## 📖 内容 + +### JavaWeb + +- [JavaWeb 面经](01.JavaWeb/99.JavaWeb面经.md) +- [JavaWeb 之 Servlet 指南](01.JavaWeb/01.JavaWeb之Servlet指南.md) +- [JavaWeb 之 Jsp 指南](01.JavaWeb/02.JavaWeb之Jsp指南.md) +- [JavaWeb 之 Filter 和 Listener](01.JavaWeb/03.JavaWeb之Filter和Listener.md) +- [JavaWeb 之 Cookie 和 Session](01.JavaWeb/04.JavaWeb之Cookie和Session.md) + +### Java 服务器 + +> Tomcat 和 Jetty 都是 Java 比较流行的轻量级服务器。 +> +> Nginx 是目前最流行的反向代理服务器,也常用于负载均衡。 + +- [Tomcat 快速入门](02.服务器/01.Tomcat/01.Tomcat快速入门.md) +- [Tomcat 连接器](02.服务器/01.Tomcat/02.Tomcat连接器.md) +- [Tomcat 容器](02.服务器/01.Tomcat/03.Tomcat容器.md) +- [Tomcat 优化](02.服务器/01.Tomcat/04.Tomcat优化.md) +- [Tomcat 和 Jetty](02.服务器/01.Tomcat/05.Tomcat和Jetty.md) +- [Jetty](02.服务器/02.Jetty.md) + +## 📚 资料 + +- **JavaWeb** + - **书籍** + - [Java Web 整合开发王者归来](https://book.douban.com/subject/4189495/) + - [Head First Servlets & JSP](https://book.douban.com/subject/1942934/) + - **教程** + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) + - [Servlet 教程](https://www.runoob.com/servlet/servlet-tutorial.html) + - [博客园孤傲苍狼 JavaWeb 学习总结](https://www.cnblogs.com/xdp-gacl/tag/JavaWeb%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/) + - [JSP 教程](https://www.runoob.com/jsp/jsp-tutorial.html) +- **服务器** + - [Tomcat 官网](http://tomcat.apache.org/) + - [Jetty 官网](http://www.eclipse.org/jetty/index.html) + - [Jetty Github](https://github.com/eclipse/jetty.project) + - [Nginx 官网](https://www.nginx.com/) + - [Nginx 的中文维基](http://tool.oschina.net/apidocs/apidoc?api=nginx-zh) + - [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" new file mode 100644 index 00000000..b2efbdda --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -0,0 +1,557 @@ +--- +title: Maven 快速入门 +date: 2020-02-07 23:04:47 +order: 01 +categories: + - Java + - 软件 + - 构建 + - Maven +tags: + - Java + - 构建 + - Maven +permalink: /pages/e5b79f/ +--- + +# Maven 快速入门 + +## Maven 简介 + +### Maven 是什么 + +[Maven](https://github.com/apache/maven) 是一个项目管理工具。它负责管理项目开发过程中的几乎所有的东西。 + +- **版本** - maven 有自己的版本定义和规则。 +- **构建** - maven 支持许多种的应用程序类型,对于每一种支持的应用程序类型都定义好了一组构建规则和工具集。 +- **输出物管理** - maven 可以管理项目构建的产物,并将其加入到用户库中。这个功能可以用于项目组和其他部门之间的交付行为。 +- **依赖关系** - maven 对依赖关系的特性进行细致的分析和划分,避免开发过程中的依赖混乱和相互污染行为 +- **文档和构建结果** - maven 的 site 命令支持各种文档信息的发布,包括构建过程的各种输出,javadoc,产品文档等。 +- **项目关系** - 一个大型的项目通常有几个小项目或者模块组成,用 maven 可以很方便地管理。 +- **移植性管理** - maven 可以针对不同的开发场景,输出不同种类的输出结果。 + +### Maven 的生命周期 + +maven 把项目的构建划分为不同的生命周期(lifecycle)。粗略一点的话,它这个过程(phase)包括:编译、测试、打包、集成测试、验证、部署。maven 中所有的执行动作(goal)都需要指明自己在这个过程中的执行位置,然后 maven 执行的时候,就依照过程的发展依次调用这些 goal 进行各种处理。 + +这个也是 maven 的一个基本调度机制。一般来说,位置稍后的过程都会依赖于之前的过程。当然,maven 同样提供了配置文件,可以依照用户要求,跳过某些阶段。 + +### Maven 的标准工程结构 + +Maven 的标准工程结构如下: + +```shell +|-- pom.xml(maven的核心配置文件) +|-- src +|-- main + |-- java(java源代码目录) + |-- resources(资源文件目录) +|-- test + |-- java(单元测试代码目录) +|-- target(输出目录,所有的输出物都存放在这个目录下) + |-- classes(编译后的class文件存放处) +``` + +### Maven 的"约定优于配置" + +所谓的"约定优于配置",在 maven 中并不是完全不可以修改的,他们只是一些配置的默认值而已。但是除非必要,并不需要去修改那些约定内容。maven 默认的文件存放结构如下: + +每一个阶段的任务都知道怎么正确完成自己的工作,比如 compile 任务就知道从 src/main/java 下编译所有的 java 文件,并把它的输出 class 文件存放到 target/classes 中。 + +对 maven 来说,采用"约定优于配置"的策略可以减少修改配置的工作量,也可以降低学习成本,更重要的是,给项目引入了统一的规范。 + +### Maven 的版本规范 + +maven 使用如下几个要素来唯一定位某一个输出物: + +- **groupId** - 团体、组织的标识符。团体标识的约定是,它以创建这个项目的组织名称的逆向域名(reverse domain name)开头。一般对应着 JAVA 的包的结构。例如 org.apache +- **artifactId** - 单独项目的唯一标识符。比如我们的 tomcat, commons 等。不要在 artifactId 中包含点号(.)。 +- **version** - 一个项目的特定版本。 +- **packaging** - 项目的类型,默认是 jar,描述了项目打包后的输出。类型为 jar 的项目产生一个 JAR 文件,类型为 war 的项目产生一个 web 应用。 + +例如:想在 maven 工程中引入 4.12 版本的 junit 包,添加如下依赖即可。 + +```xml + + junit + junit + 4.12 + compile + +``` + +maven 有自己的版本规范,一般是如下定义 ``、``、`-` ,比如 1.2.3-beta-01。要说明的是,maven 自己判断版本的算法是 major,minor,incremental 部分用数字比 较,qualifier 部分用字符串比较,所以要小心 alpha-2 和 alpha-15 的比较关系,最好用 alpha-02 的格式。 + +maven 在版本管理时候可以使用几个特殊的字符串 SNAPSHOT,LATEST,RELEASE。比如"1.0-SNAPSHOT"。各个部分的含义和处理逻辑如下说明: + +- **SNAPSHOT** - 这个版本一般用于开发过程中,表示不稳定的版本。 +- **LATEST** - 指某个特定构件的最新发布,这个发布可能是一个发布版,也可能是一个 snapshot 版,具体看哪个时间最后。 +- **RELEASE** - 指最后一个发布版。 + +## Maven 安装 + +> Linux 环境安装可以使用我写一键安装脚本:https://github.com/dunwu/linux-tutorial/tree/master/codes/linux/ops/service/maven + +### 环境准备 + +Maven 依赖于 Java,所以本地必须安装 JDK。 + +打开控制台,执行 `java -version` 确认本地已安装 JDK。 + +```shell +$ java -version +java version "1.8.0_191" +Java(TM) SE Runtime Environment (build 1.8.0_191-b12) +Java HotSpot(TM) 64-Bit Server VM (build 25.191-b12, mixed mode) +``` + +### 下载解压 + +进入 **[官网下载地址](https://maven.apache.org/download.cgi)**,选择合适版本,下载并解压到本地。解压命令如下: + +```shell +# 以下解压命令分别针对 zip 包和 tar 包 +unzip apache-maven-3.6.3-bin.zip +tar xzvf apache-maven-3.6.3-bin.tar.gz +``` + +### 环境变量 + +添加环境变量 `MAVEN_HOME`,值为 Maven 的安装路径。 + +#### 配置 Unix 系统环境变量 + +输入 `vi /etc/profile` ,添加环境变量如下: + +```shell +# MAVEN 的根路径 +export MAVEN_HOME=/opt/maven/apache-maven-3.5.2 +export PATH=$MAVEN_HOME/bin:$PATH +``` + +执行 `source /etc/profile` ,立即生效。 + +#### 配置 Windows 系统环境变量 + +右键 "计算机",选择 "属性",之后点击 "高级系统设置",点击"环境变量",来设置环境变量,有以下系统变量需要配置: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200108143017.png) + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200108143038.png) + +### 检测安装成功 + +检验是否安装成功,执行 `mvn -v` 命令,如果输出类似下面的 maven 版本信息,说明配置成功。 + +```shell +$ mvn -v +Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-18T02:33:14+08:00) +Maven home: /opt/maven/apache-maven-3.5.4 +Java version: 1.8.0_191, vendor: Oracle Corporation, runtime: /mnt/disk1/jdk1.8.0_191/jre +Default locale: zh_CN, platform encoding: UTF-8 +OS name: "linux", version: "3.10.0-327.el7.x86_64", arch: "amd64", family: "unix" +``` + +### Maven 配置文件 + +`setting.xml` 文件是 Maven 的默认配置文件,其默认路径为:`/conf/settings.xml`。 + +如果需要修改 Maven 配置,直接修改 `setting.xml` 并保持即可。 + +例如:想要修改本地仓库位置可以按如下配置,这样,所有通过 Maven 下载打包的 jar 包都会存储在 `D:\maven\repo` 路径下。 + +```xml + + D:\maven\repo + + +``` + +## 快速入门 + +### 创建 Maven 工程 + +#### 初始化工程 + +执行指令: + +```shell +mvn archetype:generate -DgroupId=com.mycompany.app -DartifactId=my-app -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false +``` + +会在当前路径新建一个名为 `my-app` 的 Maven 工程,其目录结构如下: + +```shell +my-app +|-- pom.xml +`-- src + |-- main + | `-- java + | `-- com + | `-- mycompany + | `-- app + | `-- App.java + `-- test + `-- java + `-- com + `-- mycompany + `-- app + `-- AppTest.java +``` + +其中, `src/main/java` 目录包含 java 源码, `src/test/java` 目录包含 java 测试源码,而 `pom.xml` 文件是 maven 工程的配置文件。 + +#### POM 配置 + +pom.xml 是 maven 工程的配置文件,它描述了 maven 工程的构建方式,其配置信息是很复杂的,这里给一个最简单的配置示例: + +```xml +project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + + com.mycompany.app + my-app + 1.0-SNAPSHOT + + + 1.7 + 1.7 + + + + + junit + junit + 4.12 + test + + + +``` + +#### 构建项目 + +执行以下命令,即可构建项目: + +```shell +mvn clean package -Dmaven.test.skip=true -B -U +``` + +构建成功后,会输出类似下面的信息: + +```shell +... +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +[INFO] Total time: 2.953 s +[INFO] Finished at: 2019-11-24T13:05:10+01:00 +[INFO] ------------------------------------------------------------------------ +``` + +这时,在当前路径下会产生一个 `target` 目录,其中包含了构建的输出物,如:jar 包、class 文件等。 + +这时,我们可以执行以下命令启动 jar 包: + +```shell +java -cp target/my-app-1.0-SNAPSHOT.jar com.mycompany.app.App +``` + +### 在 Intellij 中创建 Maven 工程 + +(1)创建 Maven 工程 + +依次点击 File -> New -> Project 打开创建工程对话框,选择 Maven 工程。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/1555414103572.png) + +(2)输入项目信息 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/1555415549748.png) + +(3)点击 Intellij 侧边栏中的 Maven 工具界面,有几个可以直接使用的 maven 命令,可以帮助你进行构建。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/1555415806237.png) + +### 在 Eclipse 中创建 Maven 工程 + +(1)Maven 插件 + +在 Eclipse 中创建 Maven 工程,需要安装 Maven 插件。 + +一般较新版本的 Eclipse 都会带有 Maven 插件,如果你的 Eclipse 中已经有 Maven 插件,可以跳过这一步骤。 + +点击 Help -> Eclipse Marketplace,搜索 maven 关键字,选择安装红框对应的 Maven 插件。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195117.png) + +(2)Maven 环境配置 + +点击 Window -> Preferences + +如下图所示,配置 settings.xml 文件的位置 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195128.png) + +(3)创建 Maven 工程 + +File -> New -> Maven Project -> Next,在接下来的窗口中会看到一大堆的项目模板,选择合适的模板。 + +接下来设置项目的参数,如下: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195151.png) + +**groupId**是项目组织唯一的标识符,实际对应 JAVA 的包的结构,是 main 目录里 java 的目录结构。 + +**artifactId**就是项目的唯一的标识符,实际对应项目的名称,就是项目根目录的名称。 + +点击 Finish,Eclipse 会创建一个 Maven 工程。 + +(4)使用 Maven 进行构建 + +Eclipse 中构建方式: + +在 Elipse 项目上右击 -> Run As 就能看到很多 Maven 操作。这些操作和 maven 命令是等效的。例如 Maven clean,等同于 mvn clean 命令。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195208.png) + +你也可以点击 Maven build,输入组合命令,并保存下来。如下图: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195219.png) + +Maven 命令构建方式: + +当然,你也可以直接使用 maven 命令进行构建。 + +进入工程所在目录,输入 maven 命令就可以了。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127195243.png) + +## 使用说明 + +### 如何添加依赖 + +在 Maven 工程中添加依赖 jar 包,很简单,只要在 POM 文件中引入对应的``标签即可。 + +参考下例: + +```xml + + + 4.0.0 + com.zp.maven + MavenDemo + 0.0.1-SNAPSHOT + jar + MavenDemo + http://maven.apache.org + + + UTF-8 + 3.8.1 + + + + + junit + junit + ${junit.version} + test + + + + log4j + log4j + 1.2.12 + compile + + + +``` + +`` 标签最常用的四个属性标签: + +- `` - 项目组织唯一的标识符,实际对应 JAVA 的包的结构。 +- `` - 项目唯一的标识符,实际对应项目的名称,就是项目根目录的名称。 +- `` - jar 包的版本号。可以直接填版本数字,也可以在 properties 标签中设置属性值。 +- `` - jar 包的作用范围。可以填写 compile、runtime、test、system 和 provided。用来在编译、测试等场景下选择对应的 classpath。 + +### 如何寻找 jar 包 + +可以在 [http://mvnrepository.com/](http://mvnrepository.com/) 站点搜寻你想要的 jar 包版本 + +例如,想要使用 log4j,可以找到需要的版本号,然后拷贝对应的 maven 标签信息,将其添加到 pom .xml 文件中。 + +### 如何使用 Maven 插件(Plugin) + +要添加 Maven 插件,可以在 pom.xml 文件中添加 `` 标签。 + +```xml + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 1.7 + 1.7 + + + + +``` + +`` 标签用来配置插件的一些使用参数。 + +### 如何一次编译多个工程 + +假设要创建一个父 maven 工程,它有两个子工程:my-app 和 my-webapp: + +```shell ++- pom.xml ++- my-app +| +- pom.xml +| +- src +| +- main +| +- java ++- my-webapp +| +- pom.xml +| +- src +| +- main +| +- webapp +``` + +app 工程的 pom.xml 如下,重点在于在 modules 中引入两个子 module: + +```xml + + 4.0.0 + + com.mycompany.app + app + 1.0-SNAPSHOT + pom + + + my-app + my-webapp + + +``` + +选择编译 XXX 时,会依次对它的所有 Module 执行相同操作。 + +### 常用 Maven 插件 + +> 更多详情请参考:[https://maven.apache.org/plugins/](https://maven.apache.org/plugins/) + +#### [maven-antrun-plugin](http://maven.apache.org/plugins/maven-antrun-plugin/) + +maven-antrun-plugin 能让用户在 Maven 项目中运行 Ant 任务。用户可以直接在该插件的配置以 Ant 的方式编写 Target, 然后交给该插件的 run 目标去执行。在一些由 Ant 往 Maven 迁移的项目中,该插件尤其有用。此外当你发现需要编写一些自定义程度很高的任务,同时又觉 得 Maven 不够灵活时,也可以以 Ant 的方式实现之。maven-antrun-plugin 的 run 目标通常与生命周期绑定运行。 + +#### [maven-archetype-plugin](http://maven.apache.org/archetype/maven-archetype-plugin/) + +Archtype 指项目的骨架,Maven 初学者最开始执行的 Maven 命令可能就是**mvn archetype:generate**,这实际上就是让 maven-archetype-plugin 生成一个很简单的项目骨架,帮助开发者快速上手。可能也有人看到一些文档写了**mvn archetype:create**, 但实际上 create 目标已经被弃用了,取而代之的是 generate 目标,该目标使用交互式的方式提示用户输入必要的信息以创建项目,体验更好。 maven-archetype-plugin 还有一些其他目标帮助用户自己定义项目原型,例如你由一个产品需要交付给很多客户进行二次开发,你就可以为 他们提供一个 Archtype,帮助他们快速上手。 + +#### [maven-assembly-plugin](http://maven.apache.org/plugins/maven-assembly-plugin/) + +maven-assembly-plugin 的用途是将项目打包,该包可能包含了项目的可执行文件、源代码、readme、平台脚本等等。 maven-assembly-plugin 支持各种主流的格式如 zip、tar.gz、jar 和 war 等,具体打包哪些文件是高度可控的,例如用户可以 按文件级别的粒度、文件集级别的粒度、模块级别的粒度、以及依赖级别的粒度控制打包,此外,包含和排除配置也是支持的。maven-assembly- plugin 要求用户使用一个名为`assembly.xml`的元数据文件来表述打包,它的 single 目标可以直接在命令行调用,也可以被绑定至生命周期。 + +#### [maven-dependency-plugin](http://maven.apache.org/plugins/maven-dependency-plugin/) + +maven-dependency-plugin 最大的用途是帮助分析项目依赖,**dependency:list**能够列出项目最终解析到的依赖列表,**dependency:tree**能进一步的描绘项目依赖树,**dependency:analyze**可以告诉你项目依赖潜在的问题,如果你有直接使用到的却未声明的依赖,该目标就会发出警告。maven-dependency-plugin 还有很多目标帮助你操作依赖文件,例如**dependency:copy-dependencies**能将项目依赖从本地 Maven 仓库复制到某个特定的文件夹下面。 + +#### [maven-enforcer-plugin](http://maven.apache.org/plugins/maven-enforcer-plugin/) + +在一个稍大一点的组织或团队中,你无法保证所有成员都熟悉 Maven,那他们做一些比较愚蠢的事情就会变得很正常,例如给项目引入了外部的 SNAPSHOT 依赖而导致构建不稳定,使用了一个与大家不一致的 Maven 版本而经常抱怨构建出现诡异问题。maven-enforcer- plugin 能够帮助你避免之类问题,它允许你创建一系列规则强制大家遵守,包括设定 Java 版本、设定 Maven 版本、禁止某些依赖、禁止 SNAPSHOT 依赖。只要在一个父 POM 配置规则,然后让大家继承,当规则遭到破坏的时候,Maven 就会报错。除了标准的规则之外,你还可以扩展该插 件,编写自己的规则。maven-enforcer-plugin 的 enforce 目标负责检查规则,它默认绑定到生命周期的 validate 阶段。 + +#### [maven-help-plugin](http://maven.apache.org/plugins/maven-help-plugin/) + +maven-help-plugin 是一个小巧的辅助工具,最简单的**help:system**可以打印所有可用的环境变量和 Java 系统属性。**help:effective-pom**和**help:effective-settings**最 为有用,它们分别打印项目的有效 POM 和有效 settings,有效 POM 是指合并了所有父 POM(包括 Super POM)后的 XML,当你不确定 POM 的某些信息从何而来时,就可以查看有效 POM。有效 settings 同理,特别是当你发现自己配置的 settings.xml 没有生效时,就可以用**help:effective-settings**来验证。此外,maven-help-plugin 的 describe 目标可以帮助你描述任何一个 Maven 插件的信息,还有 all-profiles 目标和 active-profiles 目标帮助查看项目的 Profile。 + +#### [maven-release-plugin](http://maven.apache.org/plugins/maven-release-plugin/) + +maven-release-plugin 的用途是帮助自动化项目版本发布,它依赖于 POM 中的 SCM 信息。**release:prepare**用来准备版本发布,具体的工作包括检查是否有未提交代码、检查是否有 SNAPSHOT 依赖、升级项目的 SNAPSHOT 版本至 RELEASE 版本、为项目打标签等等。**release:perform**则 是签出标签中的 RELEASE 源码,构建并发布。版本发布是非常琐碎的工作,它涉及了各种检查,而且由于该工作仅仅是偶尔需要,因此手动操作很容易遗漏一 些细节,maven-release-plugin 让该工作变得非常快速简便,不易出错。maven-release-plugin 的各种目标通常直接在 命令行调用,因为版本发布显然不是日常构建生命周期的一部分。 + +#### [maven-resources-plugin](http://maven.apache.org/plugins/maven-resources-plugin/) + +为了使项目结构更为清晰,Maven 区别对待 Java 代码文件和资源文件,maven-compiler-plugin 用来编译 Java 代码,maven-resources-plugin 则用来处理资源文件。默认的主资源文件目录是`src/main/resources`,很多用户会需要添加额外的资源文件目录,这个时候就可以通过配置 maven-resources-plugin 来实现。此外,资源文件过滤也是 Maven 的一大特性,你可以在资源文件中使用*\${propertyName}*形式的 Maven 属性,然后配置 maven-resources-plugin 开启对资源文件的过滤,之后就可以针对不同环境通过命令行或者 Profile 传入属性的值,以实现更为灵活的构建。 + +#### [maven-surefire-plugin](http://maven.apache.org/plugins/maven-surefire-plugin/) + +可能是由于历史的原因,Maven 2.3 中用于执行测试的插件不是 maven-test-plugin,而是 maven-surefire-plugin。其实大部分时间内,只要你的测试 类遵循通用的命令约定(以 Test 结尾、以 TestCase 结尾、或者以 Test 开头),就几乎不用知晓该插件的存在。然而在当你想要跳过测试、排除某些 测试类、或者使用一些 TestNG 特性的时候,了解 maven-surefire-plugin 的一些配置选项就很有用了。例如 **mvn test -Dtest=FooTest** 这样一条命令的效果是仅运行 FooTest 测试类,这是通过控制 maven-surefire-plugin 的 test 参数实现的。 + +#### [build-helper-maven-plugin](http://mojo.codehaus.org/build-helper-maven-plugin/) + +Maven 默认只允许指定一个主 Java 代码目录和一个测试 Java 代码目录,虽然这其实是个应当尽量遵守的约定,但偶尔你还是会希望能够指定多个 源码目录(例如为了应对遗留项目),build-helper-maven-plugin 的 add-source 目标就是服务于这个目的,通常它被绑定到 默认生命周期的 generate-sources 阶段以添加额外的源码目录。需要强调的是,这种做法还是不推荐的,因为它破坏了 Maven 的约定,而且可能会遇到其他严格遵守约定的插件工具无法正确识别额外的源码目录。 + +build-helper-maven-plugin 的另一个非常有用的目标是 attach-artifact,使用该目标你可以以 classifier 的形式选取部分项目文件生成附属构件,并同时 install 到本地仓库,也可以 deploy 到远程仓库。 + +#### [exec-maven-plugin](http://mojo.codehaus.org/exec-maven-plugin/) + +exec-maven-plugin 很好理解,顾名思义,它能让你运行任何本地的系统程序,在某些特定情况下,运行一个 Maven 外部的程序可能就是最简单的问题解决方案,这就是**exec:exec**的 用途,当然,该插件还允许你配置相关的程序运行参数。除了 exec 目标之外,exec-maven-plugin 还提供了一个 java 目标,该目标要求你 提供一个 mainClass 参数,然后它能够利用当前项目的依赖作为 classpath,在同一个 JVM 中运行该 mainClass。有时候,为了简单的 演示一个命令行 Java 程序,你可以在 POM 中配置好 exec-maven-plugin 的相关运行参数,然后直接在命令运行**mvn exec:java** 以查看运行效果。 + +#### [jetty-maven-plugin](http://wiki.eclipse.org/Jetty/Feature/Jetty_Maven_Plugin) + +在进行 Web 开发的时候,打开浏览器对应用进行手动的测试几乎是无法避免的,这种测试方法通常就是将项目打包成 war 文件,然后部署到 Web 容器 中,再启动容器进行验证,这显然十分耗时。为了帮助开发者节省时间,jetty-maven-plugin 应运而生,它完全兼容 Maven 项目的目录结构,能够周期性地检查源文件,一旦发现变更后自动更新到内置的 Jetty Web 容器中。做一些基本配置后(例如 Web 应用的 contextPath 和自动扫描变更的时间间隔),你只要执行 **mvn jetty:run** ,然后在 IDE 中修改代码,代码经 IDE 自动编译后产生变更,再由 jetty-maven-plugin 侦测到后更新至 Jetty 容器,这时你就可以直接 测试 Web 页面了。需要注意的是,jetty-maven-plugin 并不是宿主于 Apache 或 Codehaus 的官方插件,因此使用的时候需要额外 的配置`settings.xml`的 pluginGroups 元素,将 org.mortbay.jetty 这个 pluginGroup 加入。 + +#### [versions-maven-plugin](http://mojo.codehaus.org/versions-maven-plugin/) + +很多 Maven 用户遇到过这样一个问题,当项目包含大量模块的时候,为他们集体更新版本就变成一件烦人的事情,到底有没有自动化工具能帮助完成这件 事情呢?(当然你可以使用 sed 之类的文本操作工具,不过不在本文讨论范围)答案是肯定的,versions-maven- plugin 提供了很多目标帮助你管理 Maven 项目的各种版本信息。例如最常用的,命令 **mvn versions:set -DnewVersion=1.1-SNAPSHOT** 就能帮助你把所有模块的版本更新到 1.1-SNAPSHOT。该插件还提供了其他一些很有用的目标,display-dependency- updates 能告诉你项目依赖有哪些可用的更新;类似的 display-plugin-updates 能告诉你可用的插件更新;然后 use- latest-versions 能自动帮你将所有依赖升级到最新版本。最后,如果你对所做的更改满意,则可以使用 **mvn versions:commit** 提交,不满意的话也可以使用 **mvn versions:revert** 进行撤销。 + +### Maven 命令 + +常用 maven 命令清单: + +| **生命周期** | **阶段描述** | +| --------------------------- | --------------------------------------------------------------------------------------------------------------- | +| mvn validate | 验证项目是否正确,以及所有为了完整构建必要的信息是否可用 | +| mvn generate-sources | 生成所有需要包含在编译过程中的源代码 | +| mvn process-sources | 处理源代码,比如过滤一些值 | +| mvn generate-resources | 生成所有需要包含在打包过程中的资源文件 | +| mvn process-resources | 复制并处理资源文件至目标目录,准备打包 | +| mvn compile | 编译项目的源代码 | +| mvn process-classes | 后处理编译生成的文件,例如对 Java 类进行字节码增强(bytecode enhancement) | +| mvn generate-test-sources | 生成所有包含在测试编译过程中的测试源码 | +| mvn process-test-sources | 处理测试源码,比如过滤一些值 | +| mvn generate-test-resources | 生成测试需要的资源文件 | +| mvn process-test-resources | 复制并处理测试资源文件至测试目标目录 | +| mvn test-compile | 编译测试源码至测试目标目录 | +| mvn test | 使用合适的单元测试框架运行测试。这些测试应该不需要代码被打包或发布 | +| mvn prepare-package | 在真正的打包之前,执行一些准备打包必要的操作。这通常会产生一个包的展开的处理过的版本(将会在 Maven 2.1+中实现) | +| mvn package | 将编译好的代码打包成可分发的格式,如 JAR,WAR,或者 EAR | +| mvn pre-integration-test | 执行一些在集成测试运行之前需要的动作。如建立集成测试需要的环境 | +| mvn integration-test | 如果有必要的话,处理包并发布至集成测试可以运行的环境 | +| mvn post-integration-test | 执行一些在集成测试运行之后需要的动作。如清理集成测试环境。 | +| mvn verify | 执行所有检查,验证包是有效的,符合质量规范 | +| mvn install | 安装包至本地仓库,以备本地的其它项目作为依赖使用 | +| mvn deploy | 复制最终的包至远程仓库,共享给其它开发人员和项目(通常和一次正式的发布相关) | + +示例:最常用的 maven 构建命令 + +```shell +mvn clean install -Dmaven.test.skip=true -B -U +``` + +清理本地输出物,并构建 maven 项目,最后将输出物归档在本地仓库。 + +> :bulb: 想了解更多 maven 命令行细节可以参考官方文档: +> +> - [Maven 构建生命周期说明](https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html) +> - [Maven 命令行参数说明](https://maven.apache.org/ref/3.6.3/maven-embedder/cli.html) + +## 参考资料 + +- [Maven Github](https://github.com/apache/maven) +- [Maven 官方文档](https://maven.apache.org/ref/current) +- [Maven in 5 Minutes](https://maven.apache.org/guides/getting-started/maven-in-five-minutes.html) +- [Maven Getting Started Guide](https://maven.apache.org/guides/getting-started/index.html) +- [maven 常见问题问答](http://www.oschina.net/question/158170_29368) +- [常用 Maven 插件介绍](https://www.cnblogs.com/crazy-fox/archive/2012/02/09/2343722.html) \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" new file mode 100644 index 00000000..d19300f4 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" @@ -0,0 +1,802 @@ +--- +title: Maven 教程之 pom.xml 详解 +date: 2019-05-14 14:57:33 +order: 02 +categories: + - Java + - 软件 + - 构建 + - Maven +tags: + - Java + - 构建 + - Maven +permalink: /pages/d893c2/ +--- + +# Maven 教程之 pom.xml 详解 + +## pom.xml 简介 + +### 什么是 pom + +**POM 是 Project Object Model 的缩写,即项目对象模型。** + +pom.xml 就是 maven 的配置文件,用以描述项目的各种信息。 + +### pom 配置一览 + +```xml + + 4.0.0 + + + ... + ... + ... + ... + ... + ... + ... + ... + ... + + + ... + ... + + + ... + ... + ... + ... + ... + ... + ... + ... + + + ... + ... + ... + ... + ... + ... + ... + ... + ... + +``` + +## 基本配置 + +- **project** - `project` 是 pom.xml 中描述符的根。 +- **modelVersion** - `modelVersion` 指定 pom.xml 符合哪个版本的描述符。maven 2 和 3 只能为 4.0.0。 + +一般 jar 包被识别为: `groupId:artifactId:version` 的形式。 + +```xml + + 4.0.0 + + org.codehaus.mojo + my-project + 1.0 + war + +``` + +### maven 坐标 + +**在 maven 中,根据 `groupId`、`artifactId`、`version` 组合成 `groupId:artifactId:version` 来唯一识别一个 jar 包。** + +- **groupId** - 团体、组织的标识符。团体标识的约定是,它以创建这个项目的组织名称的逆向域名(reverse domain name)开头。一般对应着 java 的包结构。 +- **artifactId** - 单独项目的唯一标识符。比如我们的 tomcat、commons 等。不要在 artifactId 中包含点号(.)。 +- **version** - 一个项目的特定版本。 + - maven 有自己的版本规范,一般是如下定义 major version、minor version、incremental version-qualifier ,比如 1.2.3-beta-01。要说明的是,maven 自己判断版本的算法是 major、minor、incremental 部分用数字比较,qualifier 部分用字符串比较,所以要小心 alpha-2 和 alpha-15 的比较关系,最好用 alpha-02 的格式。 + - maven 在版本管理时候可以使用几个特殊的字符串 SNAPSHOT、LATEST、RELEASE。比如 `1.0-SNAPSHOT`。各个部分的含义和处理逻辑如下说明: + - **SNAPSHOT** - 这个版本一般用于开发过程中,表示不稳定的版本。 + - **LATEST** - 指某个特定构件的最新发布,这个发布可能是一个发布版,也可能是一个 snapshot 版,具体看哪个时间最后。 + - **RELEASE** :指最后一个发布版。 +- **packaging** - 项目的类型,描述了项目打包后的输出,默认是 jar。常见的输出类型为:pom, jar, maven-plugin, ejb, war, ear, rar, par。 + +## 依赖配置 + +### dependencies + +```xml + + ... + + + org.apache.maven + maven-embedder + 2.0 + jar + test + true + + + org.apache.maven + maven-core + + + + ... + + ... + +``` + +- **groupId**, **artifactId**, **version** - 和基本配置中的 `groupId`、`artifactId`、`version` 意义相同。 +- **type** - 对应 `packaging` 的类型,如果不使用 `type` 标签,maven 默认为 jar。 +- **scope** - 此元素指的是任务的类路径(编译和运行时,测试等)以及如何限制依赖关系的传递性。有 5 种可用的限定范围: + - **compile** - 如果没有指定 `scope` 标签,maven 默认为这个范围。编译依赖关系在所有 classpath 中都可用。此外,这些依赖关系被传播到依赖项目。 + - **provided** - 与 compile 类似,但是表示您希望 jdk 或容器在运行时提供它。它只适用于编译和测试 classpath,不可传递。 + - **runtime** - 此范围表示编译不需要依赖关系,而是用于执行。它是在运行时和测试 classpath,但不是编译 classpath。 + - **test** - 此范围表示正常使用应用程序不需要依赖关系,仅适用于测试编译和执行阶段。它不是传递的。 + - **system** - 此范围与 provided 类似,除了您必须提供明确包含它的 jar。该 artifact 始终可用,并且不是在仓库中查找。 +- **systemPath** - 仅当依赖范围是系统时才使用。否则,如果设置此元素,构建将失败。该路径必须是绝对路径,因此建议使用 `propertie` 来指定特定的路径,如\$ {java.home} / lib。由于假定先前安装了系统范围依赖关系,maven 将不会检查项目的仓库,而是检查库文件是否存在。如果没有,maven 将会失败,并建议您手动下载安装。 +- **optional** - `optional` 让其他项目知道,当您使用此项目时,您不需要这种依赖性才能正常工作。 +- **exclusions** - 包含一个或多个排除元素,每个排除元素都包含一个表示要排除的依赖关系的 `groupId` 和 `artifactId`。与可选项不同,可能或可能不会安装和使用,排除主动从依赖关系树中删除自己。 + +### parent + +maven 支持继承功能。子 POM 可以使用 `parent` 指定父 POM ,然后继承其配置。 + +```xml + + 4.0.0 + + + org.codehaus.mojo + my-parent + 2.0 + ../my-parent + + + my-project + +``` + +- **relativePath** - 注意 `relativePath` 元素。在搜索本地和远程存储库之前,它不是必需的,但可以用作 maven 的指示符,以首先搜索给定该项目父级的路径。 + +### dependencyManagement + +`dependencyManagement` 是表示依赖 jar 包的声明。即你在项目中的 `dependencyManagement` 下声明了依赖,maven 不会加载该依赖,`dependencyManagement` 声明可以被子 POM 继承。 + +`dependencyManagement` 的一个使用案例是当有父子项目的时候,父项目中可以利用 `dependencyManagement` 声明子项目中需要用到的依赖 jar 包,之后,当某个或者某几个子项目需要加载该依赖的时候,就可以在子项目中 `dependencies` 节点只配置 `groupId` 和 `artifactId` 就可以完成依赖的引用。 + +`dependencyManagement` 主要是为了统一管理依赖包的版本,确保所有子项目使用的版本一致,类似的还有`plugins`和`pluginManagement`。 + +### modules + +子模块列表。 + +```xml + + 4.0.0 + + org.codehaus.mojo + my-parent + 2.0 + pom + + + my-project + another-project + third-project/pom-example.xml + + +``` + +### properties + +属性列表。定义的属性可以在 pom.xml 文件中任意处使用。使用方式为 `${propertie}` 。 + +```xml + + ... + + 1.7 + 1.7 + UTF-8 + UTF-8 + + ... + +``` + +## 构建配置 + +### build + +build 可以分为 "project build" 和 "profile build"。 + +```xml + + ... + + ... + + + + + ... + + + +``` + +基本构建配置: + +```xml + + install + ${basedir}/target + ${artifactId}-${version} + + filters/filter1.properties + + ... + +``` + +**defaultGoal** : 默认执行目标或阶段。如果给出了一个目标,它应该被定义为它在命令行中(如 jar:jar)。如果定义了一个阶段(如安装),也是如此。 + +**directory** :构建时的输出路径。默认为:`${basedir}/target` 。 + +**finalName** :这是项目的最终构建名称(不包括文件扩展名,例如:my-project-1.0.jar) + +**filter** :定义 `* .properties` 文件,其中包含适用于接受其设置的资源的属性列表(如下所述)。换句话说,过滤器文件中定义的“name = value”对在代码中替换\$ {name}字符串。 + +#### resources + +资源的配置。资源文件通常不是代码,不需要编译,而是在项目需要捆绑使用的内容。 + +```xml + + + ... + + + META-INF/plexus + false + ${basedir}/src/main/plexus + + configuration.xml + + + **/*.properties + + + + + ... + + ... + + +``` + +- **resources**: 资源元素的列表,每个资源元素描述与此项目关联的文件和何处包含文件。 +- **targetPath**: 指定从构建中放置资源集的目录结构。目标路径默认为基本目录。将要包装在 jar 中的资源的通常指定的目标路径是 META-INF。 +- **filtering**: 值为 true 或 false。表示是否要为此资源启用过滤。请注意,该过滤器 `* .properties` 文件不必定义为进行过滤 - 资源还可以使用默认情况下在 POM 中定义的属性(例如\$ {project.version}),并将其传递到命令行中“-D”标志(例如,“-Dname = value”)或由 properties 元素显式定义。过滤文件覆盖上面。 +- **directory**: 值定义了资源的路径。构建的默认目录是`${basedir}/src/main/resources`。 +- **includes**: 一组文件匹配模式,指定目录中要包括的文件,使用\*作为通配符。 +- **excludes**: 与 `includes` 类似,指定目录中要排除的文件,使用\*作为通配符。注意:如果 `include` 和 `exclude` 发生冲突,maven 会以 `exclude` 作为有效项。 +- **testResources**: `testResources` 与 `resources` 功能类似,区别仅在于:`testResources` 指定的资源仅用于 test 阶段,并且其默认资源目录为:`${basedir}/src/test/resources` 。 + +#### plugins + +```xml + + + ... + + + org.apache.maven.plugins + maven-jar-plugin + 2.6 + false + true + + test + + ... + ... + + + + +``` + +- **groupId**, **artifactId**, **version** :和基本配置中的 `groupId`、`artifactId`、`version` 意义相同。 + +- **extensions** :值为 true 或 false。是否加载此插件的扩展名。默认为 false。 + +- **inherited** :值为 true 或 false。这个插件配置是否应该适用于继承自这个插件的 POM。默认值为 true。 + +- **configuration** - 这是针对个人插件的配置,这里不扩散讲解。 + +- **dependencies** :这里的 `dependencies` 是插件本身所需要的依赖。 + +- **executions** :需要记住的是,插件可能有多个目标。每个目标可能有一个单独的配置,甚至可能将插件的目标完全绑定到不同的阶段。执行配置插件的目标的执行。 + + - **id**: 执行目标的标识。 + - **goals**: 像所有多元化的 POM 元素一样,它包含单个元素的列表。在这种情况下,这个执行块指定的插件目标列表。 + - **phase**: 这是执行目标列表的阶段。这是一个非常强大的选项,允许将任何目标绑定到构建生命周期中的任何阶段,从而改变 maven 的默认行为。 + - **inherited**: 像上面的继承元素一样,设置这个 false 会阻止 maven 将这个执行传递给它的子代。此元素仅对父 POM 有意义。 + - **configuration**: 与上述相同,但将配置限制在此特定目标列表中,而不是插件下的所有目标。 + +```xml + + ... + + + + maven-antrun-plugin + 1.1 + + + echodir + + run + + verify + false + + + Build Dir: ${project.build.directory} + + + + + + + + + +``` + +#### pluginManagement + +与 `dependencyManagement` 很相似,在当前 POM 中仅声明插件,而不是实际引入插件。子 POM 中只配置 `groupId` 和 `artifactId` 就可以完成插件的引用,且子 POM 有权重写 pluginManagement 定义。 + +它的目的在于统一所有子 POM 的插件版本。 + +#### directories + +```xml + + ... + + ${basedir}/src/main/java + ${basedir}/src/main/scripts + ${basedir}/src/test/java + ${basedir}/target/classes + ${basedir}/target/test-classes + ... + + +``` + +目录元素集合存在于 `build` 元素中,它为整个 POM 设置了各种目录结构。由于它们在配置文件构建中不存在,所以这些不能由配置文件更改。 + +如果上述目录元素的值设置为绝对路径(扩展属性时),则使用该目录。否则,它是相对于基础构建目录:`${basedir}`。 + +#### extensions + +扩展是在此构建中使用的 artifacts 的列表。它们将被包含在运行构建的 classpath 中。它们可以启用对构建过程的扩展(例如为 Wagon 传输机制添加一个 ftp 提供程序),并使活动的插件能够对构建生命周期进行更改。简而言之,扩展是在构建期间激活的 artifacts。扩展不需要实际执行任何操作,也不包含 Mojo。因此,扩展对于指定普通插件接口的多个实现中的一个是非常好的。 + +```xml + + ... + + ... + + + org.apache.maven.wagon + wagon-ftp + 1.0-alpha-3 + + + ... + + +``` + +### reporting + +报告包含特定针对 `site` 生成阶段的元素。某些 maven 插件可以生成 `reporting` 元素下配置的报告,例如:生成 javadoc 报告。`reporting` 与 `build` 元素配置插件的能力相似。明显的区别在于:在执行块中插件目标的控制不是细粒度的,报表通过配置 `reportSet` 元素来精细控制。而微妙的区别在于 `reporting` 元素下的 `configuration` 元素可以用作 `build` 下的 `configuration` ,尽管相反的情况并非如此( `build` 下的 `configuration` 不影响 `reporting` 元素下的 `configuration` )。 + +另一个区别就是 `plugin` 下的 `outputDirectory` 元素。在报告的情况下,默认输出目录为 `${basedir}/target/site`。 + +```xml + + ... + + + + ... + + + sunlink + + javadoc + + true + + + http://java.sun.com/j2se/1.5.0/docs/api/ + + + + + + + + ... + +``` + +## 项目信息 + +项目信息相关的这部分标签**都不是必要的**,也就是说完全可以不填写。 + +它的作用仅限于描述项目的详细信息。 + +下面的示例是项目信息相关标签的清单: + +```xml + + ... + + + + + maven-notes + + + maven 学习笔记 + + + https://github.com/dunwu/maven-notes + + + 2017 + + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + A business-friendly OSS license + + + + + + ... + ... + + + + + + victor + Zhang Peng + forbreak at 163.com + https://github.com/dunwu + ... + ... + + architect + developer + + +8 + ... + + + + + + + + + + + + + ... + +``` + +这部分标签都非常简单,基本都能做到顾名思义,且都属于可有可无的标签,所以这里仅简单介绍一下: + +- **name** - 项目完整名称 + +- **description** - 项目描述 + +- **url** - 一般为项目仓库的 host + +- **inceptionYear** - 开发年份 + +- **licenses** - 开源协议 + +- **organization** - 项目所属组织信息 + +- **developers** - 项目开发者列表 + +- **contributors** - 项目贡献者列表,`` 的子标签和 `` 的完全相同。 + +## 环境配置 + +### issueManagement + +这定义了所使用的缺陷跟踪系统(Bugzilla,TestTrack,ClearQuest 等)。虽然没有什么可以阻止插件使用这些信息的东西,但它主要用于生成项目文档。 + +```xml + + ... + + Bugzilla + http://127.0.0.1/bugzilla/ + + ... + +``` + +### ciManagement + +CI 构建系统配置,主要是指定通知机制以及被通知的邮箱。 + +```xml + + ... + + continuum + http://127.0.0.1:8080/continuum + + + mail + true + true + false + false +
continuum@127.0.0.1
+
+
+
+ ... +
+``` + +### mailingLists + +邮件列表 + +```xml + + ... + + + User List + user-subscribe@127.0.0.1 + user-unsubscribe@127.0.0.1 + user@127.0.0.1 + http://127.0.0.1/user/ + + http://base.google.com/base/1/127.0.0.1 + + + + ... + +``` + +### scm + +SCM(软件配置管理,也称为源代码/控制管理或简洁的版本控制)。常见的 scm 有 svn 和 git 。 + +```xml + + ... + + scm:svn:http://127.0.0.1/svn/my-project + scm:svn:https://127.0.0.1/svn/my-project + HEAD + http://127.0.0.1/websvn/my-project + + ... + +``` + +### prerequisites + +POM 执行的预设条件。 + +```xml + + ... + + 2.0.6 + + ... + +``` + +### repositories + +`repositories` 是遵循 Maven 存储库目录布局的 artifacts 集合。默认的 Maven 中央存储库位于https://repo.maven.apache.org/maven2/上。 + +```xml + + ... + + + + false + always + warn + + + true + never + fail + + codehausSnapshots + Codehaus Snapshots + http://snapshots.maven.codehaus.org/maven2 + default + + + + ... + + ... + +``` + +### pluginRepositories + +与 `repositories` 差不多。 + +```xml + + ... + + ... + http://mojo.codehaus.org/my-project + deployed + + ... + +``` + +### distributionManagement + +它管理在整个构建过程中生成的 artifact 和支持文件的分布。从最后的元素开始: + +```xml + + ... + + ... + http://mojo.codehaus.org/my-project + deployed + + ... + +``` + +- **repository** - 与 `repositories` 相似 + +- **site** - 站点信息 + +- **relocation** - 项目迁移位置 + +### profiles + +`activation` 是一个 `profile` 的关键。配置文件的功能来自于在某些情况下仅修改基本 POM 的功能。这些情况通过 `activation` 元素指定。 + +```xml + + ... + + + test + + false + 1.5 + + Windows XP + Windows + x86 + 5.1.2600 + + + sparrow-type + African + + + ${basedir}/file2.properties + ${basedir}/file1.properties + + + ... + + + +``` + +## 参考资料 + +- [maven 官方文档之 pom](https://maven.apache.org/pom.html) \ No newline at end of file diff --git a/docs/javatool/build/maven/maven-settings-config.md "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" similarity index 69% rename from docs/javatool/build/maven/maven-settings-config.md rename to "docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" index 4ee1f753..9fce2931 100644 --- a/docs/javatool/build/maven/maven-settings-config.md +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" @@ -1,43 +1,53 @@ --- -title: Maven 之 settings.xml 详解 -date: 2016/11/10 +title: Maven 教程之 settings.xml 详解 +date: 2019-05-14 14:57:33 +order: 03 categories: -- javatool + - Java + - 软件 + - 构建 + - Maven tags: -- java -- javatool -- build -- maven + - Java + - 构建 + - Maven +permalink: /pages/1d58f1/ --- -# Maven 之 settings.xml 详解 +# Maven 教程之 settings.xml 详解 -## 概要 +## settings.xml 简介 -### settings.xml有什么用? -如果在Eclipse中使用过Maven插件,想必会有这个经验:配置settings.xml文件的路径。 -![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3101171-a9137d52a1eab02d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -**settings.xml文件是干什么的,为什么要配置它呢?** -从settings.xml的文件名就可以看出,它是用来设置maven参数的配置文件。并且,**settings.xml是maven的全局配置文件**。而pom.xml文件是所在项目的局部配置。 -Settings.xml中包含类似本地仓储位置、修改远程仓储服务器、认证信息等配置。 +### settings.xml 有什么用 -### settings.xml文件位置 -settings.xml文件一般存在于两个位置: -全局配置: ${M2_HOME}/conf/settings.xml -用户配置: ${user.home}/.m2/settings.xml -note:用户配置优先于全局配置。${user.home} 和和所有其他系统属性只能在3.0+版本上使用。请注意windows和Linux使用变量的区别。 +从 settings.xml 的文件名就可以看出,它是用来设置 maven 参数的配置文件。settings.xml 中包含类似本地仓储位置、修改远程仓储服务器、认证信息等配置。 + +- settings.xml 是 maven 的**全局配置文件**。 +- pom.xml 文件是本地**项目配置文件**。 + +### settings.xml 文件位置 + +settings.xml 文件一般存在于两个位置: + +- **全局配置** - `${maven.home}/conf/settings.xml` +- **用户配置** - `${user.home}/.m2/settings.xml` + +> 🔔 注意:用户配置优先于全局配置。`${user.home}` 和和所有其他系统属性只能在 3.0+版本上使用。请注意 windows 和 Linux 使用变量的区别。 ### 配置优先级 -需要注意的是:**局部配置优先于全局配置**。 -配置优先级从高到低:pom.xml> user settings > global settings +> 重要:**局部配置优先于全局配置**。 + +配置优先级从高到低:pom.xml > user settings > global settings + 如果这些文件同时存在,在应用配置时,会合并它们的内容,如果有重复的配置,优先级高的配置会覆盖优先级低的。 -## settings.xml元素详解 +## settings.xml 元素详解 ### 顶级元素概览 下面列举了`settings.xml`中的顶级元素 + ```xml ${user.home}/.m2/repository ``` ### InteractiveMode -**作用**:表示maven是否需要和用户交互以获得输入。 -如果maven需要和用户交互以获得输入,则设置成true,反之则应为false。默认为true。 +**作用**:表示 maven 是否需要和用户交互以获得输入。 + +如果 maven 需要和用户交互以获得输入,则设置成 true,反之则应为 false。默认为 true。 + ```xml true ``` ### UsePluginRegistry -**作用**:maven是否需要使用plugin-registry.xml文件来管理插件版本。 -如果需要让maven使用文件~/.m2/plugin-registry.xml来管理插件版本,则设为true。默认为false。 +**作用**:maven 是否需要使用 plugin-registry.xml 文件来管理插件版本。 + +如果需要让 maven 使用文件\~/.m2/plugin-registry.xml 来管理插件版本,则设为 true。默认为 false。 + ```xml false ``` ### Offline -**作用**:表示maven是否需要在离线模式下运行。 -如果构建系统需要在离线模式下运行,则为true,默认为false。 +**作用**:表示 maven 是否需要在离线模式下运行。 + +如果构建系统需要在离线模式下运行,则为 true,默认为 false。 + 当由于网络设置原因或者安全因素,构建服务器不能连接远程仓库的时候,该配置就十分有用。 + ```xml false ``` ### PluginGroups -**作用**:当插件的组织id(groupId)没有显式提供时,供搜寻插件组织Id(groupId)的列表。 -该元素包含一个pluginGroup元素列表,每个子元素包含了一个组织Id(groupId)。 -当我们使用某个插件,并且没有在命令行为其提供组织Id(groupId)的时候,Maven就会使用该列表。默认情况下该列表包含了`org.apache.maven.plugins`和`org.codehaus.mojo`。 +**作用**:当插件的组织 id(groupId)没有显式提供时,供搜寻插件组织 Id(groupId)的列表。 + +该元素包含一个 pluginGroup 元素列表,每个子元素包含了一个组织 Id(groupId)。 + +当我们使用某个插件,并且没有在命令行为其提供组织 Id(groupId)的时候,Maven 就会使用该列表。默认情况下该列表包含了 `org.apache.maven.plugins` 和 `org.codehaus.mojo`。 + ```xml 1.0通过${project.version}获得version的值。 - 3. settings.x: 指代了settings.xml中对应元素的值。例如:false通过 ${settings.offline}获得offline的值。 - 4. Java System Properties: 所有可通过java.lang.System.getProperties()访问的属性都能在POM中使用该形式访问,例如 ${java.home}。 -5. x: 在元素中,或者外部文件中设置,以${someVar}的形式使用。 + ${user.home}/our-project ``` -***注:如果该profile被激活,则可以在`pom.xml`中使用${user.install}。*** +> 注:如果该 profile 被激活,则可以在`pom.xml`中使用\${user.install}。 #### Repositories -**作用**:远程仓库列表,它是maven用来填充构建系统本地仓库所使用的一组远程仓库。 +**作用**:远程仓库列表,它是 maven 用来填充构建系统本地仓库所使用的一组远程仓库。 + ```xml @@ -323,8 +355,9 @@ maven属性和ant中的属性一样,可以用来存放一些值。这些值可 #### pluginRepositories **作用**:发现插件的远程仓库列表。 -和`repository`类似,只是`repository`是管理jar包依赖的仓库,`pluginRepositories`则是管理插件的仓库。 -maven插件是一种特殊类型的构件。由于这个原因,插件仓库独立于其它仓库。`pluginRepositories`元素的结构和`repositories`元素的结构类似。每个`pluginRepository`元素指定一个Maven可以用来寻找新插件的远程地址。 + +和 `repository` 类似,只是 `repository` 是管理 jar 包依赖的仓库,`pluginRepositories` 则是管理插件的仓库。 +maven 插件是一种特殊类型的构件。由于这个原因,插件仓库独立于其它仓库。`pluginRepositories` 元素的结构和 `repositories` 元素的结构类似。每个 `pluginRepository` 元素指定一个 Maven 可以用来寻找新插件的远程地址。 ```xml @@ -350,9 +383,12 @@ maven插件是一种特殊类型的构件。由于这个原因,插件仓库独 ### ActiveProfiles -**作用**:手动激活profiles的列表,按照`profile`被应用的顺序定义`activeProfile`。 -该元素包含了一组`activeProfile`元素,每个`activeProfile`都含有一个profile id。任何在`activeProfile`中定义的profile id,不论环境设置如何,其对应的 `profile`都会被激活。如果没有匹配的`profile`,则什么都不会发生。 -例如,env-test是一个activeProfile,则在pom.xml(或者profile.xml)中对应id的profile会被激活。如果运行过程中找不到这样一个profile,Maven则会像往常一样运行。 +**作用**:手动激活 profiles 的列表,按照`profile`被应用的顺序定义`activeProfile`。 + +该元素包含了一组 `activeProfile` 元素,每个 `activeProfile` 都含有一个 profile id。任何在 `activeProfile` 中定义的 profile id,不论环境设置如何,其对应的 `profile` 都会被激活。如果没有匹配的 `profile`,则什么都不会发生。 + +例如,env-test 是一个 activeProfile,则在 pom.xml(或者 profile.xml)中对应 id 的 profile 会被激活。如果运行过程中找不到这样一个 profile,Maven 则会像往常一样运行。 + ```xml [Help 1] +``` + +错误原因: + +maven 的 JDK 源与指定的 JDK 编译版本不符。 + +排错手段: + +- **查看 Project Settings** + +Project SDK 是否正确 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203324.png) + +SDK 路径是否正确 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203427.png) + +- **查看 Settings > Maven 的配置** + +JDK for importer 是否正确 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203408.png) + +Runner 是否正确 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203439.png) + +### 重复引入依赖 + +在 Idea 中,选中 Module,使用 Ctrl+Alt+Shift+U,打开依赖图,检索是否存在重复引用的情况。如果存在重复引用,可以将多余的引用删除。 + +### 如何打包一个可以直接运行的 Spring Boot jar 包 + +可以使用 spring-boot-maven-plugin 插件 + +```xml + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + + +``` + +如果引入了第三方 jar 包,如何打包? + +首先,要添加依赖 + +```xml + + io.github.dunwu + dunwu-common + 1.0.0 + system + ${project.basedir}/src/main/resources/lib/dunwu-common-1.0.0.jar + +``` + +接着,需要配置 spring-boot-maven-plugin 插件: + +```xml + + + + org.springframework.boot + spring-boot-maven-plugin + + + + repackage + + + + + true + + + + +``` + +### 去哪儿找 maven dependency + +问:刚接触 maven 的新手,往往会有这样的疑问,我该去哪儿找 jar? + +答:官方推荐的搜索 maven dependency 网址: + +- https://search.maven.org +- https://repository.apache.org +- https://mvnrepository.com + +### 如何指定编码 + +问:众所周知,不同编码格式常常会产生意想不到的诡异问题,那么 maven 构建时如何指定 maven 构建时的编码? + +答:在 properties 中指定 `project.build.sourceEncoding` + +```xml + + UTF-8 + +``` + +### 如何指定 JDK 版本 + +问:如何指定 maven 构建时的 JDK 版本 + +答:有两种方法: + +(1)properties 方式 + +```xml + + ... + + 1.7 + 1.7 + + ... + +``` + +(2)使用 maven-compiler-plugin 插件,并指定 source 和 target 版本 + +```xml + +... + + + org.apache.maven.plugins + maven-compiler-plugin + 3.3 + + 1.7 + 1.7 + + + +... + +``` + +### 如何避免将 dependency 打包到构件中 + +答:指定 maven dependency 的 scope 为 `provided`,这意味着:依赖关系将在运行时由其容器或 JDK 提供。 +具有此范围的依赖关系不会传递,也不会捆绑在诸如 WAR 之类的包中,也不会包含在运行时类路径中。 + +### 如何跳过单元测试 + +问:执行 mvn package 或 mvn install 时,会自动编译所有单元测试(src/test/java 目录下的代码),如何跳过这一步? + +答:在执行命令的后面,添加命令行参数 `-Dmaven.test.skip=true` 或者 `-DskipTests=true` + +### 如何引入本地 jar + +问:有时候,需要引入在中央仓库找不到的 jar,但又想通过 maven 进行管理,那么应该如何做到呢? +答:可以通过设置 dependency 的 scope 为 system 来引入本地 jar。 +例: + +- 将私有 jar 放置在 resouces/lib 下,然后以如下方式添加依赖: +- groupId 和 artifactId 可以按照 jar 包中的 package 设置,只要和其他 jar 不冲突即可。 + +```xml + + xxx + xxx + 1.0.0 + system + ${project.basedir}/src/main/resources/lib/xxx-6.0.0.jar + +``` + +### 如何排除依赖 + +问:如何排除依赖一个依赖关系?比方项目中使用的 libA 依赖某个库的 1.0 版。libB 以来某个库的 2.0 版,如今想统一使用 2.0 版,怎样去掉 1.0 版的依赖? + +答:通过 exclusion 排除指定依赖即可。 + +例: + +```xml + + org.apache.zookeeper + zookeeper + 3.4.12 + true + + + org.slf4j + slf4j-log4j12 + + + +``` + +## Maven 最佳实践 + +### 通过 bom 统一管理版本 + +采用类似 `spring-boot-dependencies` 的方式统一管理依赖版本。 + +spring-boot-dependencies 的 pom.xml 形式: + +```xml + + +4.0.0 +org.springframework.boot +spring-boot-dependencies +2.1.9.RELEASE +pom + + + + + + + + + + + + + + + + + + + +``` + +其他项目引入 spring-boot-dependencies 来管理依赖版本的方式: + +```xml + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + +``` \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" new file mode 100644 index 00000000..2f29b940 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" @@ -0,0 +1,479 @@ +--- +title: Maven 教程之发布 jar 到私服或中央仓库 +date: 2019-05-14 14:57:33 +order: 05 +categories: + - Java + - 软件 + - 构建 + - Maven +tags: + - Java + - 构建 + - Maven +permalink: /pages/7bdaf9/ +--- + +# Maven 教程之发布 jar 到私服或中央仓库 + +## 发布 jar 包到中央仓库 + +> 为了避免重复造轮子,相信每个 Java 程序员都想打造自己的脚手架或工具包(自己定制的往往才是最适合自己的)。那么如何将自己的脚手架发布到中央仓库呢?下面我们将一步步来实现。 + +### 在 Sonatype 创建 Issue + +(1)注册 Sonatype 账号 + +发布 Java 包到 Maven 中央仓库,首先需要在 [Sonatype](https://issues.sonatype.org/secure/Dashboard.jspa) 网站创建一个工单(Issues)。 + +第一次使用这个网站的时候需要注册自己的帐号(这个帐号和密码需要记住,后面会用到)。 + +(2)创建 Issue + +注册账号成功后,根据你 Java 包的功能分别写上`Summary`、`Description`、`Group Id`、`SCM url`以及`Project URL`等必要信息,可以参见我之前创建的 Issue:[OSSRH-36187](https://issues.sonatype.org/browse/OSSRH-36187)。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181106143734.png) + +创建完之后需要等待 Sonatype 的工作人员审核处理,审核时间还是很快的,我的审核差不多等待了两小时。当 Issue 的 Status 变为`RESOLVED`后,就可以进行下一步操作了。 + +> 说明:如果你的 Group Id 填写的是自己的网站(我的就是这种情况),Sonatype 的工作人员会询问你那个 Group Id 是不是你的域名,你只需要在上面回答是就行,然后就会通过审核。 + +### 使用 GPG 生成公私钥对 + +(1)安装 Gpg4win + +Windows 系统,可以下载 Gpg4win 软件来生成密钥对。 + +[Gpg4win 下载地址](https://www.gpg4win.org/download.html) + +安装后,执行命令 gpg --version 检查是否安装成功。 + +```batch +C:\Program Files (x86)\GnuPG\bin>gpg --version +gpg (GnuPG) 2.2.10 +libgcrypt 1.8.3 +Copyright (C) 2018 Free Software Foundation, Inc. +License GPLv3+: GNU GPL version 3 or later +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the exdunwu permitted by law. + +Home: C:/Users/Administrator/AppData/Roaming/gnupg +Supported algorithms: +Pubkey: RSA, ELG, DSA, ECDH, ECDSA, EDDSA +Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH, + CAMELLIA128, CAMELLIA192, CAMELLIA256 +Hash: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224 +Compression: Uncompressed, ZIP, ZLIB, BZIP2 +``` + +(2)生成密钥对 + +执行命令 `gpg --gen-key` + +```batch +C:\Program Files (x86)\GnuPG\bin>gpg --gen-key +gpg (GnuPG) 2.2.10; Copyright (C) 2018 Free Software Foundation, Inc. +This is free software: you are free to change and redistribute it. +There is NO WARRANTY, to the exdunwu permitted by law. + +Note: Use "gpg --full-generate-key" for a full featured key generation dialog. + +GnuPG needs to construct a user ID to identify your key. + +Real name: Zhang Peng +Email address: forbreak@163.com +You selected this USER-ID: + "Zhang Peng " + +Change (N)ame, (E)mail, or (O)kay/(Q)uit? O +``` + +说明:按照提示,依次输入用户名、邮箱。 + +(3)查看公钥 + +```batch +C:\Program Files (x86)\GnuPG\bin>gpg --list-keys + +gpg: checking the trustdb +gpg: marginals needed: 3 completes needed: 1 trust model: pgp +gpg: depth: 0 valid: 2 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 2u +gpg: next trustdb check due at 2020-11-05 +C:/Users/Administrator/AppData/Roaming/gnupg/pubring.kbx +-------------------------------------------------------- +pub rsa2048 2018-11-06 [SC] [expires: 2020-11-06] + E4CE537A3803D49C35332221A306519BAFF57F60 +uid [ultimate] forbreak +sub rsa2048 2018-11-06 [E] [expires: 2020-11-06] +``` + +> 说明:其中,E4CE537A3803D49C35332221A306519BAFF57F60 就是公钥 + +(4)将公钥发布到 PGP 密钥服务器 + +执行 `gpg --keyserver hkp://pool.sks-keyservers.net --send-keys` 发布公钥: + +```batch +C:\Program Files (x86)\GnuPG\bin>gpg --keyserver hkp://pool.sks-keyservers.net --send-keys E4CE537A3803D49C35332221A306519BAFF57F60 +gpg: sending key A306519BAFF57F60 to hkp://pool.sks-keyservers.net +``` + +> 🔔 注意:有可能出现 gpg: keyserver receive failed: No dat 错误,等大约 30 分钟后再执行就不会报错了。 + +(5)查看公钥是否发布成功 + +执行 `gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys` 查看公钥是否发布成功。 + +```batch +C:\Program Files (x86)\GnuPG\bin>gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys E4CE537A3803D49C35332221A306519BAFF57F60 +gpg: key A306519BAFF57F60: "forbreak " not changed +gpg: Total number processed: 1 +gpg: unchanged: 1 +``` + +### Maven 配置 + +完成了前两个章节的准备工作,就可以将 jar 包上传到中央仓库了。当然了,我们还要对 maven 做一些配置。 + +#### settings.xml 配置 + +一份完整的 settings.xml 配置示例如下: + +```xml + + + + + + org.sonatype.plugins + + + + + + sonatype-snapshots + xxxxxx + xxxxxx + + + sonatype-staging + xxxxxx + xxxxxx + + + + + + + nexus-aliyun + * + Aliyun + http://maven.aliyun.com/nexus/groups/public + + + + + + + sonatype + + C:/Program Files (x86)/GnuPG/bin/gpg.exe + xxxxxx + + + + + + sonatype + + +``` + +#### pom.xml 配置 + +(1)添加 licenses、scm、developers 配置: + +```xml + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + + xxxxxx + forbreak@163.com + https://github.com/dunwu + + + + + https://github.com/dunwu/dunwu + git@github.com:dunwu/dunwu.git + https://github.com/dunwu + +``` + +(2)添加 distributionManagement 配置 + +```xml + + + sonatype-snapshots + https://oss.sonatype.org/content/repositories/snapshots + + + sonatype-staging + https://oss.sonatype.org/service/local/staging/deploy/maven2 + + +``` + +> 说明:`` 指定的是 snapshot 仓库地址;`` 指定的是 staging (正式版)仓库地址。需要留意的是,这里的 id 需要和 settings.xml 中的 `` 的 id 保持一致。 + +(3)添加 profiles 配置 + +```xml + + + sonatype + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.7 + true + + sonatype-snapshots + https://oss.sonatype.org/ + true + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.0.1 + + false + true + + + + attach-javadocs + + jar + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + 1.6 + + + sign-artifacts + verify + + sign + + + + + + + + +``` + +### 部署和发布 + +按照上面的步骤配置完后,一切都已经 OK。 + +此时,使用 `mvn clean deploy -P sonatype` 命令就可以发布 jar 包到中央仓库了: + +> 说明:-P 参数后面的 sonatype 需要和 pom.xml 中 `` 的 id 保持一致,才能激活 profile。 + +## 部署 maven 私服 + +> 工作中,Java 程序员开发的商用 Java 项目,一般不想发布到中央仓库,使得人人尽知。这时,我们就需要搭建私服,将 maven 服务器部署在公司内部网络,从而避免 jar 包流传出去。怎么做呢,让我们来一步步学习吧。 + +### 下载安装 Nexus + +进入[官方下载地址](https://www.sonatype.com/download-oss-sonatype),选择合适版本下载。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203029.png) + +本人希望将 Nexus 部署在 Linux 机器,所以选用的是 Unix 版本。 + +这里,如果想通过命令方式直接下载(比如用脚本安装),可以在[官方历史发布版本页面](https://help.sonatype.com/repomanager3/download/download-archives---repository-manager-3)中找到合适版本,然后执行以下命令: + +```bash +wget -O /opt/maven/nexus-unix.tar.gz http://download.sonatype.com/nexus/3/nexus-3.13.0-01-unix.tar.gz +tar -zxf nexus-unix.tar.gz +``` + +解压后,有两个目录: + +- nexus-3.13.0-01 - 包含了 Nexus 运行所需要的文件。是 Nexus 运行必须的。 +- sonatype-work - 包含了 Nexus 生成的配置文件、日志文件、仓库文件等。当我们需要备份 Nexus 的时候默认备份此目录即可。 + +### 启动停止 Nexus + +进入 nexus-3.13.0-01/bin 目录,有一个可执行脚本 nexus。 + +执行 `./nexus`,可以查看允许执行的参数,如下所示,含义可谓一目了然: + +```bash +$ ./nexus +Usage: ./nexus {start|stop|run|run-redirect|status|restart|force-reload} +``` + +- 启动 nexus - `./nexus start` +- 停止 nexus - + +启动成功后,在浏览器中访问 `http://:8081`,欢迎页面如下图所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203131.png) + +点击右上角 Sign in 登录,默认用户名/密码为:admin/admin123。 + +有必要提一下的是,在 Nexus 的 Repositories 管理页面,展示了可用的 maven 仓库,如下图所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20181127203156.png) + +> 说明: +> +> - maven-central - maven 中央库(如果没有配置 mirror,默认就从这里下载 jar 包),从 https://repo1.maven.org/maven2/ 获取资源 +> - maven-releases - 存储私有仓库的发行版 jar 包 +> - maven-snapshots - 存储私有仓库的快照版(调试版本) jar 包 +> - maven-public - 私有仓库的公共空间,把上面三个仓库组合在一起对外提供服务,在本地 maven 基础配置 settings.xml 中使用。 + +### 使用 Nexus + +如果要使用 Nexus,还必须在 settings.xml 和 pom.xml 中配置认证信息。 + +#### 配置 settings.xml + +一份完整的 `settings.xml`: + +```xml + + + + + org.sonatype.plugins + + + + + + releases + admin + admin123 + + + snapshots + admin + admin123 + + + + + + + public + * + http://10.255.255.224:8081/repository/maven-public/ + + + + + + zp + + + central + http://central + + true + + + true + + + + + + central + http://central + + true + + + true + always + + + + + + + + zp + + +``` + +#### 配置 pom.xml + +在 pom.xml 中添加如下配置: + +```xml + + + releases + Releases + http://10.255.255.224:8081/repository/maven-releases + + + snapshots + Snapshot + http://10.255.255.224:8081/repository/maven-snapshots + + +``` + +> 🔔 注意: +> +> - `` 和 `` 的 id 必须和 `settings.xml` 配置文件中的 `` 标签中的 id 匹配。 +> - `` 标签的地址需要和 maven 私服的地址匹配。 + +#### 执行 maven 构建 + +如果要使用 settings.xml 中的私服配置,必须通过指定 `-P zp` 来激活 profile。 + +示例: + +```bash +## 编译并打包 maven 项目 +$ mvn clean package -Dmaven.skip.test=true -P zp + +## 编译并上传 maven 交付件(jar 包) +$ mvn clean deploy -Dmaven.skip.test=true -P zp +``` + +## 参考资料 + +- https://www.jianshu.com/p/8c3d7fb09bce +- http://www.ruanyifeng.com/blog/2013/07/gpg.html +- https://www.cnblogs.com/hoobey/p/6102382.html +- https://blog.csdn.net/wzygis/article/details/49276779 +- https://blog.csdn.net/clj198606061111/article/details/52200928 \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" new file mode 100644 index 00000000..561aa81a --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" @@ -0,0 +1,430 @@ +--- +title: Maven 插件之代码检查 +date: 2019-12-16 17:09:26 +order: 06 +categories: + - Java + - 软件 + - 构建 + - Maven +tags: + - Java + - 构建 + - Maven +permalink: /pages/370f1d/ +--- + +# Maven 插件之代码检查 + +## maven-checkstyle-plugin + +> **maven-checkstyle-plugin,用于检测代码中不符合规范的地方。** + +### 定义 checkstyle.xml + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +配置 `pom.xml`: + +```xml + + + ... + + config/maven_checks.xml + + ... + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.0 + + + + validate + validate + + + style/checkstyle.xml + UTF-8 + true + true + false + + + check + + + + + + + org.apache.maven.plugins + maven-jxr-plugin + 2.3 + + + + ... + +``` + +其中可以修改使用的检查规则文件路径,插件默认提供了四个规则文件可以直接使用,无需手动下载: + +- config/sun_checks.xml - Sun Microsystems Definition (default). +- config/maven_checks.xml - Maven Development Definitions. +- config/turbine_checks.xml - Turbine Development Definitions. +- config/avalon_checks.xml - Avalon Development Definitions. + +配置好后,可以执行 `mvn clean checkstyle:check` 检查代码。 + +## maven-pmd-plugin + +> maven-pmd-plugin 是阿里编程规范检查插件。 + +配置 `pom.xml`: + +参考 https://github.com/alibaba/p3c/blob/master/p3c-pmd/pom.xml 配置 + +```xml + + org.apache.maven.plugins + maven-pmd-plugin + 3.11.0 + + ${project.build.sourceEncoding} + ${maven.compiler.target} + true + + rulesets/java/ali-comment.xml + rulesets/java/ali-concurrent.xml + rulesets/java/ali-constant.xml + rulesets/java/ali-exception.xml + rulesets/java/ali-flowcontrol.xml + rulesets/java/ali-naming.xml + rulesets/java/ali-oop.xml + rulesets/java/ali-orm.xml + rulesets/java/ali-other.xml + rulesets/java/ali-set.xml + + true + + + + verify + + check + + + + + + com.alibaba.p3c + p3c-pmd + 2.0.0 + + + + +``` + +配置好后,可以执行 `mvn clean pmd:check` 检查代码。 + +## 参考资料 + +- https://maven.apache.org/plugins/maven-checkstyle-plugin/ +- https://maven.apache.org/jxr/maven-jxr-plugin/ +- https://www.jianshu.com/p/557b975ae40d +- 阿里巴巴编程规范 + - https://github.com/alibaba/p3c + - https://github.com/alibaba/p3c/blob/master/p3c-pmd/pom.xml \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" new file mode 100644 index 00000000..179cb604 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" @@ -0,0 +1,49 @@ +--- +title: Maven 教程 +date: 2020-08-04 15:20:54 +categories: + - Java + - 软件 + - 构建 + - Maven +tags: + - Java + - 构建 + - Maven +permalink: /pages/85f27a/ +hidden: true +index: false +--- + +# Maven 教程 + +> [Maven](https://github.com/apache/maven) 是一个项目管理工具。它负责管理项目开发过程中的几乎所有的东西。 +> +> - **版本** - maven 有自己的版本定义和规则。 +> - **构建** - maven 支持许多种的应用程序类型,对于每一种支持的应用程序类型都定义好了一组构建规则和工具集。 +> - **输出物管理** - maven 可以管理项目构建的产物,并将其加入到用户库中。这个功能可以用于项目组和其他部门之间的交付行为。 +> - **依赖关系** - maven 对依赖关系的特性进行细致的分析和划分,避免开发过程中的依赖混乱和相互污染行为 +> - **文档和构建结果** - maven 的 site 命令支持各种文档信息的发布,包括构建过程的各种输出,javadoc,产品文档等。 +> - **项目关系** - 一个大型的项目通常有几个小项目或者模块组成,用 maven 可以很方便地管理。 +> - **移植性管理** - maven 可以针对不同的开发场景,输出不同种类的输出结果。 + +## 📖 内容 + +- [Maven 快速入门](01.Maven快速入门.md) +- [Maven 教程之 pom.xml 详解](02.Maven教程之pom.xml详解.md) +- [Maven 教程之 settings.xml 详解](03.Maven教程之settings.xml详解.md) +- [Maven 实战问题和最佳实践](04.Maven实战问题和最佳实践.md) +- [Maven 教程之发布 jar 到私服或中央仓库](05.Maven教程之发布jar到私服或中央仓库.md) +- [Maven 插件之代码检查](06.Maven插件之代码检查.md) + +## 📚 资料 + +- **官网** + - [Maven Github](https://github.com/apache/maven) + - [Maven 官方文档](https://maven.apache.org/ref/current) +- **书籍** + - [《Maven 实战》](https://book.douban.com/subject/5345682/) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" new file mode 100644 index 00000000..d0b8d139 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" @@ -0,0 +1,380 @@ +--- +title: Ant 简易教程 +date: 2017-12-06 09:46:28 +order: 02 +categories: + - Java + - 软件 + - 构建 +tags: + - Java + - 构建 + - Ant +permalink: /pages/0bafae/ +--- + +# Ant 简易教程 + +## 简介 + +`Apache Ant` 是一个将软件编译、测试、部署等步骤联系在一起加以自动化的一个工具,大多用于 Java 环境中的软件开发。由 Apache 软件基金会所提供。 + +Ant 是纯 Java 语言编写的,所以具有很好的跨平台性。 + +![img](http://upload-images.jianshu.io/upload_images/3101171-d9da2a06160103d0.png) + +## 下载和安装 + +### 下载 + +ant 的官方下载地址:http://ant.apache.org/bindownload.cgi + +进入页面后,在下图的红色方框中可以下载最新版本。笔者下载的版本是 **apache-ant-1.9.4。** + +![img](http://upload-images.jianshu.io/upload_images/3101171-72d3bc81cd29e68d.png) + +### 配置环境变量 + +配置环境变量(我的电脑 -> 属性 -> 高级 -> 环境变量)。 + +设置 ant 环境变量: + +**ANT_HOME** C:/ apache-ant-1.9.4 + +![img](http://upload-images.jianshu.io/upload_images/3101171-682a8e16b82a7532.png) + +**path ** C:/ apache-ant-1.9.4/bin + +![img](http://upload-images.jianshu.io/upload_images/3101171-ea61070f97b5a7cc.png) + +**classpath** C:/apache-ant-1.9.4/lib + +![img](http://upload-images.jianshu.io/upload_images/3101171-5bc45dbe64602bc7.png) + +### 验证 + +点击 开始 -> 运行 -> 输入 cmd + +**执行构建文件** + +输入如下命令:**ant** + +如果出现如下内容,说明安装成功: + +> Buildfile: build.xml does not exist! +> Build failed + +注意:因为 ant 默认运行 build.xml 文件,这个文件需要我们创建。 + +如果不想命名为 build.xml,运行时可以使用 **ant -buildfile test.xml** 命令指明要运行的构建文件。 + +**查看版本信息** + +输入 **ant -version**,可以查看版本信息。 + +![img](http://upload-images.jianshu.io/upload_images/3101171-920e94f33b4d7dd9.png) + +但如果出现 'ant' 不是内部或外部命令,也不是可运行的程序或批处理文件,说明安装失败:(可以重复前述步骤,直至安装成功。) + +## 例子 + +在安装和配置成功后,我们就可以使用 ant 了。 + +为了让读者对 ant 有一个直观的认识,首先以 Ant 官方手册上的一个简单例子做一个说明。 + +以下是一个 build.xml 文件的内容: + +```xml + + + simple example build file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +在这个 xml 文件中,有几个 target 标签,每个 target 对应一个执行目标。 + +我们将这个 build.xml 放在 D:\Temp\ant_test 路径下,然后在 dos 界面下进行测试。 + +**ant init** + +![img](http://upload-images.jianshu.io/upload_images/3101171-0d37a1be0ef4238a.png) + +在 D:\Temp\ant_test 路径下创建了一个 build 目录,执行成功。 + +**ant compile** + +![img](http://upload-images.jianshu.io/upload_images/3101171-6f35ed13331c87c9.png) + +提示错误,原来是在 build.xml 的所在目录下找不到 src 目录。好的,我们直接创建一个 src 目录,然后再次尝试。这次,执行成功。 + +![img](http://upload-images.jianshu.io/upload_images/3101171-9e84af99a8e952e0.png) + +**ant dist ** + +![img](http://upload-images.jianshu.io/upload_images/3101171-daeaf201bf05e097.png) + +在 D:\Temp\ant_test 路径下创建了一个 dist 目录,执行成功。 + +**ant clean** + +![img](http://upload-images.jianshu.io/upload_images/3101171-be427613f7867513.png) + +清除创建的 build 和 dist 目录,执行成功。 + +**一个细节** + +细心的读者,想必已经发现一个问题——在执行 ant compile 和 ant dist 命令的时候把前面的命令也执行了。这是为什么呢? + +请留意一下 build.xml 中的内容。有部分 **target** 标签中含有 **depends** 关键字。 + +![img](http://upload-images.jianshu.io/upload_images/3101171-746a2156fbfb8d54.png) + +这表明,当前的 target 在执行时需要依赖其他的 target,必须先执行依赖的 target,然后再执行。 + +## 关键元素 + +Ant 的构件文件都是 XML 格式的。每个构件文件包含一个 project 元素和至少一个 target。 + +target 元素可以包含多个 task 元素。 + +### Project 元素 + +**project 元素**是构建文件的根元素。 + +一个 project 元素可以有多个 target 元素,一个 target 元素可以有多个 task。 + +在上节的例子中,project 标签里有三个属性。 + +```xml + +``` + +**name 属性**,指示 project 元素的名字。例子中的名字就是 MyProject。 + +**default 属性**,指示这个 project 默认执行的 target。在本文的例子中,默认执行的 target 为 dist。 + +如果我们输入命令 ant 时,不指定 target 参数,默认会执行 dist 这个 target。 + +**basedir 属性**,指定根路径的位置。该属性没有指定时,使用 Ant 的构件文件的所在目录作为根目录。 + +### Target 元素 + +**target 元素**是 task 的容器,也就是 Ant 的一个基本执行单元。 + +以上节例子中的 compile 来举例。 + +```xml + + + + +``` + +这个 target 中出现了几个属性。 + +**name 属性**,指示 target 元素的名称。 + +这个属性在一个 project 元素中必须是唯一的。这很好理解,如果出现重复,Ant 就不知道具体该执行哪个 target 了。 + +**depends 属性**,指示依赖的 target,当前的 target 必须在依赖的 target 之后执行。 + +**description 属性**,是关于 target 的简短说明。 + +此外,还有其他几个未出现在构建文件中的属性。 + +**if 属性**,验证指定的属性是否存在,若不存在,所在 target 将不会被执行。 + +**unless 属性**,**正好和 if 属性相反**,验证指定的属性是否存在,若存在,所在 target 将不会被执行。\*\*\*\* + +**extensionOf 属性**,添加当前 target 到 **extension-point** 依赖列表。**——Ant1.8.0 新特性。** + +> **extension-point 元素**和 target 元素十分类似,都可以指定依赖的 target。但是不同的是,extension-point 中不能包含任何 task。 + +请看以下实例: + +```xml + + ... + + + + ... + +``` + +**调用 target 顺序**: create-directory-layout --> 'empty slot' --> compile + +```xml + + ... + +``` + +**调用 target 顺序**: create-directory-layout --> generate-sources --> compile + +**onMissingExtensionPoint 属性**:当无法找到一个 extension-point 时,target 尝试去做的动作("fail", "warn", "ignore")。_——Ant1.8.2 新特性_ + +### Task 元素 + +task 是一段可以被执行的代码。 + +一个 task 可以有多个属性, 一个属性可以包含对一个 **property** 的引用。 + +task 的通常结构为 + +```xml + +``` + +其中,name 是 task 的名字, attributeN 是属性名, valueN 是这个属性的值。 + +还是以 compile 做为例子: + +```xml + + + + +``` + +在 compile 这个 target 标签中包含了一个任务。 + +这个任务的动作是:执行 JAVA 编译,编译 src 下的代码,并把编译生成的文件放在 build 目录中。 + +**常用 task ** + +**javac**:用于编译一个或者多个 Java 源文件,通常需要 srcdir 和 destdir 两个属性,用于指定 Java 源文件的位置和编译后 class 文件的保存位置。 + +```xml + +``` + +**java**:用于运行某个 Java 类,通常需要 classname 属性,用于指定需要运行哪个类。 + +```xml + + + + + + +``` + +**jar**:用于生成 JAR 包,通常需要指定 destfile 属性,用于指定所创建 JAR 包的文件名。除此之外,通常还应指定一个文件集,表明需要将哪些文件打包到 JAR 包里。 + +```xml + +``` + +**echo**:输出某个字符串。 + +```xml + +You are using version ${java.version} of Java! This message spans two lines. +``` + +**copy**:用于复制文件或路径。 + +```xml + + + + + + + + +``` + +**delete**:用于删除文件或路径。 + +```xml + + + + + + + + +``` + +**mkdir**:用于创建文件夹。 + +```xml + +``` + +**move**:用户移动文件和路径。 + +```xml + + + + + + +``` + +### Property 元素 + +Property 是对参数的定义。 + +project 的属性可以通过 property 元素来设定,也可在 Ant 之外设定。若要在外部引入某文件,例如 build.properties 文件,可以通过如下内容将其引入:。 + +property 元素可用作 task 的属性值。在 task 中是通过将属性名放在“\${”和“}”之间,并放在 task 属性值的位置来实现的。 + +例如 complile 例子中,使用了前面定义的 src 作为源目录。 + +```xml + +``` + +Ant 提供了一些内置的属性,它能得到的系统属性的列表与 Java 文档中 System.getPropertis()方法得到的属性一致,这些系统属性可参考 sun 网站的说明。 + +### extension-point 元素 + +和 target 元素十分类似,都可以指定依赖的 target。但是不同的是,extension-point 中不能包含任何 task。 + +_——Ant1.8.0 新增特性。_ + +在 target 元素中的例子里已提到过,不再赘述。 + +## 参考资料 + +- [ant 官方手册](http://ant.apache.org/manual/index.html) +- [http://www.blogjava.net/amigoxie/archive/2007/11/09/159413.html](http://www.blogjava.net/amigoxie/archive/2007/11/09/159413.html) \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" new file mode 100644 index 00000000..08c9c99a --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" @@ -0,0 +1,50 @@ +--- +title: Java 构建 +date: 2020-08-04 15:20:54 +categories: + - Java + - 软件 + - 构建 +tags: + - Java + - 构建 +permalink: /pages/d1859b/ +hidden: true +index: false +--- + +# Java 构建 + +> Java 项目需要通过 **构建工具** 来管理项目依赖,完成编译、打包、发布、生成 JavaDoc 等任务。 +> +> - 目前最主流的构建工具是 Maven,它的功能非常强大。 +> - Gradle 号称是要替代 Maven 等构件工具,它的版本管理确实简洁,但是需要学习 Groovy,学习成本比 Maven 高。 +> - Ant 功能比 Maven 和 Gradle 要弱,现代 Java 项目基本不用了,但也有一些传统的 Java 项目还在使用。 + +## 📖 内容 + +### Maven + +- [Maven 快速入门](01.Maven/01.Maven快速入门.md) +- [Maven 教程之 pom.xml 详解](01.Maven/02.Maven教程之pom.xml详解.md) +- [Maven 教程之 settings.xml 详解](01.Maven/03.Maven教程之settings.xml详解.md) +- [Maven 实战问题和最佳实践](01.Maven/04.Maven实战问题和最佳实践.md) +- [Maven 教程之发布 jar 到私服或中央仓库](01.Maven/05.Maven教程之发布jar到私服或中央仓库.md) +- [Maven 插件之代码检查](01.Maven/06.Maven插件之代码检查.md) + +### Ant + +- [Ant 简易教程](02.Ant.md) + +## 📚 资料 + +- **官网** + - [Maven Github](https://github.com/apache/maven) + - [Maven 官方文档](https://maven.apache.org/ref/current) + - [Ant 官方手册](http://ant.apache.org/manual/index.html) +- **书籍** + - [《Maven 实战》](https://book.douban.com/subject/5345682/) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" new file mode 100644 index 00000000..85b4f254 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" @@ -0,0 +1,278 @@ +--- +title: Intellij IDEA 快速入门 +date: 2019-11-29 18:10:14 +order: 01 +categories: + - Java + - 软件 + - IDE +tags: + - Java + - IDE + - Intellij +permalink: /pages/ac5c6a/ +--- + +# Intellij IDEA 快速入门 + +## 快捷键 + +### 核心快捷键 + +IntelliJ IDEA 作为一个以快捷键为中心的 IDE,为大多数操作建议了键盘快捷键。在这个主题中,您可以找到最不可缺少的列表,使 IntelliJ IDEA 轻松实现第一步。 + +核心快捷键表: + +| 操作 | 快捷键 | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------- | +| [根据名称查找操作](https://www.jetbrains.com/help/idea/navigating-to-action.html) | `Ctrl+Shift+A` | +| 显示可用 [意图操作](https://www.jetbrains.com/help/idea/intention-actions.html) 列表 | `Alt+Enter` | +| 切换视图 ([Project](https://www.jetbrains.com/help/idea/project-tool-window.html),[Structure](https://www.jetbrains.com/help/idea/structure-tool-window-file-structure-popup.html), etc.). | `Alt+F1` | +| [切换](https://www.jetbrains.com/help/idea/navigating-between-files-and-tool-windows.html)工具窗口和在编辑器中打开的文件 | `Ctrl+Tab` | +| 显示 [导航栏](https://www.jetbrains.com/help/idea/navigation-bar.html). | `Alt+Home` | +| [插入代码模板](https://www.jetbrains.com/help/idea/generating-code.html). | `Ctrl+J` | +| [在周围插入代码模板](https://www.jetbrains.com/help/idea/creating-code-constructs-using-surround-templates.html). | `Ctrl+Alt+J` | +| [Edit an item from the Project or another tree view](https://www.jetbrains.com/help/idea/opening-and-reopening-files-in-the-editor.html). | `F4` | +| [注释](https://www.jetbrains.com/help/idea/commenting-and-uncommenting-blocks-of-code.html) | `Ctrl+/` `Ctrl+Shift+/` | +| [根据名称查找类或文件](https://www.jetbrains.com/help/idea/navigating-to-class-file-or-symbol-by-name.html). | `Ctrl+N` `Ctrl+Shift+N` | +| [拷贝当前行或指定的行](https://www.jetbrains.com/help/idea/adding-deleting-and-moving-code-elements.html#duplicate). | `Ctrl+D` | +| [增加或减少选中的表达式](https://www.jetbrains.com/help/idea/selecting-text-in-the-editor.html). | `Ctrl+W` and `Ctrl+Shift+W` | +| [在当前文件查找或替换](https://www.jetbrains.com/help/idea/finding-and-replacing-text-in-file.html). | `Ctrl+F` `Ctrl+R` | +| [在项目中或指定的目录中查找或替换](https://www.jetbrains.com/help/idea/finding-and-replacing-text-in-project.html) | `Ctrl+Shift+F` `Ctrl+Shift+R` | +| [全局搜索](https://www.jetbrains.com/help/idea/searching-everywhere.htmls) | 双击 `Shift` | +| [快速查看选中对象的引用](https://www.jetbrains.com/help/idea/highlighting-usages.html). | `Ctrl+Shift+F7` | +| [展开或折叠编辑器中的代码块](https://www.jetbrains.com/help/idea/code-folding.html). | `Ctrl+NumPad Plus` `Ctrl+NumPad -` | +| [调用代码完成](https://www.jetbrains.com/help/idea/auto-completing-code.html#basic_completion). | `Ctrl+Space` | +| [智能声明完成](https://www.jetbrains.com/help/idea/auto-completing-code.html#statements_completion). | `Ctrl+Shift+Enter` | +| [智能补全代码](https://www.jetbrains.com/help/idea/auto-completing-code.html#smart_completion) | `Ctrl+Shift+Space` | +| 显示可用的[重构](https://www.jetbrains.com/help/idea/refactoring-source-code.html)方法列表 | `Ctrl+Shift+Alt+T` | + +### 快捷键分类 + +#### Tradition + +| 快捷键 | 介绍 | +| ---------------- | ------------------------------- | +| Ctrl + Z | 撤销 | +| Ctrl + Shift + Z | 取消撤销 | +| Ctrl + X | 剪切 | +| Ctrl + C | 复制 | +| Ctrl + S | 保存 | +| Tab | 缩进 | +| Shift + Tab | 取消缩进 | +| Shift + Home/End | 选中光标到当前行头位置/行尾位置 | +| Ctrl + Home/End | 跳到文件头/文件尾 | + +#### Editing + +| 快捷键 | 介绍 | +| ----------------------- | -------------------------------------------------------------------------------------------------------- | +| Ctrl + Space | 基础代码补全,默认在 Windows 系统上被输入法占用,需要进行修改,建议修改为 Ctrl + 逗号`(必备)` | +| Ctrl + Alt + Space | 类名自动完成 | +| Ctrl + Shift + Enter | 自动结束代码,行末自动添加分号`(必备)` | +| Ctrl + P | 方法参数提示显示 | +| Ctrl + Q | 光标所在的变量/类名/方法名等上面(也可以在提示补充的时候按),显示文档内容 | +| Shift + F1 | 如果有外部文档可以连接外部文档 | +| Ctrl + F1 | 在光标所在的错误代码处显示错误信息`(必备)` | +| Alt + Insert | 代码自动生成,如生成对象的 set/get 方法,构造函数,toString() 等`(必备)` | +| Ctrl + O | 选择可重写的方法 | +| Ctrl + I | 选择可继承的方法 | +| Ctrl + Alt + T | 对选中的代码弹出环绕选项弹出层`(必备)` | +| Ctrl + / | 注释光标所在行代码,会根据当前不同文件类型使用不同的注释符号`(必备)` | +| Ctrl + Shift + / | 代码块注释`(必备)` | +| Ctrl + W | 递进式选择代码块。可选中光标所在的单词或段落,连续按会在原有选中的基础上再扩展选中范围`(必备)` | +| Ctrl + Shift + W | 递进式取消选择代码块。可选中光标所在的单词或段落,连续按会在原有选中的基础上再扩展取消选中范围`(必备)` | +| Alt + Q | 弹出一个提示,显示当前类的声明/上下文信息 | +| Alt + Enter | IntelliJ IDEA 根据光标所在问题,提供快速修复选择,光标放在的位置不同提示的结果也不同`(必备)` | +| Ctrl + Alt + L | 格式化代码,可以对当前文件和整个包目录使用`(必备)` | +| Ctrl + Alt + O | 优化导入的类,可以对当前文件和整个包目录使用`(必备)` | +| Ctrl + Alt + I | 光标所在行 或 选中部分进行自动代码缩进,有点类似格式化 | +| Ctrl + Shift + C | 复制当前文件磁盘路径到剪贴板`(必备)` | +| Ctrl + Shift + V | 弹出缓存的最近拷贝的内容管理器弹出层 | +| Ctrl + Alt + Shift + C | 复制参考信息 | +| Ctrl + Alt + Shift + V | 无格式黏贴`(必备)` | +| Ctrl + D | 复制光标所在行 或 复制选择内容,并把复制内容插入光标位置下面`(必备)` | +| Ctrl + Y | 删除光标所在行 或 删除选中的行`(必备)` | +| Ctrl + Shift + J | 自动将下一行合并到当前行末尾`(必备)` | +| Shift + Enter | 开始新一行。光标所在行下空出一行,光标定位到新行位置`(必备)` | +| Ctrl + Shift + U | 对选中的代码进行大/小写轮流转换`(必备)` | +| Ctrl + Shift + ]/[ | 选中从光标所在位置到它的底部/顶部的中括号位置`(必备)` | +| Ctrl + Delete | 删除光标后面的单词或是中文句`(必备)` | +| Ctrl + BackSpace | 删除光标前面的单词或是中文句`(必备)` | +| Ctrl + +/- | 展开/折叠代码块 | +| Ctrl + Shift + +/- | 展开/折叠所有代码`(必备)` | +| Ctrl + F4 | 关闭当前编辑文件 | +| Ctrl + Shift + Up/Down | 光标放在方法名上,将方法移动到上一个/下一个方法前面,调整方法排序`(必备)` | +| Alt + Shift + Up/Down | 移动光标所在行向上移动/向下移动`(必备)` | +| Ctrl + Shift + 左键单击 | 把光标放在某个类变量上,按此快捷键可以直接定位到该类中`(必备)` | +| Alt + Shift + 左键双击 | 选择被双击的单词/中文句,按住不放,可以同时选择其他单词/中文句`(必备)` | +| Ctrl + Shift + T | 对当前类生成单元测试类,如果已经存在的单元测试类则可以进行选择`(必备)` | + +#### Search/Replace + +| 快捷键 | 介绍 | +| ---------------- | -------------------------------------------------------------------- | +| Double Shift | 弹出 Search Everywhere 弹出层 | +| F3 | 在查找模式下,定位到下一个匹配处 | +| Shift + F3 | 在查找模式下,查找匹配上一个 | +| Ctrl + F | 在当前文件进行文本查找`(必备)` | +| Ctrl + R | 在当前文件进行文本替换`(必备)` | +| Ctrl + Shift + F | 根据输入内容查找整个项目 或 指定目录内文件`(必备)` | +| Ctrl + Shift + R | 根据输入内容替换对应内容,范围为整个项目 或 指定目录内文件`(必备)` | + +#### Usage Search + +| 快捷键 | 介绍 | +| ----------------- | -------------------------------------------------------------------- | +| Alt + F7 | 查找光标所在的方法/变量/类被调用的地方 | +| Ctrl + Alt + F7 | 显示使用的地方。寻找被该类或是变量被调用的地方,用弹出框的方式找出来 | +| Ctrl + Shift + F7 | 高亮显示所有该选中文本,按 Esc 高亮消失`(必备)` | + +#### Compile and Run + +| 快捷键 | 介绍 | +| ----------------- | ------------------------ | +| Ctrl + F9 | 执行 Make Project 操作 | +| Ctrl + Shift + F9 | 编译选中的文件/包/Module | +| Shift + F9 | Debug | +| Shift + F10 | Run | +| Alt + Shift + F9 | 弹出 Debug 的可选择菜单 | +| Alt + Shift + F10 | 弹出 Run 的可选择菜单 | + +#### Debugging + +| 快捷键 | 介绍 | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | +| F7 | 在 Debug 模式下,进入下一步,如果当前行断点是一个方法,则进入当前方法体内,如果该方法体还有方法,则不会进入该内嵌的方法中 | +| F8 | 在 Debug 模式下,进入下一步,如果当前行断点是一个方法,则不进入当前方法体内 | +| Shift + F7 | 在 Debug 模式下,智能步入。断点所在行上有多个方法调用,会弹出进入哪个方法 | +| Shift + F8 | 在 Debug 模式下,跳出,表现出来的效果跟 F9 一样 | +| Alt + F8 | 在 Debug 模式下,选中对象,弹出可输入计算表达式调试框,查看该输入内容的调试结果 | +| Alt + F9 | 在 Debug 模式下,执行到光标处 | +| F9 | 在 Debug 模式下,恢复程序运行,但是如果该断点下面代码还有断点则停在下一个断点上 | +| Ctrl + F8 | 在 Debug 模式下,设置光标当前行为断点,如果当前已经是断点则去掉断点 | +| Ctrl + Shift + F8 | 在 Debug 模式下,指定断点进入条件 | + +#### Navigation + +| 快捷键 | 介绍 | +| ------------------------ | ----------------------------------------------------------------------------------------------------------------------- | +| Ctrl + N | 跳转到类`(必备)` | +| Ctrl + Shift + N | 跳转到文件`(必备)` | +| Ctrl + Alt + Shift + N | 跳转到符号`(必备)` | +| Alt + Left/Right | 切换当前已打开的窗口中的子视图,比如 Debug 窗口中有 Output、Debugger 等子视图,用此快捷键就可以在子视图中切换`(必备)` | +| F12 | 回到前一个工具窗口`(必备)` | +| ESC | 从工具窗口进入代码文件窗口`(必备)` | +| Shift + ESC | 隐藏当前 或 最后一个激活的工具窗口 | +| Ctrl + G | 跳转到当前文件的指定行处 | +| Ctrl + E | 显示最近打开的文件记录列表`(必备)` | +| Ctrl + Shift + E | 显示最近编辑的文件记录列表`(必备)` | +| Ctrl + Alt + Left/Right | 跳转到上一个/下一个操作的地方`(必备)` | +| Ctrl + Shift + Backspace | 退回到上次修改的地方`(必备)` | +| Alt + F1 | 显示当前文件选择目标弹出层,弹出层中有很多目标可以进行选择`(必备)` | +| Ctrl + B/Ctrl + 左键单击 | 跳转到声明处 | +| Ctrl + Alt + B | 在某个调用的方法名上使用会跳到具体的实现处,可以跳过接口 | +| Ctrl + Shift + B | 跳转到类型声明处`(必备)` | +| Ctrl + Shift + I | 快速查看光标所在的方法 或 类的定义 | +| Ctrl + U | 前往当前光标所在的方法的父类的方法/接口定义`(必备)` | +| Alt + Up/Down | 跳转到当前文件的前一个/后一个方法`(必备)` | +| Ctrl + ]/[ | 跳转到当前所在代码的花括号结束位置/开始位置 | +| Ctrl + F12 | 弹出当前文件结构层,可以在弹出的层上直接输入,进行筛选 | +| Ctrl + H | 显示当前类的层次结构 | +| Ctrl + Shift + H | 显示方法层次结构 | +| Ctrl + Alt + H | 调用层次 | +| F2/Shift + F2 | 跳转到下一个/上一个高亮错误 或 警告位置`(必备)` | +| F4 | 编辑源`(必备)` | +| Alt + Home | 定位/显示到当前文件的 Navigation Bar | +| F11 | 添加书签`(必备)` | +| Ctrl + F11 | 选中文件/文件夹,使用助记符设定/取消书签`(必备)` | +| Shift + F11 | 弹出书签显示层`(必备)` | +| Alt + 1,2,3...9 | 显示对应数值的选项卡,其中 1 是 Project 用得最多`(必备)` | +| Ctrl + 1,2,3...9 | 定位到对应数值的书签位置`(必备)` | + +#### Refactoring + +| 快捷键 | 介绍 | +| ---------------------- | ------------------------------ | +| Shift + F6 | 对文件/文件夹 重命名`(必备)` | +| Ctrl + Alt + Shift + T | 打开重构菜单`(必备)` | + +#### VCS/Local History + +| 快捷键 | 介绍 | +| --------------- | -------------------------------------------------- | +| Ctrl + K | 版本控制提交项目,需要此项目有加入到版本控制才可用 | +| Ctrl + T | 版本控制更新项目,需要此项目有加入到版本控制才可用 | +| `Alt + |` | 显示版本控制常用操作菜单弹出层`(必备)` | +| Alt + Shift + C | 查看最近操作项目的变化情况列表 | +| Alt + Shift + N | 选择/添加 task`(必备)` | + +#### Live Templates + +| 快捷键 | 介绍 | +| -------------- | -------------------------------------------- | +| Ctrl + J | 插入自定义动态代码模板`(必备)` | +| Ctrl + Alt + J | 弹出模板选择窗口,将选定的代码加入动态模板中 | + +#### General + +| 快捷键 | 介绍 | +| ---------------------- | --------------------------------------------------------------------- | +| Ctrl + Tab | 编辑窗口切换,如果在切换的过程又加按上 delete,则是关闭对应选中的窗口 | +| Ctrl + Alt + Y | 同步、刷新 | +| Ctrl + Alt + S | 打开 IntelliJ IDEA 系统设置`(必备)` | +| Ctrl + Alt + Shift + S | 打开当前项目设置`(必备)` | +| Ctrl + Shift + A | 查找动作/设置`(必备)` | +| Ctrl + Shift + F12 | 编辑器最大化`(必备)` | +| Alt + Shift + F | 显示添加到收藏夹弹出层/添加到收藏夹 | +| Alt + Shift + I | 查看项目当前文件 | + +### Intellij IDEA 官方快捷键表 + +![img](http://upload-images.jianshu.io/upload_images/3101171-6a44121ae280a10e.png) + +## 插件 + +推荐几个比较好用的插件 + +- [Key promoter](https://plugins.jetbrains.com/plugin/4455?pr=idea) [快捷键提示](https://plugins.jetbrains.com/plugin/4455?pr=idea) +- [CamelCase](https://plugins.jetbrains.com/plugin/7160?pr=idea) 驼峰式命名和下划线命名交替变化 +- [CheckStyle-IDEA](https://plugins.jetbrains.com/plugin/1065?pr=idea) 代码规范检查 +- [FindBugs-IDEA](https://plugins.jetbrains.com/plugin/3847?pr=idea)潜在 Bug 检查 +- [MetricsReloaded](https://plugins.jetbrains.com/plugin/93?pr=idea) 代码复杂度检查 +- [Statistic](https://plugins.jetbrains.com/plugin/4509?pr=idea) 代码统计 +- [JRebel Plugin](https://plugins.jetbrains.com/plugin/?id=4441) 热部署 +- [GsonFormat](https://plugins.jetbrains.com/plugin/7654?pr=idea) 把 JSON 字符串直接实例化成类 +- [Eclipse Code Formatter](https://plugins.jetbrains.com/plugin/6546-eclipse-code-formatter) 如果你以前用的是 IDE,并有自己的一套代码风格配置,可以通过此插件导入到 IDEA +- [Alibaba Java Coding Guidelines](https://plugins.jetbrains.com/plugin/10046-alibaba-java-coding-guidelines) 阿里 Java 开发规范的静态检查工具 +- [IDE Features Trainer](https://plugins.jetbrains.com/plugin/8554-ide-features-trainer) 官方的新手训练插件 +- [Markdown Navigator](https://plugins.jetbrains.com/plugin/7896-markdown-navigator) Markdown 插件,适用于喜欢用 markdown 写文档的人 + +## 个性化 + +### 颜色主题 + +[intellij-colors-solarized](https://github.com/jkaving/intellij-colors-solarized) 个人觉得这种色彩搭配十分优雅 + +[下载地址](https://github.com/altercation/solarized) + +## FAQ + +(1)运行时报错 + +> Error running XXX. Command line is too long. Shorten the command line via JAR manifest or via a classpath file and rerun + +解决方案: + +找到 `.idea/libraies/workspace.xml` 中的 `` + +添加一行配置: + +```xml + +``` + +## 参考资料 + +- [IntelliJ-IDEA-Tutorial](https://github.com/judasn/IntelliJ-IDEA-Tutorial) +- [极客学院 - Intellij IDEA 使用教程](http://wiki.jikexueyuan.com/project/intellij-idea-tutorial/) \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" new file mode 100644 index 00000000..d8f3730d --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" @@ -0,0 +1,247 @@ +--- +title: Eclipse 快速入门 +date: 2018-07-01 11:27:47 +order: 02 +categories: + - Java + - 软件 + - IDE +tags: + - Java + - IDE +permalink: /pages/2257c7/ +--- + +# Eclipse 快速入门 + +## 代码智能提示 + +### Java 智能提示 + +Window -> Preferences -> Java -> Editor -> Content Assist -> Auto Activation + +![img](http://upload-images.jianshu.io/upload_images/3101171-1a0d2206870c6542.jpg) + +delay 是自动弹出提示框的延时时间,我们可以修改成 100 毫秒;triggers 这里默认是".",只要加上"abcdefghijklmnopqrstuvwxyz"或者"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",嘿嘿!这下就能做到和 VS 一样的输入每个字母都能提示啦: + +![img](http://upload-images.jianshu.io/upload_images/3101171-5eb9ad6afbe05289.jpg) + +其它类型的文件比如 HTML、JavaScript、JSP 如果也能提供提示那不是更爽了?有了第二点设置的基础,其实这些设置都是一样的。 + +### JavaScript 智能提示 + +Window -> Preferences -> JavaScript-> Editor -> Content Assist -> Auto-Activation + +![img](http://upload-images.jianshu.io/upload_images/3101171-31bca3ed1b0d0050.jpg) + +### HTML 智能提示 + +Window -> Preferences -> Web -> HTML Files -> Editor -> Content Assist -> Auto-Activation + +![img](http://upload-images.jianshu.io/upload_images/3101171-7c1ce7a35793f234.jpg) + +保存后,我们再来输入看看,感觉真是不错呀: + +![img](http://images2015.cnblogs.com/blog/318837/201605/318837-20160519135330638-166066199.jpg) + +## 插件安装 + +很多教科书上说到 Eclipse 的插件安装都是通过 Help -> Install New SoftWare 这种自动检索的方式,操作起来固然是方便,不过当我们不需要某种插件时不太容易找到要删除哪些内容,而且以后 Eclipse 版本升级的时候,通过这种方式安装过的插件都得再重新装一次。另外一种通过 Link 链接方式,就可以解决这些问题。 + +我们以 Eclipse 的中文汉化包插件为例,先到官方提供的汉化包地址下载一个:[http://www.eclipse.org/babel/downloads.php](http://www.eclipse.org/babel/downloads.php),注意选好自己的 Eclipse 版本: + +![img](http://upload-images.jianshu.io/upload_images/3101171-d8a662eaba3550e3.jpg) + +我的版本是 Kepler,然后进入下载页面,单击红框框中的链接,即可下载汉化包了: + +![img](http://upload-images.jianshu.io/upload_images/3101171-3544d5393f4e298f.jpg) + +下载完解压缩后,会有个包含 features 和 plugin 目录的 eclipse 文件夹,把这个 eclipse 放在我们的 Eclipse 安装根目录,也就是和 eclipse.exe 同一级目录下。然后仍然在这一级目录下,新建一个 links 文件夹,并在该文件夹内,建一个 language.link 的文本文件。该文本文件的名字是可以任取的,后缀名是.link,而不是.txt 哟。好了,最后一步,编辑该文件,在里面写入刚才放入的语言包的地址,并用“\\”表示路径,一定要有 path= 这个前缀。 + +![img](http://upload-images.jianshu.io/upload_images/3101171-18f74bf3080d2c1b.jpg) + +保存文件后,重新打开 Eclipse,熟悉的中文界面终于看到了。虽然汉化不完全,不过也够用了不是么。如果仍然出现的是英文,说明汉化失败,重新检查下 language.link 文件中配置的信息是否和汉化包的目录一致。  其它的插件安装方法也是如此,当不需要某个插件时,只需删除存放插件的目录和 links 目录下相应的 link 文件,或者改变下 link 文件里面的路径变成无效路径即可;对 Eclipse 做高版本升级时,也只需把老版存放插件的目录和 links 目录复制过去就行了。 + +## 基本设置 + +在 Preference 的搜索项中搜索 Text Editors。 +可以参考我的设置: +Show line numbers +Show print margin +Insert spaces for tabs + +![img](http://upload-images.jianshu.io/upload_images/3101171-91aa025fbe7592f8.png) +设置代码的字体类型和大小: + +Window -> Preferences -> General -> Appearance -> Content Assist -> Colors and Fornts,只需修改 Basic 里面的 Text Font 就可以了。 + +推荐 Courier New。 + +![img](http://upload-images.jianshu.io/upload_images/3101171-49b9126aa0dd58c0.jpg) + +![img](http://upload-images.jianshu.io/upload_images/3101171-b7d6d71b2c211321.jpg) + +## 设置文本文件及 JSP 文件编码 + +Window -> Preferences -> General -> Workspace -> Text file encoding -> Other: + +![img](http://upload-images.jianshu.io/upload_images/3101171-b7aa010673565c88.jpg) + +![img](http://upload-images.jianshu.io/upload_images/3101171-b83fa3476fddde46.jpg) + +## 设置 JDK 本地 JavaDOC API 路径及源码路径 + +![img](http://upload-images.jianshu.io/upload_images/3101171-45b5dee3d3ce917a.jpg) + +![img](http://upload-images.jianshu.io/upload_images/3101171-f960daf4839662e3.jpg) + +还都生成的是无意义的变量名,这样可能会对含有相同类型的变量参数的调用顺序造成干扰; + +这种问题,我们把 JDK 或者相应 Jar 包的源码导入进去就能避免了: + +Window -> Preferences -> Java -> Installed JREs -> Edit: + +![img](http://upload-images.jianshu.io/upload_images/3101171-a08c9166dbbf4361.jpg) + +选中设置好的 JRE 目录,编辑,然后全选 JRE system libraries 下的所有 Jar 包,点击右边的 Source Attachment; + +![img](http://upload-images.jianshu.io/upload_images/3101171-4e6c78afa8e3e50b.jpg) + +External location 下,选中 JDK 安装目录下的 src.zip 文件,一路 OK 下来。 + +![img](http://upload-images.jianshu.io/upload_images/3101171-e82d03ce88f64312.jpg) + +设置完,我们再来看看,幸福来的好突然有木有! + +![img](http://upload-images.jianshu.io/upload_images/3101171-400b3952aa60cb8e.jpg) + +## 设置 Servlet 源码或其它 Jar 包源码 + +![img](http://upload-images.jianshu.io/upload_images/3101171-ec1980f58297e42c.jpg) + +上一步已经设置过了 JDK 的源码或 JavaDoc 路径,为啥现在又出来了呢?其实这个不难理解,因为我们使用到的类的源码并不在 JDK 的源码包中。 + +仔细看,我们会发现这些 Jar 包其实都在 Tomcat 根目录下的 lib 文件夹中,但是翻遍了 Tomcat 目录也没有相应的 jar 或 zip 文件呀。既然本地没有,那就去官网上找找: + +http://tomcat.apache.org/download-70.cgi这里有Tomcat的安装包和源码包; + +![img](http://upload-images.jianshu.io/upload_images/3101171-2962b9cd48422373.jpg) + +可以自定义一个专门用于存放 JavaSource 和 JavaDoc 的文件夹,把下载文件放到该目录下, + +然后再切换到 Eclipse 下,选中没有代码提示的类或者函数, 按下 F3,点击 Change Attached Source: + +![img](http://upload-images.jianshu.io/upload_images/3101171-e8c7cc17364206cf.jpg) + +选择我们刚才下载好的 tomcat 源码文件,一路 OK。 + +![img](http://upload-images.jianshu.io/upload_images/3101171-138cb66d553d9306.jpg) + +然后再回过头看看我们的代码提示,友好多了: + +![img](http://upload-images.jianshu.io/upload_images/3101171-b27652e5ff1d6eab.jpg) + +其它 Jar 包源码的设置方式也一样。 + +## 反编译插件 JD-Eclipse + +无论是开发还是调试,反编译必不可少,每次都用 jd-gui 打开去看,多麻烦,干脆配置下 JD 插件,自动关联.class: + +先从 http://jd.benow.ca/ 上下载离线安装包 jdeclipse_update_site.zip,解压缩后把 features、plugins 这 2 个文件夹复制到 新建文件夹 jdeclipse,然后把 jdeclipse 文件夹整个复制到 Eclipse 根目录的 dropins 文件夹下,重启 Eclipse 即可。这种方式是不是比建 link 文件更方便了? + +![img](http://upload-images.jianshu.io/upload_images/3101171-403597b1b46607ef.jpg) + +打开 Eclipse,Window -> Preferences -> General - > Editors ,把 .class 文件设置关联成 jd 插件的 editor + +![img](http://upload-images.jianshu.io/upload_images/3101171-d95625fcf362526c.jpg) + +## Validate 优化 + +我们在 eclipse 里经常看到这个进程,validating... 逐个的检查每一个文件。那么如何关闭一些 validate 操作呢? + +![img](http://upload-images.jianshu.io/upload_images/3101171-8323d6f595f96fdd.png) + +打开 eclipse,点击【window】菜单,选择【preferences】选项。 + +![img](http://upload-images.jianshu.io/upload_images/3101171-88bd81ece1b3f29f.png) + +在左侧点击【validation】选项,在右侧可以看到 eclipse 进行的自动检查都有哪些内容。 + +![img](http://upload-images.jianshu.io/upload_images/3101171-0c3cf67c6e954dd6.png) + +将 Manual(手动)保持不动,将 build 里面只留下 classpath dependency Validator,其他的全部去掉。 + +![img](http://upload-images.jianshu.io/upload_images/3101171-e1a3051fde4828d8.png) + +最后点击【OK】按钮,保存设置。 + +![img](http://upload-images.jianshu.io/upload_images/3101171-7c6803eed8cf618a.png) + +以后如果需要对文件进行校验检查的时候,在文件上点击右键,点击【Validate】进行检查。 + +## 常用快捷键 + +| 快捷键 | 描述 | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Ctrl+1 | 快速修复(最经典的快捷键,就不用多说了,可以解决很多问题,比如 import 类、try catch 包围等) | +| Ctrl+Shift+F | 格式化当前代码 | +| Ctrl+Shift+M | 添加类的 import 导入 | +| Ctrl+Shift+O | 组织类的 import 导入(既有 Ctrl+Shift+M 的作用,又可以帮你去除没用的导入,很有用) | +| Ctrl+Y | 重做(与撤销 Ctrl+Z 相反) | +| Alt+/ | 内容辅助(帮你省了多少次键盘敲打,太常用了) | +| Ctrl+D | 删除当前行或者多行 | +| Alt+↓ | 当前行和下面一行交互位置(特别实用,可以省去先剪切,再粘贴了) | +| Alt+↑ | 当前行和上面一行交互位置(同上) | +| Ctrl+Alt+↓ | 复制当前行到下一行(复制增加) | +| Ctrl+Alt+↑ | 复制当前行到上一行(复制增加) | +| Shift+Enter | 在当前行的下一行插入空行(这时鼠标可以在当前行的任一位置,不一定是最后) | +| Ctrl+/ | 注释当前行,再按则取消注释 | +| Alt+Shift+↑ | 选择封装元素 | +| Alt+Shift+← | 选择上一个元素 | +| Alt+Shift+→ | 选择下一个元素 | +| Shift+← | 从光标处开始往左选择字符 | +| Shift+→ | 从光标处开始往右选择字符 | +| Ctrl+Shift+← | 选中光标左边的单词 | +| Ctrl+Shift+→ | 选中光标又边的单词 | +| Ctrl+← | 光标移到左边单词的开头,相当于 vim 的 b | +| Ctrl+→ | 光标移到右边单词的末尾,相当于 vim 的 e | +| Ctrl+K | 参照选中的 Word 快速定位到下一个(如果没有选中 word,则搜索上一次使用搜索的 word) | +| Ctrl+Shift+K | 参照选中的 Word 快速定位到上一个 | +| Ctrl+J | 正向增量查找(按下 Ctrl+J 后,你所输入的每个字母编辑器都提供快速匹配定位到某个单词,如果没有,则在状态栏中显示没有找到了,查一个单词时,特别实用,要退出这个模式,按 escape 建) | +| Ctrl+Shift+J | 反向增量查找(和上条相同,只不过是从后往前查) | +| Ctrl+Shift+U | 列出所有包含字符串的行 | +| Ctrl+H | 打开搜索对话框 | +| Ctrl+G | 工作区中的声明 | +| Ctrl+Shift+G | 工作区中的引用 | +| Ctrl+Shift+T | 搜索类(包括工程和关联的第三 jar 包) | +| Ctrl+Shift+R | 搜索工程中的文件 | +| Ctrl+E | 快速显示当前 Editer 的下拉列表(如果当前页面没有显示的用黑体表示) | +| F4 | 打开类型层次结构 | +| F3 | 跳转到声明处 | +| Alt+← | 前一个编辑的页面 | +| Alt+→ | 下一个编辑的页面(当然是针对上面那条来说了) | +| Ctrl+PageUp/PageDown | 在编辑器中,切换已经打开的文件 | +| F5 | 单步跳入 | +| F6 | 单步跳过 | +| F7 | 单步返回 | +| F8 | 继续 | +| Ctrl+Shift+D | 显示变量的值 | +| Ctrl+Shift+B | 在当前行设置或者去掉断点 | +| Ctrl+R | 运行至行(超好用,可以节省好多的断点) | +| Alt+Shift+R | 重命名方法名、属性或者变量名 (是我自己最爱用的一个了,尤其是变量和类的 Rename,比手工方法能节省很多劳动力) | +| Alt+Shift+M | 把一段函数内的代码抽取成方法 (这是重构里面最常用的方法之一了,尤其是对一大堆泥团代码有用) | +| Alt+Shift+C | 修改函数结构(比较实用,有 N 个函数调用了这个方法,修改一次搞定) | +| Alt+Shift+L | 抽取本地变量( 可以直接把一些魔法数字和字符串抽取成一个变量,尤其是多处调用的时候) | +| Alt+Shift+F | 把 Class 中的 local 变量变为 field 变量 (比较实用的功能) | +| Alt+Shift+I | 合并变量(可能这样说有点不妥 Inline) | +| Alt+Shift+V | 移动函数和变量(不怎么常用) | +| Alt+Shift+Z | 重构的后悔药(Undo) | +| Alt+Enter | 显示当前选择资源的属性,windows 下的查看文件的属性就是这个快捷键,通常用来查看文件在 windows 中的实际路径 | +| Ctrl+↑ | 文本编辑器 上滚行 | +| Ctrl+↓ | 文本编辑器 下滚行 | +| Ctrl+M | 最大化当前的 Edit 或 View (再按则反之) | +| Ctrl+O | 快速显示 OutLine | +| Ctrl+T | 快速显示当前类的继承结构 | +| Ctrl+W | 关闭当前 Editer | +| Ctrl+L | 文本编辑器 转至行 | +| F2 | 显示工具提示描述 | \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" new file mode 100644 index 00000000..909e80c5 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" @@ -0,0 +1,61 @@ +--- +title: Vscode 快速入门 +date: 2019-05-14 14:57:33 +order: 03 +categories: + - Java + - 软件 + - IDE +tags: + - Java + - IDE +permalink: /pages/0f7153/ +--- + +# Vscode 快速入门 + +## 快捷键 + +### 命令面板(Command Palette) + +根据您当前的上下文访问所有可用命令。 + +> Mac: cmd+shift+p or f1 +> Windows / Linux: ctrl+shift+p or f1 + +### 快速打开文件(Quick Open) + +> Mac: cmd+p +> Windows / Linux: ctrl+p + +### Status Bar + +> Mac: shift+cmd+m +> Windows / Linux: ctrl+shift+m + +### 改变语言模式 + +> Mac: cmd+k m +> Windows / Linux: ctrl+k m + +### 设置 + +> Mac: cmd+, +> Windows / Linux: **File** > **Preferences** > **Settings** or ctrl+, + +## 插件 + +- Chinese (Simplified) Language Pack for Visual Studio Code +- Prettier - Code formatter +- IntelliJ IDEA Keybindings +- EditorConfig for VS Code +- Git History + +## 更多内容 + +- 官方 + - https://github.com/Microsoft/vscode + - https://github.com/Microsoft/vscode-docs + - https://github.com/Microsoft/vscode-tips-and-tricks +- 更多资源 + - https://github.com/viatsko/awesome-vscode \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/README.md" "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/README.md" new file mode 100644 index 00000000..e487ace7 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/02.IDE/README.md" @@ -0,0 +1,26 @@ +--- +title: Java IDE +date: 2022-02-18 08:53:11 +categories: + - Java + - 软件 + - IDE +tags: + - Java + - IDE +permalink: /pages/8695a7/ +hidden: true +index: false +--- + +# Java IDE + +> 自从有了 **IDE**,写代码从此就告别了刀耕火种的蛮荒时代。 +> +> - [Eclipse](02.Eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 +> - 曾经抗拒从转 [Intellij Idea](01.Intellij.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 +> - 你可以在 [vscode](03.VsCode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 + +- [Intellij IDEA 快速入门](01.Intellij.md) +- [Eclipse 快速入门](02.Eclipse.md) +- [Vscode 快速入门](03.VsCode.md) \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" new file mode 100644 index 00000000..7ccdb465 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" @@ -0,0 +1,41 @@ +--- +title: 监控工具对比 +date: 2020-02-11 17:48:32 +order: 01 +categories: + - Java + - 软件 + - 监控诊断 +tags: + - Java + - 监控 +permalink: /pages/16563a/ +--- + +# 监控工具对比 + +## 监控工具发展史 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211165813.png) + +## 监控工具比对 + +### 特性对比 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211171551.png) + +### 生态对比 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211172631.png) + +## 技术选型 + +- Zipkin 欠缺 APM 报表能力,不推荐。 +- 企业级,推荐 CAT +- 关注和试点 SkyWalking。 + +用好调用链监控,需要订制化、自研能力。 + +## 参考资料 + +[CAT、Zipkin 和 SkyWalking 该如何选型?](https://time.geekbang.org/dailylesson/detail/100028416) \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" new file mode 100644 index 00000000..ee099d36 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" @@ -0,0 +1,86 @@ +--- +title: CAT 快速入门 +date: 2020-02-11 17:48:32 +order: 02 +categories: + - Java + - 软件 + - 监控诊断 +tags: + - Java + - 监控 + - CAT +permalink: /pages/821ca3/ +--- + +# CAT 快速入门 + +## CAT 简介 + +CAT(Central Application Tracking),是基于 Java 开发的分布式实时监控系统。CAT 在基础存储、高性能通信、大规模在线访问、服务治理、实时监控、容器化及集群智能调度等领域提供业界领先的、统一的解决方案。CAT 目前在美团的产品定位是应用层的统一监控组件,基本接入了美团所有核心应用,在中间件(RPC、数据库、缓存、MQ 等)框架中得到广泛应用,为各业务线提供系统的性能指标、健康状况、实时告警等。 + +### CAT 的优势 + +- 实时处理:信息的价值会随时间锐减,尤其是事故处理过程中 +- 全量数据:最开始的设计目标就是全量采集,全量的好处有很多 +- 高可用:所有应用都倒下了,需要监控还站着,并告诉工程师发生了什么,做到故障还原和问题定位 +- 故障容忍:CAT 本身故障不应该影响业务正常运转,CAT 挂了,应用不该受影响,只是监控能力暂时减弱 +- 高吞吐:要想还原真相,需要全方位地监控和度量,必须要有超强的处理吞吐能力 +- 可扩展:支持分布式、跨 IDC 部署,横向扩展的监控系统 + +### 支持的消息类型 + +CAT 监控系统将每次 URL、Service 的请求内部执行情况都封装为一个完整的消息树、消息树可能包括 Transaction、Event、Heartbeat、Metric 等信息。 + +- **Transaction** 适合记录跨越系统边界的程序访问行为,比如远程调用,数据库调用,也适合执行时间较长的业务逻辑监控,Transaction 用来记录一段代码的执行时间和次数 +- **Event** 用来记录一件事发生的次数,比如记录系统异常,它和 transaction 相比缺少了时间的统计,开销比 transaction 要小 +- **Heartbeat** 表示程序内定期产生的统计信息, 如 CPU 利用率, 内存利用率, 连接池状态, 系统负载等 +- **Metric** 用于记录业务指标、指标可能包含对一个指标记录次数、记录平均值、记录总和,业务指标最低统计粒度为 1 分钟 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211174235.png) + +## CAT 部署 + +Cat 部署可以参考 [官方 Wiki - 服务端部署](https://github.com/dianping/cat/wiki/readme_server) ,非常详细,不赘述。 + +## CAT 报表 + +与其他监控工具(如 Zipkin、SkyWalking)相比,CAT 的报表功能最丰富。支持以下报表类型: + +- **[Transaction 报表](https://github.com/dianping/cat/wiki/transaction)** - 一段代码运行时间、次数,比如 URL、Cache、SQL 执行次数和响应时间 +- **[Event 报表](https://github.com/dianping/cat/wiki/event)** - 一行代码运行次数,比如出现一个异常 +- **[Problem 报表](https://github.com/dianping/cat/wiki/problem)** - 根据 Transaction/Event 数据分析出来系统可能出现的异常,包括访问较慢的程序等 +- **[Heartbeat 报表](https://github.com/dianping/cat/wiki/heartbeat)** - JVM 内部一些状态信息,比如 Memory,Thread 等 +- **[Business 报表](https://github.com/dianping/cat/wiki/business)** - 业务监控报表,比如订单指标,支付等业务指标 + +## CAT 配置 + +CAT 提供了以下配置: + +- **[项目配置](https://github.com/dianping/cat/wiki/project)** 包括项目基本信息、机器分组配置 +- **[告警配置](https://github.com/dianping/cat/wiki/alarm)** 包括基本告警配置、告警规则、以及具体告警配置 +- **[全局配置](https://github.com/dianping/cat/wiki/global)** 包括服务端配置、消息采样配置、客户端路由 +- **[业务指标](https://github.com/dianping/cat/wiki/business)** 包括业务监控配置、业务标签配置 + +## CAT 架构 + +CAT 主要分为三个模块: + +- **cat-client** - 提供给业务以及中间层埋点的底层 SDK。 +- **cat-consumer** - 用于实时分析从客户端的提供的数据。 +- **cat-home** - 作为用户提供给用户的展示的控制端。 + +在实际开发和部署中,cat-consumer 和 cat-home 是部署在一个 jvm 内部,每个 CAT 服务端都可以作为 consumer 也可以作为 home,这样既能减少整个 CAT 层级结构,也可以增加整个系统稳定性。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211174001.png) + +上图是 CAT 目前多机房的整体结构图: + +- 路由中心是根据应用所在机房信息来决定客户端上报的 CAT 服务端地址 +- 每个机房内部都有的独立的原始信息存储集群 HDFS +- cat-home 可以部署在一个机房也可以部署在多个机房,在做报表展示的时候,cat-home 会从 cat-consumer 中进行跨机房的调用,将所有的数据合并展示给用户 +- 实际过程中,cat-consumer、cat-home 以及路由中心都是部署在一起,每个服务端节点都可以充当任何一个角色 + +## 参考资料 + +- [CAT Github](https://github.com/dianping/cat) \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" new file mode 100644 index 00000000..e665495d --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" @@ -0,0 +1,172 @@ +--- +title: Zipkin 快速入门 +date: 2020-03-23 22:56:45 +order: 03 +categories: + - Java + - 软件 + - 监控诊断 +tags: + - Java + - 监控 + - Zipkin +permalink: /pages/0a8826/ +--- + +# Zipkin 快速入门 + +**Zipkin 是一个基于 Java 开发的、开源的、分布式实时数据跟踪系统(Distributed Tracking System)**。它采集有助于解决服务架构中延迟问题的实时数据。 + +Zipkin 主要功能是聚集来自各个异构系统的实时监控数据。分布式跟踪系统还有其他比较成熟的实现,例如:Naver 的 Pinpoint、Apache 的 HTrace、阿里的鹰眼 Tracing、京东的 Hydra、新浪的 Watchman,美团点评的 CAT,skywalking 等。 + +Zipkin 基于 Google Dapper 的论文设计而来,由 Twitter 公司开发贡献。 + +## 一、Zipkin 简介 + +### 特性 + +如果日志文件中有跟踪 ID,则可以直接跳至该跟踪 ID。 否则,您可以基于属性进行查询,例如服务,操作名称,标签和持续时间。 将为您总结一些有趣的数据,例如在服务中花费的时间百分比以及操作是否失败。 + +Zipkin UI 还提供了一个依赖关系图,该关系图显示了每个应用程序中跟踪了多少个请求。这对于识别聚合行为(包括错误路径或对不赞成使用的服务的调用)很有帮助。 + +![Zipkin UI](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211161706.png) + +### 多平台 + +Zipkin 官方支持 C#、Go、Java、JavaScript、Ruby、Scala、PHP 语言。 + +除此以外,社区还贡献了多种其他语言的支持,详情可以参考官方文档:[Tracers and Instrumentation](https://zipkin.io/pages/tracers_instrumentation.html) + +### 数据 + +Zipkin 服务器捆绑了用于采集和存储数据的扩展。 + +默认情况下,数据可以通过 `Http`,`Kafka` 、`RabbitMQ` 或 RPC 传输。 + +并存储在内存中或 `MySQL`、`Cassandra` 或 `Elasticsearch` 中。 + +数据以 json 形式存储,可以参考:[Zipkin 官方的 Swagger API](https://zipkin.io/zipkin-api/#/default/post_spans) + +![Zipkin Swagger API](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211162055.png) + +## 二、Zipkin 安装 + +### Docker + +Docker 启动方式: + +```shell +docker run -d -p 9411:9411 openzipkin/zipkin +``` + +### Java + +> 注意:必须运行在 JDK8+ 环境 + +Java 启动方式: + +```shell +curl -sSL https://zipkin.io/quickstart.sh | bash -s +java -jar zipkin.jar +``` + +### 编译方式 + +适用于需要订制化的场景。 + +```shell +# get the latest source +git clone https://github.com/openzipkin/zipkin +cd zipkin +# Build the server and also make its dependencies +./mvnw -DskipTests --also-make -pl zipkin-server clean install +# Run the server +java -jar ./zipkin-server/target/zipkin-server-*exec.jar +``` + +## 三、Zipkin 架构 + +ZipKin 可以分为两部分, + +- 一部分是 Zipkin server,用来作为数据的采集存储、数据分析与展示; +- 另一部分是 Zipkin client 是 Zipkin 基于不同的语言及框架封装的一些列客户端工具,这些工具完成了追踪数据的生成与上报功能。 + +架构如下: + +![Zipkin 架构](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211155836.png) + +### Zipkin Server + +Zipkin Server 主要包括四个模块: + +- **Collector** - 负责采集客户端传输的数据。 +- **Storage** - 负责存储采集的数据。当前支持 Memory,MySQL,Cassandra,ElasticSearch 等,默认存储在内存中。 +- **API(Query)** - 负责查询 Storage 中存储的数据。提供简单的 JSON API 获取数据,主要提供给 web UI 使用。 +- **UI** - 提供简单的 web 界面。 + +Instrumented Client 和 Instrumented Server,是指分布式架构中使用了 Trace 工具的两个应用,Client 会调用 Server 提供的服务,两者都会向 Zipkin 上报 Trace 相关信息。在 Client 和 Server 通过 Transport 上报 Trace 信息后,由 Zipkin 的 Collector 模块接收,并由 Storage 模块将数据存储在对应的存储介质中,然后 Zipkin 提供 API 供 UI 界面查询 Trace 跟踪信息。Non-Instrumented Server,指的是未使用 Trace 工具的 Server,显然它不会上报 Trace 信息。 + +### Zipkin Client + +- **Tracer** - `Tracer` 存在于你的应用中,它负责采集关于已发生操作的实时元数据。它们通常会检测库,因此对于用户是透明的。例如,已检测的 Web 服务器记录它何时接收到请求,以及何时发送响应。收集的跟踪数据称为跨度(Span)。 +- **Instrumentation** - Instrumentation 保证了生产环境的安全性和很少的开销。因此,它们仅在内部传播 ID,以告知接收方正在进行追踪。完成的 Span 将通过外部通信告知 Zipkin,类似于应用程序异步报告指标的方式。例如,当跟踪某个操作并且需要发出 http 请求时,会添加一些 header 来传播 ID。header 不用于发送详细信息,例如操作名称。 +- **Reporter** - 能够将数据发送到 Zipkin 的检测应用程序中的组件,被称为 Reporter。Reporter 有多种传输方式,可以将跟踪数据发送到 Zipkin 采集器,后者将跟踪数据持久化保存到存储中。稍后,API 会查询存储以向 UI 提供渲染数据。 + +以下是 Zipkin 的一个示例工作流: + +```shell +┌─────────────┐ ┌───────────────────────┐ ┌─────────────┐ ┌──────────────────┐ +│ User Code │ │ Trace Instrumentation │ │ Http Client │ │ Zipkin Collector │ +└─────────────┘ └───────────────────────┘ └─────────────┘ └──────────────────┘ + │ │ │ │ + ┌─────────┐ + │ ──┤GET /foo ├─▶ │ ────┐ │ │ + └─────────┘ │ record tags + │ │ ◀───┘ │ │ + ────┐ + │ │ │ add trace headers │ │ + ◀───┘ + │ │ ────┐ │ │ + │ record timestamp + │ │ ◀───┘ │ │ + ┌─────────────────┐ + │ │ ──┤GET /foo ├─▶ │ │ + │X-B3-TraceId: aa │ ────┐ + │ │ │X-B3-SpanId: 6b │ │ │ │ + └─────────────────┘ │ invoke + │ │ │ │ request │ + │ + │ │ │ │ │ + ┌────────┐ ◀───┘ + │ │ ◀─────┤200 OK ├─────── │ │ + ────┐ └────────┘ + │ │ │ record duration │ │ + ┌────────┐ ◀───┘ + │ ◀──┤200 OK ├── │ │ │ + └────────┘ ┌────────────────────────────────┐ + │ │ ──┤ asynchronously report span ├────▶ │ + │ │ + │{ │ + │ "traceId": "aa", │ + │ "id": "6b", │ + │ "name": "get", │ + │ "timestamp": 1483945573944000,│ + │ "duration": 386000, │ + │ "annotations": [ │ + │--snip-- │ + └────────────────────────────────┘ +``` + +Instrumented client 和 server 是分别使用了 ZipKin Client 的服务,Zipkin Client 会根据配置将追踪数据发送到 Zipkin Server 中进行数据存储、分析和展示。 + +## 四、Zipkin 客户端 + +[Brave](https://github.com/openzipkin/brave) 是 Java 版的 zipkin 客户端。 + +一般不会手动编写 Trace 相关的代码,Brave 提供可一些开箱即用的库,帮助我们追踪一些特定的请求。比如:dubbo、grpc、servlet、mysql、httpClient、kafka、springMVC 等。 + +## 参考资料 + +- [Zipkin 官网](https://zipkin.io/) +- [Zipkin Github](https://github.com/openzipkin/zipkin) +- [brave](https://github.com/openzipkin/brave) \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" new file mode 100644 index 00000000..e3573437 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" @@ -0,0 +1,68 @@ +--- +title: SkyWalking 快速入门 +date: 2020-02-07 23:04:47 +order: 04 +categories: + - Java + - 软件 + - 监控诊断 +tags: + - Java + - 监控 + - SkyWalking +permalink: /pages/df7dec/ +--- + +# SkyWalking 快速入门 + +SkyWalking 是一个基于 Java 开发的分布式系统的应用程序性能监视工具,专为微服务、云原生架构和基于容器(Docker、K8s、Mesos)架构而设计。 + +## 一、SkyWalking 简介 + +SkyWalking 是观察性分析平台和应用性能管理系统。 + +提供分布式追踪、服务网格遥测分析、度量聚合和可视化一体化解决方案。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211152235.png) + +### SkyWalking 特性 + +- 多种监控手段,语言探针和 service mesh +- 多语言自动探针,Java,.NET Core 和 Node.JS +- 轻量高效,不需要大数据 +- 模块化,UI、存储、集群管理多种机制可选 +- 支持告警 +- 优秀的可视化方案 + +### SkyWalking 核心概念 + +- **Service** - 服务。代表一组为传入请求提供相同的行为的工作负载。 使用代理或 SDK 时,可以定义服务名称。 +- **Service Instance** - 服务实例。服务组中的每个工作负载都称为一个实例。就像 Kubernetes 中的 Pod 一样,它在 OS 中不必是单个进程。 +- **Endpoint** - 端点。它是特定服务中用于传入请求的路径,例如 HTTP URI 路径或 RPC 服务类+方法签名。 + +## 二、SkyWalking 架构 + +从逻辑上讲,SkyWalking 分为四个部分:探针(Probes),平台后端,存储和 UI。 + +![SkyWalking 架构](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211153516.png) + +- **探针(Probes)** - 探针是指集成到目标系统中的代理或 SDK 库。它们负责收集数据(包括跟踪数据和统计数据)并将其按照 SkyWalking 的要求重新格式化为。 +- **平台后端** - 平台后端是一个提供后端服务的集群。它用于聚合、分析和驱动从探针到 UI 的流程。它还为传入格式(如 Zipkin 的格式),存储实现程序和集群管理提供可插入功能。 您甚至可以使用 Observability Analysis Language 自定义聚合和分析。 +- **存储** - 您可以选择一个 SkyWalking 已实现的存储,如由 Sharding-Sphere 管理的 ElasticSearch,H2 或 MySQL 集群,也可以自行实现一个存储。 +- **UI** - 用户界面很酷,对于 SkyWalking 最终用户而言非常强大。它也可以自定义以匹配您的自定义后端。 + +## 三、SkyWalking 安装 + +进入 [Apache SkyWalking 官方下载页面](http://skywalking.apache.org/downloads/),选择安装版本,下载解压到本地。 + +![SkyWalking 组件](https://raw.githubusercontent.com/dunwu/images/master/snap/20200211154612.png) + +安装分为三个部分: + +- [Backend setup document](https://github.com/apache/skywalking/blob/master/docs/en/setup/backend/backend-setup.md) +- [UI setup document](https://github.com/apache/skywalking/blob/master/docs/en/setup/backend/ui-setup.md) +- [CLI set up document](https://github.com/apache/skywalking-cli) + +## 参考资料 + +- [SkyWalking Github](https://github.com/apache/skywalking) \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" new file mode 100644 index 00000000..7e5726c9 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" @@ -0,0 +1,427 @@ +--- +title: Arthas 快速入门 +date: 2020-02-07 23:04:47 +order: 05 +categories: + - Java + - 软件 + - 监控诊断 +tags: + - Java + - 诊断 + - Arthas +permalink: /pages/c689d1/ +--- + +# Arthas 快速入门 + +> `Arthas` 是 Alibaba 开源的 Java 诊断工具 。 + +## 简介 + +`Arthas`可以解决的问题: + +1. 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception? +2. 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了? +3. 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗? +4. 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现! +5. 是否有一个全局视角来查看系统的运行状况? +6. 有什么办法可以监控到 JVM 的实时运行状态? + +`Arthas`支持 JDK 6+,支持 Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 `Tab` 自动补全功能,进一步方便进行问题的定位和诊断。 + +## 安装 + +### 使用`arthas-boot`(推荐) + +下载`arthas-boot.jar`,然后用`java -jar`的方式启动: + +``` +wget https://alibaba.github.io/arthas/arthas-boot.jar +java -jar arthas-boot.jar +``` + +打印帮助信息: + +``` +java -jar arthas-boot.jar -h +``` + +- 如果下载速度比较慢,可以使用 aliyun 的镜像: + + ``` + java -jar arthas-boot.jar --repo-mirror aliyun --use-http + ``` + +- 如果从 github 下载有问题,可以使用 gitee 镜像 + + ``` + wget https://arthas.gitee.io/arthas-boot.jar + ``` + +### 使用`as.sh` + +Arthas 支持在 Linux/Unix/Mac 等平台上一键安装,请复制以下内容,并粘贴到命令行中,敲 `回车` 执行即可: + +``` +curl -L https://alibaba.github.io/arthas/install.sh | sh +``` + +上述命令会下载启动脚本文件 `as.sh` 到当前目录,你可以放在任何地方或将其加入到 `$PATH` 中。 + +直接在 shell 下面执行`./as.sh`,就会进入交互界面。 + +也可以执行`./as.sh -h`来获取更多参数信息。 + +- 如果从 github 下载有问题,可以使用 gitee 镜像 + + ``` + curl -L https://arthas.gitee.io/install.sh | sh + ``` + +### 全量安装 + +最新版本,点击下载:[下载地址](http://repository.sonatype.org/service/local/artifact/maven/redirect?r=central-proxy&g=com.taobao.arthas&a=arthas-packaging&e=zip&c=bin&v=LATEST) + +解压后,在文件夹里有`arthas-boot.jar`,直接用`java -jar`的方式启动: + +``` +java -jar arthas-boot.jar +``` + +打印帮助信息: + +``` +java -jar arthas-boot.jar -h +``` + +## 基础使用 + +### 启动 Demo + +```bash +wget https://alibaba.github.io/arthas/arthas-demo.jar +java -jar arthas-demo.jar +``` + +`arthas-demo`是一个简单的程序,每隔一秒生成一个随机数,再执行质因式分解,并打印出分解结果。 + +`arthas-demo`源代码:[查看](https://github.com/alibaba/arthas/blob/master/demo/src/main/java/demo/MathGame.java) + +### 启动 arthas + +在命令行下面执行(使用和目标进程一致的用户启动,否则可能 attach 失败): + +``` +wget https://alibaba.github.io/arthas/arthas-boot.jar +java -jar arthas-boot.jar +``` + +- 执行该程序的用户需要和目标进程具有相同的权限。比如以`admin`用户来执行:`sudo su admin && java -jar arthas-boot.jar` 或 `sudo -u admin -EH java -jar arthas-boot.jar`。 +- 如果 attach 不上目标进程,可以查看`~/logs/arthas/` 目录下的日志。 +- 如果下载速度比较慢,可以使用 aliyun 的镜像:`java -jar arthas-boot.jar --repo-mirror aliyun --use-http` +- `java -jar arthas-boot.jar -h` 打印更多参数信息。 + +选择应用 java 进程: + +```bash +$ $ java -jar arthas-boot.jar +* [1]: 35542 + [2]: 71560 arthas-demo.jar +``` + +Demo 进程是第 2 个,则输入 2,再输入`回车/enter`。Arthas 会 attach 到目标进程上,并输出日志: + +```bash +[INFO] Try to attach process 71560 +[INFO] Attach process 71560 success. +[INFO] arthas-client connect 127.0.0.1 3658 + ,---. ,------. ,--------.,--. ,--. ,---. ,---. + / O \ | .--. ''--. .--'| '--' | / O \ ' .-' +| .-. || '--'.' | | | .--. || .-. |`. `-. +| | | || |\ \ | | | | | || | | |.-' | +`--' `--'`--' '--' `--' `--' `--'`--' `--'`-----' + +wiki: https://alibaba.github.io/arthas +version: 3.0.5.20181127201536 +pid: 71560 +time: 2018-11-28 19:16:24 + +$ +``` + +### 查看 dashboard + +输入[dashboard](https://alibaba.github.io/arthas/dashboard.html),按`回车/enter`,会展示当前进程的信息,按`ctrl+c`可以中断执行。 + +```bash +$ dashboard +ID NAME GROUP PRIORI STATE %CPU TIME INTERRU DAEMON +17 pool-2-thread-1 system 5 WAITIN 67 0:0 false false +27 Timer-for-arthas-dashb system 10 RUNNAB 32 0:0 false true +11 AsyncAppender-Worker-a system 9 WAITIN 0 0:0 false true +9 Attach Listener system 9 RUNNAB 0 0:0 false true +3 Finalizer system 8 WAITIN 0 0:0 false true +2 Reference Handler system 10 WAITIN 0 0:0 false true +4 Signal Dispatcher system 9 RUNNAB 0 0:0 false true +26 as-command-execute-dae system 10 TIMED_ 0 0:0 false true +13 job-timeout system 9 TIMED_ 0 0:0 false true +1 main main 5 TIMED_ 0 0:0 false false +14 nioEventLoopGroup-2-1 system 10 RUNNAB 0 0:0 false false +18 nioEventLoopGroup-2-2 system 10 RUNNAB 0 0:0 false false +23 nioEventLoopGroup-2-3 system 10 RUNNAB 0 0:0 false false +15 nioEventLoopGroup-3-1 system 10 RUNNAB 0 0:0 false false +Memory used total max usage GC +heap 32M 155M 1820M 1.77% gc.ps_scavenge.count 4 +ps_eden_space 14M 65M 672M 2.21% gc.ps_scavenge.time(m 166 +ps_survivor_space 4M 5M 5M s) +ps_old_gen 12M 85M 1365M 0.91% gc.ps_marksweep.count 0 +nonheap 20M 23M -1 gc.ps_marksweep.time( 0 +code_cache 3M 5M 240M 1.32% ms) +Runtime +os.name Mac OS X +os.version 10.13.4 +java.version 1.8.0_162 +java.home /Library/Java/JavaVir + tualMachines/jdk1.8.0 + _162.jdk/Contents/Hom + e/jre +``` + +### 通过 thread 命令来获取到`arthas-demo`进程的 Main Class + +`thread 1`会打印线程 ID 1 的栈,通常是 main 函数的线程。 + +```bash +$ thread 1 | grep 'main(' + at demo.MathGame.main(MathGame.java:17) +``` + +### 通过 jad 来反编译 Main Class + +```java +$ jad demo.MathGame + +ClassLoader: ++-sun.misc.Launcher$AppClassLoader@3d4eac69 + +-sun.misc.Launcher$ExtClassLoader@66350f69 + +Location: +/tmp/arthas-demo.jar + +/* + * Decompiled with CFR 0_132. + */ +package demo; + +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +public class MathGame { + private static Random random = new Random(); + private int illegalArgumentCount = 0; + + public static void main(String[] args) throws InterruptedException { + MathGame game = new MathGame(); + do { + game.run(); + TimeUnit.SECONDS.sleep(1L); + } while (true); + } + + public void run() throws InterruptedException { + try { + int number = random.nextInt(); + List primeFactors = this.primeFactors(number); + MathGame.print(number, primeFactors); + } + catch (Exception e) { + System.out.println(String.format("illegalArgumentCount:%3d, ", this.illegalArgumentCount) + e.getMessage()); + } + } + + public static void print(int number, List primeFactors) { + StringBuffer sb = new StringBuffer("" + number + "="); + Iterator iterator = primeFactors.iterator(); + while (iterator.hasNext()) { + int factor = iterator.next(); + sb.append(factor).append('*'); + } + if (sb.charAt(sb.length() - 1) == '*') { + sb.deleteCharAt(sb.length() - 1); + } + System.out.println(sb); + } + + public List primeFactors(int number) { + if (number < 2) { + ++this.illegalArgumentCount; + throw new IllegalArgumentException("number is: " + number + ", need >= 2"); + } + ArrayList result = new ArrayList(); + int i = 2; + while (i <= number) { + if (number % i == 0) { + result.add(i); + number /= i; + i = 2; + continue; + } + ++i; + } + return result; + } +} + +Affect(row-cnt:1) cost in 970 ms. +``` + +### watch + +通过[watch](https://alibaba.github.io/arthas/watch.html)命令来查看`demo.MathGame#primeFactors`函数的返回值: + +```bash +$ watch demo.MathGame primeFactors returnObj +Press Ctrl+C to abort. +Affect(class-cnt:1 , method-cnt:1) cost in 107 ms. +ts=2018-11-28 19:22:30; [cost=1.715367ms] result=null +ts=2018-11-28 19:22:31; [cost=0.185203ms] result=null +ts=2018-11-28 19:22:32; [cost=19.012416ms] result=@ArrayList[ + @Integer[5], + @Integer[47], + @Integer[2675531], +] +ts=2018-11-28 19:22:33; [cost=0.311395ms] result=@ArrayList[ + @Integer[2], + @Integer[5], + @Integer[317], + @Integer[503], + @Integer[887], +] +ts=2018-11-28 19:22:34; [cost=10.136007ms] result=@ArrayList[ + @Integer[2], + @Integer[2], + @Integer[3], + @Integer[3], + @Integer[31], + @Integer[717593], +] +ts=2018-11-28 19:22:35; [cost=29.969732ms] result=@ArrayList[ + @Integer[5], + @Integer[29], + @Integer[7651739], +] +``` + +更多的功能可以查看[进阶使用](https://alibaba.github.io/arthas/advanced-use.html)。 + +### 退出 arthas + +如果只是退出当前的连接,可以用`quit`或者`exit`命令。Attach 到目标进程上的 arthas 还会继续运行,端口会保持开放,下次连接时可以直接连接上。 + +如果想完全退出 arthas,可以执行`shutdown`命令。 + +## 进阶使用 + +### 基础命令 + +- help——查看命令帮助信息 +- [cat](https://alibaba.github.io/arthas/cat.html)——打印文件内容,和 linux 里的 cat 命令类似 +- [pwd](https://alibaba.github.io/arthas/pwd.html)——返回当前的工作目录,和 linux 命令类似 +- cls——清空当前屏幕区域 +- session——查看当前会话的信息 +- [reset](https://alibaba.github.io/arthas/reset.html)——重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类 +- version——输出当前目标 Java 进程所加载的 Arthas 版本号 +- history——打印命令历史 +- quit——退出当前 Arthas 客户端,其他 Arthas 客户端不受影响 +- shutdown——关闭 Arthas 服务端,所有 Arthas 客户端全部退出 +- [keymap](https://alibaba.github.io/arthas/keymap.html)——Arthas 快捷键列表及自定义快捷键 + +### jvm 相关 + +- [dashboard](https://alibaba.github.io/arthas/dashboard.html)——当前系统的实时数据面板 +- [thread](https://alibaba.github.io/arthas/thread.html)——查看当前 JVM 的线程堆栈信息 +- [jvm](https://alibaba.github.io/arthas/jvm.html)——查看当前 JVM 的信息 +- [sysprop](https://alibaba.github.io/arthas/sysprop.html)——查看和修改 JVM 的系统属性 +- [sysenv](https://alibaba.github.io/arthas/sysenv.html)——查看 JVM 的环境变量 +- [vmoption](https://alibaba.github.io/arthas/vmoption.html)——查看和修改 JVM 里诊断相关的 option +- [logger](https://alibaba.github.io/arthas/logger.html)——查看和修改 logger +- [getstatic](https://alibaba.github.io/arthas/getstatic.html)——查看类的静态属性 +- [ognl](https://alibaba.github.io/arthas/ognl.html)——执行 ognl 表达式 +- [mbean](https://alibaba.github.io/arthas/mbean.html)——查看 Mbean 的信息 +- [heapdump](https://alibaba.github.io/arthas/heapdump.html)——dump java heap, 类似 jmap 命令的 heap dump 功能 + +### class/classloader 相关 + +- [sc](https://alibaba.github.io/arthas/sc.html)——查看 JVM 已加载的类信息 +- [sm](https://alibaba.github.io/arthas/sm.html)——查看已加载类的方法信息 +- [jad](https://alibaba.github.io/arthas/jad.html)——反编译指定已加载类的源码 +- [mc](https://alibaba.github.io/arthas/mc.html)——内存编绎器,内存编绎`.java`文件为`.class`文件 +- [redefine](https://alibaba.github.io/arthas/redefine.html)——加载外部的`.class`文件,redefine 到 JVM 里 +- [dump](https://alibaba.github.io/arthas/dump.html)——dump 已加载类的 byte code 到特定目录 +- [classloader](https://alibaba.github.io/arthas/classloader.html)——查看 classloader 的继承树,urls,类加载信息,使用 classloader 去 getResource + +### monitor/watch/trace 相关 + +> 请注意,这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行 `shutdown` 或将增强过的类执行 `reset` 命令。 + +- [monitor](https://alibaba.github.io/arthas/monitor.html)——方法执行监控 +- [watch](https://alibaba.github.io/arthas/watch.html)——方法执行数据观测 +- [trace](https://alibaba.github.io/arthas/trace.html)——方法内部调用路径,并输出方法路径上的每个节点上耗时 +- [stack](https://alibaba.github.io/arthas/stack.html)——输出当前方法被调用的调用路径 +- [tt](https://alibaba.github.io/arthas/tt.html)——方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测 + +### options + +- [options](https://alibaba.github.io/arthas/options.html)——查看或设置 Arthas 全局开关 + +### 管道 + +Arthas 支持使用管道对上述命令的结果进行进一步的处理,如`sm java.lang.String * | grep 'index'` + +- grep——搜索满足条件的  结果 +- plaintext——将  命令的结果去除 ANSI 颜色 +- wc——按行统计输出结果 + +### 后台异步任务 + +当线上出现偶发的问题,比如需要 watch 某个条件,而这个条件一天可能才会出现一次时,异步后台任务就派上用场了,详情请参考[这里](https://alibaba.github.io/arthas/async.html) + +- 使用 > 将结果重写向到日志文件,使用 & 指定命令是后台运行,session 断开不影响任务执行(生命周期默认为 1 天) +- jobs——列出所有 job +- kill——强制终止任务 +- fg——将暂停的任务拉到前台执行 +- bg——将暂停的任务放到后台执行 + +### Web Console + +通过 websocket 连接 Arthas。 + +- [Web Console](https://alibaba.github.io/arthas/web-console.html) + +### 用户数据回报 + +在`3.1.4`版本后,增加了用户数据回报功能,方便统一做安全或者历史数据统计。 + +在启动时,指定`stat-url`,就会回报执行的每一行命令,比如: `./as.sh --stat-url 'http://192.168.10.11:8080/api/stat'` + +在 tunnel server 里有一个示例的回报代码,用户可以自己在服务器上实现。 + +[StatController.java](https://github.com/alibaba/arthas/blob/master/tunnel-server/src/main/java/com/alibaba/arthas/tunnel/server/app/web/StatController.java) + +### 其他特性 + +- [异步命令支持](https://alibaba.github.io/arthas/async.html) +- [执行结果存日志](https://alibaba.github.io/arthas/save-log.html) +- [批处理的支持](https://alibaba.github.io/arthas/batch-support.html) +- [ognl 表达式的用法说明](https://github.com/alibaba/arthas/issues/11) + +## 参考资料 + +- [Arthas Github](https://github.com/alibaba/arthas) +- [Arthas 用户文档](https://alibaba.github.io/arthas/index.html) +- [arthas 源码分析](https://www.jianshu.com/p/4e34d0ab47d1) \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" new file mode 100644 index 00000000..a2caaea0 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" @@ -0,0 +1,33 @@ +--- +title: Java 监控诊断 +date: 2020-02-11 17:48:32 +categories: + - Java + - 软件 + - 监控诊断 +tags: + - Java + - 监控 + - 诊断 +permalink: /pages/3d16d3/ +hidden: true +index: false +--- + +# Java 监控诊断 + +## 内容 + +- [监控工具对比](01.监控工具对比.md) +- [CAT](02.CAT.md) +- [Zipkin](03.Zipkin.md) +- [SkyWalking](04.Skywalking.md) +- [Arthas](05.Arthas.md) + +## 资料 + +- [CAT Github](https://github.com/dianping/cat) +- [Zipkin Github](https://github.com/openzipkin/zipkin) +- [SkyWalking Github](https://github.com/apache/skywalking) +- [PinPoint Github](https://github.com/naver/pinpoint) +- [Arthas Github](https://github.com/alibaba/arthas) \ No newline at end of file diff --git "a/docs/01.Java/11.\350\275\257\344\273\266/README.md" "b/docs/01.Java/11.\350\275\257\344\273\266/README.md" new file mode 100644 index 00000000..d44fbe38 --- /dev/null +++ "b/docs/01.Java/11.\350\275\257\344\273\266/README.md" @@ -0,0 +1,70 @@ +--- +title: Java 软件 +date: 2022-02-18 08:53:11 +categories: + - Java + - 软件 +tags: + - Java +permalink: /pages/2cb045/ +hidden: true +index: false +--- + +# Java 软件 + +> 本部分内容主要是 Java 开发领域使用的一些 Java 软件,如构建工具、IDE、服务器、日志中心等等。 + +## 📖 内容 + +### 构建 + +> Java 项目需要通过 [**构建工具**](01.构建) 来管理项目依赖,完成编译、打包、发布、生成 JavaDoc 等任务。 +> +> - 目前最主流的构建工具是 Maven,它的功能非常强大。 +> - Gradle 号称是要替代 Maven 等构件工具,它的版本管理确实简洁,但是需要学习 Groovy,学习成本比 Maven 高。 +> - Ant 功能比 Maven 和 Gradle 要弱,现代 Java 项目基本不用了,但也有一些传统的 Java 项目还在使用。 + +- [Maven](01.构建/01.Maven) 📚 + - [Maven 快速入门](01.构建/01.Maven/01.Maven快速入门.md) + - [Maven 教程之 pom.xml 详解](01.构建/01.Maven/02.Maven教程之pom.xml详解.md) + - [Maven 教程之 settings.xml 详解](01.构建/01.Maven/03.Maven教程之settings.xml详解.md) + - [Maven 实战问题和最佳实践](01.构建/01.Maven/04.Maven实战问题和最佳实践.md) + - [Maven 教程之发布 jar 到私服或中央仓库](01.构建/01.Maven/05.Maven教程之发布jar到私服或中央仓库.md) + - [Maven 插件之代码检查](01.构建/01.Maven/06.Maven插件之代码检查.md) +- [Ant 简易教程](01.构建/02.Ant.md) + +### IDE + +> 自从有了 [**IDE**](02.IDE),写代码从此就告别了刀耕火种的蛮荒时代。 +> +> - [Eclipse](02.IDE/02.Eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 +> - 曾经抗拒从转 [Intellij Idea](02.IDE/01.Intellij.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 +> - 你可以在 [vscode](02.IDE/03.VsCode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 + +- [Intellij Idea](02.IDE/01.Intellij.md) +- [Eclipse](02.IDE/02.Eclipse.md) +- [vscode](02.IDE/03.VsCode.md) + +### 监控诊断 + +> [监控/诊断](03.监控诊断) 工具主要用于 Java 应用的运维。通过采集、分析、存储、可视化应用的有效数据,帮助开发者、使用者快速定位问题,找到性能瓶颈。 + +- [监控工具对比](03.监控诊断/01.监控工具对比.md) +- [CAT](03.监控诊断/02.CAT.md) +- [Zipkin](03.监控诊断/03.Zipkin.md) +- [SkyWalking](03.监控诊断/04.Skywalking.md) +- [Arthas](03.监控诊断/05.Arthas.md) + +## 📚 资料 + +- **官网** + - [Maven Github](https://github.com/apache/maven) + - [Maven 官方文档](https://maven.apache.org/ref/current) + - [Ant 官方手册](http://ant.apache.org/manual/index.html) +- **书籍** + - [《Maven 实战》](https://book.douban.com/subject/5345682/) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" new file mode 100644 index 00000000..2e3cfea8 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" @@ -0,0 +1,485 @@ +--- +title: Java 和 JSON 序列化 +date: 2022-02-17 22:34:30 +order: 01 +categories: + - Java + - 工具 + - IO +tags: + - Java + - IO + - 序列化 + - JSON +permalink: /pages/4622a6/ +--- + +# Java 和 JSON 序列化 + +> JSON(JavaScript Object Notation)是一种基于文本的数据交换格式。几乎所有的编程语言都有很好的库或第三方工具来提供基于 JSON 的 API 支持,因此你可以非常方便地使用任何自己喜欢的编程语言来处理 JSON 数据。 +> +> 本文主要从 Java 语言的角度来讲解 JSON 的应用。 + +## JSON 简介 + +### JSON 是什么 + +JSON 起源于 1999 年的 [JS 语言规范 ECMA262 的一个子集](http://javascript.crockford.com/)(即 15.12 章节描述了格式与解析),后来 2003 年作为一个数据格式[ECMA404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf)(很囧的序号有不有?)发布。 +2006 年,作为 [rfc4627](http://www.ietf.org/rfc/rfc4627.txt) 发布,这时规范增加到 18 页,去掉没用的部分,十页不到。 + +JSON 的应用很广泛,这里有超过 100 种语言下的 JSON 库:[json.org](http://www.json.org/)。 + +更多的可以参考这里,[关于 json 的一切](https://github.com/burningtree/awesome-json)。 + +### JSON 标准 + +这估计是最简单标准规范之一: + +- 只有两种结构:对象内的键值对集合结构和数组,对象用 `{}` 表示、内部是 `"key":"value"`,数组用 `[]` 表示,不同值用逗号分开 +- 基本数值有 7 个: `false` / `null` / `true` / `object` / `array` / `number` / `string` +- 再加上结构可以嵌套,进而可以用来表达复杂的数据 +- 一个简单实例: + +```json +{ + "Image": { + "Width": 800, + "Height": 600, + "Title": "View from 15th Floor", + "Thumbnail": { + "Url": "http://www.example.com/image/481989943", + "Height": 125, + "Width": "100" + }, + "IDs": [116, 943, 234, 38793] + } +} +``` + +> 扩展阅读: +> +> - - 图文并茂介绍 json 数据形式 +> +> - [json 的 RFC 文档](http://tools.ietf.org/html/rfc4627) + +### JSON 优缺点 + +优点: + +- 基于纯文本,所以对于人类阅读是很友好的。 +- 规范简单,所以容易处理,开箱即用,特别是 JS 类的 ECMA 脚本里是内建支持的,可以直接作为对象使用。 +- 平台无关性,因为类型和结构都是平台无关的,而且好处理,容易实现不同语言的处理类库,可以作为多个不同异构系统之间的数据传输格式协议,特别是在 HTTP/REST 下的数据格式。 + +缺点: + +- 性能一般,文本表示的数据一般来说比二进制大得多,在数据传输上和解析处理上都要更影响性能。 +- 缺乏 schema,跟同是文本数据格式的 XML 比,在类型的严格性和丰富性上要差很多。XML 可以借由 XSD 或 DTD 来定义复杂的格式,并由此来验证 XML 文档是否符合格式要求,甚至进一步的,可以基于 XSD 来生成具体语言的操作代码,例如 apache xmlbeans。并且这些工具组合到一起,形成一套庞大的生态,例如基于 XML 可以实现 SOAP 和 WSDL,一系列的 ws-\*规范。但是我们也可以看到 JSON 在缺乏规范的情况下,实际上有更大一些的灵活性,特别是近年来 REST 的快速发展,已经有一些 schema 相关的发展(例如[理解 JSON Schema](https://spacetelescope.github.io/understanding-json-schema/index.html),[使用 JSON Schema](http://usingjsonschema.com/downloads/), [在线 schema 测试](http://azimi.me/json-schema-view/demo/demo.html)),也有类似于 WSDL 的[WADL](https://www.w3.org/Submission/wadl/)出现。 + +### JSON 工具 + +- 使用 JSON 实现 RPC(类似 XML-RPC):[JSON-RPC](http://www.jsonrpc.org/) +- 使用 JSON 实现 path 查询操作(类似 XML-PATH):[JsonPATH](https://github.com/json-path/JsonPath) +- 在线查询工具:[JsonPATH](http://jsonpath.com/) + +- 格式化工具:[jsbeautifier](http://jsbeautifier.org/) +- chrome 插件:[5 个 Json View 插件](http://www.cnplugins.com/zhuanti/five-chrome-json-plugins.html) +- 在线 Mock: [在线 mock](https://www.easy-mock.com/) +- 其他 Mock:[SoapUI](https://www.soapui.org/rest-testing-mocking/rest-service-mocking.html)可以支持,SwaggerUI 也可以,[RestMock](https://github.com/andrzejchm/RESTMock)也可以。 + +### Java JSON 库 + +Java 中比较流行的 JSON 库有: + +- [Fastjson](https://github.com/alibaba/fastjson) - 阿里巴巴开发的 JSON 库,性能十分优秀。 +- [Jackson](http://wiki.fasterxml.com/JacksonHome) - 社区十分活跃且更新速度很快。Spring 框架默认 JSON 库。 +- [Gson](https://github.com/google/gson) - 谷歌开发的 JSON 库,目前功能最全的 JSON 库 。 + +从性能上来看,一般情况下:Fastjson > Jackson > Gson + +### JSON 编码指南 + +> 遵循好的设计与编码风格,能提前解决 80%的问题,个人推荐 Google JSON 风格指南。 +> +> - 英文版[Google JSON Style Guide](https://google.github.io/styleguide/jsoncstyleguide.xml): +> - 中文版[Google JSON 风格指南](https://github.com/darcyliu/google-styleguide/blob/master/JSONStyleGuide.md): + +简单摘录如下: + +- 属性名和值都是用双引号,不要把注释写到对象里面,对象数据要简洁 +- 不要随意结构化分组对象,推荐是用扁平化方式,层次不要太复杂 +- 命名方式要有意义,比如单复数表示 +- 驼峰式命名,遵循 Bean 规范 +- 使用版本来控制变更冲突 +- 对于一些关键字,不要拿来做 key +- 如果一个属性是可选的或者包含空值或 null 值,考虑从 JSON 中去掉该属性,除非它的存在有很强的语义原因 +- 序列化枚举类型时,使用 name 而不是 value +- 日期要用标准格式处理 +- 设计好通用的分页参数 +- 设计好异常处理 + +[JSON API](http://jsonapi.org.cn/format/)与 Google JSON 风格指南有很多可以相互参照之处。 + +[JSON API](http://jsonapi.org.cn/format/)是数据交互规范,用以定义客户端如何获取与修改资源,以及服务器如何响应对应请求。 + +JSON API 设计用来最小化请求的数量,以及客户端与服务器间传输的数据量。在高效实现的同时,无需牺牲可读性、灵活性和可发现性。 + +## Fastjson 应用 + +### 添加 maven 依赖 + +```xml + + com.alibaba + fastjson + x.x.x + +``` + +### Fastjson API + +#### 定义 Bean + +**Group.java** + +```java +public class Group { + + private Long id; + private String name; + private List users = new ArrayList(); +} +``` + +**User.java** + +```java +public class User { + + private Long id; + private String name; +} +``` + +**初始化 Bean** + +```java +Group group = new Group(); +group.setId(0L); +group.setName("admin"); + +User guestUser = new User(); +guestUser.setId(2L); +guestUser.setName("guest"); + +User rootUser = new User(); +rootUser.setId(3L); +rootUser.setName("root"); + +group.addUser(guestUser); +group.addUser(rootUser); +``` + +#### 序列化 + +```java +String jsonString = JSON.toJSONString(group); +System.out.println(jsonString); +``` + +#### 反序列化 + +```java +Group bean = JSON.parseObject(jsonString, Group.class); +``` + +### Fastjson 注解 + +#### `@JSONField` + +> 扩展阅读:更多 API 使用细节可以参考:[JSONField 用法](https://github.com/alibaba/fastjson/wiki/JSONField),这里介绍基本用法。 + +可以配置在属性(setter、getter)和字段(必须是 public field)上。 + +```java +@JSONField(name="ID") +public int getId() {return id;} + +// 配置date序列化和反序列使用yyyyMMdd日期格式 +@JSONField(format="yyyyMMdd") +public Date date1; + +// 不序列化 +@JSONField(serialize=false) +public Date date2; + +// 不反序列化 +@JSONField(deserialize=false) +public Date date3; + +// 按ordinal排序 +@JSONField(ordinal = 2) +private int f1; + +@JSONField(ordinal = 1) +private int f2; +``` + +#### `@JSONType` + +- 自定义序列化:[ObjectSerializer](https://github.com/alibaba/fastjson/wiki/JSONType_serializer) +- 子类型处理:[SeeAlso](https://github.com/alibaba/fastjson/wiki/JSONType_seeAlso_cn) + +JSONType.alphabetic 属性: fastjson 缺省时会使用字母序序列化,如果你是希望按照 java fields/getters 的自然顺序序列化,可以配置 JSONType.alphabetic,使用方法如下: + +```java +@JSONType(alphabetic = false) +public static class B { + public int f2; + public int f1; + public int f0; +} +``` + +## Jackson 应用 + +> 扩展阅读:更多 API 使用细节可以参考 [jackson-databind 官方说明](https://github.com/FasterXML/jackson-databind) + +### 添加 maven 依赖 + +```xml + + com.fasterxml.jackson.core + jackson-databind + 2.9.8 + +``` + +### Jackson API + +#### 序列化 + +```java +ObjectMapper mapper = new ObjectMapper(); + +mapper.writeValue(new File("result.json"), myResultObject); +// or: +byte[] jsonBytes = mapper.writeValueAsBytes(myResultObject); +// or: +String jsonString = mapper.writeValueAsString(myResultObject); +``` + +#### 反序列化 + +```java +ObjectMapper mapper = new ObjectMapper(); + +MyValue value = mapper.readValue(new File("data.json"), MyValue.class); +// or: +value = mapper.readValue(new URL("http://some.com/api/entry.json"), MyValue.class); +// or: +value = mapper.readValue("{\"name\":\"Bob\", \"age\":13}", MyValue.class); +``` + +#### 容器的序列化和反序列化 + +```java +Person p = new Person("Tom", 20); +Person p2 = new Person("Jack", 22); +Person p3 = new Person("Mary", 18); + +List persons = new LinkedList<>(); +persons.add(p); +persons.add(p2); +persons.add(p3); + +Map map = new HashMap<>(); +map.put("persons", persons); + +String json = null; +try { + json = mapper.writeValueAsString(map); +} catch (JsonProcessingException e) { + e.printStackTrace(); +} +``` + +### Jackson 注解 + +> 扩展阅读:更多注解使用细节可以参考 [jackson-annotations 官方说明](https://github.com/FasterXML/jackson-annotations/wiki/Jackson-Annotations) + +#### `@JsonProperty` + +```java +public class MyBean { + private String _name; + + // without annotation, we'd get "theName", but we want "name": + @JsonProperty("name") + public String getTheName() { return _name; } + + // note: it is enough to add annotation on just getter OR setter; + // so we can omit it here + public void setTheName(String n) { _name = n; } +} +``` + +#### `@JsonIgnoreProperties` 和 `@JsonIgnore` + +```java +// means that if we see "foo" or "bar" in JSON, they will be quietly skipped +// regardless of whether POJO has such properties +@JsonIgnoreProperties({ "foo", "bar" }) +public class MyBean { + // will not be written as JSON; nor assigned from JSON: + @JsonIgnore + public String internal; + + // no annotation, public field is read/written normally + public String external; + + @JsonIgnore + public void setCode(int c) { _code = c; } + + // note: will also be ignored because setter has annotation! + public int getCode() { return _code; } +} +``` + +#### `@JsonCreator` + +```java +public class CtorBean { + public final String name; + public final int age; + + @JsonCreator // constructor can be public, private, whatever + private CtorBean(@JsonProperty("name") String name, + @JsonProperty("age") int age) + { + this.name = name; + this.age = age; + } +} +``` + +#### `@JsonPropertyOrder` + +alphabetic 设为 true 表示,json 字段按自然顺序排列,默认为 false。 + +```java +@JsonPropertyOrder(alphabetic = true) +public class JacksonAnnotationBean {} +``` + +## Gson 应用 + +> 详细内容可以参考官方文档:[Gson 用户指南](https://github.com/google/gson/blob/master/UserGuide.md) + +### 添加 maven 依赖 + +```xml + + com.google.code.gson + gson + 2.8.6 + +``` + +### Gson API + +#### 序列化 + +```java +Gson gson = new Gson(); +gson.toJson(1); // ==> 1 +gson.toJson("abcd"); // ==> "abcd" +gson.toJson(10L); // ==> 10 +int[] values = { 1 }; +gson.toJson(values); // ==> [1] +``` + +#### 反序列化 + +```java +int i1 = gson.fromJson("1", int.class); +Integer i2 = gson.fromJson("1", Integer.class); +Long l1 = gson.fromJson("1", Long.class); +Boolean b1 = gson.fromJson("false", Boolean.class); +String str = gson.fromJson("\"abc\"", String.class); +String[] anotherStr = gson.fromJson("[\"abc\"]", String[].class); +``` + +#### GsonBuilder + +`Gson` 实例可以通过 `GsonBuilder` 来定制实例化,以控制其序列化、反序列化行为。 + +```java +Gson gson = new GsonBuilder() + .setPrettyPrinting() + .setDateFormat("yyyy-MM-dd HH:mm:ss") + .excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT, Modifier.VOLATILE) + .create(); +``` + +### Gson 注解 + +#### `@Since` + +[`@Since`](https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/annotations/Since.java) 用于控制对象的序列化版本。示例: + +```java +public class VersionedClass { + @Since(1.1) private final String newerField; + @Since(1.0) private final String newField; + private final String field; + + public VersionedClass() { + this.newerField = "newer"; + this.newField = "new"; + this.field = "old"; + } +} + +VersionedClass versionedObject = new VersionedClass(); +Gson gson = new GsonBuilder().setVersion(1.0).create(); +String jsonOutput = gson.toJson(versionedObject); +System.out.println(jsonOutput); +System.out.println(); + +gson = new Gson(); +jsonOutput = gson.toJson(versionedObject); +System.out.println(jsonOutput); +``` + +#### `@SerializedName` + +`@SerializedName` 用于将类成员按照指定名称序列化、反序列化。示例: + +```java +private class SomeObject { + @SerializedName("custom_naming") private final String someField; + private final String someOtherField; + + public SomeObject(String a, String b) { + this.someField = a; + this.someOtherField = b; + } +} +``` + +## 示例源码 + +> 示例源码:[javalib-io-json](https://github.com/dunwu/java-tutorial/tree/master/javalib-io-json) + +## 参考资料 + +- **官方** + - [Fastjson Github](https://github.com/alibaba/fastjson) + - [Gson Github](https://github.com/google/gson) + - [jackson 官方文档](https://github.com/FasterXML/jackson-docs) + - [jackson-databind](https://github.com/FasterXML/jackson-databind) +- **文章** + - + - [json 的 RFC 文档](http://tools.ietf.org/html/rfc4627) + - [JSON 最佳实践](https://kimmking.github.io/2017/06/06/json-best-practice/) + - [【简明教程】JSON](https://www.jianshu.com/p/8b428e1d1564) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" new file mode 100644 index 00000000..631a5fcb --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" @@ -0,0 +1,391 @@ +--- +title: Java 二进制序列化 +date: 2022-02-17 22:34:30 +order: 02 +categories: + - Java + - 工具 + - IO +tags: + - Java + - IO + - 序列化 + - 二进制 +permalink: /pages/08d872/ +--- + +# Java 二进制序列化 + +## 简介 + +### 为什么需要二进制序列化库 + +原因很简单,就是 Java 默认的序列化机制(`ObjectInputStream` 和 `ObjectOutputStream`)具有很多缺点。 + +> 不了解 Java 默认的序列化机制,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/2b2f0f/) + +Java 自身的序列化方式具有以下缺点: + +- **无法跨语言使用**。这点最为致命,对于很多需要跨语言通信的异构系统来说,不能跨语言序列化,即意味着完全无法通信(彼此数据不能识别,当然无法交互了)。 +- **序列化的性能不高**。序列化后的数据体积较大,这大大影响存储和传输的效率。 +- 序列化一定需要实现 `Serializable` 接口。 +- 需要关注 `serialVersionUID`。 + +引入二进制序列化库就是为了解决这些问题,这在 RPC 应用中尤为常见。 + +### 主流序列化库简介 + +#### Protobuf + +[Protobuf](https://developers.google.com/protocol-buffers/) 是 Google 公司内部的混合语言数据标准,是一种轻便、高效的结构化数据存储 +格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。Protobuf +使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL +编译器,生成序列化工具类。 + +优点: + +- 序列化后体积相比 JSON、Hessian 小很多 +- 序列化反序列化速度很快,不需要通过反射获取类型 +- 语言和平台无关(基于 IDL),IDL 能清晰地描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器 +- 消息格式升级和兼容性不错,可以做到后向兼容 +- 支持 Java, C++, Python 三种语言 + +缺点: + +- Protobuf 对于具有反射和动态能力的语言来说,用起来很费劲。 + +#### Thrift + +> [Thrift](https://github.com/apache/thrift) 是 apache 开源项目,是一个点对点的 RPC 实现。 + +它具有以下特性: + +- 支持多种语言(目前支持 28 种语言,如:C++、go、Java、Php、Python、Ruby 等等)。 +- 使用了组建大型数据交换及存储工具,对于大型系统中的内部数据传输,相对于 Json 和 xml 在性能上和传输大小上都有明显的优势。 +- 支持三种比较典型的编码方式(通用二进制编码,压缩二进制编码,优化的可选字段压缩编解码)。 + +#### Hessian + +[Hessian](http://hessian.caucho.com/) 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。Hessian 协 +议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节 +数也更小。 + +RPC 框架 Dubbo 就支持 Thrift 和 Hession。 + +它具有以下特性: + +- 支持多种语言。如:Java、Python、C++、C#、PHP、Ruby 等。 +- 相对其他二进制序列化库较慢。 + +Hessian 本身也有问题,官方版本对 Java 里面一些常见对象的类型不支持: + +- Linked 系列,LinkedHashMap、LinkedHashSet 等,但是可以通过扩展 CollectionDeserializer 类修复; +- Locale 类,可以通过扩展 ContextSerializerFactory 类修复; +- Byte/Short 反序列化的时候变成 Integer。 + +#### Kryo + +> [Kryo](https://github.com/EsotericSoftware/kryo) 是用于 Java 的快速高效的二进制对象图序列化框架。Kryo 还可以执行自动的深拷贝和浅拷贝。 这是从对象到对象的直接复制,而不是从对象到字节的复制。 + +它具有以下特性: + +- 速度快,序列化体积小 +- 官方不支持 Java 以外的其他语言 + +#### FST + +> [FST](https://github.com/RuedigerMoeller/fast-serialization) 是一个 Java 实现二进制序列化库。 + +它具有以下特性: + +- 近乎于 100% 兼容 JDK 序列化,且比 JDK 原序列化方式快 10 倍 +- 2.17 开始与 Android 兼容 +- (可选)2.29 开始支持将任何可序列化的对象图编码/解码为 JSON(包括共享引用) + +#### 小结 + +了解了以上这些常见的二进制序列化库的特性。在技术选型时,我们就可以做到有的放矢。 + +**(1)选型参考依据** + +对于二进制序列化库,我们的选型考量一般有以下几点: + +- **是否支持跨语言** + - 根据业务实际需求来决定。一般来说,支持跨语言,为了兼容,使用复杂度上一般会更高一些。 +- **序列化、反序列化的性能** +- **类库是否轻量化,API 是否简单易懂** + +**(2)选型建议** + +- 如果需要跨语言通信,那么可以考虑:Protobuf、Thrift、Hession。 + + - [thrift](https://github.com/apache/thrift)、[protobuf](https://github.com/protocolbuffers/protobuf) - 适用于对性能敏感,对开发体验要求不高的内部系统。 + - [hessian](http://hessian.caucho.com/doc/hessian-overview.xtp) - 适用于对开发体验敏感,性能有要求的内外部系统。 + +- 如果不需要跨语言通信,可以考虑:[Kryo](https://github.com/EsotericSoftware/kryo) 和 [FST](https://github.com/RuedigerMoeller/fast-serialization),性能不错,且 API 十分简单。 + +## FST 应用 + +### 引入依赖 + +```xml + + de.ruedigermoeller + fst + 2.56 + +``` + +### FST API + +示例: + +```java +import org.nustaq.serialization.FSTConfiguration; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class FstDemo { + + private static FSTConfiguration DEFAULT_CONFIG = FSTConfiguration.createDefaultConfiguration(); + + /** + * 将对象序列化为 byte 数组 + * + * @param obj 任意对象 + * @param 对象的类型 + * @return 序列化后的 byte 数组 + */ + public static byte[] writeToBytes(T obj) { + return DEFAULT_CONFIG.asByteArray(obj); + } + + /** + * 将对象序列化为 byte 数组后,再使用 Base64 编码 + * + * @param obj 任意对象 + * @param 对象的类型 + * @return 序列化后的字符串 + */ + public static String writeToString(T obj) { + byte[] bytes = writeToBytes(obj); + return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8); + } + + /** + * 将 byte 数组反序列化为原对象 + * + * @param bytes {@link #writeToBytes} 方法序列化后的 byte 数组 + * @param clazz 原对象的类型 + * @param 原对象的类型 + * @return 原对象 + */ + public static T readFromBytes(byte[] bytes, Class clazz) throws IOException { + Object obj = DEFAULT_CONFIG.asObject(bytes); + if (clazz.isInstance(obj)) { + return (T) obj; + } else { + throw new IOException("derialize failed"); + } + } + + /** + * 将字符串反序列化为原对象,先使用 Base64 解码 + * + * @param str {@link #writeToString} 方法序列化后的字符串 + * @param clazz 原对象的类型 + * @param 原对象的类型 + * @return 原对象 + */ + public static T readFromString(String str, Class clazz) throws IOException { + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + return readFromBytes(Base64.getDecoder().decode(bytes), clazz); + } + +} +``` + +测试: + +```java +long begin = System.currentTimeMillis(); +for (int i = 0; i < BATCH_SIZE; i++) { + TestBean oldBean = BeanUtils.initJdk8Bean(); + byte[] bytes = FstDemo.writeToBytes(oldBean); + TestBean newBean = FstDemo.readFromBytes(bytes, TestBean.class); +} +long end = System.currentTimeMillis(); +System.out.printf("FST 序列化/反序列化耗时:%s", (end - begin)); +``` + +## Kryo 应用 + +### 引入依赖 + +```xml + + com.esotericsoftware + kryo + 5.0.0-RC4 + +``` + +### Kryo API + +示例: + +```java +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; +import com.esotericsoftware.kryo.util.DefaultInstantiatorStrategy; +import org.objenesis.strategy.StdInstantiatorStrategy; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class KryoDemo { + + // 每个线程的 Kryo 实例 + private static final ThreadLocal kryoLocal = ThreadLocal.withInitial(() -> { + Kryo kryo = new Kryo(); + + /** + * 不要轻易改变这里的配置!更改之后,序列化的格式就会发生变化, + * 上线的同时就必须清除 Redis 里的所有缓存, + * 否则那些缓存再回来反序列化的时候,就会报错 + */ + //支持对象循环引用(否则会栈溢出) + kryo.setReferences(true); //默认值就是 true,添加此行的目的是为了提醒维护者,不要改变这个配置 + + //不强制要求注册类(注册行为无法保证多个 JVM 内同一个类的注册编号相同;而且业务系统中大量的 Class 也难以一一注册) + kryo.setRegistrationRequired(false); //默认值就是 false,添加此行的目的是为了提醒维护者,不要改变这个配置 + + //Fix the NPE bug when deserializing Collections. + ((DefaultInstantiatorStrategy) kryo.getInstantiatorStrategy()) + .setFallbackInstantiatorStrategy(new StdInstantiatorStrategy()); + + return kryo; + }); + + /** + * 获得当前线程的 Kryo 实例 + * + * @return 当前线程的 Kryo 实例 + */ + public static Kryo getInstance() { + return kryoLocal.get(); + } + + /** + * 将对象序列化为 byte 数组 + * + * @param obj 任意对象 + * @param 对象的类型 + * @return 序列化后的 byte 数组 + */ + public static byte[] writeToBytes(T obj) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + Output output = new Output(byteArrayOutputStream); + + Kryo kryo = getInstance(); + kryo.writeObject(output, obj); + output.flush(); + + return byteArrayOutputStream.toByteArray(); + } + + /** + * 将对象序列化为 byte 数组后,再使用 Base64 编码 + * + * @param obj 任意对象 + * @param 对象的类型 + * @return 序列化后的字符串 + */ + public static String writeToString(T obj) { + byte[] bytes = writeToBytes(obj); + return new String(Base64.getEncoder().encode(bytes), StandardCharsets.UTF_8); + } + + /** + * 将 byte 数组反序列化为原对象 + * + * @param bytes {@link #writeToBytes} 方法序列化后的 byte 数组 + * @param clazz 原对象的类型 + * @param 原对象的类型 + * @return 原对象 + */ + @SuppressWarnings("unchecked") + public static T readFromBytes(byte[] bytes, Class clazz) { + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); + Input input = new Input(byteArrayInputStream); + + Kryo kryo = getInstance(); + return (T) kryo.readObject(input, clazz); + } + + /** + * 将字符串反序列化为原对象,先使用 Base64 解码 + * + * @param str {@link #writeToString} 方法序列化后的字符串 + * @param clazz 原对象的类型 + * @param 原对象的类型 + * @return 原对象 + */ + public static T readFromString(String str, Class clazz) { + byte[] bytes = str.getBytes(StandardCharsets.UTF_8); + return readFromBytes(Base64.getDecoder().decode(bytes), clazz); + } + +} +``` + +测试: + +```java +long begin = System.currentTimeMillis(); +for (int i = 0; i < BATCH_SIZE; i++) { + TestBean oldBean = BeanUtils.initJdk8Bean(); + byte[] bytes = KryoDemo.writeToBytes(oldBean); + TestBean newBean = KryoDemo.readFromBytes(bytes, TestBean.class); +} +long end = System.currentTimeMillis(); +System.out.printf("Kryo 序列化/反序列化耗时:%s", (end - begin)); +``` + +Hessian 应用 + +```java +Student student = new Student(); +student.setNo(101); +student.setName("HESSIAN"); +//把student对象转化为byte数组 +ByteArrayOutputStream bos = new ByteArrayOutputStream(); +Hessian2Output output = new Hessian2Output(bos); +output.writeObject(student); +output.flushBuffer(); +byte[] data = bos.toByteArray(); +bos.close(); +//把刚才序列化出来的byte数组转化为student对象 +ByteArrayInputStream bis = new ByteArrayInputStream(data); +Hessian2Input input = new Hessian2Input(bis); +Student deStudent = (Student) input.readObject(); +input.close(); +System.out.println(deStudent); +``` + +## 参考资料 + +- **官方** + - [Protobuf 官网](https://developers.google.com/protocol-buffers/) + - [Protobuf Github](https://github.com/protocolbuffers/protobuf) + - [Thrift Github](https://github.com/apache/thrift) + - [Kryo Github](https://github.com/EsotericSoftware/kryo) + - [Hessian 官网](http://hessian.caucho.com/) + - [FST Github](https://github.com/RuedigerMoeller/fast-serialization) +- **文章** + - [java 序列化框架对比](https://www.jianshu.com/p/937883b6b2e5) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/01.IO/README.md" "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/README.md" new file mode 100644 index 00000000..1cd5ba95 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/01.IO/README.md" @@ -0,0 +1,39 @@ +--- +title: Java 序列化工具 +date: 2022-02-17 22:34:30 +categories: + - Java + - 工具 + - IO +tags: + - Java + - IO + - 序列化 +permalink: /pages/08b504/ +hidden: true +index: false +--- + +# Java 序列化工具 + +Java 官方的序列化存在许多问题,因此,很多人更愿意使用优秀的第三方序列化工具来替代 Java 自身的序列化机制。 如果想详细了解 Java 自身序列化方式,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/2b2f0f/) + +序列化库技术选型: + +- [thrift](https://github.com/apache/thrift)、[protobuf](https://github.com/protocolbuffers/protobuf) - 适用于对性能敏感,对开发体验要求不高的内部系统。 +- [hessian](http://hessian.caucho.com/doc/hessian-overview.xtp) - 适用于对开发体验敏感,性能有要求的内外部系统。 +- [jackson](https://github.com/FasterXML/jackson)、[gson](https://github.com/google/gson)、[fastjson](https://github.com/alibaba/fastjson) - 适用于对序列化后的数据要求有良好的可读性(转为 json 、xml 形式)。 + +## 内容 + +- [JSON](01.JSON序列化.md) - Fastjson、Jackson、Gson +- [二进制](02.二进制序列化.md) - Protobuf、Thrift、Hessian、Kryo、FST + +## 资料 + +- [Thrift Github](https://github.com/apache/thrift) +- [Protobuf Github](https://github.com/protocolbuffers/protobuf) +- [Hessian 官网](http://hessian.caucho.com/doc/hessian-overview.xtp) +- [Fastjson Github](https://github.com/alibaba/fastjson) +- [Jackson Github](https://github.com/FasterXML/jackson) +- [Gson Github](https://github.com/google/gson) \ No newline at end of file diff --git a/docs/javalib/lombok.md "b/docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" similarity index 51% rename from docs/javalib/lombok.md rename to "docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" index 70c147c3..39b1df3a 100644 --- a/docs/javalib/lombok.md +++ "b/docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" @@ -1,67 +1,63 @@ --- -title: Lombok 使用小结 -date: 2017/11/09 +title: Lombok 快速入门 +date: 2022-02-17 22:34:30 +order: 01 categories: -- javalib + - Java + - 工具 + - JavaBean tags: -- java -- javalib -- bean + - Java + - JavaBean + - Lombok +permalink: /pages/eb1d46/ --- -# Lombok 使用小结 +# Lombok 快速入门 ## Lombok 简介 -Lombok 是一种 Java 实用工具,可用来帮助开发人员消除 Java 的冗长,尤其是对于简单的 Java 对象(POJO)。它通过注释实现这一目的。通过在开发环境中实现 Lombok,开发人员可以节省构建诸如 `hashCode()` 和 `equals()` 、`getter / setter` 这样的方法以及以往用来分类各种 accessor 和 mutator 的大量时间。 +Lombok 是一种 Java 实用工具,可用来帮助开发人员消除 Java 的冗长,尤其是对于简单的 Java 对象(POJO)。它通过注释实现这一目的。通过在开发环境中实现 Lombok,开发人员可以节省构建诸如 `hashCode()` 和 `equals()` 、`getter / setter` 这样的方法以及以往用来分类各种 accessor 和 mutator 的大量时间。 ## Lombok 安装 -使 IntelliJ IDEA 支持 Lombok 方式如下: - -1. **Intellij 设置支持注解处理** - - 点击 File > Settings > Build > Annotation Processors - - 勾选 Enable annotation processing - +由于 Lombok 仅在编译阶段生成代码,所以使用 Lombok 注解的源代码,在 IDE 中会被高亮显示错误,针对这个问题可以通过安装 IDE 对应的插件来解决。具体的安装方式可以参考:[Setting up Lombok with Eclipse and Intellij](https://www.baeldung.com/lombok-ide) -2. **安装插件** - - 点击 Settings > Plugins > Browse repositories - - 查找 Lombok Plugin 并进行安装 - - 重启 IntelliJ IDEA +使 IntelliJ IDEA 支持 Lombok 方式如下: -3. **将 lombok 添加到 pom 文件** +- **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 - - ``` +```xml + + org.projectlombok + lombok + 1.16.8 + +``` ## Lombok 使用 -### API - -Lombok 提供注解API 来修饰指定的类: +Lombok 提供注解 API 来修饰指定的类: -#### @Getter and @Setter +### @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; @@ -77,18 +73,19 @@ protected void setName(final String name) { this.name = name; } ``` -#### @NonNull + +### @NonNull [@NonNull](http://jnb.ociweb.com/jnb/jnbJan2010.html#nonnull) Lombok 代码: -``` +```java @Getter @Setter @NonNull private List members; ``` 等价于 Java 源码: -``` +```java @NonNull private List members; @@ -96,7 +93,7 @@ public Family(@NonNull final List members) { if (members == null) throw new java.lang.NullPointerException("members"); this.members = members; } - + @NonNull public List getMembers() { return members; @@ -107,11 +104,12 @@ public void setMembers(@NonNull final List members) { this.members = members; } ``` -#### @ToString + +### @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; @@ -122,12 +120,12 @@ public class Foo extends Bar { 等价于 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() + @@ -136,18 +134,19 @@ public class Foo extends Bar { } } ``` -#### @EqualsAndHashCode + +### @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; @@ -158,9 +157,9 @@ public class Person extends SentientBeing { 等价于 Java 源码: -``` +```java public class Person extends SentientBeing { - + enum Gender { /*public static final*/ Male /* = new Gender() */, /*public static final*/ Female /* = new Gender() */; @@ -174,7 +173,7 @@ public class Person extends SentientBeing { 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; @@ -187,7 +186,7 @@ public class Person extends SentientBeing { 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; @@ -200,11 +199,12 @@ public class Person extends SentientBeing { } } ``` -#### @Data + +### @Data [@Data](http://jnb.ociweb.com/jnb/jnbJan2010.html#data) Lombok 代码: -``` +```java @Data(staticConstructor="of") public class Company { private final Person founder; @@ -215,40 +215,40 @@ public class Company { 等价于 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; @@ -260,7 +260,7 @@ public class Company { 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; @@ -270,18 +270,19 @@ public class Company { result = result * PRIME + (this.employees == null ? 0 : this.employees.hashCode()); return result; } - + @java.lang.Override public java.lang.String toString() { return "Company(founder=" + founder + ", name=" + name + ", employees=" + employees + ")"; } } ``` -#### @Cleanup + +### @Cleanup [@Cleanup](http://jnb.ociweb.com/jnb/jnbJan2010.html#cleanup) Lombok 代码: -``` +```java public void testCleanUp() { try { @Cleanup ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -295,7 +296,7 @@ public void testCleanUp() { 等价于 Java 源码: -``` +```java public void testCleanUp() { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -310,11 +311,12 @@ public void testCleanUp() { } } ``` -#### @Synchronized + +### @Synchronized [@Synchronized](http://jnb.ociweb.com/jnb/jnbJan2010.html#synchronized) Lombok 代码: -``` +```java private DateFormat format = new SimpleDateFormat("MM-dd-YYYY"); @Synchronized @@ -325,7 +327,7 @@ public String synchronizedFormat(Date date) { 等价于 Java 源码: -``` +```java private final java.lang.Object $lock = new java.lang.Object[0]; private DateFormat format = new SimpleDateFormat("MM-dd-YYYY"); @@ -335,11 +337,12 @@ public String synchronizedFormat(Date date) { } } ``` -#### @SneakyThrows + +### @SneakyThrows [@SneakyThrows](http://jnb.ociweb.com/jnb/jnbJan2010.html#sneaky) Lombok 代码: -``` +```java @SneakyThrows public void testSneakyThrows() { throw new IllegalAccessException(); @@ -348,7 +351,7 @@ public void testSneakyThrows() { 等价于 Java 源码: -``` +```java public void testSneakyThrows() { try { throw new IllegalAccessException(); @@ -357,36 +360,177 @@ public void testSneakyThrows() { } } ``` -### 示例 -使用 Lombok 定义一个 Java Bean +### 示例源码 + +> 示例源码:[javalib-bean](https://github.com/dunwu/java-tutorial/tree/master/javalib-bean) + +## Lombok 使用注意点 + +### 谨慎使用 `@Builder` + +在类上标注了 `@Data` 和 `@Builder` 注解的时候,编译时,lombok 优化后的 Class 中会没有默认的构造方法。在反序列化的时候,没有默认构造方法就可能会报错。 + +【示例】使用 `@Builder` 不当导致 json 反序列化失败 ```java -import lombok.Data; -import lombok.ToString; +@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 -@ToString(exclude = "age") -public class Person { +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class BuilderDemo02 { + private String name; - private Integer age; - private String sex; + + public static void main(String[] args) throws JsonProcessingException { + BuilderDemo02 demo02 = BuilderDemo02.builder().name("demo01").build(); + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(demo02); + BuilderDemo02 expectDemo02 = mapper.readValue(json, BuilderDemo02.class); + System.out.println(expectDemo02.toString()); + } + } ``` -测试 +### `@Data` 注解和继承 + +使用 `@Data` 注解时,则有了 `@EqualsAndHashCode` 注解,那么就会在此类中存在 `equals(Object other)` 和 `hashCode()` 方法,且不会使用父类的属性,这就导致了可能的问题。比如,有多个类有相同的部分属性,把它们定义到父类中,恰好 id(数据库主键)也在父类中,那么就会存在部分对象在比较时,它们并不相等,这是因为:lombok 自动生成的 `equals(Object other)` 和 `hashCode()` 方法判定为相等,从而导致和预期不符。 + +修复此问题的方法很简单: + +- 使用 `@Data` 时,加上 `@EqualsAndHashCode(callSuper=true)` 注解。 +- 使用 `@Getter @Setter @ToString` 代替 `@Data` 并且自定义 `equals(Object other)` 和 `hashCode()` 方法。 + +【示例】测试 `@Data` 和 `@EqualsAndHashCode` ```java -Person person = new Person(); -person.setName("张三"); -person.setAge(20); -person.setSex("男"); -System.out.println(person.toString()); -// output: Person(name=张三, sex=男) +@Data +@ToString(exclude = "age") +@EqualsAndHashCode(exclude = { "age", "sex" }) +public class Person { + + protected String name; + + protected Integer age; + + protected String sex; + +} + +@Data +@EqualsAndHashCode(callSuper = true, exclude = { "address", "city", "state", "zip" }) +public class EqualsAndHashCodeDemo extends Person { + + @NonNull + private String name; + + @NonNull + private Gender gender; + + private String ssn; + + private String address; + + private String city; + + private String state; + + private String zip; + + public EqualsAndHashCodeDemo(@NonNull String name, @NonNull Gender gender) { + this.name = name; + this.gender = gender; + } + + public EqualsAndHashCodeDemo(@NonNull String name, @NonNull Gender gender, + String ssn, String address, String city, String state, String zip) { + this.name = name; + this.gender = gender; + this.ssn = ssn; + this.address = address; + this.city = city; + this.state = state; + this.zip = zip; + } + + public enum Gender { + Male, + Female + } + +} + +@Test +@DisplayName("测试 @EqualsAndHashCode") +public void testEqualsAndHashCodeDemo() { + EqualsAndHashCodeDemo demo1 = + new EqualsAndHashCodeDemo("name1", EqualsAndHashCodeDemo.Gender.Female, "ssn", "xxx", "xxx", "xxx", "xxx"); + EqualsAndHashCodeDemo demo2 = + new EqualsAndHashCodeDemo("name1", EqualsAndHashCodeDemo.Gender.Female, "ssn", "ooo", "ooo", "ooo", "ooo"); + Assertions.assertEquals(demo1, demo2); + + Person person = new Person(); + person.setName("张三"); + person.setAge(20); + person.setSex("男"); + + Person person2 = new Person(); + person2.setName("张三"); + person2.setAge(18); + person2.setSex("男"); + + Person person3 = new Person(); + person3.setName("李四"); + person3.setAge(20); + person3.setSex("男"); + + Assertions.assertEquals(person2, person); + Assertions.assertNotEquals(person3, person); +} ``` -## 资料 +上面的单元测试可以通过,但如果将 `@EqualsAndHashCode(callSuper = true, exclude = { "address", "city", "state", "zip" })` 注掉就会报错。 -[Lombok 官网](https://projectlombok.org/) | [Lombok Github](https://github.com/rzwitserloot/lombok) +## 参考资料 -[IntelliJ IDEA - Lombok Plugin](http://plugins.jetbrains.com/plugin/6317-lombok-plugin) +- [Lombok 官网](https://projectlombok.org/) +- [Lombok Github](https://github.com/rzwitserloot/lombok) +- [IntelliJ IDEA - Lombok Plugin](http://plugins.jetbrains.com/plugin/6317-lombok-plugin) \ No newline at end of file diff --git a/docs/javalib/dozer.md "b/docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" similarity index 69% rename from docs/javalib/dozer.md rename to "docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" index b1c7d8b8..e9a7c230 100644 --- a/docs/javalib/dozer.md +++ "b/docs/01.Java/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" @@ -1,25 +1,29 @@ --- -title: Dozer 使用小结 -date: 2016/10/12 +title: Dozer 快速入门 +date: 2022-02-17 22:34:30 +order: 02 categories: -- javalib + - Java + - 工具 + - JavaBean tags: -- java -- javalib -- bean + - Java + - JavaBean + - Dozer +permalink: /pages/45e21b/ --- -# Dozer 使用小结 +# Dozer 快速入门 -这篇文章是本人在阅读Dozer官方文档(5.5.1版本,官网已经一年多没更新了)的过程中,整理下来我认为比较基础的应用场景。 +这篇文章是本人在阅读 Dozer 官方文档(5.5.1 版本,官网已经一年多没更新了)的过程中,整理下来我认为比较基础的应用场景。 -本文中提到的例子应该能覆盖JavaBean映射的大部分场景,希望对你有所帮助。 +本文中提到的例子应该能覆盖 JavaBean 映射的大部分场景,希望对你有所帮助。 -## 概述 +## 简介 -**Dozer是什么?** +**Dozer 是什么?** -**Dozer是一个JavaBean映射工具库。** +**Dozer 是一个 JavaBean 映射工具库。** 它支持简单的属性映射,复杂类型映射,双向映射,隐式显式的映射,以及递归映射。 @@ -29,11 +33,11 @@ tags: ## 安装 -### 引入jar包 +### 引入 jar 包 -**maven方式** +**maven 方式** -如果你的项目使用maven,添加以下依赖到你的pom.xml即可: +如果你的项目使用 maven,添加以下依赖到你的 pom.xml 即可: ```xml @@ -43,26 +47,26 @@ tags: ``` -**非maven方式** +**非 maven 方式** -如果你的项目不使用maven,那就只能发扬不怕苦不怕累的精神了。 +如果你的项目不使用 maven,那就只能发扬不怕苦不怕累的精神了。 -使用Dozer需要引入Dozer的jar包以及其依赖的第三方jar包。 +使用 Dozer 需要引入 Dozer 的 jar 包以及其依赖的第三方 jar 包。 - [Dozer](http://sourceforge.net/project/showfiles.php?group_id=133517) -- [Dozer依赖的第三方jar包](http://dozer.sourceforge.net/dependencies.html) +- [Dozer 依赖的第三方 jar 包](http://dozer.sourceforge.net/dependencies.html) -### Eclipse插件 +### Eclipse 插件 -Dozer有插件可以在Eclipse中使用(不知道是否好用,反正我没用过) +Dozer 有插件可以在 Eclipse 中使用(不知道是否好用,反正我没用过) -插件地址: http://dozer.sourceforge.net/eclipse-plugin +插件地址: ## 使用 -将Dozer引入到工程中后,我们就可以来小试一番了。 +将 Dozer 引入到工程中后,我们就可以来小试一番了。 -实践出真知,先以一个最简单的例子来展示Dozer映射的处理过程。 +实践出真知,先以一个最简单的例子来展示 Dozer 映射的处理过程。 ### 准备 @@ -91,33 +95,35 @@ public class NotSameAttributeB { // 省略getter/setter } ``` + 这两个类存在属性名不完全相同的情况:name 和 value。 -### Dozer的配置 +### Dozer 的配置 #### 为什么要有映射配置? 如果要映射的两个对象有完全相同的属性名,那么一切都很简单。 -只需要直接使用Dozer的API即可: +只需要直接使用 Dozer 的 API 即可: ```java Mapper mapper = new DozerBeanMapper(); -DestinationObject destObject = +DestinationObject destObject = mapper.map(sourceObject, DestinationObject.class); ``` 但实际映射时,往往存在属性名不同的情况。 -所以,你需要一些配置来告诉Dozer应该转换什么,怎么转换。 +所以,你需要一些配置来告诉 Dozer 应该转换什么,怎么转换。 -***注:官网着重建议:在现实应用中,最好不要每次映射对象时都创建一个`Mapper`实例来工作,这样会产生不必要的开销。如果你不使用IoC容器(如:spring)来管理你的项目,那么,最好将`Mapper`定义为单例模式。*** +**_注:官网着重建议:在现实应用中,最好不要每次映射对象时都创建一个`Mapper`实例来工作,这样会产生不必要的开销。如果你不使用 IoC 容器(如:spring)来管理你的项目,那么,最好将`Mapper`定义为单例模式。_** #### 映射配置文件 在`src/test/resources`目录下添加`dozer/dozer-mapping.xml`文件。 ``标签中允许你定义``和``,对应着相互映射的类。 ``标签里定义要映射的特殊属性。需要注意``和``对应,``和``对应,聪明的你,猜也猜出来了吧。 + ```xml ``` -### 与Spring整合 +### 与 Spring 整合 #### 配置 DozerBeanMapperFactoryBean 在`src/test/resources`目录下添加`spring/spring-dozer.xml`文件。 -Dozer与Spring的整合很便利,你只需要声明一个`DozerBeanMapperFactoryBean`, -将所有的dozer映射配置文件作为属性注入到`mappingFiles`, +Dozer 与 Spring 的整合很便利,你只需要声明一个`DozerBeanMapperFactoryBean`, +将所有的 dozer 映射配置文件作为属性注入到`mappingFiles`, `DozerBeanMapperFactoryBean`会加载这些规则。 -spring-dozer.xml文件范例 +spring-dozer.xml 文件范例 ```xml @@ -191,13 +197,13 @@ public class DozerTest extends TestCase { 运行一下单元测试,绿灯通过。 -## Dozer支持的数据类型转换 +## Dozer 支持的数据类型转换 -Dozer可以自动做数据类型转换。当前,Dozer支持以下数据类型转换(都是双向的) +Dozer 可以自动做数据类型转换。当前,Dozer 支持以下数据类型转换(都是双向的) - **Primitive to Primitive Wrapper** - 原型(int、long等)和原型包装类(Integer、Long) + 原型(int、long 等)和原型包装类(Integer、Long) - **Primitive to Custom Wrapper** @@ -229,7 +235,7 @@ Dozer可以自动做数据类型转换。当前,Dozer支持以下数据类型 - **String to Map** - 字符串和Map + 字符串和 Map - **Collection to Collection** @@ -241,11 +247,11 @@ Dozer可以自动做数据类型转换。当前,Dozer支持以下数据类型 - **Map to Complex Type** - Map和复杂类型 + Map 和复杂类型 - **Map to Custom Map Type** - Map和定制Map类型 + Map 和定制 Map 类型 - **Enum to Enum** @@ -257,21 +263,21 @@ Dozer可以自动做数据类型转换。当前,Dozer支持以下数据类型 - **String to any of the supported Date/Calendar Objects.** - 字符串和支持Date/Calendar的对象 + 字符串和支持 Date/Calendar 的对象 -- **Objects containing a toString() method that produces a long representing time in (ms) to any supported Date/Calendar object. ** +- **Objects containing a toString() method that produces a long representing time in (ms) to any supported Date/Calendar object.** - 如果一个对象的toString()方法返回的是一个代表long型的时间数值(单位:ms),就可以和任何支持Date/Calendar的对象转换。 + 如果一个对象的 toString()方法返回的是一个代表 long 型的时间数值(单位:ms),就可以和任何支持 Date/Calendar 的对象转换。 -## Dozer的映射配置 +## Dozer 的映射配置 -在前面的简单例子中,我们体验了一把Dozer的映射流程。但是两个类进行映射,有很多复杂的情况,相应的,你也需要一些更复杂的配置。 +在前面的简单例子中,我们体验了一把 Dozer 的映射流程。但是两个类进行映射,有很多复杂的情况,相应的,你也需要一些更复杂的配置。 -Dozer有三种映射配置方式: +Dozer 有三种映射配置方式: - **注解方式** -- **API方式** -- **XML方式** +- **API 方式** +- **XML 方式** ### 用注解来配置映射 @@ -325,36 +331,36 @@ public class TargetBean { } ``` -定义了两个相互映射的Java类,只需要在源类中用`@Mapping`标记和目标类中对应的属性就可以了。 +定义了两个相互映射的 Java 类,只需要在源类中用`@Mapping`标记和目标类中对应的属性就可以了。 ```java @Test public void testAnnotationMapping() { - SourceBean src = new SourceBean(); - src.setId(7L); - src.setName("邦德"); - src.setData("00000111"); + SourceBean src = new SourceBean(); + src.setId(7L); + src.setName("邦德"); + src.setData("00000111"); - TargetBean desc = mapper.map(src, TargetBean.class); - Assert.assertNotNull(desc); + TargetBean desc = mapper.map(src, TargetBean.class); + Assert.assertNotNull(desc); } ``` 测试一下,绿灯通过。 -官方文档说,虽然当前版本(文档的版本对应Dozer 5.5.1)仅支持`@Mapping`,但是在未来的发布版本会提供其他的注解功能,那就敬请期待吧(再次吐槽一下:一年多没更新了)。 +官方文档说,虽然当前版本(文档的版本对应 Dozer 5.5.1)仅支持`@Mapping`,但是在未来的发布版本会提供其他的注解功能,那就敬请期待吧(再次吐槽一下:一年多没更新了)。 -### 用API来配置映射 +### 用 API 来配置映射 个人觉得这种方式比较麻烦,不推荐,也不想多做介绍,就是这么任性。 -### 用XML来配置映射 +### 用 XML 来配置映射 -需要**强调**的是:如果两个类的所有属性都能很好的互转,可以你中有我,我中有你,不分彼此,那么就不要画蛇添足的在xml中去声明映射规则了。 +需要**强调**的是:如果两个类的所有属性都能很好的互转,可以你中有我,我中有你,不分彼此,那么就不要画蛇添足的在 xml 中去声明映射规则了。 #### 属性名不同时的映射(Basic Property Mapping) -**Dozer会自动映射属性名相同的属性,所以不必添加在xml文件中。** +**Dozer 会自动映射属性名相同的属性,所以不必添加在 xml 文件中。** ```xml @@ -418,7 +424,7 @@ public void testAnnotationMapping() { #### 集合和数组映射(Collection and Array Mapping) -Dozer可以自动处理以下类型的双向转换。 +Dozer 可以自动处理以下类型的双向转换。 - List to List - List to Array @@ -427,27 +433,27 @@ Dozer可以自动处理以下类型的双向转换。 - Set to Array - Set to List -**使用hint** +**使用 hint** -如果使用泛型或数组,没有必要使用hint。 +如果使用泛型或数组,没有必要使用 hint。 如果不使用泛型或数组。在处理集合或数组之间的转换时,你需要用`hint`指定目标列表的数据类型。 -若你不指定`hint`,Dozer将认为目标集合和源集合的类型是一致的。 +若你不指定`hint`,Dozer 将认为目标集合和源集合的类型是一致的。 -使用Hints的范例: +使用 Hints 的范例: ```xml - hintList - hintList - org.dozer.vo.TheFirstSubClassPrime - + hintList + hintList + org.dozer.vo.TheFirstSubClassPrime + ``` **累计映射和非累计映射(Cumulative vs. Non-Cumulative List Mapping)** -如果你要转换的目标类已经初始化,你可以选择让Dozer添加或更新对象到你的集合中。 +如果你要转换的目标类已经初始化,你可以选择让 Dozer 添加或更新对象到你的集合中。 而这取决于`relationship-type`配置,默认是累计。 @@ -468,8 +474,8 @@ Dozer可以自动处理以下类型的双向转换。 ```xml - - + + ``` @@ -478,9 +484,9 @@ Dozer可以自动处理以下类型的双向转换。 ```xml hintList - hintList - org.dozer.vo.TheFirstSubClass - org.dozer.vo.TheFirstSubClassPrime + hintList + org.dozer.vo.TheFirstSubClass + org.dozer.vo.TheFirstSubClassPrime ``` @@ -492,9 +498,9 @@ Dozer可以自动处理以下类型的双向转换。 ```xml - srcList - destList - + srcList + destList + ``` #### 深度映射(Deep Mapping) @@ -505,7 +511,7 @@ Source.java ```java public class Source { - private long id; + private long id; private String info; } ``` @@ -514,14 +520,14 @@ Dest.java ```java public class Dest { - private long id; + private long id; private Info info; } ``` ```java public class Info { - private String content; + private String content; } ``` @@ -542,22 +548,22 @@ public class Info { 就像任何团体都有捣乱分子,类之间转换时也有想要排除的因子。 -如何在做类型转换时,自动排除一些属性,Dozer提供了几种方法,这里只介绍一种比较通用的方法。 +如何在做类型转换时,自动排除一些属性,Dozer 提供了几种方法,这里只介绍一种比较通用的方法。 更多详情参考[官网](http://dozer.sourceforge.net/documentation/exclude.html)。 -field-exclude可以排除不需要映射的属性。 +field-exclude 可以排除不需要映射的属性。 ```xml - - fieldToExclude - fieldToExclude + + fieldToExclude + fieldToExclude ``` #### 单向映射(One-Way Mapping) -***注:本文的映射方式,无特殊说明,都是双向映射的。*** +**_注:本文的映射方式,无特殊说明,都是双向映射的。_** 有的场景可能希望转换过程不可逆,即单向转换。 @@ -566,22 +572,22 @@ field-exclude可以排除不需要映射的属性。 类级别 ```xml - + org.dozer.vo.TestObjectFoo - org.dozer.vo.TestObjectFooPrime + org.dozer.vo.TestObjectFooPrime oneFoo oneFooPrime - + ``` 属性级别 ```xml - + org.dozer.vo.TestObjectFoo2 - org.dozer.vo.TestObjectFooPrime2 + org.dozer.vo.TestObjectFooPrime2 oneFoo2 oneFooPrime2 @@ -606,51 +612,51 @@ field-exclude可以排除不需要映射的属性。 ```xml - + MM/dd/yyyy HH:mm true true false - + org.dozer.vo.TestCustomConverterObject another.type.to.Associate - - + + ``` -全局配置的作用是帮助你少配置一些参数,如果个别类的映射规则需要变更,你可以mapping中覆盖它。 +全局配置的作用是帮助你少配置一些参数,如果个别类的映射规则需要变更,你可以 mapping 中覆盖它。 覆盖的范例如下 ```xml - + - + - + - + - + ``` #### 定制转换(Custom Converters) -如果Dozer默认的转换规则不能满足实际需要,你可以选择定制转换。 +如果 Dozer 默认的转换规则不能满足实际需要,你可以选择定制转换。 -定制转换通过配置XML来告诉Dozer如何去转换两个指定的类。当Dozer转换这两个指定类的时候,会调用你的映射规则去替换标准映射规则。 +定制转换通过配置 XML 来告诉 Dozer 如何去转换两个指定的类。当 Dozer 转换这两个指定类的时候,会调用你的映射规则去替换标准映射规则。 -为了让Dozer识别,你必须实现`org.dozer.CustomConverter`接口。否则,Dozer会抛异常。 +为了让 Dozer 识别,你必须实现`org.dozer.CustomConverter`接口。否则,Dozer 会抛异常。 具体做法: @@ -658,8 +664,8 @@ field-exclude可以排除不需要映射的属性。 ```java public class TestCustomConverter implements CustomConverter { - - public Object convert(Object destination, Object source, + + public Object convert(Object destination, Object source, Class destClass, Class sourceClass) { if (source == null) { return null; @@ -675,7 +681,7 @@ public class TestCustomConverter implements CustomConverter { dest.setTheDouble(((Double) source).doubleValue()); return dest; } else if (source instanceof CustomDoubleObject) { - double sourceObj = + double sourceObj = ((CustomDoubleObject) source).getTheDouble(); return new Double(sourceObj); } else { @@ -683,10 +689,10 @@ public class TestCustomConverter implements CustomConverter { + "used incorrectly. Arguments passed in were:" + destination + " and " + source); } - } + } ``` -(2) 在xml中引用定制的映射规则 +(2) 在 xml 中引用定制的映射规则 引用定制的映射规则也是分级的,你可以酌情使用。 @@ -706,14 +712,14 @@ public class TestCustomConverter implements CustomConverter { java.lang.Double - - org.dozer.vo.TestCustomConverterHashMapObject org.dozer.vo.TestCustomConverterHashMapPrimeObject - +
``` @@ -723,20 +729,20 @@ public class TestCustomConverter implements CustomConverter { ```xml org.dozer.vo.SimpleObj - org.dozer.vo.SimpleObjPrime2 + org.dozer.vo.SimpleObjPrime2 field1 field1Prime - + ``` #### 映射的继承(Inheritance Mapping) -Dozer支持映射规则的继承机制。 +Dozer 支持映射规则的继承机制。 -属性如果有着相同的名字则不需要在xml中配置,除非使用了`hint` +属性如果有着相同的名字则不需要在 xml 中配置,除非使用了`hint` 我们来看一个例子 @@ -744,27 +750,27 @@ Dozer支持映射规则的继承机制。 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 @@ -772,12 +778,12 @@ Dozer支持映射规则的继承机制。 ``` -在上面的例子中SubClass、SubClass2是SuperClass的子类; +在上面的例子中 SubClass、SubClass2 是 SuperClass 的子类; -SubClassPrime和SubClassPrime2是SuperClassPrime的子类。 +SubClassPrime 和 SubClassPrime2 是 SuperClassPrime 的子类。 -superAttribute和superAttr的映射规则会被子类所继承,所以不必再重复的在子类中去声明。 +superAttribute 和 superAttr 的映射规则会被子类所继承,所以不必再重复的在子类中去声明。 ## 参考 -[Dozer官方文档](http://dozer.sourceforge.net/documentation/gettingstarted.html) | [Dozer源码地址](https://github.com/DozerMapper/dozer) +[Dozer 官方文档](http://dozer.sourceforge.net/documentation/gettingstarted.html) | [Dozer 源码地址](https://github.com/DozerMapper/dozer) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" new file mode 100644 index 00000000..8de94dbc --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" @@ -0,0 +1,175 @@ +--- +title: Freemark 快速入门 +date: 2022-02-17 22:34:30 +order: 01 +categories: + - Java + - 工具 + - 模板引擎 +tags: + - Java + - 模板引擎 + - Freemark +permalink: /pages/a60ccf/ +--- + +# Freemark 快速入门 + +> FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML 网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个 Java 类库,是一款程序员可以嵌入他们所开发产品的组件。 + +## Freemark 简介 + +Freemark 模板编写为 FreeMarker Template Language (FTL)。它是简单的,专用的语言, _不是_ 像 PHP 那样成熟的编程语言。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。 + +![img](http://freemarker.foofun.cn/figures/overview.png) + +这种方式通常被称为 [MVC (模型 视图 控制器) 模式](http://freemarker.foofun.cn/gloss.html#gloss.MVC),对于动态网页来说,是一种特别流行的模式。 它帮助从开发人员(Java 程序员)中分离出网页设计师(HTML 设计师)。设计师无需面对模板中的复杂逻辑, 在没有程序员来修改或重新编译代码时,也可以修改页面的样式。 + +Freemark 模板一句话概括就是:**_`模板 + 数据模型 = 输出`_** + +## 总体结构 + +- **文本**:文本会照着原样来输出。 +- **插值**:这部分的输出会被计算的值来替换。插值由 `${` and `}` 所分隔(或者 `#{` and `}`,这种风格已经不建议再使用了;[点击查看更多](http://freemarker.foofun.cn/ref_depr_numerical_interpolation.html))。 +- **FTL 标签**:FTL 标签和 HTML 标签很相似,但是它们却是给 FreeMarker 的指示, 而且不会打印在输出内容中。 +- **注释**:注释和 HTML 的注释也很相似,但它们是由 `<#--` 和 `-->`来分隔的。注释会被 FreeMarker 直接忽略, 更不会在输出内容中显示。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/ftl-template.png) + +> 🔔 注意: +> +> - FTL 是区分大小写的。 +> - `插值` 仅仅可以在 `文本` 中使用。 +> - `FTL 标签` 不可以在其他 `FTL 标签` 和 `插值` 中使用。 +> - `注释` 可以放在 `FTL 标签` 和 `插值` 中。 + +### 指令 + +FTL 指令有两种类型: [预定义指令](http://freemarker.foofun.cn/gloss.html#gloss.predefinedDirective) 和 [用户自定义指令](http://freemarker.foofun.cn/gloss.html#gloss.userDefinedDirective)。 对于用户自定义的指令使用 `@` 来代替 `#`。 + +> 🔔 注意: +> +> - FreeMarker 仅仅关心 FTL 标签的嵌套而不关心 HTML 标签的嵌套。 它只会把 HTML 看做是文本,不会来解释 HTML。 +> - 如果你尝试使用一个不存在的指令(比如,输错了指令的名称), FreeMarker 就会拒绝执行模板,同时抛出错误信息。 +> - FreeMarker 会忽略 FTL 标签中多余的 [空白标记](http://freemarker.foofun.cn/gloss.html#gloss.whiteSpace)。 + +### 表达式 + +以下为快速浏览清单,如果需要了解更多细节,请参考[**这里**](http://freemarker.foofun.cn/dgui_template_exp.html)。 + +- [直接指定值](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_direct) + - [字符串](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_direct_string): `"Foo"` 或者 `'Foo'` 或者 `"It's \"quoted\""` 或者 `'It\'s "quoted"'` 或者 `r"C:\raw\string"` + - [数字](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_direct_number): `123.45` + - [布尔值](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_direct_boolean): `true`, `false` + - [序列](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_direct_seuqence): `["foo", "bar", 123.45]`; 值域: `0..9`, `0..<10` (或 `0..!10`), `0..` + - [哈希表](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_direct_hash): `{"name":"green mouse", "price":150}` +- [检索变量](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_var) + - [顶层变量](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_var_toplevel): `user` + - [从哈希表中检索数据](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_var_hash): `user.name`, `user["name"]` + - [从序列中检索数据](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_var_sequence): `products[5]` + - [特殊变量](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_var_special): `.main` +- [字符串操作](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_stringop) + - [插值(或连接)](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_stringop_interpolation): `"Hello ${user}!"` (或 `"Hello " + user + "!"`) + - [获取一个字符](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_get_character): `name[0]` + - [字符串切分:](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_stringop_slice) 包含结尾: `name[0..4]`,不包含结尾: `name[0..<5]`,基于长度(宽容处理): `name[0..*5]`,去除开头:`name[5..]` +- [序列操作](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_sequenceop) + - [连接](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_sequenceop_cat): `users + ["guest"]` + - [序列切分](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_seqenceop_slice):包含结尾: `products[20..29]`, 不包含结尾: `products[20..<30]`,基于长度(宽容处理):`products[20..*10]`,去除开头: `products[20..]` +- [哈希表操作](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_hashop) + - [连接](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_hashop_cat): `passwords + { "joe": "secret42" }` +- [算术运算](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_arit): `(x * 1.5 + 10) / 2 - y % 100` +- [比较运算](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_comparison): `x == y`, `x != y`, `x < y`, `x > y`, `x >= y`, `x <= y`, `x lt y`, `x lte y`, `x gt y`, `x gte y`, 等等。。。。。。 +- [逻辑操作](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_logicalop): `!registered && (firstVisit || fromEurope)` +- [内建函数](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_builtin): `name?upper_case`, `path?ensure_starts_with('/')` +- [方法调用](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_methodcall): `repeat("What", 3)` +- [处理不存在的值](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_missing) + - [默认值](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_missing_default): `name!"unknown"` 或者 `(user.name)!"unknown"` 或者 `name!` 或者 `(user.name)!` + - [检测不存在的值](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_missing_test): `name??` 或者 `(user.name)??` +- [赋值操作](http://freemarker.foofun.cn/dgui_template_exp.html#dgui_template_exp_assignment): `=`, `+=`, `-=`, `*=`, `/=`, `%=`, `++`, `--` + +### 变量 + +注意:变量 _仅仅_ 在 [文本区](http://freemarker.foofun.cn/dgui_template_overallstructure.html) (比如 `

Hello ${name}!

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

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

    Welcome everyone!

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

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

    Parsed HTML into a doc.

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

    Lorem ipsum.

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

    An example link.

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

    lorem ipsum

    "); //

    lorem ipsum

    +div.prepend("

    First

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

    Last

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

    First

    lorem ipsum

    Last

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

    Link

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

    Link

    +``` + +**说明** + +XSS 又叫 CSS (Cross Site Script) ,跨站脚本攻击。它指的是恶意攻击者往 Web 页面里插入恶意 html 代码,当用户浏览该页之时,嵌入其中 Web 里面的 html 代码会被执行,从而达到恶意攻击用户的特殊目的。XSS 属于被动式的攻击,因为其被动且不好利用,所以许多人常忽略其危害性。所以我们经常只让用户输入纯文本的内容,但这样用户体验就比较差了。 + +一个更好的解决方法就是使用一个富文本编辑器 WYSIWYG 如 [CKEditor](http://ckeditor.com/) 和 [TinyMCE](http://tinymce.moxiecode.com/)。这些可以输出 HTML 并能够让用户可视化编辑。虽然他们可以在客户端进行校验,但是这样还不够安全,需要在服务器端进行校验并清除有害的 HTML 代码,这样才能确保输入到你网站的 HTML 是安全的。否则,攻击者能够绕过客户端的 Javascript 验证,并注入不安全的 HMTL 直接进入您的网站。 + +jsoup 的 whitelist 清理器能够在服务器端对用户输入的 HTML 进行过滤,只输出一些安全的标签和属性。 + +jsoup 提供了一系列的 `Whitelist` 基本配置,能够满足大多数要求;但如有必要,也可以进行修改,不过要小心。 + +这个 cleaner 非常好用不仅可以避免 XSS 攻击,还可以限制用户可以输入的标签范围。 + +**参见** + +- 参阅[XSS cheat sheet](http://ha.ckers.org/xss.html) ,有一个例子可以了解为什么不能使用正则表达式,而采用安全的 whitelist parser-based 清理器才是正确的选择。 +- 参阅`Cleaner` ,了解如何返回一个 `Document` 对象,而不是字符串 +- 参阅`Whitelist`,了解如何创建一个自定义的 whitelist +- [nofollow](http://en.wikipedia.org/wiki/Nofollow) 链接属性了解 + +## 参考 + +- [jsoup github 托管代码](https://github.com/jhy/jsoup) +- [jsoup Cookbook](https://jsoup.org/cookbook/) +- [jsoup Cookbook(中文版)](http://www.open-open.com/jsoup/) +- [不错的 jsoup 学习笔记](https://github.com/code4craft/jsoup-learning) \ No newline at end of file diff --git a/docs/javalib/thumbnailator.md "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" similarity index 74% rename from docs/javalib/thumbnailator.md rename to "docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" index 88a722f8..71151154 100644 --- a/docs/javalib/thumbnailator.md +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" @@ -1,17 +1,21 @@ --- -title: Thumbnailator 使用小结 -date: 2017/01/17 +title: Thumbnailator 快速入门 +date: 2022-02-17 22:34:30 +order: 06 categories: -- javalib + - Java + - 工具 + - 其他 tags: -- java -- javalib -- image + - Java + - 图形处理 + - Thumbnailator +permalink: /pages/adacc5/ --- -# Thumbnailator 使用小结 +# Thumbnailator 快速入门 -## 概述 +## 简介 `Thumbnailator` 是一个开源的 **Java** 项目,它提供了非常简单的 API 来对图片进行缩放、旋转以及加水印的处理。 @@ -51,7 +55,7 @@ public static Builder fromInputStreams(Iterable fromImages(Iterable images) {...} ``` -很显然,**Thumbnails 允许通过传入文件名、文件、网络图的URL、图片流、图片缓存多种方式来初始化构造器**。 +很显然,**Thumbnails 允许通过传入文件名、文件、网络图的 URL、图片流、图片缓存多种方式来初始化构造器**。 因此,你可以根据实际需求来灵活的选择图片的输入方式。 @@ -61,7 +65,7 @@ public static Builder fromImages(Iterable images) `Thumbnails.Builder` 是 `Thumbnails` 的内部静态类。它用于设置生成缩略图任务的相关参数。 -***注:`Thumbnails.Builder` 的构造函数是私有函数。所以,它只允许通过 `Thumbnails` 的实例化函数来进行初始化。*** +**_注:`Thumbnails.Builder` 的构造函数是私有函数。所以,它只允许通过 `Thumbnails` 的实例化函数来进行初始化。_** #### 设置参数的函数 @@ -72,18 +76,18 @@ public static Builder fromImages(Iterable images) ```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; + updateStatus(Properties.SIZE, Status.ALREADY_SET); + updateStatus(Properties.SCALE, Status.CANNOT_SET); + + validateDimensions(width, height); + this.width = width; + this.height = height; + + return this; } ``` -通过返回this指针,使得设置参数函数可以以链式调用的方式来使用,形式如下: +通过返回 this 指针,使得设置参数函数可以以链式调用的方式来使用,形式如下: ```java Thumbnails.of(new File("original.jpg")) @@ -131,7 +135,7 @@ Thumbnailator 的工作步骤十分简单,可分为三步: 3. **输出**:`Thumbnails.Builder` 输出图片文件或图片流。 -> 更多详情可以参考: [Thumbnailator 官网javadoc](https://coobird.github.io/thumbnailator/javadoc/0.4.8/) +> 更多详情可以参考: [Thumbnailator 官网 javadoc](https://coobird.github.io/thumbnailator/javadoc/0.4.8/) ## 实战 @@ -141,7 +145,7 @@ Thumbnailator 生成什么样的图片,是根据设置参数来决定的。 ### 安装 -maven项目中引入依赖: +maven 项目中引入依赖: ```xml @@ -150,29 +154,32 @@ maven项目中引入依赖: [0.4, 0.5) ``` + ### 图片缩放 `Thumbnails.Builder` 的 `size` 函数可以设置新图片精确的宽度和高度,也可以用 `scale` 函数设置缩放比例。 ```java Thumbnails.of("oldFile.png") - .size(16, 16) - .toFile("newFile_16_16.png"); + .size(16, 16) + .toFile("newFile_16_16.png"); Thumbnails.of("oldFile.png") - .scale(2.0) - .toFile("newFile_scale_2.0.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"); + .scale(1.0, 0.5) + .toFile("newFile_scale_1.0_0.5.png"); ``` **oldFile.png** -![lion_scale_1.0.png](http://upload-images.jianshu.io/upload_images/3101171-ba63439898602e8f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +![img](http://upload-images.jianshu.io/upload_images/3101171-ba63439898602e8f.png) **newFile_scale_1.0_0.5.png** -![lion_scale_1.0_0.5.png](http://upload-images.jianshu.io/upload_images/3101171-a01ea4515fff865d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) + +![img](http://upload-images.jianshu.io/upload_images/3101171-a01ea4515fff865d.png) ### 图片旋转 @@ -180,19 +187,19 @@ Thumbnails.of("oldFile.png") ```java Thumbnails.of("oldFile.png") - .scale(0.8) - .rotate(90) - .toFile("newFile_rotate_90.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"); + .scale(0.8) + .rotate(180) + .toFile("newFile_rotate_180.png"); ``` **newFile_rotate_90.png** -![lion2_rotate_90.png](http://upload-images.jianshu.io/upload_images/3101171-17d54bc33b38d45b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![img](http://upload-images.jianshu.io/upload_images/3101171-17d54bc33b38d45b.png) ### 加水印 @@ -201,18 +208,18 @@ Thumbnails.of("oldFile.png") ```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"); + .scale(0.8) + .watermark(Positions.BOTTOM_LEFT, watermarkImage, 0.5f) + .toFile("newFile_watermark.png"); ``` **wartermarkFile.png** -![wartermark.png](http://upload-images.jianshu.io/upload_images/3101171-97909ee6c066c195.png?imageMogr2/auto-orient/strip) +![img](http://upload-images.jianshu.io/upload_images/3101171-97909ee6c066c195.png?imageMogr2/auto-orient/strip) **newFile_watermark.png** -![lion2_watermark.png](http://upload-images.jianshu.io/upload_images/3101171-93eb7ef71b811a0c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) +![img](http://upload-images.jianshu.io/upload_images/3101171-93eb7ef71b811a0c.png) ### 批量处理图片 @@ -223,13 +230,13 @@ 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); + .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) +[Thumbnailator 官方示例文档](https://github.com/coobird/thumbnailator/wiki/Examples) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" new file mode 100644 index 00000000..d5f691bb --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" @@ -0,0 +1,97 @@ +--- +title: ZXing 快速入门 +date: 2022-02-17 22:34:30 +order: 07 +categories: + - Java + - 工具 + - 其他 +tags: + - Java + - 条形码 + - ZXing +permalink: /pages/b563af/ +--- + +# ZXing 快速入门 + +## 简介 + +`ZXing` 是一个开源 Java 类库用于解析多种格式的 1D/2D 条形码。目标是能够对 QR 编码、Data Matrix、UPC 的 1D 条形码进行解码。 其提供了多种平台下的客户端包括:J2ME、J2SE 和 Android。 + +官网:[ZXing github 仓库](https://github.com/zxing/zxing) + +## 实战 + +**_本例演示如何在一个非 android 的 Java 项目中使用 ZXing 来生成、解析二维码图片。_** + +### 安装 + +maven 项目只需引入依赖: + +```xml + + com.google.zxing + core + 3.3.0 + + + com.google.zxing + javase + 3.3.0 + +``` + +如果非 maven 项目,就去官网下载发布版本:[下载地址](https://github.com/zxing/zxing/releases) + +### 生成二维码图片 + +ZXing 生成二维码图片有以下步骤: + +1. `com.google.zxing.MultiFormatWriter` 根据内容以及图像编码参数生成图像 2D 矩阵。 +2. ​ `com.google.zxing.client.j2se.MatrixToImageWriter` 根据图像矩阵生成图片文件或图片缓存 `BufferedImage` 。 + +```java +public void encode(String content, String filepath) throws WriterException, IOException { + int width = 100; + int height = 100; + Map encodeHints = new HashMap(); + encodeHints.put(EncodeHintType.CHARACTER_SET, "UTF-8"); + BitMatrix bitMatrix = new MultiFormatWriter().encode(content, BarcodeFormat.QR_CODE, width, height, encodeHints); + Path path = FileSystems.getDefault().getPath(filepath); + MatrixToImageWriter.writeToPath(bitMatrix, "png", path); +} +``` + +### 解析二维码图片 + +ZXing 解析二维码图片有以下步骤: + +1. 使用 `javax.imageio.ImageIO` 读取图片文件,并存为一个 `java.awt.image.BufferedImage` 对象。 + +2. 将 `java.awt.image.BufferedImage` 转换为 ZXing 能识别的 `com.google.zxing.BinaryBitmap` 对象。 + +3. `com.google.zxing.MultiFormatReader` 根据图像解码参数来解析 `com.google.zxing.BinaryBitmap` 。 + +```java +public String decode(String filepath) throws IOException, NotFoundException { + BufferedImage bufferedImage = ImageIO.read(new FileInputStream(filepath)); + LuminanceSource source = new BufferedImageLuminanceSource(bufferedImage); + Binarizer binarizer = new HybridBinarizer(source); + BinaryBitmap bitmap = new BinaryBitmap(binarizer); + HashMap decodeHints = new HashMap(); + decodeHints.put(DecodeHintType.CHARACTER_SET, "UTF-8"); + Result result = new MultiFormatReader().decode(bitmap, decodeHints); + return result.getText(); +} +``` + +完整参考示例:[测试例代码](https://github.com/dunwu/JavaParty/blob/master/toolbox/image/src/test/java/org/zp/image/QRCodeUtilTest.java) + +以下是一个生成的二维码图片示例: + +![img](http://upload-images.jianshu.io/upload_images/3101171-26b73730088f0ab8.png) + +## 参考 + +[ZXing github 仓库](https://github.com/zxing/zxing) \ No newline at end of file diff --git "a/docs/01.Java/12.\345\267\245\345\205\267/README.md" "b/docs/01.Java/12.\345\267\245\345\205\267/README.md" new file mode 100644 index 00000000..dbd1c924 --- /dev/null +++ "b/docs/01.Java/12.\345\267\245\345\205\267/README.md" @@ -0,0 +1,56 @@ +--- +title: Java 工具 +date: 2022-02-18 08:53:11 +categories: + - Java + - 工具 +tags: + - Java + - 工具 +permalink: /pages/1123e1/ +hidden: true +index: false +--- + +# Java 工具 + +## 📖 内容 + +### Java IO + +- [JSON 序列化](01.IO/01.JSON序列化.md) - [fastjson](https://github.com/alibaba/fastjson)、[Jackson](https://github.com/FasterXML/jackson)、[Gson](https://github.com/google/gson) +- [二进制序列化](01.IO/02.二进制序列化.md) - [Protobuf](https://developers.google.com/protocol-buffers)、[Thrift](https://thrift.apache.org/)、[Hessian](http://hessian.caucho.com/)、[Kryo](https://github.com/EsotericSoftware/kryo)、[FST](https://github.com/RuedigerMoeller/fast-serialization) + +### JavaBean 工具 + +- [Lombok](02.JavaBean/01.Lombok.md) +- [Dozer](02.JavaBean/02.Dozer.md) + +### Java 模板引擎 + +- [Freemark](03.模板引擎/01.Freemark.md) +- [Velocity](03.模板引擎/02.Thymeleaf.md) +- [Thymeleaf](03.模板引擎/03.Velocity.md) + +### Java 测试工具 + +- [Junit](04.测试/01.Junit.md) +- [Mockito](04.测试/02.Mockito.md) +- [Jmeter](04.测试/03.Jmeter.md) +- [JMH](04.测试/04.JMH.md) + +### 其他 + +- [Java 日志](99.其他/01.Java日志.md) +- [Java 工具包](99.其他/02.Java工具包.md) +- [Reflections](99.其他/03.Reflections.md) +- [JavaMail](99.其他/04.JavaMail.md) +- [Jsoup](99.其他/05.Jsoup.md) +- [Thumbnailator](99.其他/06.Thumbnailator.md) +- [Zxing](99.其他/07.Zxing.md) + +## 📚 资料 + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/01.Spring\346\246\202\350\277\260.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/01.Spring\346\246\202\350\277\260.md" new file mode 100644 index 00000000..3a202241 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/01.Spring\346\246\202\350\277\260.md" @@ -0,0 +1,188 @@ +--- +title: Spring Framework 综述 +date: 2019-11-22 10:46:02 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring +permalink: /pages/9d3091/ +--- + +# Spring Framework 综述 + +## Spring Framework 简介 + +Spring Framework 是最受欢迎的企业级 Java 应用程序开发框架。用于构建企业级应用的轻量级、一站式解决方案。 + +当谈论到大小和透明度时, Spring 是轻量级的。 Spring 框架的基础版本是在 2 MB 左右的。 + +Spring 框架的核心特性可以用于开发任何 Java 应用程序,但是在 Java EE 平台上构建 web 应用程序是需要扩展的。 Spring 框架的目标是使 J2EE 开发变得更容易使用,通过启用基于 POJO 编程模型来促进良好的编程实践。 + +Spring Framework 设计理念如下: + +- 力争让选择无处不在 +- 体现海纳百川的精神 +- 保持后向兼容性 +- 专注 API 设计 +- 追求严苛的代码质量 + +## 为什么使用 Spring + +下面列出的是使用 Spring 框架主要的好处: + +- Spring 可以使开发人员使用 POJOs 开发企业级的应用程序。只使用 POJOs 的好处是你不需要一个 EJB 容器产品,比如一个应用程序服务器,但是你可以选择使用一个健壮的 servlet 容器,比如 Tomcat 或者一些商业产品。 +- Spring 在一个单元模式中是有组织的。即使包和类的数量非常大,你只需要选择你需要的部分,而忽略剩余的那部分。 +- Spring 不会让你白费力气做重复工作,它真正的利用了一些现有的技术,像几个 ORM 框架、日志框架、JEE、Quartz 和 JDK 计时器,其他视图技术。 +- 测试一个用 Spring 编写的应用程序很容易,因为 environment-dependent 代码被放进了这个框架中。此外,通过使用 JavaBean-style POJOs,它在使用依赖注入注入测试数据时变得更容易。 +- Spring 的 web 框架是一个设计良好的 web MVC 框架,它为 web 框架,比如 Structs 或者其他工程上的或者很少受欢迎的 web 框架,提供了一个很好的供替代的选择。 +- 为将特定技术的异常(例如,由 JDBC、Hibernate,或者 JDO 抛出的异常)翻译成一致的, Spring 提供了一个方便的 API,而这些都是未经检验的异常。 +- 轻量级的 IOC 容器往往是轻量级的,例如,特别是当与 EJB 容器相比的时候。这有利于在内存和 CPU 资源有限的计算机上开发和部署应用程序。 +- Spring 提供了一个一致的事务管理界面,该界面可以缩小成一个本地事务(例如,使用一个单一的数据库)和扩展成一个全局事务(例如,使用 JTA)。 + +## 核心思想 + +Spring 最核心的两个技术思想是:IoC 和 Aop + +### IoC + +`IoC` 即 `Inversion of Control` ,意为控制反转。 + +Spring 最认同的技术是控制反转的**依赖注入(DI)**模式。控制反转(IoC)是一个通用的概念,它可以用许多不同的方式去表达,依赖注入仅仅是控制反转的一个具体的例子。 + +当编写一个复杂的 Java 应用程序时,应用程序类应该尽可能的独立于其他的 Java 类来增加这些类可重用可能性,当进行单元测试时,可以使它们独立于其他类进行测试。依赖注入(或者有时被称为配线)有助于将这些类粘合在一起,并且在同一时间让它们保持独立。 + +到底什么是依赖注入?让我们将这两个词分开来看一看。这里将依赖关系部分转化为两个类之间的关联。例如,类 A 依赖于类 B。现在,让我们看一看第二部分,注入。所有这一切都意味着类 B 将通过 IoC 被注入到类 A 中。 + +依赖注入可以以向构造函数传递参数的方式发生,或者通过使用 setter 方法 post-construction。由于依赖注入是 Spring 框架的核心部分,所以我将在一个单独的章节中利用很好的例子去解释这一概念。 + +### Aop + +Spring 框架的一个关键组件是**面向方面的程序设计(AOP)**框架。一个程序中跨越多个点的功能被称为**横切关注点**,这些横切关注点在概念上独立于应用程序的业务逻辑。有各种各样常见的很好的关于方面的例子,比如日志记录、声明性事务、安全性,和缓存等等。 + +在 OOP 中模块化的关键单元是类,而在 AOP 中模块化的关键单元是方面。AOP 帮助你将横切关注点从它们所影响的对象中分离出来,然而依赖注入帮助你将你的应用程序对象从彼此中分离出来。 + +Spring 框架的 AOP 模块提供了面向方面的程序设计实现,允许你定义拦截器方法和切入点,可以实现将应该被分开的代码干净的分开功能。我将在一个独立的章节中讨论更多关于 Spring AOP 的概念。 + +## Spring 体系结构 + +Spring 当前框架有**20**个 jar 包,大致可以分为**6**大模块: + +- 1. 为什么使用 Spring +- 2. 核心思想 + - 2.1. IoC + - 2.2. Aop +- 3. Spring 体系结构 + - 3.1. Core Container + - 3.1.1. BeanFactory + - 3.1.2. ApplicationContext + - 3.2. AOP and Instrumentation + - 3.3. Messaging + - 3.4. Data Access / Integaration + - 3.5. Web + - 3.6. Test +- 4. 术语 + +Spring 框架提供了非常丰富的功能,因此整个架构也很庞大。 +在我们实际的应用开发中,并不一定要使用所有的功能,而是可以根据需要选择合适的 Spring 模块。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/spring-framework.png) + +### Core Container + +IoC 容器是 Spring 框架的核心。spring 容器使用依赖注入管理构成应用的组件,它会创建相互协作的组件之间的关联。毫无疑问,这些对象更简单干净,更容易理解,也更容易重用和测试。 +Spring 自带了几种容器的实现,可归纳为两种类型: + +#### BeanFactory + +由 org.springframework.beans.factory.BeanFactory 接口定义。 +它是最简单的容器,提供基本的 DI 支持。 + +#### ApplicationContext + +由 org.springframework.context.ApplicationContext 接口定义。 +它是基于 BeanFactory 之上构建,并提供面向应用的服务,例如从属性文件解析文本信息的能力,以及发布应用事件给感兴趣的事件监听者的能力。 +**_注:Bean 工厂对于大多数应用来说往往太低级了,所以应用上下文使用更广泛。推荐在开发中使用应用上下文容器。_** + +Spring 自带了多种应用上下文,最可能遇到的有以下几种: +`ClassPathXmlApplicationContext`:从类路径下的 XML 配置文件中加载上下文定义,把应用上下文定义文件当做类资源。 +`FileSystemXmlApplicationContext`:读取文件系统下的 XML 配置文件并加载上下文定义。 +`XmlWebApplicationContext`:读取 Web 应用下的 XML 配置文件并装载上下文定义。 + +**_范例_** + +```java +ApplicationContext context = new FileSystemXmlApplicationContext("D:\Temp\build.xml"); +ApplicationContext context2 = new ClassPathXmlApplicationContext("build.xml"); +``` + +可以看到,加载 `FileSystemXmlApplicationContext` 和 `ClassPathXmlApplicationContext` 十分相似。 +差异在于:前者在指定文件系统路径下查找 build.xml 文件;而后在所有类路径(包含 JAR 文件)下查找 build.xml 文件。 +通过引用应用上下文,可以很方便的调用 getBean() 方法从 Spring 容器中获取 Bean。 + +**相关 jar 包** + +- `spring-core`, `spring-beans`, 提供框架的基础部分,包括 IoC 和依赖注入特性。 + +- `spring-context`, 在`spring-core`, `spring-beans`基础上构建。它提供一种框架式的访问对象的方法。它也支持类似 Java EE 特性,例如:EJB,JMX 和基本 remoting。ApplicationContext 接口是它的聚焦点。 +- `springcontext-support`, 集成第三方库到 Spring application context。 +- `spring-expression`,提供一种强有力的表达语言在运行时来查询和操纵一个对象图。 + +### AOP and Instrumentation + +**相关 jar 包** + +- `spring-aop`,提供了对面向切面编程的丰富支持。 +- `spring-aspects`,提供了对 AspectJ 的集成。 +- `spring-instrument`,提供了对类 instrumentation 的支持和类加载器。 +- `spring-instrument-tomcat`,包含了 Spring 对 Tomcat 的 instrumentation 代理。 + +### Messaging + +**相关 jar 包** + +- `spring-messaging`,包含 spring 的消息处理功能,如 Message,MessageChannel,MessageHandler。 + +### Data Access / Integaration + +Data Access/Integration 层包含了 JDBC / ORM / OXM / JMS 和 Transaction 模块。 + +**相关 jar 包** + +- `spring-jdbc`,提供了一个 JDBC 抽象层。 + +- `spring-tx`,支持编程和声明式事务管理类。 +- `spring-orm`,提供了流行的对象关系型映射 API 集,如 JPA,JDO,Hibernate。 +- `spring-oxm`,提供了一个抽象层以支持对象/XML 映射的实现,如 JAXB,Castor,XMLBeans,JiBX 和 XStream. +- `spring-jms`,包含了生产和消费消息的功能。 + +### Web + +**相关 jar 包** + +- `spring-web`,提供了基本的面向 web 的功能,如多文件上传、使用 Servlet 监听器的 Ioc 容器的初始化。一个面向 web 的应用层上下文。 + +- `spring-webmvc`,包括 MVC 和 REST web 服务实现。 +- `spring-webmvc-portlet`,提供在 Protlet 环境的 MVC 实现和`spring-webmvc`功能的镜像。 + +### Test + +**相关 jar 包** + +- `spring-test`,以 Junit 和 TestNG 来支持 spring 组件的单元测试和集成测试。 + +## 术语 + +- **应用程序**:是能完成我们所需要功能的成品,比如购物网站、OA 系统。 +- **框架**:是能完成一定功能的半成品,比如我们可以使用框架进行购物网站开发;框架做一部分功能,我们自己做一部分功能,这样应用程序就创建出来了。而且框架规定了你在开发应用程序时的整体架构,提供了一些基础功能,还规定了类和对象的如何创建、如何协作等,从而简化我们开发,让我们专注于业务逻辑开发。 +- **非侵入式设计**:从框架角度可以这样理解,无需继承框架提供的类,这种设计就可以看作是非侵入式设计,如果继承了这些框架类,就是侵入设计,如果以后想更换框架之前写过的代码几乎无法重用,如果非侵入式设计则之前写过的代码仍然可以继续使用。 +- **轻量级及重量级**:轻量级是相对于重量级而言的,轻量级一般就是非入侵性的、所依赖的东西非常少、资源占用非常少、部署简单等等,其实就是比较容易使用,而重量级正好相反。 +- **POJO**:POJO(Plain Old Java Objects)简单的 Java 对象,它可以包含业务逻辑或持久化逻辑,但不担当任何特殊角色且不继承或不实现任何其它 Java 框架的类或接口。 +- **容器**:在日常生活中容器就是一种盛放东西的器具,从程序设计角度看就是装对象的的对象,因为存在放入、拿出等操作,所以容器还要管理对象的生命周期。 +- **控制反转:**即 Inversion of Control,缩写为 IoC,控制反转还有一个名字叫做依赖注入(Dependency Injection),就是由容器控制程序之间的关系,而非传统实现中,由程序代码直接操控。 +- **JavaBean**:一般指容器管理对象,在 Spring 中指 Spring IoC 容器管理对象。 \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/21.SpringBoot\347\237\245\350\257\206\345\233\276\350\260\261.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/21.SpringBoot\347\237\245\350\257\206\345\233\276\350\260\261.md" new file mode 100644 index 00000000..3a5733b4 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/21.SpringBoot\347\237\245\350\257\206\345\233\276\350\260\261.md" @@ -0,0 +1,951 @@ +--- +title: SpringBoot 知识图谱 +date: 2020-08-12 07:01:26 +order: 21 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/430f53/ +--- + +# SpringBoot 知识图谱 + +> 1. 预警:本文非常长,建议先 mark 后看,也许是最后一次写这么长的文章 +> 2. 说明:前面有 4 个小节关于 Spring 的基础知识,分别是:IOC 容器、JavaConfig、事件监听、SpringFactoriesLoader 详解,它们占据了本文的大部分内容,虽然它们之间可能没有太多的联系,但这些知识对于理解 Spring Boot 的核心原理至关重要,如果你对 Spring 框架烂熟于心,完全可以跳过这 4 个小节。正是因为这个系列的文章是由这些看似不相关的知识点组成,因此取名知识清单。 + +在过去两三年的 Spring 生态圈,最让人兴奋的莫过于 Spring Boot 框架。或许从命名上就能看出这个框架的设计初衷:快速的启动 Spring 应用。因而 Spring Boot 应用本质上就是一个基于 Spring 框架的应用,它是 Spring 对“约定优先于配置”理念的最佳实践产物,它能够帮助开发者更快速高效地构建基于 Spring 生态圈的应用。 + +那 Spring Boot 有何魔法?**自动配置**、**起步依赖**、**Actuator**、**命令行界面(CLI)** 是 Spring Boot 最重要的 4 大核心特性,其中 CLI 是 Spring Boot 的可选特性,虽然它功能强大,但也引入了一套不太常规的开发模型,因而这个系列的文章仅关注其它 3 种特性。如文章标题,本文是这个系列的第一部分,将为你打开 Spring Boot 的大门,重点为你剖析其启动流程以及自动配置实现原理。要掌握这部分核心内容,理解一些 Spring 框架的基础知识,将会让你事半功倍。 + +## 一、抛砖引玉:探索 Spring IoC 容器 + +如果有看过`SpringApplication.run()`方法的源码,Spring Boot 冗长无比的启动流程一定会让你抓狂,透过现象看本质,SpringApplication 只是将一个典型的 Spring 应用的启动流程进行了扩展,因此,透彻理解 Spring 容器是打开 Spring Boot 大门的一把钥匙。 + +### 1.1、Spring IoC 容器 + +可以把 Spring IoC 容器比作一间餐馆,当你来到餐馆,通常会直接招呼服务员:点菜!至于菜的原料是什么?如何用原料把菜做出来?可能你根本就不关心。IoC 容器也是一样,你只需要告诉它需要某个 bean,它就把对应的实例(instance)扔给你,至于这个 bean 是否依赖其他组件,怎样完成它的初始化,根本就不需要你关心。 + +作为餐馆,想要做出菜肴,得知道菜的原料和菜谱,同样地,IoC 容器想要管理各个业务对象以及它们之间的依赖关系,需要通过某种途径来记录和管理这些信息。`BeanDefinition`对象就承担了这个责任:容器中的每一个 bean 都会有一个对应的 BeanDefinition 实例,该实例负责保存 bean 对象的所有必要信息,包括 bean 对象的 class 类型、是否是抽象类、构造方法和参数、其它属性等等。当客户端向容器请求相应对象时,容器就会通过这些信息为客户端返回一个完整可用的 bean 实例。 + +原材料已经准备好(把 BeanDefinition 看着原料),开始做菜吧,等等,你还需要一份菜谱,`BeanDefinitionRegistry`和`BeanFactory`就是这份菜谱,BeanDefinitionRegistry 抽象出 bean 的注册逻辑,而 BeanFactory 则抽象出了 bean 的管理逻辑,而各个 BeanFactory 的实现类就具体承担了 bean 的注册以及管理工作。它们之间的关系就如下图: + +![img](https://user-gold-cdn.xitu.io/2018/9/9/165bd49d06649b0b?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) _BeanFactory、BeanDefinitionRegistry 关系图(来自:Spring 揭秘)_ + +`DefaultListableBeanFactory`作为一个比较通用的 BeanFactory 实现,它同时也实现了 BeanDefinitionRegistry 接口,因此它就承担了 Bean 的注册管理工作。从图中也可以看出,BeanFactory 接口中主要包含 getBean、containBean、getType、getAliases 等管理 bean 的方法,而 BeanDefinitionRegistry 接口则包含 registerBeanDefinition、removeBeanDefinition、getBeanDefinition 等注册管理 BeanDefinition 的方法。 + +下面通过一段简单的代码来模拟 BeanFactory 底层是如何工作的: + +``` +// 默认容器实现 +DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory(); +// 根据业务对象构造相应的BeanDefinition +AbstractBeanDefinition definition = new RootBeanDefinition(Business.class,true); +// 将bean定义注册到容器中 +beanRegistry.registerBeanDefinition("beanName",definition); +// 如果有多个bean,还可以指定各个bean之间的依赖关系 +// ........ + +// 然后可以从容器中获取这个bean的实例 +// 注意:这里的beanRegistry其实实现了BeanFactory接口,所以可以强转, +// 单纯的BeanDefinitionRegistry是无法强制转换到BeanFactory类型的 +BeanFactory container = (BeanFactory)beanRegistry; +Business business = (Business)container.getBean("beanName"); +``` + +这段代码仅为了说明 BeanFactory 底层的大致工作流程,实际情况会更加复杂,比如 bean 之间的依赖关系可能定义在外部配置文件(XML/Properties)中、也可能是注解方式。Spring IoC 容器的整个工作流程大致可以分为两个阶段: + +①、容器启动阶段 + +容器启动时,会通过某种途径加载`Configuration MetaData`。除了代码方式比较直接外,在大部分情况下,容器需要依赖某些工具类,比如:`BeanDefinitionReader`,BeanDefinitionReader 会对加载的`Configuration MetaData`进行解析和分析,并将分析后的信息组装为相应的 BeanDefinition,最后把这些保存了 bean 定义的 BeanDefinition,注册到相应的 BeanDefinitionRegistry,这样容器的启动工作就完成了。这个阶段主要完成一些准备性工作,更侧重于 bean 对象管理信息的收集,当然一些验证性或者辅助性的工作也在这一阶段完成。 + +来看一个简单的例子吧,过往,所有的 bean 都定义在 XML 配置文件中,下面的代码将模拟 BeanFactory 如何从配置文件中加载 bean 的定义以及依赖关系: + +``` +// 通常为BeanDefinitionRegistry的实现类,这里以DeFaultListabeBeanFactory为例 +BeanDefinitionRegistry beanRegistry = new DefaultListableBeanFactory(); +// XmlBeanDefinitionReader实现了BeanDefinitionReader接口,用于解析XML文件 +XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReaderImpl(beanRegistry); +// 加载配置文件 +beanDefinitionReader.loadBeanDefinitions("classpath:spring-bean.xml"); + +// 从容器中获取bean实例 +BeanFactory container = (BeanFactory)beanRegistry; +Business business = (Business)container.getBean("beanName"); +``` + +②、Bean 的实例化阶段 + +经过第一阶段,所有 bean 定义都通过 BeanDefinition 的方式注册到 BeanDefinitionRegistry 中,当某个请求通过容器的 getBean 方法请求某个对象,或者因为依赖关系容器需要隐式的调用 getBean 时,就会触发第二阶段的活动:容器会首先检查所请求的对象之前是否已经实例化完成。如果没有,则会根据注册的 BeanDefinition 所提供的信息实例化被请求对象,并为其注入依赖。当该对象装配完毕后,容器会立即将其返回给请求方法使用。 + +BeanFactory 只是 Spring IoC 容器的一种实现,如果没有特殊指定,它采用采用延迟初始化策略:只有当访问容器中的某个对象时,才对该对象进行初始化和依赖注入操作。而在实际场景下,我们更多的使用另外一种类型的容器:`ApplicationContext`,它构建在 BeanFactory 之上,属于更高级的容器,除了具有 BeanFactory 的所有能力之外,还提供对事件监听机制以及国际化的支持等。它管理的 bean,在容器启动时全部完成初始化和依赖注入操作。 + +### 1.2、Spring 容器扩展机制 + +IoC 容器负责管理容器中所有 bean 的生命周期,而在 bean 生命周期的不同阶段,Spring 提供了不同的扩展点来改变 bean 的命运。在容器的启动阶段,`BeanFactoryPostProcessor`允许我们在容器实例化相应对象之前,对注册到容器的 BeanDefinition 所保存的信息做一些额外的操作,比如修改 bean 定义的某些属性或者增加其他信息等。 + +如果要自定义扩展类,通常需要实现`org.springframework.beans.factory.config.BeanFactoryPostProcessor`接口,与此同时,因为容器中可能有多个 BeanFactoryPostProcessor,可能还需要实现`org.springframework.core.Ordered`接口,以保证 BeanFactoryPostProcessor 按照顺序执行。Spring 提供了为数不多的 BeanFactoryPostProcessor 实现,我们以`PropertyPlaceholderConfigurer`来说明其大致的工作流程。 + +在 Spring 项目的 XML 配置文件中,经常可以看到许多配置项的值使用占位符,而将占位符所代表的值单独配置到独立的 properties 文件,这样可以将散落在不同 XML 文件中的配置集中管理,而且也方便运维根据不同的环境进行配置不同的值。这个非常实用的功能就是由 PropertyPlaceholderConfigurer 负责实现的。 + +根据前文,当 BeanFactory 在第一阶段加载完所有配置信息时,BeanFactory 中保存的对象的属性还是以占位符方式存在的,比如`${jdbc.mysql.url}`。当 PropertyPlaceholderConfigurer 作为 BeanFactoryPostProcessor 被应用时,它会使用 properties 配置文件中的值来替换相应的 BeanDefinition 中占位符所表示的属性值。当需要实例化 bean 时,bean 定义中的属性值就已经被替换成我们配置的值。当然其实现比上面描述的要复杂一些,这里仅说明其大致工作原理,更详细的实现可以参考其源码。 + +与之相似的,还有`BeanPostProcessor`,其存在于对象实例化阶段。跟 BeanFactoryPostProcessor 类似,它会处理容器内所有符合条件并且已经实例化后的对象。简单的对比,BeanFactoryPostProcessor 处理 bean 的定义,而 BeanPostProcessor 则处理 bean 完成实例化后的对象。BeanPostProcessor 定义了两个接口: + +``` +public interface BeanPostProcessor { + // 前置处理 + Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException; + // 后置处理 + Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException; +} +``` + +为了理解这两个方法执行的时机,简单的了解下 bean 的整个生命周期: + +`postProcessBeforeInitialization()`方法与`postProcessAfterInitialization()`分别对应图中前置处理和后置处理两个步骤将执行的方法。这两个方法中都传入了 bean 对象实例的引用,为扩展容器的对象实例化过程提供了很大便利,在这儿几乎可以对传入的实例执行任何操作。注解、AOP 等功能的实现均大量使用了`BeanPostProcessor`,比如有一个自定义注解,你完全可以实现 BeanPostProcessor 的接口,在其中判断 bean 对象的脑袋上是否有该注解,如果有,你可以对这个 bean 实例执行任何操作,想想是不是非常的简单? + +再来看一个更常见的例子,在 Spring 中经常能够看到各种各样的 Aware 接口,其作用就是在对象实例化完成以后将 Aware 接口定义中规定的依赖注入到当前实例中。比如最常见的`ApplicationContextAware`接口,实现了这个接口的类都可以获取到一个 ApplicationContext 对象。当容器中每个对象的实例化过程走到 BeanPostProcessor 前置处理这一步时,容器会检测到之前注册到容器的 ApplicationContextAwareProcessor,然后就会调用其 postProcessBeforeInitialization()方法,检查并设置 Aware 相关依赖。看看代码吧,是不是很简单: + +``` +// 代码来自:org.springframework.context.support.ApplicationContextAwareProcessor +// 其postProcessBeforeInitialization方法调用了invokeAwareInterfaces方法 +private void invokeAwareInterfaces(Object bean) { + if (bean instanceof EnvironmentAware) { + ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment()); + } + if (bean instanceof ApplicationContextAware) { + ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); + } + // ...... +} +``` + +最后总结一下,本小节内容和你一起回顾了 Spring 容器的部分核心内容,限于篇幅不能写更多,但理解这部分内容,足以让您轻松理解 Spring Boot 的启动原理,如果在后续的学习过程中遇到一些晦涩难懂的知识,再回过头来看看 Spring 的核心知识,也许有意想不到的效果。也许 Spring Boot 的中文资料很少,但 Spring 的中文资料和书籍有太多太多,总有东西能给你启发。 + +## 二、夯实基础:JavaConfig 与常见 Annotation + +### 2.1、JavaConfig + +我们知道`bean`是 Spring IOC 中非常核心的概念,Spring 容器负责 bean 的生命周期的管理。在最初,Spring 使用 XML 配置文件的方式来描述 bean 的定义以及相互间的依赖关系,但随着 Spring 的发展,越来越多的人对这种方式表示不满,因为 Spring 项目的所有业务类均以 bean 的形式配置在 XML 文件中,造成了大量的 XML 文件,使项目变得复杂且难以管理。 + +后来,基于纯 Java Annotation 依赖注入框架`Guice`出世,其性能明显优于采用 XML 方式的 Spring,甚至有部分人认为,`Guice`可以完全取代 Spring(`Guice`仅是一个轻量级 IOC 框架,取代 Spring 还差的挺远)。正是这样的危机感,促使 Spring 及社区推出并持续完善了`JavaConfig`子项目,它基于 Java 代码和 Annotation 注解来描述 bean 之间的依赖绑定关系。比如,下面是使用 XML 配置方式来描述 bean 的定义: + +``` + +``` + +而基于 JavaConfig 的配置形式是这样的: + +``` +@Configuration +public class MoonBookConfiguration { + + // 任何标志了@Bean的方法,其返回值将作为一个bean注册到Spring的IOC容器中 + // 方法名默认成为该bean定义的id + @Bean + public BookService bookService() { + return new BookServiceImpl(); + } +} +``` + +如果两个 bean 之间有依赖关系的话,在 XML 配置中应该是这样: + +``` + + + + + + + + + +``` + +而在 JavaConfig 中则是这样: + +``` +@Configuration +public class MoonBookConfiguration { + + // 如果一个bean依赖另一个bean,则直接调用对应JavaConfig类中依赖bean的创建方法即可 + // 这里直接调用dependencyService() + @Bean + public BookService bookService() { + return new BookServiceImpl(dependencyService()); + } + + @Bean + public OtherService otherService() { + return new OtherServiceImpl(dependencyService()); + } + + @Bean + public DependencyService dependencyService() { + return new DependencyServiceImpl(); + } +} +``` + +你可能注意到这个示例中,有两个 bean 都依赖于 dependencyService,也就是说当初始化 bookService 时会调用`dependencyService()`,在初始化 otherService 时也会调用`dependencyService()`,那么问题来了?这时候 IOC 容器中是有一个 dependencyService 实例还是两个?这个问题留着大家思考吧,这里不再赘述。 + +### 2.2、@ComponentScan + +`@ComponentScan`注解对应 XML 配置形式中的``元素,表示启用组件扫描,Spring 会自动扫描所有通过注解配置的 bean,然后将其注册到 IOC 容器中。我们可以通过`basePackages`等属性来指定`@ComponentScan`自动扫描的范围,如果不指定,默认从声明`@ComponentScan`所在类的`package`进行扫描。正因为如此,SpringBoot 的启动类都默认在`src/main/java`下。 + +### 2.3、@Import + +`@Import`注解用于导入配置类,举个简单的例子: + +``` +@Configuration +public class MoonBookConfiguration { + @Bean + public BookService bookService() { + return new BookServiceImpl(); + } +} +``` + +现在有另外一个配置类,比如:`MoonUserConfiguration`,这个配置类中有一个 bean 依赖于`MoonBookConfiguration`中的 bookService,如何将这两个 bean 组合在一起?借助`@Import`即可: + +``` +@Configuration +// 可以同时导入多个配置类,比如:@Import({A.class,B.class}) +@Import(MoonBookConfiguration.class) +public class MoonUserConfiguration { + @Bean + public UserService userService(BookService bookService) { + return new BookServiceImpl(bookService); + } +} +``` + +需要注意的是,在 4.2 之前,`@Import`注解只支持导入配置类,但是在 4.2 之后,它支持导入普通类,并将这个类作为一个 bean 的定义注册到 IOC 容器中。 + +### 2.4、@Conditional + +`@Conditional`注解表示在满足某种条件后才初始化一个 bean 或者启用某些配置。它一般用在由`@Component`、`@Service`、`@Configuration`等注解标识的类上面,或者由`@Bean`标记的方法上。如果一个`@Configuration`类标记了`@Conditional`,则该类中所有标识了`@Bean`的方法和`@Import`注解导入的相关类将遵从这些条件。 + +在 Spring 里可以很方便的编写你自己的条件类,所要做的就是实现`Condition`接口,并覆盖它的`matches()`方法。举个例子,下面的简单条件类表示只有在`Classpath`里存在`JdbcTemplate`类时才生效: + +``` +public class JdbcTemplateCondition implements Condition { + + @Override + public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) { + try { + conditionContext.getClassLoader().loadClass("org.springframework.jdbc.core.JdbcTemplate"); + return true; + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + return false; + } +} +``` + +当你用 Java 来声明 bean 的时候,可以使用这个自定义条件类: + +``` +@Conditional(JdbcTemplateCondition.class) +@Service +public MyService service() { + ...... +} +``` + +这个例子中只有当`JdbcTemplateCondition`类的条件成立时才会创建 MyService 这个 bean。也就是说 MyService 这 bean 的创建条件是`classpath`里面包含`JdbcTemplate`,否则这个 bean 的声明就会被忽略掉。 + +`Spring Boot`定义了很多有趣的条件,并把他们运用到了配置类上,这些配置类构成了`Spring Boot`的自动配置的基础。`Spring Boot`运用条件化配置的方法是:定义多个特殊的条件化注解,并将它们用到配置类上。下面列出了`Spring Boot`提供的部分条件化注解: + +| 条件化注解 | 配置生效条件 | +| ------------------------------- | ------------------------------------------------------- | +| @ConditionalOnBean | 配置了某个特定 bean | +| @ConditionalOnMissingBean | 没有配置特定的 bean | +| @ConditionalOnClass | Classpath 里有指定的类 | +| @ConditionalOnMissingClass | Classpath 里没有指定的类 | +| @ConditionalOnExpression | 给定的 Spring Expression Language 表达式计算结果为 true | +| @ConditionalOnJava | Java 的版本匹配特定指或者一个范围值 | +| @ConditionalOnProperty | 指定的配置属性要有一个明确的值 | +| @ConditionalOnResource | Classpath 里有指定的资源 | +| @ConditionalOnWebApplication | 这是一个 Web 应用程序 | +| @ConditionalOnNotWebApplication | 这不是一个 Web 应用程序 | + +### 2.5、@ConfigurationProperties 与@EnableConfigurationProperties + +当某些属性的值需要配置的时候,我们一般会在`application.properties`文件中新建配置项,然后在 bean 中使用`@Value`注解来获取配置的值,比如下面配置数据源的代码。 + +``` +// jdbc config +jdbc.mysql.url=jdbc:mysql://localhost:3306/sampledb +jdbc.mysql.username=root +jdbc.mysql.password=123456 +...... + +// 配置数据源 +@Configuration +public class HikariDataSourceConfiguration { + + @Value("jdbc.mysql.url") + public String url; + @Value("jdbc.mysql.username") + public String user; + @Value("jdbc.mysql.password") + public String password; + + @Bean + public HikariDataSource dataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(url); + hikariConfig.setUsername(user); + hikariConfig.setPassword(password); + // 省略部分代码 + return new HikariDataSource(hikariConfig); + } +} +``` + +使用`@Value`注解注入的属性通常都比较简单,如果同一个配置在多个地方使用,也存在不方便维护的问题(考虑下,如果有几十个地方在使用某个配置,而现在你想改下名字,你改怎么做?)。对于更为复杂的配置,Spring Boot 提供了更优雅的实现方式,那就是`@ConfigurationProperties`注解。我们可以通过下面的方式来改写上面的代码: + +``` +@Component +// 还可以通过@PropertySource("classpath:jdbc.properties")来指定配置文件 +@ConfigurationProperties("jdbc.mysql") +// 前缀=jdbc.mysql,会在配置文件中寻找jdbc.mysql.*的配置项 +pulic class JdbcConfig { + public String url; + public String username; + public String password; +} + +@Configuration +public class HikariDataSourceConfiguration { + + @AutoWired + public JdbcConfig config; + + @Bean + public HikariDataSource dataSource() { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(config.url); + hikariConfig.setUsername(config.username); + hikariConfig.setPassword(config.password); + // 省略部分代码 + return new HikariDataSource(hikariConfig); + } +} +``` + +`@ConfigurationProperties`对于更为复杂的配置,处理起来也是得心应手,比如有如下配置文件: + +``` +#App +app.menus[0].title=Home +app.menus[0].name=Home +app.menus[0].path=/ +app.menus[1].title=Login +app.menus[1].name=Login +app.menus[1].path=/login + +app.compiler.timeout=5 +app.compiler.output-folder=/temp/ + +app.error=/error/ +``` + +可以定义如下配置类来接收这些属性 + +``` +@Component +@ConfigurationProperties("app") +public class AppProperties { + + public String error; + public List menus = new ArrayList<>(); + public Compiler compiler = new Compiler(); + + public static class Menu { + public String name; + public String path; + public String title; + } + + public static class Compiler { + public String timeout; + public String outputFolder; + } +} +``` + +`@EnableConfigurationProperties`注解表示对`@ConfigurationProperties`的内嵌支持,默认会将对应 Properties Class 作为 bean 注入的 IOC 容器中,即在相应的 Properties 类上不用加`@Component`注解。 + +## 三、削铁如泥:SpringFactoriesLoader 详解 + +JVM 提供了 3 种类加载器:`BootstrapClassLoader`、`ExtClassLoader`、`AppClassLoader`分别加载 Java 核心类库、扩展类库以及应用的类路径(`CLASSPATH`)下的类库。JVM 通过双亲委派模型进行类的加载,我们也可以通过继承`java.lang.classloader`实现自己的类加载器。 + +何为双亲委派模型?当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的 BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。 + +采用双亲委派模型的一个好处是保证使用不同类加载器最终得到的都是同一个对象,这样就可以保证 Java 核心库的类型安全,比如,加载位于 rt.jar 包中的`java.lang.Object`类,不管是哪个加载器加载这个类,最终都是委托给顶层的 BootstrapClassLoader 来加载的,这样就可以保证任何的类加载器最终得到的都是同样一个 Object 对象。查看 ClassLoader 的源码,对双亲委派模型会有更直观的认识: + +``` +protected Class loadClass(String name, boolean resolve) { + synchronized (getClassLoadingLock(name)) { + // 首先,检查该类是否已经被加载,如果从JVM缓存中找到该类,则直接返回 + Class c = findLoadedClass(name); + if (c == null) { + try { + // 遵循双亲委派的模型,首先会通过递归从父加载器开始找, + // 直到父类加载器是BootstrapClassLoader为止 + if (parent != null) { + c = parent.loadClass(name, false); + } else { + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) {} + if (c == null) { + // 如果还找不到,尝试通过findClass方法去寻找 + // findClass是留给开发者自己实现的,也就是说 + // 自定义类加载器时,重写此方法即可 + c = findClass(name); + } + } + if (resolve) { + resolveClass(c); + } + return c; + } +} +``` + +但双亲委派模型并不能解决所有的类加载器问题,比如,Java 提供了很多服务提供者接口(`Service Provider Interface`,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JNDI、JAXP 等,这些 SPI 的接口由核心类库提供,却由第三方实现,这样就存在一个问题:SPI 的接口是 Java 核心库的一部分,是由 BootstrapClassLoader 加载的;SPI 实现的 Java 类一般是由 AppClassLoader 来加载的。BootstrapClassLoader 是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给 AppClassLoader,因为它是最顶层的类加载器。也就是说,双亲委派模型并不能解决这个问题。 + +线程上下文类加载器(`ContextClassLoader`)正好解决了这个问题。从名称上看,可能会误解为它是一种新的类加载器,实际上,它仅仅是 Thread 类的一个变量而已,可以通过`setContextClassLoader(ClassLoader cl)`和`getContextClassLoader()`来设置和获取该对象。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是 AppClassLoader。在核心类库使用 SPI 接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。但在 JDBC 中,你可能会看到一种更直接的实现方式,比如,JDBC 驱动管理`java.sql.Driver`中的`loadInitialDrivers()`方法中,你可以直接看到 JDK 是如何加载驱动的: + +``` +for (String aDriver : driversList) { + try { + // 直接使用AppClassLoader + Class.forName(aDriver, true, ClassLoader.getSystemClassLoader()); + } catch (Exception ex) { + println("DriverManager.Initialize: load failed: " + ex); + } +} +``` + +其实讲解线程上下文类加载器,最主要是让大家在看到`Thread.currentThread().getClassLoader()`和`Thread.currentThread().getContextClassLoader()`时不会一脸懵逼,这两者除了在许多底层框架中取得的 ClassLoader 可能会有所不同外,其他大多数业务场景下都是一样的,大家只要知道它是为了解决什么问题而存在的即可。 + +类加载器除了加载 class 外,还有一个非常重要功能,就是加载资源,它可以从 jar 包中读取任何资源文件,比如,`ClassLoader.getResources(String name)`方法就是用于读取 jar 包中的资源文件,其代码如下: + +``` +public Enumeration getResources(String name) throws IOException { + Enumeration[] tmp = (Enumeration[]) new Enumeration[2]; + if (parent != null) { + tmp[0] = parent.getResources(name); + } else { + tmp[0] = getBootstrapResources(name); + } + tmp[1] = findResources(name); + return new CompoundEnumeration<>(tmp); +} +``` + +是不是觉得有点眼熟,不错,它的逻辑其实跟类加载的逻辑是一样的,首先判断父类加载器是否为空,不为空则委托父类加载器执行资源查找任务,直到 BootstrapClassLoader,最后才轮到自己查找。而不同的类加载器负责扫描不同路径下的 jar 包,就如同加载 class 一样,最后会扫描所有的 jar 包,找到符合条件的资源文件。 + +类加载器的`findResources(name)`方法会遍历其负责加载的所有 jar 包,找到 jar 包中名称为 name 的资源文件,这里的资源可以是任何文件,甚至是.class 文件,比如下面的示例,用于查找 Array.class 文件: + +``` +// 寻找Array.class文件 +public static void main(String[] args) throws Exception{ + // Array.class的完整路径 + String name = "java/sql/Array.class"; + Enumeration urls = Thread.currentThread().getContextClassLoader().getResources(name); + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + System.out.println(url.toString()); + } +} +``` + +运行后可以得到如下结果: + +``` +$JAVA_HOME/jre/lib/rt.jar!/java/sql/Array.class +``` + +根据资源文件的 URL,可以构造相应的文件来读取资源内容。 + +看到这里,你可能会感到挺奇怪的,你不是要详解`SpringFactoriesLoader`吗?上来讲了一堆 ClassLoader 是几个意思?看下它的源码你就知道了: + +``` +public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; +// spring.factories文件的格式为:key=value1,value2,value3 +// 从所有的jar包中找到META-INF/spring.factories文件 +// 然后从文件中解析出key=factoryClass类名称的所有value值 +public static List loadFactoryNames(Class factoryClass, ClassLoader classLoader) { + String factoryClassName = factoryClass.getName(); + // 取得资源文件的URL + Enumeration urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); + List result = new ArrayList(); + // 遍历所有的URL + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + // 根据资源文件URL解析properties文件 + Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); + String factoryClassNames = properties.getProperty(factoryClassName); + // 组装数据,并返回 + result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); + } + return result; +} +``` + +有了前面关于 ClassLoader 的知识,再来理解这段代码,是不是感觉豁然开朗:从`CLASSPATH`下的每个 Jar 包中搜寻所有`META-INF/spring.factories`配置文件,然后将解析 properties 文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去 ClassPath 路径下查找,会扫描所有路径下的 Jar 包,只不过这个文件只会在 Classpath 下的 jar 包中。来简单看下`spring.factories`文件的内容吧: + +``` +// 来自 org.springframework.boot.autoconfigure下的META-INF/spring.factories +// EnableAutoConfiguration后文会讲到,它用于开启Spring Boot自动配置功能 +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ +org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ +org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration\ +``` + +执行`loadFactoryNames(EnableAutoConfiguration.class, classLoader)`后,得到对应的一组`@Configuration`类, +我们就可以通过反射实例化这些类然后注入到 IOC 容器中,最后容器里就有了一系列标注了`@Configuration`的 JavaConfig 形式的配置类。 + +这就是`SpringFactoriesLoader`,它本质上属于 Spring 框架私有的一种扩展方案,类似于 SPI,Spring Boot 在 Spring 基础上的很多核心功能都是基于此,希望大家可以理解。 + +## 四、另一件武器:Spring 容器的事件监听机制 + +过去,事件监听机制多用于图形界面编程,比如:**点击**按钮、在文本框**输入**内容等操作被称为事件,而当事件触发时,应用程序作出一定的响应则表示应用监听了这个事件,而在服务器端,事件的监听机制更多的用于异步通知以及监控和异常处理。Java 提供了实现事件监听机制的两个基础类:自定义事件类型扩展自`java.util.EventObject`、事件的监听器扩展自`java.util.EventListener`。来看一个简单的实例:简单的监控一个方法的耗时。 + +首先定义事件类型,通常的做法是扩展 EventObject,随着事件的发生,相应的状态通常都封装在此类中: + +``` +public class MethodMonitorEvent extends EventObject { + // 时间戳,用于记录方法开始执行的时间 + public long timestamp; + + public MethodMonitorEvent(Object source) { + super(source); + } +} +``` + +事件发布之后,相应的监听器即可对该类型的事件进行处理,我们可以在方法开始执行之前发布一个 begin 事件,在方法执行结束之后发布一个 end 事件,相应地,事件监听器需要提供方法对这两种情况下接收到的事件进行处理: + +``` +// 1、定义事件监听接口 +public interface MethodMonitorEventListener extends EventListener { + // 处理方法执行之前发布的事件 + public void onMethodBegin(MethodMonitorEvent event); + // 处理方法结束时发布的事件 + public void onMethodEnd(MethodMonitorEvent event); +} +// 2、事件监听接口的实现:如何处理 +public class AbstractMethodMonitorEventListener implements MethodMonitorEventListener { + + @Override + public void onMethodBegin(MethodMonitorEvent event) { + // 记录方法开始执行时的时间 + event.timestamp = System.currentTimeMillis(); + } + + @Override + public void onMethodEnd(MethodMonitorEvent event) { + // 计算方法耗时 + long duration = System.currentTimeMillis() - event.timestamp; + System.out.println("耗时:" + duration); + } +} +``` + +事件监听器接口针对不同的事件发布实际提供相应的处理方法定义,最重要的是,其方法只接收 MethodMonitorEvent 参数,说明这个监听器类只负责监听器对应的事件并进行处理。有了事件和监听器,剩下的就是发布事件,然后让相应的监听器监听并处理。通常情况,我们会有一个事件发布者,它本身作为事件源,在合适的时机,将相应的事件发布给对应的事件监听器: + +``` +public class MethodMonitorEventPublisher { + + private List listeners = new ArrayList(); + + public void methodMonitor() { + MethodMonitorEvent eventObject = new MethodMonitorEvent(this); + publishEvent("begin",eventObject); + // 模拟方法执行:休眠5秒钟 + TimeUnit.SECONDS.sleep(5); + publishEvent("end",eventObject); + + } + + private void publishEvent(String status,MethodMonitorEvent event) { + // 避免在事件处理期间,监听器被移除,这里为了安全做一个复制操作 + List copyListeners = ➥ new ArrayList(listeners); + for (MethodMonitorEventListener listener : copyListeners) { + if ("begin".equals(status)) { + listener.onMethodBegin(event); + } else { + listener.onMethodEnd(event); + } + } + } + + public static void main(String[] args) { + MethodMonitorEventPublisher publisher = new MethodMonitorEventPublisher(); + publisher.addEventListener(new AbstractMethodMonitorEventListener()); + publisher.methodMonitor(); + } + // 省略实现 + public void addEventListener(MethodMonitorEventListener listener) {} + public void removeEventListener(MethodMonitorEventListener listener) {} + public void removeAllListeners() {} +``` + +对于事件发布者(事件源)通常需要关注两点: + +1. 在合适的时机发布事件。此例中的 methodMonitor()方法是事件发布的源头,其在方法执行之前和结束之后两个时间点发布 MethodMonitorEvent 事件,每个时间点发布的事件都会传给相应的监听器进行处理。在具体实现时需要注意的是,事件发布是顺序执行,为了不影响处理性能,事件监听器的处理逻辑应尽量简单。 +2. 事件监听器的管理。publisher 类中提供了事件监听器的注册与移除方法,这样客户端可以根据实际情况决定是否需要注册新的监听器或者移除某个监听器。如果这里没有提供 remove 方法,那么注册的监听器示例将一直被 MethodMonitorEventPublisher 引用,即使已经废弃不用了,也依然在发布者的监听器列表中,这会导致隐性的内存泄漏。 + +#### Spring 容器内的事件监听机制 + +Spring 的 ApplicationContext 容器内部中的所有事件类型均继承自`org.springframework.context.ApplicationEvent`,容器中的所有监听器都实现`org.springframework.context.ApplicationListener`接口,并且以 bean 的形式注册在容器中。一旦在容器内发布 ApplicationEvent 及其子类型的事件,注册到容器的 ApplicationListener 就会对这些事件进行处理。 + +你应该已经猜到是怎么回事了。 + +ApplicationEvent 继承自 EventObject,Spring 提供了一些默认的实现,比如:`ContextClosedEvent`表示容器在即将关闭时发布的事件类型,`ContextRefreshedEvent`表示容器在初始化或者刷新的时候发布的事件类型...... + +容器内部使用 ApplicationListener 作为事件监听器接口定义,它继承自 EventListener。ApplicationContext 容器在启动时,会自动识别并加载 EventListener 类型的 bean,一旦容器内有事件发布,将通知这些注册到容器的 EventListener。 + +ApplicationContext 接口继承了 ApplicationEventPublisher 接口,该接口提供了`void publishEvent(ApplicationEvent event)`方法定义,不难看出,ApplicationContext 容器担当的就是事件发布者的角色。如果有兴趣可以查看`AbstractApplicationContext.publishEvent(ApplicationEvent event)`方法的源码:ApplicationContext 将事件的发布以及监听器的管理工作委托给`ApplicationEventMulticaster`接口的实现类。在容器启动时,会检查容器内是否存在名为 applicationEventMulticaster 的 ApplicationEventMulticaster 对象实例。如果有就使用其提供的实现,没有就默认初始化一个 SimpleApplicationEventMulticaster 作为实现。 + +最后,如果我们业务需要在容器内部发布事件,只需要为其注入 ApplicationEventPublisher 依赖即可:实现 ApplicationEventPublisherAware 接口或者 ApplicationContextAware 接口(Aware 接口相关内容请回顾上文)。 + +## 五、出神入化:揭秘自动配置原理 + +典型的 Spring Boot 应用的启动类一般均位于`src/main/java`根路径下,比如`MoonApplication`类: + +``` +@SpringBootApplication +public class MoonApplication { + + public static void main(String[] args) { + SpringApplication.run(MoonApplication.class, args); + } +} +``` + +其中`@SpringBootApplication`开启组件扫描和自动配置,而`SpringApplication.run`则负责启动引导应用程序。`@SpringBootApplication`是一个复合`Annotation`,它将三个有用的注解组合在一起: + +``` +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@SpringBootConfiguration +@EnableAutoConfiguration +@ComponentScan(excludeFilters = { + @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), + @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) +public @interface SpringBootApplication { + // ...... +} +``` + +`@SpringBootConfiguration`就是`@Configuration`,它是 Spring 框架的注解,标明该类是一个`JavaConfig`配置类。而`@ComponentScan`启用组件扫描,前文已经详细讲解过,这里着重关注`@EnableAutoConfiguration`。 + +`@EnableAutoConfiguration`注解表示开启 Spring Boot 自动配置功能,Spring Boot 会根据应用的依赖、自定义的 bean、classpath 下有没有某个类 等等因素来猜测你需要的 bean,然后注册到 IOC 容器中。那`@EnableAutoConfiguration`是如何推算出你的需求?首先看下它的定义: + +``` +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@AutoConfigurationPackage +@Import(EnableAutoConfigurationImportSelector.class) +public @interface EnableAutoConfiguration { + // ...... +} +``` + +你的关注点应该在`@Import(EnableAutoConfigurationImportSelector.class)`上了,前文说过,`@Import`注解用于导入类,并将这个类作为一个 bean 的定义注册到容器中,这里它将把`EnableAutoConfigurationImportSelector`作为 bean 注入到容器中,而这个类会将所有符合条件的@Configuration 配置都加载到容器中,看看它的代码: + +``` +public String[] selectImports(AnnotationMetadata annotationMetadata) { + // 省略了大部分代码,保留一句核心代码 + // 注意:SpringBoot最近版本中,这句代码被封装在一个单独的方法中 + // SpringFactoriesLoader相关知识请参考前文 + List factories = new ArrayList(new LinkedHashSet( + SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, this.beanClassLoader))); +} +``` + +这个类会扫描所有的 jar 包,将所有符合条件的@Configuration 配置类注入的容器中,何为符合条件,看看`META-INF/spring.factories`的文件内容: + +``` +// 来自 org.springframework.boot.autoconfigure下的META-INF/spring.factories +// 配置的key = EnableAutoConfiguration,与代码中一致 +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\ +org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ +org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration\ +..... +``` + +以`DataSourceAutoConfiguration`为例,看看 Spring Boot 是如何自动配置的: + +``` +@Configuration +@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) +@EnableConfigurationProperties(DataSourceProperties.class) +@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class }) +public class DataSourceAutoConfiguration { +} +``` + +分别说一说: + +- `@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })`:当 Classpath 中存在 DataSource 或者 EmbeddedDatabaseType 类时才启用这个配置,否则这个配置将被忽略。 +- `@EnableConfigurationProperties(DataSourceProperties.class)`:将 DataSource 的默认配置类注入到 IOC 容器中,DataSourceproperties 定义为: + +``` +// 提供对datasource配置信息的支持,所有的配置前缀为:spring.datasource +@ConfigurationProperties(prefix = "spring.datasource") +public class DataSourceProperties { + private ClassLoader classLoader; + private Environment environment; + private String name = "testdb"; + ...... +} +``` + +- `@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })`:导入其他额外的配置,就以`DataSourcePoolMetadataProvidersConfiguration`为例吧。 + +``` +@Configuration +public class DataSourcePoolMetadataProvidersConfiguration { + + @Configuration + @ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class) + static class TomcatDataSourcePoolMetadataProviderConfiguration { + @Bean + public DataSourcePoolMetadataProvider tomcatPoolDataSourceMetadataProvider() { + ..... + } + } + ...... +} +``` + +DataSourcePoolMetadataProvidersConfiguration 是数据库连接池提供者的一个配置类,即 Classpath 中存在`org.apache.tomcat.jdbc.pool.DataSource.class`,则使用 tomcat-jdbc 连接池,如果 Classpath 中存在`HikariDataSource.class`则使用 Hikari 连接池。 + +这里仅描述了 DataSourceAutoConfiguration 的冰山一角,但足以说明 Spring Boot 如何利用条件话配置来实现自动配置的。回顾一下,`@EnableAutoConfiguration`中导入了 EnableAutoConfigurationImportSelector 类,而这个类的`selectImports()`通过 SpringFactoriesLoader 得到了大量的配置类,而每一个配置类则根据条件化配置来做出决策,以实现自动配置。 + +整个流程很清晰,但漏了一个大问题:`EnableAutoConfigurationImportSelector.selectImports()`是何时执行的?其实这个方法会在容器启动过程中执行:`AbstractApplicationContext.refresh()`,更多的细节在下一小节中说明。 + +## 六、启动引导:Spring Boot 应用启动的秘密 + +### 6.1 SpringApplication 初始化 + +SpringBoot 整个启动流程分为两个步骤:初始化一个 SpringApplication 对象、执行该对象的 run 方法。看下 SpringApplication 的初始化流程,SpringApplication 的构造方法中调用 initialize(Object[] sources)方法,其代码如下: + +``` +private void initialize(Object[] sources) { + if (sources != null && sources.length > 0) { + this.sources.addAll(Arrays.asList(sources)); + } + // 判断是否是Web项目 + this.webEnvironment = deduceWebEnvironment(); + setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class)); + setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); + // 找到入口类 + this.mainApplicationClass = deduceMainApplicationClass(); +} +``` + +初始化流程中最重要的就是通过 SpringFactoriesLoader 找到`spring.factories`文件中配置的`ApplicationContextInitializer`和`ApplicationListener`两个接口的实现类名称,以便后期构造相应的实例。`ApplicationContextInitializer`的主要目的是在`ConfigurableApplicationContext`做 refresh 之前,对 ConfigurableApplicationContext 实例做进一步的设置或处理。ConfigurableApplicationContext 继承自 ApplicationContext,其主要提供了对 ApplicationContext 进行设置的能力。 + +实现一个 ApplicationContextInitializer 非常简单,因为它只有一个方法,但大多数情况下我们没有必要自定义一个 ApplicationContextInitializer,即便是 Spring Boot 框架,它默认也只是注册了两个实现,毕竟 Spring 的容器已经非常成熟和稳定,你没有必要来改变它。 + +而`ApplicationListener`的目的就没什么好说的了,它是 Spring 框架对 Java 事件监听机制的一种框架实现,具体内容在前文 Spring 事件监听机制这个小节有详细讲解。这里主要说说,如果你想为 Spring Boot 应用添加监听器,该如何实现? + +Spring Boot 提供两种方式来添加自定义监听器: + +- 通过`SpringApplication.addListeners(ApplicationListener... listeners)`或者`SpringApplication.setListeners(Collection> listeners)`两个方法来添加一个或者多个自定义监听器 +- 既然 SpringApplication 的初始化流程中已经从`spring.factories`中获取到`ApplicationListener`的实现类,那么我们直接在自己的 jar 包的`META-INF/spring.factories`文件中新增配置即可: + +``` +org.springframework.context.ApplicationListener=\ +cn.moondev.listeners.xxxxListener\ +``` + +关于 SpringApplication 的初始化,我们就说这么多。 + +### 6.2 Spring Boot 启动流程 + +Spring Boot 应用的整个启动流程都封装在 SpringApplication.run 方法中,其整个流程真的是太长太长了,但本质上就是在 Spring 容器启动的基础上做了大量的扩展,按照这个思路来看看源码: + +``` +public ConfigurableApplicationContext run(String... args) { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + ConfigurableApplicationContext context = null; + FailureAnalyzers analyzers = null; + configureHeadlessProperty(); + // ① + SpringApplicationRunListeners listeners = getRunListeners(args); + listeners.starting(); + try { + // ② + ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); + ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments); + // ③ + Banner printedBanner = printBanner(environment); + // ④ + context = createApplicationContext(); + // ⑤ + analyzers = new FailureAnalyzers(context); + // ⑥ + prepareContext(context, environment, listeners, applicationArguments,printedBanner); + // ⑦ + refreshContext(context); + // ⑧ + afterRefresh(context, applicationArguments); + // ⑨ + listeners.finished(context, null); + stopWatch.stop(); + return context; + } + catch (Throwable ex) { + handleRunFailure(context, listeners, analyzers, ex); + throw new IllegalStateException(ex); + } + } +``` + +① 通过 SpringFactoriesLoader 查找并加载所有的`SpringApplicationRunListeners`,通过调用 starting()方法通知所有的 SpringApplicationRunListeners:应用开始启动了。SpringApplicationRunListeners 其本质上就是一个事件发布者,它在 SpringBoot 应用启动的不同时间点发布不同应用事件类型(ApplicationEvent),如果有哪些事件监听者(ApplicationListener)对这些事件感兴趣,则可以接收并且处理。还记得初始化流程中,SpringApplication 加载了一系列 ApplicationListener 吗?这个启动流程中没有发现有发布事件的代码,其实都已经在 SpringApplicationRunListeners 这儿实现了。 + +简单的分析一下其实现流程,首先看下 SpringApplicationRunListener 的源码: + +``` +public interface SpringApplicationRunListener { + + // 运行run方法时立即调用此方法,可以用户非常早期的初始化工作 + void starting(); + + // Environment准备好后,并且ApplicationContext创建之前调用 + void environmentPrepared(ConfigurableEnvironment environment); + + // ApplicationContext创建好后立即调用 + void contextPrepared(ConfigurableApplicationContext context); + + // ApplicationContext加载完成,在refresh之前调用 + void contextLoaded(ConfigurableApplicationContext context); + + // 当run方法结束之前调用 + void finished(ConfigurableApplicationContext context, Throwable exception); + +} +``` + +SpringApplicationRunListener 只有一个实现类:`EventPublishingRunListener`。① 处的代码只会获取到一个 EventPublishingRunListener 的实例,我们来看看 starting()方法的内容: + +``` +public void starting() { + // 发布一个ApplicationStartedEvent + this.initialMulticaster.multicastEvent(new ApplicationStartedEvent(this.application, this.args)); +} +``` + +顺着这个逻辑,你可以在 ② 处的`prepareEnvironment()`方法的源码中找到`listeners.environmentPrepared(environment);`即 SpringApplicationRunListener 接口的第二个方法,那不出你所料,`environmentPrepared()`又发布了另外一个事件`ApplicationEnvironmentPreparedEvent`。接下来会发生什么,就不用我多说了吧。 + +② 创建并配置当前应用将要使用的`Environment`,Environment 用于描述应用程序当前的运行环境,其抽象了两个方面的内容:配置文件(profile)和属性(properties),开发经验丰富的同学对这两个东西一定不会陌生:不同的环境(eg:生产环境、预发布环境)可以使用不同的配置文件,而属性则可以从配置文件、环境变量、命令行参数等来源获取。因此,当 Environment 准备好后,在整个应用的任何时候,都可以从 Environment 中获取资源。 + +总结起来,② 处的两句代码,主要完成以下几件事: + +- 判断 Environment 是否存在,不存在就创建(如果是 web 项目就创建`StandardServletEnvironment`,否则创建`StandardEnvironment`) +- 配置 Environment:配置 profile 以及 properties +- 调用 SpringApplicationRunListener 的`environmentPrepared()`方法,通知事件监听者:应用的 Environment 已经准备好 + +③、SpringBoot 应用在启动时会输出这样的东西: + +``` + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v1.5.6.RELEASE) +``` + +如果想把这个东西改成自己的涂鸦,你可以研究以下 Banner 的实现,这个任务就留给你们吧。 + +④、根据是否是 web 项目,来创建不同的 ApplicationContext 容器。 + +⑤、创建一系列`FailureAnalyzer`,创建流程依然是通过 SpringFactoriesLoader 获取到所有实现 FailureAnalyzer 接口的 class,然后在创建对应的实例。FailureAnalyzer 用于分析故障并提供相关诊断信息。 + +⑥、初始化 ApplicationContext,主要完成以下工作: + +- 将准备好的 Environment 设置给 ApplicationContext +- 遍历调用所有的 ApplicationContextInitializer 的`initialize()`方法来对已经创建好的 ApplicationContext 进行进一步的处理 +- 调用 SpringApplicationRunListener 的`contextPrepared()`方法,通知所有的监听者:ApplicationContext 已经准备完毕 +- 将所有的 bean 加载到容器中 +- 调用 SpringApplicationRunListener 的`contextLoaded()`方法,通知所有的监听者:ApplicationContext 已经装载完毕 + +⑦、调用 ApplicationContext 的`refresh()`方法,完成 IoC 容器可用的最后一道工序。从名字上理解为刷新容器,那何为刷新?就是插手容器的启动,联系一下第一小节的内容。那如何刷新呢?且看下面代码: + +``` +// 摘自refresh()方法中一句代码 +invokeBeanFactoryPostProcessors(beanFactory); +``` + +看看这个方法的实现: + +``` +protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) { + PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors()); + ...... +} +``` + +获取到所有的`BeanFactoryPostProcessor`来对容器做一些额外的操作。BeanFactoryPostProcessor 允许我们在容器实例化相应对象之前,对注册到容器的 BeanDefinition 所保存的信息做一些额外的操作。这里的 getBeanFactoryPostProcessors()方法可以获取到 3 个 Processor: + +``` +ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessor +SharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessor +ConfigFileApplicationListener$PropertySourceOrderingPostProcessor +``` + +不是有那么多 BeanFactoryPostProcessor 的实现类,为什么这儿只有这 3 个?因为在初始化流程获取到的各种 ApplicationContextInitializer 和 ApplicationListener 中,只有上文 3 个做了类似于如下操作: + +``` +public void initialize(ConfigurableApplicationContext context) { + context.addBeanFactoryPostProcessor(new ConfigurationWarningsPostProcessor(getChecks())); +} +``` + +然后你就可以进入到`PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors()`方法了,这个方法除了会遍历上面的 3 个 BeanFactoryPostProcessor 处理外,还会获取类型为`BeanDefinitionRegistryPostProcessor`的 bean:`org.springframework.context.annotation.internalConfigurationAnnotationProcessor`,对应的 Class 为`ConfigurationClassPostProcessor`。`ConfigurationClassPostProcessor`用于解析处理各种注解,包括:@Configuration、@ComponentScan、@Import、@PropertySource、@ImportResource、@Bean。当处理`@import`注解的时候,就会调用自动配置这一小节中的`EnableAutoConfigurationImportSelector.selectImports()`来完成自动配置功能。其他的这里不再多讲,如果你有兴趣,可以查阅参考资料 6。 + +⑧、查找当前 context 中是否注册有 CommandLineRunner 和 ApplicationRunner,如果有则遍历执行它们。 + +⑨、执行所有 SpringApplicationRunListener 的 finished()方法。 + +这就是 Spring Boot 的整个启动流程,其核心就是在 Spring 容器初始化并启动的基础上加入各种扩展点,这些扩展点包括:ApplicationContextInitializer、ApplicationListener 以及各种 BeanFactoryPostProcessor 等等。你对整个流程的细节不必太过关注,甚至没弄明白也没有关系,你只要理解这些扩展点是在何时如何工作的,能让它们为你所用即可。 + +整个启动流程确实非常复杂,可以查询参考资料中的部分章节和内容,对照着源码,多看看,我想最终你都能弄清楚的。言而总之,Spring 才是核心,理解清楚 Spring 容器的启动流程,那 Spring Boot 启动流程就不在话下了。 + +## 参考资料 + +[1][王福强 著;springboot 揭秘:快速构建微服务体系; 机械工业出版社, 2016](https://link.jianshu.com/?t=http%3A%2F%2Funion-click.jd.com%2Fjdc%3Fd%3D4jESQ9) +[2][王福强 著;spring 揭秘; 人民邮件出版社, 2009](https://link.jianshu.com/?t=http%3A%2F%2Funion-click.jd.com%2Fjdc%3Fd%3DyzfgeF) +[3][craig walls 著;丁雪丰 译;spring boot 实战;中国工信出版集团 人民邮电出版社,2016](https://link.jianshu.com/?t=http%3A%2F%2Funion-click.jd.com%2Fjdc%3Fd%3DAQ6oHO) +[4][深入探讨 java 类加载器](https://link.jianshu.com/?t=https%3A%2F%2Fwww.ibm.com%2Fdeveloperworks%2Fcn%2Fjava%2Fj-lo-classloader%2F) : [www.ibm.com/developerwo…](https://link.jianshu.com/?t=https%3A%2F%2Fwww.ibm.com%2Fdeveloperworks%2Fcn%2Fjava%2Fj-lo-classloader%2F) +[5][spring boot 实战:自动配置原理分析](https://link.jianshu.com/?t=http%3A%2F%2Fblog.csdn.net%2Fliaokailin%2Farticle%2Fdetails%2F49559951) : [blog.csdn.net/liaokailin/…](https://link.jianshu.com/?t=http%3A%2F%2Fblog.csdn.net%2Fliaokailin%2Farticle%2Fdetails%2F49559951) +[6][spring boot实战:spring boot bean加载源码分析](https://link.jianshu.com/?t=http%3A%2F%2Fblog.csdn.net%2Fliaokailin%2Farticle%2Fdetails%2F49107209): [blog.csdn.net/liaokailin/…](https://link.jianshu.com/?t=http%3A%2F%2Fblog.csdn.net%2Fliaokailin%2Farticle%2Fdetails%2F49107209) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/22.SpringBoot\345\237\272\346\234\254\345\216\237\347\220\206.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/22.SpringBoot\345\237\272\346\234\254\345\216\237\347\220\206.md" new file mode 100644 index 00000000..a7ee34ae --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/22.SpringBoot\345\237\272\346\234\254\345\216\237\347\220\206.md" @@ -0,0 +1,286 @@ +--- +title: SpringBoot 基本原理 +date: 2020-08-12 07:01:26 +order: 22 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/dbf521/ +--- + +# SpringBoot 基本原理 + +SpringBoot 为我们做的自动配置,确实方便快捷,但一直搞不明白它的内部启动原理,这次就来一步步解开 SpringBoot 的神秘面纱,让它不再神秘。 + +![img](https:////upload-images.jianshu.io/upload_images/6430208-ebcb376f96103703.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) + +--- + +```java +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +从上面代码可以看出,**Annotation 定义(@SpringBootApplication)和类定义(SpringApplication.run)**最为耀眼,所以要揭开 SpringBoot 的神秘面纱,我们要从这两位开始就可以了。 + +## SpringBootApplication 背后的秘密 + +```kotlin +@Target(ElementType.TYPE) // 注解的适用范围,其中TYPE用于描述类、接口(包括包注解类型)或enum声明 +@Retention(RetentionPolicy.RUNTIME) // 注解的生命周期,保留到class文件中(三个生命周期) +@Documented // 表明这个注解应该被javadoc记录 +@Inherited // 子类可以继承该注解 +@SpringBootConfiguration // 继承了Configuration,表示当前是注解类 +@EnableAutoConfiguration // 开启springboot的注解功能,springboot的四大神器之一,其借助@import的帮助 +@ComponentScan(excludeFilters = { // 扫描路径设置(具体使用待确认) + @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), + @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) +public @interface SpringBootApplication { +... +} +``` + +虽然定义使用了多个 Annotation 进行了原信息标注,但实际上重要的只有三个 Annotation: + +**@Configuration**(@SpringBootConfiguration 点开查看发现里面还是应用了@Configuration) +**@EnableAutoConfiguration +@ComponentScan** +所以,如果我们使用如下的 SpringBoot 启动类,整个 SpringBoot 应用依然可以与之前的启动类功能对等: + +```java +@Configuration +@EnableAutoConfiguration +@ComponentScan +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +每次写这 3 个比较累,所以写一个@SpringBootApplication 方便点。接下来分别介绍这 3 个 Annotation。 + +## @Configuration + +这里的@Configuration 对我们来说不陌生,**它就是 JavaConfig 形式的 Spring Ioc 容器的配置类使用的那个@Configuration**,SpringBoot 社区推荐使用基于 JavaConfig 的配置形式,所以,这里的启动类标注了@Configuration 之后,本身其实也是一个 IoC 容器的配置类。 +举几个简单例子回顾下,XML 跟 config 配置方式的区别: + +表达形式层面 +基于 XML 配置的方式是这样: + +```xml + + + + +``` + +而基于 JavaConfig 的配置方式是这样: + +```java +@Configuration +public class MockConfiguration{ + //bean定义 +} +``` + +**任何一个标注了@Configuration 的 Java 类定义都是一个 JavaConfig 配置类。** + +注册 bean 定义层面 +基于 XML 的配置形式是这样: + +```csharp + + ... + +``` + +而基于 JavaConfig 的配置形式是这样的: + +```java +@Configuration +public class MockConfiguration{ + @Bean + public MockService mockService(){ + return new MockServiceImpl(); + } +} +``` + +**任何一个标注了@Bean 的方法,其返回值将作为一个 bean 定义注册到 Spring 的 IoC 容器,方法名将默认成该 bean 定义的 id。** + +表达依赖注入关系层面 +为了表达 bean 与 bean 之间的依赖关系,在 XML 形式中一般是这样: + +```jsx + + + + + +``` + +而基于 JavaConfig 的配置形式是这样的: + +```java +@Configuration +public class MockConfiguration{ + @Bean + public MockService mockService(){ + return new MockServiceImpl(dependencyService()); + } + + @Bean + public DependencyService dependencyService(){ + return new DependencyServiceImpl(); + } +} +``` + +**如果一个 bean 的定义依赖其他 bean,则直接调用对应的 JavaConfig 类中依赖 bean 的创建方法就可以了。** + +## @ComponentScan + +**@ComponentScan 这个注解在 Spring 中很重要,它对应 XML 配置中的元素,@ComponentScan 的功能其实就是自动扫描并加载符合条件的组件(比如@Component 和@Repository 等)或者 bean 定义,最终将这些 bean 定义加载到 IoC 容器中。** + +我们可以通过 basePackages 等属性来细粒度的定制@ComponentScan 自动扫描的范围,如果不指定,则默认 Spring 框架实现会从声明@ComponentScan 所在类的 package 进行扫描。 + +> 注:所以 SpringBoot 的启动类最好是放在 root package 下,因为默认不指定 basePackages。 + +## @EnableAutoConfiguration + +个人感觉**@EnableAutoConfiguration 这个 Annotation 最为重要**,所以放在最后来解读,大家是否还记得 Spring 框架提供的各种名字为@Enable 开头的 Annotation 定义?比如@EnableScheduling、@EnableCaching、@EnableMBeanExport 等,@EnableAutoConfiguration 的理念和做事方式其实一脉相承,简单概括一下就是,借助@Import 的支持,收集和注册特定场景相关的 bean 定义。 + +**@EnableScheduling**是通过@Import 将 Spring 调度框架相关的 bean 定义都加载到 IoC 容器。 +**@EnableMBeanExport**是通过@Import 将 JMX 相关的 bean 定义加载到 IoC 容器。 +而**@EnableAutoConfiguration**也是借助@Import 的帮助,将所有符合自动配置条件的 bean 定义加载到 IoC 容器,仅此而已! + +@EnableAutoConfiguration 作为一个复合 Annotation,其自身定义关键信息如下: + +```kotlin +@SuppressWarnings("deprecation") +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@AutoConfigurationPackage +@Import(EnableAutoConfigurationImportSelector.class) +public @interface EnableAutoConfiguration { + ... +} +``` + +两个比较重要的注解: + +**@AutoConfigurationPackage:自动配置包** + +**@Import: 导入自动配置的组件** + +#### AutoConfigurationPackage 注解: + +```java +static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { + + @Override + public void registerBeanDefinitions(AnnotationMetadata metadata, + BeanDefinitionRegistry registry) { + register(registry, new PackageImport(metadata).getPackageName()); + } +``` + +它其实是注册了一个 Bean 的定义。 + +new PackageImport(metadata).getPackageName(),它其实返回了当前主程序类的 同级以及子级 的包组件。 + +![img](https:////upload-images.jianshu.io/upload_images/6430208-439283a70a24c7a0.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/281/format/webp) + +以上图为例,DemoApplication 是和 demo 包同级,但是 demo2 这个类是 DemoApplication 的父级,和 example 包同级 + +也就是说,DemoApplication 启动加载的 Bean 中,并不会加载 demo2,这也就是为什么,我们要把 DemoApplication 放在项目的最高级中。 + +#### Import(AutoConfigurationImportSelector.class)注解: + +![img](https:////upload-images.jianshu.io/upload_images/6430208-1c448a69c41dc35c.png?imageMogr2/auto-orient/strip|imageView2/2/w/877/format/webp) + +可以从图中看出 AutoConfigurationImportSelector 继承了 DeferredImportSelector 继承了 ImportSelector + +ImportSelector 有一个方法为:selectImports。 + +```dart +@Override + public String[] selectImports(AnnotationMetadata annotationMetadata) { + if (!isEnabled(annotationMetadata)) { + return NO_IMPORTS; + } + AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader + .loadMetadata(this.beanClassLoader); + AnnotationAttributes attributes = getAttributes(annotationMetadata); + List configurations = getCandidateConfigurations(annotationMetadata, + attributes); + configurations = removeDuplicates(configurations); + Set exclusions = getExclusions(annotationMetadata, attributes); + checkExcludedClasses(configurations, exclusions); + configurations.removeAll(exclusions); + configurations = filter(configurations, autoConfigurationMetadata); + fireAutoConfigurationImportEvents(configurations, exclusions); + return StringUtils.toStringArray(configurations); + } +``` + +可以看到第九行,它其实是去加载 public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";外部文件。这个外部文件,有很多自动配置的类。如下: + +![img](https:////upload-images.jianshu.io/upload_images/6430208-250f3320c15e5c99.jpg?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) + +image + +其中,最关键的要属**@Import(EnableAutoConfigurationImportSelector.class)**,借助**EnableAutoConfigurationImportSelector**,**@EnableAutoConfiguration**可以帮助 SpringBoot 应用将所有符合条件的**@Configuration**配置都加载到当前 SpringBoot 创建并使用的 IoC 容器。就像一只“八爪鱼”一样。 + +![img](https:////upload-images.jianshu.io/upload_images/6430208-6f3a835755ee7710.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) + +### 自动配置幕后英雄:SpringFactoriesLoader 详解 + +借助于 Spring 框架原有的一个工具类:SpringFactoriesLoader 的支持,@EnableAutoConfiguration 可以智能的自动配置功效才得以大功告成! + +SpringFactoriesLoader 属于 Spring 框架私有的一种扩展方案,其主要功能就是从指定的配置文件 META-INF/spring.factories 加载配置。 + +```php +public abstract class SpringFactoriesLoader { + //... + public static List loadFactories(Class factoryClass, ClassLoader classLoader) { + ... + } + + + public static List loadFactoryNames(Class factoryClass, ClassLoader classLoader) { + .... + } +} +``` + +配合**@EnableAutoConfiguration**使用的话,它更多是提供一种配置查找的功能支持,即根据@EnableAutoConfiguration 的完整类名 org.springframework.boot.autoconfigure.EnableAutoConfiguration 作为查找的 Key,获取对应的一组**@Configuration**类 + +![img](https:////upload-images.jianshu.io/upload_images/6430208-fcdfcb56828a015a?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp) + +上图就是从 SpringBoot 的 autoconfigure 依赖包中的 META-INF/spring.factories 配置文件中摘录的一段内容,可以很好地说明问题。 + +所以,@EnableAutoConfiguration 自动配置的魔法骑士就变成了:**从 classpath 中搜寻所有的 META-INF/spring.factories 配置文件,并将其中 org.springframework.boot.autoconfigure.EnableutoConfiguration 对应的配置项通过反射(Java Refletion)实例化为对应的标注了@Configuration 的 JavaConfig 形式的 IoC 容器配置类,然后汇总为一个并加载到 IoC 容器。** + +![img](https:////upload-images.jianshu.io/upload_images/6430208-10850d62d44c95ce.png?imageMogr2/auto-orient/strip|imageView2/2/w/822/format/webp) + +## 参考资料 + +- [一文搞懂 springboot 启动原理](https://www.jianshu.com/p/943650ab7dfd) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/99.Spring\351\235\242\350\257\225.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/99.Spring\351\235\242\350\257\225.md" new file mode 100644 index 00000000..461f16fb --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/99.Spring\351\235\242\350\257\225.md" @@ -0,0 +1,612 @@ +--- +title: Spring 面试 +date: 2018-08-02 17:33:32 +order: 99 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring + - 面试 +permalink: /pages/db33b0/ +--- + +# Spring 面试 + +## 综合篇 + +### 不同版本的 Spring Framework 有哪些主要功能? + +| Version | Feature | +| ---------- | ------------------------------------------------------------------- | +| Spring 2.5 | 发布于 2007 年。这是第一个支持注解的版本。 | +| Spring 3.0 | 发布于 2009 年。它完全利用了 Java5 中的改进,并为 JEE6 提供了支持。 | +| Spring 4.0 | 发布于 2013 年。这是第一个完全支持 JAVA8 的版本。 | + +### 什么是 Spring Framework? + +- Spring 是一个开源应用框架,旨在降低应用程序开发的复杂度。 +- 它是轻量级、松散耦合的。 +- 它具有分层体系结构,允许用户选择组件,同时还为 J2EE 应用程序开发提供了一个有凝聚力的框架。 +- 它可以集成其他框架,如 Structs、Hibernate、EJB 等,所以又称为框架的框架。 + +### 列举 Spring Framework 的优点。 + +- 由于 Spring Frameworks 的分层架构,用户可以自由选择自己需要的组件。 +- Spring Framework 支持 POJO(Plain Old Java Object) 编程,从而具备持续集成和可测试性。 +- 由于依赖注入和控制反转,JDBC 得以简化。 +- 它是开源免费的。 + +### Spring Framework 有哪些不同的功能? + +- **轻量级** - Spring 在代码量和透明度方面都很轻便。 +- **IOC** - 控制反转 +- **AOP** - 面向切面编程可以将应用业务逻辑和系统服务分离,以实现高内聚。 +- **容器** - Spring 负责创建和管理对象(Bean)的生命周期和配置。 +- **MVC** - 对 web 应用提供了高度可配置性,其他框架的集成也十分方便。 +- **事务管理** - 提供了用于事务管理的通用抽象层。Spring 的事务支持也可用于容器较少的环境。 +- **JDBC 异常** - Spring 的 JDBC 抽象层提供了一个异常层次结构,简化了错误处理策略。 + +### Spring Framework 中有多少个模块,它们分别是什么? + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/spring-framework.png) + +- **Spring 核心容器** – 该层基本上是 Spring Framework 的核心。它包含以下模块: + - Spring Core + - Spring Bean + - SpEL (Spring Expression Language) + - Spring Context +- **数据访问/集成** – 该层提供与数据库交互的支持。它包含以下模块: + - JDBC (Java DataBase Connectivity) + - ORM (Object Relational Mapping) + - OXM (Object XML Mappers) + - JMS (Java Messaging Service) + - Transaction +- **Web** – 该层提供了创建 Web 应用程序的支持。它包含以下模块: + - Web + - Web – Servlet + - Web – Socket + - Web – Portlet +- **AOP** – 该层支持面向切面编程 +- **Instrumentation** – 该层为类检测和类加载器实现提供支持。 +- **Test** – 该层为使用 JUnit 和 TestNG 进行测试提供支持。 +- **几个杂项模块:** + - Messaging – 该模块为 STOMP 提供支持。它还支持注解编程模型,该模型用于从 WebSocket 客户端路由和处理 STOMP 消息。 + - Aspects – 该模块为与 AspectJ 的集成提供支持。 + +### 什么是 Spring 配置文件? + +Spring 配置文件是 XML 文件。该文件主要包含类信息。它描述了这些类是如何配置以及相互引入的。但是,XML 配置文件冗长且更加干净。如果没有正确规划和编写,那么在大项目中管理变得非常困难。 + +### Spring 应用程序有哪些不同组件? + +Spring 应用一般有以下组件: + +- **接口** - 定义功能。 +- **Bean 类** - 它包含属性,setter 和 getter 方法,函数等。 +- **Spring 面向切面编程(AOP)** - 提供面向切面编程的功能。 +- **Bean 配置文件** - 包含类的信息以及如何配置它们。 +- **用户程序** - 它使用接口。 + +### 使用 Spring 有哪些方式? + +使用 Spring 有以下方式: + +- 作为一个成熟的 Spring Web 应用程序。 +- 作为第三方 Web 框架,使用 Spring Frameworks 中间层。 +- 用于远程使用。 +- 作为企业级 Java Bean,它可以包装现有的 POJO(Plain Old Java Objects)。 + +## 核心篇 + +### IoC + +#### 什么是 IoC?什么是依赖注入?什么是 Spring IoC? + +**IoC** 即**控制反转**(Inversion of Control,缩写为 IoC)。IoC 又称为**依赖倒置原则**(设计模式六大原则之一),它的要点在于:**程序要依赖于抽象接口,不要依赖于具体实现**。它的作用就是**用于降低代码间的耦合度**。 + +IoC 的实现方式有两种: + +- **依赖注入**(Dependency Injection,简称 DI):不通过 `new()` 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造器、函数参数等方式传递(或注入)给类使用。 +- **依赖查找**(Dependency Lookup):容器中的受控对象通过容器的 API 来查找自己所依赖的资源和协作对象。 + +Spring IoC 是 IoC 的一种实现。DI 是 Spring IoC 的主要实现原则。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221005163639.png) + +#### 依赖注入有哪些实现方式? + +依赖注入有如下方式: + +| 依赖注入方式 | 配置元数据举例 | +| --------------- | -------------------------------------------------- | +| Setter 方法注入 | `` | +| 构造器注入 | `` | +| 字段注入 | `@Autowired User user;` | +| 方法注入 | `@Autowired public void user(User user) { ... }` | +| 接口回调注入 | `class MyBean implements BeanFactoryAware { ... }` | + +#### 构造器注入 VS. setter 注入 + +| 构造器注入 | setter 注入 | +| -------------------------- | -------------------------- | +| 没有部分注入 | 有部分注入 | +| 不会覆盖 setter 属性 | 会覆盖 setter 属性 | +| 任意修改都会创建一个新实例 | 任意修改不会创建一个新实例 | +| 适用于设置很多属性 | 适用于设置少量属性 | + +官方推荐使用构造器注入。 + +#### BeanFactory VS. ApplicationContext + +在 Spring 中,有两种 IoC 容器:`BeanFactory` 和 `ApplicationContext`。 + +- `BeanFactory`:**`BeanFactory` 是 Spring 基础 IoC 容器**。`BeanFactory` 提供了 Spring 容器的配置框架和基本功能。 +- `ApplicationContext`:**`ApplicationContext` 是具备应用特性的 `BeanFactory` 的子接口**。它还扩展了其他一些接口,以支持更丰富的功能,如:国际化、访问资源、事件机制、更方便的支持 AOP、在 web 应用中指定应用层上下文等。 + +实际开发中,更推荐使用 `ApplicationContext` 作为 IoC 容器,因为它的功能远多于 `BeanFactory`。 + +#### BeanFactory VS. FactoryBean + +**`BeanFactory` 是 Spring 基础 IoC 容器**。 + +`FactoryBean` 是创建 Bean 的一种方式,帮助实现复杂的初始化逻辑。 + +#### Spring IoC 启动时做了哪些准备 + +IoC 配置元信息读取和解析 + +IoC 容器生命周期管理 + +Spring 事件发布 + +国际化 + +等等 + +#### Spring IoC 的实现机制是什么 + +Spring 中的 IoC 的实现原理就是工厂模式加反射机制。 + +示例: + +```java +interface Fruit { + public abstract void eat(); +} +class Apple implements Fruit { + public void eat(){ + System.out.println("Apple"); + } +} +class Orange implements Fruit { + public void eat(){ + System.out.println("Orange"); + } +} +class Factory { + public static Fruit getInstance(String ClassName) { + Fruit f=null; + try { + f=(Fruit)Class.forName(ClassName).newInstance(); + } catch (Exception e) { + e.printStackTrace(); + } + return f; + } +} +class Client { + public static void main(String[] a) { + Fruit f=Factory.getInstance("io.github.dunwu.spring.Apple"); + if(f!=null){ + f.eat(); + } + } +} +``` + +### Bean + +#### 什么是 Spring Bean + +在 Spring 中,构成应用程序主体由 Spring IoC 容器管理的对象称为 Bean。**Bean 是由 Spring IoC 容器实例化、装配和管理的对象**。 Bean 以及它们之间的依赖关系反映在容器使用的配置元数据中。 + +Spring IoC 容器本身,并不能识别配置的元数据。为此,要将这些配置信息转为 Spring 能识别的格式——`BeanDefinition` 对象。 + +**`BeanDefinition` 是 Spring 中定义 Bean 的配置元信息接口**,它包含: + +- Bean 类名 +- Bean 行为配置元素,如:作用域、自动绑定的模式、生命周期回调等 +- 其他 Bean 引用,也可称为合作者(Collaborators)或依赖(Dependencies) +- 配置设置,如 Bean 属性(Properties) + +#### 如何注册 Spring Bean + +通过 `BeanDefinition` 和外部单例对象来注册。 + +#### spring 提供了哪些配置方式? + +- 基于 xml 配置 + +bean 所需的依赖项和服务在 XML 格式的配置文件中指定。这些配置文件通常包含许多 bean 定义和特定于应用程序的配置选项。它们通常以 bean 标签开头。例如: + +```xml + + + +``` + +- 基于注解配置 + +您可以通过在相关的类,方法或字段声明上使用注解,将 bean 配置为组件类本身,而不是使用 XML 来描述 bean 装配。默认情况下,Spring 容器中未打开注解装配。因此,您需要在使用它之前在 Spring 配置文件中启用它。例如: + +```xml + + + + +``` + +- 基于 Java API 配置 + +Spring 的 Java 配置是通过使用 @Bean 和 @Configuration 来实现。 + +1. @Bean 注解扮演与 `` 元素相同的角色。 +2. @Configuration 类允许通过简单地调用同一个类中的其他 @Bean 方法来定义 bean 间依赖关系。 + +例如: + +```java +@Configuration +public class StudentConfig { + @Bean + public StudentBean myStudent() { + return new StudentBean(); + } +} +``` + +#### spring 支持集中 bean scope? + +Spring bean 支持 5 种 scope: + +- **Singleton** - 每个 Spring IoC 容器仅有一个单实例。 +- **Prototype** - 每次请求都会产生一个新的实例。 +- **Request** - 每一次 HTTP 请求都会产生一个新的实例,并且该 bean 仅在当前 HTTP 请求内有效。 +- **Session** - 每一次 HTTP 请求都会产生一个新的 bean,同时该 bean 仅在当前 HTTP session 内有效。 +- **Global-session** - 类似于标准的 HTTP Session 作用域,不过它仅仅在基于 portlet 的 web 应用中才有意义。Portlet 规范定义了全局 Session 的概念,它被所有构成某个 portlet web 应用的各种不同的 portlet 所共享。在 global session 作用域中定义的 bean 被限定于全局 portlet Session 的生命周期范围内。如果你在 web 中使用 global session 作用域来标识 bean,那么 web 会自动当成 session 类型来使用。 + +仅当用户使用支持 Web 的 ApplicationContext 时,最后三个才可用。 + +#### Spring Bean 的生命周期 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20211201102734.png) + +spring bean 容器的生命周期如下: + +1. Spring 对 Bean 进行实例化(相当于 new XXX()) + +2. Spring 将值和引用注入到 Bean 对应的属性中 + +3. 如果 Bean 实现了 `BeanNameAware` 接口,Spring 将 Bean 的 ID 传递给 `setBeanName` 方法 + - 作用是通过 Bean 的引用来获得 Bean ID,一般业务中是很少有用到 Bean 的 ID 的 +4. 如果 Bean 实现了 `BeanFactoryAware` 接口,Spring 将调用 `setBeanDactory` 方法,并把 `BeanFactory` 容器实例作为参数传入。 + - 作用是获取 Spring 容器,如 Bean 通过 Spring 容器发布事件等 +5. 如果 Bean 实现了 `ApplicationContextAware` 接口,Spring 容器将调用 `setApplicationContext` 方法,把应用上下文作为参数传入 + - 作用与 `BeanFactory` 类似都是为了获取 Spring 容器,不同的是 Spring 容器在调用 `setApplicationContext` 方法时会把它自己作为 `setApplicationContext` 的参数传入,而 Spring 容器在调用 `setBeanFactory` 前需要使用者自己指定(注入)`setBeanFactory` 里的参数 `BeanFactory` +6. 如果 Bean 实现了 `BeanPostProcess` 接口,Spring 将调用 `postProcessBeforeInitialization` 方法 + - 作用是在 Bean 实例创建成功后对其进行增强处理,如对 Bean 进行修改,增加某个功能 +7. 如果 Bean 实现了 `InitializingBean` 接口,Spring 将调用 `afterPropertiesSet` 方法,作用与在配置文件中对 Bean 使用 `init-method` 声明初始化的作用一样,都是在 Bean 的全部属性设置成功后执行的初始化方法。 +8. 如果 Bean 实现了 `BeanPostProcess` 接口,Spring 将调用 `postProcessAfterInitialization` 方法 + - `postProcessBeforeInitialization` 是在 Bean 初始化前执行的,而 `postProcessAfterInitialization` 是在 Bean 初始化后执行的 +9. 经过以上的工作后,Bean 将一直驻留在应用上下文中给应用使用,直到应用上下文被销毁 +10. 如果 Bean 实现了 `DispostbleBean` 接口,Spring 将调用它的 `destory` 方法,作用与在配置文件中对 Bean 使用 `destory-method` 属性的作用一样,都是在 Bean 实例销毁前执行的方法。 + +#### 什么是 spring 的内部 bean? + +只有将 bean 用作另一个 bean 的属性时,才能将 bean 声明为内部 bean。为了定义 bean,Spring 的基于 XML 的配置元数据在 `` 或 `` 中提供了 `` 元素的使用。内部 bean 总是匿名的,它们总是作为原型。 + +例如,假设我们有一个 Student 类,其中引用了 Person 类。这里我们将只创建一个 Person 类实例并在 Student 中使用它。 + +Student.java + +```java +public class Student { + private Person person; + //Setters and Getters +} +public class Person { + private String name; + private String address; + //Setters and Getters +} +``` + +bean.xml + +```xml + + + + + + + + + +``` + +#### 什么是 spring 装配 + +当 bean 在 Spring 容器中组合在一起时,它被称为装配或 bean 装配。 Spring 容器需要知道需要什么 bean 以及容器应该如何使用依赖注入来将 bean 绑定在一起,同时装配 bean。 + +#### 自动装配有哪些方式? + +Spring 容器能够自动装配 bean。也就是说,可以通过检查 BeanFactory 的内容让 Spring 自动解析 bean 的协作者。 + +自动装配的不同模式: + +- **no** - 这是默认设置,表示没有自动装配。应使用显式 bean 引用进行装配。 +- **byName** - 它根据 bean 的名称注入对象依赖项。它匹配并装配其属性与 XML 文件中由相同名称定义的 bean。 +- **byType** - 它根据类型注入对象依赖项。如果属性的类型与 XML 文件中的一个 bean 名称匹配,则匹配并装配属性。 +- **构造器** - 它通过调用类的构造器来注入依赖项。它有大量的参数。 +- **autodetect** - 首先容器尝试通过构造器使用 autowire 装配,如果不能,则尝试通过 byType 自动装配。 + +#### 自动装配有什么局限? + +- 覆盖的可能性 - 您始终可以使用 `` 和 `` 设置指定依赖项,这将覆盖自动装配。 +- 基本元数据类型 - 简单属性(如原数据类型,字符串和类)无法自动装配。 +- 令人困惑的性质 - 总是喜欢使用明确的装配,因为自动装配不太精确。 + +### AOP + +#### 什么是 AOP? + +AOP(Aspect-Oriented Programming), 即 **面向切面编程**, 它与 OOP( Object-Oriented Programming, 面向对象编程) 相辅相成, 提供了与 OOP 不同的抽象软件结构的视角. +在 OOP 中, 我们以类(class)作为我们的基本单元, 而 AOP 中的基本单元是 **Aspect(切面)** + +#### AOP 中的 Aspect、Advice、Pointcut、JointPoint 和 Advice 参数分别是什么? + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/core/spring-aop.png) + +- **Aspect** - Aspect 是一个实现交叉问题的类,例如事务管理。方面可以是配置的普通类,然后在 Spring Bean 配置文件中配置,或者我们可以使用 Spring AspectJ 支持使用 @Aspect 注解将类声明为 Aspect。 +- **Advice** - Advice 是针对特定 JoinPoint 采取的操作。在编程方面,它们是在应用程序中达到具有匹配切入点的特定 JoinPoint 时执行的方法。您可以将 Advice 视为 Spring 拦截器(Interceptor)或 Servlet 过滤器(filter)。 +- **Advice Arguments** - 我们可以在 advice 方法中传递参数。我们可以在切入点中使用 args() 表达式来应用于与参数模式匹配的任何方法。如果我们使用它,那么我们需要在确定参数类型的 advice 方法中使用相同的名称。 +- **Pointcut** - Pointcut 是与 JoinPoint 匹配的正则表达式,用于确定是否需要执行 Advice。 Pointcut 使用与 JoinPoint 匹配的不同类型的表达式。Spring 框架使用 AspectJ Pointcut 表达式语言来确定将应用通知方法的 JoinPoint。 +- **JoinPoint** - JoinPoint 是应用程序中的特定点,例如方法执行,异常处理,更改对象变量值等。在 Spring AOP 中,JoinPoint 始终是方法的执行器。 + +#### 什么是通知(Advice)? + +特定 JoinPoint 处的 Aspect 所采取的动作称为 Advice。Spring AOP 使用一个 Advice 作为拦截器,在 JoinPoint “周围”维护一系列的拦截器。 + +#### 有哪些类型的通知(Advice)? + +- **Before** - 这些类型的 Advice 在 joinpoint 方法之前执行,并使用 @Before 注解标记进行配置。 +- **After Returning** - 这些类型的 Advice 在连接点方法正常执行后执行,并使用@AfterReturning 注解标记进行配置。 +- **After Throwing** - 这些类型的 Advice 仅在 joinpoint 方法通过抛出异常退出并使用 @AfterThrowing 注解标记配置时执行。 +- **After (finally)** - 这些类型的 Advice 在连接点方法之后执行,无论方法退出是正常还是异常返回,并使用 @After 注解标记进行配置。 +- **Around** - 这些类型的 Advice 在连接点之前和之后执行,并使用 @Around 注解标记进行配置。 + +#### 指出在 spring aop 中 concern 和 cross-cutting concern 的不同之处。 + +concern 是我们想要在应用程序的特定模块中定义的行为。它可以定义为我们想要实现的功能。 + +cross-cutting concern 是一个适用于整个应用的行为,这会影响整个应用程序。例如,日志记录,安全性和数据传输是应用程序几乎每个模块都需要关注的问题,因此它们是跨领域的问题。 + +#### AOP 有哪些实现方式? + +实现 AOP 的技术,主要分为两大类: + +- 静态代理 - 指使用 AOP 框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强; + - 编译时编织(特殊编译器实现) + - 类加载时编织(特殊的类加载器实现)。 +- 动态代理 - 在运行时在内存中“临时”生成 AOP 动态代理类,因此也被称为运行时增强。 + - JDK 动态代理 + - CGLIB + +#### Spring AOP and AspectJ AOP 有什么区别? + +Spring AOP 基于动态代理方式实现;AspectJ 基于静态代理方式实现。 +Spring AOP 仅支持方法级别的 PointCut;提供了完全的 AOP 支持,它还支持属性级别的 PointCut。 + +#### 如何理解 Spring 中的代理? + +将 Advice 应用于目标对象后创建的对象称为代理。在客户端对象的情况下,目标对象和代理对象是相同的。 + +``` +Advice + Target Object = Proxy +``` + +#### 什么是编织(Weaving)? + +为了创建一个 advice 对象而链接一个 aspect 和其它应用类型或对象,称为编织(Weaving)。在 Spring AOP 中,编织在运行时执行。请参考下图: + +![img](https://upload-images.jianshu.io/upload_images/3101171-cfaa92f0e4115b4a.png) + +## 注解 + +### 你用过哪些重要的 Spring 注解? + +- **@Controller** - 用于 Spring MVC 项目中的控制器类。 +- **@Service** - 用于服务类。 +- **@RequestMapping** - 用于在控制器处理程序方法中配置 URI 映射。 +- **@ResponseBody** - 用于发送 Object 作为响应,通常用于发送 XML 或 JSON 数据作为响应。 +- **@PathVariable** - 用于将动态值从 URI 映射到处理程序方法参数。 +- **@Autowired** - 用于在 spring bean 中自动装配依赖项。 +- **@Qualifier** - 使用 @Autowired 注解,以避免在存在多个 bean 类型实例时出现混淆。 +- **@Scope** - 用于配置 spring bean 的范围。 +- **@Configuration**,**@ComponentScan** 和 **@Bean** - 用于基于 java 的配置。 +- **@Aspect**,**@Before**,**@After**,**@Around**,**@Pointcut** - 用于切面编程(AOP)。 + +### 如何在 spring 中启动注解装配? + +默认情况下,Spring 容器中未打开注解装配。因此,要使用基于注解装配,我们必须通过配置`` 元素在 Spring 配置文件中启用它。 + +### @Component, @Controller, @Repository, @Service 有何区别? + +- @Component:这将 java 类标记为 bean。它是任何 Spring 管理组件的通用构造型。spring 的组件扫描机制现在可以将其拾取并将其拉入应用程序环境中。 +- @Controller:这将一个类标记为 Spring Web MVC 控制器。标有它的 Bean 会自动导入到 IoC 容器中。 +- @Service:此注解是组件注解的特化。它不会对 @Component 注解提供任何其他行为。您可以在服务层类中使用 @Service 而不是 @Component,因为它以更好的方式指定了意图。 +- @Repository:这个注解是具有类似用途和功能的 @Component 注解的特化。它为 DAO 提供了额外的好处。它将 DAO 导入 IoC 容器,并使未经检查的异常有资格转换为 Spring DataAccessException。 + +### @Required 注解有什么用? + +@Required 应用于 bean 属性 setter 方法。此注解仅指示必须在配置时使用 bean 定义中的显式属性值或使用自动装配填充受影响的 bean 属性。如果尚未填充受影响的 bean 属性,则容器将抛出 BeanInitializationException。 + +示例: + +```java +public class Employee { + private String name; + @Required + public void setName(String name){ + this.name=name; + } + public string getName(){ + return name; + } +} +``` + +### @Autowired 注解有什么用? + +@Autowired 可以更准确地控制应该在何处以及如何进行自动装配。此注解用于在 setter 方法,构造器,具有任意名称或多个参数的属性或方法上自动装配 bean。默认情况下,它是类型驱动的注入。 + +```java +public class Employee { + private String name; + @Autowired + public void setName(String name) { + this.name=name; + } + public string getName(){ + return name; + } +} +``` + +### @Qualifier 注解有什么用? + +当您创建多个相同类型的 bean 并希望仅使用属性装配其中一个 bean 时,您可以使用@Qualifier 注解和 @Autowired 通过指定应该装配哪个确切的 bean 来消除歧义。 + +例如,这里我们分别有两个类,Employee 和 EmpAccount。在 EmpAccount 中,使用@Qualifier 指定了必须装配 id 为 emp1 的 bean。 + +Employee.java + +```java +public class Employee { + private String name; + @Autowired + public void setName(String name) { + this.name=name; + } + public string getName() { + return name; + } +} +``` + +EmpAccount.java + +```java +public class EmpAccount { + private Employee emp; + + @Autowired + @Qualifier(emp1) + public void showName() { + System.out.println(“Employee name : ”+emp.getName); + } +} +``` + +### @RequestMapping 注解有什么用? + +@RequestMapping 注解用于将特定 HTTP 请求方法映射到将处理相应请求的控制器中的特定类/方法。此注解可应用于两个级别: + +- 类级别:映射请求的 URL +- 方法级别:映射 URL 以及 HTTP 请求方法 + +## 数据篇 + +### spring DAO 有什么用? + +Spring DAO 使得 JDBC,Hibernate 或 JDO 这样的数据访问技术更容易以一种统一的方式工作。这使得用户容易在持久性技术之间切换。它还允许您在编写代码时,无需考虑捕获每种技术不同的异常。 + +### 列举 Spring DAO 抛出的异常。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/data-access/spring-data-access-exception.png) + +### spring JDBC API 中存在哪些类? + +- JdbcTemplate +- SimpleJdbcTemplate +- NamedParameterJdbcTemplate +- SimpleJdbcInsert +- SimpleJdbcCall + +### 使用 Spring 访问 Hibernate 的方法有哪些? + +我们可以通过两种方式使用 Spring 访问 Hibernate: + +1. 使用 Hibernate 模板和回调进行控制反转 +2. 扩展 HibernateDAOSupport 并应用 AOP 拦截器节点 + +### 列举 spring 支持的事务管理类型 + +Spring 支持两种类型的事务管理: + +1. 程序化事务管理:在此过程中,在编程的帮助下管理事务。它为您提供极大的灵活性,但维护起来非常困难。 +2. 声明式事务管理:在此,事务管理与业务代码分离。仅使用注解或基于 XML 的配置来管理事务。 + +### spring 支持哪些 ORM 框架 + +- Hibernate +- iBatis +- JPA +- JDO +- OJB + +## MVC + +### Spring MVC 框架有什么用? + +Spring Web MVC 框架提供 **模型-视图-控制器** 架构和随时可用的组件,用于开发灵活且松散耦合的 Web 应用程序。 MVC 模式有助于分离应用程序的不同方面,如输入逻辑,业务逻辑和 UI 逻辑,同时在所有这些元素之间提供松散耦合。 + +### 描述一下 DispatcherServlet 的工作流程 + +DispatcherServlet 的工作流程可以用一幅图来说明: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/web/spring-dispatcher-servlet.png) + +1. 向服务器发送 HTTP 请求,请求被前端控制器 `DispatcherServlet` 捕获。 +2. `DispatcherServlet` 根据 **`-servlet.xml`** 中的配置对请求的 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用 `HandlerMapping` 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以`HandlerExecutionChain` 对象的形式返回。 +3. `DispatcherServlet` 根据获得的`Handler`,选择一个合适的 `HandlerAdapter`。(附注:如果成功获得`HandlerAdapter`后,此时将开始执行拦截器的 preHandler(...)方法)。 +4. 提取`Request`中的模型数据,填充`Handler`入参,开始执行`Handler`(`Controller`)。 在填充`Handler`的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作: + - HttpMessageConveter: 将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息。 + - 数据转换:对请求消息进行数据转换。如`String`转换成`Integer`、`Double`等。 + - 数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等。 + - 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到`BindingResult`或`Error`中。 +5. Handler(Controller)执行完成后,向 `DispatcherServlet` 返回一个 `ModelAndView` 对象; +6. 根据返回的`ModelAndView`,选择一个适合的 `ViewResolver`(必须是已经注册到 Spring 容器中的`ViewResolver`)返回给`DispatcherServlet`。 +7. `ViewResolver` 结合`Model`和`View`,来渲染视图。 +8. 视图负责将渲染结果返回给客户端。 + +### 介绍一下 WebApplicationContext + +WebApplicationContext 是 ApplicationContext 的扩展。它具有 Web 应用程序所需的一些额外功能。它与普通的 ApplicationContext 在解析主题和决定与哪个 servlet 关联的能力方面有所不同。 + +(完) + +--- + +:point_right: 想学习更多 Spring 内容可以访问我的 Spring 教程:**[spring-notes](https://github.com/dunwu/spring-notes)** + +## 资料 + +- [Top 50 Spring Interview Questions You Must Prepare In 2018](https://www.edureka.co/blog/interview-questions/spring-interview-questions/) +- [Spring Interview Questions and Answers](https://www.journaldev.com/2696/spring-interview-questions-and-answers) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/README.md" new file mode 100644 index 00000000..cb55bf58 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/README.md" @@ -0,0 +1,43 @@ +--- +title: Spring 综述 +date: 2020-02-26 23:48:06 +categories: + - Java + - 框架 + - Spring + - Spring综合 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/9e0b67/ +hidden: true +index: false +--- + +# Spring 综述 + +## 📖 内容 + +- [Spring 概述](01.Spring概述.md) +- [SpringBoot 知识图谱](21.SpringBoot知识图谱.md) +- [SpringBoot 基本原理](22.SpringBoot基本原理.md) +- [Spring 面试](99.Spring面试.md) + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/01.SpringBean.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/01.SpringBean.md" new file mode 100644 index 00000000..1df26d7b --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/01.SpringBean.md" @@ -0,0 +1,214 @@ +--- +title: Spring Bean +date: 2021-12-10 19:15:42 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Bean + - BeanDefinition +permalink: /pages/68097d/ +--- + +# Spring Bean + +在 Spring 中,构成应用程序主体由 Spring IoC 容器管理的对象称为 Bean。**Bean 是由 Spring IoC 容器实例化、装配和管理的对象**。 Bean 以及它们之间的依赖关系反映在容器使用的配置元数据中。 + +## Spring Bean 定义 + +### BeanDefinition + +Spring IoC 容器本身,并不能识别配置的元数据。为此,要将这些配置信息转为 Spring 能识别的格式——`BeanDefinition` 对象。 + +**`BeanDefinition` 是 Spring 中定义 Bean 的配置元信息接口**,它包含: + +- Bean 类名 +- Bean 行为配置元素,如:作用域、自动绑定的模式、生命周期回调等 +- 其他 Bean 引用,也可称为合作者(Collaborators)或依赖(Dependencies) +- 配置设置,如 Bean 属性(Properties) + +#### BeanDefinition 元信息 + +`BeanDefinition` 元信息如下: + +| 属性(Property) | 说明 | +| ----------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | +| [Class](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-class) | 全类名,必须是具体类,不能用抽象类或接口 | +| [Name](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-beanname) | Bean 的名称或者 ID | +| [Scope](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes) | Bean 的作用域(如:`singleton`、`prototype` 等) | +| [Constructor arguments](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-collaborators) | Bean 构造器参数(用于依赖注入) | +| [Properties](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-collaborators) | Bean 属性设置(用于依赖注入) | +| [Autowiring mode](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-autowire) | Bean 自动绑定模式(如:通过名称 byName) | +| [Lazy initialization mode](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-lazy-init) | Bean 延迟初始化模式(延迟和非延迟) | +| [Initialization method](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-lifecycle-initializingbean) | Bean 初始化回调方法名称 | +| [Destruction method](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-lifecycle-disposablebean) | Bean 销毁回调方法名称 | + +#### BeanDefinition 构建 + +BeanDefinition 构建方式: + +- 通过 `BeanDefinitionBuilder` + +- 通过 `AbstractBeanDefinition` 以及派生类 + +> 💻 Spring Bean 定义示例源码:[BeanDefinitionTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanDefinitionTests.java) + +### Spring Bean 命名 + +#### Spring Bean 命名规则 + +每个 Bean 拥有一个或多个标识符(identifiers),这些标识符在 Bean 所在的容器必须是唯一的。通常,一个 Bean 仅有一个标识符,如果需要额外的,可考虑使用别名(Alias)来扩充。 + +在基于 XML 的配置元信息中,开发人员**可以使用 `id` 属性、`name` 属性或来指定 Bean 标识符**。通常,Bean 的标识符由字母组成,允许出现特殊字符。如果要想引入 Bean 的别名的话,可在 `name` 属性使用半角逗号(“,”)或分号(“;”) 来间隔。 + +Spring 中,**为 Bean 指定 `id` 和 `name` 属性不是必须的**。如果不指定,Spring 会自动为 Bean 分配一个唯一的名称。尽管 Bean 的命名没有限制,不过**官方建议采用驼峰命名法来命名 Bean**。 + +#### Spring Bean 命名生成器 + +Spring 提供了两种 Spring Bean 命名生成器: + +- `DefaultBeanNameGenerator`:默认通用 `BeanNameGenerator` 实现。 +- `AnnotationBeanNameGenerator`:基于注解扫描的 `BeanNameGenerator` 实现。 + +```java +public interface BeanNameGenerator { + String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry); +} +``` + +#### Spring Bean 别名 + +Spring 支持通过 `` 属性为 Bean 设置别名。 + +Bean 别名(Alias)的作用: + +- 复用现有的 `BeanDefinition` +- 更具有场景化的命名方法,比如: + - `` + - `` + +```xml + + + + +``` + +## Spring Bean 生命周期 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20211201102734.png) + +1. Spring 对 Bean 进行实例化(相当于 new XXX()) + +2. Spring 将值和引用注入到 Bean 对应的属性中 + +3. 如果 Bean 实现了 `BeanNameAware` 接口,Spring 将 Bean 的 ID 传递给 `setBeanName` 方法 + - 作用是通过 Bean 的引用来获得 Bean ID,一般业务中是很少有用到 Bean 的 ID 的 +4. 如果 Bean 实现了 `BeanFactoryAware` 接口,Spring 将调用 `setBeanDactory` 方法,并把 `BeanFactory` 容器实例作为参数传入。 + - 作用是获取 Spring 容器,如 Bean 通过 Spring 容器发布事件等 +5. 如果 Bean 实现了 `ApplicationContextAware` 接口,Spring 容器将调用 `setApplicationContext` 方法,把应用上下文作为参数传入 + - 作用与 `BeanFactory` 类似都是为了获取 Spring 容器,不同的是 Spring 容器在调用 `setApplicationContext` 方法时会把它自己作为 `setApplicationContext` 的参数传入,而 Spring 容器在调用 `setBeanFactory` 前需要使用者自己指定(注入)`setBeanFactory` 里的参数 `BeanFactory` +6. 如果 Bean 实现了 `BeanPostProcess` 接口,Spring 将调用 `postProcessBeforeInitialization` 方法 + - 作用是在 Bean 实例创建成功后对其进行增强处理,如对 Bean 进行修改,增加某个功能 +7. 如果 Bean 实现了 `InitializingBean` 接口,Spring 将调用 `afterPropertiesSet` 方法,作用与在配置文件中对 Bean 使用 `init-method` 声明初始化的作用一样,都是在 Bean 的全部属性设置成功后执行的初始化方法。 +8. 如果 Bean 实现了 `BeanPostProcess` 接口,Spring 将调用 `postProcessAfterInitialization` 方法 + - `postProcessBeforeInitialization` 是在 Bean 初始化前执行的,而 `postProcessAfterInitialization` 是在 Bean 初始化后执行的 +9. 经过以上的工作后,Bean 将一直驻留在应用上下文中给应用使用,直到应用上下文被销毁 +10. 如果 Bean 实现了 `DispostbleBean` 接口,Spring 将调用它的 `destory` 方法,作用与在配置文件中对 Bean 使用 `destory-method` 属性的作用一样,都是在 Bean 实例销毁前执行的方法。 + +## Spring Bean 注册 + +注册 Spring Bean 实际上是将 `BeanDefinition` 注册到 IoC 容器中。 + +### XML 配置元信息 + +Spring 的传统配置方式。在 `` 标签中配置元数据内容。 + +缺点是当 JavaBean 过多时,产生的配置文件足以让你眼花缭乱。 + +### 注解配置元信息 + +使用 `@Bean`、`@Component`、`@Import` 注解注册 Spring Bean。 + +### Java API 配置元信息 + +- 命名方式:`BeanDefinitionRegistry#registerBeanDefinition(String,BeanDefinition)` +- 非命名方式:`BeanDefinitionReaderUtils#registerWithGeneratedName(AbstractBeanDefinition,BeanDefinitionRegistry)` +- 配置类方式:`AnnotatedBeanDefinitionReader#register(Class...)` + +> 💻 Spring Bean 注册示例源码:[BeanRegistryTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanRegistryTests.java) + +## Spring Bean 实例化 + +Spring Bean 实例化方式: + +- 常规方式 + - 通过构造器(配置元信息:XML、Java 注解和 Java API) + - 通过静态方法(配置元信息:XML、Java 注解和 Java API) + - 通过 Bean 工厂方法(配置元信息:XML、Java 注解和 Java API) + - 通过 `FactoryBean`(配置元信息:XML、Java 注解和 Java API) +- 特殊方式 + - 通过 `ServiceLoaderFactoryBean`(配置元信息:XML、Java 注解和 Java API ) + - 通过 `AutowireCapableBeanFactory#createBean(java.lang.Class, int, boolean)` + - 通过 `BeanDefinitionRegistry#registerBeanDefinition(String,BeanDefinition)` + +> 💻 Spring Bean 实例化示例源码:[BeanInstantiationTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanInstantiationTests.java)、[BeanInstantiationSpecialTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanInstantiationSpecialTests.java) + +## Spring Bean 初始化和销毁 + +Spring Bean 初始化和销毁的方式有以下几种: + +1. 使用 `@PostConstruct` 和 `@PreDestroy` 注解分别指定相应的初始化方法和销毁方法。 +2. 实现 `InitializingBean` 接口的 `afterPropertiesSet()` 方法来编写初始化方法;实现 `DisposableBean` 接口的 `destroy()` 方法来编写销毁方法。 + + - `InitializingBean` 接口包含一个 `afterPropertiesSet` 方法,可以通过实现该接口,然后在这个方法中编写初始化逻辑。 + - `DisposableBean`接口包含一个 `destory` 方法,可以通过实现该接口,然后在这个方法中编写销毁逻辑。 + +3. 自定义初始化方法 + - XML 配置:`` + - Java 注解:`@Bean(initMethod = "init", destroyMethod = "destroy")` + - Java API:`AbstractBeanDefinition#setInitMethodName(String)` 和 `AbstractBeanDefinition#setDestroyMethodName(String)` 分别定义初始化和销毁方法 + +注意:如果同时存在,执行顺序会按照序列执行。 + +Bean 的延迟初始化 + +- xml 方式:`` +- 注解方式:`@Lazy` + +Spring 提供了一个 `BeanPostProcessor` 接口,提供了两个方法 `postProcessBeforeInitialization` 和 `postProcessAfterInitialization`。其中`postProcessBeforeInitialization` 在组件的初始化方法调用之前执行,`postProcessAfterInitialization` 在组件的初始化方法调用之后执行。它们都包含两个入参: + +- `bean`:当前组件对象; +- `beanName`:当前组件在容器中的名称。 + +> 💻 Spring Bean 初始化和销毁示例源码:[BeanInitDestroyTests](https://github.com/dunwu/spring-tutorial/blob/master/codes/core/spring-core-ioc/src/test/java/io/github/dunwu/spring/core/bean/BeanInitDestroyTests.java) + +## Spring Bean 垃圾回收 + +Spring Bean 垃圾回收步骤: + +1. 关闭 Spring 容器(应用上下文) +2. 执行 GC +3. Spring Bean 覆盖的 `finalize()` 方法被回调 + +## Spring Bean 作用范围 + +| Scope | Description | +| :---------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [singleton](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-singleton) | (Default) Scopes a single bean definition to a single object instance for each Spring IoC container. | +| [prototype](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-prototype) | Scopes a single bean definition to any number of object instances. | +| [request](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-request) | Scopes a single bean definition to the lifecycle of a single HTTP request. That is, each HTTP request has its own instance of a bean created off the back of a single bean definition. Only valid in the context of a web-aware Spring `ApplicationContext`. | +| [session](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-session) | Scopes a single bean definition to the lifecycle of an HTTP `Session`. Only valid in the context of a web-aware Spring `ApplicationContext`. | +| [application](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-scopes-application) | Scopes a single bean definition to the lifecycle of a `ServletContext`. Only valid in the context of a web-aware Spring `ApplicationContext`. | +| [websocket](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket-stomp-websocket-scope) | Scopes a single bean definition to the lifecycle of a `WebSocket`. Only valid in the context of a web-aware Spring `ApplicationContext`. | + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/02.SpringIoC.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/02.SpringIoC.md" new file mode 100644 index 00000000..505172fb --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/02.SpringIoC.md" @@ -0,0 +1,921 @@ +--- +title: Spring IoC +date: 2020-08-30 16:06:10 +order: 02 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - IOC +permalink: /pages/915530/ +--- + +# Spring IoC + +## IoC 简介 + +### IoC 是什么 + +**IoC** 即**控制反转**(Inversion of Control,缩写为 IoC)。IoC 又称为**依赖倒置原则**(设计模式六大原则之一),它的要点在于:**程序要依赖于抽象接口,不要依赖于具体实现**。它的作用就是**用于降低代码间的耦合度**。 + +IoC 的实现方式有两种: + +- **依赖注入**(Dependency Injection,简称 DI):不通过 `new()` 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。 +- **依赖查找**(Dependency Lookup):容器中的受控对象通过容器的 API 来查找自己所依赖的资源和协作对象。 + +理解 Ioc 的关键是要明确两个要点: + +- **谁控制谁,控制什么**:传统 Java SE 程序设计,我们直接在对象内部通过 new 进行创建对象,是程序主动去创建依赖对象;而 IoC 是有专门一个容器来创建这些对象,即由 Ioc 容器来控制对象的创建;谁控制谁?当然是 IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。 +- **为何是反转,哪些方面反转了**:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221006120112.png) + +### IoC 能做什么 + +IoC 不是一种技术,而是编程思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了 IoC 容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。 + +其实 IoC 对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在 IoC/DI 思想中,应用程序就变成被动的了,被动的等待 IoC 容器来创建并注入它所需要的资源了。 + +IoC 很好的体现了面向对象设计法则之一—— **好莱坞法则:“别找我们,我们找你”**;即由 IoC 容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。 + +### IoC 和 DI + +其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以 2004 年大师级人物 Martin Fowler 又给出了一个新的名字:“依赖注入”,相对 IoC 而言,“依赖注入”明确描述了“被注入对象依赖 IoC 容器配置依赖对象”。 + +> 注:如果想要更加深入的了解 IoC 和 DI,请参考大师级人物 Martin Fowler 的一篇经典文章 [Inversion of Control Containers and the Dependency Injection pattern](http://www.martinfowler.com/articles/injection.html) 。 + +### IoC 容器 + +IoC 容器就是具有依赖注入功能的容器。IoC 容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。应用程序无需直接在代码中 new 相关的对象,应用程序由 IoC 容器进行组装。在 Spring 中 BeanFactory 是 IoC 容器的实际代表者。 + +Spring IoC 容器如何知道哪些是它管理的对象呢?这就需要配置文件,Spring IoC 容器通过读取配置文件中的配置元数据,通过元数据对应用中的各个对象进行实例化及装配。一般使用基于 xml 配置文件进行配置元数据,而且 Spring 与配置文件完全解耦的,可以使用其他任何可能的方式进行配置元数据,比如注解、基于 java 文件的、基于属性文件的配置都可以。 + +### Bean + +> **JavaBean** 是一种 JAVA 语言写成的可重用组件。为写成 JavaBean,类必须是具体的和公共的,并且具有无参数的构造器。JavaBean 对外部通过提供 getter / setter 方法来访问其成员。 + +由 IoC 容器管理的那些组成你应用程序的对象我们就叫它 Bean。Bean 就是由 Spring 容器初始化、装配及管理的对象,除此之外,bean 就与应用程序中的其他对象没有什么区别了。那 IoC 怎样确定如何实例化 Bean、管理 Bean 之间的依赖关系以及管理 Bean 呢?这就需要配置元数据,在 Spring 中由 BeanDefinition 代表,后边会详细介绍,配置元数据指定如何实例化 Bean、如何组装 Bean 等。 + +### Spring IoC + +Spring IoC 容器中的对象仅通过构造函数参数、工厂方法的参数或在对象实例被构造或从工厂方法返回后设置的属性来定义它们的依赖关系(即与它们一起工作的其他对象)。然后容器在创建 bean 时注入这些依赖项。这个过程基本上是 bean 本身通过使用类的直接构造或诸如服务定位器模式之类的机制来控制其依赖关系的实例化或位置的逆过程(因此称为控制反转)。 + +`org.springframework.beans` 和 `org.springframework.context` 是 IoC 容器的基础。 + +## IoC 容器 + +在 Spring 中,有两种 IoC 容器:`BeanFactory` 和 `ApplicationContext`。 + +- `BeanFactory`:**`BeanFactory` 是 Spring 基础 IoC 容器**。`BeanFactory` 提供了 Spring 容器的配置框架和基本功能。 +- `ApplicationContext`:**`ApplicationContext` 是具备应用特性的 `BeanFactory` 的子接口**。它还扩展了其他一些接口,以支持更丰富的功能,如:国际化、访问资源、事件机制、更方便的支持 AOP、在 web 应用中指定应用层上下文等。 + +实际开发中,更推荐使用 `ApplicationContext` 作为 IoC 容器,因为它的功能远多于 `BeanFactory`。 + +`org.springframework.context.ApplicationContext` 接口代表 Spring IoC 容器,负责实例化、配置和组装 bean。容器通过读取配置元数据来获取关于要实例化、配置和组装哪些对象的指令。配置元数据以 XML、Java 注释或 Java 代码表示。它允许您表达组成应用程序的对象以及这些对象之间丰富的相互依赖关系。 + +Spring 提供了 `ApplicationContext` 接口的几个实现,例如: + +- **[ClassPathXmlApplicationContext](https://docs.spring.io/spring-framework/docs/5.3.23/javadoc-api/org/springframework/context/support/ClassPathXmlApplicationContext.html)**:`ApplicationContext` 的实现,从 classpath 获取配置信息。 + +```java +BeanFactory beanFactory = new ClassPathXmlApplicationContext("classpath.xml"); +``` + +- **[FileSystemXmlApplicationContext](https://docs.spring.io/spring-framework/docs/5.3.23/javadoc-api/org/springframework/context/support/FileSystemXmlApplicationContext.html)**:`ApplicationContext` 的实现,从文件系统获取配置信息。 + +```java +BeanFactory beanFactory = new FileSystemXmlApplicationContext("fileSystemConfig.xml"); +``` + +在大多数应用场景中,不需要显式通过用户代码来实例化 Spring IoC 容器的一个或多个实例。 + +下图显示了 Spring IoC 容器的工作步骤 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200723102456.png) + +使用 IoC 容器可分为三步骤: + +1. **配置元数据**:需要配置一些元数据来告诉 Spring,你希望容器如何工作,具体来说,就是如何去初始化、配置、管理 JavaBean 对象。 +2. **实例化容器**:由 IoC 容器解析配置的元数据。IoC 容器的 Bean Reader 读取并解析配置文件,根据定义生成 BeanDefinition 配置元数据对象,IoC 容器根据 `BeanDefinition` 进行实例化、配置及组装 Bean。 +3. **使用容器**:由客户端实例化容器,获取需要的 Bean。 + +### 配置元数据 + +**元数据(Metadata)**又称中介数据、中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息。 + +配置元数据的方式: + +- **基于 xml 配置**:Spring 的传统配置方式。通常是在顶级元素 `` 中通过 ``元素配置元数据。这种方式的缺点是:如果 JavaBean 过多,则产生的配置文件足以让你眼花缭乱。 +- **[基于注解配置](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-annotation-config)**:Spring 2.5 引入了对基于注解的配置元数据的支持。可以大大简化你的配置。 +- **[基于 Java 配置](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-java)**:从 Spring 3.0 开始,Spring 支持使用 Java 代码来配置元数据。通常是在 `@Configuration` 修饰的类中通过 `@Bean` 指定实例化 Bean 的方法。更多详情,可以参阅 [`@Configuration`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Configuration.html)、[`@Bean`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Bean.html)、[`@Import`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/Import.html) 和 [`@DependsOn`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/DependsOn.html) 注释。 + +这些 bean 定义对应于构成应用程序的实际对象。例如:定义服务层对象、数据访问对象 (DAO)、表示对象(如 Struts Action 实例)、基础设施对象(如 Hibernate SessionFactories、JMS 队列等)。通常,不会在容器中配置细粒度的域对象,因为创建和加载域对象通常是 DAO 和业务逻辑的责任。但是,可以使用 Spring 与 AspectJ 的集成来配置在 IoC 容器控制之外创建的对象。 + +以下示例显示了基于 XML 的配置元数据的基本结构: + +```xml + + + + + + + + + + + + + + + + +``` + +### 实例化容器 + +可以通过为 `ApplicationContext` 的构造函数指定外部资源路径,来加载配置元数据。 + +```java +ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); +``` + +以下示例显示了服务层对象 (services.xml) 配置文件: + +```xml + + + + + + + + + + + + + + +``` + +以下示例显示了数据访问对象 (daos.xml) 配置文件: + +```xml + + + + + + + + + + + + + + +``` + +上面的示例中,服务层由 `PetStoreServiceImpl` 类和类型为 `JpaAccountDao` 和 `JpaItemDao` 的两个数据访问对象(基于 JPA 对象关系映射标准)组成。 `property name` 元素指的是 JavaBean 属性的名称,`ref` 元素指的是另一个 bean 定义的名称。 `id` 和 `ref` 元素之间的这种联系表达了协作对象之间的依赖关系。 + +**Spring 支持通过多个 xml 文件来定义 Bean,每个单独的 XML 配置文件都代表架构中的一个逻辑层或模块。可以使用 `ApplicationContext` 构造函数从所有这些 XML 片段加载 bean 定义。或者,使用 `` 元素从另一个或多个文件加载 bean 定义**。如下所示: + +```xml + + + + + + + + +``` + +在上面的示例中,外部 bean 定义从三个文件加载:`services.xml`、`messageSource.xml` 和 `themeSource.xml`。`services.xml` 文件必须和当前 xml 文件位于同一目录或类路径位置;而 `messageSource.xml` 和 `themeSource.xml` 必须位于当前文件所在目录的子目录 `resources` 下。`/resources` 的 `/` 会被忽略。但是,鉴于这些路径是相对的,最好不要使用 `/`。根据 Spring Schema,被导入文件的内容,包括顶级 `` 元素,必须是有效的 XML bean 定义。 + +> 注意: +> +> 可以,但不推荐使用相对 `“../”` 路径来引用父目录中的文件。这样做会创建对当前应用程序之外的文件的依赖。特别是,不建议将此引用用于 `classpath`:URL(例如, `classpath:../services.xml`),其中运行时解析过程会选择“最近的”类路径根,然后查看其父目录。类路径配置更改可能会导致选择不同的、不正确的目录。 +> +> 可以使用完全限定的资源位置而不是相对路径:例如,`file:C:/config/services.xml` 或 `classpath:/config/services.xml`。建议为此类绝对路径保留一定的间接性  —  例如,通过 `“${...}”` 占位符来引用运行时指定 的 JVM 参数。 + +命名空间本身提供了导入指令功能。 Spring 提供的一系列 XML 命名空间中提供了除了普通 bean 定义之外的更多配置特性  —  例如,`context` 和 `util` 命名空间。 + +### 使用容器 + +`ApplicationContext` 能够维护不同 bean 及其依赖项的注册表。通过**使用方法 `T getBean(String name, Class T requiredType)`,可以检索并获取 bean 的实例**。 + +`ApplicationContext` 允许读取 bean 定义并访问它们,如以下示例所示: + +```java +// create and configure beans +ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml"); + +// retrieve configured instance +PetStoreService service = context.getBean("petStore", PetStoreService.class); + +// use configured instance +List userList = service.getUsernameList(); +``` + +最灵活的变体是 `GenericApplicationContext` 结合阅读器委托  —  例如,结合 XML 文件的 `XmlBeanDefinitionReader`,如下例所示: + +```java +GenericApplicationContext context = new GenericApplicationContext(); +new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml"); +context.refresh(); +``` + +可以在同一个 `ApplicationContext` 上混合和匹配此类读取器委托,从不同的配置源读取 bean 定义。 + +然后,可以使用 `getBean` 检索 bean 的实例。 `ApplicationContext` 接口还有一些其他方法用于检索 bean,但理想情况下,应用程序代码不应该使用它们。实际上,应用程序代码根本不应该调用 `getBean()` 方法,因此根本不依赖 Spring API。例如,Spring 与 Web 框架的集成为各种 Web 框架组件(例如控制器和 JSF 管理的 bean)提供了依赖注入,让您可以通过元数据(例如自动装配注释)声明对特定 bean 的依赖。 + +## IoC 依赖来源 + +自定义 Bean + +容器内建 Bean 对象 + +容器内建依赖 + +## IoC 配置元数据 + +IoC 容器的配置有三种方式: + +- 基于 xml 配置 +- 基于 properties 配置 +- 基于注解配置 +- 基于 Java 配置 + +作为 Spring 传统的配置方式,xml 配置方式一般为大家所熟知。 + +如果厌倦了 xml 配置,Spring 也提供了注解配置方式或 Java 配置方式来简化配置。 + +**本文,将对 Java 配置 IoC 容器做详细的介绍。** + +### Xml 配置 + +```xml + + + + + + + + + + +``` + +标签说明: + +- `` 是 Spring 配置文件的根节点。 +- `` 用来定义一个 JavaBean。`id` 属性是它的标识,在文件中必须唯一;`class` 属性是它关联的类。 +- `` 用来定义 Bean 的别名。 +- `` 用来导入其他配置文件的 Bean 定义。这是为了加载多个配置文件,当然也可以把这些配置文件构造为一个数组(new String[] {“config1.xml”, config2.xml})传给 `ApplicationContext` 实现类进行加载多个配置文件,那一个更适合由用户决定;这两种方式都是通过调用 Bean Definition Reader 读取 Bean 定义,内部实现没有任何区别。`` 标签可以放在 `` 下的任何位置,没有顺序关系。 + +#### 实例化容器 + +实例化容器的过程: +定位资源(XML 配置文件) +读取配置信息(Resource) +转化为 Spring 可识别的数据形式(BeanDefinition) + +```java +ApplicationContext context = + new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"}); +``` + +组合 xml 配置文件 +配置的 Bean 功能各不相同,都放在一个 xml 文件中,不便管理。 +Java 设计模式讲究职责单一原则。配置其实也是如此,功能不同的 JavaBean 应该被组织在不同的 xml 文件中。然后使用 import 标签把它们统一导入。 + +```xml + + +``` + +#### 使用容器 + +使用容器的方式就是通过`getBean`获取 IoC 容器中的 JavaBean。 +Spring 也有其他方法去获得 JavaBean,但是 Spring 并不推荐其他方式。 + +```java +// create and configure beans +ApplicationContext context = +new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"}); +// retrieve configured instance +PetStoreService service = context.getBean("petStore", PetStoreService.class); +// use configured instance +List userList = service.getUsernameList(); +``` + +### 注解配置 + +Spring2.5 引入了注解。 +于是,一个问题产生了:**使用注解方式注入 JavaBean 是不是一定完爆 xml 方式?** +未必。正所谓,仁者见仁智者见智。任何事物都有其优缺点,看你如何取舍。来看看注解的优缺点: +**优点**:大大减少了配置,并且可以使配置更加精细——类,方法,字段都可以用注解去标记。 +**缺点**:使用注解,不可避免产生了侵入式编程,也产生了一些问题。 + +- 你需要将注解加入你的源码并编译它; + +- 注解往往比较分散,不易管控。 + +> 注:spring 中,先进行注解注入,然后才是 xml 注入,因此如果注入的目标相同,后者会覆盖前者。 + +#### 启动注解 + +Spring 默认是不启用注解的。如果想使用注解,需要先在 xml 中启动注解。 +启动方式:在 xml 中加入一个标签,很简单吧。 + +```xml + +``` + +> 注:`` 只会检索定义它的上下文。什么意思呢?就是说,如果你 +> 为 DispatcherServlet 指定了一个`WebApplicationContext`,那么它只在 controller 中查找`@Autowired`注解,而不会检查其它的路径。 + +#### `@Required` + +`@Required` 注解只能用于修饰 bean 属性的 setter 方法。受影响的 bean 属性必须在配置时被填充在 xml 配置文件中,否则容器将抛出`BeanInitializationException`。 + +```java +public class AnnotationRequired { + private String name; + private String sex; + + public String getName() { + return name; + } + + /** + * @Required 注解用于bean属性的setter方法并且它指示,受影响的bean属性必须在配置时被填充在xml配置文件中, + * 否则容器将抛出BeanInitializationException。 + */ + @Required + public void setName(String name) { + this.name = name; + } + + public String getSex() { + return sex; + } + + public void setSex(String sex) { + this.sex = sex; + } +} +``` + +#### `@Autowired` + +`@Autowired`注解可用于修饰属性、setter 方法、构造方法。 + +@Autowired 注入过程 + +- 元信息解析 +- 依赖查找 +- 依赖注入(字段、方法) + +> 注:`@Autowired`注解也可用于修饰构造方法,但如果类中只有默认构造方法,则没有必要。如果有多个构造器,至少应该修饰一个,来告诉容器哪一个必须使用。 + +可以使用 JSR330 的注解`@Inject`来替代`@Autowired`。 + +**_范例_** + +```java +public class AnnotationAutowired { + private static final Logger log = LoggerFactory.getLogger(AnnotationRequired.class); + + @Autowired + private Apple fieldA; + + private Banana fieldB; + + private Orange fieldC; + + public Apple getFieldA() { + return fieldA; + } + + public void setFieldA(Apple fieldA) { + this.fieldA = fieldA; + } + + public Banana getFieldB() { + return fieldB; + } + + @Autowired + public void setFieldB(Banana fieldB) { + this.fieldB = fieldB; + } + + public Orange getFieldC() { + return fieldC; + } + + public void setFieldC(Orange fieldC) { + this.fieldC = fieldC; + } + + public AnnotationAutowired() {} + + @Autowired + public AnnotationAutowired(Orange fieldC) { + this.fieldC = fieldC; + } + + public static void main(String[] args) throws Exception { + AbstractApplicationContext ctx = + new ClassPathXmlApplicationContext("spring/spring-annotation.xml"); + + AnnotationAutowired annotationAutowired = + (AnnotationAutowired) ctx.getBean("annotationAutowired"); + log.debug("fieldA: {}, fieldB:{}, fieldC:{}", annotationAutowired.getFieldA().getName(), + annotationAutowired.getFieldB().getName(), + annotationAutowired.getFieldC().getName()); + ctx.close(); + } +} +``` + +xml 中的配置 + +```xml + + + + + +``` + +#### `@Qualifier` + +在`@Autowired`注解中,提到了如果发现有多个候选的 bean 都符合修饰类型,Spring 就会抓瞎了。 + +那么,如何解决这个问题。 + +可以通过`@Qualifier`指定 bean 名称来锁定真正需要的那个 bean。 + +**_范例_** + +```java +public class AnnotationQualifier { + private static final Logger log = LoggerFactory.getLogger(AnnotationQualifier.class); + + @Autowired + @Qualifier("dog") /** 去除这行,会报异常 */ + Animal dog; + + Animal cat; + + public Animal getDog() { + return dog; + } + + public void setDog(Animal dog) { + this.dog = dog; + } + + public Animal getCat() { + return cat; + } + + @Autowired + public void setCat(@Qualifier("cat") Animal cat) { + this.cat = cat; + } + + public static void main(String[] args) throws Exception { + AbstractApplicationContext ctx = + new ClassPathXmlApplicationContext("spring/spring-annotation.xml"); + + AnnotationQualifier annotationQualifier = + (AnnotationQualifier) ctx.getBean("annotationQualifier"); + + log.debug("Dog name: {}", annotationQualifier.getDog().getName()); + log.debug("Cat name: {}", annotationQualifier.getCat().getName()); + ctx.close(); + } +} + +abstract class Animal { + public String getName() { + return null; + } +} + +class Dog extends Animal { + public String getName() { + return "狗"; + } +} + +class Cat extends Animal { + public String getName() { + return "猫"; + } +} +``` + +xml 中的配置 + +```xml + + + + +``` + +#### `@Resource` + +Spring 支持 JSP250 规定的注解`@Resource`。这个注解根据指定的名称来注入 bean。 + +如果没有为`@Resource`指定名称,它会像`@Autowired`一样按照类型去寻找匹配。 + +在 Spring 中,由`CommonAnnotationBeanPostProcessor`来处理`@Resource`注解。 + +**_范例_** + +```java +public class AnnotationResource { + private static final Logger log = LoggerFactory.getLogger(AnnotationResource.class); + + @Resource(name = "flower") + Plant flower; + + @Resource(name = "tree") + Plant tree; + + public Plant getFlower() { + return flower; + } + + public void setFlower(Plant flower) { + this.flower = flower; + } + + public Plant getTree() { + return tree; + } + + public void setTree(Plant tree) { + this.tree = tree; + } + + public static void main(String[] args) throws Exception { + AbstractApplicationContext ctx = + new ClassPathXmlApplicationContext("spring/spring-annotation.xml"); + + AnnotationResource annotationResource = + (AnnotationResource) ctx.getBean("annotationResource"); + log.debug("type: {}, name: {}", annotationResource.getFlower().getClass(), annotationResource.getFlower().getName()); + log.debug("type: {}, name: {}", annotationResource.getTree().getClass(), annotationResource.getTree().getName()); + ctx.close(); + } +} +``` + +xml 的配置 + +```xml + + + + +``` + +#### `@PostConstruct` 和 `@PreDestroy` + +`@PostConstruct` 和 `@PreDestroy` 是 JSR 250 规定的用于生命周期的注解。 + +从其名号就可以看出,一个是在构造之后调用的方法,一个是销毁之前调用的方法。 + +```java +public class AnnotationPostConstructAndPreDestroy { + private static final Logger log = LoggerFactory.getLogger(AnnotationPostConstructAndPreDestroy.class); + + @PostConstruct + public void init() { + log.debug("call @PostConstruct method"); + } + + @PreDestroy + public void destroy() { + log.debug("call @PreDestroy method"); + } +} +``` + +#### `@Inject` + +从 Spring3.0 开始,Spring 支持 JSR 330 标准注解(依赖注入)。 + +注:如果要使用 JSR 330 注解,需要使用外部 jar 包。 + +若你使用 maven 管理 jar 包,只需要添加依赖到 pom.xml 即可: + +```xml + + javax.inject + javax.inject + 1 + +``` + +`@Inject` 和 `@Autowired` 一样,可以修饰属性、setter 方法、构造方法。 + +**_范例_** + +```java +public class AnnotationInject { + private static final Logger log = LoggerFactory.getLogger(AnnotationInject.class); + @Inject + Apple fieldA; + + Banana fieldB; + + Orange fieldC; + + public Apple getFieldA() { + return fieldA; + } + + public void setFieldA(Apple fieldA) { + this.fieldA = fieldA; + } + + public Banana getFieldB() { + return fieldB; + } + + @Inject + public void setFieldB(Banana fieldB) { + this.fieldB = fieldB; + } + + public Orange getFieldC() { + return fieldC; + } + + public AnnotationInject() {} + + @Inject + public AnnotationInject(Orange fieldC) { + this.fieldC = fieldC; + } + + public static void main(String[] args) throws Exception { + AbstractApplicationContext ctx = + new ClassPathXmlApplicationContext("spring/spring-annotation.xml"); + AnnotationInject annotationInject = (AnnotationInject) ctx.getBean("annotationInject"); + + log.debug("type: {}, name: {}", annotationInject.getFieldA().getClass(), + annotationInject.getFieldA().getName()); + + log.debug("type: {}, name: {}", annotationInject.getFieldB().getClass(), + annotationInject.getFieldB().getName()); + + log.debug("type: {}, name: {}", annotationInject.getFieldC().getClass(), + annotationInject.getFieldC().getName()); + + ctx.close(); + } +} +``` + +### Java 配置 + +基于 Java 配置 Spring IoC 容器,实际上是**Spring 允许用户定义一个类,在这个类中去管理 IoC 容器的配置**。 + +为了让 Spring 识别这个定义类为一个 Spring 配置类,需要用到两个注解:`@Configuration`和`@Bean`。 + +如果你熟悉 Spring 的 xml 配置方式,你可以将`@Configuration`等价于``标签;将`@Bean`等价于``标签。 + +#### `@Bean` + +@Bean 的修饰目标只能是方法或注解。 + +@Bean 只能定义在 `@Configuration` 或 `@Component` 注解修饰的类中。 + +#### 声明一个 bean + +此外,@Configuration 类允许在同一个类中通过@Bean 定义内部 bean 依赖。 + +声明一个 bean,只需要在 bean 属性的 set 方法上标注@Bean 即可。 + +```java +@Configuration +public class AnnotationConfiguration { + private static final Logger log = LoggerFactory.getLogger(JavaComponentScan.class); + + @Bean + public Job getPolice() { + return new Police(); + } + + public static void main(String[] args) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AnnotationConfiguration.class); + ctx.scan("org.zp.notes.spring.beans"); + ctx.refresh(); + Job job = (Job) ctx.getBean("police"); + log.debug("job: {}, work: {}", job.getClass(), job.work()); + } +} + +public interface Job { + String work(); +} + +@Component("police") +public class Police implements Job { + @Override + public String work() { + return "抓罪犯"; + } +} +``` + +这等价于配置 + +```xml + + + +``` + +@Bean 注解用来表明一个方法实例化、配置合初始化一个被 Spring IoC 容器管理的新对象。 + +如果你熟悉 Spring 的 xml 配置,你可以将@Bean 视为等价于``标签。 + +@Bean 注解可以用于任何的 Spring `@Component` bean,然而,通常被用于`@Configuration` bean。 + +#### `@Configuration` + +`@Configuration` 是一个类级别的注解,用来标记被修饰类的对象是一个`BeanDefinition`。 + +`@Configuration` 声明 bean 是通过被 `@Bean` 修饰的公共方法。此外,`@Configuration` 允许在同一个类中通过 `@Bean` 定义内部 bean 依赖。 + +```java +@Configuration +public class AppConfig { + @Bean + public MyService myService() { + return new MyServiceImpl(); + } +} +``` + +这等价于配置 + +```xml + + + +``` + +用 `AnnotationConfigApplicationContext` 实例化 IoC 容器。 + +## 依赖解决过程 + +容器执行 bean 依赖解析如下: + +- `ApplicationContext` 使用配置元数据创建和初始化 Bean。配置元数据可以由 XML、Java 代码或注解指定。 +- 对于每个 bean,其依赖关系以属性、构造函数参数或静态工厂方法的参数的形式表示。这些依赖项在实际创建 bean 时提供给 bean。 +- 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个 bean 的引用。 +- 作为值的每个属性或构造函数参数都从其指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将以字符串格式提供的值转换为所有内置类型,例如 int、long、String、boolean 等。 + +Spring 容器在创建容器时验证每个 bean 的配置。但是,在实际创建 bean 之前,不会设置 bean 属性本身。在创建容器时会创建 singleton 型的实例并设置为默认的 Bean。否则,只有在请求时才会创建 bean。 + +需注意:构造器注入,可能会导致无法解决循环依赖问题。 + +例如:A 类通过构造器注入需要 B 类的实例,B 类通过构造器注入需要 A 类的实例。Spring IoC 容器会在运行时检测到此循环引用,并抛出 `BeanCurrentlyInCreationException`。 + +一种解决方案是使用 setter 方法注入替代构造器注入。 + +另一种解决方案是:bean A 和 bean B 之间的循环依赖关系,强制其中一个 bean 在完全初始化之前注入另一个 bean(典型的先有鸡还是先有蛋的场景)。 + +Spring 会在容器加载时检测配置问题,例如引用不存在的 bean 或循环依赖。在实际创建 bean 时,Spring 会尽可能晚地设置属性并解析依赖关系。这意味着,如果在创建该对象或其依赖项之一时出现问题,则正确加载的 Spring 容器稍后可以在您请求对象时生成异常  —  例如,bean 由于丢失或无效而引发异常。某些配置问题的这种潜在的延迟可见性是默认情况下 ApplicationContext 实现预实例化单例 bean 的原因。以在实际需要之前创建这些 bean 的一些前期时间和内存为代价,您会在创建 ApplicationContext 时发现配置问题,而不是稍后。您仍然可以覆盖此默认行为,以便单例 bean 延迟初始化,而不是急切地预先实例化。 + +## 最佳实践 + +### singleton 的 Bean 如何注入 prototype 的 Bean + +Spring 创建的 Bean 默认是单例的,但当 Bean 遇到继承的时候,可能会忽略这一点。 + +假设有一个 SayService 抽象类,其中维护了一个类型是 ArrayList 的字段 data,用于保存方法处理的中间数据。每次调用 say 方法都会往 data 加入新数据,可以认为 SayService 是有状态,如果 SayService 是单例的话必然会 OOM。 + +```java +/** + * SayService 是有状态,如果 SayService 是单例的话必然会 OOM + */ +@Slf4j +public abstract class SayService { + + List data = new ArrayList<>(); + + public void say() { + data.add(IntStream.rangeClosed(1, 1000000) + .mapToObj(__ -> "a") + .collect(Collectors.joining("")) + UUID.randomUUID().toString()); + log.info("I'm {} size:{}", this, data.size()); + } + +} +``` + +但实际开发的时候,开发同学没有过多思考就把 SayHello 和 SayBye 类加上了 @Service 注解,让它们成为了 Bean,也没有考虑到父类是有状态的。 + +```java +@Service +@Slf4j +public class SayBye extends SayService { + + @Override + public void say() { + super.say(); + log.info("bye"); + } + +} + +@Service +@Slf4j +public class SayHello extends SayService { + + @Override + public void say() { + super.say(); + log.info("hello"); + } + +} +``` + +在为类标记上 @Service 注解把类型交由容器管理前,首先评估一下类是否有状态,然后为 Bean 设置合适的 Scope。 + +调用代码: + +```java +@Slf4j +@RestController +@RequestMapping("beansingletonandorder") +public class BeanSingletonAndOrderController { + + @Autowired + List sayServiceList; + @Autowired + private ApplicationContext applicationContext; + + @GetMapping("test") + public void test() { + log.info("===================="); + sayServiceList.forEach(SayService::say); + } + +} +``` + +可能有人认为,为 SayHello 和 SayBye 两个类都标记了 @Scope 注解,设置了 PROTOTYPE 的生命周期就可以解决上面的问题。 + +```java +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE) +``` + +但实际上还是有问题。因为@RestController 注解 =@Controller 注解 +@ResponseBody 注解,又因为 @Controller 标记了 @Component 元注解,所以 @RestController 注解其实也是一个 Spring Bean。 + +Bean 默认是单例的,所以单例的 Controller 注入的 Service 也是一次性创建的,即使 Service 本身标识了 prototype 的范围也没用。 + +修复方式是,让 Service 以代理方式注入。这样虽然 Controller 本身是单例的,但每次都能从代理获取 Service。这样一来,prototype 范围的配置才能真正生效。 + +```java +@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProx) +``` + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/03.Spring\344\276\235\350\265\226\346\237\245\346\211\276.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/03.Spring\344\276\235\350\265\226\346\237\245\346\211\276.md" new file mode 100644 index 00000000..0d1fa338 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/03.Spring\344\276\235\350\265\226\346\237\245\346\211\276.md" @@ -0,0 +1,149 @@ +--- +title: Spring 依赖查找 +date: 2020-08-30 16:06:10 +order: 03 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - IOC + - 依赖查找 +permalink: /pages/9a6f6b/ +--- + +# Spring 依赖查找 + +**依赖查找是主动或手动的依赖查找方式,通常需要依赖容器或标准 API 实现**。 + +IoC 依赖查找大致可以分为以下几类: + +- 根据 Bean 名称查找 +- 根据 Bean 类型查找 +- 根据 Bean 名称 + 类型查找 +- 根据 Java 注解查找 + +此外,根据查找的 Bean 对象是单一或集合对象,是否需要延迟查找等特定常见,有相应不同的 API。 + +## 单一类型依赖查找 + +单一类型依赖查找接口- `BeanFactory` + +- 根据 Bean 名称查找 + - `getBean(String)` + - Spring 2.5 覆盖默认参数:`getBean(String,Object...)` +- 根据 Bean 类型查找 + - Bean 实时查找 + - Spring 3.0 `getBean(Class)` + - Spring 4.1 覆盖默认参数:`getBean(Class,Object...)` + - Spring 5.1 Bean 延迟查找 + - `getBeanProvider(Class)` + - `getBeanProvider(ResolvableType)` +- 根据 Bean 名称 + 类型查找:`getBean(String,Class)` + +## 集合类型依赖查找 + +集合类型依赖查找接口- `ListableBeanFactory` + +- 根据 Bean 类型查找 + - 获取同类型 Bean 名称列表 + - `getBeanNamesForType(Class)` + - Spring 4.2 `getBeanNamesForType(ResolvableType)` + - 获取同类型 Bean 实例列表 + - `getBeansOfType(Class)` 以及重载方法 +- 通过注解类型查找 + + - Spring 3.0 获取标注类型 Bean 名称列表 + + - `getBeanNamesForAnnotation(Class)` + + - Spring 3.0 获取标注类型 Bean 实例列表 + + - `getBeansWithAnnotation(Class)` + + - Spring 3.0 获取指定名称+ 标注类型 Bean 实例 + + - `findAnnotationOnBean(String,Class)` + +## 层次性依赖查找 + +层次性依赖查找接口- `HierarchicalBeanFactory` + +- 双亲 `BeanFactory`:`getParentBeanFactory()` +- 层次性查找 + - 根据 Bean 名称查找 + - 基于 `containsLocalBean` 方法实现 + - 根据 Bean 类型查找实例列表 + - 单一类型:`BeanFactoryUtils#beanOfType` + - 集合类型:`BeanFactoryUtils#beansOfTypeIncludingAncestors` + - 根据 Java 注解查找名称列表 + - `BeanFactoryUtils#beanNamesForTypeIncludingAncestors` + +## 延迟依赖查找 + +Bean 延迟依赖查找接口 + +- `org.springframework.beans.factory.ObjectFactory` +- `org.springframework.beans.factory.ObjectProvider`(Spring 5 对 Java 8 特性扩展) +- 函数式接口 + - `getIfAvailable(Supplier)` + - `ifAvailable(Consumer)` +- Stream 扩展- stream() + +## 安全依赖查找 + +| 依赖查找类型 | 代表实现 | 是否安全 | +| ------------ | ------------------------------------ | -------- | +| 单一类型查找 | `BeanFactory#getBean` | 否 | +| | `ObjectFactory#getObject` | 否 | +| | `ObjectProvider#getIfAvailable` | 是 | +| | | | +| 集合类型查找 | `ListableBeanFactory#getBeansOfType` | 是 | +| | `ObjectProvider#stream` | 是 | + +注意:层次性依赖查找的安全性取决于其扩展的单一或集合类型的 `BeanFactory` 接口 + +## 内建可查找的依赖 + +`AbstractApplicationContext` 内建可查找的依赖 + +| Bean | 名称 Bean | 实例使用场景 | +| --------------------------- | -------------------------------- | ----------------------- | +| environment | Environment 对象 | 外部化配置以及 Profiles | +| systemProperties | java.util.Properties 对象 | Java 系统属性 | +| systemEnvironment | java.util.Map 对象 | 操作系统环境变量 | +| messageSource | MessageSource 对象 | 国际化文案 | +| lifecycleProcessor | LifecycleProcessor 对象 | Lifecycle Bean 处理器 | +| applicationEventMulticaster | ApplicationEventMulticaster 对象 | Spring 事件广播器 | + +注解驱动 Spring 应用上下文内建可查找的依赖(部分) + +| Bean 名称 | Bean 实例 | 使用场景 | +| ------------------------------------------------------------------------------- | ------------------------------------------- | ----------------------------------------------------- | +| org.springframework.context.annotation.internalConfigurationAnnotationProcessor | ConfigurationClassPostProcessor 对象 | 处理 Spring 配置类 | +| org.springframework.context.annotation.internalAutowiredAnnotationProcessor | AutowiredAnnotationBeanPostProcessor 对象 | 处理@Autowired 以及@Value 注解 | +| org.springframework.context.annotation.internalCommonAnnotationProcessor | CommonAnnotationBeanPostProcessor 对象 | (条件激活)处理 JSR-250 注解,如@PostConstruct 等 | +| org.springframework.context.event.internalEventListenerProcessor | EventListenerMethodProcessor 对象 | 处理标注@EventListener 的 Spring 事件监听方法 | +| org.springframework.context.event.internalEventListenerFactory | DefaultEventListenerFactory 对象 | @EventListener 事件监听方法适配为 ApplicationListener | +| org.springframework.context.annotation.internalPersistenceAnnotationProcessor | PersistenceAnnotationBeanPostProcessor 对象 | (条件激活)处理 JPA 注解场景 | + +## 依赖查找中的经典异常 + +`BeansException` 子类型 + +| 异常类型 | 触发条件(举例) | 场景举例 | +| --------------------------------- | ------------------------------------------ | -------------------------------------------- | +| `NoSuchBeanDefinitionException` | 当查找 Bean 不存在于 IoC 容器时 | `BeanFactory#getBeanObjectFactory#getObject` | +| `NoUniqueBeanDefinitionException` | 类型依赖查找时,IoC 容器存在多个 Bean 实例 | `BeanFactory#getBean(Class)` | +| `BeanInstantiationException` | 当 Bean 所对应的类型非具体类时 | `BeanFactory#getBean` | +| `BeanCreationException` | 当 Bean 初始化过程中 | Bean 初始化方法执行异常时 | +| `BeanDefinitionStoreException` | 当 `BeanDefinition` 配置元信息非法时 | XML 配置资源无法打开时 | + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/04.Spring\344\276\235\350\265\226\346\263\250\345\205\245.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/04.Spring\344\276\235\350\265\226\346\263\250\345\205\245.md" new file mode 100644 index 00000000..c44e438f --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/04.Spring\344\276\235\350\265\226\346\263\250\345\205\245.md" @@ -0,0 +1,378 @@ +--- +title: Spring 依赖注入 +date: 2020-08-30 16:06:10 +order: 04 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - IOC + - 依赖注入 +permalink: /pages/f61a1c/ +--- + +# Spring 依赖注入 + +DI,是 Dependency Injection 的缩写,即依赖注入。依赖注入是 IoC 的最常见形式。依赖注入是手动或自动绑定的方式,无需依赖特定的容器或 API。 + +依赖注入 (Dependency Injection,简称 DI) 是一个过程,其中对象仅通过构造函数参数、工厂方法的参数或对象实例在构造或从工厂方法返回。然后容器在创建 bean 时注入这些依赖项。这个过程基本上是 bean 本身的逆过程(因此得名,控制反转),它通过使用类的直接构造或服务定位器模式自行控制其依赖项的实例化或位置。 + +使用 DI,代码更干净,当对象具有依赖关系时,解耦更有效。对象不查找其依赖项,也不知道依赖项的位置或类别。结果,您的类变得更容易测试,特别是当依赖关系在接口或抽象基类上时,它们允许在单元测试中使用存根或模拟实现。 + +**容器全权负责组件的装配,它会把符合依赖关系的对象通过 JavaBean 属性或者构造函数传递给需要的对象**。 + +DI 是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。 + +理解 DI 的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下: + +- **谁依赖于谁:**当然是应用程序依赖于 IoC 容器; +- **为什么需要依赖:**应用程序需要 IoC 容器来提供对象需要的外部资源; +- **谁注入谁:**很明显是 IoC 容器注入应用程序某个对象,应用程序依赖的对象; +- **注入了什么**:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。 + +## IoC 依赖注入 API + +- 根据 Bean 名称注入 +- 根据 Bean 类型注入 +- 注入容器内建 Bean 对象 +- 注入非 Bean 对象 +- 注入类型 + - 实时注入 + - 延迟注入 + +## 依赖注入模式 + +依赖注入模式可以分为手动注入模式和自动注入模式。 + +### 手动注入模式 + +手动注入模式:配置或者编程的方式,提前安排注入规则 + +- XML 资源配置元信息 +- Java 注解配置元信息 +- API 配置元信息 + +### 自动注入模式 + +自动注入模式即自动装配。自动装配(Autowiring)是指 Spring 容器可以自动装配 Bean 之间的关系。Spring 可以通过检查 `ApplicationContext` 的内容,自动解析合作者(其他 Bean)。 + +- 自动装配可以显著减少属性或构造函数参数的配置。 +- 随着对象的发展,自动装配可以更新配置。 + +> 注:由于自动装配存在一些限制和不足,官方不推荐使用。 + +#### 自动装配策略 + +当使用基于 XML 的配置元数据时,可以使用 `` 元素的 `autowire` 属性为 Bean 指定自动装配模式。自动装配模式有以下类型: + +| 模式 | 说明 | +| ------------- | ---------------------------------------------------------------------- | +| `no` | 默认值,未激活 Autowiring,需要手动指定依赖注入对象。 | +| `byName` | 根据被注入属性的名称作为 Bean 名称进行依赖查找,并将对象设置到该属性。 | +| `byType` | 根据被注入属性的类型作为依赖类型进行查找,并将对象设置到该属性。 | +| `constructor` | 特殊 byType 类型,用于构造器参数。 | + +`org.springframework.beans.factory.config.AutowireCapableBeanFactory` 是 `BeanFactory` 的子接口,它是 Spring 中用于实现自动装配的容器。 + +#### @Autowired 注入过程 + +- 元信息解析 +- 依赖查找 +- 依赖注入(字段、方法) + +#### 自动装配的限制和不足 + +自动装配有以下限制和不足: + +- 属性和构造函数参数设置中的显式依赖项会覆盖自动装配。您不能自动装配简单属性,例如基础数据类型、字符串和类(以及此类简单属性的数组)。 +- 自动装配不如显式装配精准。Spring 会尽量避免猜测可能存在歧义的结果。 +- Spring 容器生成文档的工具可能无法解析自动装配信息。 +- 如果同一类型存在多个 Bean 时,自动装配时会存在歧义。容器内的多个 Bean 定义可能与要自动装配的 Setter 方法或构造函数参数指定的类型匹配。对于数组、集合或 Map 实例,这不一定是问题。但是,对于期望单值的依赖项,如果没有唯一的 Bean 定义可用,则会引发异常。 + +> 自动装配的限制和不足,详情可以参考官方文档:[Limitations and Disadvantages of Autowiring 小节](https://docs.spring.io/spring/docs/5.2.2.RELEASE/spring-frameworkreference/core.html#beans-autowired-exceptions) + +## 依赖注入方式 + +依赖注入有如下方式: + +| 依赖注入方式 | 配置元数据举例 | +| --------------- | -------------------------------------------------- | +| Setter 方法注入 | `` | +| 构造器注入 | `` | +| 字段注入 | `@Autowired User user;` | +| 方法注入 | `@Autowired public void user(User user) { ... }` | +| 接口回调注入 | `class MyBean implements BeanFactoryAware { ... }` | + +### 构造器注入 + +- 手动模式 + - xml 配置元信息 + - 注解配置元信息 + - Java 配置元信息 +- 自动模式 + - constructor + +构造器注入是通过容器调用具有多个参数的构造函数来完成的,每个参数代表一个依赖项。调用带有特定参数的静态工厂方法来构造 bean 几乎是等价的,并且本次讨论对构造函数和静态工厂方法的参数进行了类似的处理。 + +下面是一个构造器注入示例: + +```java +public class SimpleMovieLister { + + // the SimpleMovieLister has a dependency on a MovieFinder + private final MovieFinder movieFinder; + + // a constructor so that the Spring container can inject a MovieFinder + public SimpleMovieLister(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + // business logic that actually uses the injected MovieFinder is omitted... +} +``` + +构造函数参数解析匹配通过使用参数的类型进行。如果 bean 定义的构造函数参数中不存在潜在的歧义,则在 bean 定义中定义构造函数参数的顺序是在实例化 bean 时将这些参数提供给适当构造函数的顺序。 + +``` +package x.y; + +public class ThingOne { + + public ThingOne(ThingTwo thingTwo, ThingThree thingThree) { + // ... + } +} +``` + +假设 ThingTwo 和 ThingThree 类没有继承关系,则不存在潜在的歧义。因此,以下配置工作正常,您无需在 `` 元素中显式指定构造函数参数索引或类型。 + +```xml + + + + + + + + + + +``` + +当引用另一个 bean 时,类型是已知的,并且可以发生匹配(就像前面的示例一样)。当使用简单类型时,例如 `true` ,Spring 无法确定 value 的类型,因此无法在没有帮助的情况下按类型匹配。考虑以下类: + +```java +package examples; + +public class ExampleBean { + + // Number of years to calculate the Ultimate Answer + private final int years; + + // The Answer to Life, the Universe, and Everything + private final String ultimateAnswer; + + public ExampleBean(int years, String ultimateAnswer) { + this.years = years; + this.ultimateAnswer = ultimateAnswer; + } +} +``` + +构造函数参数类型匹配 + +在上述场景中,如果您使用 type 属性显式指定构造函数参数的类型,则容器可以使用简单类型的类型匹配,如以下示例所示: + +```xml + + + + +``` + +构造函数参数索引匹配 + +可以使用 `index` 属性显式指定构造函数参数的索引,如以下示例所示 + +```xml + + + + +``` + +构造函数参数名称匹配 + +```xml + + + + +``` + +可以使用 `@ConstructorProperties` 显式命名构造函数参数。 + +```java +package examples; + +public class ExampleBean { + + // Fields omitted + + @ConstructorProperties({"years", "ultimateAnswer"}) + public ExampleBean(int years, String ultimateAnswer) { + this.years = years; + this.ultimateAnswer = ultimateAnswer; + } +} +``` + +### Setter 方法注入 + +- 手动模式 + - xml 配置元信息 + - 注解配置元信息 + - Java 配置元信息 +- 自动模式 + - byName + - byType + +Setter 方法注入是通过容器在调用无参数构造函数或无参数静态工厂方法来实例化 bean 后调用 bean 上的 setter 方法来完成的。 + +以下示例显示了一个只能通过使用纯 setter 注入进行依赖注入的类。 + +```java +public class SimpleMovieLister { + + // the SimpleMovieLister has a dependency on the MovieFinder + private MovieFinder movieFinder; + + // a setter method so that the Spring container can inject a MovieFinder + public void setMovieFinder(MovieFinder movieFinder) { + this.movieFinder = movieFinder; + } + + // business logic that actually uses the injected MovieFinder is omitted... +} +``` + +在 Spring 中,可以混合使用构造器注入和 setter 方法注入。建议将构造器注入用于强制依赖项;并将 setter 方法注入或配置方法用于可选依赖项。需要注意的是,在 setter 方法上使用 `@Required` 注解可用于使属性成为必需的依赖项;然而,更建议使用构造器注入来完成这项工作。 + +### 字段注入 + +手动模式(Java 注解配置元信息) + +- `@Autowired` +- `@Resource` +- `@Inject`(可选) + +### 方法注入 + +手动模式(Java 注解配置元信息) + +- `@Autowired` +- `@Resource` +- `@Inject`(可选) +- `@Bean` + +### 接口回调注入 + +Aware 系列接口回调 + +| 內建接口 | 说明 | +| -------------------------------- | ---------------------------------------------------------- | +| `BeanFactoryAware` | 获取 IoC 容器- `BeanFactory` | +| `ApplicationContextAware` | 获取 Spring 应用上下文- `ApplicationContext` 对象 | +| `EnvironmentAware` | 获取 `Environment` 对象 | +| `ResourceLoaderAware` | 获取资源加载器对象- `ResourceLoader` | +| `BeanClassLoaderAware` | 获取加载当前 Bean Class 的 `ClassLoader` | +| `BeanNameAware` | 获取当前 Bean 的名称 | +| `MessageSourceAware` | 获取 `MessageSource` 对象,用于 Spring 国际化 | +| `ApplicationEventPublisherAware` | 获取 `ApplicationEventPublishAware` 对象,用于 Spring 事件 | +| `EmbeddedValueResolverAware` | 获取 `StringValueResolver` 对象,用于占位符处理 | + +### 依赖注入选型 + +- 低依赖:构造器注入 +- 多依赖:Setter 方法注入 +- 便利性:字段注入 +- 声明类:方法注入 + +## 限定注入和延迟注入 + +### 限定注入 + +- 使用 `@Qualifier` 注解限定 + - 通过 Bean 名称限定 + - 通过分组限定 +- 通过 `@Qualifier` 注解扩展限定 + - 自定义注解:如 Spring Cloud 的 `@LoadBalanced` + +### 延迟注入 + +- 使用 `ObjectFactory` +- 使用 `ObjectProvider`(推荐) + +## 依赖注入数据类型 + +### 基础类型 + +- 基础数据类型:`boolean`、`byte`、`char`、`short`、`int`、`float`、`long`、`double` +- 标量类型:`Number`、`Character`、`Boolean`、`Enum`、`Locale`、`Charset`、`Currency`、`Properties`、`UUID` +- 常规类型:`Object`、`String`、`TimeZone`、`Calendar`、`Optional` 等 +- Spring 类型:`Resource`、`InputSource`、`Formatter` 等。 + +### 集合类型 + +数组类型:基础数据类型、标量类型、常规类型、String 类型的数组 + +集合类型: + +- `Collection`:`List`、`Set` +- `Map`:`Properties` + +## 依赖处理过程 + +入口:`DefaultListableBeanFactory#resolveDependency` + +依赖描述符:`DependencyDescriptor` + +自定义绑定候选对象处理器:`AutowireCandidateResolver` + +`@Autowired`、`@Value`、`@javax.inject.Inject` 处理器:`AutowiredAnnotationBeanPostProcessor` + +通用注解处理器:`CommonAnnotationBeanPostProcessor` + +- 注入注解 + - `javax.xml.ws.WebServiceRef` + - `javax.ejb.EJB` + - `javax.annotation.Resources` +- 生命周期注解 + - `javax.annotation.PostConstruct` + - `javax.annotation.PreDestroy` + +自定义依赖注入注解: + +- 生命周期处理 + - `InstantiationAwareBeanPostProcessor` + - `MergedBeanDefinitionPostProcessor` +- 元数据 + - `InjectionMetadata` + - `InjectionMetadata.InjectedElement` + +## 依赖查找 VS. 依赖注入 + +| 类型 | 依赖处理 | 实现复杂度 | 代码侵入性 | API 依赖性 | 可读性 | +| -------- | -------- | ---------- | ------------ | -------------- | ------ | +| 依赖查找 | 主动 | 相对繁琐 | 侵入业务逻辑 | 依赖容器 API | 良好 | +| 依赖注入 | 被动 | 相对便利 | 低侵入性 | 不依赖容器 API | 一般 | + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/05.SpringIoC\344\276\235\350\265\226\346\235\245\346\272\220.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/05.SpringIoC\344\276\235\350\265\226\346\235\245\346\272\220.md" new file mode 100644 index 00000000..b633b853 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/05.SpringIoC\344\276\235\350\265\226\346\235\245\346\272\220.md" @@ -0,0 +1,124 @@ +--- +title: Spring IoC 依赖来源 +date: 2022-12-20 20:33:51 +order: 05 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - IOC + - 依赖注入 +permalink: /pages/a5f257/ +--- + +# Spring IoC 依赖来源 + +## 依赖查找的来源 + +查找来源 + +| 来源 | 配置元数据 | +| --------------------- | ---------------------------------------- | +| Spring BeanDefinition | `` | +| | `@Bean public User user() {...}` | +| | `BeanDefinitionBuilder` | +| 单例对象 | API 实现 | + +Spring 內建 BeanDefintion + +| Bean 名称 | Bean 实例 | 使用场景 | +| ------------------------------------------------------------------------------- | ----------------------------------------- | --------------------------------------------------- | +| org.springframework.context.annotation.internalConfigurationAnnotationProcessor | ConfigurationClassPostProcessor 对象 | 处理 Spring 配置类 | +| org.springframework.context.annotation.internalAutowiredAnnotationProcessor | AutowiredAnnotationBeanPostProcessor 对象 | 处理 @Autowired 以及 @Value 注解 | +| org.springframework.context.annotation.internalCommonAnnotationProcessor | CommonAnnotationBeanPostProcessor 对象 | (条件激活)处理 JSR-250 注解,如 @PostConstruct 等 | +| org.springframework.context.event.internalEventListenerProcessor | EventListenerMethodProcessor 对象 | 处理标注 @EventListener 的 Spring 事件监听方法 | + +Spring 內建单例对象 + +| Bean 名称 | Bean 实例 | 使用场景 | +| --------------------------- | -------------------------------- | ----------------------- | +| environment | Environment 对象 | 外部化配置以及 Profiles | +| systemProperties | java.util.Properties 对象 | Java 系统属性 | +| systemEnvironment | java.util.Map 对象 | 操作系统环境变量 | +| messageSource | MessageSource 对象 | 国际化文案 | +| lifecycleProcessor | LifecycleProcessor 对象 | Lifecycle Bean 处理器 | +| applicationEventMulticaster | ApplicationEventMulticaster 对象 | Spring 事件广播器 | + +## 依赖注入的来源 + +| 来源 | 配置元数据 | +| ---------------------- | ---------------------------------------- | +| Spring BeanDefinition | `` | +| | `@Bean public User user() {...}` | +| | `BeanDefinitionBuilder` | +| 单例对象 | API 实现 | +| 非 Spring 容器管理对象 | | + +## Spring 容器管理和游离对象 + +| 来源 | Spring Bean 对象 | 生命周期管理 | 配置元信息 | 使用场景 | +| --------------------- | ---------------- | ------------ | ---------- | ------------------ | +| Spring BeanDefinition | 是 | 是 | 有 | 依赖查找、依赖注入 | +| 单体对象 | 是 | 否 | 无 | 依赖查找、依赖注入 | +| Resolvable Dependency | 否 | 否 | 无 | 依赖注入 | + +## Spring BeanDefinition 作为依赖来源 + +- 元数据:`BeanDefinition` +- 注册:`BeanDefinitionRegistry#registerBeanDefinition` +- 类型:延迟和非延迟 +- 顺序:Bean 生命周期顺序按照注册顺序 + +## 单例对象作为依赖来源 + +- 要素 + - 来源:外部普通 Java 对象(不一定是 POJO) + - 注册:`SingletonBeanRegistry#registerSingleton` +- 限制 + - 无生命周期管理 + - 无法实现延迟初始化 Bean + +## 非 Spring 对象容器管理对象作为依赖来源 + +- 要素 + - 注册:`ConfigurableListableBeanFactory#registerResolvableDependency` +- 限制 + - 无生命周期管理 + - 无法实现延迟初始化 Bean + - 无法通过依赖查找 + +## 外部化配置作为依赖来源 + +- 要素 + - 类型:非常规 Spring 对象依赖来源 +- 限制 + - 无生命周期管理 + - 无法实现延迟初始化 Bean + - 无法通过依赖查找 + +## 问题 + +注入和查找的依赖来源是否相同? + +否,依赖查找的来源仅限于 Spring `BeanDefinition` 以及单例对象,而依赖注入的来源还包括 Resolvable Dependency 以及 `@Value` 所标注的外部化配置 + +单例对象能在 IoC 容器启动后注册吗? + +可以的,单例对象的注册与 `BeanDefinition` 不同,`BeanDefinition` 会被 `ConfigurableListableBeanFactory#freezeConfiguration()` 方法影响,从而冻结注册,单例对象则没有这个限制。 + +Spring 依赖注入的来源有哪些? + +- Spring `BeanDefinition` +- 单例对象 +- Resolvable Dependency +- `@Value` 外部化配置 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/06.SpringBean\344\275\234\347\224\250\345\237\237.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/06.SpringBean\344\275\234\347\224\250\345\237\237.md" new file mode 100644 index 00000000..c07dacac --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/06.SpringBean\344\275\234\347\224\250\345\237\237.md" @@ -0,0 +1,102 @@ +--- +title: Spring Bean 作用域 +date: 2022-12-21 11:42:00 +order: 06 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Bean +permalink: /pages/8289f5/ +--- + +# Spring Bean 作用域 + +## Spring Bean 作用域 + +| 来源 | 说明 | +| ----------- | ---------------------------------------------------------- | +| singleton | 默认 Spring Bean 作用域,一个 BeanFactory 有且仅有一个实例 | +| prototype | 原型作用域,每次依赖查找和依赖注入生成新 Bean 对象 | +| request | 将 Spring Bean 存储在 ServletRequest 上下文中 | +| session | 将 Spring Bean 存储在 HttpSession 中 | +| application | 将 Spring Bean 存储在 ServletContext 中 | + +## "singleton" Bean 作用域 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221221170833.png) + +## "prototype" Bean 作用域 + +Spring 容器没有办法管理 prototype Bean 的完整生命周期,也没有办法记录实例的存在。销毁回调方法将不会执行,可以利用 `BeanPostProcessor` 进行清扫工作。 + +## "request" Bean 作用域 + +- 配置 + - XML - `` + - Java 注解 - `@RequestScope` 或 `@Scope(WebApplicationContext.SCOPE_REQUEST)` +- 实现 + - API - RequestScope + +## "session" Bean 作用域 + +- 配置 + - XML - `` + - Java 注解 - `@SessionScope` 或 `@Scope(WebApplicationContext.SCOPE_SESSION)` +- 实现 + - API - SessionScope + +## "application" Bean 作用域 + +- 配置 + - XML - `` + - Java 注解 - `@ApplicationScope` 或 `@Scope(WebApplicationContext.SCOPE_APPLICATION)` +- 实现 + - API - ServletContextScope + +## 自定义 Bean 作用域 + +- 实现 Scope + + - `org.springframework.beans.factory.config.Scope` + +- 注册 Scope + + - API - `org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope` + +- 配置 + + ```xml + + + + + + + + + ``` + +## 问题 + +Spring 內建的 Bean 作用域有几种? + +singleton、prototype、request、session、application 以及 websocket + +singleton Bean 是否在一个应用是唯一的? + +否。singleton bean 仅在当前 Spring IoC 容器(BeanFactory)中是单例对象。 + +application Bean 是否可以被其他方案替代? + +可以的,实际上,“application” Bean 与“singleton” Bean 没有本质区别 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/07.SpringBean\347\224\237\345\221\275\345\221\250\346\234\237.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/07.SpringBean\347\224\237\345\221\275\345\221\250\346\234\237.md" new file mode 100644 index 00000000..4a72cd8c --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/07.SpringBean\347\224\237\345\221\275\345\221\250\346\234\237.md" @@ -0,0 +1,186 @@ +--- +title: Spring Bean 生命周期 +date: 2022-12-21 19:26:01 +order: 07 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Bean +permalink: /pages/4ab176/ +--- + +# Spring Bean 生命周期 + +## Spring Bean 元信息配置阶段 + +BeanDefinition 配置 + +- 面向资源 + - XML 配置 + - Properties 资源配置 +- 面向注解 +- 面向 API + +## Spring Bean 元信息解析阶段 + +- 面向资源 BeanDefinition 解析 + - BeanDefinitionReader + - XML 解析器 - BeanDefinitionParser +- 面向注解 BeanDefinition 解析 + - AnnotatedBeanDefinitionReader + +## Spring Bean 注册阶段 + +BeanDefinition 注册接口:BeanDefinitionRegistry + +## Spring BeanDefinition 合并阶段 + +BeanDefinition 合并 + +父子 BeanDefinition 合并 + +- 当前 BeanFactory 查找 +- 层次性 BeanFactory 查找 + +## Spring Bean Class 加载阶段 + +- ClassLoader 类加载 +- Java Security 安全控制 +- ConfigurableBeanFactory 临时 ClassLoader + +## Spring Bean 实例化前阶段 + +实例化方式 + +- 传统实例化方式:实例化策略(InstantiationStrategy) +- 构造器依赖注入 + +## Spring Bean 实例化阶段 + +非主流生命周期 - Bean 实例化前阶段 + +InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation + +## Spring Bean 实例化后阶段 + +Bean 属性赋值(Populate)判断 + +InstantiationAwareBeanPostProcessor#postProcessAfterInstantiation + +## Spring Bean 属性赋值前阶段 + +- Bean 属性值元信息 + - PropertyValues +- Bean 属性赋值前回调 + - Spring 1.2 - 5.0:InstantiationAwareBeanPostProcessor#postProcessPropertyValues + - Spring 5.1:InstantiationAwareBeanPostProcessor#postProcessProperties + +## Spring Bean Aware 接口回调阶段 + +Spring Aware 接口: + +- BeanNameAware +- BeanClassLoaderAware +- BeanFactoryAware +- EnvironmentAware +- EmbeddedValueResolverAware +- ResourceLoaderAware +- ApplicationEventPublisherAware +- MessageSourceAware +- ApplicationContextAware + +## Spring Bean 初始化前阶段 + +已完成: + +- Bean 实例化 + +- Bean 属性赋值 + +- Bean Aware 接口回调 + +方法回调: + +- BeanPostProcessor#postProcessBeforeInitialization + +## Spring Bean 初始化阶段 + +Bean 初始化(Initialization) + +- @PostConstruct 标注方法 +- 实现 InitializingBean 接口的 afterPropertiesSet() 方法 +- 自定义初始化方法 + +## Spring Bean 初始化后阶段 + +方法回调:BeanPostProcessor#postProcessAfterInitialization + +## Spring Bean 初始化完成阶段 + +方法回调:Spring 4.1 +:SmartInitializingSingleton#afterSingletonsInstantiated + +## Spring Bean 销毁前阶段 + +方法回调:DestructionAwareBeanPostProcessor#postProcessBeforeDestruction + +## Spring Bean 销毁阶段 + +Bean 销毁(Destroy) + +- @PreDestroy 标注方法 +- 实现 DisposableBean 接口的 destroy() 方法 +- 自定义销毁方法 + +## Spring Bean 垃圾收集 + +Bean 垃圾回收(GC) + +- 关闭 Spring 容器(应用上下文) +- 执行 GC +- Spring Bean 覆盖的 finalize() 方法被回调 + +## 问题 + +**BeanPostProcessor 的使用场景有哪些**? + +BeanPostProcessor 提供 Spring Bean 初始化前和初始化后的生命周期回调,分别对应 postProcessBeforeInitialization 以及 postProcessAfterInitialization 方法,允许对关心的 Bean 进行扩展,甚至是替换。 + +加分项:其中,ApplicationContext 相关的 Aware 回调也是基于 BeanPostProcessor 实现,即 ApplicationContextAwareProcessor。 + +**BeanFactoryPostProcessor 与 BeanPostProcessor 的区别**? + +BeanFactoryPostProcessor 是 Spring BeanFactory(实际为 ConfigurableListableBeanFactory) 的后置处理器,用于扩展 BeanFactory,或通过 BeanFactory 进行依赖查找和依赖注入。 + +BeanFactoryPostProcessor 必须有 Spring ApplicationContext 执行,BeanFactory 无法与其直接交互。 + +而 BeanPostProcessor 则直接与 BeanFactory 关联,属于 N 对 1 的关系。 + +**BeanFactory 是怎样处理 Bean 生命周期**? + +BeanFactory 的默认实现为 `DefaultListableBeanFactory`,其中 Bean生命周期与方法映射如下: + +- BeanDefinition 注册阶段 - registerBeanDefinition +- BeanDefinition 合并阶段 - getMergedBeanDefinition +- Bean 实例化前阶段 - resolveBeforeInstantiation +- Bean 实例化阶段 - createBeanInstance +- Bean 初始化后阶段 - populateBean +- Bean 属性赋值前阶段 - populateBean +- Bean 属性赋值阶段 - populateBean +- Bean Aware 接口回调阶段 - initializeBean +- Bean 初始化前阶段 - initializeBean +- Bean 初始化阶段 - initializeBean +- Bean 初始化后阶段 - initializeBean +- Bean 初始化完成阶段 - preInstantiateSingletons +- Bean 销毁前阶段 - destroyBean +- Bean 销毁阶段 - destroyBean + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/08.Spring\351\205\215\347\275\256\345\205\203\346\225\260\346\215\256.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/08.Spring\351\205\215\347\275\256\345\205\203\346\225\260\346\215\256.md" new file mode 100644 index 00000000..54150343 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/08.Spring\351\205\215\347\275\256\345\205\203\346\225\260\346\215\256.md" @@ -0,0 +1,299 @@ +--- +title: Spring 配置元数据 +date: 2022-12-21 19:49:48 +order: 08 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Bean +permalink: /pages/55f315/ +--- + +# Spring 配置元数据 + +## Spring 配置元信息 + +- Spring Bean 配置元信息 - BeanDefinition +- Spring Bean 属性元信息 - PropertyValues +- Spring 容器配置元信息 +- Spring 外部化配置元信息 - PropertySource +- Spring Profile 元信息 - @Profile + +## Spring Bean 配置元信息 + +Bean 配置元信息 - BeanDefinition + +- GenericBeanDefinition:通用型 BeanDefinition +- RootBeanDefinition:无 Parent 的 BeanDefinition 或者合并后 BeanDefinition +- AnnotatedBeanDefinition:注解标注的 BeanDefinition + +## Spring Bean 属性元信息 + +- Bean 属性元信息 - PropertyValues + - 可修改实现 - MutablePropertyValues + - 元素成员 - PropertyValue +- Bean 属性上下文存储 - AttributeAccessor +- Bean 元信息元素 - BeanMetadataElement + +## Spring 容器配置元信息 + +Spring XML 配置元信息 - beans 元素相关 + +| beans 元素属性 | 默认值 | 使用场景 | +| --------------------------- | ------------ | ----------------------------------------------------------------------- | +| profile | null(留空) | Spring Profiles 配置值 | +| default-lazy-init | default | 当 outter beans “default-lazy-init” 属性存在时,继承该值,否则为“false” | +| default-merge | default | 当 outter beans “default-merge” 属性存在时,继承该值,否则为“false” | +| default-autowire | default | 当 outter beans “default-autowire” 属性存在时,继承该值,否则为“no” | +| default-autowire-candidates | null(留空) | 默认 Spring Beans 名称 pattern | +| default-init-method | null(留空) | 默认 Spring Beans 自定义初始化方法 | +| default-destroy-method | null(留空) | 默认 Spring Beans 自定义销毁方法 | + +Spring XML 配置元信息 - 应用上下文相关 + +| XML 元素 | 使用场景 | +| ---------------------------------- | ------------------------------------ | +| `` | 激活 Spring 注解驱动 | +| `` | Spring @Component 以及自定义注解扫描 | +| `` | 激活 Spring LoadTimeWeaver | +| `` | 暴露 Spring Beans 作为 JMX Beans | +| `` | 将当前平台作为 MBeanServer | +| `` | 加载外部化配置资源作为 Spring 属性配 | +| `` | 利用外部化配置资源覆盖 Spring 属 | + +## 基于 XML 文件装载 Spring Bean 配置元信息 + +底层实现 - XmlBeanDefinitionReader + +| XML 元素 | 使用场景 | +| ------------------ | --------------------------------------------- | +| `` | 单 XML 资源下的多个 Spring Beans 配置 | +| `` | 单个 Spring Bean 定义(BeanDefinition)配置 | +| `` | 为 Spring Bean 定义(BeanDefinition)映射别名 | +| `` | 加载外部 Spring XML 配置资源 | + +## 基于 Properties 文件装载 Spring Bean 配置元信息 + +底层实现 - PropertiesBeanDefinitionReader + +| Properties 属性名 | 使用场景 | +| ----------------- | ------------------------------- | +| `class` | Bean 类全称限定名 | +| `abstract` | 是否为抽象的 BeanDefinition | +| `parent` | 指定 parent BeanDefinition 名称 | +| `lazy-init` | 是否为延迟初始化 | +| `ref` | 引用其他 Bean 的名称 | +| `scope` | 设置 Bean 的 scope 属性 | +| ${n} | n 表示第 n+1 个构造器参数 | + +## 基于 Java 注解装载 Spring Bean 配置元信息 + +Spring 模式注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ---------------- | ------------------ | -------- | +| `@Repository` | 数据仓储模式注解 | 2.0 | +| `@Component` | 通用组件模式注解 | 2.5 | +| `@Service` | 服务模式注解 | 2.5 | +| `@Controller` | Web 控制器模式注解 | 2.5 | +| `@Configuration` | 配置类模式注解 | 3.0 | + +Spring Bean 定义注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ------------ | ------------------------------------------ | ----------- | --- | +| `@Bean` | 替换 XML 元素 `` | 3.0 | +| `@DependsOn` | 替代 XML 属性 `` | 3.0 | +| `@Lazy` | 替代 XML 属性 `` | 3.0 | +| `@Primary` | 替换 XML 元素 `` | 3.0 | +| `@Role` | 替换 XML 元素 `` | 3.1 | +| `@Lookup` | 替代 XML 属性 `` | 4.1 | + +Spring Bean 依赖注入注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ------------ | ----------------------------------- | -------- | +| `@Autowired` | Bean 依赖注入,支持多种依赖查找方式 | 2.5 | +| `@Qualifier` | 细粒度的 @Autowired 依赖查找 | 2.5 | + +| Java 注解 | 场景说明 | 起始版本 | +| --------- | ----------------- | -------- | +| @Resource | 类似于 @Autowired | 2.5 | +| @Inject | 类似于 @Autowired | 2.5 | + +Spring Bean 条件装配注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ------------ | -------------- | -------- | +| @Profile | 配置化条件装配 | 3.1 | +| @Conditional | 编程条件装配 | 4.0 | + +Spring Bean 生命周期回调注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| -------------- | ------------------------------------------------------------- | -------- | +| @PostConstruct | 替换 XML 元素 或 InitializingBean | 2.5 | +| @PreDestroy | 替换 XML 元素 或 DisposableBean | 2.5 | + +Spring BeanDefinition 解析与注册 + +| Spring 注解 | 场景说明 | 起始版本 | +| --------------- | ------------------------------ | -------- | +| XML 资源 | XmlBeanDefinitionReader | 1.0 | +| Properties 资源 | PropertiesBeanDefinitionReader | 1.0 | +| Java 注解 | AnnotatedBeanDefinitionReader | 3.0 | + +## Spring Bean 配置元信息底层实现 + +### Spring XML 资源 BeanDefinition 解析与注册 + +核心 API - XmlBeanDefinitionReader + +- 资源 - Resource +- 底层 - BeanDefinitionDocumentReader + - XML 解析 - Java DOM Level 3 API + - BeanDefinition 解析 - BeanDefinitionParserDelegate + - BeanDefinition 注册 - BeanDefinitionRegistry + +### Spring Properties 资源 BeanDefinition 解析与注册 + +核心 API - PropertiesBeanDefinitionReader + +- 资源 + - 字节流 - Resource + - 字符流 - EncodedResouce +- 底层 + - 存储 - java.util.Properties + - BeanDefinition 解析 - API 内部实现 + - BeanDefinition 注册 - BeanDefinitionRegistry + +### Spring Java 注册 BeanDefinition 解析与注册 + +核心 API - AnnotatedBeanDefinitionReader + +- 资源 + - 类对象 - java.lang.Class +- 底层 + - 条件评估 - ConditionEvaluator + - Bean 范围解析 - ScopeMetadataResolver + - BeanDefinition 解析 - 内部 API 实现 + - BeanDefinition 处理 - AnnotationConfigUtils.processCommonDefinitionAnnotations + - BeanDefinition 注册 - BeanDefinitionRegistry + +## 基于 XML 文件装载 Spring IoC 容器配置元信息 + +Spring IoC 容器相关 XML 配置 + +| 命名空间 | 所属模块 | Schema 资源 URL | +| -------- | -------------- | ----------------------------------------------------------------- | +| beans | spring-beans | https://www.springframework.org/schema/beans/spring-beans.xsd | +| context | spring-context | https://www.springframework.org/schema/context/spring-context.xsd | +| aop | spring-aop | https://www.springframework.org/schema/aop/spring-aop.xsd | +| tx | spring-tx | https://www.springframework.org/schema/tx/spring-tx.xsd | +| util | spring-beans | beans https://www.springframework.org/schema/util/spring-util.xsd | +| tool | spring-beans | https://www.springframework.org/schema/tool/spring-tool.xsd | + +## 基于 Java 注解装载 Spring IoC 容器配置元信息 + +Spring IoC 容器装配注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| --------------- | ------------------------------------------- | -------- | +| @ImportResource | 替换 XML 元素 `` | 3.0 | +| @Import | 导入 Configuration Class | 3.0 | +| @ComponentScan | 扫描指定 package 下标注 Spring 模式注解的类 | 3.1 | + +Spring IoC 配属属性注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ---------------- | -------------------------------- | -------- | +| @PropertySource | 配置属性抽象 PropertySource 注解 | 3.1 | +| @PropertySources | @PropertySource 集合注解 | 4.0 | + +## 基于 Extensible XML authoring 扩展 SpringXML 元素 + +Spring XML 扩展 + +- 编写 XML Schema 文件:定义 XML 结构 +- 自定义 NamespaceHandler 实现:命名空间绑定 +- 自定义 BeanDefinitionParser 实现:XML 元素与 BeanDefinition 解析 +- 注册 XML 扩展:命名空间与 XML Schema 映射 + +## Extensible XML authoring 扩展原理 + +### 触发时机 + +- AbstractApplicationContext#obtainFreshBeanFactory + - AbstractRefreshableApplicationContext#refreshBeanFactory + - AbstractXmlApplicationContext#loadBeanDefinitions + - ... + - XmlBeanDefinitionReader#doLoadBeanDefinitions + - ... + - BeanDefinitionParserDelegate#parseCustomElement + +### 核心流程 + +BeanDefinitionParserDelegate#parseCustomElement(org.w3c.dom.Element, BeanDefinition) + +- 获取 namespace +- 通过 namespace 解析 NamespaceHandler +- 构造 ParserContext +- 解析元素,获取 BeanDefinintion + +## 基于 Properties 文件装载外部化配置 + +注解驱动 + +- @org.springframework.context.annotation.PropertySource +- @org.springframework.context.annotation.PropertySources + +API 编程 + +- org.springframework.core.env.PropertySource +- org.springframework.core.env.PropertySources + +## 基于 YAML 文件装载外部化配置 + +API 编程 + +- org.springframework.beans.factory.config.YamlProcessor + - org.springframework.beans.factory.config.YamlMapFactoryBean + - org.springframework.beans.factory.config.YamlPropertiesFactoryBean + +## 问题 + +**Spring 內建 XML Schema 常见有哪些**? + +| 命名空间 | 所属模块 | Schema 资源 URL | +| -------- | -------------- | ----------------------------------------------------------------- | +| beans | spring-beans | https://www.springframework.org/schema/beans/spring-beans.xsd | +| context | spring-context | https://www.springframework.org/schema/context/spring-context.xsd | +| aop | spring-aop | https://www.springframework.org/schema/aop/spring-aop.xsd | +| tx | spring-tx | https://www.springframework.org/schema/tx/spring-tx.xsd | +| util | spring-beans | beans https://www.springframework.org/schema/util/spring-util.xsd | +| tool | spring-beans | https://www.springframework.org/schema/tool/spring-tool.xsd | + +**Spring 配置元信息具体有哪些**? + +- Bean 配置元信息:通过媒介(如 XML、Proeprties 等),解析 BeanDefinition +- IoC 容器配置元信息:通过媒介(如 XML、Proeprties 等),控制 IoC 容器行为,比如注解驱动、AOP 等 +- 外部化配置:通过资源抽象(如 Proeprties、YAML 等),控制 PropertySource +- Spring Profile:通过外部化配置,提供条件分支流程 + +**Extensible XML authoring 的缺点**? + +- 高复杂度:开发人员需要熟悉 XML Schema,spring.handlers,spring.schemas 以及 Spring API +- 嵌套元素支持较弱:通常需要使用方法递归或者其嵌套解析的方式处理嵌套(子)元素 +- XML 处理性能较差:Spring XML 基于 DOM Level 3 API 实现,该 API 便于理解,然而性能较差 +- XML 框架移植性差:很难适配高性能和便利性的 XML 框架,如 JAXB + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/09.Spring\345\272\224\347\224\250\344\270\212\344\270\213\346\226\207\347\224\237\345\221\275\345\221\250\346\234\237.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/09.Spring\345\272\224\347\224\250\344\270\212\344\270\213\346\226\207\347\224\237\345\221\275\345\221\250\346\234\237.md" new file mode 100644 index 00000000..fea4b398 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/09.Spring\345\272\224\347\224\250\344\270\212\344\270\213\346\226\207\347\224\237\345\221\275\345\221\250\346\234\237.md" @@ -0,0 +1,171 @@ +--- +title: Spring 应用上下文生命周期 +date: 2022-12-23 09:58:09 +order: 09 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/ad472e/ +--- + +# Spring 应用上下文生命周期 + +## Spring 应用上下文启动准备阶段 + +AbstractApplicationContext#prepareRefresh() 方法 + +- 启动时间 - startupDate +- 状态标识 - closed(false)、active(true) +- 初始化 PropertySources - initPropertySources() +- 检验 Environment 中必须属性 +- 初始化事件监听器集合 +- 初始化早期 Spring 事件集合 + +## BeanFactory 创建阶段 + +AbstractApplicationContext#obtainFreshBeanFactory() 方法 + +- 刷新 Spring 应用上下文底层 BeanFactory - refreshBeanFactory() + - 销毁或关闭 BeanFactory,如果已存在的话 + - 创建 BeanFactory - createBeanFactory() + - 设置 BeanFactory Id + - 设置“是否允许 BeanDefinition 重复定义” - customizeBeanFactory(DefaultListableBeanFactory) + - 设置“是否允许循环引用(依赖)” - customizeBeanFactory(DefaultListableBeanFactory) + - 加载 BeanDefinition - loadBeanDefinitions(DefaultListableBeanFactory) 方法 + - 关联新建 BeanFactory 到 Spring 应用上下文 +- 返回 Spring 应用上下文底层 BeanFactory - getBeanFactory() + +## BeanFactory 准备阶段 + +AbstractApplicationContext#prepareBeanFactory(ConfigurableListableBeanFactory) 方法 + +- 关联 ClassLoader +- 设置 Bean 表达式处理器 +- 添加 PropertyEditorRegistrar 实现 - ResourceEditorRegistrar +- 添加 Aware 回调接口 BeanPostProcessor 实现 - ApplicationContextAwareProcessor +- 忽略 Aware 回调接口作为依赖注入接口 +- 注册 ResolvableDependency 对象 - BeanFactory、ResourceLoader、ApplicationEventPublisher 以及 ApplicationContext +- 注册 ApplicationListenerDetector 对象 +- 注册 LoadTimeWeaverAwareProcessor 对象 +- 注册单例对象 - Environment、Java System Properties 以及 OS 环境变量 + +## BeanFactory 后置处理阶段 + +- AbstractApplicationContext#postProcessBeanFactory(ConfigurableListableBeanFactory) 方法 + - 由子类覆盖该方法 +- AbstractApplicationContext#invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory 方法 + - 调用 BeanFactoryPostProcessor 或 BeanDefinitionRegistry 后置处理方法 + - 注册 LoadTimeWeaverAwareProcessor 对象 + +## BeanFactory 注册 BeanPostProcessor 阶段 + +AbstractApplicationContext#registerBeanPostProcessors(ConfigurableListableBeanFactory) 方法 + +- 注册 PriorityOrdered 类型的 BeanPostProcessor Beans +- 注册 Ordered 类型的 BeanPostProcessor Beans +- 注册普通 BeanPostProcessor Beans +- 注册 MergedBeanDefinitionPostProcessor Beans +- 注册 ApplicationListenerDetector 对象 + +## 初始化內建 Bean:MessageSource + +AbstractApplicationContext#initMessageSource() 方法 + +## 初始化內建 Bean:Spring 事件广播器 + +AbstractApplicationContext#initApplicationEventMulticaster() 方法 + +## Spring 应用上下文刷新阶段 + +AbstractApplicationContext#onRefresh() 方法 + +子类覆盖该方法 + +- org.springframework.web.context.support.AbstractRefreshableWebApplicationContext#onRefresh() +- org.springframework.web.context.support.GenericWebApplicationContext#onRefresh() +- org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext#onRefresh() +- org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh() +- org.springframework.web.context.support.StaticWebApplicationContext#onRefresh() + +## Spring 事件监听器注册阶段 + +AbstractApplicationContext#registerListeners() 方法 + +- 添加当前应用上下文所关联的 ApplicationListener 对象(集合) +- 添加 BeanFactory 所注册 ApplicationListener Beans +- 广播早期 Spring 事件 + +## BeanFactory 初始化完成阶段 + +AbstractApplicationContext#finishBeanFactoryInitialization(ConfigurableListableBeanFactory) 方法 + +- BeanFactory 关联 ConversionService Bean,如果存在 +- 添加 StringValueResolver 对象 +- 依赖查找 LoadTimeWeaverAware Bean +- BeanFactory 临时 ClassLoader 置为 null +- BeanFactory 冻结配置 +- BeanFactory 初始化非延迟单例 Beans + +## Spring 应用上下刷新完成阶段 + +AbstractApplicationContext#finishRefresh() 方法 + +- 清除 ResourceLoader 缓存 - clearResourceCaches() @since 5.0 +- 初始化 LifecycleProcessor 对象 - initLifecycleProcessor() +- 调用 LifecycleProcessor#onRefresh() 方法 +- 发布 Spring 应用上下文已刷新事件 - ContextRefreshedEvent +- 向 MBeanServer 托管 Live Beans + +## Spring 应用上下文启动阶段 + +AbstractApplicationContext#start() 方法 + +- 启动 LifecycleProcessor + - 依赖查找 Lifecycle Beans + - 启动 Lifecycle Beans +- 发布 Spring 应用上下文已启动事件 - ContextStartedEvent + +## Spring 应用上下文停止阶段 + +AbstractApplicationContext#stop() 方法 + +- 停止 LifecycleProcessor + - 依赖查找 Lifecycle Beans + - 停止 Lifecycle Beans +- 发布 Spring 应用上下文已停止事件 - ContextStoppedEvent + +## Spring 应用上下文关闭阶段 + +AbstractApplicationContext#close() 方法 + +- 状态标识:active(false)、closed(true) +- Live Beans JMX 撤销托管 + - LiveBeansView.unregisterApplicationContext(ConfigurableApplicationContext) +- 发布 Spring 应用上下文已关闭事件 - ContextClosedEvent +- 关闭 LifecycleProcessor + - 依赖查找 Lifecycle Beans + - 停止 Lifecycle Beans +- 销毁 Spring Beans +- 关闭 BeanFactory +- 回调 onClose() +- 注册 Shutdown Hook 线程(如果曾注册) + +## 问题 + +**Spring 应用上下文生命周期有哪些阶段**? + +- 刷新阶段 - ConfigurableApplicationContext#refresh() +- 启动阶段 - ConfigurableApplicationContext#start() +- 停止阶段 - ConfigurableApplicationContext#stop() +- 关闭阶段 - ConfigurableApplicationContext#close() + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/10.SpringAop.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/10.SpringAop.md" new file mode 100644 index 00000000..1a89468a --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/10.SpringAop.md" @@ -0,0 +1,399 @@ +--- +title: Spring AOP +date: 2020-02-26 23:47:47 +order: 10 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - AOP +permalink: /pages/53aedb/ +--- + +# Spring AOP + +## AOP 概念 + +### 什么是 AOP + +AOP(Aspect-Oriented Programming,即 **面向切面编程**)与 OOP( Object-Oriented Programming,面向对象编程) 相辅相成,提供了与 OOP 不同的抽象软件结构的视角。 + +在 OOP 中,我们以类(class)作为我们的基本单元,而 AOP 中的基本单元是 **Aspect(切面)** + +### 术语 + +#### Aspect(切面) + +`aspect` 由 `pointcount` 和 `advice` 组成, 它既包含了横切逻辑的定义, 也包括了连接点的定义. Spring AOP 就是负责实施切面的框架, 它将切面所定义的横切逻辑织入到切面所指定的连接点中. +AOP 的工作重心在于如何将增强织入目标对象的连接点上, 这里包含两个工作: + +1. 如何通过 pointcut 和 advice 定位到特定的 joinpoint 上 +2. 如何在 advice 中编写切面代码. + +**可以简单地认为, 使用 @Aspect 注解的类就是切面.** + +#### advice(增强) + +由 aspect 添加到特定的 join point(即满足 point cut 规则的 join point) 的一段代码. +许多 AOP 框架, 包括 Spring AOP, 会将 advice 模拟为一个拦截器(interceptor), 并且在 join point 上维护多个 advice, 进行层层拦截. +例如 HTTP 鉴权的实现, 我们可以为每个使用 RequestMapping 标注的方法织入 advice, 当 HTTP 请求到来时, 首先进入到 advice 代码中, 在这里我们可以分析这个 HTTP 请求是否有相应的权限, 如果有, 则执行 Controller, 如果没有, 则抛出异常. 这里的 advice 就扮演着鉴权拦截器的角色了. + +#### 连接点(join point) + +> a point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution. + +程序运行中的一些时间点, 例如一个方法的执行, 或者是一个异常的处理. +`在 Spring AOP 中, join point 总是方法的执行点, 即只有方法连接点.` + +#### 切点(point cut) + +匹配 join point 的谓词(a predicate that matches join points). +Advice 是和特定的 point cut 关联的, 并且在 point cut 相匹配的 join point 中执行. +`在 Spring 中, 所有的方法都可以认为是 joinpoint, 但是我们并不希望在所有的方法上都添加 Advice, 而 pointcut 的作用就是提供一组规则(使用 AspectJ pointcut expression language 来描述) 来匹配joinpoint, 给满足规则的 joinpoint 添加 Advice.` + +#### 关于 join point 和 point cut 的区别 + +在 Spring AOP 中, 所有的方法执行都是 join point. 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 因此 join point 和 point cut 本质上就是两个不同纬度上的东西. +`advice 是在 join point 上执行的, 而 point cut 规定了哪些 join point 可以执行哪些 advice` + +#### introduction + +为一个类型添加额外的方法或字段. Spring AOP 允许我们为 `目标对象` 引入新的接口(和对应的实现). 例如我们可以使用 introduction 来为一个 bean 实现 IsModified 接口, 并以此来简化 caching 的实现. + +#### 目标对象(Target) + +织入 advice 的目标对象. 目标对象也被称为 `advised object`. +`因为 Spring AOP 使用运行时代理的方式来实现 aspect, 因此 adviced object 总是一个代理对象(proxied object)` +`注意, adviced object 指的不是原来的类, 而是织入 advice 后所产生的代理类.` + +#### AOP proxy + +一个类被 AOP 织入 advice, 就会产生一个结果类, 它是融合了原类和增强逻辑的代理类. +在 Spring AOP 中, 一个 AOP 代理是一个 JDK 动态代理对象或 CGLIB 代理对象. + +#### 织入(Weaving) + +将 aspect 和其他对象连接起来, 并创建 adviced object 的过程. +根据不同的实现技术, AOP 织入有三种方式: + +- 编译器织入, 这要求有特殊的 Java 编译器. +- 类装载期织入, 这需要有特殊的类装载器. +- 动态代理织入, 在运行期为目标类添加增强(Advice)生成子类的方式. + Spring 采用动态代理织入, 而 AspectJ 采用编译器织入和类装载期织入. + +### advice 的类型 + +- before advice, 在 join point 前被执行的 advice. 虽然 before advice 是在 join point 前被执行, 但是它并不能够阻止 join point 的执行, 除非发生了异常(即我们在 before advice 代码中, 不能人为地决定是否继续执行 join point 中的代码) +- after return advice, 在一个 join point 正常返回后执行的 advice +- after throwing advice, 当一个 join point 抛出异常后执行的 advice +- after(final) advice, 无论一个 join point 是正常退出还是发生了异常, 都会被执行的 advice. +- around advice, 在 join point 前和 joint point 退出后都执行的 advice. 这个是最常用的 advice. + +### 关于 AOP Proxy + +Spring AOP 默认使用标准的 JDK 动态代理(dynamic proxy)技术来实现 AOP 代理, 通过它, 我们可以为任意的接口实现代理. +`如果需要为一个类实现代理, 那么可以使用 CGLIB 代理.` 当一个业务逻辑对象没有实现接口时, 那么 Spring AOP 就默认使用 CGLIB 来作为 AOP 代理了. 即如果我们需要为一个方法织入 advice, 但是这个方法不是一个接口所提供的方法, 则此时 Spring AOP 会使用 CGLIB 来实现动态代理. 鉴于此, Spring AOP 建议基于接口编程, 对接口进行 AOP 而不是类. + +### 彻底理解 aspect, join point, point cut, advice + +看完了上面的理论部分知识, 我相信还是会有不少朋友感觉到 AOP 的概念还是很模糊, 对 AOP 中的各种概念理解的还不是很透彻. 其实这很正常, 因为 AOP 中的概念是在是太多了, 我当时也是花了老大劲才梳理清楚的. +下面我以一个简单的例子来比喻一下 AOP 中 aspect, jointpoint, pointcut 与 advice 之间的关系. + +让我们来假设一下, 从前有一个叫爪哇的小县城, 在一个月黑风高的晚上, 这个县城中发生了命案. 作案的凶手十分狡猾, 现场没有留下什么有价值的线索. 不过万幸的是, 刚从隔壁回来的老王恰好在这时候无意中发现了凶手行凶的过程, 但是由于天色已晚, 加上凶手蒙着面, 老王并没有看清凶手的面目, 只知道凶手是个男性, 身高约七尺五寸. 爪哇县的县令根据老王的描述, 对守门的士兵下命令说: 凡是发现有身高七尺五寸的男性, 都要抓过来审问. 士兵当然不敢违背县令的命令, 只好把进出城的所有符合条件的人都抓了起来. + +来让我们看一下上面的一个小故事和 AOP 到底有什么对应关系. +首先我们知道, 在 Spring AOP 中 join point 指代的是所有方法的执行点, 而 point cut 是一个描述信息, 它修饰的是 join point, 通过 point cut, 我们就可以确定哪些 join point 可以被织入 Advice. 对应到我们在上面举的例子, 我们可以做一个简单的类比, join point 就相当于 **爪哇的小县城里的百姓**, point cut 就相当于 **老王所做的指控, 即凶手是个男性, 身高约七尺五寸**, 而 advice 则是施加在符合老王所描述的嫌疑人的动作: **抓过来审问**. +为什么可以这样类比呢? + +- join point --> 爪哇的小县城里的百姓: 因为根据定义, join point 是所有可能被织入 advice 的候选的点, 在 Spring AOP 中, 则可以认为所有方法执行点都是 join point. 而在我们上面的例子中, 命案发生在小县城中, 按理说在此县城中的所有人都有可能是嫌疑人. +- point cut --> 男性, 身高约七尺五寸: 我们知道, 所有的方法(joint point) 都可以织入 advice, 但是我们并不希望在所有方法上都织入 advice, 而 pointcut 的作用就是提供一组规则来匹配 joinpoint, 给满足规则的 joinpoint 添加 advice. 同理, 对于县令来说, 他再昏庸, 也知道不能把县城中的所有百姓都抓起来审问, 而是根据`凶手是个男性, 身高约七尺五寸`, 把符合条件的人抓起来. 在这里`凶手是个男性, 身高约七尺五寸` 就是一个修饰谓语, 它限定了凶手的范围, 满足此修饰规则的百姓都是嫌疑人, 都需要抓起来审问. +- advice --> 抓过来审问, advice 是一个动作, 即一段 Java 代码, 这段 Java 代码是作用于 point cut 所限定的那些 join point 上的. 同理, 对比到我们的例子中, `抓过来审问` 这个动作就是对作用于那些满足 `男性, 身高约七尺五寸` 的`爪哇的小县城里的百姓`. +- aspect: aspect 是 point cut 与 advice 的组合, 因此在这里我们就可以类比: **"根据老王的线索, 凡是发现有身高七尺五寸的男性, 都要抓过来审问"** 这一整个动作可以被认为是一个 aspect. + +或则我们也可以从语法的角度来简单类比一下. 我们在学英语时, 经常会接触什么 `定语`, `被动句` 之类的概念, 那么可以做一个不严谨的类比, 即 `joinpoint` 可以认为是一个 `宾语`, 而 `pointcut` 则可以类比为修饰 `joinpoint` 的定语, 那么整个 `aspect` 就可以描述为: `满足 pointcut 规则的 joinpoint 会被添加相应的 advice 操作.` + +## @AspectJ 支持 + +**`@AspectJ`** 是一种使用 Java 注解来实现 AOP 的编码风格。 + +@AspectJ 风格的 AOP 是 AspectJ Project 在 AspectJ 5 中引入的, 并且 Spring 也支持 @AspectJ 的 AOP 风格. + +### 使能 @AspectJ 支持 + +@AspectJ 可以以 XML 的方式或以注解的方式来使能, 并且不论以哪种方式使能@ASpectJ, 我们都必须保证 aspectjweaver.jar 在 classpath 中. + +#### 使用 Java Configuration 方式使能@AspectJ + +```java +@Configuration +@EnableAspectJAutoProxy +public class AppConfig { +} +``` + +#### 使用 XML 方式使能@AspectJ + +``` + +``` + +### 定义 aspect(切面) + +当使用注解 **@Aspect** 标注一个 Bean 后, 那么 Spring 框架会自动收集这些 Bean, 并添加到 Spring AOP 中, 例如: + +```java +@Component +@Aspect +public class MyTest { +} +``` + +`注意, 仅仅使用@Aspect 注解, 并不能将一个 Java 对象转换为 Bean, 因此我们还需要使用类似 @Component 之类的注解.` +`注意, 如果一个 类被@Aspect 标注, 则这个类就不能是其他 aspect 的 **advised object** 了, 因为使用 @Aspect 后, 这个类就会被排除在 auto-proxying 机制之外.` + +### 声明 pointcut + +一个 pointcut 的声明由两部分组成: + +- 一个方法签名, 包括方法名和相关参数 +- 一个 pointcut 表达式, 用来指定哪些方法执行是我们感兴趣的(即因此可以织入 advice). + +在@AspectJ 风格的 AOP 中, 我们使用一个方法来描述 pointcut, 即: + +```java +@Pointcut("execution(* com.xys.service.UserService.*(..))") // 切点表达式 +private void dataAccessOperation() {} // 切点前面 +``` + +`这个方法必须无返回值.` +`这个方法本身就是 pointcut signature, pointcut 表达式使用@Pointcut 注解指定.` +上面我们简单地定义了一个 pointcut, 这个 pointcut 所描述的是: 匹配所有在包 **com.xys.service.UserService** 下的所有方法的执行. + +#### 切点标志符(designator) + +AspectJ5 的切点表达式由标志符(designator)和操作参数组成. 如 "execution(\* greetTo(..))" 的切点表达式, \*\*execution** 就是 标志符, 而圆括号里的 \*\*\***greetTo(..) 就是操作参数 + +##### execution + +匹配 join point 的执行, 例如 "execution(\* hello(..))" 表示匹配所有目标类中的 hello() 方法. 这个是最基本的 pointcut 标志符. + +##### within + +匹配特定包下的所有 join point, 例如 `within(com.xys.*)` 表示 com.xys 包中的所有连接点, 即包中的所有类的所有方法. 而`within(com.xys.service.*Service)` 表示在 com.xys.service 包中所有以 Service 结尾的类的所有的连接点. + +##### this 与 target + +this 的作用是匹配一个 bean, 这个 bean(Spring AOP proxy) 是一个给定类型的实例(instance of). 而 target 匹配的是一个目标对象(target object, 即需要织入 advice 的原始的类), 此对象是一个给定类型的实例(instance of). + +##### bean + +匹配 bean 名字为指定值的 bean 下的所有方法, 例如: + +``` +bean(*Service) // 匹配名字后缀为 Service 的 bean 下的所有方法 +bean(myService) // 匹配名字为 myService 的 bean 下的所有方法 +``` + +##### args + +匹配参数满足要求的的方法. +例如: + +```java +@Pointcut("within(com.xys.demo2.*)") +public void pointcut2() { +} + +@Before(value = "pointcut2() && args(name)") +public void doSomething(String name) { + logger.info("---page: {}---", name); +} +``` + +```java +@Service +public class NormalService { + private Logger logger = LoggerFactory.getLogger(getClass()); + + public void someMethod() { + logger.info("---NormalService: someMethod invoked---"); + } + + public String test(String name) { + logger.info("---NormalService: test invoked---"); + return "服务一切正常"; + } +} +``` + +当 NormalService.test 执行时, 则 advice `doSomething` 就会执行, test 方法的参数 name 就会传递到 `doSomething` 中. + +常用例子: + +```java +// 匹配只有一个参数 name 的方法 +@Before(value = "aspectMethod() && args(name)") +public void doSomething(String name) { +} + +// 匹配第一个参数为 name 的方法 +@Before(value = "aspectMethod() && args(name, ..)") +public void doSomething(String name) { +} + +// 匹配第二个参数为 name 的方法 +Before(value = "aspectMethod() && args(*, name, ..)") +public void doSomething(String name) { +} +``` + +##### @annotation + +匹配由指定注解所标注的方法, 例如: + +```java +@Pointcut("@annotation(com.xys.demo1.AuthChecker)") +public void pointcut() { +} +``` + +则匹配由注解 `AuthChecker` 所标注的方法. + +#### 常见的切点表达式 + +##### 匹配方法签名 + +``` +// 匹配指定包中的所有的方法 +execution(* com.xys.service.*(..)) + +// 匹配当前包中的指定类的所有方法 +execution(* UserService.*(..)) + +// 匹配指定包中的所有 public 方法 +execution(public * com.xys.service.*(..)) + +// 匹配指定包中的所有 public 方法, 并且返回值是 int 类型的方法 +execution(public int com.xys.service.*(..)) + +// 匹配指定包中的所有 public 方法, 并且第一个参数是 String, 返回值是 int 类型的方法 +execution(public int com.xys.service.*(String name, ..)) +``` + +##### 匹配类型签名 + +``` +// 匹配指定包中的所有的方法, 但不包括子包 +within(com.xys.service.*) + +// 匹配指定包中的所有的方法, 包括子包 +within(com.xys.service..*) + +// 匹配当前包中的指定类中的方法 +within(UserService) + + +// 匹配一个接口的所有实现类中的实现的方法 +within(UserDao+) +``` + +##### 匹配 Bean 名字 + +``` +// 匹配以指定名字结尾的 Bean 中的所有方法 +bean(*Service) +``` + +##### 切点表达式组合 + +``` +// 匹配以 Service 或 ServiceImpl 结尾的 bean +bean(*Service || *ServiceImpl) + +// 匹配名字以 Service 结尾, 并且在包 com.xys.service 中的 bean +bean(*Service) && within(com.xys.service.*) +``` + +### 声明 advice + +advice 是和一个 pointcut 表达式关联在一起的, 并且会在匹配的 join point 的方法执行的前/后/周围 运行. `pointcut 表达式可以是简单的一个 pointcut 名字的引用, 或者是完整的 pointcut 表达式`. +下面我们以几个简单的 advice 为例子, 来看一下一个 advice 是如何声明的. + +#### Before advice + +```java +/** + * @author xiongyongshun + * @version 1.0 + * @created 16/9/9 13:13 + */ +@Component +@Aspect +public class BeforeAspectTest { + // 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise. + @Pointcut("execution(* com.xys.service.UserService.*(..))") + public void dataAccessOperation() { + } +} +``` + +```java +@Component +@Aspect +public class AdviseDefine { + // 定义 advise + @Before("com.xys.aspect.PointcutDefine.dataAccessOperation()") + public void doBeforeAccessCheck(JoinPoint joinPoint) { + System.out.println("*****Before advise, method: " + joinPoint.getSignature().toShortString() + " *****"); + } +} +``` + +这里, **@Before** 引用了一个 pointcut, 即 "com.xys.aspect.PointcutDefine.dataAccessOperation()" 是一个 pointcut 的名字. +如果我们在 advice 在内置 pointcut, 则可以: + +```java +@Component +@Aspect +public class AdviseDefine { + // 将 pointcut 和 advice 同时定义 + @Before("within(com.xys.service..*)") + public void doAccessCheck(JoinPoint joinPoint) { + System.out.println("*****doAccessCheck, Before advise, method: " + joinPoint.getSignature().toShortString() + " *****"); + } +} +``` + +#### around advice + +around advice 比较特别, 它可以在一个方法的之前之前和之后添加不同的操作, 并且甚至可以决定何时, 如何, 是否调用匹配到的方法. + +```java +@Component +@Aspect +public class AdviseDefine { + // 定义 advise + @Around("com.xys.aspect.PointcutDefine.dataAccessOperation()") + public Object doAroundAccessCheck(ProceedingJoinPoint pjp) throws Throwable { + StopWatch stopWatch = new StopWatch(); + stopWatch.start(); + // 开始 + Object retVal = pjp.proceed(); + stopWatch.stop(); + // 结束 + System.out.println("invoke method: " + pjp.getSignature().getName() + ", elapsed time: " + stopWatch.getTotalTimeMillis()); + return retVal; + } +} +``` + +around advice 和前面的 before advice 差不多, 只是我们把注解 **@Before** 改为了 **@Around** 了. + +## 参考资料 + +- [《 Spring 实战(第 4 版)》](https://item.jd.com/11899370.html) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/20.Spring\350\265\204\346\272\220\347\256\241\347\220\206.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/20.Spring\350\265\204\346\272\220\347\256\241\347\220\206.md" new file mode 100644 index 00000000..b285f663 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/20.Spring\350\265\204\346\272\220\347\256\241\347\220\206.md" @@ -0,0 +1,261 @@ +--- +title: Spring 资源管理 +date: 2019-09-04 19:46:41 +order: 20 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - Resource +permalink: /pages/a1549f/ +--- + +# Spring 资源管理 + +> Version 6.0.3 + +## Resource 接口 + +相对标准 URL 访问机制,Spring 的 `org.springframework.core.io.Resource` 接口抽象了对底层资源的访问接口,提供了一套更好的访问方式。 + +```java +public interface Resource extends InputStreamSource { + + boolean exists(); + + boolean isReadable(); + + boolean isOpen(); + + boolean isFile(); + + URL getURL() throws IOException; + + URI getURI() throws IOException; + + File getFile() throws IOException; + + ReadableByteChannel readableChannel() throws IOException; + + long contentLength() throws IOException; + + long lastModified() throws IOException; + + Resource createRelative(String relativePath) throws IOException; + + String getFilename(); + + String getDescription(); +} +``` + +正如 `Resource` 接口的定义所示,它扩展了 `InputStreamSource` 接口。`Resource` 最核心的方法如下: + +- `getInputStream()` - 定位并且打开当前资源,返回当前资源的 `InputStream`。每次调用都会返回一个新的 `InputStream`。调用者需要负责关闭流。 +- `exists()` - 判断当前资源是否真的存在。 +- `isOpen()` - 判断当前资源是否是一个已打开的 `InputStream`。如果为 true,则 `InputStream` 不能被多次读取,必须只读取一次然后关闭以避免资源泄漏。对所有常用资源实现返回 false,`InputStreamResource` 除外。 +- `getDescription()` - 返回当前资源的描述,当处理资源出错时,资源的描述会用于错误信息的输出。一般来说,资源的描述是一个完全限定的文件名称,或者是当前资源的真实 URL。 + +常见 Spring 资源接口: + +| 类型 | 接口 | +| ---------- | ----------------------------------------------------- | +| 输入流 | `org.springframework.core.io.InputStreamSource` | +| 只读资源 | `org.springframework.core.io.Resource` | +| 可写资源 | `org.springframework.core.io.WritableResource` | +| 编码资源 | `org.springframework.core.io.support.EncodedResource` | +| 上下文资源 | `org.springframework.core.io.ContextResource` | + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221223155859.png) + +## 内置的 Resource 实现 + +Spring 包括几个内置的 Resource 实现: + +| 资源来源 | 前缀 | 说明 | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [`UrlResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-urlresource) | `file:`、`https:`、`ftp:` 等 | `UrlResource` 封装了一个 `java.net.URL` 对象,**用于访问可通过 URL 访问的任何对象**,例如文件、HTTPS 目标、FTP 目标等。所有 URL 都可以通过标准化的字符串形式表示,因此可以使用适当的标准化前缀来指示一种 URL 类型与另一种 URL 类型的区别。 这包括:`file`:用于访问文件系统路径;`https`:用于通过 HTTPS 协议访问资源;`ftp`:用于通过 FTP 访问资源等等。 | +| [`ClassPathResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-classpathresource) | `classpath:` | `ClassPathResource` **从类路径上加载资源**。它使用线程上下文加载器、给定的类加载器或指定的 class 类型中的任意一个来加载资源。 | +| [`FileSystemResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-filesystemresource) | `file:` | `FileSystemResource` **是 `java.io.File` 的资源实现**。它还支持 `java.nio.file.Path` ,应用 Spring 的标准对字符串路径进行转换。`FileSystemResource` 支持解析为文件和 URL。 | +| [`PathResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-pathresource) | 无 | `PathResource` 是 `java.nio.file.Path` 的资源实现。 | +| [`ServletContextResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-servletcontextresource) | 无 | `ServletContextResource` **是 `ServletContext` 的资源实现**。它表示相应 Web 应用程序根目录中的相对路径。 | +| [`InputStreamResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-inputstreamresource) | 无 | `InputStreamResource` **是指定 `InputStream` 的资源实现**。注意:如果该 `InputStream` 已被打开,则不可以多次读取该流。 | +| [`ByteArrayResource`](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#resources-implementations-bytearrayresource) | 无 | `ByteArrayResource` 是指定的二进制数组的资源实现。它会为给定的字节数组创建一个 `ByteArrayInputStream`。 | + +## ResourceLoader 接口 + +`ResourceLoader` 接口用于加载 `Resource` 对象。其定义如下: + +```java +public interface ResourceLoader { + + Resource getResource(String location); + + ClassLoader getClassLoader(); +} +``` + +Spring 中主要的 ResourceLoader 实现: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20221223164745.png) + +Spring 中,所有的 `ApplicationContext` 都实现了 `ResourceLoader` 接口。因此,所有 `ApplicationContext` 都可以通过 `getResource()` 方法获取 `Resource` 实例。 + +【示例】 + +```java +// 如果没有指定资源前缀,Spring 会尝试返回合适的资源 +Resource template = ctx.getResource("some/resource/path/myTemplate.txt"); +// 如果指定 classpath: 前缀,Spring 会强制使用 ClassPathResource +Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt"); +// 如果指定 file:、http 等 URL 前缀,Spring 会强制使用 UrlResource +Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt"); +Resource template = ctx.getResource("http://myhost.com/resource/path/myTemplate.txt"); +``` + +下表列举了 Spring 根据各种位置路径加载资源的策略: + +| 前缀 | 样例 | 说明 | +| ------------ | -------------------------------- | :----------------------------------- | +| `classpath:` | `classpath:com/myapp/config.xml` | 从类路径加载 | +| `file:` | `file:///data/config.xml` | 以 URL 形式从文件系统加载 | +| `http:` | `http://myserver/logo.png` | 以 URL 形式加载 | +| 无 | `/data/config.xml` | 由底层的 ApplicationContext 实现决定 | + +## ResourcePatternResolver 接口 + +`ResourcePatternResolver` 接口是 `ResourceLoader` 接口的扩展,它的作用是定义策略,根据位置模式解析 `Resource` 对象。 + +```java +public interface ResourcePatternResolver extends ResourceLoader { + + String CLASSPATH_ALL_URL_PREFIX = "classpath*:"; + + Resource[] getResources(String locationPattern) throws IOException; +} +``` + +`PathMatchingResourcePatternResolver` 是一个独立的实现,可以在 `ApplicationContext` 之外使用,也可以被 `ResourceArrayPropertyEditor` 用于填充 `Resource[]` bean 属性。`PathMatchingResourcePatternResolver` 能够将指定的资源位置路径解析为一个或多个匹配的 `Resource` 对象。 + +> 注意:任何标准 `ApplicationContext` 中的默认 `ResourceLoader` 实际上是 `PathMatchingResourcePatternResolver` 的一个实例,它实现了 `ResourcePatternResolver` 接口。 + +## ResourceLoaderAware 接口 + +`ResourceLoaderAware` 接口是一个特殊的回调接口,用来标记提供 `ResourceLoader` 引用的对象。`ResourceLoaderAware` 接口定义如下: + +```java +public interface ResourceLoaderAware { + void setResourceLoader(ResourceLoader resourceLoader); +} +``` + +当一个类实现 `ResourceLoaderAware` 并部署到应用程序上下文中(作为 Spring 管理的 bean)时,它会被应用程序上下文识别为 `ResourceLoaderAware`,然后,应用程序上下文会调用 `setResourceLoader(ResourceLoader)`,将自身作为参数提供(请记住,Spring 中的所有应用程序上下文都实现 `ResourceLoader` 接口)。 + +由于 `ApplicationContext` 是一个 `ResourceLoader`,该 bean 还可以实现 `ApplicationContextAware` 接口并直接使用提供的应用程序上下文来加载资源。 但是,一般来说,如果您只需要这些,最好使用专门的 `ResourceLoader` 接口。 该代码将仅耦合到资源加载接口(可以被视为实用程序接口),而不耦合到整个 Spring `ApplicationContext` 接口。 + +在应用程序中,还可以使用 `ResourceLoader` 的自动装配作为实现 `ResourceLoaderAware` 接口的替代方法。传统的构造函数和 `byType` 自动装配模式能够分别为构造函数参数或 setter 方法参数提供 `ResourceLoader`。 为了获得更大的灵活性(包括自动装配字段和多参数方法的能力),请考虑使用基于注解的自动装配功能。 在这种情况下,`ResourceLoader` 会自动连接到需要 `ResourceLoader` 类型的字段、构造函数参数或方法参数中,只要相关字段、构造函数或方法带有 `@Autowired` 注解即可。 + +## 资源依赖 + +如果 bean 本身要通过某种动态过程来确定和提供资源路径,那么 bean 可以使用 `ResourceLoader` 或 `ResourcePatternResolver` 接口来加载资源。 例如,考虑加载某种模板,其中所需的特定资源取决于用户的角色。 如果资源是静态的,完全消除 `ResourceLoader` 接口(或 `ResourcePatternResolver` 接口)的使用,让 bean 公开它需要的 `Resource` 属性,并期望将它们注入其中是有意义的。 + +使注入这些属性变得简单的原因是所有应用程序上下文都注册并使用一个特殊的 JavaBeans `PropertyEditor`,它可以将 `String` 路径转换为 `Resource` 对象。 例如,下面的 MyBean 类有一个 `Resource` 类型的模板属性。 + +【示例】 + +```xml + + + +``` + +请注意,配置中引用的模板资源路径没有前缀,因为应用程序上下文本身将用作 `ResourceLoader`,资源本身将根据需要通过 `ClassPathResource`,`FileSystemResource` 或 ServletContextResource 加载,具体取决于上下文的确切类型。 + +如果需要强制使用特定的资源类型,则可以使用前缀。 以下两个示例显示如何强制使用 `ClassPathResource` 和 `UrlResource`(后者用于访问文件系统文件)。 + +```xml + + +``` + +可以通过 `@Value` 注解加载资源文件 `myTemplate.txt`,示例如下: + +```java +@Component +public class MyBean { + + private final Resource template; + + public MyBean(@Value("${template.path}") Resource template) { + this.template = template; + } + + // ... +} +``` + +Spring 的 `PropertyEditor` 会根据资源文件的路径字符串,加载 `Resource` 对象,并将其注入到 MyBean 的构造方法。 + +如果想要加载多个资源文件,可以使用 `classpath*:` 前缀,例如:`classpath*:/config/templates/*.txt`。 + +```java +@Component +public class MyBean { + + private final Resource[] templates; + + public MyBean(@Value("${templates.path}") Resource[] templates) { + this.templates = templates; + } + + // ... +} +``` + +## 应用上下文和资源路径 + +### 构造应用上下文 + +应用上下文构造函数(针对特定的应用上下文类型)通常将字符串或字符串数组作为资源的位置路径,例如构成上下文定义的 XML 文件。 + +【示例】 + +```java +ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml"); +ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml"); +ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:conf/appContext.xml"); +ApplicationContext ctx = new ClassPathXmlApplicationContext( + new String[] {"services.xml", "daos.xml"}, MessengerService.class); +``` + +### 使用通配符构造应用上下文 + +ApplicationContext 构造器的中的资源路径可以是单一的路径(即一对一地映射到目标资源);也可以是通配符形式——可包含 classpath\*:也可以是前缀或 ant 风格的正则表达式(使用 spring 的 PathMatcher 来匹配)。 + +示例: + +```java +ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml"); +``` + +使用 `classpath*` 表示类路径下所有匹配文件名称的资源都会被获取(本质上就是调用了 ClassLoader.getResources(…) 方法),接着将获取到的资源组装成最终的应用上下文。 + +在位置路径的其余部分,`classpath*:` 前缀可以与 PathMatcher 结合使用,如:`classpath*:META-INF/*-beans.xml`。 + +## 问题 + +Spring 配置资源中有哪些常见类型? + +- XML 资源 +- Properties 资源 +- YAML 资源 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/21.Spring\346\240\241\351\252\214.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/21.Spring\346\240\241\351\252\214.md" new file mode 100644 index 00000000..d99a6fcc --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/21.Spring\346\240\241\351\252\214.md" @@ -0,0 +1,637 @@ +--- +title: Spring 校验 +date: 2022-12-22 17:42:28 +order: 21 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/fe6aad/ +--- + +# Spring 校验 + +Java API 规范(`JSR303`)定义了`Bean`校验的标准`validation-api`,但没有提供实现。`hibernate validation`是对这个规范的实现,并增加了校验注解如`@Email`、`@Length`等。`Spring Validation`是对`hibernate validation`的二次封装,用于支持`spring mvc`参数自动校验。 + +## 快速入门 + +### 引入依赖 + +如果 spring-boot 版本小于 2.3.x,spring-boot-starter-web 会自动传入 hibernate-validator 依赖。如果 spring-boot 版本大于 2.3.x,则需要手动引入依赖: + +```xml + + org.hibernate.validator + hibernate-validator-parent + 6.2.5.Final + +``` + +对于 web 服务来说,为防止非法参数对业务造成影响,在 Controller 层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式: + +- POST、PUT 请求,使用 requestBody 传递参数; +- GET 请求,使用 requestParam/PathVariable 传递参数。 + +实际上,不管是 requestBody 参数校验还是方法级别的校验,最终都是调用 Hibernate Validator 执行校验,Spring Validation 只是做了一层封装。 + +### 校验示例 + +(1)在实体上标记校验注解 + +```kotlin +@Data +@NoArgsConstructor +@AllArgsConstructor +public class User implements Serializable { + + @NotNull + private Long id; + + @NotBlank + @Size(min = 2, max = 10) + private String name; + + @Min(value = 1) + @Max(value = 100) + private Integer age; + +} +``` + +(2)在方法参数上声明校验注解 + +```less +@Slf4j +@Validated +@RestController +@RequestMapping("validate1") +public class ValidatorController { + + /** + * {@link RequestBody} 参数校验 + */ + @PostMapping(value = "save") + public DataResult save(@Valid @RequestBody User entity) { + log.info("保存一条记录:{}", JSONUtil.toJsonStr(entity)); + return DataResult.ok(true); + } + + /** + * {@link RequestParam} 参数校验 + */ + @GetMapping(value = "queryByName") + public DataResult queryByName( + @RequestParam("username") + @NotBlank + @Size(min = 2, max = 10) + String name + ) { + User user = new User(1L, name, 18); + return DataResult.ok(user); + } + + /** + * {@link PathVariable} 参数校验 + */ + @GetMapping(value = "detail/{id}") + public DataResult detail(@PathVariable("id") @Min(1L) Long id) { + User user = new User(id, "李四", 18); + return DataResult.ok(user); + } + +} +``` + +(3)如果请求参数不满足校验规则,则会抛出 `ConstraintViolationException` 或 `MethodArgumentNotValidException` 异常。 + +### 统一异常处理 + +在实际项目开发中,通常会用统一异常处理来返回一个更友好的提示。 + +```java +@Slf4j +@ControllerAdvice +public class GlobalExceptionHandler { + + /** + * 处理所有不可知的异常 + */ + @ResponseBody + @ResponseStatus(HttpStatus.OK) + @ExceptionHandler(Throwable.class) + public Result handleException(Throwable e) { + log.error("未知异常", e); + return new Result(ResultStatus.HTTP_SERVER_ERROR.getCode(), e.getMessage()); + } + + /** + * 统一处理请求参数校验异常(普通传参) + * + * @param e ConstraintViolationException + * @return {@link DataResult} + */ + @ResponseBody + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({ ConstraintViolationException.class }) + public Result handleConstraintViolationException(final ConstraintViolationException e) { + log.error("ConstraintViolationException", e); + List errors = new ArrayList<>(); + for (ConstraintViolation violation : e.getConstraintViolations()) { + Path path = violation.getPropertyPath(); + List pathArr = StrUtil.split(path.toString(), ','); + errors.add(pathArr.get(0) + " " + violation.getMessage()); + } + return new Result(ResultStatus.REQUEST_ERROR.getCode(), CollectionUtil.join(errors, ",")); + } + + /** + * 处理参数校验异常 + * + * @param e MethodArgumentNotValidException + * @return {@link DataResult} + */ + @ResponseBody + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler({ MethodArgumentNotValidException.class }) + private Result handleMethodArgumentNotValidException(final MethodArgumentNotValidException e) { + log.error("MethodArgumentNotValidException", e); + List errors = new ArrayList<>(); + for (ObjectError error : e.getBindingResult().getAllErrors()) { + errors.add(((FieldError) error).getField() + " " + error.getDefaultMessage()); + } + return new Result(ResultStatus.REQUEST_ERROR.getCode(), CollectionUtil.join(errors, ",")); + } + +} +``` + +## 进阶使用 + +### 分组校验 + +在实际项目中,可能多个方法需要使用同一个 DTO 类来接收参数,而不同方法的校验规则很可能是不一样的。这个时候,简单地在 DTO 类的字段上加约束注解无法解决这个问题。因此,spring-validation 支持了分组校验的功能,专门用来解决这类问题。 + +(1)定义分组 + +```java +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface AddCheck { } + +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface EditCheck { } +``` + +(2)在实体上标记校验注解 + +```less +@Data +public class User2 { + + @NotNull(groups = EditCheck.class) + private Long id; + + @NotNull(groups = { AddCheck.class, EditCheck.class }) + @Size(min = 2, max = 10, groups = { AddCheck.class, EditCheck.class }) + private String name; + + @IsMobile(message = "不是有效手机号", groups = { AddCheck.class, EditCheck.class }) + private String mobile; + +} +``` + +(3)在方法上根据不同场景进行校验分组 + +```less +@Slf4j +@Validated +@RestController +@RequestMapping("validate2") +public class ValidatorController2 { + + /** + * {@link RequestBody} 参数校验 + */ + @PostMapping(value = "add") + public DataResult add(@Validated(AddCheck.class) @RequestBody User2 entity) { + log.info("添加一条记录:{}", JSONUtil.toJsonStr(entity)); + return DataResult.ok(true); + } + + /** + * {@link RequestBody} 参数校验 + */ + @PostMapping(value = "edit") + public DataResult edit(@Validated(EditCheck.class) @RequestBody User2 entity) { + log.info("编辑一条记录:{}", JSONUtil.toJsonStr(entity)); + return DataResult.ok(true); + } + +} +``` + +### 嵌套校验 + +前面的示例中,DTO 类里面的字段都是基本数据类型和 String 类型。但是实际场景中,有可能某个字段也是一个对象,这种情况先,可以使用嵌套校验。 +post +比如,上面保存 User 信息的时候同时还带有 Job 信息。需要注意的是,此时 DTO 类的对应字段必须标记@Valid 注解。 + +```less +@Data +public class UserDTO { + + @Min(value = 10000000000000000L, groups = Update.class) + private Long userId; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 2, max = 10, groups = {Save.class, Update.class}) + private String userName; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 6, max = 20, groups = {Save.class, Update.class}) + private String account; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 6, max = 20, groups = {Save.class, Update.class}) + private String password; + + @NotNull(groups = {Save.class, Update.class}) + @Valid + private Job job; + + @Data + public static class Job { + + @Min(value = 1, groups = Update.class) + private Long jobId; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 2, max = 10, groups = {Save.class, Update.class}) + private String jobName; + + @NotNull(groups = {Save.class, Update.class}) + @Length(min = 2, max = 10, groups = {Save.class, Update.class}) + private String position; + } + + /** + * 保存的时候校验分组 + */ + public interface Save { + } + + /** + * 更新的时候校验分组 + */ + public interface Update { + } +} +复制代码 +``` + +嵌套校验可以结合分组校验一起使用。还有就是嵌套集合校验会对集合里面的每一项都进行校验,例如`List`字段会对这个 list 里面的每一个 Job 对象都进行校验 + +### 自定义校验注解 + +(1)自定义校验注解 `@IsMobile` + +```less +@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) +@Retention(RUNTIME) +@Constraint(validatedBy = MobileValidator.class) +public @interface IsMobile { + + String message(); + + Class[] groups() default {}; + + Class[] payload() default {}; + +} +``` + +(2)实现 `ConstraintValidator` 接口,编写 `@IsMobile` 校验注解的解析器 + +```java +import cn.hutool.core.util.StrUtil; +import io.github.dunwu.spring.core.validation.annotation.IsMobile; +import io.github.dunwu.tool.util.ValidatorUtil; + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; + +public class MobileValidator implements ConstraintValidator { + + @Override + public void initialize(IsMobile isMobile) { } + + @Override + public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { + if (StrUtil.isBlank(s)) { + return false; + } else { + return ValidatorUtil.isMobile(s); + } + } + +} +``` + +### 自定义校验 + +可以通过实现 `org.springframework.validation.Validator` 接口来自定义校验。 + +有以下要点 + +- 实现 `supports` 方法 +- 实现 `validate` 方法 + - 通过 `Errors` 对象收集错误 + - `ObjectError`:对象(Bean)错误: + - `FieldError`:对象(Bean)属性(Property)错误 + - 通过 `ObjectError` 和 `FieldError` 关联 `MessageSource` 实现获取最终的错误文案 + +```less +package io.github.dunwu.spring.core.validation; + +import io.github.dunwu.spring.core.validation.annotation.Valid; +import io.github.dunwu.spring.core.validation.config.CustomValidatorConfig; +import io.github.dunwu.spring.core.validation.entity.Person; +import org.springframework.stereotype.Component; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Component +public class CustomValidator implements Validator { + + private final CustomValidatorConfig validatorConfig; + + public CustomValidator(CustomValidatorConfig validatorConfig) { + this.validatorConfig = validatorConfig; + } + + /** + * 本校验器只针对 Person 对象进行校验 + */ + @Override + public boolean supports(Class clazz) { + return Person.class.equals(clazz); + } + + @Override + public void validate(Object target, Errors errors) { + ValidationUtils.rejectIfEmpty(errors, "name", "name.empty"); + + List fields = getFields(target.getClass()); + for (Field field : fields) { + Annotation[] annotations = field.getAnnotations(); + for (Annotation annotation : annotations) { + if (annotation.annotationType().getAnnotation(Valid.class) != null) { + try { + ValidatorRule validatorRule = validatorConfig.findRule(annotation); + if (validatorRule != null) { + validatorRule.valid(annotation, target, field, errors); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + } + } + + private List getFields(Class clazz) { + // 声明Field数组 + List fields = new ArrayList<>(); + // 如果class类型不为空 + while (clazz != null) { + // 添加属性到属性数组 + Collections.addAll(fields, clazz.getDeclaredFields()); + clazz = clazz.getSuperclass(); + } + return fields; + } + +} +``` + +### 快速失败(Fail Fast) + +Spring Validation 默认会校验完所有字段,然后才抛出异常。可以通过一些简单的配置,开启 Fali Fast 模式,一旦校验失败就立即返回。 + +```scss +@Bean +public Validator validator() { + ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class) + .configure() + // 快速失败模式 + .failFast(true) + .buildValidatorFactory(); + return validatorFactory.getValidator(); +} +``` + +## Spring 校验原理 + +### Spring 校验使用场景 + +- Spring 常规校验(Validator) +- Spring 数据绑定(DataBinder) +- Spring Web 参数绑定(WebDataBinder) +- Spring WebMVC/WebFlux 处理方法参数校验 + +### Validator 接口设计 + +- 接口职责 + - Spring 内部校验器接口,通过编程的方式校验目标对象 +- 核心方法 + - `supports(Class)`:校验目标类能否校验 + - `validate(Object,Errors)`:校验目标对象,并将校验失败的内容输出至 Errors 对象 +- 配套组件 + - 错误收集器:`org.springframework.validation.Errors` + - Validator 工具类:`org.springframework.validation.ValidationUtils` + +### Errors 接口设计 + +- 接口职责 + - 数据绑定和校验错误收集接口,与 Java Bean 和其属性有强关联性 +- 核心方法 + - `reject` 方法(重载):收集错误文案 + - `rejectValue` 方法(重载):收集对象字段中的错误文案 +- 配套组件 + - Java Bean 错误描述:`org.springframework.validation.ObjectError` + - Java Bean 属性错误描述:`org.springframework.validation.FieldError` + +### Errors 文案来源 + +Errors 文案生成步骤 + +- 选择 Errors 实现(如:`org.springframework.validation.BeanPropertyBindingResult`) +- 调用 reject 或 rejectValue 方法 +- 获取 Errors 对象中 ObjectError 或 FieldError +- 将 ObjectError 或 FieldError 中的 code 和 args,关联 MessageSource 实现(如:`ResourceBundleMessageSource`) + +### spring web 校验原理 + +#### RequestBody 参数校验实现原理 + +在 spring-mvc 中,`RequestResponseBodyMethodProcessor` 是用于解析 `@RequestBody` 标注的参数以及处理`@ResponseBody` 标注方法的返回值的。其中,执行参数校验的逻辑肯定就在解析参数的方法 `resolveArgument()` 中: + +```java +@Override +public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + parameter = parameter.nestedIfOptional(); + Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); + String name = Conventions.getVariableNameForParameter(parameter); + + if (binderFactory != null) { + WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name); + if (arg != null) { + // 尝试进行参数校验 + validateIfApplicable(binder, parameter); + if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { + // 如果存在校验错误,则抛出 MethodArgumentNotValidException + throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); + } + } + if (mavContainer != null) { + mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); + } + } + + return adaptArgumentIfNecessary(arg, parameter); +} +``` + +可以看到,resolveArgument()调用了 validateIfApplicable()进行参数校验。 + +```java +protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) { + // 获取参数注解,如 @RequestBody、@Valid、@Validated + Annotation[] annotations = parameter.getParameterAnnotations(); + for (Annotation ann : annotations) { + // 先尝试获取 @Validated 注解 + Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); + // 如果标注了 @Validated,直接开始校验。 + // 如果没有,那么判断参数前是否有 Valid 开头的注解。 + if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { + Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); + Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); + // 执行校验 + binder.validate(validationHints); + break; + } + } +} +``` + +以上代码,就解释了 Spring 为什么能同时支持 `@Validated`、`@Valid` 两个注解。 + +接下来,看一下 WebDataBinder.validate() 的实现: + +```typescript +@Override +public void validate(Object target, Errors errors, Object... validationHints) { + if (this.targetValidator != null) { + processConstraintViolations( + // 此处调用 Hibernate Validator 执行真正的校验 + this.targetValidator.validate(target, asValidationGroups(validationHints)), errors); + } +} +``` + +通过上面代码,可以看出 Spring 校验实际上是基于 Hibernate Validator 的封装。 + +#### 方法级别的参数校验实现原理 + +Spring 支持根据方法去进行拦截、校验,原理就在于应用了 AOP 技术。具体来说,是通过 `MethodValidationPostProcessor` 动态注册 AOP 切面,然后使用 `MethodValidationInterceptor` 对切点方法织入增强。 + +```java +public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean { + @Override + public void afterPropertiesSet() { + // 为所有 @Validated 标注的 Bean 创建切面 + Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true); + // 创建 Advisor 进行增强 + this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator)); + } + + // 创建 Advice,本质就是一个方法拦截器 + protected Advice createMethodValidationAdvice(@Nullable Validator validator) { + return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor()); + } +} +``` + +接着看一下 `MethodValidationInterceptor`: + +```scss +public class MethodValidationInterceptor implements MethodInterceptor { + @Override + public Object invoke(MethodInvocation invocation) throws Throwable { + // 无需增强的方法,直接跳过 + if (isFactoryBeanMetadataMethod(invocation.getMethod())) { + return invocation.proceed(); + } + // 获取分组信息 + Class[] groups = determineValidationGroups(invocation); + ExecutableValidator execVal = this.validator.forExecutables(); + Method methodToValidate = invocation.getMethod(); + Set> result; + try { + // 方法入参校验,最终还是委托给 Hibernate Validator 来校验 + result = execVal.validateParameters( + invocation.getThis(), methodToValidate, invocation.getArguments(), groups); + } + catch (IllegalArgumentException ex) { + ... + } + // 有异常直接抛出 + if (!result.isEmpty()) { + throw new ConstraintViolationException(result); + } + // 真正的方法调用 + Object returnValue = invocation.proceed(); + // 对返回值做校验,最终还是委托给Hibernate Validator来校验 + result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups); + // 有异常直接抛出 + if (!result.isEmpty()) { + throw new ConstraintViolationException(result); + } + return returnValue; + } +} +``` + +实际上,不管是 requestBody 参数校验还是方法级别的校验,最终都是调用 Hibernate Validator 执行校验,Spring Validation 只是做了一层封装。 + +## 问题 + +**Spring 有哪些校验核心组件**? + +- 检验器:`org.springframework.validation.Validator` +- 错误收集器:`org.springframework.validation.Errors` +- Java Bean 错误描述:`org.springframework.validation.ObjectError` +- Java Bean 属性错误描述:`org.springframework.validation.FieldError` +- Bean Validation 适配:`org.springframework.validation.beanvalidation.LocalValidatorFactoryBean` + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) +- https://juejin.cn/post/6856541106626363399 \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/22.Spring\346\225\260\346\215\256\347\273\221\345\256\232.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/22.Spring\346\225\260\346\215\256\347\273\221\345\256\232.md" new file mode 100644 index 00000000..3c5fe260 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/22.Spring\346\225\260\346\215\256\347\273\221\345\256\232.md" @@ -0,0 +1,181 @@ +--- +title: Spring 数据绑定 +date: 2022-12-22 19:26:57 +order: 22 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - 数据绑定 +permalink: /pages/267b4c/ +--- + +# Spring 数据绑定 + +**Spring 数据绑定(Data Binding)的作用是将用户的输入动态绑定到 JavaBean**。换句话说,Spring 数据绑定机制是将属性值设置到目标对象中。 + +在 Spring 中,数据绑定功能主要由 `DataBinder` 类实现。此外,`BeanWrapper` 也具有类似的功能,但 `DataBinder` 额外支持字段验证、字段格式化和绑定结果分析。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20230111150930.png) + +## 快速入门 + +定义一个用于测试的 JavaBean + +```java +public class TestBean { + + private int num; + + public int getNum() { + return num; + } + + public void setNum(int num) { + this.num = num; + } + + @Override + public String toString() { + return "TestBean{" + "num=" + num + '}'; + } + +} +``` + +数据绑定示例 + +```java +public class DataBindingDemo { + + public static void main(String[] args) { + + MutablePropertyValues mpv = new MutablePropertyValues(); + mpv.add("num", "10"); + + TestBean testBean = new TestBean(); + DataBinder db = new DataBinder(testBean); + + db.bind(mpv); + System.out.println(testBean); + } + +} +``` + +## Spring 数据绑定使用场景 + +- Spring `BeanDefinition` 到 Bean 实例创建 +- Spring 数据绑定(`DataBinder`) +- Spring Web 参数绑定(`WebDataBinder`) + +## DataBinder + +在 Spring 中,`DataBinder` 类是数据绑定功能的基类。`WebDataBinder` 是 `DataBinder` 的子类,主要用于 Spring Web 数据绑定,此外,还有一些 `WebDataBinder` 的扩展子类,其类族如下图所示: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20230111152225.png) + +DataBinder 核心属性: + +| 属性 | 说明 | +| ---------------------- | ------------------------------ | +| `target` | 关联目标 Bean | +| `objectName` | 目标 Bean 名称 | +| `bindingResult` | 属性绑定结果 | +| `typeConverter` | 类型转换器 | +| `conversionService` | 类型转换服务 | +| `messageCodesResolver` | 校验错误文案 Code 处理器 | +| `validators` | 关联的 Bean Validator 实例集合 | + +`DataBinder` 类的核心方法是 `bind(PropertyValues)`:将 PropertyValues Key-Value 内容映射到关联 Bean(target)中的属性上 + +- 假设 PropertyValues 中包含 name=dunwu 的键值对时, 同时 Bean 对象 User 中存在 name 属性, 当 bind 方法执行时, User 对象中的 name 属性值将被绑定为 dunwu + +## Spring 数据绑定元数据 + +DataBinder 元数据 - PropertyValues + +| 特征 | 说明 | +| ------------ | -------------------------------------------------------------------- | +| 数据来源 | BeanDefinition,主要来源 XML 资源配置 BeanDefinition | +| 数据结构 | 由一个或多个 PropertyValue 组成 | +| 成员结构 | PropertyValue 包含属性名称,以及属性值(包括原始值、类型转换后的值) | +| 常见实现 | MutablePropertyValues | +| Web 扩展实现 | ServletConfigPropertyValues、ServletRequestParameterPropertyValues | +| 相关生命周期 | InstantiationAwareBeanPostProcessor#postProcessProperties | + +## Spring 数据绑定控制参数 + +DataBinder 绑定特殊场景分析 + +- 当 PropertyValues 中包含名称 x 的 PropertyValue,目标对象 B 不存在 x 属性,当 bind 方法执 + 行时,会发生什么? +- 当 PropertyValues 中包含名称 x 的 PropertyValue,目标对象 B 中存在 x 属性,当 bind 方法执 + 行时,如何避免 B 属性 x 不被绑定? +- 当 PropertyValues 中包含名称 x.y 的 PropertyValue,目标对象 B 中存在 x 属性(嵌套 y 属性) + ,当 bind 方法执行时,会发生什么? + +### DataBinder 绑定控制参数 + +| 参数名称 | 说明 | +| ------------------- | ---------------------------------- | +| ignoreUnknownFields | 是否忽略未知字段,默认值:true | +| ignoreInvalidFields | 是否忽略非法字段,默认值:false | +| autoGrowNestedPaths | 是否自动增加嵌套路径,默认值:true | +| allowedFields | 绑定字段白名单 | +| disallowedFields | 绑定字段黑名单 | +| requiredFields | 必须绑定字段 | + +## BeanWrapper 的使用场景 + +- Spring 底层 JavaBeans 基础设施的中心化接口 +- 通常不会直接使用,间接用于 BeanFactory 和 DataBinder +- 提供标准 JavaBeans 分析和操作,能够单独或批量存储 Java Bean 的属性(properties) +- 支持嵌套属性路径(nested path) +- 实现类 org.springframework.beans.BeanWrapperImpl + +## Spring 底层 Java Beans 替换实现 + +JavaBeans 核心实现 - `java.beans.BeanInfo` + +- 属性(Property) + - `java.beans.PropertyEditor` +- 方法(Method) +- 事件(Event) +- 表达式(Expression) + +Spring 替代实现 - `org.springframework.beans.BeanWrapper` + +- 属性(Property) + - `java.beans.PropertyEditor` +- 嵌套属性路径(nested path) + +## DataBinder 数据校验 + +DataBinder 与 BeanWrapper + +- bind 方法生成 BeanPropertyBindingResult +- BeanPropertyBindingResult 关联 BeanWrapper + +## 问题 + +标准 JavaBeans 是如何操作属性的? + +| API | 说明 | +| ----------------------------- | ------------------------ | +| java.beans.Introspector | Java Beans 内省 API | +| java.beans.BeanInfo | Java Bean 元信息 API | +| java.beans.BeanDescriptor | Java Bean 信息描述符 | +| java.beans.PropertyDescriptor | Java Bean 属性描述符 | +| java.beans.MethodDescriptor | Java Bean 方法描述符 | +| java.beans.EventSetDescriptor | Java Bean 事件集合描述符 | + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/23.Spring\347\261\273\345\236\213\350\275\254\346\215\242.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/23.Spring\347\261\273\345\236\213\350\275\254\346\215\242.md" new file mode 100644 index 00000000..2da331a6 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/23.Spring\347\261\273\345\236\213\350\275\254\346\215\242.md" @@ -0,0 +1,214 @@ +--- +title: Spring 类型转换 +date: 2022-12-22 19:43:59 +order: 23 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/6662dc/ +--- + +# Spring 类型转换 + +## Spring 类型转换的实现 + +- 基于 JavaBeans 接口的类型转换实现 + - 基于 java.beans.PropertyEditor 接口扩展 +- Spring 3.0+ 通用类型转换实现 + +## 使用场景 + +| 场景 | 基于 JavaBeans 接口的类型转换实现 | Spring 3.0+ 通用类型转换实现 | +| ------------------ | --------------------------------- | ---------------------------- | +| 数据绑定 | YES | YES | +| BeanWrapper | YES | YES | +| Bean 属性类型转换 | YES | YES | +| 外部化属性类型转换 | NO | YES | + +## 基于 JavaBeans 接口的类型转换 + +核心职责 + +- 将 String 类型的内容转化为目标类型的对象 + +扩展原理 + +- Spring 框架将文本内容传递到 PropertyEditor 实现的 setAsText(String) 方法 +- PropertyEditor#setAsText(String) 方法实现将 String 类型转化为目标类型的对象 +- 将目标类型的对象传入 PropertyEditor#setValue(Object) 方法 +- PropertyEditor#setValue(Object) 方法实现需要临时存储传入对象 +- Spring 框架将通过 PropertyEditor#getValue() 获取类型转换后的对象 + +## Spring 內建 PropertyEditor 扩展 + +內建扩展(org.springframework.beans.propertyeditors 包下) + +| 转换场景 | 实现类 | +| ------------------- | ----------------------------------------------------------------- | +| String -> Byte 数组 | org.springframework.beans.propertyeditors.ByteArrayPropertyEditor | +| String -> Char | org.springframework.beans.propertyeditors.CharacterEditor | +| String -> Char 数组 | org.springframework.beans.propertyeditors.CharArrayPropertyEditor | +| String -> Charset | org.springframework.beans.propertyeditors.CharsetEditor | +| String -> Class | org.springframework.beans.propertyeditors.ClassEditor | +| String -> Currency | org.springframework.beans.propertyeditors.CurrencyEditor | +| | | + +## 自定义 PropertyEditor 扩展 + +扩展模式 + +- 扩展 `java.beans.PropertyEditorSupport` 类 + +实现 `org.springframework.beans.PropertyEditorRegistrar` + +- 实现 `registerCustomEditors(org.springframework.beans.PropertyEditorRegistry)` 方法 +- 将 `PropertyEditorRegistrar` 实现注册为 Spring Bean + +向 `org.springframework.beans.PropertyEditorRegistry` 注册自定义 PropertyEditor 实现 + +- 通用类型实现 `registerCustomEditor(Class, PropertyEditor)` +- Java Bean 属性类型实现:`registerCustomEditor(Class, String, PropertyEditor)` + +## Spring PropertyEditor 的设计缺陷 + +违反职责单一原则 + +- `java.beans.PropertyEditor` 接口职责太多,除了类型转换,还包括 Java Beans 事件和 Java GUI 交 + 互 + +`java.beans.PropertyEditor` 实现类型局限 + +- 来源类型只能为 `java.lang.String` 类型 + +`java.beans.PropertyEditor` 实现缺少类型安全 + +- 除了实现类命名可以表达语义,实现类无法感知目标转换类型 + +## Spring 3 通用类型转换接口 + +类型转换接口 - org.springframework.core.convert.converter.Converter + +- 泛型参数 S:来源类型,参数 T:目标类型 +- 核心方法:T convert(S) + +通用类型转换接口 - org.springframework.core.convert.converter.GenericConverter + +- 核心方法:convert(Object,TypeDescriptor,TypeDescriptor) +- 配对类型:org.springframework.core.convert.converter.GenericConverter.ConvertiblePair +- 类型描述:org.springframework.core.convert.TypeDescriptor + +## Spring 內建类型转换器 + +內建扩展 + +| 转换场景 | 实现类所在包名(package) | +| -------------------- | -------------------------------------------- | +| 日期/时间相关 | org.springframework.format.datetime | +| Java 8 日期/时间相关 | org.springframework.format.datetime.standard | +| 通用实现 | org.springframework.core.convert.support | + +## Converter 接口的局限性 + +局限一:缺少 Source Type 和 Target Type 前置判断 + +- 应对:增加 org.springframework.core.convert.converter.ConditionalConverter 实现 + +局限二:仅能转换单一的 Source Type 和 Target Type + +- 应对:使用 org.springframework.core.convert.converter.GenericConverter 代替 + +## GenericConverter 接口 + +`org.springframework.core.convert.converter.GenericConverter` + +| 核心要素 | 说明 | +| -------- | ----------------------------------------------------------------------------- | +| 使用场景 | 用于“复合”类型转换场景,比如 Collection、Map、数组等 | +| 转换范围 | `Set getConvertibleTypes()` | +| 配对类型 | `org.springframework.core.convert.converter.GenericConverter.ConvertiblePair` | +| 转换方法 | `convert(Object,TypeDescriptor,TypeDescriptor)` | +| 类型描述 | `org.springframework.core.convert.TypeDescriptor` | + +## 优化 GenericConverter 接口 + +GenericConverter 局限性 + +- 缺少 Source Type 和 Target Type 前置判断 +- 单一类型转换实现复杂 + +GenericConverter 优化接口 - `ConditionalGenericConverter` + +- 复合类型转换:`org.springframework.core.convert.converter.GenericConverter` +- 类型条件判断:`org.springframework.core.convert.converter.ConditionalConverter` + +## 扩展 Spring 类型转换器 + +实现转换器接口 + +- `org.springframework.core.convert.converter.Converter` +- `org.springframework.core.convert.converter.ConverterFactory` +- `org.springframework.core.convert.converter.GenericConverter` + +注册转换器实现 + +- 通过 `ConversionServiceFactoryBean` Spring Bean +- 通过 `org.springframework.core.convert.ConversionService API` + +## 统一类型转换服务 + +`org.springframework.core.convert.ConversionService` + +| 实现类型 | 说明 | +| ------------------------------------ | ----------------------------------------------------------------------------------------- | +| `GenericConversionService` | 通用 ConversionService 模板实现,不内置转化器实现 | +| `DefaultConversionService` | 基础 ConversionService 实现,内置常用转化器实现 | +| `FormattingConversionService` | 通用 Formatter + GenericConversionService 实现,不内置转化器和 Formatter 实现 | +| `DefaultFormattingConversionService` | DefaultConversionService + 格式化 实现(如:JSR-354 Money & Currency, JSR-310 Date-Time) | + +## ConversionService 作为依赖 + +类型转换器底层接口 - `org.springframework.beans.TypeConverter` + +- 起始版本:Spring 2.0 +- 核心方法 - convertIfNecessary 重载方法 +- 抽象实现 - `org.springframework.beans.TypeConverterSupport` +- 简单实现 - `org.springframework.beans.SimpleTypeConverter` + +类型转换器底层抽象实现 - `org.springframework.beans.TypeConverterSupport` + +- 实现接口 - `org.springframework.beans.TypeConverter` +- 扩展实现 - `org.springframework.beans.PropertyEditorRegistrySupport` +- 委派实现 - `org.springframework.beans.TypeConverterDelegate` + +类型转换器底层委派实现 - `org.springframework.beans.TypeConverterDelegate` + +- 构造来源 - `org.springframework.beans.AbstractNestablePropertyAccessor` 实现 + - `org.springframework.beans.BeanWrapperImpl` +- 依赖 - `java.beans.PropertyEditor` 实现 + - 默认內建实现 - `PropertyEditorRegistrySupport#registerDefaultEditors` +- 可选依赖 - `org.springframework.core.convert.ConversionService` 实现 + +## 问题 + +**Spring 类型转换实现有哪些**? + +- 基于 JavaBeans PropertyEditor 接口实现 +- Spring 3.0+ 通用类型转换实现 + +**Spring 类型转换器接口有哪些**? + +- 类型转换接口 - `org.springframework.core.convert.converter.Converter` +- 通用类型转换接口 - `org.springframework.core.convert.converter.GenericConverter` +- 类型条件接口 - `org.springframework.core.convert.converter.ConditionalConverter` +- 综合类型转换接口 - `org.springframework.core.convert.converter.ConditionalGenericConverter` + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/24.SpringEL.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/24.SpringEL.md" new file mode 100644 index 00000000..1e939038 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/24.SpringEL.md" @@ -0,0 +1,22 @@ +--- +title: Spring EL 表达式 +date: 2023-01-12 20:26:46 +order: 24 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/1f743f/ +--- + +# Spring EL 表达式 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/25.Spring\344\272\213\344\273\266.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/25.Spring\344\272\213\344\273\266.md" new file mode 100644 index 00000000..8dc880d0 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/25.Spring\344\272\213\344\273\266.md" @@ -0,0 +1,232 @@ +--- +title: Spring 事件 +date: 2022-12-22 20:31:02 +order: 25 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/cca414/ +--- + +# Spring 事件 + +## Java 事件/监听器编程模型 + +设计模式 - 观察者模式扩展 + +- 可观者对象(消息发送者) - java.util.Observable +- 观察者 - java.util.Observer + +标准化接口 + +- 事件对象 - java.util.EventObject +- 事件监听器 - java.util.EventListener + +## 面向接口的事件/监听器设计模式 + +事件/监听器场景举例 + +| Java 技术规范 | 事件接口 | 监听器接口 | +| --------------- | ------------------------------------- | ---------------------------------------- | +| JavaBeans | java.beans.PropertyChangeEvent | java.beans.PropertyChangeListener | +| Java AWT | java.awt.event.MouseEvent | java.awt.event.MouseListener | +| Java Swing | javax.swing.event.MenuEvent | javax.swing.event.MenuListener | +| Java Preference | java.util.prefs.PreferenceChangeEvent | java.util.prefs.PreferenceChangeListener | + +## 面向注解的事件/监听器设计模式 + +事件/监听器注解场景举例 + +| Java 技术规范 | 事件注解 | 监听器注解 | +| ------------- | ------------------------------ | ------------------------------------- | +| Servlet 3.0+ | | @javax.servlet.annotation.WebListener | +| JPA 1.0+ | @javax.persistence.PostPersist | | +| Java Common | @PostConstruct | | +| EJB 3.0+ | @javax.ejb.PrePassivate | | +| JSF 2.0+ | @javax.faces.event.ListenerFor | | + +## Spring 标准事件 - ApplicationEvent + +Java 标准事件 `java.util.EventObject` 扩展 + +- 扩展特性:事件发生事件戳 +- Spring 应用上下文 ApplicationEvent 扩展 - `ApplicationContextEvent` +- Spring 应用上下文(ApplicationContext)作为事件源 + +具体实现: + +- `org.springframework.context.event.ContextClosedEvent` +- `org.springframework.context.event.ContextRefreshedEvent` +- `org.springframework.context.event.ContextStartedEvent` +- `org.springframework.context.event.ContextStoppedEvent` + +## 基于接口的 Spring 事件监听器 + +Java 标准事件监听器 `java.util.EventListener` 扩展 + +- 扩展接口 - `org.springframework.context.ApplicationListener` +- 设计特点:单一类型事件处理 +- 处理方法:`onApplicationEvent(ApplicationEvent)` +- 事件类型:`org.springframework.context.ApplicationEvent` + +## 基于注解的 Spring 事件监听器 + +Spring 注解 - `@org.springframework.context.event.EventListener` + +| 特性 | 说明 | +| -------------------- | -------------------------------------------- | +| 设计特点 | 支持多 `ApplicationEvent` 类型,无需接口约束 | +| 注解目标 | 方法 | +| 是否支持异步执行 | 支持 | +| 是否支持泛型类型事件 | 支持 | +| 是指支持顺序控制 | 支持,配合 `@Order` 注解控制 | + +## 注册 Spring ApplicationListener + +- 方法一:ApplicationListener 作为 Spring Bean 注册 +- 方法二:通过 ConfigurableApplicationContext API 注册 + +## Spring 事件发布器 + +- 方法一:通过 ApplicationEventPublisher 发布 Spring 事件 + - 获取 ApplicationEventPublisher + - 依赖注入 +- 方法二:通过 ApplicationEventMulticaster 发布 Spring 事件 + - 获取 ApplicationEventMulticaster + - 依赖注入 + - 依赖查找 + +## Spring 层次性上下文事件传播 + +- 发生说明 +- 当 Spring 应用出现多层次 Spring 应用上下文(ApplicationContext)时,如 Spring WebMVC、Spring Boot 或 Spring Cloud 场景下,由子 ApplicationContext 发起 Spring 事件可能会传递到其 Parent ApplicationContext(直到 Root)的过程 +- 如何避免 +- 定位 Spring 事件源(ApplicationContext)进行过滤处理 + +## Spring 内建事件 + +ApplicationContextEvent 派生事件 + +- ContextRefreshedEvent :Spring 应用上下文就绪事件 +- ContextStartedEvent :Spring 应用上下文启动事件 +- ContextStoppedEvent :Spring 应用上下文停止事件 +- ContextClosedEvent :Spring 应用上下文关闭事件 + +## Spring 4.2 Payload 事件 + +Spring Payload 事件 - org.springframework.context.PayloadApplicationEvent + +- 使用场景:简化 Spring 事件发送,关注事件源主体 +- 发送方法:ApplicationEventPublisher#publishEvent(java.lang.Object) + +## 自定义 Spring 事件 + +- 扩展 org.springframework.context.ApplicationEvent +- 实现 org.springframework.context.ApplicationListener +- 注册 org.springframework.context.ApplicationListener + +## 依赖注入 ApplicationEventPublisher + +- 通过 ApplicationEventPublisherAware 回调接口 +- 通过 @Autowired ApplicationEventPublisher + +## 依赖查找 ApplicationEventMulticaster + +查找条件 + +- Bean 名称:"applicationEventMulticaster" +- Bean 类型:org.springframework.context.event.ApplicationEventMulticaster + +## ApplicationEventPublisher 底层实现 + +- 接口:org.springframework.context.event.ApplicationEventMulticaster +- 抽象类:org.springframework.context.event.AbstractApplicationEventMulticaster +- 实现类:org.springframework.context.event.SimpleApplicationEventMulticaster + +## 同步和异步 Spring 事件广播 + +基于实现类 - `org.springframework.context.event.SimpleApplicationEventMulticaster` + +- 模式切换:`setTaskExecutor(java.util.concurrent.Executor)` 方法 + - 默认模式:同步 + - 异步模式:如 `java.util.concurrent.ThreadPoolExecutor` +- 设计缺陷:非基于接口契约编程 + +基于注解 - `@org.springframework.context.event.EventListener` + +- 模式切换 + - 默认模式:同步 + - 异步模式:标注 `@org.springframework.scheduling.annotation.Async` +- 实现限制:无法直接实现同步/异步动态切换 + +## Spring 4.1 事件异常处理 + +Spring 3.0 错误处理接口 - org.springframework.util.ErrorHandler + +使用场景 + +- Spring 事件(Events) + - SimpleApplicationEventMulticaster Spring 4.1 开始支持 +- Spring 本地调度(Scheduling) + - org.springframework.scheduling.concurrent.ConcurrentTaskScheduler + - org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler + +## Spring 事件/监听器实现原理 + +核心类 - `org.springframework.context.event.SimpleApplicationEventMulticaster` + +- 设计模式:观察者模式扩展 + - 被观察者 - org.springframework.context.ApplicationListener + - API 添加 + - 依赖查找 + - 通知对象 - org.springframework.context.ApplicationEvent +- 执行模式:同步/异步 +- 异常处理:org.springframework.util.ErrorHandler +- 泛型处理:org.springframework.core.ResolvableType + +## 问题 + +**Spring Boot 事件** + +| 事件类型 | 发生时机 | +| ----------------------------------- | --------------------------------------- | +| ApplicationStartingEvent | 当 Spring Boot 应用已启动时 | +| ApplicationStartedEvent | 当 Spring Boot 应用已启动时 | +| ApplicationEnvironmentPreparedEvent | 当 Spring Boot Environment 实例已准备时 | +| ApplicationPreparedEvent | 当 Spring Boot 应用预备时 | +| ApplicationReadyEvent | 当 Spring Boot 应用完全可用时 | +| ApplicationFailedEvent | 当 Spring Boot 应用启动失败时 | + +**Spring Cloud 事件** + +| 事件类型 | 发生时机 | +| -------------------------- | ------------------------------------- | +| EnvironmentChangeEvent | 当 Environment 示例配置属性发生变化时 | +| HeartbeatEvent | 当 DiscoveryClient 客户端发送心跳时 | +| InstancePreRegisteredEvent | 当服务实例注册前 | +| InstanceRegisteredEvent | 当服务实例注册后 | +| RefreshEvent | 当 RefreshEndpoint 被调用时 | +| RefreshScopeRefreshedEvent | 当 Refresh Scope Bean 刷新后 | + +**Spring 事件核心接口/组件**? + +- Spring 事件 - org.springframework.context.ApplicationEvent +- Spring 事件监听器 - org.springframework.context.ApplicationListener +- Spring 事件发布器 - org.springframework.context.ApplicationEventPublisher +- Spring 事件广播器 - org.springframework.context.event.ApplicationEventMulticaster + +**Spring 同步和异步事件处理的使用场景**? + +- Spring 同步事件 - 绝大多数 Spring 使用场景,如 ContextRefreshedEvent +- Spring 异步事件 - 主要 @EventListener 与 @Async 配合,实现异步处理,不阻塞主线程,比如长时间的数据计算任务等。不要轻易调整 SimpleApplicationEventMulticaster 中关联的 taskExecutor 对象,除非使用者非常了解 Spring 事件机制,否则容易出现异常行为。 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/26.Spring\345\233\275\351\231\205\345\214\226.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/26.Spring\345\233\275\351\231\205\345\214\226.md" new file mode 100644 index 00000000..e07843fd --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/26.Spring\345\233\275\351\231\205\345\214\226.md" @@ -0,0 +1,121 @@ +--- +title: Spring 国际化 +date: 2022-12-22 11:44:54 +order: 26 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/b5b8ad/ +--- + +# Spring 国际化 + +## Spring 国际化使用场景 + +- 普通国际化文案 +- Bean Validation 校验国际化文案 +- Web 站点页面渲染 +- Web MVC 错误消息提示 + +## Spring 国际化接口 + +- 核心接口:`org.springframework.context.MessageSource` +- 主要概念 + - 文案模板编码(code) + - 文案模板参数(args) + - 区域(Locale) + +## 层次性 MessageSource + +- Spring 层次性接口回顾 + - `org.springframework.beans.factory.HierarchicalBeanFactory` + - `org.springframework.context.ApplicationContext` + - `org.springframework.beans.factory.config.BeanDefinition` +- Spring 层次性国际化接口 + - `org.springframework.context.HierarchicalMessageSource` + +## Java 国际化标准实现 + +核心接口: + +- 抽象实现 - `java.util.ResourceBundle` +- Properties 资源实现 - `java.util.PropertyResourceBundle` +- 例举实现 - `java.util.ListResourceBundle` + +`ResourceBundle` 核心特性 + +- Key-Value 设计 +- 层次性设计 +- 缓存设计 +- 字符编码控制 - `java.util.ResourceBundle.Control`(@since 1.6) +- Control SPI 扩展 - `java.util.spi.ResourceBundleControlProvider`(@since 1.8) + +## Java 文本格式化 + +- 核心接口 + - java.text.MessageFormat +- 基本用法 + - 设置消息格式模式- new MessageFormat(...) + - 格式化 - format(new Object[]{...}) +- 消息格式模式 + - 格式元素:{ArgumentIndex (,FormatType,(FormatStyle))} + - FormatType:消息格式类型,可选项,每种类型在 number、date、time 和 choice 类型选其一 + - FormatStyle:消息格式风格,可选项,包括:short、medium、long、full、integer、currency、 + percent +- 高级特性 + - 重置消息格式模式 + - 重置 java.util.Locale + - 重置 java.text.Format + +## MessageSource 开箱即用实现 + +- 基于 ResourceBundle + MessageFormat 组合 MessageSource 实现 +- org.springframework.context.support.ResourceBundleMessageSource +- 可重载 Properties + MessageFormat 组合 MessageSource 实现 +- org.springframework.context.support.ReloadableResourceBundleMessageSource + +## MessageSource 內建依赖 + +- MessageSource 內建 Bean 可能来源 +- 预注册 Bean 名称为:“messageSource”,类型为:MessageSource Bean +- 默认內建实现 - DelegatingMessageSource +- 层次性查找 MessageSource 对象 + +## 问题 + +**Spring Boot 为什么要新建 MessageSource Bean**? + +- AbstractApplicationContext 的实现决定了 MessageSource 內建实现 +- Spring Boot 通过外部化配置简化 MessageSource Bean 构建 +- Spring Boot 基于 Bean Validation 校验非常普遍 + +**Spring 国际化接口有哪些**? + +- 核心接口 - MessageSource +- 层次性接口 - `org.springframework.context.HierarchicalMessageSource` + +**Spring 有哪些 MessageSource 內建实现**? + +- `org.springframework.context.support.ResourceBundleMessageSource` +- `org.springframework.context.support.ReloadableResourceBundleMessageSource` +- `org.springframework.context.support.StaticMessageSource` +- `org.springframework.context.support.DelegatingMessageSource` + +**如何实现配置自动更新 MessageSource**? + +主要技术 + +- Java NIO 2:`java.nio.file.WatchService` +- Java Concurrency : `java.util.concurrent.ExecutorService` +- Spring:`org.springframework.context.support.AbstractMessageSource` + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/27.Spring\346\263\233\345\236\213\345\244\204\347\220\206.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/27.Spring\346\263\233\345\236\213\345\244\204\347\220\206.md" new file mode 100644 index 00000000..ffc2db65 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/27.Spring\346\263\233\345\236\213\345\244\204\347\220\206.md" @@ -0,0 +1,141 @@ +--- +title: Spring 泛型处理 +date: 2022-12-22 20:11:52 +order: 27 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/175cbd/ +--- + +# Spring 泛型处理 + +## Java 泛型基础 + +泛型类型 + +- 泛型类型是在类型上参数化的泛型类或接口 + +泛型使用场景 + +- 编译时强类型检查 +- 避免类型强转 +- 实现通用算法 + +泛型类型擦写 + +- 泛型被引入到 Java 语言中,以便在编译时提供更严格的类型检查并支持泛型编程。类型擦除确保不会 + 为参数化类型创建新类;因此,泛型不会产生运行时开销。为了实现泛型,编译器将类型擦除应用于: + - 将泛型类型中的所有类型参数替换为其边界,如果类型参数是无边界的,则将其替换为 + “Object”。因此,生成的字节码只包含普通类、接口和方法 + - 必要时插入类型转换以保持类型安全 + - 生成桥方法以保留扩展泛型类型中的多态性 + +## Java 5 类型接口 + +Java 5 类型接口 - `java.lang.reflect.Type` + +| 派生类或接口 | 说明 | +| ------------------------------------- | --------------------------------------- | +| `java.lang.Class` | Java 类 API,如 `java.lang.String` | +| `java.lang.reflect.GenericArrayType` | 泛型数组类型 | +| `java.lang.reflect.ParameterizedType` | 泛型参数类型 | +| `java.lang.reflect.TypeVariable` | 泛型类型变量,如 `Collection` 中的 E | +| `java.lang.reflect.WildcardType` | 泛型通配类型 | + +Java 泛型反射 API + +| 类型 | API | +| -------------------------------- | ---------------------------------------- | +| 泛型信息(Generics Info) | `java.lang.Class#getGenericInfo()` | +| 泛型参数(Parameters) | `java.lang.reflect.ParameterizedType` | +| 泛型父类(Super Classes) | `java.lang.Class#getGenericSuperclass()` | +| 泛型接口(Interfaces) | `java.lang.Class#getGenericInterfaces()` | +| 泛型声明(Generics Declaration) | `java.lang.reflect.GenericDeclaration` | + +## Spring 泛型类型辅助类 + +核心 API - `org.springframework.core.GenericTypeResolver` + +- 版本支持:[2.5.2 , ) +- 处理类型相关(Type)相关方法 + - `resolveReturnType` + - `resolveType` +- 处理泛型参数类型(`ParameterizedType`)相关方法 + - `resolveReturnTypeArgument` + - `resolveTypeArgument` + - `resolveTypeArguments` +- 处理泛型类型变量(`TypeVariable`)相关方法 + - `getTypeVariableMap` + +## Spring 泛型集合类型辅助类 + +核心 API - `org.springframework.core.GenericCollectionTypeResolver` + +- 版本支持:[2.0 , 4.3] +- 替换实现:`org.springframework.core.ResolvableType` +- 处理 Collection 相关 + - `getCollection*Type` +- 处理 Map 相关 + - `getMapKey*Type` + - `getMapValue*Type` + +## Spring 方法参数封装 - MethodParameter + +核心 API - `org.springframework.core.MethodParameter` + +- 起始版本:[2.0 , ) +- 元信息 + - 关联的方法 - Method + - 关联的构造器 - Constructor + - 构造器或方法参数索引 - parameterIndex + - 构造器或方法参数类型 - parameterType + - 构造器或方法参数泛型类型 - genericParameterType + - 构造器或方法参数参数名称 - parameterName + - 所在的类 - containingClass + +## Spring 4.0 泛型优化实现 - ResolvableType + +核心 API - `org.springframework.core.ResolvableType` + +- 起始版本:[4.0 , ) +- 扮演角色:`GenericTypeResolver` 和 `GenericCollectionTypeResolver` 替代者 +- 工厂方法:`for*` 方法 +- 转换方法:`as*` 方法 +- 处理方法:`resolve*` 方法 + +## ResolvableType 的局限性 + +- 局限一:ResolvableType 无法处理泛型擦写 +- 局限二:ResolvableType 无法处理非具体化的 ParameterizedType + +## 问题 + +**Java 泛型擦写发生在编译时还是运行时**? + +运行时 + +**请介绍 Java 5 Type 类型的派生类或接口** + +- `java.lang.Class` +- `java.lang.reflect.GenericArrayType` +- `java.lang.reflect.ParameterizedType` +- `java.lang.reflect.TypeVariable` +- `java.lang.reflect.WildcardType` + +**请说明 ResolvableType 的设计优势**? + +- 简化 Java 5 Type API 开发,屏蔽复杂 API 的运用,如 ParameterizedType +- 不变性设计(Immutability) +- Fluent API 设计(Builder 模式),链式(流式)编程 + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/28.Spring\346\263\250\350\247\243.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/28.Spring\346\263\250\350\247\243.md" new file mode 100644 index 00000000..e8ce595c --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/28.Spring\346\263\250\350\247\243.md" @@ -0,0 +1,147 @@ +--- +title: Spring 注解 +date: 2022-12-23 09:08:15 +order: 28 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/b6556f/ +--- + +# Spring 注解 + +## Spring 注解驱动编程发展历程 + +- 注解驱动启蒙时代:Spring Framework 1.x +- 注解驱动过渡时代:Spring Framework 2.x +- 注解驱动黄金时代:Spring Framework 3.x +- 注解驱动完善时代:Spring Framework 4.x +- 注解驱动当下时代:Spring Framework 5.x + +## Spring 核心注解场景分类 + +Spring 模式注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| -------------- | ------------------ | -------- | +| @Repository | 数据仓储模式注解 | 2.0 | +| @Component | 通用组件模式注解 | 2.5 | +| @Service | 服务模式注解 | 2.5 | +| @Controller | Web 控制器模式注解 | 2.5 | +| @Configuration | 配置类模式注解 | 3.0 | + +装配注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| --------------- | ------------------------------------------- | -------- | +| @ImportResource | 替换 XML 元素 `` | 2.5 | +| @Import | 导入 Configuration 类 | 2.5 | +| @ComponentScan | 扫描指定 package 下标注 Spring 模式注解的类 | 3.1 | + +依赖注入注解 + +| Spring 注解 | 场景说明 | 起始版本 | +| ----------- | ----------------------------------- | -------- | +| @Autowired | Bean 依赖注入,支持多种依赖查找方式 | 2.5 | +| @Qualifier | 细粒度的 @Autowired 依赖查找 | 2.5 | + +## Spring 注解编程模型 + +- 元注解(Meta-Annotations) +- Spring 模式注解(Stereotype Annotations) +- Spring 组合注解(Composed Annotations) +- Spring 注解属性别名和覆盖(Attribute Aliases and Overrides) + +## Spring 元注解(Meta-Annotations) + +- java.lang.annotation.Documented +- java.lang.annotation.Inherited +- java.lang.annotation.Repeatable + +## Spring 模式注解(Stereotype Annotations) + +理解 @Component “派⽣性”:元标注 @Component 的注解在 XML 元素 或注解 @ComponentScan 扫描中“派生”了 @Component 的特性,并且从 Spring Framework 4.0 开始支持多层次“派⽣性”。 + +举例说明: + +- @Repository +- @Service +- @Controller +- @Configuration +- @SpringBootConfiguration(Spring Boot) + +@Component “派⽣性”原理 + +- 核心组件 - org.springframework.context.annotation.ClassPathBeanDefinitionScanner +- org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider +- 资源处理 - org.springframework.core.io.support.ResourcePatternResolver +- 资源-类元信息 +- org.springframework.core.type.classreading.MetadataReaderFactory +- 类元信息 - org.springframework.core.type.ClassMetadata +- ASM 实现 - org.springframework.core.type.classreading.ClassMetadataReadingVisitor +- 反射实现 - org.springframework.core.type.StandardAnnotationMetadata +- 注解元信息 - org.springframework.core.type.AnnotationMetadata +- ASM 实现 - org.springframework.core.type.classreading.AnnotationMetadataReadingVisitor +- 反射实现 - org.springframework.core.type.StandardAnnotationMetadata + +## Spring 组合注解(Composed Annotations) + +Spring 组合注解(Composed Annotations)中的元注允许是 Spring 模式注解(Stereotype Annotation)与其他 Spring 功能性注解的任意组合。 + +## Spring 注解属性别名(Attribute Aliases) + +## Spring 注解属性覆盖(Attribute Overrides) + +## Spring @Enable 模块驱动 + +@Enable 模块驱动 + +@Enable 模块驱动是以 @Enable 为前缀的注解驱动编程模型。所谓“模块”是指具备相同领域的功能组件集合,组合所形成⼀个独⽴的单元。⽐如 Web MVC 模块、AspectJ 代理模块、Caching(缓存)模块、JMX(Java 管理扩展)模块、Async(异步处理)模块等。 + +举例说明 + +- @EnableWebMvc +- @EnableTransactionManagement +- @EnableCaching +- @EnableMBeanExport +- @EnableAsync + +@Enable 模块驱动编程模式 + +- 驱动注解:@EnableXXX +- 导入注解:@Import 具体实现 +- 具体实现 +- 基于 Configuration Class +- 基于 ImportSelector 接口实现 +- 基于 ImportBeanDefinitionRegistrar 接口实现 + +## Spring 条件注解 + +基于配置条件注解 - @org.springframework.context.annotation.Profile + +- 关联对象 - org.springframework.core.env.Environment 中的 Profiles +- 实现变化:从 Spring 4.0 开始,@Profile 基于 @Conditional 实现 + +基于编程条件注解 - @org.springframework.context.annotation.Conditional + +- 关联对象 - org.springframework.context.annotation.Condition 具体实现 + +@Conditional 实现原理 + +- 上下文对象 - org.springframework.context.annotation.ConditionContext +- 条件判断 - org.springframework.context.annotation.ConditionEvaluator +- 配置阶段 - org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase +- 判断入口 + - org.springframework.context.annotation.ConfigurationClassPostProcessor + - org.springframework.context.annotation.ConfigurationClassParser + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/29.SpringEnvironment\346\212\275\350\261\241.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/29.SpringEnvironment\346\212\275\350\261\241.md" new file mode 100644 index 00000000..e95e298a --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/29.SpringEnvironment\346\212\275\350\261\241.md" @@ -0,0 +1,167 @@ +--- +title: Spring Environment 抽象 +date: 2022-12-23 09:27:44 +order: 29 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring +permalink: /pages/03d838/ +--- + +# Spring Environment 抽象 + +## 理解 Spring Environment 抽象 + +统一的 Spring 配置属性管理 + +Spring Framework 3.1 开始引入 Environment 抽象,它统一 Spring 配置属性的存储,包括占位符处理和类型转换,不仅完整地替换 PropertyPlaceholderConfigurer,而且还支持更丰富的配置属性源(PropertySource) + +条件化 Spring Bean 装配管理 + +通过 Environment Profiles 信息,帮助 Spring 容器提供条件化地装配 Bean + +## Spring Environment 接口使用场景 + +- ⽤于属性占位符处理 +- 用于转换 Spring 配置属性类型 +- 用于存储 Spring 配置属性源(PropertySource) +- 用于 Profiles 状态的维护 + +## Environment 占位符处理 + +Spring 3.1 前占位符处理 + +- 组件:org.springframework.beans.factory.config.PropertyPlaceholderConfigurer +- 接口:org.springframework.util.StringValueResolver + +Spring 3.1 + 占位符处理 + +- 组件:org.springframework.context.support.PropertySourcesPlaceholderConfigurer +- 接口:org.springframework.beans.factory.config.EmbeddedValueResolver + +## 理解条件配置 Spring Profiles + +Spring 3.1 条件配置 + +- API:org.springframework.core.env.ConfigurableEnvironment +- 修改:addActiveProfile(String)、setActiveProfiles(String...) 和 setDefaultProfiles(String...) +- 获取:getActiveProfiles() 和 getDefaultProfiles() +- 匹配:#acceptsProfiles(String...) 和 acceptsProfiles(Profiles) +- 注解:@org.springframework.context.annotation.Profile + +## Spring 4 重构 @Profile + +基于 Spring 4 org.springframework.context.annotation.Condition 接口实现 + +org.springframework.context.annotation.ProfileCondition + +## 依赖注入 Environment + +直接依赖注入 + +- 通过 EnvironmentAware 接口回调 +- 通过 @Autowired 注入 Environment + +间接依赖注入 + +- 通过 ApplicationContextAware 接口回调 +- 通过 @Autowired 注入 ApplicationContext + +## 依赖查找 Environment + +直接依赖查找 + +- 通过 org.springframework.context.ConfigurableApplicationContext#ENVIRONMENT_BEAN_NAME + +间接依赖查找 + +- 通过 org.springframework.context.ConfigurableApplicationContext#getEnvironment + +## 依赖注入 @Value + +通过注入 @Value + +实现 - org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + +## Spring 类型转换在 Environment 中的运用 + +Environment 底层实现 + +- 底层实现 - org.springframework.core.env.PropertySourcesPropertyResolver +- 核心方法 - convertValueIfNecessary(Object,Class) +- 底层服务 - org.springframework.core.convert.ConversionService +- 默认实现 - org.springframework.core.convert.support.DefaultConversionService + +## Spring 类型转换在 @Value 中的运用 + +@Value 底层实现 + +- 底层实现 - org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor + - org.springframework.beans.factory.support.DefaultListableBeanFactory#doResolveDependency +- 底层服务 - org.springframework.beans.TypeConverter + - 默认实现 - org.springframework.beans.TypeConverterDelegate + - java.beans.PropertyEditor + - org.springframework.core.convert.ConversionService + +## Spring 配置属性源 PropertySource + +- API + - 单配置属性源 - org.springframework.core.env.PropertySource + - 多配置属性源 - org.springframework.core.env.PropertySources +- 注解 + - 单配置属性源 - @org.springframework.context.annotation.PropertySource + - 多配置属性源 - @org.springframework.context.annotation.PropertySources +- 关联 + - 存储对象 - org.springframework.core.env.MutablePropertySources + - 关联方法 - org.springframework.core.env.ConfigurableEnvironment#getPropertySources() + +## Spring 內建的配置属性源 + +內建 PropertySource + +| PropertySource 类型 | 说明 | +| -------------------------------------------------------------------- | ------------------------- | +| org.springframework.core.env.CommandLinePropertySource | 命令行配置属性源 | +| org.springframework.jndi.JndiPropertySource | JDNI 配置属性源 | +| org.springframework.core.env.PropertiesPropertySource | Properties 配置属性源 | +| org.springframework.web.context.support.ServletConfigPropertySource | Servlet 配置属性源 | +| org.springframework.web.context.support.ServletContextPropertySource | ServletContext 配置属性源 | +| org.springframework.core.env.SystemEnvironmentPropertySource | 环境变量配置属性源 | + +## 基于注解扩展 Spring 配置属性源 + +@org.springframework.context.annotation.PropertySource 实现原理 + +- 入口 - org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass + - org.springframework.context.annotation.ConfigurationClassParser#processPropertySource +- 4.3 新增语义 + - 配置属性字符编码 - encoding + - org.springframework.core.io.support.PropertySourceFactory +- 适配对象 - org.springframework.core.env.CompositePropertySource + +## 基于 API 扩展 Spring 配置属性源 + +- Spring 应用上下文启动前装配 PropertySource +- Spring 应用上下文启动后装配 PropertySource + +## 问题 + +简单介绍 Spring Environment 接口? + +- 核心接口 - org.springframework.core.env.Environment +- 父接口 - org.springframework.core.env.PropertyResolver +- 可配置接口 - org.springframework.core.env.ConfigurableEnvironment +- 职责: + - 管理 Spring 配置属性源 + - 管理 Profiles + +## 参考资料 + +- [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/31.SpringBoot\344\271\213\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/31.SpringBoot\344\271\213\345\277\253\351\200\237\345\205\245\351\227\250.md" new file mode 100644 index 00000000..da73823d --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/31.SpringBoot\344\271\213\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -0,0 +1,387 @@ +--- +title: SpringBoot 之快速入门 +date: 2021-12-10 18:22:26 +order: 31 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/950e4d/ +--- + +# SpringBoot 之快速入门 + +## Spring Boot 简介 + +Spring Boot 可以让使用者非常方便的创建 Spring 应用。 + +Spring Boot 的目标是: + +- 为所有 Spring 开发者提供更快且可广泛访问的入门体验。 +- 开箱即用 +- 提供一系列通用的非功能特性(例如嵌入式服务、安全、指标、健康检查和外部化配置) +- 完全不需要代码生成,也不需要 XML 配置。 + +## Spring Boot 系统要求 + +Spring Boot 的构建工具要求: + +| Build Tool | Version | +| :--------- | :-------------------- | +| Maven | 3.5+ | +| Gradle | 6.8.x, 6.9.x, and 7.x | + +Spring Boot 支持的 Servlet 容器: + +| Name | Servlet Version | +| :----------- | :-------------- | +| Tomcat 9.0 | 4.0 | +| Jetty 9.4 | 3.1 | +| Jetty 10.0 | 4.0 | +| Undertow 2.0 | 4.0 | + +## 部署第一个 Spring Boot 项目 + +> 本节介绍如何开发一个小的“Hello World!” web 应用示例,来展示 Spring Boot 的一些关键功能。我们使用 Maven 来构建这个项目,因为大多数 IDE 都支持它。 + +### 环境检查 + +Spring Boot 项目依赖于 Java 环境和 Mave,开始项目之前需要先检查一下环境。 + +本地是否已安装 Java: + +```shell +$ java -version +java version "1.8.0_102" +Java(TM) SE Runtime Environment (build 1.8.0_102-b14) +Java HotSpot(TM) 64-Bit Server VM (build 25.102-b14, mixed mode) +``` + +本地是否已安装 Maven: + +```java +$ mvn -v +Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-17T14:33:14-04:00) +Maven home: /usr/local/Cellar/maven/3.3.9/libexec +Java version: 1.8.0_102, vendor: Oracle Corporation +``` + +### 创建 pom + +我们需要从创建 Maven pom.xml 文件开始。 pom.xml 是 Maven 用于构建项目的配置文件。 + +```xml + + + 4.0.0 + + com.example + myproject + 0.0.1-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-parent + 2.6.1 + + + + + +``` + +使用者可以通过运行 mvn package 来测试它 + +### 添加依赖 + +Spring Boot 提供了许多启动器(Starters)以应对不同的使用场景。使用者可将 jars 添加到类路径中。我们的示例程序在 POM 的 parent 使用 spring-boot-starter-parent。 spring-boot-starter-parent 是一个特殊的启动器,提供有用的 Maven 默认值。它还提供了一个依赖项的版本管理,可以让使用者使用时不必显示指定版本。 + +其他启动器(Starters)提供了各种针对不同使用场景的功能。比如,我们需要开发一个 Web 应用程序,就可以添加了一个 spring-boot-starter-web 依赖项。在此之前,我们可以通过运行以下命令来查看我们当前拥有的 maven 依赖: + +```shell +$ mvn dependency:tree + +[INFO] com.example:myproject:jar:0.0.1-SNAPSHOT +``` + +mvn dependency:tree 命令打印项目依赖项的层级结构。可以看到 spring-boot-starter-parent 本身没有提供任何依赖。要添加必要的依赖,需要编辑 pom.xml 并在 `` 部分添加 spring-boot-starter-web 依赖项: + +```xml + + + org.springframework.boot + spring-boot-starter-web + + +``` + +### 编写代码 + +要运行应用程序,我们需要创建一个启动类。默认情况下,Maven 从 `src/main/java` 编译源代码,因此您需要创建该目录结构,然后添加一个名为 `src/main/java/MyApplication.java` 的文件以包含以下代码: + +```java +@RestController +@EnableAutoConfiguration +public class MyApplication { + + @RequestMapping("/") + String home() { + return "Hello World!"; + } + + public static void main(String[] args) { + SpringApplication.run(MyApplication.class, args); + } + +} +``` + +说明: + +@RestController 注解告诉 Spring,这个类是用来处理 Rest 请求的。 + +`@RequestMapping` 注解提供了“路由”信息。它告诉 Spring 任何带有 `/` 路径的 HTTP 请求都应该映射到 `home` 方法。 `@RestController` 注解告诉 Spring 将结果字符串直接呈现给调用者。 + +`@EnableAutoConfiguration` 注解告诉 Spring Boot 根据你添加的 jar 依赖去自动装配 Spring。 + +> 自动配置旨在与“Starters”配合使用,但这两个概念并没有直接联系。您可以自由选择 starters 之外的 jar 依赖项。 Spring Boot 仍然尽力自动配置您的应用程序。 + +Spring Boot 的 main 方法通过调用 run 委托给 Spring Boot 的 `SpringApplication` 类。 `SpringApplication` 引导我们的应用程序,启动 Spring,进而启动自动配置的 Tomcat Web 服务器。我们需要将 `MyApplication.class` 作为参数传递给 run 方法,以告诉 `SpringApplication` 哪个是入口类。还传递 args 数组以公开任何命令行参数。 + +### 运行示例 + +此时,您的应用程序应该可以工作了。由于您使用了 spring-boot-starter-parent POM,因此您有一个有用的运行目标,可用于启动应用程序。从项目根目录键入 mvn spring-boot:run 以启动应用程序。您应该会看到类似于以下内容的输出: + +```shell +$ mvn spring-boot:run + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v2.6.1) +....... . . . +....... . . . (log output here) +....... . . . +........ Started MyApplication in 2.222 seconds (JVM running for 6.514) +``` + +如果您打开 Web 浏览器访问 localhost:8080,您应该会看到以下输出: + +``` +Hello World! +``` + +要正常退出应用程序,请按 `ctrl-c`。 + +### 创建可执行 jar + +要创建一个可执行的 jar,我们需要将 spring-boot-maven-plugin 添加到我们的 pom.xml 中。为此,请在依赖项部分下方插入以下行: + +```xml + + + + org.springframework.boot + spring-boot-maven-plugin + + + +``` + +保存 pom.xml 并从命令行运行 mvn package,如下所示: + +```shell +$ mvn package + +[INFO] Scanning for projects... +[INFO] +[INFO] ------------------------------------------------------------------------ +[INFO] Building myproject 0.0.1-SNAPSHOT +[INFO] ------------------------------------------------------------------------ +[INFO] .... .. +[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ myproject --- +[INFO] Building jar: /Users/developer/example/spring-boot-example/target/myproject-0.0.1-SNAPSHOT.jar +[INFO] +[INFO] --- spring-boot-maven-plugin:2.6.1:repackage (default) @ myproject --- +[INFO] ------------------------------------------------------------------------ +[INFO] BUILD SUCCESS +[INFO] ------------------------------------------------------------------------ +``` + +如果您查看 target 目录,应该会看到 `myproject-0.0.1-SNAPSHOT.jar`。该文件的大小应约为 10 MB。如果想看里面,可以使用 jar tvf,如下: + +```shell +$ jar tvf target/myproject-0.0.1-SNAPSHOT.jar +``` + +您还应该在目标目录中看到一个更小的名为 `myproject-0.0.1-SNAPSHOT.jar.original` 的文件。这是 Maven 在 Spring Boot 重新打包之前创建的原始 jar 文件。 + +要运行该应用程序,请使用 java -jar 命令,如下所示: + +``` +$ java -jar target/myproject-0.0.1-SNAPSHOT.jar + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v2.6.1) +....... . . . +....... . . . (log output here) +....... . . . +........ Started MyApplication in 2.536 seconds (JVM running for 2.864) +``` + +和以前一样,要退出应用程序,请按 `ctrl-c`。 + +## 通过 SPRING INITIALIZR 创建 Spring Boot 项目 + +### 创建项目 + +通过 `SPRING INITIALIZR` 工具产生基础项目 + +1. 访问:`http://start.spring.io/` +2. 选择构建工具`Maven Project`、Spring Boot 版本 `1.5.10` 以及一些工程基本信息,可参考下图所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/start.spring.io.png) + +3. 点击`Generate Project`下载项目压缩包 +4. 解压压缩包,包中已是一个完整的项目。 + +如果你使用 Intellij 作为 IDE,那么你可以直接使用 SPRING INITIALIZR,参考下图操作: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/intellij-spring-initializr.gif) + +### 项目说明 + +**重要文件** + +- `src/main/java` 路径下的 `Chapter1Application` 类 :程序入口 +- `src/main/resources` 路径下的 `application.properties` :项目配置文件 +- `src/test/java` 路径下的 `Chapter01ApplicationTests` :程序测试入口 + +**pom.xml** + +pom 中指定 parent 为以下内容,表示此项目继承了 `spring-boot-starter-parent` 的 maven 配置(主要是指定了常用依赖、插件的版本)。 + +```xml + + org.springframework.boot + spring-boot-starter-parent + 1.5.10.RELEASE + + +``` + +此外,pom 中默认引入两个依赖包,和一个插件。 + +```xml + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + +``` + +- `spring-boot-starter-web`:核心模块,包括自动配置支持、日志和 YAML。 +- `spring-boot-starter-test`:测试模块,包括 JUnit、Hamcrest、Mockito。 +- `spring-boot-maven-plugin`:spring boot 插件, 提供了一系列 spring boot 相关的 maven 操作。 + - `spring-boot:build-info`,生成 Actuator 使用的构建信息文件 build-info.properties + - `spring-boot:repackage`,默认 goal。在 mvn package 之后,再次打包可执行的 jar/war,同时保留 mvn package 生成的 jar/war 为.origin + - `spring-boot:run`,运行 Spring Boot 应用 + - `spring-boot:start`,在 mvn integration-test 阶段,进行 Spring Boot 应用生命周期的管理 + - `spring-boot:stop`,在 mvn integration-test 阶段,进行 Spring Boot 应用生命周期的管理 + +### 编写 REST 服务 + +- 创建 `package` ,名为 `io.github.zp.springboot.chapter1.web`(根据项目情况修改) +- 创建 `HelloController` 类,内容如下: + +```java +@RestController +public class HelloController { + + @RequestMapping("/hello") + public String index() { + return "Hello World"; + } + +} +``` + +- 启动主程序 `XXXApplication`,打开浏览器访问`http://localhost:8080/hello` ,可以看到页面输出`Hello World` + +### 编写单元测试用例 + +在 `XXXApplicationTests` 类中编写一个简单的单元测试来模拟 HTTP 请求,具体如下: + +```java +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = MockServletContext.class) +@WebAppConfiguration +public class SpringBootHelloWorldApplicationTest { + + private MockMvc mvc; + + @Before + public void setUp() { + mvc = MockMvcBuilders.standaloneSetup(new HelloController()).build(); + } + + @Test + public void getHello() throws Exception { + mvc.perform(MockMvcRequestBuilders.get("/hello").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().string(equalTo("Hello World"))); + } + +} +``` + +使用`MockServletContext`来构建一个空的`WebApplicationContext`,这样我们创建的`HelloController`就可以在`@Before`函数中创建并传递到`MockMvcBuilders.standaloneSetup()`函数中。 + +- 注意引入下面内容,让`status`、`content`、`equalTo`函数可用 + +```java +import static org.hamcrest.Matchers.equalTo; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +``` + +至此已完成目标,通过 Maven 构建了一个空白 Spring Boot 项目,再通过引入 web 模块实现了一个简单的请求处理。 + +### 示例源码 + +> 示例源码:[spring-boot-web-helloworld](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/web/spring-boot-web-helloworld) + +## 参考资料 + +- [Spring Boot 官方文档之 Getting Started](https://docs.spring.io/spring-boot/docs/current/reference/html/getting-started.html#getting-started) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/32.SpringBoot\344\271\213\345\261\236\346\200\247\345\212\240\350\275\275.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/32.SpringBoot\344\271\213\345\261\236\346\200\247\345\212\240\350\275\275.md" new file mode 100644 index 00000000..8a6ab562 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/32.SpringBoot\344\271\213\345\261\236\346\200\247\345\212\240\350\275\275.md" @@ -0,0 +1,387 @@ +--- +title: SpringBoot 之属性加载详解 +date: 2019-01-10 11:55:54 +order: 32 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/0fb992/ +--- + +# SpringBoot 之属性加载详解 + +## 加载 property 顺序 + +Spring Boot 加载 property 顺序如下: + +1. [Devtools 全局配置](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#using-boot-devtools-globalsettings) (当 devtools 被激活 `~/.spring-boot-devtools.properties`). +2. [测试环境中的 `@TestPropertySource` 注解配置](https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/test/context/TestPropertySource.html) +3. 测试环境中的属性 `properties`:[`@SpringBootTest`](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/test/context/SpringBootTest.html) 和 [测试注解](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-testing-spring-boot-applications-testing-autoconfigured-tests). +4. 命令行参数 +5. `SPRING_APPLICATION_JSON` 属性 +6. `ServletConfig` 初始化参数 +7. `ServletContext` 初始化参数 +8. JNDI attributes from 通过 `java:comp/env` 配置的 JNDI 属性 +9. Java 系统属性 (`System.getProperties()`) +10. 操作系统环境比那里 +11. `RandomValuePropertySource` 加载 `random.*` 形式的属性 +12. jar 包外的 `application-{profile}.properties` 或 `application-{profile}.yml` 配置 +13. jar 包内的 `application-{profile}.properties` 或 `application-{profile}.yml` 配置 +14. jar 包外的 `application.properties` 或 `application.yml` 配置 +15. jar 包内的 `application.properties` 或 `application.yml` 配置 +16. `@PropertySource` 绑定的配置 +17. 默认属性 (通过 `SpringApplication.setDefaultProperties` 指定) + +## 随机属性 + +`RandomValuePropertySource` 类用于配置随机值。 + +示例: + +```properties +my.secret=${random.value} +my.number=${random.int} +my.bignumber=${random.long} +my.uuid=${random.uuid} +my.number.less.than.ten=${random.int(10)} +my.number.in.range=${random.int[1024,65536]} +``` + +## 命令行属性 + +默认情况下, `SpringApplication` 会获取 `--` 参数(例如 `--server.port=9000` ),并将这个 `property` 添加到 Spring 的 `Environment` 中。 + +如果不想加载命令行属性,可以通过 `SpringApplication.setAddCommandLineProperties(false)` 禁用。 + +## Application 属性文件 + +`SpringApplication` 会自动加载以下路径下的 `application.properties` 配置文件,将其中的属性读到 Spring 的 `Environment` 中。 + +1. 当前目录的 `/config` 子目录 +2. 当前目录 +3. classpath 路径下的 `/config` package +4. classpath 根路径 + +> 注: +> +> 以上列表的配置文件会根据顺序,后序的配置会覆盖前序的配置。 +> +> 你可以选择 [YAML(yml)](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config-yaml) 配置文件替换 properties 配置文件。 + +如果不喜欢 `application.properties` 作为配置文件名,可以使用 `spring.config.name` 环境变量替换: + +``` +$ java -jar myproject.jar --spring.config.name=myproject +``` + +可以使用 `spring.config.location` 环境变量指定配置文件路径: + +```properties +$ java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties +``` + +## Profile 特定属性 + +如果定义 `application-{profile}.properties` 形式的配置文件,将被视为 `profile` 环境下的特定配置。 + +可以通过 `spring.profiles.active` 参数来激活 profile,如果没有激活的 profile,默认会加载 `application-default.properties` 中的配置。 + +## 属性中的占位符 + +`application.properties` 中的值会被 `Environment` 过滤,所以,可以引用之前定义的属性。 + +``` +app.name=MyApp +app.description=${app.name} is a Spring Boot application +``` + +> 注:你可以使用此技术来创建 Spring Boot 属性变量。请参考: [Section 77.4, “Use ‘Short’ Command Line Arguments](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#howto-use-short-command-line-arguments) + +## YAML 属性 + +Spring 框架有两个类支持加载 YAML 文件。 + +- `YamlPropertiesFactoryBean` 将 YAML 文件的配置加载为 `Properties` 。 +- `YamlMapFactoryBean` 将 YAML 文件的配置加载为 `Map` 。 + +示例 1 + +```yaml +environments: + dev: + url: http://dev.example.com + name: Developer Setup + prod: + url: http://another.example.com + name: My Cool App +``` + +等价于: + +```properties +environments.dev.url=http://dev.example.com +environments.dev.name=Developer Setup +environments.prod.url=http://another.example.com +environments.prod.name=My Cool App +``` + +YAML 支持列表形式,等价于 property 中的 `[index]` : + +```yaml +my: +servers: + - dev.example.com + - another.example.com +``` + +等价于 + +```properties +my.servers[0]=dev.example.com +my.servers[1]=another.example.com +``` + +### 访问属性 + +`YamlPropertySourceLoader` 类会将 YAML 配置转化为 Spring `Environment` 类中的 `PropertySource` 。然后,你可以如同 properties 文件中的属性一样,使用 `@Value` 注解来访问 YAML 中配置的属性。 + +### 多 profile 配置 + +```yaml +server: + address: 192.168.1.100 +--- +spring: + profiles: development +server: + address: 127.0.0.1 +--- +spring: + profiles: production & eu-central +server: + address: 192.168.1.120 +``` + +### YAML 的缺点 + +注:YAML 注解中的属性不能通过 `@PropertySource` 注解来访问。所以,如果你的项目中使用了一些自定义属性文件,建议不要用 YAML。 + +## 属性前缀 + +```java +package com.example; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix="acme") +public class AcmeProperties { + + private boolean enabled; + + private InetAddress remoteAddress; + + private final Security security = new Security(); + + public boolean isEnabled() { ... } + + public void setEnabled(boolean enabled) { ... } + + public InetAddress getRemoteAddress() { ... } + + public void setRemoteAddress(InetAddress remoteAddress) { ... } + + public Security getSecurity() { ... } + + public static class Security { + + private String username; + + private String password; + + private List roles = new ArrayList<>(Collections.singleton("USER")); + + public String getUsername() { ... } + + public void setUsername(String username) { ... } + + public String getPassword() { ... } + + public void setPassword(String password) { ... } + + public List getRoles() { ... } + + public void setRoles(List roles) { ... } + + } +} +``` + +相当于支持配置以下属性: + +- `acme.enabled` +- `acme.remote-address` +- `acme.security.username` +- `acme.security.password` +- `acme.security.roles` + +然后,你需要使用 `@EnableConfigurationProperties` 注解将属性类注入配置类中。 + +```java +@Configuration +@EnableConfigurationProperties(AcmeProperties.class) +public class MyConfiguration { +} +``` + +## 属性松散绑定规则 + +Spring Boot 属性名绑定比较松散。 + +以下属性 key 都是等价的: + +| Property | Note | +| ----------------------------------- | -------- | +| `acme.my-project.person.first-name` | `-` 分隔 | +| `acme.myProject.person.firstName` | 驼峰命名 | +| `acme.my_project.person.first_name` | `_` 分隔 | +| `ACME_MYPROJECT_PERSON_FIRSTNAME` | 大写字母 | + +## 属性转换 + +如果需要类型转换,你可以提供一个 `ConversionService` bean (一个名叫 `conversionService` 的 bean) 或自定义属性配置 (一个 `CustomEditorConfigurer` bean) 或自定义的 `Converters` (被 `@ConfigurationPropertiesBinding` 注解修饰的 bena)。 + +### 时间单位转换 + +Spring 使用 `java.time.Duration` 类代表时间大小,以下场景适用: + +- 除非指定 `@DurationUnit` ,否则一个 long 代表的时间为毫秒。 +- ISO-8601 标准格式( [`java.time.Duration`](https://docs.oracle.com/javase/8/docs/api//java/time/Duration.html#parse-java.lang.CharSequence-) 的实现就是参照此标准) +- 你也可以使用以下支持的单位: + - `ns` - 纳秒 + - `us` - 微秒 + - `ms` - 毫秒 + - `s` - 秒 + - `m` - 分 + - `h` - 时 + - `d` - 天 + +示例: + +```java +@ConfigurationProperties("app.system") +public class AppSystemProperties { + + @DurationUnit(ChronoUnit.SECONDS) + private Duration sessionTimeout = Duration.ofSeconds(30); + + private Duration readTimeout = Duration.ofMillis(1000); + + public Duration getSessionTimeout() { + return this.sessionTimeout; + } + + public void setSessionTimeout(Duration sessionTimeout) { + this.sessionTimeout = sessionTimeout; + } + + public Duration getReadTimeout() { + return this.readTimeout; + } + + public void setReadTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + } + +} +``` + +### 数据大小转换 + +Spring 使用 `DataSize` 类代表数据大小,以下场景适用: + +- long 值(默认视为 byte) +- 你也可以使用以下支持的单位: + - `B` + - `KB` + - `MB` + - `GB` + - `TB` + +示例: + +```java +@ConfigurationProperties("app.io") +public class AppIoProperties { + + @DataSizeUnit(DataUnit.MEGABYTES) + private DataSize bufferSize = DataSize.ofMegabytes(2); + + private DataSize sizeThreshold = DataSize.ofBytes(512); + + public DataSize getBufferSize() { + return this.bufferSize; + } + + public void setBufferSize(DataSize bufferSize) { + this.bufferSize = bufferSize; + } + + public DataSize getSizeThreshold() { + return this.sizeThreshold; + } + + public void setSizeThreshold(DataSize sizeThreshold) { + this.sizeThreshold = sizeThreshold; + } + +} +``` + +## 校验属性 + +```java +@ConfigurationProperties(prefix="acme") +@Validated +public class AcmeProperties { + + @NotNull + private InetAddress remoteAddress; + + @Valid + private final Security security = new Security(); + + // ... getters and setters + + public static class Security { + + @NotEmpty + public String username; + + // ... getters and setters + + } + +} +``` + +你也可以自定义一个名为 `configurationPropertiesValidator` 的校验器 Bean。获取这个 `@Bean` 的方法必须声明为 `static`。 + +## 示例源码 + +> 示例源码:[spring-boot-property](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-property) + +## 参考资料 + +- [Spring Boot 官方文档之 boot-features-external-config](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-external-config) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/33.SpringBoot\344\271\213Profile.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/33.SpringBoot\344\271\213Profile.md" new file mode 100644 index 00000000..667df738 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/33.SpringBoot\344\271\213Profile.md" @@ -0,0 +1,200 @@ +--- +title: SpringBoot 之 Profile +date: 2019-11-18 14:55:01 +order: 33 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/cb598e/ +--- + +# SpringBoot 之 Profile + +> 一个应用为了在不同的环境下工作,常常会有不同的配置,代码逻辑处理。Spring Boot 对此提供了简便的支持。 +> +> 关键词: `@Profile`、`spring.profiles.active` + +## 区分环境的配置 + +### properties 配置 + +假设,一个应用的工作环境有:dev、test、prod + +那么,我们可以添加 4 个配置文件: + +- `applcation.properties` - 公共配置 +- `application-dev.properties` - 开发环境配置 +- `application-test.properties` - 测试环境配置 +- `application-prod.properties` - 生产环境配置 + +在 `applcation.properties` 文件中可以通过以下配置来激活 profile: + +```properties +spring.profiles.active = test +``` + +### yml 配置 + +与 properties 文件类似,我们也可以添加 4 个配置文件: + +- `applcation.yml` - 公共配置 +- `application-dev.yml` - 开发环境配置 +- `application-test.yml` - 测试环境配置 +- `application-prod.yml` - 生产环境配置 + +在 `applcation.yml` 文件中可以通过以下配置来激活 profile: + +```yml +spring: + profiles: + active: prod +``` + +此外,yml 文件也可以在一个文件中完成所有 profile 的配置: + +```yml +# 激活 prod +spring: + profiles: + active: prod +# 也可以同时激活多个 profile +# spring.profiles.active: prod,proddb,prodlog +--- +# dev 配置 +spring: + profiles: dev + +# 略去配置 + +--- +spring: + profiles: test + +# 略去配置 + +--- +spring.profiles: prod +spring.profiles.include: + - proddb + - prodlog + +--- +spring: + profiles: proddb + +# 略去配置 + +--- +spring: + profiles: prodlog +# 略去配置 +``` + +注意:不同 profile 之间通过 `---` 分割 + +## 区分环境的代码 + +使用 `@Profile` 注解可以指定类或方法在特定的 Profile 环境生效。 + +### 修饰类 + +```java +@Configuration +@Profile("production") +public class JndiDataConfig { + + @Bean(destroyMethod="") + public DataSource dataSource() throws Exception { + Context ctx = new InitialContext(); + return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); + } +} +``` + +### 修饰注解 + +```java +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Profile("production") +public @interface Production { +} +``` + +### 修饰方法 + +```java +@Configuration +public class AppConfig { + + @Bean("dataSource") + @Profile("development") + public DataSource standaloneDataSource() { + return new EmbeddedDatabaseBuilder() + .setType(EmbeddedDatabaseType.HSQL) + .addScript("classpath:com/bank/config/sql/schema.sql") + .addScript("classpath:com/bank/config/sql/test-data.sql") + .build(); + } + + @Bean("dataSource") + @Profile("production") + public DataSource jndiDataSource() throws Exception { + Context ctx = new InitialContext(); + return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource"); + } +} +``` + +## 激活 profile + +### 插件激活 profile + +``` +spring-boot:run -Drun.profiles=prod +``` + +### main 方法激活 profile + +``` +--spring.profiles.active=prod +``` + +### jar 激活 profile + +``` +java -jar -Dspring.profiles.active=prod *.jar +``` + +### 在 Java 代码中激活 profile + +直接指定环境变量来激活 profile: + +```java +System.setProperty("spring.profiles.active", "test"); +``` + +在 Spring 容器中激活 profile: + +```java +AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); +ctx.getEnvironment().setActiveProfiles("development"); +ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class); +ctx.refresh(); +``` + +## 示例源码 + +> 示例源码:[spring-boot-profile](https://github.com/dunwu/spring-boot-tutorial/tree/master/codes/spring-boot-profile) + +## 参考资料 + +- [Spring 官方文档之 Bean Definition Profiles](https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#beans-definition-profiles) +- [Spring Boot 官方文档之 boot-features-profiles](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-profiles) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/README.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/README.md" new file mode 100644 index 00000000..544eb1c1 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/README.md" @@ -0,0 +1,62 @@ +--- +title: Spring 核心 +date: 2020-02-26 23:47:47 +categories: + - Java + - 框架 + - Spring + - Spring核心 +tags: + - Java + - 框架 + - Spring + - SpringBoot +permalink: /pages/5e7c20/ +hidden: true +index: false +--- + +# Spring 核心 + +> 章节主要针对:Spring & Spring Boot 框架的核心技术。如;Spring Bean、IoC、依赖查找、依赖注入、AOP、数据绑定、资源管理等。 + +## 📖 内容 + +- [Spring Bean](01.SpringBean.md) +- [Spring IoC](02.SpringIoC.md) +- [Spring 依赖查找](03.Spring依赖查找.md) +- [Spring 依赖注入](04.Spring依赖注入.md) +- [Spring IoC 依赖来源](05.SpringIoC依赖来源.md) +- [Spring Bean 作用域](06.SpringBean作用域.md) +- [Spring Bean 生命周期](07.SpringBean生命周期.md) +- [Spring 配置元数据](08.Spring配置元数据.md) +- [Spring AOP](10.SpringAop.md) +- [Spring 资源管理](20.Spring资源管理.md) +- [Spring 校验](21.Spring校验.md) +- [Spring 数据绑定](22.Spring数据绑定.md) +- [Spring 类型转换](23.Spring类型转换.md) +- [Spring EL 表达式](24.SpringEL.md) +- [Spring 事件](25.Spring事件.md) +- [Spring 国际化](26.Spring国际化.md) +- [Spring 泛型处理](27.Spring泛型处理.md) +- [Spring 注解](28.Spring注解.md) +- [SpringBoot 教程之快速入门](31.SpringBoot之快速入门.md) +- [SpringBoot 之属性加载](32.SpringBoot之属性加载.md) +- [SpringBoot 之 Profile](33.SpringBoot之Profile.md) + +## 📚 资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/01.Spring\344\271\213\346\225\260\346\215\256\346\272\220.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/01.Spring\344\271\213\346\225\260\346\215\256\346\272\220.md" new file mode 100644 index 00000000..5d01d703 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/01.Spring\344\271\213\346\225\260\346\215\256\346\272\220.md" @@ -0,0 +1,555 @@ +--- +title: Spring 之数据源 +date: 2017-10-20 09:27:55 +order: 01 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 数据库 + - DataSource +permalink: /pages/1b774c/ +--- + +# Spring 之数据源 + +> 本文基于 Spring Boot 2.7.3 版本。 + +## Spring Boot 数据源基本配置 + +Spring Boot 提供了一系列 `spring.datasource.*` 配置来控制 `DataSource` 的配置。用户可以在 `application.properties` 或 `application.yml` 文件中指定数据源配置。这些配置项维护在 [`DataSourceProperties`](https://github.com/spring-projects/spring-boot/tree/v2.7.4/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java) 。 + +下面是一个最基本的 mysql 数据源配置示例(都是必填项): + +```properties +# 数据库访问地址 +spring.datasource.url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +# 数据库驱动类,必须保证驱动类是可加载的 +spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver +# 数据库账号 +spring.datasource.username = root +# 数据库账号密码 +spring.datasource.password = root +``` + +需要根据实际情况,替换 `url`、`username`、`password`。 + +## Spring Boot 连接嵌入式数据源 + +使用内存嵌入式数据库开发应用程序通常很方便。显然,内存数据库不提供持久存储。使用者需要在应用程序启动时填充数据库,并准备在应用程序结束时丢弃数据。 + +Spring Boot 可以自动配置嵌入式数据库 [H2](https://www.h2database.com/)、[HSQL](https://hsqldb.org/) 和 [Derby](https://db.apache.org/derby/)。使用者无需提供任何连接 URL,只需要包含对要使用的嵌入式数据库的构建依赖项。如果类路径上有多个嵌入式数据库,需要设置 `spring.datasource.embedded-database-connection` 配置属性来控制使用哪一个。将该属性设置为 none 会禁用嵌入式数据库的自动配置。 + +> 注意:如果在测试中使用此功能,无论使用多少应用程序上下文,整个测试套件都会重用同一个数据库。如果要确保每个上下文都有一个单独的嵌入式数据库,则应将 `spring.datasource.generate-unique-name` 设置为 true。 + +下面,通过一个实例展示如何连接 H2 嵌入式数据库。 + +(1)在 pom.xml 中引入所需要的依赖: + +```xml + + org.springframework.boot + spring-boot-starter-data-jdbc + + + com.h2database + h2 + +``` + +(2)数据源配置 + +```properties +spring.datasource.jdbc-url = jdbc:h2:mem:test +spring.datasource.driver-class-name = org.h2.Driver +spring.datasource.username = sa +spring.datasource.password = +``` + +## Spring Boot 连接池化数据源 + +> 完整示例:[spring-boot-data-jdbc](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/spring-boot-data-jdbc) + +在生产环境中,出于性能考虑,一般会通过数据库连接池连接数据源。 + +除了 [`DataSourceProperties`](https://github.com/spring-projects/spring-boot/tree/v2.7.4/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceProperties.java) 中的数据源通用配置以外,Spring Boot 还支持通过使用类似`spring.datasource.hikari.*`、`spring.datasource.tomcat.*`、`spring.datasource.dbcp2.*` 和 `spring.datasource.oracleucp.*` 的前缀来配置指定的数据库连接池属性。 + +下面,就是一份 hikari 的连接池配置示例: + +```properties +# 连接池名称 +spring.datasource.hikari.pool-name = SpringTutorialHikariPool +# 最大连接数,小于等于 0 会被重置为默认值 10;大于零小于 1 会被重置为 minimum-idle 的值 +spring.datasource.hikari.maximum-pool-size = 10 +# 最小空闲连接,默认值10,小于 0 或大于 maximum-pool-size,都会重置为 maximum-pool-size +spring.datasource.hikari.minimum-idle = 10 +# 连接超时时间(单位:毫秒),小于 250 毫秒,会被重置为默认值 30 秒 +spring.datasource.hikari.connection-timeout = 60000 +# 空闲连接超时时间,默认值 600000(10分钟),大于等于 max-lifetime 且 max-lifetime>0,会被重置为0;不等于 0 且小于 10 秒,会被重置为 10 秒 +# 只有空闲连接数大于最大连接数且空闲时间超过该值,才会被释放 +spring.datasource.hikari.idle-timeout = 600000 +# 连接最大存活时间,不等于 0 且小于 30 秒,会被重置为默认值 30 分钟。该值应该比数据库所设置的超时时间短 +spring.datasource.hikari.max-lifetime = 540000 +``` + +Spring Boot 会按以下顺序检测连接池是否可用,如果可用就选择对应的池化 `DataSource`: + +HikariCP -> Tomcat pooling DataSource -> DBCP2 -> Oracle UCP + +用户也可以通过 `spring.datasource.type` 来指定数据源类型。 + +此外,也可以使用 `DataSourceBuilder` 手动配置其他连接池。如果自定义 DataSource bean,则不会发生自动配置。 `DataSourceBuilder` 支持以下连接池: + +- HikariCP +- Tomcat pooling `Datasource` +- Commons DBCP2 +- Oracle UCP & `OracleDataSource` +- Spring Framework’s `SimpleDriverDataSource` +- H2 `JdbcDataSource` +- PostgreSQL `PGSimpleDataSource` +- C3P0 + +### 引入 Spring Boot 依赖 + +你可以通过 Spring Boot 官方的初始化器([Spring Initializr](https://start.spring.io/))选择需要的组件来创建一个 Spring Boot 工程。或者,直接在 pom.xml 中引入所需要的依赖: + +```xml + + org.springframework.boot + spring-boot-starter-data-jdbc + + + mysql + mysql-connector-java + +``` + +### 测试单数据源连接 + +```java +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Connection; +import javax.sql.DataSource; + +@Slf4j +@SpringBootApplication +public class SpringBootDataJdbcApplication implements CommandLineRunner { + + private final JdbcTemplate jdbcTemplate; + + public SpringBootDataJdbcApplication(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public static void main(String[] args) { + SpringApplication.run(SpringBootDataJdbcApplication.class, args); + } + + @Override + public void run(String... args) throws Exception { + DataSource dataSource = jdbcTemplate.getDataSource(); + + Connection connection; + if (dataSource != null) { + connection = dataSource.getConnection(); + } else { + log.error("连接数据源失败!"); + return; + } + + if (connection != null) { + log.info("数据源 Url: {}", connection.getMetaData().getURL()); + } else { + log.error("连接数据源失败!"); + } + } + +} +``` + +运行 `main` 方法后,控制台会输出以下内容,表示数据源连接成功: + +``` +20:50:18.449 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcApplication.run - 数据源 Url: jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +``` + +## Spring Boot 连接多数据源 + +> 完整示例:[spring-boot-data-jdbc-multi-datasource](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/spring-boot-data-jdbc-multi-datasource) + +Spring Boot 连接多数据源所需要的依赖并无不同,主要差异在于数据源的配置。Spring Boot 默认的数据源配置类为 `org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration`。使用者只要指定一些必要的 spring.datasource 配置,`DataSourceAutoConfiguration` 类就会自动完成剩下的数据源实例化工作。 + +### 多数据源配置 + +下面的示例中,自定义了一个数据源配置类,通过读取不同的 spring.datasource.xxx 来完成对于不同数据源的实例化工作。对于 JDBC 来说,最重要的,就是实例化 `DataSource` 和 `JdbcTemplate`。 + +```java +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.JdbcTemplate; + +@Configuration +public class DataSourceConfig { + + @Primary + @Bean("mysqlDataSource") + @ConfigurationProperties(prefix = "spring.datasource.mysql") + public DataSource mysqlDataSource() { + return DataSourceBuilder.create().build(); + } + + @Primary + @Bean("mysqlJdbcTemplate") + public JdbcTemplate mysqlJdbcTemplate(@Qualifier("mysqlDataSource") DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + + @Bean("h2DataSource") + @ConfigurationProperties(prefix = "spring.datasource.h2") + public DataSource h2DataSource() { + return DataSourceBuilder.create().build(); + } + + @Bean(name = "h2JdbcTemplate") + public JdbcTemplate h2JdbcTemplate(@Qualifier("h2DataSource") DataSource dataSource) { + return new JdbcTemplate(dataSource); + } + +} +``` + +`application.properties` 或 `application.yml` 配置文件中也必须以 `@ConfigurationProperties` 所指定的配置前缀进行配置: + +```properties +# 数据源一:Mysql +spring.datasource.mysql.jdbc-url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false +spring.datasource.mysql.driver-class-name = com.mysql.cj.jdbc.Driver +spring.datasource.mysql.username = root +spring.datasource.mysql.password = root +# 数据源一:H2 +spring.datasource.h2.jdbc-url = jdbc:h2:mem:test +spring.datasource.h2.driver-class-name = org.h2.Driver +spring.datasource.h2.username = sa +spring.datasource.h2.password = +``` + +### 测试多数据源连接 + +```java + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Connection; +import java.sql.SQLException; +import javax.sql.DataSource; + +@SpringBootApplication +public class SpringBootDataJdbcMultiDataSourceApplication implements CommandLineRunner { + + private static final Logger log = LoggerFactory.getLogger(SpringBootDataJdbcMultiDataSourceApplication.class); + + private final UserDao mysqlUserDao; + + private final UserDao h2UserDao; + + public SpringBootDataJdbcMultiDataSourceApplication(@Qualifier("mysqlUserDao") UserDao mysqlUserDao, + @Qualifier("h2UserDao") UserDao h2UserDao) { + this.mysqlUserDao = mysqlUserDao; + this.h2UserDao = h2UserDao; + } + + public static void main(String[] args) { + SpringApplication.run(SpringBootDataJdbcMultiDataSourceApplication.class, args); + } + + @Override + public void run(String... args) throws Exception { + + if (mysqlUserDao != null && mysqlUserDao.getJdbcTemplate() != null) { + printDataSourceInfo(mysqlUserDao.getJdbcTemplate()); + log.info("Connect to mysql datasource success."); + } else { + log.error("Connect to mysql datasource failed!"); + return; + } + + if (h2UserDao != null) { + printDataSourceInfo(h2UserDao.getJdbcTemplate()); + log.info("Connect to h2 datasource success."); + } else { + log.error("Connect to h2 datasource failed!"); + return; + } + + // 主数据源执行 JDBC SQL + mysqlUserDao.recreateTable(); + + // 次数据源执行 JDBC SQL + h2UserDao.recreateTable(); + } + + private void printDataSourceInfo(JdbcTemplate jdbcTemplate) throws SQLException { + + DataSource dataSource = jdbcTemplate.getDataSource(); + + Connection connection; + if (dataSource != null) { + connection = dataSource.getConnection(); + } else { + log.error("Get dataSource failed!"); + return; + } + + if (connection != null) { + log.info("DataSource Url: {}", connection.getMetaData().getURL()); + } else { + log.error("Connect to datasource failed!"); + } + } + +} +``` + +运行 `main` 方法后,控制台会输出以下内容,表示数据源连接成功: + +``` +21:16:44.654 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.printDataSourceInfo - DataSource Url: jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8&useSSL=false +21:16:44.654 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.run - Connect to mysql datasource success. + +21:16:44.726 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.printDataSourceInfo - DataSource Url: jdbc:h2:mem:test +21:16:44.726 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcMultiDataSourceApplication.run - Connect to h2 datasource success. +``` + +## Spring 之数据源 + +如果你的项目是传统的 Spring 项目,当然也可以轻松建立数据源连接,只是需要自行设置的配置更多一些。 + +### 引入 Spring 依赖 + +在 pom.xml 中引入所需要的依赖: + +```xml + + + com.alibaba + druid + + + mysql + mysql-connector-java + + + + org.springframework + spring-context-support + + + org.springframework + spring-jdbc + + + org.springframework + spring-tx + + + +``` + +### Spring 配置数据源 + +Spring 配置数据源有多种方式,下面一一列举: + +#### 使用 JNDI 数据源 + +如果 Spring 应用部署在支持 JNDI 的 WEB 服务器上(如 WebSphere、JBoss、Tomcat 等),就可以使用 JNDI 获取数据源。 + +```xml + + + + + + + + + + + +``` + +#### 使用数据库连接池 + +Spring 本身并没有提供数据库连接池的实现,需要自行选择合适的数据库连接池。下面是一个使用 [Druid](https://github.com/alibaba/druid) 作为数据库连接池的示例: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +#### 基于 JDBC 驱动的数据源 + +```xml + + + + + + +``` + +## SpringBoot 数据源配置 + +> Spring Boot 数据库配置官方文档:https://docs.spring.io/spring-boot/docs/current/reference/html/data.html#data.sql + +通过前面的实战,我们已经知道了 Spring、Spring Boot 是如何连接数据源,并通过 JDBC 方式访问数据库。 + +SpringBoot 数据源的配置方式是在 `application.properties` 或 `application.yml` 文件中指定 `spring.datasource.*` 的配置。 + +(1)数据源基本配置方式是指定 url、用户名、密码 + +```properties +spring.datasource.url=jdbc:mysql://localhost/test +spring.datasource.username=dbuser +spring.datasource.password=dbpass +``` + +(2)配置 JNDI + +如果想要通过 JNDI 方式连接数据源,可以采用如下方式: + +```properties +spring.datasource.jndi-name=java:jboss/datasources/customers +``` + +## DataSourceAutoConfiguration 类 + +显而易见,Spring Boot 的配置更加简化,那么, Spring Boot 做了哪些工作,使得接入更加便捷呢?奥秘就在于 `spring-boot-autoconfigure` jar 包,其中定义了大量的 Spring Boot 自动配置类。其中,与数据库访问相关的比较核心的配置类有: + +- `DataSourceAutoConfiguration`:数据源自动配置类 +- `JdbcTemplateAutoConfiguration`:`JdbcTemplate` 自动配置类 +- `DataSourceTransactionManagerAutoConfiguration`:数据源事务管理自动配置类 +- `JndiDataSourceAutoConfiguration`:JNDI 数据源自动配置类 +- `EmbeddedDataSourceConfiguration`:嵌入式数据库数据源自动配置类 +- 等等 + +这些自动配置类会根据各种条件控制核心类的实例化。 + +`DataSourceAutoConfiguration` 是数据源自动配置类,它负责实例化 `DataSource`。 + +`DataSourceAutoConfiguration` 的源码如下(省略部分代码): + +```java +@AutoConfiguration(before = SqlInitializationAutoConfiguration.class) +@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) +@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory") +@EnableConfigurationProperties(DataSourceProperties.class) +@Import(DataSourcePoolMetadataProvidersConfiguration.class) +public class DataSourceAutoConfiguration { + + @Configuration(proxyBeanMethods = false) + @Conditional(EmbeddedDatabaseCondition.class) + @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) + @Import(EmbeddedDataSourceConfiguration.class) + protected static class EmbeddedDatabaseConfiguration { + } + + @Configuration(proxyBeanMethods = false) + @Conditional(PooledDataSourceCondition.class) + @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) + @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, + DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, + DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class }) + protected static class PooledDataSourceConfiguration { + } + + static class PooledDataSourceCondition extends AnyNestedCondition { + // 略 + } + + static class PooledDataSourceAvailableCondition extends SpringBootCondition { + // 略 + } + + static class EmbeddedDatabaseCondition extends SpringBootCondition { + // 略 + } +} +``` + +`DataSourceAutoConfiguration` 类的源码解读: + +- `DataSourceProperties` 是 `DataSourceAutoConfiguration` 的配置选项类,允许使用者通过设置选项控制 `DataSource` 初始化行为。 +- `DataSourceAutoConfiguration` 通过 `@Import` 注解引入 `DataSourcePoolMetadataProvidersConfiguration` 类。 +- `DataSourceAutoConfiguration` 中定义了两个内部类:嵌入式数据源配置类 `EmbeddedDatabaseConfiguration` 和 池化数据源配置类 `PooledDataSourceConfiguration`,分别标记了不同的实例化条件。 + - 当满足 `EmbeddedDatabaseConfiguration` 的示例化条件时,将引入 `EmbeddedDataSourceConfiguration` 类初始化数据源,这个类实际上是加载嵌入式数据源驱动的 ClassLoader 去进行初始化。 + - 当满足 `PooledDataSourceConfiguration` 的示例化条件时,将引入 `DataSourceConfiguration.Hikari.class`、`DataSourceConfiguration.Tomcat.class`、`DataSourceConfiguration.Dbcp2.class`、`DataSourceConfiguration.OracleUcp.class`、`DataSourceConfiguration.Generic.class`、`DataSourceJmxConfiguration.class` 这些配置类,分别对应不同的数据库连接池方式。具体选用哪种数据库连接池,可以通过 `spring.datasource.type` 配置指定。其中,Hikari 是 Spring Boot 默认的数据库连接池,spring-boot-starter-data-jdbc 中内置了 Hikari 连接池驱动包。如果想要替换其他数据库连接池,前提是必须先手动引入对应的连接池驱动包。 + +## 参考资料 + +- [Spring 官网](https://spring.io/) +- [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/02.Spring\344\271\213JDBC.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/02.Spring\344\271\213JDBC.md" new file mode 100644 index 00000000..e1990392 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/02.Spring\344\271\213JDBC.md" @@ -0,0 +1,768 @@ +--- +title: Spring 之 JDBC +date: 2019-02-18 14:33:55 +order: 02 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - JDBC + - JdbcTemplate +permalink: /pages/cf19fd/ +--- + +# Spring 之 JDBC + +JDBC 是 Java 语言中用来规范客户端程序如何访问数据库的应用程序接口,提供了增、删、改、查数据库的方法。 + +## JDBC 入门示例 + +JDBC 的工作步骤大致如下: + +1. 创建实体类。 +2. 声明数据库读写接口的 DAO 接口。定义 DAO 的好处在于对于数据层上层的业务,调用 DAO 时仅关注对外暴露的读写方法,而不考虑底层的具体持久化方式。这样,便于替换持久化方式。 +3. 创建一个 DAO 接口的实现类,使用 Spring 的 JDBC 模板去实现接口。 +4. 最后,定义一个 DAO 接口的实现类的 JavaBean,并将数据源注入进去。 + +假设,我们要通过 Spring + JDBC 访问一张 Mysql 数据表 `user`,`user` 表的数据结构如下: + +```sql +-- 创建用户表 +CREATE TABLE `user` ( + `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'ID', + `name` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '用户名', + `age` INT(3) NOT NULL DEFAULT 0 COMMENT '年龄', + `address` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '地址', + `email` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '邮件', + PRIMARY KEY (`id`), + UNIQUE (`name`) +) COMMENT = '用户表'; + +INSERT INTO `user` (`name`, `age`, `address`, `email`) +VALUES ('张三', 18, '北京', 'xxx@163.com'); +INSERT INTO `user` (`name`, `age`, `address`, `email`) +VALUES ('李四', 19, '上海', 'xxx@163.com'); +``` + +### 定义实体 + +```java +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Objects; + +@Data +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class User { + private Long id; + private String name; + private Integer age; + private String address; + private String email; +} +``` + +### 定义 DAO 接口 + +```java +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.List; + +/** + * user 表 Dao 接口 + * + * @author Zhang Peng + * @since 2019-11-18 + */ +public interface UserDao { + + // DML + // ------------------------------------------------------------------- + void insert(User user); + + void batchInsert(List users); + + void deleteByName(String name); + + void deleteAll(); + + void update(User user); + + Integer count(); + + List list(); + + User queryByName(String name); + + JdbcTemplate getJdbcTemplate(); + + // DDL + // ------------------------------------------------------------------- + void truncate(); + + void recreateTable(); + +} +``` + +### 定义 DAO 实现类 + +通过 `JdbcTemplate` 执行对应数据源符合语法的 SQL,即可完成各种数据库访问。 + +```java +package io.github.dunwu.springboot.core.data.jdbc; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; + +/** + * user 表 Dao 接口实现类 + * + * @author Zhang Peng + * @since 2019-11-18 + */ +@Repository +public class UserDaoImpl implements UserDao { + + private JdbcTemplate jdbcTemplate; + + public UserDaoImpl(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public void insert(User user) { + jdbcTemplate.update("INSERT INTO user(name, age, address, email) VALUES(?, ?, ?, ?)", + user.getName(), user.getAge(), user.getAddress(), user.getEmail()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void batchInsert(List users) { + String sql = "INSERT INTO user(name, age, address, email) VALUES(?, ?, ?, ?)"; + + List params = new ArrayList<>(); + + users.forEach(user -> { + params.add(new Object[] { user.getName(), user.getAge(), user.getAddress(), user.getEmail() }); + }); + jdbcTemplate.batchUpdate(sql, params); + } + + @Override + public void deleteByName(String name) { + jdbcTemplate.update("DELETE FROM user WHERE name = ?", name); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteAll() { + jdbcTemplate.execute("DELETE FROM user"); + } + + @Override + public void update(User user) { + jdbcTemplate.update("UPDATE user SET name=?, age=?, address=?, email=? WHERE id=?", + user.getName(), user.getAge(), user.getAddress(), user.getEmail(), user.getId()); + } + + @Override + public Integer count() { + try { + return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM user", Integer.class); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public List list() { + return jdbcTemplate.query("SELECT * FROM user", new BeanPropertyRowMapper<>(User.class)); + } + + @Override + public User queryByName(String name) { + try { + return jdbcTemplate.queryForObject("SELECT * FROM user WHERE name = ?", + new BeanPropertyRowMapper<>(User.class), name); + } catch (EmptyResultDataAccessException e) { + return null; + } + } + + @Override + public JdbcTemplate getJdbcTemplate() { + return jdbcTemplate; + } + + @Override + public void truncate() { + jdbcTemplate.execute("TRUNCATE TABLE user"); + } + + @Override + public void recreateTable() { + jdbcTemplate.execute("DROP TABLE IF EXISTS user"); + + String sqlStatement = + "CREATE TABLE IF NOT EXISTS user (\n" + + " id BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Id',\n" + + " name VARCHAR(255) NOT NULL DEFAULT '' COMMENT '用户名',\n" + + " age INT(3) NOT NULL DEFAULT 0 COMMENT '年龄',\n" + + " address VARCHAR(255) NOT NULL DEFAULT '' COMMENT '地址',\n" + + " email VARCHAR(255) NOT NULL DEFAULT '' COMMENT '邮件',\n" + + " PRIMARY KEY (id)\n" + + ") COMMENT = '用户表';"; + jdbcTemplate.execute(sqlStatement); + } + +} +``` + +### 测试类 + +```java +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.Rollback; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +@Rollback +@SpringBootTest(classes = { SpringBootDataJdbcApplication.class }) +public class DataJdbcMysqlDataSourceTest { + + @Autowired + private UserDao userDAO; + + @BeforeEach + public void before() { + userDAO.truncate(); + } + + @Test + public void insert() { + userDAO.insert(new User("张三", 18, "北京", "user1@163.com")); + User linda = userDAO.queryByName("张三"); + assertThat(linda).isNotNull(); + } + + @Test + public void batchInsert() { + List users = new ArrayList<>(); + users.add(new User("张三", 18, "北京", "user1@163.com")); + users.add(new User("李四", 19, "上海", "user1@163.com")); + users.add(new User("王五", 18, "南京", "user1@163.com")); + users.add(new User("赵六", 20, "武汉", "user1@163.com")); + + userDAO.batchInsert(users); + int count = userDAO.count(); + assertThat(count).isEqualTo(4); + + List list = userDAO.list(); + assertThat(list).isNotEmpty().hasSize(4); + list.forEach(user -> { + log.info(user.toString()); + }); + } + + @Test + public void delete() { + List users = new ArrayList<>(); + users.add(new User("张三", 18, "北京", "user1@163.com")); + users.add(new User("李四", 19, "上海", "user1@163.com")); + users.add(new User("王五", 18, "南京", "user1@163.com")); + users.add(new User("赵六", 20, "武汉", "user1@163.com")); + userDAO.batchInsert(users); + + userDAO.deleteByName("张三"); + User user = userDAO.queryByName("张三"); + assertThat(user).isNull(); + + userDAO.deleteAll(); + List list = userDAO.list(); + assertThat(list).isEmpty(); + } + + @Test + public void update() { + userDAO.insert(new User("张三", 18, "北京", "user1@163.com")); + User oldUser = userDAO.queryByName("张三"); + oldUser.setName("张三丰"); + userDAO.update(oldUser); + User newUser = userDAO.queryByName("张三丰"); + assertThat(newUser).isNotNull(); + } + +} +``` + +## Spring Boot JDBC + +> 完整示例:[spring-boot-data-jdbc](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/spring-boot-data-jdbc) + +### 引入 Spring Boot 依赖 + +你可以通过 Spring Boot 官方的初始化器([Spring Initializr](https://start.spring.io/))选择需要的组件来创建一个 Spring Boot 工程。或者,直接在 pom.xml 中引入所需要的依赖: + +```xml + + org.springframework.boot + spring-boot-starter-parent + 2.7.7 + + + + + org.springframework.boot + spring-boot-starter-data-jdbc + + + mysql + mysql-connector-java + + +``` + +### 配置数据源 + +引入依赖后,需要在 `application.properties` 或 `application.yml` 文件中指定数据源配置。 + +下面是一个最基本的数据源配置示例: + +```properties +spring.datasource.url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver +spring.datasource.username = root +spring.datasource.password = root +``` + +需要根据实际情况,替换 `url`、`username`、`password`。 + +### 测试 + +```java +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.sql.Connection; +import javax.sql.DataSource; + +@Slf4j +@SpringBootApplication +public class SpringBootDataJdbcApplication implements CommandLineRunner { + + private final JdbcTemplate jdbcTemplate; + + public SpringBootDataJdbcApplication(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public static void main(String[] args) { + SpringApplication.run(SpringBootDataJdbcApplication.class, args); + } + + @Override + public void run(String... args) throws Exception { + DataSource dataSource = jdbcTemplate.getDataSource(); + + Connection connection; + if (dataSource != null) { + connection = dataSource.getConnection(); + } else { + log.error("连接数据源失败!"); + return; + } + + if (connection != null) { + log.info("数据源 Url: {}", connection.getMetaData().getURL()); + } else { + log.error("连接数据源失败!"); + } + } + +} +``` + +运行 `main` 方法后,控制台会输出以下内容,表示数据源连接成功: + +``` +20:50:18.449 [main] [INFO ] i.g.d.s.d.SpringBootDataJdbcApplication.run - 数据源 Url: jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +``` + +## Spring JDBC + +> 完整示例:[spring-data-jdbc](https://github.com/dunwu/spring-tutorial/tree/master/codes/data/jdbc/spring-data-jdbc) + +`spring-boot-starter-data-jdbc` 引入了 `spring-jdbc` ,其 JDBC 特性就是基于 `spring-jdbc`。 + +### 引入 Spring 依赖 + +在 pom.xml 中引入所需要的依赖: + +```xml + + + com.alibaba + druid + + + mysql + mysql-connector-java + + + + org.springframework + spring-context-support + + + org.springframework + spring-jdbc + + + org.springframework + spring-tx + + + +``` + +### 基于 JDBC 驱动的数据源配置 + +下面是一个 mysql 的 JDBC 数据源配置实例: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### 测试 + +```java + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.io.IOException; +import java.sql.SQLException; + +@SuppressWarnings("all") +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration(locations = { "classpath:data/spring-mysql.xml" }) +public class MysqlJdbcTest { + + @Autowired + private ApplicationContext ctx; + + @Before + public void before() { + ctx = JdbcDemo.getMysqlApplicationContext(); + } + + @Test + public void testExecJdbcOper() throws SQLException, IOException { + UserDao userDao = (UserDaoImpl) ctx.getBean("userDao"); + JdbcDemo.execJdbcOper(userDao); + } + + @After + public void after() { + ((ClassPathXmlApplicationContext) ctx).close(); + } + +} +``` + +## JdbcTemplate API + +Spring 将数据访问的样板式代码提取到模板类中。Spring 提供了 3 个 JDBC 模板类: + +- `JdbcTemplate`:最基本的 Spring JDBC 模板,这个模板支持最简单的 JDBC 数据库访问功能以及简单的索引参数查询。 +- `SimpleJdbcTemplate`:改模板类利用 Java 5 的一些特性,如自动装箱、泛型以及可变参数列表来简化 JDBC 模板的使用。 +- `NamedParameterJdbcTemplate`:使用该模板类执行查询时,可以将查询值以命名参数的形式绑定到 SQL 中,而不是使用简单的索引参数。 + +`spring-jdbc` 最核心的 API 无疑就是 `JdbcTemplate`,可以说所有的 JDBC 数据访问,几乎都是围绕着这个类去工作的。Spring 对数据库的操作在 Jdbc 层面做了深层次的封装,利用依赖注入,把数据源配置装配到 `JdbcTemplate` 中,再由 `JdbcTemplate` 负责具体的数据访问。 + +`JdbcTemplate` 主要提供以下几类方法: + +- `execute` 方法:可以用于执行任何 SQL 语句,一般用于执行 DDL 语句; +- `update` 方法及 `batchUpdate` 方法:`update` 方法用于执行新增、修改、删除等语句;`batchUpdate` 方法用于执行批处理相关语句; +- `query` 方法及 `queryForXXX` 方法:用于执行查询相关语句; +- `call` 方法:用于执行存储过程、函数相关语句。 + +为了方便演示,以下增删改查操作都围绕一个名为 user 的表(该表的主键 id 是自增序列)进行,该表的数据实体如下: + +```java +public class User { + private Integer id; + private String name; + private Integer age; + + // 省略 getter/setter +} +``` + +数据实体只要是一个纯粹的 Java Bean 即可,无需任何注解修饰。 + +### execute 方法 + +使用 execute 执行 DDL 语句,创建一个名为 test 的数据库,并在此数据库下新建一个名为 user 的表。 + +```java +public void recreateTable() { + jdbcTemplate.execute("DROP DATABASE IF EXISTS test"); + jdbcTemplate.execute("CREATE DATABASE test"); + jdbcTemplate.execute("USE test"); + jdbcTemplate.execute("DROP TABLE if EXISTS user"); + jdbcTemplate.execute("DROP TABLE if EXISTS user"); + // @formatter:off + StringBuilder sb = new StringBuilder(); + sb.append("CREATE TABLE user (id int (10) unsigned NOT NULL AUTO_INCREMENT,\n") + .append("name varchar (64) NOT NULL DEFAULT '',\n") + .append("age tinyint (3) NOT NULL DEFAULT 0,\n") + .append("PRIMARY KEY (ID));\n"); + // @formatter:on + jdbcTemplate.execute(sb.toString()); +} +``` + +### update 方法 + +新增数据 + +```java +public void insert(String name, Integer age) { + jdbcTemplate.update("INSERT INTO user(name, age) VALUES(?, ?)", name, age); +} +``` + +删除数据 + +```java +public void delete(String name) { + jdbcTemplate.update("DELETE FROM user WHERE name = ?", name); +} +``` + +修改数据 + +```java +public void update(User user) { + jdbcTemplate.update("UPDATE USER SET name=?, age=? WHERE id=?", user.getName(), user.getAge(), user.getId()); +} +``` + +批处理 + +```java +public void batchInsert(List users) { + String sql = "INSERT INTO user(name, age) VALUES(?, ?)"; + + List params = new ArrayList<>(); + + users.forEach(item -> { + params.add(new Object[] {item.getName(), item.getAge()}); + }); + jdbcTemplate.batchUpdate(sql, params); +} +``` + +### query 方法 + +查单个对象 + +```java +public User queryByName(String name) { + try { + return jdbcTemplate + .queryForObject("SELECT * FROM user WHERE name = ?", new BeanPropertyRowMapper<>(User.class), name); + } catch (EmptyResultDataAccessException e) { + return null; + } +} +``` + +查多个对象 + +```java +public List list() { + return jdbcTemplate.query("select * from USER", new BeanPropertyRowMapper(User.class)); +} +``` + +获取某个记录某列或者 count、avg、sum 等函数返回唯一值 + +```java +public Integer count() { + try { + return jdbcTemplate.queryForObject("SELECT COUNT(*) FROM user", Integer.class); + } catch (EmptyResultDataAccessException e) { + return null; + } +} +``` + +## SpringBoot JDBC 配置 + +### JdbcTemplateAutoConfiguration 类 + +`JdbcTemplateAutoConfiguration` 是 `JdbcTemplate` 自动配置类,它负责实例化 `JdbcTemplate`。 + +```java +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass({ DataSource.class, JdbcTemplate.class }) +@ConditionalOnSingleCandidate(DataSource.class) +@AutoConfigureAfter(DataSourceAutoConfiguration.class) +@EnableConfigurationProperties(JdbcProperties.class) +@Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class }) +public class JdbcTemplateAutoConfiguration { + +} +``` + +`JdbcTemplateAutoConfiguration` 类的源码解读: + +- `@AutoConfigureAfter(DataSourceAutoConfiguration.class)` 表明 `JdbcTemplateAutoConfiguration` 必须在 `DataSourceAutoConfiguration` 执行完之后才开始工作,这意味着:`JdbcTemplate` 的初始化必须在 `DataSource` 初始化之后。 +- `JdbcProperties` 是 `JdbcTemplateAutoConfiguration` 的配置选项类,允许使用者通过设置选项控制 `JdbcTemplate` 初始化行为。 +- `@Import({ JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class })` 表明引入 `JdbcTemplateConfiguration`、`NamedParameterJdbcTemplateConfiguration` 两个配置类,具体的实例化 `JdbcTemplate` 的工作也是放在这两个配置中完成。 + +### JdbcTemplateConfiguration 类 + +`JdbcTemplateConfiguration` 源码如下: + +```java +@Configuration(proxyBeanMethods = false) +@ConditionalOnMissingBean(JdbcOperations.class) +class JdbcTemplateConfiguration { + + @Bean + @Primary + JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + JdbcProperties.Template template = properties.getTemplate(); + jdbcTemplate.setFetchSize(template.getFetchSize()); + jdbcTemplate.setMaxRows(template.getMaxRows()); + if (template.getQueryTimeout() != null) { + jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds()); + } + return jdbcTemplate; + } + +} +``` + +`JdbcTemplateConfiguration` 源码解读:`JdbcTemplateConfiguration` 中根据 `DataSource` 和 `JdbcProperties` 实例化了一个 `JdbcTemplate`。 + +### NamedParameterJdbcTemplateConfiguration 类 + +`NamedParameterJdbcTemplateConfiguration` 源码如下: + +```java +@Configuration(proxyBeanMethods = false) +@ConditionalOnSingleCandidate(JdbcTemplate.class) +@ConditionalOnMissingBean(NamedParameterJdbcOperations.class) +class NamedParameterJdbcTemplateConfiguration { + + @Bean + @Primary + NamedParameterJdbcTemplate namedParameterJdbcTemplate(JdbcTemplate jdbcTemplate) { + return new NamedParameterJdbcTemplate(jdbcTemplate); + } + +} +``` + +`NamedParameterJdbcTemplateConfiguration` 源码解读:`NamedParameterJdbcTemplateConfiguration` 中根据 `JdbcTemplate` 实例化了一个 `NamedParameterJdbcTemplate`。 + +## spring-data-jdbc + +Spring Data 项目包含了对 JDBC 的存储库支持,并将自动为 `CrudRepository` 上的方法生成 SQL。对于更高级的查询,提供了 `@Query` 注解。 + +当 classpath 上存在必要的依赖项时,Spring Boot 将自动配置 Spring Data 的 JDBC 存储库。它们可以通过 `spring-boot-starter-data-jdbc` 的单一依赖项添加到项目中。如有必要,可以通过将 `@EnableJdbcRepositories` 批注或 `JdbcConfiguration` 子类添加到应用程序来控制 Spring Data JDBC 的配置。 + +> 更多 Spring Data JDBC 细节,可以参考 [Spring Data JDBC 官方文档](http://spring.io/projects/spring-data-jdbc)。 + +## 参考资料 + +- [Spring 官网](https://spring.io/) +- [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/03.Spring\344\271\213\344\272\213\345\212\241.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/03.Spring\344\271\213\344\272\213\345\212\241.md" new file mode 100644 index 00000000..5a9b29fe --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/03.Spring\344\271\213\344\272\213\345\212\241.md" @@ -0,0 +1,1272 @@ +--- +title: Spring 之事务 +date: 2022-09-22 07:46:49 +order: 03 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - 事务 +permalink: /pages/128c54/ +--- + +# Spring 之事务 + +Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API(JPA) 等事务 API,实现了一致的编程模型,而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式,配合 Spring Boot 的自动配置,大多数 Spring Boot 项目只需要在方法上标记 `@Transactional` 注解,即可一键开启方法的事务性配置。 + +## 理解事务 + +在软件开发领域,全有或全无的操作被称为**事务(transaction)**。事务允许你将几个操作组合成一个要么全部发生要么全部不发生的工作单元。传统上 Java EE 开发对事务管理有两种选择:**全局事务**或**本地事务**,两者都有很大的局限性。 + +### 事务的特性 + +事务应该具有 4 个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为 **ACID**。 + +- **原子性(Atomic)**:一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。 +- **一致性(Consistent)**:事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。 +- **隔离性(Isolated)**:一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。 +- **持久性(Durable)**:持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。 + +### 全局事务 + +全局事务允许您使用多个事务资源,通常是关系数据库和消息队列。应用服务器通过 JTA 管理全局事务,这是一个繁琐的 API(部分原因在于其异常模型)。此外,JTA UserTransaction 通常需要来自 JNDI,这意味着您还需要使用 JNDI 才能使用 JTA。全局事务的使用限制了应用程序代码的任何潜在重用,因为 JTA 通常仅在应用程序服务器环境中可用。 + +以前,使用全局事务的首选方式是通过 EJB CMT(容器管理事务)。 CMT 是一种声明式事务管理(不同于程序化事务管理)。 EJB CMT 消除了对与事务相关的 JNDI 查找的需要,尽管使用 EJB 本身就需要使用 JNDI。它消除了大部分(但不是全部)编写 Java 代码来控制事务的需要。其明显的缺点是 CMT 与 JTA 和应用程序服务器环境相关联。此外,它仅在选择在 EJB 中实现业务逻辑(或至少在事务性 EJB 外观之后)时才可用。一般来说,EJB 的负面影响是如此之大,以至于这不是一个有吸引力的提议,尤其是在面对声明式事务管理的引人注目的替代方案时。 + +### 本地事务 + +本地事务是指定资源的,例如与 JDBC 连接关联的事务。本地事务可能更容易使用,但有一个明显的缺点:它们不能跨多个事务资源工作。例如,使用 JDBC 连接管理事务的代码不能在全局 JTA 事务中运行。因为应用服务器不参与事务管理,它不能确保跨多个资源的正确性(值得注意的是,大多数应用程序使用单个事务资源。)。另一个缺点是本地事务对编程模型具有侵入性。 + +### Spring 对事务的支持 + +Spring 通过回调机制将实际的事务实现从事务性的代码中抽象出来。Spring 解决了全局和本地事务的缺点。它允许开发人员在任何环境中使用一致的编程模型。您只需编写一次代码,它就可以从不同环境中的不同事务管理策略中受益。Spring 提供了对编码式和声明式事务管理的支持,大多数情况下都推荐使用声明式事务管理。 + +- 编码式事务允许用户在代码中精确定义事务的边界 +- 声明式事务(基于 AOP)有助于用户将操作与事务规则进行解耦 + +通过程序化事务管理,开发人员可以使用 Spring 事务抽象,它可以在任何底层事务基础上运行。使用首选的声明性模型,开发人员通常编写很少或根本不编写与事务管理相关的代码,因此不依赖 Spring 事务 API 或任何其他事务 API。 + +### Spring 事务的优点 + +Spring 框架为事务管理提供了一致的抽象,具有以下好处: + +- 跨不同事务 API 的一致编程模型,例如 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA)。 +- 支持声明式事务管理。 +- 用于编程事务管理的 API 比复杂事务 API(如 JTA)更简单。 +- 与 Spring 的数据访问抽象完美集成。 + +## 核心 API + +### TransactionManager + +Spring 事务抽象的关键是事务策略的概念。事务策略由 `TransactionManager` 定义,特别是用于命令式事务管理的 `org.springframework.transaction.PlatformTransactionManager` 接口和用于响应式事务管理的 `org.springframework.transaction.ReactiveTransactionManager` 接口。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220922073737.png) + +#### PlatformTransactionManager + +以下清单显示了 `PlatformTransactionManager` API 的定义: + +```java +public interface PlatformTransactionManager extends TransactionManager { + + TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; + + void commit(TransactionStatus status) throws TransactionException; + + void rollback(TransactionStatus status) throws TransactionException; +} +``` + +`PlatformTransactionManager` 是一个 SPI 接口,所以使用者可以以编程方式使用它。因为 `PlatformTransactionManager` 是一个接口,所以可以根据需要轻松地 MOCK 或存根。它不依赖于查找策略,例如 JNDI。 `PlatformTransactionManager` 实现的定义与 Spring IoC 容器中的任何其他对象(或 bean)一样。仅此一项优势就使 Spring 事务成为有价值的抽象,即使您使用 JTA 也是如此。与直接使用 JTA 相比,您可以更轻松地测试事务代码。 + +同样,为了与 Spring 的理念保持一致,任何 `PlatformTransactionManager` 接口的方法可以抛出的 `TransactionException` 都是未经检查的(也就是说,它扩展了 `java.lang.RuntimeException` 类)。事务架构故障几乎总是致命的。极少数情况下,应用程序可以从事务失败中恢复,开发人员可以选择捕获和处理 `TransactionException`。重点是开发人员并非被迫这样做。 + +`getTransaction(..)` 方法根据 `TransactionDefinition` 参数返回一个 `TransactionStatus` 对象。如果当前调用堆栈中存在匹配的事务,则返回的 `TransactionStatus` 可能表示新事务或可以表示现有事务。后一种情况的含义是,与 Java EE 事务上下文一样,`TransactionStatus` 与执行线程相关联。 + +从以上可以看出,具体的事务管理机制对 Spring 来说是透明的,它并不关心那些,那些是对应各个平台需要关心的,所以 Spring 事务管理的一个优点就是为不同的事务 API 提供一致的编程模型,如 JTA、JDBC、Hibernate、JPA。下面分别介绍各个平台框架实现事务管理的机制。 + +#### JDBC 事务 + +如果应用程序中直接使用 JDBC 来进行持久化,`DataSourceTransactionManager` 会为你处理事务边界。为了使用 `DataSourceTransactionManager`,你需要使用如下的 XML 将其装配到应用程序的上下文定义中: + +```xml + + + +``` + +实际上,`DataSourceTransactionManager` 是通过调用 `java.sql.Connection` 来管理事务,而后者是通过 `DataSource` 获取到的。通过调用连接的 `commit()` 方法来提交事务,同样,事务失败则通过调用 `rollback()` 方法进行回滚。 + +#### Hibernate 事务 + +如果应用程序的持久化是通过 Hibernate 实现的,那么你需要使用 `HibernateTransactionManager`。对于 Hibernate3,需要在 Spring 上下文定义中添加如下的 `bean` 声明: + +```xml + + + +``` + +`sessionFactory` 属性需要装配一个 Hibernate 的 session 工厂,`HibernateTransactionManager` 的实现细节是它将事务管理的职责委托给 `org.hibernate.Transaction` 对象,而后者是从 Hibernate Session 中获取到的。当事务成功完成时,`HibernateTransactionManager` 将会调用 `Transaction` 对象的 `commit()` 方法,反之,将会调用 `rollback()` 方法。 + +#### Java 持久化 API 事务(JPA) + +Hibernate 多年来一直是事实上的 Java 持久化标准,但是现在 Java 持久化 API 作为真正的 Java 持久化标准进入大家的视野。如果你计划使用 JPA 的话,那你需要使用 Spring 的 `JpaTransactionManager` 来处理事务。你需要在 Spring 中这样配置 `JpaTransactionManager`: + +```xml + + + +``` + +`JpaTransactionManager` 只需要装配一个 JPA 实体管理工厂(`javax.persistence.EntityManagerFactory` 接口的任意实现)。`JpaTransactionManager` 将与由工厂所产生的 JPA EntityManager 合作来构建事务。 + +#### Java 原生 API 事务(JTA) + +如果你没有使用以上所述的事务管理,或者是跨越了多个事务管理源(比如两个或者是多个不同的数据源),你就需要使用`JtaTransactionManager`: + +```xml + + + +``` + +`JtaTransactionManager` 将事务管理的责任委托给 `javax.transaction.UserTransaction` 和 `javax.transaction.TransactionManager` 对象,其中事务成功完成通过 `UserTransaction.commit()` 方法提交,事务失败通过 `UserTransaction.rollback()` 方法回滚。 + +#### ReactiveTransactionManager + +Spring 还为使用响应式类型或 Kotlin 协程的响应式应用程序提供了事务管理抽象。以下清单显示了 `org.springframework.transaction.ReactiveTransactionManager` 定义的事务策略: + +```java +public interface ReactiveTransactionManager extends TransactionManager { + + Mono getReactiveTransaction(TransactionDefinition definition) throws TransactionException; + + Mono commit(ReactiveTransaction status) throws TransactionException; + + Mono rollback(ReactiveTransaction status) throws TransactionException; +} +``` + +响应式事务管理器主要是一个 SPI,所以使用者可以以编程方式使用它。因为 `ReactiveTransactionManager` 是一个接口,所以可以根据需要轻松地 MOCK 或存根。 + +### TransactionDefinition + +`PlatformTransactionManager` 通过 `getTransaction(TransactionDefinition definition)` 方法来得到事务,这个方法里面的参数是 `TransactionDefinition` 类,这个类就定义了一些基本的事务属性。事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上。 + +`TransactionDefinition` 接口内容如下: + +```java +public interface TransactionDefinition { + int getPropagationBehavior(); // 返回事务的传播行为 + int getIsolationLevel(); // 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据 + int getTimeout(); // 返回事务必须在多少秒内完成 + boolean isReadOnly(); // 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的 +} +``` + +我们可以发现 `TransactionDefinition` 正好用来定义事务属性,下面详细介绍一下各个事务属性。 + +#### 传播行为 + +事务的传播行为(propagation behavior)是指:当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。Spring 定义了七种传播行为: + +| 传播行为 | 含义 | +| --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PROPAGATION_REQUIRED` | 表示当前方法必须运行在事务中。如果当前事务存在,方法将会在该事务中运行。否则,会启动一个新的事务 | +| `PROPAGATION_SUPPORTS` | 表示当前方法不需要事务上下文,但是如果存在当前事务的话,那么该方法会在这个事务中运行 | +| `PROPAGATION_MANDATORY` | 表示该方法必须在事务中运行,如果当前事务不存在,则会抛出一个异常 | +| `PROPAGATION_REQUIRED_NEW` | 表示当前方法必须运行在它自己的事务中。一个新的事务将被启动。如果存在当前事务,在该方法执行期间,当前事务会被挂起。如果使用 JTATransactionManager 的话,则需要访问 TransactionManager | +| `PROPAGATION_NOT_SUPPORTED` | 表示该方法不应该运行在事务中。如果存在当前事务,在该方法运行期间,当前事务将被挂起。如果使用 JTATransactionManager 的话,则需要访问 TransactionManager | +| `PROPAGATION_NEVER` | 表示当前方法不应该运行在事务上下文中。如果当前正有一个事务在运行,则会抛出异常 | +| `PROPAGATION_NESTED` | 表示如果当前已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立于当前事务进行单独地提交或回滚。如果当前事务不存在,那么其行为与 PROPAGATION_REQUIRED 一样。注意各厂商对这种传播行为的支持是有所差异的。可以参考资源管理器的文档来确认它们是否支持嵌套事务 | + +_注:以下具体讲解传播行为的内容参考自 Spring 事务机制详解_ + +1. PROPAGATION_REQUIRED 如果存在一个事务,则支持当前事务。如果没有事务则开启一个新的事务。 + +```java +// 事务属性 PROPAGATION_REQUIRED +methodA { + …… + methodB(); + …… +} +``` + +```java +// 事务属性 PROPAGATION_REQUIRED +methodB { + …… +} +``` + +使用 spring 声明式事务,spring 使用 AOP 来支持声明式事务,会根据事务属性,自动在方法调用之前决定是否开启一个事务,并在方法执行之后决定事务提交或回滚事务。 + +单独调用 methodB 方法: + +```java +main { + metodB(); +} +``` + +相当于 + +```java +Main { + Connection con=null; + try{ + con = getConnection(); + con.setAutoCommit(false); + + //方法调用 + methodB(); + + //提交事务 + con.commit(); + } Catch(RuntimeException ex) { + //回滚事务 + con.rollback(); + } finally { + //释放资源 + closeCon(); + } +} +``` + +Spring 保证在 methodB 方法中所有的调用都获得到一个相同的连接。在调用 methodB 时,没有一个存在的事务,所以获得一个新的连接,开启了一个新的事务。 +单独调用 MethodA 时,在 MethodA 内又会调用 MethodB. + +执行效果相当于: + +```java +main{ + Connection con = null; + try{ + con = getConnection(); + methodA(); + con.commit(); + } catch(RuntimeException ex) { + con.rollback(); + } finally { + closeCon(); + } +} +``` + +调用 MethodA 时,环境中没有事务,所以开启一个新的事务.当在 MethodA 中调用 MethodB 时,环境中已经有了一个事务,所以 methodB 就加入当前事务。 + +2. `PROPAGATION_SUPPORTS` 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行。但是对于事务同步的事务管理器,`PROPAGATION_SUPPORTS` 与不使用事务有少许不同。 + +```java +//事务属性 PROPAGATION_REQUIRED +methodA(){ + methodB(); +} + +//事务属性 PROPAGATION_SUPPORTS +methodB(){ + …… +} +``` + +单纯的调用 methodB 时,methodB 方法是非事务的执行的。当调用 methdA 时,methodB 则加入了 methodA 的事务中,事务地执行。 + +3. `PROPAGATION_MANDATORY` 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。 + +``` +//事务属性 PROPAGATION_REQUIRED +methodA(){ + methodB(); +} + +//事务属性 PROPAGATION_MANDATORY + methodB(){ + …… +} +``` + +当单独调用 methodB 时,因为当前没有一个活动的事务,则会抛出异常 throw new IllegalTransactionStateException(“Transaction propagation ‘mandatory’ but no existing transaction found”);当调用 methodA 时,methodB 则加入到 methodA 的事务中,事务地执行。 + +4. `PROPAGATION_REQUIRES_NEW` 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。 + +``` +//事务属性 PROPAGATION_REQUIRED +methodA(){ + doSomeThingA(); + methodB(); + doSomeThingB(); +} + +//事务属性 PROPAGATION_REQUIRES_NEW +methodB(){ + …… +} +``` + +调用 A 方法: + +``` +main(){ + methodA(); +} +``` + +相当于 + +``` +main(){ + TransactionManager tm = null; + try{ + //获得一个JTA事务管理器 + tm = getTransactionManager(); + tm.begin();//开启一个新的事务 + Transaction ts1 = tm.getTransaction(); + doSomeThing(); + tm.suspend();//挂起当前事务 + try{ + tm.begin();//重新开启第二个事务 + Transaction ts2 = tm.getTransaction(); + methodB(); + ts2.commit();//提交第二个事务 + } Catch(RunTimeException ex) { + ts2.rollback();//回滚第二个事务 + } finally { + //释放资源 + } + //methodB执行完后,恢复第一个事务 + tm.resume(ts1); + doSomeThingB(); + ts1.commit();//提交第一个事务 + } catch(RunTimeException ex) { + ts1.rollback();//回滚第一个事务 + } finally { + //释放资源 + } +} +``` + +在这里,我把 ts1 称为外层事务,ts2 称为内层事务。从上面的代码可以看出,ts2 与 ts1 是两个独立的事务,互不相干。Ts2 是否成功并不依赖于 ts1。如果 methodA 方法在调用 methodB 方法后的 doSomeThingB 方法失败了,而 methodB 方法所做的结果依然被提交。而除了 methodB 之外的其它代码导致的结果却被回滚了。使用 PROPAGATION_REQUIRES_NEW,需要使用 JtaTransactionManager 作为事务管理器。 + +5. `PROPAGATION_NOT_SUPPORTED` 总是非事务地执行,并挂起任何存在的事务。使用 PROPAGATION_NOT_SUPPORTED,也需要使用 JtaTransactionManager 作为事务管理器。(代码示例同上,可同理推出) +6. PROPAGATION_NEVER 总是非事务地执行,如果存在一个活动事务,则抛出异常。 +7. PROPAGATION_NESTED 如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务, 则按 TransactionDefinition.PROPAGATION_REQUIRED 属性执行。这是一个嵌套事务,使用 JDBC 3.0 驱动时,仅仅支持 DataSourceTransactionManager 作为事务管理器。需要 JDBC 驱动的 java.sql.Savepoint 类。有一些 JTA 的事务管理器实现可能也提供了同样的功能。使用 PROPAGATION_NESTED,还需要把 PlatformTransactionManager 的 nestedTransactionAllowed 属性设为 true;而 nestedTransactionAllowed 属性值默认为 false。 + +``` +//事务属性 PROPAGATION_REQUIRED +methodA(){ + doSomeThingA(); + methodB(); + doSomeThingB(); +} + +//事务属性 PROPAGATION_NESTED +methodB(){ + …… +} +``` + +如果单独调用 methodB 方法,则按 REQUIRED 属性执行。如果调用 methodA 方法,相当于下面的效果: + +``` +main(){ + Connection con = null; + Savepoint savepoint = null; + try{ + con = getConnection(); + con.setAutoCommit(false); + doSomeThingA(); + savepoint = con2.setSavepoint(); + try{ + methodB(); + } catch(RuntimeException ex) { + con.rollback(savepoint); + } finally { + //释放资源 + } + doSomeThingB(); + con.commit(); + } catch(RuntimeException ex) { + con.rollback(); + } finally { + //释放资源 + } +} +``` + +当 methodB 方法调用之前,调用 setSavepoint 方法,保存当前的状态到 savepoint。如果 methodB 方法调用失败,则恢复到之前保存的状态。但是需要注意的是,这时的事务并没有进行提交,如果后续的代码(doSomeThingB()方法)调用失败,则回滚包括 methodB 方法的所有操作。 + +嵌套事务一个非常重要的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。 + +PROPAGATION_NESTED 与 PROPAGATION_REQUIRES_NEW 的区别:它们非常类似,都像一个嵌套事务,如果不存在一个活动的事务,都会开启一个新的事务。使用 PROPAGATION_REQUIRES_NEW 时,内层事务与外层事务就像两个独立的事务一样,一旦内层事务进行了提交后,外层事务不能对其进行回滚。两个事务互不影响。两个事务不是一个真正的嵌套事务。同时它需要 JTA 事务管理器的支持。 + +使用 PROPAGATION_NESTED 时,外层事务的回滚可以引起内层事务的回滚。而内层事务的异常并不会导致外层事务的回滚,它是一个真正的嵌套事务。DataSourceTransactionManager 使用 savepoint 支持 PROPAGATION_NESTED 时,需要 JDBC 3.0 以上驱动及 1.4 以上的 JDK 版本支持。其它的 JTA TrasactionManager 实现可能有不同的支持方式。 + +PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。 + +另一方面, PROPAGATION_NESTED 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务. 潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 潜套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。 + +由此可见, PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back. + +PROPAGATION_REQUIRED 应该是我们首先的事务传播行为。它能够满足我们大多数的事务需求。 + +#### 隔离级别 + +事务的第二个维度就是隔离级别(isolation level)。隔离级别定义了一个事务可能受其他并发事务影响的程度。 + +1. 并发事务引起的问题 + +在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务。并发虽然是必须的,但可能会导致一下的问题。 + +- 脏读(Dirty reads)——脏读发生在一个事务读取了另一个事务改写但尚未提交的数据时。如果改写在稍后被回滚了,那么第一个事务获取的数据就是无效的。 +- 不可重复读(Nonrepeatable read)——不可重复读发生在一个事务执行相同的查询两次或两次以上,但是每次都得到不同的数据时。这通常是因为另一个并发事务在两次查询期间进行了更新。 +- 幻读(Phantom read)——幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录。 + +**不可重复读与幻读的区别** + +不可重复读的重点是修改: +同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 +例如:在事务 1 中,Mary 读取了自己的工资为 1000,操作并没有完成 + +``` + con1 = getConnection(); + select salary from employee empId ="Mary"; +``` + +在事务 2 中,这时财务人员修改了 Mary 的工资为 2000,并提交了事务. + +``` + con2 = getConnection(); + update employee set salary = 2000; + con2.commit(); +``` + +在事务 1 中,Mary 再次读取自己的工资时,工资变为了 2000 + +``` + //con1 + select salary from employee empId ="Mary"; +``` + +在一个事务中前后两次读取的结果并不一致,导致了不可重复读。 + +幻读的重点在于新增或者删除: +同样的条件, 第 1 次和第 2 次读出来的记录数不一样 +例如:目前工资为 1000 的员工有 10 人。事务 1,读取所有工资为 1000 的员工。 + +``` + con1 = getConnection(); + Select * from employee where salary =1000; +``` + +共读取 10 条记录 + +这时另一个事务向 employee 表插入了一条员工记录,工资也为 1000 + +``` + con2 = getConnection(); + Insert into employee(empId,salary) values("Lili",1000); + con2.commit(); +``` + +事务 1 再次读取所有工资为 1000 的员工 + +``` + //con1 + select * from employee where salary =1000; +``` + +共读取到了 11 条记录,这就产生了幻像读。 + +从总的结果来看, 似乎不可重复读和幻读都表现为两次读取的结果不一致。但如果你从控制的角度来看, 两者的区别就比较大。 +对于前者, 只需要锁住满足条件的记录。 +对于后者, 要锁住满足条件及其相近的记录。 + +2. 隔离级别 + +| 隔离级别 | 含义 | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | +| ISOLATION_DEFAULT | 使用后端数据库默认的隔离级别 | +| ISOLATION_READ_UNCOMMITTED | 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读 | +| ISOLATION_READ_COMMITTED | 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生 | +| ISOLATION_REPEATABLE_READ | 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生 | +| ISOLATION_SERIALIZABLE | 最高的隔离级别,完全服从 ACID 的隔离级别,确保阻止脏读、不可重复读以及幻读,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的 | + +#### 只读 + +事务的第三个特性是它是否为只读事务。如果事务只对后端的数据库进行该操作,数据库可以利用事务的只读特性来进行一些特定的优化。通过将事务设置为只读,你就可以给数据库一个机会,让它应用它认为合适的优化措施。 + +#### 事务超时 + +为了使应用程序很好地运行,事务不能运行太长的时间。因为事务可能涉及对后端数据库的锁定,所以长时间的事务会不必要的占用数据库资源。事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束。 + +#### 回滚规则 + +事务五边形的最后一个方面是一组规则,这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常时才会回滚,而在遇到检查型异常时不会回滚(这一行为与 EJB 的回滚行为是一致的) +但是你可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚。同样,你还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常。 + +### TransactionStatus + +`TransactionStatus` 接口为事务代码提供了一种简单的方式来控制事务执行和查询事务状态。这些概念应该很熟悉,因为它们对所有事务 API 都是通用的。以下清单显示了 `TransactionStatus` 接口: + +```java +public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable { + + @Override + boolean isNewTransaction(); + + boolean hasSavepoint(); + + @Override + void setRollbackOnly(); + + @Override + boolean isRollbackOnly(); + + void flush(); + + @Override + boolean isCompleted(); +} +``` + +可以发现这个接口描述的是一些处理事务提供简单的控制事务执行和查询事务状态的方法,在回滚或提交的时候需要应用对应的事务状态。 + +### TransactionTemplate + +Spring 提供了对编程式事务和声明式事务的支持。编程式事务允许用户在代码中精确定义事务的边界,而声明式事务(基于 AOP)有助于用户将操作与事务规则进行解耦。TransactionTemplate 就是用于支持编程式事务的核心 API。 + +采用 TransactionTemplate 和采用其他 Spring 模板,如 JdbcTempalte 和 HibernateTemplate 是一样的方法。它使用回调方法,把应用程序从处理取得和释放资源中解脱出来。如同其他模板,TransactionTemplate 是线程安全的。代码片段: + +```java + TransactionTemplate tt = new TransactionTemplate(); // 新建一个TransactionTemplate + Object result = tt.execute( + new TransactionCallback(){ + public Object doTransaction(TransactionStatus status){ + updateOperation(); + return resultOfUpdateOperation(); + } + }); // 执行execute方法进行事务管理 +``` + +使用 TransactionCallback()可以返回一个值。如果使用 TransactionCallbackWithoutResult 则没有返回值。 + +## 声明式事务管理 + +> 大多数 Spring 用户选择声明式事务管理。此选项对应用程序代码的影响最小,因此最符合非侵入式轻量级容器的理想。 + +Spring 框架的声明式事务管理是通过 Spring AOP 实现的。然而,由于事务方面代码随 Spring 发行版一起提供并且可以以样板方式使用,因此通常不必理解 AOP 概念即可有效地使用此代码。 + +Spring 框架的声明式事务管理类似于 EJB CMT,因为您可以指定事务行为(或缺少它)到单个方法级别。如有必要,您可以在事务上下文中进行 `setRollbackOnly()` 调用。两种类型的事务管理之间的区别是: + +- 与绑定到 JTA 的 EJB CMT 不同,Spring 框架的声明式事务管理适用于任何环境。通过调整配置文件,它可以使用 JDBC、JPA 或 Hibernate 处理 JTA 事务或本地事务。 +- 您可以将 Spring 声明式事务管理应用于任何类,而不仅仅是诸如 EJB 之类的特殊类。 +- Spring 提供声明性回滚规则,这是一个没有 EJB 等效功能的特性。提供了对回滚规则的编程和声明性支持。 +- Spring 允许您使用 AOP 自定义事务行为。例如,您可以在事务回滚的情况下插入自定义行为。您还可以添加任意 advice 以及事务性 advice。使用 EJB CMT,您无法影响容器的事务管理,除非使用 `setRollbackOnly()`。 +- Spring 不像高端应用服务器那样支持跨远程调用传播事务上下文。如果您需要此功能,我们建议您使用 EJB。但是,在使用这种特性之前要仔细考虑,因为通常情况下,不希望事务跨越远程调用。 + +回滚规则的概念很重要。它们让您指定哪些异常(和 throwable)应该导致自动回滚。您可以在配置中以声明方式指定它,而不是在 Java 代码中。因此,尽管您仍然可以在 TransactionStatus 对象上调用 setRollbackOnly() 来回滚当前事务,但通常您可以指定 MyApplicationException 必须始终导致回滚的规则。此选项的显着优势是业务对象不依赖于事务基础架构。例如,它们通常不需要导入 Spring 事务 API 或其他 Spring API。 + +尽管 EJB 容器默认行为会在系统异常(通常是运行时异常)上自动回滚事务,但 EJB CMT 不会在应用程序异常(即除 java.rmi.RemoteException 之外的检查异常)上自动回滚事务。虽然声明式事务管理的 Spring 默认行为遵循 EJB 约定(回滚仅在未经检查的异常上自动),但自定义此行为通常很有用。 + +### Spring 声明式事务管理的实现 + +关于 Spring 框架的声明式事务支持,最重要的概念是这种支持是通过 AOP 代理启用的,并且事务 advice 是由元数据驱动的(目前是基于 XML 或基于注释的)。 AOP 与事务元数据的结合产生了一个 AOP 代理,它使用 `TransactionInterceptor` 和适当的 `TransactionManager` 实现来驱动围绕方法调用的事务。 + +Spring 的 `TransactionInterceptor` 为命令式和响应式编程模型提供事务管理。拦截器通过检查方法返回类型来检测所需的事务管理风格。返回响应式类型的方法,例如 Publisher 或 Kotlin Flow(或它们的子类型)有资格进行响应式事务管理。包括 void 在内的所有其他返回类型都使用代码路径进行命令式事务管理。 + +事务管理风格会影响需要哪个事务管理器。命令式事务需要 `PlatformTransactionManager`,而响应式事务使用 `ReactiveTransactionManager` 实现。 + +> `@Transactional` 通常与 `PlatformTransactionManager` 管理的线程绑定事务一起使用,将事务公开给当前执行线程中的所有数据访问操作。注意:这不会传播到方法中新启动的线程。 +> +> 由 `ReactiveTransactionManager` 管理的反应式事务使用 Reactor 上下文而不是线程本地属性。因此,所有参与的数据访问操作都需要在同一个反应式管道中的同一个 Reactor 上下文中执行。 + +下图显示了在事务代理上调用方法的概念视图: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220927093737.png) + +### 声明式事务示例 + +考虑以下接口及其伴随的实现。此示例使用 Foo 和 Bar 类作为占位符,以便您可以专注于事务使用,而无需关注特定的域模型。就本示例而言,DefaultFooService 类在每个已实现方法的主体中抛出 `UnsupportedOperationException` 实例这一事实很好。该行为使您可以看到正在创建的事务,然后回滚以响应 `UnsupportedOperationException` 实例。 + +以下清单显示了 FooService 接口: + +```java +// the service interface that we want to make transactional + +package x.y.service; + +public interface FooService { + + Foo getFoo(String fooName); + + Foo getFoo(String fooName, String barName); + + void insertFoo(Foo foo); + + void updateFoo(Foo foo); + +} +``` + +以下示例显示了上述接口的实现: + +```java +package x.y.service; + +public class DefaultFooService implements FooService { + + @Override + public Foo getFoo(String fooName) { + // ... + } + + @Override + public Foo getFoo(String fooName, String barName) { + // ... + } + + @Override + public void insertFoo(Foo foo) { + // ... + } + + @Override + public void updateFoo(Foo foo) { + // ... + } +} +``` + +假设 FooService 接口的前两个方法 getFoo(String) 和 getFoo(String, String) 必须在具有只读语义的事务上下文中运行,并且其他方法 insertFoo(Foo) 和 updateFoo(Foo ),必须在具有读写语义的事务上下文中运行。以下配置将在接下来的几段中详细说明: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +检查前面的配置。它假定您要使服务对象 fooService bean 具有事务性。要应用的事务语义封装在 `` 定义中。`` 定义读作“所有以 get 开头的方法都将在只读事务的上下文中运行,所有其他方法都将以默认事务语义运行”。`` 标签的 `transaction-manager` 属性设置为将驱动事务的 TransactionManager bean 的名称(在本例中为 txManager bean)。 + +> 如果要连接的 TransactionManager 的 bean 名称具有名称 transactionManager,则可以省略事务 advice (tx:advice/) 中的 transaction-manager 属性。如果要连接的 TransactionManager bean 有任何其他名称,则必须显式使用 transaction-manager 属性,如前面的示例所示。 + +`` 定义确保由 `txAdvice` bean 定义的事务性建议在程序中的适当位置运行。首先,您定义一个切入点,该切入点与 `FooService` 接口 (fooServiceOperation) 中定义的任何操作的执行相匹配。然后,您使用一个 adviser 将切入点与 `txAdvice` 相关联。结果表明,在执行 fooServiceOperation 时,会运行 `txAdvice` 定义的建议。 + +一个常见的要求是使整个服务层具有事务性。最好的方法是更改切入点表达式以匹配服务层中的任何操作。以下示例显示了如何执行此操作: + +```xml + + + + +``` + +前面显示的配置用于围绕从 fooService bean 定义创建的对象创建事务代理。代理配置了事务 advice,以便在代理上调用适当的方法时,根据与该方法关联的事务配置,启动、暂停、标记为只读等事务。考虑以下测试驱动前面显示的配置的程序: + +```java +public final class Boot { + + public static void main(final String[] args) throws Exception { + ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml"); + FooService fooService = ctx.getBean(FooService.class); + fooService.insertFoo(new Foo()); + } +} +``` + +### 回滚一个声明性事务 + +Spring 框架中,触发事务回滚的推荐方式是在事务上下文的代码中抛出异常。Spring 事务框架会捕获任何未处理的异常,并确定是否将事务标记为回滚。 + +在其默认配置中,Spring 事务框架只会将存在运行时且未经检查异常的事务标记为回滚。也就是说,当抛出的异常是 `RuntimeException` 的实例或子类时。 (默认情况下,错误实例也会导致回滚)。从事务方法抛出的检查异常不会导致默认配置中的回滚。 + +您可以通过指定回滚规则,明确指定哪些异常类型将导致事务回滚。 + +> 回滚规则约定在抛出指定异常时是否应回滚事务,并且规则基于模式。模式可以是完全限定的类名或异常类型的完全限定类名的子字符串(必须是 `Throwable` 的子类),目前不支持通配符。例如,`javax.servlet.ServletException` 或 `ServletException` 的值将匹配 `javax.servlet.ServletException` 及其子类。 +> +> 回滚规则可以通过 `rollback-for` 和 `no-rollback-for` 属性在 XML 中配置,这允许将模式指定为字符串。使用 `@Transactional` 时,可以通过 `rollbackFor` / `noRollbackFor` 和`rollbackForClassName` / `noRollbackForClassName` 属性配置回滚规则,它们允许将模式分别指定为类引用或字符串。当异常类型被指定为类引用时,其完全限定名称将用作模式。因此,`@Transactional(rollbackFor = example.CustomException.class)` 等价于 `@Transactional(rollbackForClassName = 'example.CustomException')`。 + +以下 XML 片段演示了如何通过 `rollback-for` 属性提供异常模式来为已检查的、特定的 `Exception` 类型配置回滚: + +```xml + + + + + + +``` + +如果您不希望在抛出异常时回滚事务,您还可以指定“不回滚”规则。下面的例子告诉 Spring 事务框架,即使在面对未处理的 InstrumentNotFoundException 时也要提交伴随事务。 + +```xml + + + + + + +``` + +当 Spring Framework 事务框架捕获到异常,并检查配置的回滚规则以确定是否将事务标记为回滚时,由最重要的匹配规则决定。因此,在以下配置的情况下,除 `InstrumentNotFoundException` 之外的任何异常都会导致伴随事务的回滚。 + +```xml + + + + + +``` + +您还可以以编程方式指示所需的回滚。虽然很简单,但这个过程非常具有侵入性,并且将您的代码与 Spring Framework 的事务基础设施紧密耦合。以下示例显示如何以编程方式指示所需的回滚。 + +```java +public void resolvePosition() { + try { + // some business logic... + } catch (NoProductInStockException ex) { + // trigger rollback programmatically + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + } +} +``` + +如果可能的话,强烈建议您使用声明性方法进行回滚。如果您绝对需要,可以使用程序化回滚,但它的使用与实现干净的基于 POJO 的架构背道而驰。 + +### 为不同的 Bean 配置不同的事务语义 + +考虑您有许多服务层对象的场景,并且您希望对每个对象应用完全不同的事务配置。您可以通过定义具有不同 `` 元素和不同 `advice-ref` 属性值的切点来实现这一点。 + +作为一个比较点,首先假设您的所有服务层类都定义在根 x.y.service 包中。 要使作为该包(或子包)中定义的类的实例并且名称以 Service 结尾的所有 bean 都具有默认的事务配置,您可以编写以下内容: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +以下示例显示了如何使用完全不同的事务设置配置两个不同的 bean + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +### `` 配置 + +`` 的默认配置为: + +- 传播设置是 `REQUIRED` + +- 隔离级别为 `DEFAULT` + +- 事务是 read-write + +- 事务超时默认为底层事务系统的默认超时,如果不支持超时,则为无。 + +- 任何 `RuntimeException` 都会触发回滚,而任何已检查的 `Exception` 都不会 + +`` 配置属性 + +| 属性 | 是否必要 | 默认值 | 描述 | +| :---------------- | :------- | :--------- | :--------------------------------------------------------------------------- | +| `name` | Yes | | 与事务属性关联的方法名称。支持通配符,如:`get*`、`handle*`、`on*Event` | +| `propagation` | No | `REQUIRED` | 事务传播行为 | +| `isolation` | No | `DEFAULT` | 事务隔离级别。仅适用于 `REQUIRED` 或 `REQUIRES_NEW` 的传播设置。 | +| `timeout` | No | -1 | 事务超时时间(单位:秒)。仅适用于 `REQUIRED` 或 `REQUIRES_NEW` 的传播设置。 | +| `read-only` | No | false | read-write 或 read-only 事务。 | +| `rollback-for` | No | | 触发回滚的 `Exception` 实例列表(通过逗号分隔)。 | +| `no-rollback-for` | No | | 不触发回滚的 `Exception` 实例列表(通过逗号分隔)。 | + +### 使用 `@Transactional` 注解 + +除了基于 XML 的声明式事务配置方法之外,您还可以使用基于注解的方法。 + +下面是一个使用 `@Transactional` 注解的示例: + +```java +@Transactional +public class DefaultFooService implements FooService { + + @Override + public Foo getFoo(String fooName) { + // ... + } + + @Override + public Foo getFoo(String fooName, String barName) { + // ... + } + + @Override + public void insertFoo(Foo foo) { + // ... + } + + @Override + public void updateFoo(Foo foo) { + // ... + } +} +``` + +如上所述在类级别使用,`@Transactional` 注解表明声明类(及其子类)的所有方法都使用默认事务配置。 或者,可以单独为每个方法指定注解。请注意,类级别的注解不适用于类层次结构中的祖先类; 在这种情况下,继承的方法需要在本地重新声明才能参与子类级别的注解。 + +当上面的 POJO 类在 Spring 上下文中定义为 bean 时,您可以通过 `@Configuration` 类中的 `@EnableTransactionManagement` 注解使 bean 实例具有事务性。 + +在 XML 配置中, `` 标签提供了类似的便利: + +```xml + + + + + + + + + + + + + + + + + + + +``` + +#### `@Transactional` 配置 + +| Property | Type | Description | +| :------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------- | +| [value](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-multiple-tx-mgrs-with-attransactional) | `String` | Optional qualifier that specifies the transaction manager to be used. | +| `transactionManager` | `String` | Alias for `value`. | +| `label` | Array of `String` labels to add an expressive description to the transaction. | Labels may be evaluated by transaction managers to associate implementation-specific behavior with the actual transaction. | +| [propagation](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#tx-propagation) | `enum`: `Propagation` | Optional propagation setting. | +| `isolation` | `enum`: `Isolation` | Optional isolation level. Applies only to propagation values of `REQUIRED` or `REQUIRES_NEW`. | +| `timeout` | `int` (in seconds of granularity) | Optional transaction timeout. Applies only to propagation values of `REQUIRED` or `REQUIRES_NEW`. | +| `timeoutString` | `String` (in seconds of granularity) | Alternative for specifying the `timeout` in seconds as a `String` value — for example, as a placeholder. | +| `readOnly` | `boolean` | Read-write versus read-only transaction. Only applicable to values of `REQUIRED` or `REQUIRES_NEW`. | +| `rollbackFor` | Array of `Class` objects, which must be derived from `Throwable.` | Optional array of exception types that must cause rollback. | +| `rollbackForClassName` | Array of exception name patterns. | Optional array of exception name patterns that must cause rollback. | +| `noRollbackFor` | Array of `Class` objects, which must be derived from `Throwable.` | Optional array of exception types that must not cause rollback. | +| `noRollbackForClassName` | Array of exception name patterns. | Optional array of exception name patterns that must not cause rollback. | + +#### 多事务管理器场景下使用 `@Transactional` + +某些情况下,应用程序中可能需要接入多个数据源,相应的,也需要多个独立的事务管理器。使用者可以使用 `@Transactional` 注释的 value 或 `transactionManager` 属性来选择性地指定要使用的 `TransactionManager` 的标识。这可以是 bean 名称或事务管理器 bean 的限定符值。 + +```java +public class TransactionalService { + + @Transactional("order") + public void setSomething(String name) { ... } + + @Transactional("account") + public void doSomething() { ... } + + @Transactional("reactive-account") + public Mono doSomethingReactive() { ... } +} +``` + +下面展示如何定义 `TransactionManager`: + +```xml + + + + ... + + + + + ... + + + + + ... + + +``` + +在这种情况下,`TransactionalService` 上的各个方法在单独的事务管理器下运行,由 order、account 和 reactive-account 限定符区分。 如果没有找到明确指定的 `TransactionManager` bean,则仍使用默认的 `` 目标 bean 名称。 + +#### 自定义组合注解 + +如果您发现在许多不同的方法上重复使用 `@Transactional` 相同的属性,可以使用 Spring 的元注解自定义组合注解。 + +```java +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Transactional(transactionManager = "order", label = "causal-consistency") +public @interface OrderTx { +} + +@Target({ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Transactional(transactionManager = "account", label = "retryable") +public @interface AccountTx { +} +``` + +使用示例: + +```java +public class TransactionalService { + + @OrderTx + public void setSomething(String name) { + // ... + } + + @AccountTx + public void doSomething() { + // ... + } +} +``` + +在上面的示例中,我们使用语法来定义事务管理器限定符和事务标签,但我们也可以包括传播行为、回滚规则、超时和其他特性。 + +#### 事务传播 + +在 Spring 管理的事务中,请注意物理事务和逻辑事务之间的差异,以及传播设置如何应用于这种差异。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220928114544.png) + +`PROPAGATION_REQUIRED` 强制执行物理事务,如果尚不存在事务,则在当前范围的本地执行或参与更大范围定义的现有“外部”事务。 这是同一线程内的常见调用堆栈安排中的一个很好的默认设置(例如,委托给多个存储库方法的服务外观,其中所有底层资源都必须参与服务级事务)。 + +当传播设置为 PROPAGATION_REQUIRED 时,将为应用该设置的每个方法创建一个逻辑事务范围。每个这样的逻辑事务范围可以单独确定仅回滚状态,外部事务范围在逻辑上独立于内部事务范围。在标准 PROPAGATION_REQUIRED 行为的情况下,所有这些范围都映射到同一个物理事务。因此,在内部事务范围内设置的仅回滚标记确实会影响外部事务实际提交的机会。 + +但是,在内部事务范围设置了仅回滚标记的情况下,外部事务尚未决定回滚本身,因此回滚(由内部事务范围静默触发)是意外的。此时会引发相应的 `UnexpectedRollbackException`。这是预期的行为,因此事务的调用者永远不会被误导以为执行了提交,而实际上并没有执行。因此,如果内部事务(外部调用者不知道)默默地将事务标记为仅回滚,外部调用者仍会调用提交。外部调用者需要接收 `UnexpectedRollbackException` 以清楚地指示执行了回滚。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220928115243.png) + +PROPAGATION_REQUIRES_NEW 与 PROPAGATION_REQUIRED 相比,始终为每个受影响的事务范围使用独立的物理事务,从不参与外部范围的现有事务。 在这种安排下,底层资源事务是不同的,因此可以独立提交或回滚,外部事务不受内部事务回滚状态的影响,内部事务的锁在完成后立即释放。 这样一个独立的内部事务也可以声明自己的隔离级别、超时和只读设置,而不是继承外部事务的特性。 + +## JDBC 异常抽象 + +Spring 会将数据操作的异常转换为 `DataAccessException`。 + +Spring 是怎么认识那些错误码的 + +通过 SQLErrorCodeSQLExceptionTranslator 解析错误码 + +ErrorCode 定义(sql-error-codes.xml 文件) + +## Spring 事务最佳实践 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200805171418.png) + +### Spring 事务未生效 + +使用 `@Transactional` 注解开启声明式事务时, 最容易忽略的问题是,很可能事务并没有生效。 + +`@Transactional` 生效原则: + +#### @Transactional 方法必须是 public + +原则一:除非特殊配置(比如使用 AspectJ 静态织入实现 AOP),否则**只有定义在 `public` 方法上的 `@Transactional` 才能生效**。原因是,Spring 默认通过动态代理的方式实现 AOP,对目标方法进行增强,private 方法无法代理到,Spring 自然也无法动态增强事务处理逻辑。 + +【示例】错误使用 `@Transactional` 案例一 + +```java + @Transactional + void createUserPrivate(UserEntity entity) { + userRepository.save(entity); + if (entity.getName().contains("test")) { throw new RuntimeException("invalid username!"); } + } + + //私有方法 + public int createUserWrong1(String name) { + try { + this.createUserPrivate(new UserEntity(name)); + } catch (Exception ex) { + log.error("create user failed because {}", ex.getMessage()); + } + return userRepository.findByName(name).size(); + } +``` + +当传入名为 test 的用户实体,会抛出异常,但 `@Transactional` 未生效,不会触发回滚。 + +#### 必须通过 Spring 注入的 Bean 进行调用 + +原则二:**必须通过代理过的类从外部调用目标方法才能生效**。 + +【示例】错误使用 `@Transactional` 案例二 + +```java + //自调用 + public int createUserWrong2(String name) { + try { + this.createUserPublic(new UserEntity(name)); + } catch (Exception ex) { + log.error("create user failed because {}", ex.getMessage()); + } + return userRepository.findByName(name).size(); + } + + //可以传播出异常 + @Transactional + public void createUserPublic(UserEntity entity) { + userRepository.save(entity); + if (entity.getName().contains("test")) { throw new RuntimeException("invalid username!"); } + } +``` + +当传入名为 test 的用户实体,会抛出异常,但 `@Transactional` 未生效,不会触发回滚。 + +说明:Spring 通过 AOP 技术对方法进行字节码增强,要调用增强过的方法必然是调用代理后的对象。 + +### 事务虽然生效但未回滚 + +通过 AOP 实现事务处理可以理解为,使用 `try…catch…` 来包裹标记了 `@Transactional` 注解的方法,当方法出现了异常并且满足**一定条件**的时候,在 `catch` 里面我们可以设置事务回滚,没有异常则直接提交事务。 + +“一定条件”,主要包括两点: + +第一,只有异常传播出了标记了 @Transactional 注解的方法,事务才能回滚。在 Spring 的 TransactionAspectSupport 里有个 invokeWithinTransaction 方法,里面就是处理事务的逻辑。 + +第二,默认情况下,**出现 RuntimeException(非受检异常)或 Error 的时候,Spring 才会回滚事务**。 + +```java +@Service +@Slf4j +public class UserService { + + @Autowired + private UserRepository userRepository; + + //异常无法传播出方法,导致事务无法回滚 + @Transactional + public void createUserWrong1(String name) { + try { + userRepository.save(new UserEntity(name)); + throw new RuntimeException("error"); + } catch (Exception ex) { + log.error("create user failed", ex); + } + } + + //即使出了受检异常也无法让事务回滚 + @Transactional + public void createUserWrong2(String name) throws IOException { + userRepository.save(new UserEntity(name)); + otherTask(); + } + + //因为文件不存在,一定会抛出一个IOException + private void otherTask() throws IOException { + Files.readAllLines(Paths.get("file-that-not-exist")); + } + +} +``` + +在 createUserWrong1 方法中会抛出一个 RuntimeException,但由于方法内 catch 了所有异常,异常无法从方法传播出去,事务自然无法回滚。 + +在 createUserWrong2 方法中,注册用户的同时会有一次 otherTask 文件读取操作,如果文件读取失败,我们希望用户注册的数据库操作回滚。虽然这里没有捕获异常,但因为 otherTask 方法抛出的是受检异常,createUserWrong2 传播出去的也是受检异常,事务同样不会回滚。 + +【解决方案一】如果你希望自己捕获异常进行处理的话,也没关系,**可以手动设置 `TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();` 让当前事务处于回滚状态**: + +```java +@Transactional +public void createUserRight1(String name) { + try { + userRepository.save(new UserEntity(name)); + throw new RuntimeException("error"); + } catch (Exception ex) { + log.error("create user failed", ex); + TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); + } +} +``` + +【解决方案二】在注解中声明 `@Transactional(rollbackFor = Exception.class)`,期望遇到所有的 Exception 都回滚事务(来突破默认不回滚受检异常的限制): + +```java +@Transactional(rollbackFor = Exception.class) +public void createUserRight2(String name) throws IOException { + userRepository.save(new UserEntity(name)); + otherTask(); +} +``` + +### 细化事务传播方式 + +如果方法涉及多次数据库操作,并希望将它们作为独立的事务进行提交或回滚,那么 +我们需要考虑进一步细化配置事务传播方式,也就是 `@Transactional` 注解的 `Propagation` 属性。 + +```java +/** + * {@link Propagation#REQUIRES_NEW} 表示执行到这个方法时需要开启新的事务,并挂起当前事务 + */ +@Transactional(propagation = Propagation.REQUIRES_NEW) +public void createSubUserWithExceptionRight(UserEntity entity) { + log.info("createSubUserWithExceptionRight start"); + userRepository.save(entity); + throw new RuntimeException("invalid status"); +} +``` + +## 参考资料 + +- [Spring 官网](https://spring.io/) +- [Spring 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html) +- [《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) \ No newline at end of file diff --git "a/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/04.Spring\344\271\213JPA.md" "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/04.Spring\344\271\213JPA.md" new file mode 100644 index 00000000..8d362789 --- /dev/null +++ "b/docs/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/04.Spring\344\271\213JPA.md" @@ -0,0 +1,609 @@ +--- +title: Spring 之 JPA +date: 2019-02-18 14:33:55 +order: 04 +categories: + - Java + - 框架 + - Spring + - Spring数据 +tags: + - Java + - 框架 + - Spring + - SpringBoot + - JPA +permalink: /pages/a03d7b/ +--- + +# Spring 之 JPA + +JPA 为对象关系映射提供了一种基于 POJO 的持久化模型。 + +- 简化数据持久化代码的开发 +- 为 Java 社区屏蔽不同持久化 API 的差异 + +## 快速入门 + +(1)在 pom.xml 中引入依赖 + +```xml + + org.springframework.boot + spring-boot-starter-data-jpa + +``` + +(2)设置启动注解 + +```java +// 【可选】指定扫描的 Entity 目录,如果不指定,会扫描全部目录 +@EntityScan("io.github.dunwu.springboot.data.jpa") +// 【可选】指定扫描的 Repository 目录,如果不指定,会扫描全部目录 +@EnableJpaRepositories(basePackages = {"io.github.dunwu.springboot.data.jpa"}) +// 【可选】开启 JPA auditing 能力,可以自动赋值一些字段,比如创建时间、最后一次修改时间等等 +@EnableJpaAuditing +@SpringBootApplication +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} +``` + +(3)配置 + +```properties +# 数据库连接 +spring.datasource.url = jdbc:mysql://localhost:3306/spring_tutorial?serverTimezone=UTC&useUnicode=true&characterEncoding=utf8 +spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver +spring.datasource.username = root +spring.datasource.password = root +# 是否打印 JPA SQL 日志 +spring.jpa.show-sql = true +# Hibernate的DDL策略 +spring.jpa.hibernate.ddl-auto = create-drop +``` + +(4)定义实体 + +```java +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.Objects; +import javax.persistence.*; + +@Entity +@Data +@ToString +@NoArgsConstructor +@AllArgsConstructor +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private Long id; + + @Column(unique = true) + private String name; + + private Integer age; + + private String address; + + private String email; + + public User(String name, Integer age, String address, String email) { + this.name = name; + this.age = age; + this.address = address; + this.email = email; + } + + @Override + public int hashCode() { + return Objects.hash(id, name); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof User)) { + return false; + } + + User user = (User) o; + + if (id != null && id.equals(user.id)) { + return true; + } + + return name.equals(user.name); + } + +} +``` + +(5)定义 Repository + +```java + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.data.rest.core.annotation.RepositoryRestResource; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.bind.annotation.PathVariable; + +import java.util.List; + +@RepositoryRestResource(collectionResourceRel = "user", path = "user") +public interface UserRepository extends JpaRepository { + + User findUserById(@PathVariable("id") Long id); + + /** + * 根据用户名查找用户 + *

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

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

    基本的 CRUD 应用

    +

    数据来源于后台系统

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

    User Information

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

    + +

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

    + +

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

    + + logo + +

    + +

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

    + +

    JavaTutorial

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

    -**建项原因** + + star + -系统化的学习知识,有利于将知识碎片拼凑起来,融会贯通。这对于掌握任何一门技术,都是非常必要的。所以,作为一名 Java 工程师,应该系统化管理自己的 Java 技术栈知识。 + + fork + -**范围** + + build + -学海无涯,学的越多,越感到个人精力有限,难以对所有技术都面面俱到。所以,根据自己的工作领域,有侧重点的去掌握知识才是明智的。 + + code style + -由于本人人从事 Java Web 领域的开发,所以本项目主要是整理 Java Web 及相关领域的知识点。 +

    -## 目录 +> ☕ **java-tutorial** 是一个 Java 教程,汇集一个老司机在 Java 领域的十年积累。 +> +> - 🔁 项目同步维护:[Github](https://github.com/dunwu/java-tutorial/) | [Gitee](https://gitee.com/turnon/java-tutorial/) +> - 📖 电子书阅读:[Github Pages](https://dunwu.github.io/java-tutorial/) | [Gitee Pages](https://turnon.gitee.io/java-tutorial/) +> +> 说明: +> +> - 下面的内容清单中,凡是有 📚 标记的技术,都已整理成详细的教程。 +> - 部分技术因为可以应用于不同领域,所以可能会同时出现在不同的类别下。 -### [JavaSE](https://github.com/dunwu/javase-notes) +## 📖 内容 -### [JavaEE](https://github.com/dunwu/javaee-notes) +### JavaSE -### Java 框架 +> 📚 [javacore](https://dunwu.github.io/javacore/) 是一个 Java 核心技术教程。内容包含:Java 基础特性、Java 高级特性、Java 并发、JVM、Java IO 等。 + +### JavaEE + +#### JavaWeb + +- [JavaWeb 面经](01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb面经.md) +- [JavaWeb 之 Servlet 指南](01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb之Servlet指南.md) +- [JavaWeb 之 Jsp 指南](01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb之Jsp指南.md) +- [JavaWeb 之 Filter 和 Listener](01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb之Filter和Listener.md) +- [JavaWeb 之 Cookie 和 Session](01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb之Cookie和Session.md) + +#### Java 服务器 + +> Tomcat 和 Jetty 都是 Java 比较流行的轻量级服务器。 +> +> Nginx 是目前最流行的反向代理服务器,也常用于负载均衡。 + +- [Tomcat 快速入门](01.Java/02.JavaEE/02.服务器/01.Tomcat/01.Tomcat快速入门.md) +- [Tomcat 连接器](01.Java/02.JavaEE/02.服务器/01.Tomcat/02.Tomcat连接器.md) +- [Tomcat 容器](01.Java/02.JavaEE/02.服务器/01.Tomcat/03.Tomcat容器.md) +- [Tomcat 优化](01.Java/02.JavaEE/02.服务器/01.Tomcat/04.Tomcat优化.md) +- [Tomcat 和 Jetty](01.Java/02.JavaEE/02.服务器/01.Tomcat/05.Tomcat和Jetty.md) +- [Jetty](01.Java/02.JavaEE/02.服务器/02.Jetty.md) + +### Java 软件 + +#### Java 构建 + +> Java 项目需要通过 [**构建工具**](01.Java/11.软件/01.构建) 来管理项目依赖,完成编译、打包、发布、生成 JavaDoc 等任务。 +> +> - 目前最主流的构建工具是 Maven,它的功能非常强大。 +> - Gradle 号称是要替代 Maven 等构件工具,它的版本管理确实简洁,但是需要学习 Groovy,学习成本比 Maven 高。 +> - Ant 功能比 Maven 和 Gradle 要弱,现代 Java 项目基本不用了,但也有一些传统的 Java 项目还在使用。 + +- [Maven](01.Java/11.软件/01.构建/01.Maven) 📚 + - [Maven 快速入门](01.Java/11.软件/01.构建/01.Maven/01.Maven快速入门.md) + - [Maven 教程之 pom.xml 详解](01.Java/11.软件/01.构建/01.Maven/02.Maven教程之pom.xml详解.md) + - [Maven 教程之 settings.xml 详解](01.Java/11.软件/01.构建/01.Maven/03.Maven教程之settings.xml详解.md) + - [Maven 实战问题和最佳实践](01.Java/11.软件/01.构建/01.Maven/04.Maven实战问题和最佳实践.md) + - [Maven 教程之发布 jar 到私服或中央仓库](01.Java/11.软件/01.构建/01.Maven/05.Maven教程之发布jar到私服或中央仓库.md) + - [Maven 插件之代码检查](01.Java/11.软件/01.构建/01.Maven/06.Maven插件之代码检查.md) +- [Ant 简易教程](01.Java/11.软件/01.构建/02.Ant.md) + +#### Java IDE + +> 自从有了 [**IDE**](01.Java/11.软件/02.IDE),写代码从此就告别了刀耕火种的蛮荒时代。 +> +> - [Eclipse](01.Java/11.软件/02.IDE/02.Eclipse.md) 是久负盛名的开源 Java IDE,我的学生时代一直使用它写 Java。 +> - 曾经抗拒从转 [Intellij Idea](01.Java/11.软件/02.IDE/01.Intellij.md) ,但后来发现真香,不得不说,确实是目前最优秀的 Java IDE。 +> - 你可以在 [vscode](01.Java/11.软件/02.IDE/03.VsCode.md) 中写各种语言,只要安装相应插件即可。如果你的项目中使用了很多种编程语言,又懒得在多个 IDE 之间切换,那么就用 vscode 来一网打尽吧。 -* [Spring](https://github.com/dunwu/spring-notes) +- [Intellij Idea](01.Java/11.软件/02.IDE/01.Intellij.md) +- [Eclipse](01.Java/11.软件/02.IDE/02.Eclipse.md) +- [vscode](01.Java/11.软件/02.IDE/03.VsCode.md) -### Java 库 +#### Java 监控诊断 -* [Java 库](docs/javalib/README.md) - * [ActiveMQ 使用小结](docs/javalib/activemq.md) - * [Dozer 使用小结](docs/javalib/dozer.md) - * [细说 Java 主流日志工具库](docs/javalib/java-log.md) - * [JavaMail 使用小结](docs/javalib/javamail.md) - * [jsoup 使用小结](docs/javalib/jsoup.md) - * [JUnit 使用小结](docs/javalib/junit.md) - * [Lombok 使用小结](docs/javalib/lombok.md) - * [Thumbnailator 使用小结](docs/javalib/thumbnailator.md) - * [ZXing 使用小结](docs/javalib/zxing.md) +> [监控/诊断](01.Java/11.软件/03.监控诊断) 工具主要用于 Java 应用的运维。通过采集、分析、存储、可视化应用的有效数据,帮助开发者、使用者快速定位问题,找到性能瓶颈。 + +- [监控工具对比](01.Java/11.软件/03.监控诊断/01.监控工具对比.md) +- [CAT](01.Java/11.软件/03.监控诊断/02.CAT.md) +- [Zipkin](01.Java/11.软件/03.监控诊断/03.Zipkin.md) +- [SkyWalking](01.Java/11.软件/03.监控诊断/04.Skywalking.md) +- [Arthas](01.Java/11.软件/03.监控诊断/05.Arthas.md) ### Java 工具 -* [构建工具](docs/javatool/build/README.md) - * [Maven 快速指南(一)](docs/javatool/build/maven/maven-quickstart-01.md) - * [Maven 快速指南(二)](docs/javatool/build/maven/maven-quickstart-02.md) - * [Maven 之 pom.xml 详解(一)](docs/javatool/build/maven/maven-pom-01.md) - * [Maven 之 pom.xml 详解(二)](docs/javatool/build/maven/maven-pom-02.md) - * [Maven 之 pom.xml 详解(三)](docs/javatool/build/maven/maven-pom-03.md) - * [Maven 之 settings.xml 详解](docs/javatool/build/maven/maven-settings-config.md) - * [发布项目到中央仓库](docs/javatool/build/maven/maven-deploy.md) - * [Maven 排错](docs/javatool/build/maven/maven-faq.md) - * [Ant 简易教程](docs/javatool/build/ant.md) -* [Elastic](docs/javatool/elastic/README.md) - * [Elastic 快速指南](docs/javatool/elastic/elastic-quickstart.md) - * [Elastic 学习资源](docs/javatool/elastic/resources.md) -* [Java IDE](docs/javatool/ide/README.md) - * [Intellij IDEA 使用小结](docs/javatool/ide/intellij.md) - * [Eclipse 使用小结](docs/javatool/ide/eclipse.md) -* [Java 服务器](docs/javatool/server/README.md) - * [Jetty 使用小结](docs/javatool/server/jetty.md) - -### 技术扩展 - -* [数据库](docs/extend/database.md) -* [数据结构和算法](docs/extend/algorithm.md) -* [Linux](docs/extend/linux.md) - -### [附录](docs/appendix/README.md) - -* [Java 学习资源](docs/spring/appendix/resources.md) +#### Java IO + +- [JSON 序列化](01.Java/12.工具/01.IO/01.JSON序列化.md) - [fastjson](https://github.com/alibaba/fastjson)、[Jackson](https://github.com/FasterXML/jackson)、[Gson](https://github.com/google/gson) +- [二进制序列化](01.Java/12.工具/01.IO/02.二进制序列化.md) - [Protobuf](https://developers.google.com/protocol-buffers)、[Thrift](https://thrift.apache.org/)、[Hessian](http://hessian.caucho.com/)、[Kryo](https://github.com/EsotericSoftware/kryo)、[FST](https://github.com/RuedigerMoeller/fast-serialization) + +#### JavaBean 工具 + +- [Lombok](01.Java/12.工具/02.JavaBean/01.Lombok.md) +- [Dozer](01.Java/12.工具/02.JavaBean/02.Dozer.md) + +#### Java 模板引擎 + +- [Freemark](01.Java/12.工具/03.模板引擎/01.Freemark.md) +- [Velocity](01.Java/12.工具/03.模板引擎/02.Thymeleaf.md) +- [Thymeleaf](01.Java/12.工具/03.模板引擎/03.Velocity.md) + +#### Java 测试工具 + +- [Junit](01.Java/12.工具/04.测试/01.Junit.md) +- [Mockito](01.Java/12.工具/04.测试/02.Mockito.md) +- [Jmeter](01.Java/12.工具/04.测试/03.Jmeter.md) +- [JMH](01.Java/12.工具/04.测试/04.JMH.md) + +#### 其他 + +- [Java 日志](01.Java/12.工具/99.其他/01.Java日志.md) +- [Java 工具包](01.Java/12.工具/99.其他/02.Java工具包.md) +- [Reflections](01.Java/12.工具/99.其他/03.Reflections.md) +- [JavaMail](01.Java/12.工具/99.其他/04.JavaMail.md) +- [Jsoup](01.Java/12.工具/99.其他/05.Jsoup.md) +- [Thumbnailator](01.Java/12.工具/99.其他/06.Thumbnailator.md) +- [Zxing](01.Java/12.工具/99.其他/07.Zxing.md) + +### Java 框架 + +#### ORM + +- [Mybatis 快速入门](01.Java/13.框架/11.ORM/01.Mybatis快速入门.md) +- [Mybatis 原理](01.Java/13.框架/11.ORM/02.Mybatis原理.md) + +#### Spring + +📚 [spring-tutorial](https://dunwu.github.io/spring-tutorial/) 是一个 Spring 实战教程。 + +#### Spring Boot + +📚 [Spring Boot 教程](https://dunwu.github.io/spring-boot-tutorial/) 是一个 Spring Boot 实战教程。 + +#### 安全 + +> Java 领域比较流行的安全框架就是 shiro 和 spring-security。 +> +> shiro 更为简单、轻便,容易理解,能满足大多数基本安全场景下的需要。 +> +> spring-security 功能更丰富,也比 shiro 更复杂。值得一提的是由于 spring-security 是 spring 团队开发,所以集成 spring 和 spring-boot 框架更容易。 + +- [Shiro](01.Java/13.框架/12.安全/01.Shiro.md) +- [SpringSecurity](01.Java/13.框架/12.安全/02.SpringSecurity.md) + +#### IO + +- [Shiro](01.Java/13.框架/13.IO/01.Netty.md) + +### Java 中间件 + +#### 缓存 + +> 缓存可以说是优化系统性能的第一手段,在各种技术中都会有缓存的应用。 +> +> 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://dunwu.github.io/design/distributed/分布式缓存.html),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 + +- [Java 缓存中间件](01.Java/14.中间件/02.缓存/02.Java缓存中间件.md) +- [Ehcache 快速入门](01.Java/14.中间件/02.缓存/04.Ehcache.md) +- [Java 进程内缓存](01.Java/14.中间件/02.缓存/05.Java进程内缓存.md) +- [Http 缓存](01.Java/14.中间件/02.缓存/06.Http缓存.md) + +#### 流量控制 + +- [Hystrix](01.Java/14.中间件/03.流量控制/01.Hystrix.md) + +### [大数据](https://dunwu.github.io/bigdata-tutorial) + +> 大数据技术点以归档在:[bigdata-tutorial](https://dunwu.github.io/bigdata-tutorial) + +- [Hdfs](https://dunwu.github.io/bigdata-tutorial/hdfs) 📚 +- [Hbase](https://dunwu.github.io/bigdata-tutorial/hbase) 📚 +- [Hive](https://dunwu.github.io/bigdata-tutorial/hive) 📚 +- [MapReduce](https://dunwu.github.io/bigdata-tutorial/mapreduce) +- [Yarn](https://dunwu.github.io/bigdata-tutorial/yarn) +- [ZooKeeper](https://dunwu.github.io/bigdata-tutorial/zookeeper) 📚 +- [Kafka](https://dunwu.github.io/bigdata-tutorial/kafka) 📚 +- Spark +- Storm +- [Flink](https://dunwu.github.io/bigdata-tutorial/tree/master/docs/flink) + +## 📚 资料 + +- Java 经典书籍 + - [《Effective Java 中文版》](https://item.jd.com/12507084.html) - 本书介绍了在 Java 编程中 78 条极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。同推荐《重构 : 改善既有代码的设计》、《代码整洁之道》、《代码大全》,有一定的内容重叠。 + - [《Java 并发编程实战》](https://item.jd.com/10922250.html) - 本书深入浅出地介绍了 Java 线程和并发,是一本完美的 Java 并发参考手册。 + - [《深入理解 Java 虚拟机》](https://item.jd.com/11252778.html) - 不去了解 JVM 的工程师,和咸鱼有什么区 + - [《Maven 实战》](https://item.jd.com/10476794.html) - 国内最权威的 Maven 专家的力作,唯一一本哦! +- 其他领域书籍 + - [《Redis 设计与实现》](https://item.jd.com/11486101.html) - 系统而全面地描述了 Redis 内部运行机制。图示丰富,描述清晰,并给出大量参考信息,是 NoSQL 数据库开发人员案头必备。 + - [《鸟哥的 Linux 私房菜 (基础学习篇)》](https://item.jd.com/12443890.html) - 本书是最具知名度的 Linux 入门书《鸟哥的 Linux 私房菜基础学习篇》的最新版,全面而详细地介绍了 Linux 操作系统。内容非常全面,建议挑选和自己实际工作相关度较高的,其他部分有需要再阅读。 + - [《Head First 设计模式》](https://item.jd.com/10100236.html) - 《Head First 设计模式》(中文版)共有 14 章,每章都介绍了几个设计模式,完整地涵盖了四人组版本全部 23 个设计模式。 + - [《HTTP 权威指南》](https://item.jd.com/11056556.html) - 本书尝试着将 HTTP 中一些互相关联且常被误解的规则梳理清楚,并编写了一系列基于各种主题的章节,对 HTTP 各方面的特性进行了介绍。纵观全书,对 HTTP“为什么”这样做进行了详细的解释,而不仅仅停留在它是“怎么做”的。 + - [《TCP/IP 详解 系列》](https://item.jd.com/11966296.html) - 完整而详细的 TCP/IP 协议指南。针对任何希望理解 TCP/IP 协议是如何实现的读者设计。 + - [《剑指 Offer:名企面试官精讲典型编程题》](https://item.jd.com/12163054.html) - 剖析了 80 个典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这 5 个面试要点。 + +## 🚪 传送 + +◾ 🏠 [JAVA-TUTORIAL 首页](https://github.com/dunwu/java-tutorial) ◾ 🎯 [我的博客](https://github.com/dunwu/blog) ◾ + +> 你可能会感兴趣: + +- [Java 教程](https://github.com/dunwu/java-tutorial) 📚 +- [JavaCore 教程](https://dunwu.github.io/javacore/) 📚 +- [Spring 教程](https://dunwu.github.io/spring-tutorial/) 📚 +- [Spring Boot 教程](https://dunwu.github.io/spring-boot-tutorial/) 📚 +- [数据库教程](https://dunwu.github.io/db-tutorial/) 📚 +- [数据结构和算法教程](https://dunwu.github.io/algorithm-tutorial/) 📚 +- [Linux 教程](https://dunwu.github.io/linux-tutorial/) 📚 +- [Nginx 教程](https://github.com/dunwu/nginx-tutorial/) 📚 \ No newline at end of file diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md deleted file mode 100644 index af084c7d..00000000 --- a/docs/SUMMARY.md +++ /dev/null @@ -1,65 +0,0 @@ -# Summary - -## [简介](README.md) - -## JavaSE - -* [JavaSE](javase/README.md) - -## JavaEE - -* [JavaEE](javaee/README.md) - -## Java 框架 - -## Java 库 - -* [Java 库](javalib/README.md) - * [ActiveMQ 使用小结](javalib/activemq.md) - * [Dozer 使用小结](javalib/dozer.md) - * [细说 Java 主流日志工具库](javalib/java-log.md) - * [JavaMail 使用小结](javalib/javamail.md) - * [jsoup 使用小结](javalib/jsoup.md) - * [JUnit 使用小结](javalib/junit.md) - * [Lombok 使用小结](javalib/lombok.md) - * [Thumbnailator 使用小结](javalib/thumbnailator.md) - * [ZXing 使用小结](javalib/zxing.md) - -## Java 工具 - -* [构建工具](javatool/build/README.md) - * [Maven 快速指南(一)](javatool/build/maven/maven-quickstart-01.md) - * [Maven 快速指南(二)](javatool/build/maven/maven-quickstart-02.md) - * [Maven 之 pom.xml 详解(一)](javatool/build/maven/maven-pom-01.md) - * [Maven 之 pom.xml 详解(二)](javatool/build/maven/maven-pom-02.md) - * [Maven 之 pom.xml 详解(三)](javatool/build/maven/maven-pom-03.md) - * [Maven 之 settings.xml 详解](javatool/build/maven/maven-settings-config.md) - * [发布项目到中央仓库](javatool/build/maven/maven-deploy.md) - * [Maven 排错](javatool/build/maven/maven-faq.md) - * [Ant 简易教程](javatool/build/ant.md) -* [Elastic](javatool/elastic/README.md) - * [Elastic 技术栈之快速入门](javatool/elastic/elastic-quickstart.md) - * [Elastic 技术栈之 Logstash](javatool/elastic/elastic-logstash.md) - * [Elastic 技术栈之 Filebeat](javatool/elastic/elastic-beats.md) - * [Elastic 技术栈之 Kibana](javatool/elastic/elastic-kibana.md) -* [Java IDE](javatool/ide/README.md) - * [Intellij IDEA 使用小结](javatool/ide/intellij.md) - * [Eclipse 使用小结](javatool/ide/eclipse.md) -* [Java 服务器](javatool/server/README.md) - * [Tomcat](javatool/server/tomcat/README.md) - * [Tomcat 快速入门](javatool/server/tomcat/tomcat-quickstart.md) - * [Tomcat 工作原理](javatool/server/tomcat/tomcat-how-to-work.md) - * [Jetty 使用小结](javatool/server/jetty.md) - -## 技术扩展 - -* [数据库](extend/database.md) -* [数据结构和算法](extend/algorithm.md) -* [操作系统](extend/os.md) -* [网络](extend/web/README.md) - * [nginx 快速入门](extend/web/nginx-quickstart.md) - ------- - -* [附录](appendix/README.md) - * [Java 学习资源](appendix/resources.md) diff --git a/docs/appendix/README.md b/docs/appendix/README.md deleted file mode 100644 index b2fcd334..00000000 --- a/docs/appendix/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# 附录 - -## [Java 资源](resources.html) diff --git a/docs/appendix/resources.md b/docs/appendix/resources.md deleted file mode 100644 index 3d5b1549..00000000 --- a/docs/appendix/resources.md +++ /dev/null @@ -1,66 +0,0 @@ -# Java 资源 - -## 经典书籍 - -### javase - -#### [《Effective Java 中文版》](https://union-click.jd.com/jdc?d=S003h8) - -- 豆瓣评分:9.1【1235 人评价】 -- 推荐理由:本书介绍了在Java编程中78条极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。 -- 友情提示:同推荐《重构 : 改善既有代码的设计》、《代码整洁之道》、《代码大全》,有一定的内容重叠。 - -#### [《Java并发编程实战》](https://union-click.jd.com/jdc?d=x2yrwq) - -- 豆瓣评分:9.0 【651 人评价】 -- 推荐理由:本书深入浅出地介绍了Java线程和并发,是一本完美的Java并发参考手册。 - -#### [《深入理解Java虚拟机:JVM高级特性与最佳实践》](https://union-click.jd.com/jdc?d=Wa6dWb) - -- 豆瓣评分:8.9 【657 人评价】 -- 推荐理由:不去了解 JVM 的工程师,和咸鱼有什么区别? - -### javaee - -### javatool - -#### [《Maven 实战》](https://union-click.jd.com/jdc?d=hNj9Lu) - -- 豆瓣评分:8.1【563 人评价】 -- 推荐理由:国内最权威的Maven专家的力作,唯一一本哦! - -### database - -#### [《Redis设计与实现》](https://union-click.jd.com/jdc?d=6L6sMX) - -- 豆瓣评分:8.5 【427 人评价】 -- 推荐理由:系统而全面地描述了 Redis 内部运行机制。图示丰富,描述清晰,并给出大量参考信息,是NoSQL数据库开发人员案头必备。 - -### others - -#### [《鸟哥的Linux私房菜 (基础学习篇)》](https://union-click.jd.com/jdc?d=yB7dwu) - -- 豆瓣评分:9.1【2269 人评价】 -- 推荐理由:本书是最具知名度的Linux入门书《鸟哥的Linux私房菜基础学习篇》的最新版,全面而详细地介绍了Linux操作系统。 -- 友情提示:内容非常全面,建议挑选和自己实际工作相关度较高的,其他部分有需要再阅读。 - -### [《Head First 设计模式》](https://union-click.jd.com/jdc?d=HYyuyM) - -- 豆瓣评分:9.2【2394 人评价】 -- 推荐理由:《Head First设计模式》(中文版)共有14章,每章都介绍了几个设计模式,完整地涵盖了四人组版本全部23个设计模式。 - -### [《HTTP权威指南》](https://union-click.jd.com/jdc?d=TgCRBb) - -- 豆瓣评分:8.7 【1126 人评价】 -- 推荐理由:本书尝试着将HTTP中一些互相关联且常被误解的规则梳理清楚,并编写了一系列基于各种主题的章节,对HTTP各方面的特性进行了介绍。纵观全书,对HTTP“为什么”这样做进行了详细的解释,而不仅仅停留在它是“怎么做”的。 - -### [《TCP/IP详解 系列》](https://union-click.jd.com/jdc?d=5uHlXS) - -- 豆瓣评分:9.3 【1883 人评价】 -- 推荐理由:完整而详细的TCP/IP协议指南。针对任何希望理解TCP/IP协议是如何实现的读者设计。 - -### [《剑指Offer:名企面试官精讲典型编程题》](https://union-click.jd.com/jdc?d=wnrKQh) - -- 豆瓣评分:8.5【508 人评价】 -- 推荐理由:剖析了80个典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这5个面试要点。 -- 推荐网站:[牛客网-专业IT笔试面试备考平台](https://www.nowcoder.com/) diff --git a/docs/extend/README.md b/docs/extend/README.md deleted file mode 100644 index f0200619..00000000 --- a/docs/extend/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# 技术扩展 - -> 这部分内容主要是 Java 技术栈的延伸领域。例如:数据库、数据结构和算法、网络协议、操作系统等。 - -## 目录 - -* [数据库](database.md) -* [数据结构和算法](algorithm.md) -* [Linux](os.md) -* [网络](web/README.md) - * [nginx 快速入门](web/nginx-quickstart.md) diff --git a/docs/extend/algorithm.md b/docs/extend/algorithm.md deleted file mode 100644 index 24b6b44c..00000000 --- a/docs/extend/algorithm.md +++ /dev/null @@ -1,3 +0,0 @@ -# 数据结构和算法 - -> 数据结构和算法知识非常庞大。全部整理归纳在 [**algorithm-notes**](https://github.com/dunwu/algorithm-notes) 项目中。 diff --git a/docs/extend/database.md b/docs/extend/database.md deleted file mode 100644 index 980d3e88..00000000 --- a/docs/extend/database.md +++ /dev/null @@ -1,3 +0,0 @@ -# 数据库 - -> 数据库知识非常庞大。全部整理归纳在 [**db-notes**](https://github.com/dunwu/db-notes) 项目中。 diff --git a/docs/extend/os.md b/docs/extend/os.md deleted file mode 100644 index 11695a9d..00000000 --- a/docs/extend/os.md +++ /dev/null @@ -1,3 +0,0 @@ -# 操作系统 - -> Linux 知识非常庞大。全部整理归纳在 [**linux-notes**](https://github.com/dunwu/linux-notes) 项目中。 diff --git a/docs/extend/web/README.md b/docs/extend/web/README.md deleted file mode 100644 index ce64222c..00000000 --- a/docs/extend/web/README.md +++ /dev/null @@ -1 +0,0 @@ -# 网络 diff --git a/docs/extend/web/nginx-quickstart.md b/docs/extend/web/nginx-quickstart.md deleted file mode 100644 index 163ea62b..00000000 --- a/docs/extend/web/nginx-quickstart.md +++ /dev/null @@ -1,514 +0,0 @@ ---- -title: nginx 快速入门 -date: 2016-10-10 -categories: -- web -tags: -- server -- nginx -- load balance ---- - -# nginx 快速入门 - -## 概述 - -**什么是nginx?** - -**Nginx (engine x)** 是一款轻量级的Web 服务器 、反向代理服务器及电子邮件(IMAP/POP3)代理服务器。 - -**什么是反向代理?** - -反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。 - -![正向反向代理示意图.png](http://upload-images.jianshu.io/upload_images/3101171-71de739352457081.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 安装与使用 - -### 安装 -[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/framework/README.md b/docs/framework/README.md deleted file mode 100644 index e013b78d..00000000 --- a/docs/framework/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Java 框架 - -> 这部分内容主要是 Java 技术领域的一些流行框架。如 rpc 框架(dubbo)、安全框架(shiro)、mvc 框架(spring)、orm 框架(hibernate、mybatis) -> 这部分内容还在整理。 - -## 目录 diff --git a/docs/framework/dubbo/README.md b/docs/framework/dubbo/README.md deleted file mode 100644 index 7b747227..00000000 --- a/docs/framework/dubbo/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# dubbo-notes - -## 资料 - -**Dubbo** - -[Github](https://github.com/alibaba/dubbo) | [用户手册](https://dubbo.gitbooks.io/dubbo-user-book/content/) | [开发手册](https://dubbo.gitbooks.io/dubbo-dev-book/content/) | [管理员手册](https://dubbo.gitbooks.io/dubbo-admin-book/content/) - -**ZooKeeper** - -[官网](http://zookeeper.apache.org/) | [官方文档](http://zookeeper.apache.org/doc/trunk/) diff --git a/docs/framework/dubbo/dubbo-config.md b/docs/framework/dubbo/dubbo-config.md deleted file mode 100644 index 7a47109e..00000000 --- a/docs/framework/dubbo/dubbo-config.md +++ /dev/null @@ -1,113 +0,0 @@ -# Dubbo 配置 - -## 简介 - -### 配置方式 - -Dubbo 支持四中配置方式: - -- XML 配置 -- 属性配置 -- API 配置 -- 注解配置 - -我认为根据自己实际需要去掌握配置方式即可,没必要全都了解。更多内容可以参考:[Dubbo 官方用户手册](https://dubbo.gitbooks.io/dubbo-user-book/) - -在这里,只记录我使用到的 xml 配置方式。 - -### 配置分类 - -所有配置项分为三大类 - -- 服务发现:表示该配置项用于服务的注册与发现,目的是让消费方找到提供方。 - -- 服务治理:表示该配置项用于治理服务间的关系,或为开发测试提供便利条件。 - -- 性能调优:表示该配置项用于调优性能,不同的选项对性能会产生影响。 - -所有配置最终都将转换为 URL 表示,并由服务提供方生成,经注册中心传递给消费方,各属性对应 URL 的参数,参见配置项一览表中的 "对应URL参数" 列。 - -> **注意** -> -> 只有 group,interface,version 是服务的匹配条件,三者决定是不是同一个服务,其它配置项均为调优和治理参数。 -> -> **URL 格式** -> -> `protocol://username:password@host:port/path?key=value&key=value` - -## xml 配置 - -### provider.xml 示例 - -```xml - - - - - - - - -``` - -所有标签都支持自定义参数,用于不同扩展点实现的特殊配置,如: - -```xml - - - -``` - -或: - -```xml - - - -``` - -### 配置之间的关系 - -![image.png](http://upload-images.jianshu.io/upload_images/3101171-5c7371e9ab5999e7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -| 标签 | 用途 | 解释 | -| ---------------------- | ------ | ---------------------------------------- | -| `` | 服务配置 | 用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心 | -| `` | 引用配置 | 用于创建一个远程服务代理,一个引用可以指向多个注册中心 | -| `` | 协议配置 | 用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受 | -| `` | 应用配置 | 用于配置当前应用信息,不管该应用是提供者还是消费者 | -| `` | 模块配置 | 用于配置当前模块信息,可选 | -| `` | 注册中心配置 | 用于配置连接注册中心相关信息 | -| `` | 监控中心配置 | 用于配置连接监控中心相关信息,可选 | -| `` | 提供方配置 | 当 ProtocolConfig 和 ServiceConfig 某属性没有配置时,采用此缺省值,可选 | -| `` | 消费方配置 | 当 ReferenceConfig 某属性没有配置时,采用此缺省值,可选 | -| `` | 方法配置 | 用于 ServiceConfig 和 ReferenceConfig 指定方法级的配置信息 | -| `` | 参数配置 | 用于指定方法参数配置 | - -### 配置覆盖关系 - -以 timeout 为例,显示了配置的查找顺序,其它 retries, loadbalance, actives 等类似: - -- 方法级优先,接口级次之,全局配置再次之。 -- 如果级别一样,则消费方优先,提供方次之。 - -其中,服务提供方配置,通过 URL 经由注册中心传递给消费方。 - -![image.png](http://upload-images.jianshu.io/upload_images/3101171-0b046a7b9ac95ff1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -建议由服务提供方设置超时,因为一个方法需要执行多长时间,服务提供方更清楚,如果一个消费方同时引用多个服务,就不需要关心每个服务的超时设置。 - -理论上 ReferenceConfig 的非服务标识配置,在 ConsumerConfig,ServiceConfig, ProviderConfig 均可以缺省配置。 - -## 资料 - -[Dubbo 配置](https://dubbo.gitbooks.io/dubbo-user-book/configuration/) - -[Dubbo 官方 schema 配置参考手册](https://dubbo.gitbooks.io/dubbo-user-book/references/xml/introduction.html) \ No newline at end of file diff --git a/docs/framework/dubbo/dubbo-feature.md b/docs/framework/dubbo/dubbo-feature.md deleted file mode 100644 index 9374561f..00000000 --- a/docs/framework/dubbo/dubbo-feature.md +++ /dev/null @@ -1,143 +0,0 @@ -# Dubbo 特性 - -## 启动时检查 - -Dubbo 缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常。 - -需要关闭检查的场景: - -- 有些服务不关心,或者出现了循环依赖,必须有一方先启动。 -- Spring 容器是懒加载的,或者通过 API 编程延迟引用服务。 - -默认 `check="true"` - -关闭某个服务的启动时检查 (没有提供者时报错): - -``` - -``` - -关闭所有服务的启动时检查 (没有提供者时报错): - -``` - -``` - -关闭注册中心启动时检查 (注册订阅失败时报错): - -``` - -``` - -## 负载均衡 - -### 均衡策略 - -Dubbo 提供了多种均衡策略: - -#### Random - -- **随机**,按权重设置随机概率。 -- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。 - -#### RoundRobin - -- **轮循**,按公约后的权重设置轮循比率。 -- 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。 - -#### LeastActive - -- **最少活跃调用数**,相同活跃数的随机,活跃数指调用前后计数差。 -- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。 - -#### ConsistentHash - -- **一致性 Hash**,相同参数的请求总是发到同一提供者。 -- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。 -- 算法参见: -- 缺省只对第一个参数 Hash,如果要修改,请配置 `` -- 缺省用 160 份虚拟节点,如果要修改,请配置 `` - -### 均衡策略配置 - -- **服务端服务级别** - -```xml - -``` - -- **客户端服务级别** - -```xml - -``` - -- **服务端方法级别** - -```xml - - - -``` - -- **客户端方法级别** - -```xml - - - -``` - -### 负载均衡扩展 - -Dubbo 支持扩展新的负载均衡策略。 - -**扩展接口:**`com.alibaba.dubbo.rpc.cluster.LoadBalance` - -**扩展配置** - -```xml - - - -``` - -**扩展示例** - -Maven 项目结构: - -``` -src - |-main - |-java - |-com - |-xxx - |-XxxLoadBalance.java (实现LoadBalance接口) - |-resources - |-META-INF - |-dubbo - |-com.alibaba.dubbo.rpc.cluster.LoadBalance (纯文本文件,内容为:xxx=com.xxx.XxxLoadBalance) -``` - -XxxLoadBalance.java: - -```java -package com.xxx; - -import com.alibaba.dubbo.rpc.cluster.LoadBalance; -import com.alibaba.dubbo.rpc.Invoker; -import com.alibaba.dubbo.rpc.Invocation; -import com.alibaba.dubbo.rpc.RpcException; - -public class XxxLoadBalance implements LoadBalance { - public Invoker select(List> invokers, Invocation invocation) throws RpcException { - // ... - } -} -``` - -META-INF/dubbo/com.alibaba.dubbo.rpc.cluster.LoadBalance: - -``` -xxx=com.xxx.XxxLoadBalance -``` \ No newline at end of file diff --git a/docs/framework/dubbo/dubbo-overview.md b/docs/framework/dubbo/dubbo-overview.md deleted file mode 100644 index 5c4626eb..00000000 --- a/docs/framework/dubbo/dubbo-overview.md +++ /dev/null @@ -1,81 +0,0 @@ -# dubbo 概述 - -## 背景 - -发展趋势为:orm > mvc > rpc > soa 。 - -![image.png](http://upload-images.jianshu.io/upload_images/3101171-51e9492ecc8e13ea.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 需求 - -最基本的需求: - -- 负载均衡 -- 服务治理 - -## 架构 - -![image.png](http://upload-images.jianshu.io/upload_images/3101171-09cef50b0dd8c197.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**节点角色说明** - -| 节点 | 角色说明 | -| ----------- | ------------------- | -| `Provider` | 暴露服务的服务提供方 | -| `Consumer` | 调用远程服务的服务消费方 | -| `Registry` | 服务注册与发现的注册中心 | -| `Monitor` | 统计服务的调用次调和调用时间的监控中心 | -| `Container` | 服务运行容器 | - -**调用关系说明** - -1. 服务容器负责启动,加载,运行服务提供者。 -2. 服务提供者在启动时,向注册中心注册自己提供的服务。 -3. 服务消费者在启动时,向注册中心订阅自己所需的服务。 -4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 -5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 -6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 - -Dubbo 架构具有以下几个特点,分别是连通性、健壮性、伸缩性、以及向未来架构的升级性。 - -### 连通性 - -- 注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小 -- 监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示 -- 服务提供者向注册中心注册其提供的服务,并汇报调用时间到监控中心,此时间不包含网络开销 -- 服务消费者向注册中心获取服务提供者地址列表,并根据负载算法直接调用提供者,同时汇报调用时间到监控中心,此时间包含网络开销 -- 注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外 -- 注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者 -- 注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表 -- 注册中心和监控中心都是可选的,服务消费者可以直连服务提供者 - -### 健状性 - -- 监控中心宕掉不影响使用,只是丢失部分采样数据 -- 数据库宕掉后,注册中心仍能通过缓存提供服务列表查询,但不能注册新服务 -- 注册中心对等集群,任意一台宕掉后,将自动切换到另一台 -- 注册中心全部宕掉后,服务提供者和服务消费者仍能通过本地缓存通讯 -- 服务提供者无状态,任意一台宕掉后,不影响使用 -- 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复 - -### 伸缩性 - -- 注册中心为对等集群,可动态增加机器部署实例,所有客户端将自动发现新的注册中心 -- 服务提供者无状态,可动态增加机器部署实例,注册中心将推送新的服务提供者信息给消费者 - -### 升级性 - -当服务集群规模进一步扩大,带动IT治理结构进一步升级,需要实现动态部署,进行流动计算,现有分布式服务架构不会带来阻力。下图是未来可能的一种架构: - -![image.png](http://upload-images.jianshu.io/upload_images/3101171-e8fc326e375a0c61.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**节点角色说明** - -| 节点 | 角色说明 | -| ------------ | ------------------- | -| `Deployer` | 自动部署服务的本地代理 | -| `Repository` | 仓库用于存储服务应用发布包 | -| `Scheduler` | 调度中心基于访问压力自动增减服务提供者 | -| `Admin` | 统一管理控制台 | -| `Registry` | 服务注册与发现的注册中心 | -| `Monitor` | 统计服务的调用次调和调用时间的监控中心 | \ No newline at end of file diff --git a/docs/framework/dubbo/dubbo-pratice.md b/docs/framework/dubbo/dubbo-pratice.md deleted file mode 100644 index 64c0e3fd..00000000 --- a/docs/framework/dubbo/dubbo-pratice.md +++ /dev/null @@ -1,224 +0,0 @@ -# Dubbo 最佳实践 - -## 分包 - -建议将服务接口,服务模型,服务异常等均放在 API 包中,因为服务模型及异常也是 API 的一部分,同时,这样做也符合分包原则:重用发布等价原则(REP),共同重用原则(CRP)。 - -如果需要,也可以考虑在 API 包中放置一份 spring 的引用配置,这样使用方,只需在 spring 加载过程中引用此配置即可,配置建议放在模块的包目录下,以免冲突,如:`com/alibaba/china/xxx/dubbo-reference.xml`。 - -## 粒度 - -服务接口尽可能大粒度,每个服务方法应代表一个功能,而不是某功能的一个步骤,否则将面临分布式事务问题,Dubbo 暂未提供分布式事务支持。 - -服务接口建议以业务场景为单位划分,并对相近业务做抽象,防止接口数量爆炸。 - -不建议使用过于抽象的通用接口,如:`Map query(Map)`,这样的接口没有明确语义,会给后期维护带来不便。 - -## 版本 - -每个接口都应定义版本号,为后续不兼容升级提供可能,如: ``。 - -建议使用两位版本号,因为第三位版本号通常表示兼容升级,只有不兼容时才需要变更服务版本。 - -当不兼容时,先升级一半提供者为新版本,再将消费者全部升为新版本,然后将剩下的一半提供者升为新版本。 - -## 兼容性 - -服务接口增加方法,或服务模型增加字段,可向后兼容,删除方法或删除字段,将不兼容,枚举类型新增字段也不兼容,需通过变更版本号升级。 - -各协议的兼容性不同,参见: [服务协议](https://dubbo.gitbooks.io/dubbo-user-book/references/protocol/introduction.html) - -## 枚举值 - -如果是完备集,可以用 `Enum`,比如:`ENABLE`, `DISABLE`。 - -如果是业务种类,以后明显会有类型增加,不建议用 `Enum`,可以用 `String` 代替。 - -如果是在返回值中用了 `Enum`,并新增了 `Enum` 值,建议先升级服务消费方,这样服务提供方不会返回新值。 - -如果是在传入参数中用了 `Enum`,并新增了 `Enum` 值,建议先升级服务提供方,这样服务消费方不会传入新值。 - -## 序列化 - -服务参数及返回值建议使用 POJO 对象,即通过 `setter`, `getter` 方法表示属性的对象。 - -服务参数及返回值不建议使用接口,因为数据模型抽象的意义不大,并且序列化需要接口实现类的元信息,并不能起到隐藏实现的意图。 - -服务参数及返回值都必需是 byValue 的,而不能是 byReference 的,消费方和提供方的参数或返回值引用并不是同一个,只是值相同,Dubbo 不支持引用远程对象。 - -## 异常 - -建议使用异常汇报错误,而不是返回错误码,异常信息能携带更多信息,以及语义更友好。 - -如果担心性能问题,在必要时,可以通过 override 掉异常类的 `fillInStackTrace()` 方法为空方法,使其不拷贝栈信息。 - -查询方法不建议抛出 checked 异常,否则调用方在查询时将过多的 `try...catch`,并且不能进行有效处理。 - -服务提供方不应将 DAO 或 SQL 等异常抛给消费方,应在服务实现中对消费方不关心的异常进行包装,否则可能出现消费方无法反序列化相应异常。 - -## 调用 - -不要只是因为是 Dubbo 调用,而把调用 `try...catch` 起来。`try...catch` 应该加上合适的回滚边界上。 - -对于输入参数的校验逻辑在 Provider 端要有。如有性能上的考虑,服务实现者可以考虑在 API 包上加上服务 Stub 类来完成检验。 - -## 推荐用法 - -### 在 Provider 上尽量多配置 Consumer 端属性 - -原因如下: - -- 作服务的提供者,比服务使用方更清楚服务性能参数,如调用的超时时间,合理的重试次数,等等 -- 在 Provider 配置后,Consumer 不配置则会使用 Provider 的配置值,即 Provider 配置可以作为 Consumer 的缺省值。否则,Consumer 会使用 Consumer 端的全局设置,这对于 Provider 不可控的,并且往往是不合理的 - -Provider 上尽量多配置 Consumer 端的属性,让 Provider 实现者一开始就思考 Provider 服务特点、服务质量的问题。 - -示例: - -``` - - - - - -``` - -在 Provider 上可以配置的 Consumer 端属性有: - -1. `timeout` 方法调用超时 -2. `retries` 失败重试次数,缺省是 2 -3. `loadbalance` 负载均衡算法,缺省是随机 `random`。还可以有轮询 `roundrobin`、最不活跃优先`leastactive` -4. `actives` 消费者端,最大并发调用限制,即当 Consumer 对一个服务的并发调用到上限后,新调用会 Wait 直到超时 在方法上配置 `dubbo:method` 则并发限制针对方法,在接口上配置 `dubbo:service`,则并发限制针对服务 - -详细配置说明参见:[Dubbo配置参考手册](https://dubbo.gitbooks.io/dubbo-user-book/references/xml/introduction.html) - -### Provider 上配置合理的 Provider 端属性 - -```xml - - - - -``` - -Provider 上可以配置的 Provider 端属性有: - -1. `threads` 服务线程池大小 -2. `executes` 一个服务提供者并行执行请求上限,即当 Provider 对一个服务的并发调用到上限后,新调用会 Wait,这个时候 Consumer可能会超时。在方法上配置 `dubbo:method` 则并发限制针对方法,在接口上配置 `dubbo:service`,则并发限制针对服务 - -### 配置管理信息 - -目前有负责人信息和组织信息用于区分站点。有问题时便于的找到服务的负责人,至少写两个人以便备份。负责人和组织的信息可以在注册中心的上看到。 - -应用配置负责人、组织: - -```xml - -``` - -service 配置负责人: - -```xml - -``` - -reference 配置负责人: - -```xml - -``` - -`dubbo:service`、`dubbo:reference` 没有配置负责人,则使用 `dubbo:application` 设置的负责人。 - -## 配置 Dubbo 缓存文件 - -提供者列表缓存文件: - -```xml - -``` - -注意: - -1. 文件的路径,应用可以根据需要调整,保证这个文件不会在发布过程中被清除。 -2. 如果有多个应用进程注意不要使用同一个文件,避免内容被覆盖。 - -这个文件会缓存注册中心的列表和服务提供者列表。有了这项配置后,当应用重启过程中,Dubbo 注册中心不可用时则应用会从这个缓存文件读取服务提供者列表的信息,进一步保证应用可靠性。 - -### 监控配置 - -1. 使用固定端口暴露服务,而不要使用随机端口 - - 这样在注册中心推送有延迟的情况下,消费者通过缓存列表也能调用到原地址,保证调用成功。 - -2. 使用 Dragoon 的 http 监控项监控注册中心上服务提供方 - - Dragoon 监控服务在注册中心上的状态: 确保注册中心上有该服务的存在。 - -3. 服务提供方,使用 Dragoon 的 telnet 或 shell 监控项 - - 监控服务提供者端口状态:`echo status | nc -i 1 20880 | grep OK | wc -l`,其中的 20880 为服务端口 - -4. 服务消费方,通过将服务强制转型为 EchoService,并调用 `$echo()` 测试该服务的提供者是可用 - - 如 `assertEqauls(“OK”, ((EchoService)memberService).$echo(“OK”));` - -### 不要使用 dubbo.properties 文件配置,推荐使用对应 XML 配置 - -Dubbo 中所有的配置项都可以配置在 Spring 配置文件中,并且可以针对单个服务配置。 - -如完全不配置则使用 Dubbo 缺省值,参见 [Dubbo配置参考手册](https://dubbo.gitbooks.io/dubbo-user-book/references/xml/introduction.html) 中的说明。 - -#### dubbo.properties 中属性名与 XML 的对应关系 - -1. 应用名 `dubbo.application.name` - - ``` - - ``` - -2. 注册中心地址 `dubbo.registry.address` - - ``` - - ``` - -3. 调用超时 `dubbo.service.*.timeout` - - 可以在多个配置项设置超时 `timeout`,由上至下覆盖(即上面的优先)[5](https://dubbo.gitbooks.io/dubbo-user-book/recommend.html#fn_5),其它的参数(`retries`、`loadbalance`、`actives`等)的覆盖策略也一样示例如下: - - 提供者端特定方法的配置 - - ```xml - - - - ``` - - 提供者端特定接口的配置 - - ```xml - - ``` - -4. 服务提供者协议 `dubbo.service.protocol`、服务的监听端口 `dubbo.service.server.port` - - ```xml - - ``` - -5. 服务线程池大小 `dubbo.service.max.thread.threads.size` - - ```xml - - ``` - -6. 消费者启动时,没有提供者是否抛异常 Fast-Fail `alibaba.intl.commons.dubbo.service.allow.no.provider` - - ```xml - - ``` \ No newline at end of file diff --git a/docs/framework/dubbo/dubbo-protocol.md b/docs/framework/dubbo/dubbo-protocol.md deleted file mode 100644 index bc18985e..00000000 --- a/docs/framework/dubbo/dubbo-protocol.md +++ /dev/null @@ -1,142 +0,0 @@ -# Dubbo 协议 - -## 概述 - -Dubbo 支持以下通信协议: - -- dubbo -- rmi -- hessian -- http -- webservice -- thrift -- memcached -- redis - -**官方推荐使用 Dubbo 协议。** 所以本文只介绍 Dubbo 协议。 - -更多详情请参考:[Dubbo 官方协议参考手册](https://dubbo.gitbooks.io/dubbo-user-book/references/protocol/introduction.html) - -选用哪个协议,可以通过 `` 标签配置。 - -Dubbo 对应的配置类: `com.alibaba.dubbo.config.ProtocolConfig`。同时,如果需要支持多协议,可以声明多个 `` 标签,并在 `` 中通过 `protocol` 属性指定使用的协议。 - -## dubbo:// - -Dubbo 缺省协议采用单一长连接和 NIO 异步通讯,适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。 - -反之,Dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。 - -![image.png](http://upload-images.jianshu.io/upload_images/3101171-4ade06a7fdd941b0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -- Transporter: mina, netty, grizzy -- Serialization: dubbo, hessian2, java, json -- Dispatcher: all, direct, message, execution, connection -- ThreadPool: fixed, cached - -### 特性 - -缺省协议,使用基于 mina `1.1.7` 和 hessian `3.2.1` 的 tbremoting 交互。 - -- 连接个数:单连接 -- 连接方式:长连接 -- 传输协议:TCP -- 传输方式:NIO 异步传输 -- 序列化:Hessian 二进制序列化 -- 适用范围:传入传出参数数据包较小(建议小于100K),消费者比提供者个数多,单一消费者无法压满提供者,尽量不要用 dubbo 协议传输大文件或超大字符串。 -- 适用场景:常规远程服务方法调用 - -### 约束 - -- 参数及返回值需实现 `Serializable` 接口 -- 参数及返回值不能自定义实现 `List`, `Map`, `Number`, `Date`, `Calendar` 等接口,只能用 JDK 自带的实现,因为 hessian 会做特殊处理,自定义实现类中的属性值都会丢失。 -- Hessian 序列化,只传成员属性值和值的类型,不传方法或静态变量,兼容情况 [1](https://dubbo.gitbooks.io/dubbo-user-book/references/protocol/dubbo.html#fn_1)[2](https://dubbo.gitbooks.io/dubbo-user-book/references/protocol/dubbo.html#fn_2): - -| 数据通讯 | 情况 | 结果 | -| ---- | ------------------------------------- | ------------------------- | -| A->B | 类A多一种 属性(或者说类B少一种 属性) | 不抛异常,A多的那 个属性的值,B没有, 其他正常 | -| A->B | 枚举A多一种 枚举(或者说B少一种 枚举),A使用多 出来的枚举进行传输 | 抛异常 | -| A->B | 枚举A多一种 枚举(或者说B少一种 枚举),A不使用 多出来的枚举进行传输 | 不抛异常,B正常接 收数据 | -| A->B | A和B的属性 名相同,但类型不相同 | 抛异常 | -| A->B | serialId 不相同 | 正常传输 | - -接口增加方法,对客户端无影响,如果该方法不是客户端需要的,客户端不需要重新部署。输入参数和结果集中增加属性,对客户端无影响,如果客户端并不需要新属性,不用重新部署。 - -输入参数和结果集属性名变化,对客户端序列化无影响,但是如果客户端不重新部署,不管输入还是输出,属性名变化的属性值是获取不到的。 - -总结:服务器端和客户端对领域对象并不需要完全一致,而是按照最大匹配原则。 - -### 配置 - -配置协议: - -```xml - -``` - -设置默认协议: - -```xml - -``` - -设置服务协议: - -```xml - -``` - -多端口: - -```xml - - -``` - -配置协议选项: - -```xml - -``` - -多连接配置: - -Dubbo 协议缺省每服务每提供者每消费者使用单一长连接,如果数据量较大,可以使用多个连接。 - -```xml - -``` - -- `` 或 `` 表示该服务使用 JVM 共享长连接。**缺省** -- `` 或 `` 表示该服务使用独立长连接。 -- `` 或`` 表示该服务使用独立两条长连接。 - -为防止被大量连接撑挂,可在服务提供方限制大接收连接数,以实现服务提供方自我保护。 - -```xml - -``` - -`dubbo.properties` 配置: - -```properties -dubbo.service.protocol=dubbo -``` - -### FAQ - -#### 为什么要消费者比提供者个数多? - -因 dubbo 协议采用单一长连接,假设网络为千兆网卡,根据测试经验数据每条连接最多只能压满 7MByte(不同的环境可能不一样,供参考),理论上 1 个服务提供者需要 20 个服务消费者才能压满网卡。 - -#### 为什么不能传大包? - -因 dubbo 协议采用单一长连接,如果每次请求的数据包大小为 500KByte,假设网络为千兆网卡 [3](https://dubbo.gitbooks.io/dubbo-user-book/references/protocol/dubbo.html#fn_3),每条连接最大 7MByte(不同的环境可能不一样,供参考),单个服务提供者的 TPS(每秒处理事务数)最大为:128MByte / 500KByte = 262。单个消费者调用单个服务提供者的 TPS(每秒处理事务数)最大为:7MByte / 500KByte = 14。如果能接受,可以考虑使用,否则网络将成为瓶颈。 - -#### 为什么采用异步单一长连接? - -因为服务的现状大都是服务提供者少,通常只有几台机器,而服务的消费者多,可能整个网站都在访问该服务,比如 Morgan 的提供者只有 6 台提供者,却有上百台消费者,每天有 1.5 亿次调用,如果采用常规的 hessian 服务,服务提供者很容易就被压跨,通过单一连接,保证单一消费者不会压死提供者,长连接,减少连接握手验证等,并使用异步 IO,复用线程池,防止 C10K 问题。 - -## 资料 - -[Dubbo 官方协议参考手册](https://dubbo.gitbooks.io/dubbo-user-book/references/protocol/introduction.html) \ No newline at end of file diff --git a/docs/framework/dubbo/dubbo.xmind b/docs/framework/dubbo/dubbo.xmind deleted file mode 100644 index 227a80f8..00000000 Binary files a/docs/framework/dubbo/dubbo.xmind and /dev/null differ diff --git a/docs/framework/spring.md b/docs/framework/spring.md deleted file mode 100644 index e5109c68..00000000 --- a/docs/framework/spring.md +++ /dev/null @@ -1,5 +0,0 @@ -# Spring - -> Spring 是一个开源的 Java/Java EE 全功能栈(full-stack)的应用程序框架。 -> -> Spring 知识结构非常庞大。全部整理归纳在 [**spring-notes**](https://github.com/dunwu/spring-notes) 项目中。 diff --git a/docs/javaee/README.md b/docs/javaee/README.md deleted file mode 100644 index 4510441b..00000000 --- a/docs/javaee/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# JavaEE - -> JavaEE 知识非常庞大。全部整理归纳在 [**javaee-notes**](https://github.com/dunwu/javaee-notes) 项目中。 diff --git a/docs/javalib/README.md b/docs/javalib/README.md deleted file mode 100644 index d6e3b558..00000000 --- a/docs/javalib/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Java 库 - -## 目录 - -* [ActiveMQ 使用小结](activemq.md) -* [Dozer 使用小结](dozer.md) -* [细说 Java 主流日志工具库](java-log.md) -* [JavaMail 使用小结](javamail.md) -* [jsoup 使用小结](jsoup.md) -* [JUnit 使用小结](junit.md) -* [Lombok 使用小结](lombok.md) -* [Thumbnailator 使用小结](thumbnailator.md) -* [ZXing 使用小结](zxing.md) diff --git a/docs/javalib/activemq.md b/docs/javalib/activemq.md deleted file mode 100644 index 7c35278a..00000000 --- a/docs/javalib/activemq.md +++ /dev/null @@ -1,303 +0,0 @@ ---- -title: ActiveMQ 使用小结 -date: 2017/8/18 -categories: -- javalib -tags: -- java -- javalib -- jms ---- - -# ActiveMQ 使用小结 - -## JMS基本概念 - -`JMS` 即 **Java消息服务(Java Message Service)API**,是一个Java平台中关于面向消息中间件的API。它用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。Java消息服务是一个与具体平台无关的API,绝大多数MOM提供商都对JMS提供支持。 - -### 消息模型 - -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模型。 - -### 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 | - -## 安装 - -**安装条件** - -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) - - -## 项目中的应用 - -**引入依赖** - -```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/javalib/images/log-pattern.png b/docs/javalib/images/log-pattern.png deleted file mode 100644 index 8cc6b4e9..00000000 Binary files a/docs/javalib/images/log-pattern.png and /dev/null differ diff --git a/docs/javalib/java-log-style.md b/docs/javalib/java-log-style.md deleted file mode 100644 index 07493287..00000000 --- a/docs/javalib/java-log-style.md +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Java 日志规范 -date: 2018/3/29 -categories: -- javalib -tags: -- java -- javalib -- log ---- - -# Java 日志规范 - -> 本文基于[阿里巴巴Java开发手册](https://yq.aliyun.com/attachment/download/?id=4942)日志规约章节,结合自己的开发经验做了一些增删和调整。 - -1. 【强制】应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架SLF4J中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。 - -```java -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -private static final Logger logger = LoggerFactory.getLogger(Abc.class); -``` - -2. 【强制】日志文件推荐至少保存 `30` 天,因为有些异常具备以“周”为频次发生的特点。 - -3. 【强制】应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:`appName_logType_logName.log`。logType:日志类型,推荐分类有stats/desc/monitor/visit等;logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。 - -**正例**:mppserver应用中单独监控时区转换异常,如:`mppserver_monitor_timeZoneConvert.log` - -**说明**:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。 - -4. 【强制】对 `trace/debug/info` 级别的日志输出,必须使用条件输出形式或者使用占位符的方式。 - -说明:`logger.debug("Processing trade with id: " + id + " and symbol: " + symbol);` 如果日志级别是warn,上述日志不会打印,但是会执行字符串拼接操作,如果symbol是对象,会执行toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。 - -**正例:(条件)** - -``` -if (logger.isDebugEnabled()) { -logger.debug("Processing trade with id: " + id + " and symbol: " + symbol); -} -``` - -**正例:(占位符)** - -``` -logger.debug("Processing trade with id: {} and symbol : {} ", id, symbol); -``` - -5. 【强制】避免重复打印日志,浪费磁盘空间。务必在 `log4j.xml` 或 `logback.xml` 中设置 `additivity=false`。 - -**正例**: - -```xml - -``` - -6. 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字throws往上抛出。 - -**正例**:logger.error(各类参数或者对象toString + "_" + e.getMessage(), e); - -9. 【强制】日志格式遵循如下格式: - -![log-pattern.png](images/log-pattern.png) - -打印出的日志信息如: - -``` -2018-03-29 15:06:57.277 [javalib] [main] [TRACE] i.g.dunwu.javalib.log.LogbackDemo#main - 这是一条 trace 日志记录 -2018-03-29 15:06:57.282 [javalib] [main] [DEBUG] i.g.dunwu.javalib.log.LogbackDemo#main - 这是一条 debug 日志记录 -2018-03-29 15:06:57.282 [javalib] [main] [INFO] i.g.dunwu.javalib.log.LogbackDemo#main - 这是一条 info 日志记录 -2018-03-29 15:06:57.282 [javalib] [main] [WARN] i.g.dunwu.javalib.log.LogbackDemo#main - 这是一条 warn 日志记录 -2018-03-29 15:06:57.282 [javalib] [main] [ERROR] i.g.dunwu.javalib.log.LogbackDemo#main - 这是一条 error 日志记录 -``` - -8. 【参考】slf4j 支持的日志级别,按照级别从低到高,分别为:`trace < debug < info < warn < error`。 - -建议只使用 `debug < info < warn < error` 四个级别。 - -* `error` 日志级别只记录系统逻辑出错、异常等重要的错误信息。如非必要,请不要在此场景打出error级别。 -* `warn` 日志级别记录用户输入参数错误的情况,避免用户投诉时,无所适从。 -* `info` 日志级别记录业务逻辑中一些重要步骤信息。 -* `debug` 日志级别记录一些用于调试的信息。 - - -10. 【参考】有一些第三方框架或库的日志对于排查问题具有一定的帮助,如 Spring、Dubbo、Mybatis 等。这些框架所使用的日志库未必和本项目一样,为了避免出现日志无法输出的问题,请引入对应的桥接 jar 包。 - -## 资源 - -* [阿里巴巴Java开发手册](https://yq.aliyun.com/attachment/download/?id=4942)日志规约章节 \ No newline at end of file diff --git a/docs/javalib/java-log.md b/docs/javalib/java-log.md deleted file mode 100644 index 06af096e..00000000 --- a/docs/javalib/java-log.md +++ /dev/null @@ -1,614 +0,0 @@ ---- -title: 细说 Java 主流日志工具库 -date: 2016/10/14 -categories: -- javalib -tags: -- java -- javalib -- log ---- - -# 细说 Java 主流日志工具库 - -## 概述 - -在项目开发中,为了跟踪代码的运行情况,常常要使用日志来记录信息。 - -在Java世界,有很多的日志工具库来实现日志功能,避免了我们重复造轮子。 - -我们先来逐一了解一下主流日志工具。 - -### java.util.logging (JUL) - -JDK1.4开始,通过`java.util.logging`提供日志功能。 - -它能满足基本的日志需要,但是功能没有Log4j强大,而且使用范围也没有Log4j广泛。 - -### Log4j - -Log4j是apache的一个开源项目,创始人Ceki Gulcu。 - -Log4j应该说是Java领域资格最老,应用最广的日志工具。从诞生之日到现在一直广受业界欢迎。 - -Log4j是高度可配置的,并可通过在运行时的外部文件配置。它根据记录的优先级别,并提供机制,以指示记录信息到许多的目的地,诸如:数据库,文件,控制台,UNIX系统日志等。 - -Log4j中有三个主要组成部分: - -- **loggers:** 负责捕获记录信息。 -- **appenders :** 负责发布日志信息,以不同的首选目的地。 -- **layouts:** 负责格式化不同风格的日志信息。 - -[官网地址](http://logging.apache.org/log4j/2.x/) - -### Logback - -Logback是由log4j创始人Ceki Gulcu设计的又一个开源日记组件,目标是替代log4j。 - -logback当前分成三个模块:`logback-core`,`logback- classic`和`logback-access`。 - -`logback-core`是其它两个模块的基础模块。 - -`logback-classic`是log4j的一个 改良版本。此外`logback-classic`完整实现SLF4J API使你可以很方便地更换成其它日记系统如log4j或JDK14 Logging。 - -`logback-access`访问模块与Servlet容器集成提供通过Http来访问日记的功能。 - -[官网地址](http://logback.qos.ch/) - -### Log4j vs Logback - -Logback相比Log4j具有许多好处: - -**性能提升** - -logback在log4j基础上做了优化,使性能提高了近10倍。此外,内存开销也减少了。 - -**更充足的测试** - -尽管log4j也做了测试,但是logback的测试更加充分。所以,logback应该更加稳定。 - -**天然支持slf4j** - -因为Logback-classic完全实现了slf4j的接口,所以天然支持slf4j。使用slf4j,有利于你切换日志工具库,减少工作量。 - -**自动重载配置文件** - -Logback-classic可以自动重载更新过的配置文件。 - -**自动移除旧日志** - -通过配置文件最大数和过期时间,Logback可以控制日志文件数并自动清除过期的日志。 - -**更灵活、更精细的配置** - -Logback在配置中提供更加丰富的功能来帮助你更加精细的去定制你的日志组件: - -``提供比log4j更丰富的过滤条件; - -增加``, `` 和 ``这样的条件控制; - -**打印异常的调用栈信息** - -Logback在打印异常时,会打印调用栈的包装数据。 - -**Logback-access** - -Logback-access支持Logback-classic的所有特性,并且它可以提供丰富的HTTP-access日志功能。 - -**总结** - -以上优点摘自官方推荐理由:[Reasons to prefer logback over log4j](http://logback.qos.ch/reasonsToSwitch.html)。 - -由于Logback的作者也是Log4j的作者,所有推荐理由应该比较靠谱。 - -总之,相比于Log4j,好处多多,你心动了没? - -### 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/) - -![slf4j工作模型](http://oyz7npk35.bkt.clouddn.com//image/java/libs/log/slf4j-to-other-log.png) - -### common-logging vs slf4j - -slf4j库类似于Apache Common-Logging。但是,他在编译时静态绑定真正的日志库。这点似乎很麻烦,其实也不过是导入桥接jar包而已。 - -slf4j一大亮点是提供了更方便的日志记录方式: - -不需要使用`logger.isDebugEnabled()`来解决日志因为字符拼接产生的性能问题。slf4j的方式是使用`{}`作为字符串替换符,形式如下: - -``` -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.logbacklogback-classic1.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.slf4jslf4j-log4j121.7.21 - -``` - -**slf4j + java.util.logging** - -添加依赖到pom.xml中即可。 - -*slf4j-jdk14-1.7.21.jar* 会自动将 *slf4j-api-1.7.21.jar* 也添加到你的项目中。 - -```xml -org.slf4jslf4j-jdk141.7.21 - -``` - -#### slf4j兼容非slf4j日志组件 - -在介绍解决方案前,先提一个概念——桥接 - -**什么是桥接呢** - -假如你正在开发应用程序所调用的组件当中已经使用了common-logging,这时你需要 jcl-over-slf4j.jar把日志信息输出重定向到 slf4j-api,slf4j-api再去调用slf4j实际依赖的日志组件。这个过程称为桥接。 -下图是官方的slf4j桥接策略图: - -![slf4j桥接策略](http://oyz7npk35.bkt.clouddn.com//image/java/libs/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 - -``` - -### 配置 - -日志配置文件大同小异,需要注意的是:**logback.xml和log4j.xml都需要放在classpath路径下**。 - -#### 完整的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.xml参考示例 - -log4j的配置文件一般有xml格式或properties格式。这里为了和logback.xml做个对比,就不介绍properties了,其实也没太大差别。 - -```xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -#### logback配置参数说明 - -logback基本兼容log4j的配置,并提供更多的功能。 - -这里奉献一张本人整理的logback配置思维导图,高清无码。 - -![logback配置](http://oyz7npk35.bkt.clouddn.com//image/java/libs/log/popular-logs-mind.png) - -### 使用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"); - } -} -``` - -## 参考 - -- [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/) -- http://blog.csdn.net/yycdaizi/article/details/8276265 diff --git a/docs/javalib/jsoup.md b/docs/javalib/jsoup.md deleted file mode 100644 index 7fbf27d7..00000000 --- a/docs/javalib/jsoup.md +++ /dev/null @@ -1,460 +0,0 @@ ---- -title: jsoup 使用小结 -date: 2017/8/18 -categories: -- javalib -tags: -- java -- javalib -- html ---- - -# jsoup 使用小结 - -## 概述 - -jsoup 是一款 Java 的 HTML 解析器,可直接解析某个 URL 地址、HTML 文本内容。它提供了一套非常省力的 API,可通过 DOM,CSS 以及类似于 JQuery 的操作方法来取出和操作数据。 - -jsoup 工作的流程主要如下: - -1. 从一个 URL,文件或字符串中解析 HTML,并加载为一个 `Document` 对象。 -2. 使用 DOM 或 CSS 选择器来取出数据; -3. 可操作 HTML 元素、属性、文本。 - -jsoup 是基于 MIT 协议发布的,可放心使用于商业项目。 - -## 加载 - -### 从HTML字符串加载一个文档 - -使用静态 `Jsoup.parse(String html)` 方法或 `Jsoup.parse(String html, String baseUri)` 示例代码: - -```java -String html = "First parse" - + "

    Parsed HTML into a doc.

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

    Lorem ipsum.

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

    An example link.

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

    lorem ipsum

    "); //

    lorem ipsum

    -div.prepend("

    First

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

    Last

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

    First

    lorem ipsum

    Last

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

    Link

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

    Link

    -``` - -**说明** - -XSS 又叫 CSS (Cross Site Script) ,跨站脚本攻击。它指的是恶意攻击者往 Web 页面里插入恶意 html 代码,当用户浏览该页之时,嵌入其中 Web 里面的 html 代码会被执行,从而达到恶意攻击用户的特殊目的。XSS 属于被动式的攻击,因为其被动且不好利用,所以许多人常忽略其危害性。所以我们经常只让用户输入纯文本的内容,但这样用户体验就比较差了。 - -一个更好的解决方法就是使用一个富文本编辑器 WYSIWYG 如 [CKEditor](http://ckeditor.com/) 和 [TinyMCE](http://tinymce.moxiecode.com/)。这些可以输出 HTML 并能够让用户可视化编辑。虽然他们可以在客户端进行校验,但是这样还不够安全,需要在服务器端进行校验并清除有害的HTML代码,这样才能确保输入到你网站的HTML是安全的。否则,攻击者能够绕过客户端的 Javascript 验证,并注入不安全的 HMTL 直接进入您的网站。 - -jsoup 的 whitelist 清理器能够在服务器端对用户输入的 HTML 进行过滤,只输出一些安全的标签和属性。 - -jsoup 提供了一系列的 `Whitelist` 基本配置,能够满足大多数要求;但如有必要,也可以进行修改,不过要小心。 - -这个 cleaner 非常好用不仅可以避免 XSS 攻击,还可以限制用户可以输入的标签范围。 - -**参见** - -- 参阅[XSS cheat sheet](http://ha.ckers.org/xss.html) ,有一个例子可以了解为什么不能使用正则表达式,而采用安全的whitelist parser-based清理器才是正确的选择。 -- 参阅`Cleaner` ,了解如何返回一个 `Document` 对象,而不是字符串 -- 参阅`Whitelist`,了解如何创建一个自定义的whitelist -- [nofollow](http://en.wikipedia.org/wiki/Nofollow) 链接属性了解 - -## 参考 - -- [jsoup github托管代码](https://github.com/jhy/jsoup) - -- [jsoup Cookbook](https://jsoup.org/cookbook/) - -- [jsoup Cookbook(中文版)](http://www.open-open.com/jsoup/) - -- [不错的jsoup学习笔记](https://github.com/code4craft/jsoup-learning) diff --git a/docs/javalib/junit.md b/docs/javalib/junit.md deleted file mode 100644 index f350a72d..00000000 --- a/docs/javalib/junit.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: JUnit 使用小结 -date: 2017/8/18 -categories: -- javalib -tags: -- java -- javalib -- test ---- - -# JUnit 使用小结 - -## 安装 - -在 pom 中添加依赖 - -``` - - junit - junit - 4.9 - test - -``` - -## JUnit 注解 - -JUnit 4 开始使用 Java 5 中的注解(annotation),常用的几个 annotation 介绍: - -`@BeforeClass`:针对所有测试,只执行一次,且必须为static void - -`@Before`:初始化方法 - -`@Test`:测试方法,在这里可以测试期望异常和超时时间 - -`@After`:释放资源 - -`@AfterClass`:针对所有测试,只执行一次,且必须为static void - -`@Ignore`:忽略的测试方法 - -一个单元测试用例执行顺序为: - -@BeforeClass –> @Before –> @Test –> @After –> @AfterClass diff --git a/docs/javalib/zxing.md b/docs/javalib/zxing.md deleted file mode 100644 index 2934df91..00000000 --- a/docs/javalib/zxing.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: ZXing 使用小结 -date: 2017/01/17 -categories: -- javalib -tags: -- java -- javalib -- barcode ---- - -# 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) - -以下是一个生成的二维码图片示例: - -![qrcode.png](http://upload-images.jianshu.io/upload_images/3101171-26b73730088f0ab8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 参考 - -[ZXing github仓库](https://github.com/zxing/zxing) diff --git a/docs/javase/README.md b/docs/javase/README.md deleted file mode 100644 index ab37c1be..00000000 --- a/docs/javase/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# JavaSE - -> JavaSE 知识非常庞大。全部整理归纳在 [**javase-notes**](https://github.com/dunwu/javase-notes) 项目中。 diff --git a/docs/javatool/README.md b/docs/javatool/README.md deleted file mode 100644 index 21882de3..00000000 --- a/docs/javatool/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Java 工具 - -> 本部分内容主要是 Java 开发领域使用的一些 Java 工具,如构建工具、IDE、服务器、日志中心等等。 - -## 目录 - -* [构建工具](build/README.md) - * [Maven 快速指南(一)](build/maven/maven-quickstart-01.md) - * [Maven 快速指南(二)](build/maven/maven-quickstart-02.md) - * [Maven 之 pom.xml 详解(一)](build/maven/maven-pom-01.md) - * [Maven 之 pom.xml 详解(二)](build/maven/maven-pom-02.md) - * [Maven 之 pom.xml 详解(三)](build/maven/maven-pom-03.md) - * [Maven 之 settings.xml 详解](build/maven/maven-settings-config.md) - * [发布项目到中央仓库](build/maven/maven-deploy.md) - * [Maven 排错](build/maven/maven-faq.md) - * [Ant 简易教程](build/ant.md) -* [Elastic](elastic/README.md) - * [Elastic 技术栈之快速指南](elastic/elastic-quickstart.md) - * [Elastic 技术栈之 Logstash 基础](elastic/elastic-logstash.md) -* [Java IDE](ide/README.md) - * [Intellij IDEA 使用小结](ide/intellij.md) - * [Eclipse 使用小结](ide/eclipse.md) -* [Java 服务器](server/README.md) - * [Jetty 使用小结](server/jetty.md) diff --git a/docs/javatool/build/README.md b/docs/javatool/build/README.md deleted file mode 100644 index 080b253a..00000000 --- a/docs/javatool/build/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# 构建工具 - -## 目录 - -### Maven - -* [Maven 快速指南(一)](maven/maven-quickstart-01.md) -* [Maven 快速指南(二)](maven/maven-quickstart-02.md) -* [Maven 之 pom.xml 详解(一)](maven/maven-pom-01.md) -* [Maven 之 pom.xml 详解(二)](maven/maven-pom-02.md) -* [Maven 之 pom.xml 详解(三)](maven/maven-pom-03.md) -* [Maven 之 settings.xml 详解](maven/maven-settings-config.md) -* [发布项目到中央仓库](maven/maven-deploy.md) -* [Maven 排错](maven/maven-faq.md) - -### Ant - -* [Ant 简易教程](ant.md) diff --git a/docs/javatool/build/ant.md b/docs/javatool/build/ant.md deleted file mode 100644 index 06cf4364..00000000 --- a/docs/javatool/build/ant.md +++ /dev/null @@ -1,378 +0,0 @@ ---- -title: Ant 简易教程 -date: 2015-04-19 -categories: -- javatool -tags: -- java -- javatool -- build ---- - -# Ant 简易教程 - -## 前言 - -`Apache Ant` 是一个将软件编译、测试、部署等步骤联系在一起加以自动化的一个工具,大多用于Java环境中的软件开发。由Apache软件基金会所提供。 - -Ant是纯Java语言编写的,所以具有很好的跨平台性。 - -![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3101171-d9da2a06160103d0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 下载和安装 - -### 下载 - -ant的官方下载地址:http://ant.apache.org/bindownload.cgi - -进入页面后,在下图的红色方框中可以下载最新版本。笔者下载的版本是 **apache-ant-1.9.4。** - -![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3101171-72d3bc81cd29e68d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -### 配置环境变量 - -配置环境变量(我的电脑 -> 属性 -> 高级 -> 环境变量)。 - -设置ant环境变量: - -**ANT_HOME**    C:/ apache-ant-1.9.4 - -![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3101171-682a8e16b82a7532.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**path **              C:/ apache-ant-1.9.4/bin - -![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3101171-ea61070f97b5a7cc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**classpath**        C:/apache-ant-1.9.4/lib - -![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3101171-5bc45dbe64602bc7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -### 验证 - -点击 开始 -> 运行 -> 输入cmd - -**执行构建文件** - -输入如下命令:**ant** - -如果出现如下内容,说明安装成功: - -> Buildfile: build.xml does not exist! -> Build failed - -注意:因为ant默认运行build.xml文件,这个文件需要我们创建。 - -如果不想命名为build.xml,运行时可以使用 **ant -buildfile test.xml** 命令指明要运行的构建文件。 - -**查看版本信息** - -输入 **ant  -version**,可以查看版本信息。  - -![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3101171-920e94f33b4d7dd9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -但如果出现 'ant' 不是内部或外部命令,也不是可运行的程序或批处理文件,说明安装失败:(可以重复前述步骤,直至安装成功。) - -## 例子 - -在安装和配置成功后,我们就可以使用ant了。 - -为了让读者对ant有一个直观的认识,首先以Ant官方手册上的一个简单例子做一个说明。 - -以下是一个build.xml文件的内容:  - -```xml - - - simple example build file - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -``` - -在这个xml文件中,有几个target标签,每个target对应一个执行目标。 - -我们将这个build.xml放在 D:\Temp\ant_test 路径下,然后在dos界面下进行测试。  - -**ant init** - -![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3101171-0d37a1be0ef4238a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -在 D:\Temp\ant_test 路径下创建了一个build目录,执行成功。 - -**ant compile**  - -![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3101171-6f35ed13331c87c9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -提示错误,原来是在build.xml的所在目录下找不到src目录。好的,我们直接创建一个src目录,然后再次尝试。这次,执行成功。 - -![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3101171-9e84af99a8e952e0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -**ant dist ** - -![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3101171-daeaf201bf05e097.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -在 D:\Temp\ant_test 路径下创建了一个dist目录,执行成功。 - -**ant clean** - -![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3101171-be427613f7867513.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -清除创建的build和dist目录,执行成功。  - -**一个细节** - -细心的读者,想必已经发现一个问题——在执行 ant compile 和 ant dist 命令的时候把前面的命令也执行了。这是为什么呢? - -请留意一下build.xml中的内容。有部分 **target** 标签中含有 **depends** 关键字。  - -![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3101171-746a2156fbfb8d54.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -这表明,当前的 target 在执行时需要依赖其他的target,必须先执行依赖的target,然后再执行。  - -## 关键元素 - -Ant的构件文件都是XML格式的。每个构件文件包含一个project元素和至少一个target。 - -target元素可以包含多个task元素。  - -### Project 元素 - -**project 元素**是构建文件的根元素。  - -一个 project 元素可以有多个 target 元素,一个 target 元素可以有多个 task。 - -在上节的例子中,project标签里有三个属性。   - -```xml - -``` - -**name属性**,指示 project 元素的名字。例子中的名字就是 MyProject。 - -**default属性**,指示这个 project 默认执行的 target。在本文的例子中,默认执行的 target 为 dist。 - -如果我们输入命令 ant 时,不指定 target 参数,默认会执行 dist 这个 target。 - -**basedir属性**,指定根路径的位置。该属性没有指定时,使用Ant的构件文件的所在目录作为根目录。 - -  - -### Target 元素 - -**target 元素**是 task 的容器,也就是 Ant 的一个基本执行单元。  - -以上节例子中的 compile 来举例。 - -```xml - - - - -``` - -这个 target 中出现了几个属性。 - -**name属性**,指示target元素的名称。 - -这个属性在一个project元素中必须是唯一的。这很好理解,如果出现重复,Ant就不知道具体该执行哪个 target 了。 - -**depends属性**,指示依赖的 target,当前的 target 必须在依赖的 target 之后执行。 - -**description属性**,是关于 target 的简短说明。 - -此外,还有其他几个未出现在构建文件中的属性。 - -**if属性**,验证指定的属性是否存在,若不存在,所在target将不会被执行。 - -**unless属性**,**正好和 if属性相反**,验证指定的属性是否存在,若存在,所在target将不会被执行。**** - -**extensionOf属性**,添加当前 target 到 **extension-point** 依赖列表。**——Ant1.8.0新特性。** - -> **extension-point 元素**和 target 元素十分类似,都可以指定依赖的target。但是不同的是,extension-point 中不能包含任何 task。 - -请看以下实例:  - -```xml - - ... - - - - ... - -``` - -**调用target顺序**:  create-directory-layout --> 'empty slot' --> compile  - -```xml - - ... - -``` - -**调用target顺序**:  create-directory-layout --> generate-sources  --> compile - -**onMissingExtensionPoint属性**:当无法找到一个extension-point时,target尝试去做的动作("fail", "warn", "ignore")。*——Ant1.8.2新特性* - -### Task 元素 - -task是一段可以被执行的代码。 - -一个task可以有多个属性, 一个属性可以包含对一个 **property** 的引用。 - -task的通常结构为 - -```xml - -``` - -其中,name 是 task 的名字, attributeN 是属性名, valueN 是这个属性的值。 - -还是以 compile 做为例子: - -```xml - -     -     - -``` - -在 compile 这个 target 标签中包含了一个任务。 - -这个任务的动作是:执行JAVA编译,编译src下的代码,并把编译生成的文件放在build目录中。 - -**常用task ** - -**javac**:用于编译一个或者多个Java源文件,通常需要srcdir和destdir两个属性,用于指定Java源文件的位置和编译后class文件的保存位置。 - -```xml - -``` - -**java**:用于运行某个Java类,通常需要classname属性,用于指定需要运行哪个类。 - -```xml - - - - - - -``` - -**jar**:用于生成JAR包,通常需要指定destfile属性,用于指定所创建JAR包的文件名。除此之外,通常还应指定一个文件集,表明需要将哪些文件打包到JAR包里。 - -```xml - -``` - -**echo**:输出某个字符串。 - -```xml - -You are using version ${java.version} of Java! This message spans two lines. -``` - -**copy**:用于复制文件或路径。 - -```xml - - - - - - - - -``` - -**delete**:用于删除文件或路径。 - -```xml - - - - - - - - -``` - -**mkdir**:用于创建文件夹。 - -```xml -  -``` - -**move**:用户移动文件和路径。 - -```xml - - - - - - -``` - -### Property 元素 - -Property 是对参数的定义。 - -project的属性可以通过property元素来设定,也可在Ant之外设定。若要在外部引入某文件,例如build.properties文件,可以通过如下内容将其引入:。 - -property元素可用作 task 的属性值。在task中是通过将属性名放在“${”和“}”之间,并放在task属性值的位置来实现的。 - -例如 complile 例子中,使用了前面定义的 src 作为源目录。  - -```xml - -``` - -Ant提供了一些内置的属性,它能得到的系统属性的列表与Java文档中System.getPropertis()方法得到的属性一致,这些系统属性可参考sun网站的说明。 - -### extension-point元素 - -和 target 元素十分类似,都可以指定依赖的target。但是不同的是,extension-point 中不能包含任何 task。 - -*——Ant1.8.0新增特性。* - -在 target元素中的例子里已提到过,不再赘述。  - -## 参考资料 - -- [ant官方手册](http://ant.apache.org/manual/index.html ) -- [http://www.blogjava.net/amigoxie/archive/2007/11/09/159413.html](http://www.blogjava.net/amigoxie/archive/2007/11/09/159413.html) diff --git a/docs/javatool/build/maven/README.md b/docs/javatool/build/maven/README.md deleted file mode 100644 index 8acba117..00000000 --- a/docs/javatool/build/maven/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Maven - -## 知识点 - -maven 知识点见 `maven.xmind`。 - -_xmind 是思维导图文件,打开需使用 [xmind](https://www.xmind.cn/) 工具。_ - -## 目录 - -* [Maven 快速指南(一)](maven-quickstart-01.html) -* [Maven 快速指南(二)](maven-quickstart-02.html) -* [Maven 之 pom.xml 详解(一)](maven-pom-01.html) -* [Maven 之 pom.xml 详解(二)](maven-pom-02.html) -* [Maven 之 pom.xml 详解(三)](maven-pom-03.html) -* [Maven 之 settings.xml 详解](maven-settings-config.html) -* [发布项目到中央仓库](maven-deploy.html) -* [Maven 排错](maven-faq.html) diff --git a/docs/javatool/build/maven/maven-deploy.md b/docs/javatool/build/maven/maven-deploy.md deleted file mode 100644 index 895d3f75..00000000 --- a/docs/javatool/build/maven/maven-deploy.md +++ /dev/null @@ -1,171 +0,0 @@ -# 发布项目到中央仓库 - -## 创建工单 - -在发布Java包到Maven中央仓库首先需要在[https://issues.sonatype.org/secure/Dashboard.jspa](https://www.iteblog.com/redirect.php?url=aHR0cHM6Ly9pc3N1ZXMuc29uYXR5cGUub3JnL3NlY3VyZS9EYXNoYm9hcmQuanNwYQ==&article=true)网站创建一个工单(Issues),第一次使用这个网站的时候需要注册自己的帐号(这个帐号和密码需要记住,后面会用到),之后创建自己的Issue。 - -然后根据你Java包的功能分别写上`Summary`、`Description`、`Group Id`、`SCM url`以及`Project URL`等必要信息,可以参见我之前创建的Issue:[https://issues.sonatype.org/browse/OSSRH-33765](https://issues.sonatype.org/browse/OSSRH-33765)。创建完之后需要等待Sonatype的工作人员审核处理,审核时间还是很快的,我的审核差不多等待了两小时。当Issue的Status变为`RESOLVED`后,就可以进行下一步操作了。 - -> 注:如果你的Group Id填写的是自己的网站(我的就是这种情况),Sonatype的工作人员会询问你那个Group Id是不是你的域名,你只需要在上面回答是就行,然后就会通过审核。 - -## gpg - -### 安装、下载 gpg - -上面创建的issuce经过审核之后,我们可以使用gpg生成密钥对,这里分两种情况: - -1. 如果使用的是Windows,可以到[https://www.gpg4win.org/download.html](https://www.iteblog.com/redirect.php?url=aHR0cHM6Ly93d3cuZ3BnNHdpbi5vcmcvZG93bmxvYWQuaHRtbA==&article=true)下载gpg4win,推荐使用 Gpg4win-Vanilla 2.3.3版本,因为它仅包括 GnuPG,这个工具才是我们所需要的; -2. 如果使用的是Linux,可以通过`yum install gpg`命令安装gpg。 - -之后可以通过`gpg --version`命令查看是否安装成功,如果出现版本等信息说明安装成功了。 - -### 生成密钥 - -安装 gpg 成功后,执行命令: - -```bash -$ gpg --gen-key -``` - -按照提示信息依次设置好名字、邮箱等信息。不过输入Passphrase的值需要记住,这个相当于密钥的密码,发布过程中进行签名操作的时候会用到。 - -到这里我们就设置好密钥对了。上面代码中导数第四行的`B15C5AA3`需要记住,其相当于我们生成的key,后面会用到。 - -### 上传keys - -```bash -$ gpg --list-keys -xxxx/gnupg/pubring.gpg --------------------------------------------------------- -pub 2048R/D416FE08 2017-12-06 -uid [ultimate] Zhang Peng (author) -sub 2048R/CBD8FA39 2017-12-06 - - -$ gpg --keyserver hkp://keyserver.ubuntu.com:11371 --send-keys D416FE08 -gpg: sending key D416FE08 to hkp server keyserver.ubuntu.com -``` - -## settings 配置 - -修改 maven 的 settings.xml 文件 - -```xml - - - sonatype-nexus-snapshots - Sonatype网站的账号 - Sonatype网站的密码 - - - sonatype-nexus-staging - Sonatype网站的账号 - Sonatype网站的密码 - - -``` - -## pom 配置 - -licenses、scm、developers 配置: - -```xml - - - The Apache Software License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - - - VictorZhang - forbreak@163.com - https://github.com/dunwu - - - - - https://github.com/dunwu/tent - git@github.com:dunwu/tent.git - https://github.com/dunwu - -``` - -distributionManagement 配置 - -```xml - - - nexus - https://oss.sonatype.org/content/repositories/snapshots - - - nexus - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - -``` - -profiles 配置 - -```xml - - - release - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.7 - true - - nexus - https://oss.sonatype.org/ - true - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.10.3 - - false - true - - - - attach-javadocs - - jar - - - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - - - - - - -``` - -## 部署和发布 - -```bash -$ mvn clean deploy -P sonatype-oss-release -Darguments="gpg.passphrase=设置gpg设置密钥时候输入的Passphrase" -Dmaven.test.skip=true -``` \ No newline at end of file diff --git a/docs/javatool/build/maven/maven-faq.md b/docs/javatool/build/maven/maven-faq.md deleted file mode 100644 index 342ec7c4..00000000 --- a/docs/javatool/build/maven/maven-faq.md +++ /dev/null @@ -1,56 +0,0 @@ ---- -title: Maven 排错 -date: 2017/11/10 -categories: -- javatool -tags: -- java -- javatool -- build -- maven ---- - -# Maven 排错 - -> 本文收录我在开发过程中遇到的各种 maven 问题,持续更新。。。 - -## 问题 - -### IDEA 修改 JDK 版本后编译报错 - -**错误现象** - -修改 JDK 版本,指定 maven-compiler-plugin 的 source 和 target 为 1.8 。 - -然后,在 Intellij IDEA 中执行 maven 指令,报错: - -``` -[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.0:compile (default-compile) on project apollo-common: Fatal error compiling: 无效的目标版本: 1.8 -> [Help 1] -... -``` - -**错误原因** - -maven 的 JDK 源与指定的 JDK 编译版本不符。 - -**排错手段** - -- **查看 Project Settings** - -Project SDK 是否正确 - -![image.png](http://upload-images.jianshu.io/upload_images/3101171-d1d56489ca196361.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -SDK 路径是否正确 - -![image.png](http://upload-images.jianshu.io/upload_images/3101171-6aa4d32f6ea8833a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -2. **查看 Settings > Maven 的配置** - -JDK for importer 是否正确 - -![image.png](http://upload-images.jianshu.io/upload_images/3101171-6f76b18625f7fd1d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -Runner 是否正确 - -![image.png](http://upload-images.jianshu.io/upload_images/3101171-a7973d82931236a4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) diff --git a/docs/javatool/build/maven/maven-pom-01.md b/docs/javatool/build/maven/maven-pom-01.md deleted file mode 100644 index adffb7b0..00000000 --- a/docs/javatool/build/maven/maven-pom-01.md +++ /dev/null @@ -1,227 +0,0 @@ ---- -title: Maven 之 pom.xml 详解(一) -date: 2016/11/10 -categories: -- javatool -tags: -- java -- javatool -- build -- maven ---- - -# Maven 之 pom.xml 详解(一) - -## 简介 - -- [The POM 4.0.0 XSD](http://maven.apache.org/xsd/maven-4.0.0.xsd) and [descriptor reference documentation](http://maven.apache.org/ref/current/maven-model/maven.html) - -### 什么是 pom? - -POM 是 Project Object Model 的缩写,即项目对象模型。 - -pom.xml 就是 maven 的配置文件,用以描述项目的各种信息。 - -### pom 配置一览 - -```xml - - 4.0.0 - - - ... - ... - ... - ... - ... - ... - ... - ... - ... - - - ... - ... - - - ... - ... - ... - ... - ... - ... - ... - ... - - - ... - ... - ... - ... - ... - ... - ... - ... - ... - -``` - -## 基本配置 - -- **project**:`project` 是 pom.xml 中描述符的根。 -- **modelVersion**:`modelVersion` 指定 pom.xml 符合哪个版本的描述符。maven 2 和 3 只能为 4.0.0。 - -一般 jar 包被识别为: `groupId:artifactId:version` 的形式。 - -```xml - - 4.0.0 - - org.codehaus.mojo - my-project - 1.0 - war - -``` - -### maven 坐标 - -在 maven 中,根据 `groupId`、`artifactId`、`version` 组合成 `groupId:artifactId:version` 来唯一识别一个 jar 包。 - -- **groupId**:团体、组织的标识符。团体标识的约定是,它以创建这个项目的组织名称的逆向域名(reverse domain name)开头。一般对应着 java 的包结构。 - -- **artifactId**:单独项目的唯一标识符。比如我们的 tomcat、commons 等。不要在artifactId中包含点号(.)。 - -- **version**:一个项目的特定版本。 - - maven 有自己的版本规范,一般是如下定义 major version、minor version、incremental version-qualifier ,比如1.2.3-beta-01。要说明的是,maven 自己判断版本的算法是 major、minor、incremental 部分用数字比较,qualifier部分用字符串比较,所以要小心 alpha-2 和 alpha-15 的比较关系,最好用 alpha-02 的格式。 - - maven 在版本管理时候可以使用几个特殊的字符串 SNAPSHOT、LATEST、RELEASE。比如 “1.0-SNAPSHOT”。各个部分的含义和处理逻辑如下说明: - - - **SNAPSHOT**:这个版本一般用于开发过程中,表示不稳定的版本。 - - **LATEST**:指某个特定构件的最新发布,这个发布可能是一个发布版,也可能是一个 snapshot 版,具体看哪个时间最后。 - - **RELEASE** :指最后一个发布版。 - -- **packaging**:项目的类型,描述了项目打包后的输出,默认是 jar。常见的输出类型为:pom, jar, maven-plugin, ejb, war, ear, rar, par。 - -## 依赖配置 - -### dependencies - -```xml - - ... - - - org.apache.maven - maven-embedder - 2.0 - jar - test - true - - - org.apache.maven - maven-core - - - - ... - - ... - -``` - -- **groupId**, **artifactId**, **version** :和基本配置中的 `groupId`、`artifactId`、`version` 意义相同。 -- **type**:对应 `packaging` 的类型,如果不使用 `type` 标签,maven 默认为 jar。 -- **scope**:此元素指的是任务的类路径(编译和运行时,测试等)以及如何限制依赖关系的传递性。有 5 种可用的限定范围: - - **compile** - 如果没有指定 `scope` 标签,maven 默认为这个范围。编译依赖关系在所有 classpath 中都可用。此外,这些依赖关系被传播到依赖项目。 - - **provided** - 与 compile 类似,但是表示您希望 jdk 或容器在运行时提供它。它只适用于编译和测试 classpath,不可传递。 - - **runtime** - 此范围表示编译不需要依赖关系,而是用于执行。它是在运行时和测试 classpath,但不是编译 classpath。 - - **test** - 此范围表示正常使用应用程序不需要依赖关系,仅适用于测试编译和执行阶段。它不是传递的。 - - **system** - 此范围与 provided 类似,除了您必须提供明确包含它的 jar。该 artifact 始终可用,并且不是在仓库中查找。 -- **systemPath**:仅当依赖范围是系统时才使用。否则,如果设置此元素,构建将失败。该路径必须是绝对路径,因此建议使用 `propertie` 来指定特定的路径,如$ {java.home} / lib。由于假定先前安装了系统范围依赖关系,maven 将不会检查项目的仓库,而是检查库文件是否存在。如果没有,maven 将会失败,并建议您手动下载安装。 -- **optional**:`optional` 让其他项目知道,当您使用此项目时,您不需要这种依赖性才能正常工作。 -- **exclusions**:包含一个或多个排除元素,每个排除元素都包含一个表示要排除的依赖关系的 `groupId` 和 `artifactId`。与可选项不同,可能或可能不会安装和使用,排除主动从依赖关系树中删除自己。 - -### parent - -maven 支持继承功能。子 POM 可以使用 `parent` 指定父 POM ,然后继承其配置。 - -```xml - - 4.0.0 - - - org.codehaus.mojo - my-parent - 2.0 - ../my-parent - - - my-project - -``` - -- **relativePath**:注意 `relativePath` 元素。在搜索本地和远程存储库之前,它不是必需的,但可以用作 maven 的指示符,以首先搜索给定该项目父级的路径。 - -### dependencyManagement - -`dependencyManagement ` 是表示依赖 jar 包的声明。即你在项目中的 `dependencyManagement` 下声明了依赖,maven不会加载该依赖,`dependencyManagement` 声明可以被子 POM 继承。 - -`dependencyManagement` 的一个使用案例是当有父子项目的时候,父项目中可以利用 `dependencyManagement` 声明子项目中需要用到的依赖 jar 包,之后,当某个或者某几个子项目需要加载该依赖的时候,就可以在子项目中 `dependencies` 节点只配置 `groupId` 和 `artifactId` 就可以完成依赖的引用。 - -`dependencyManagement` 主要是为了统一管理依赖包的版本,确保所有子项目使用的版本一致,类似的还有`plugins`和`pluginManagement`。 - -### modules - -子模块列表。 - -```xml - - 4.0.0 - - org.codehaus.mojo - my-parent - 2.0 - pom - - - my-project - another-project - third-project/pom-example.xml - - -``` - -### properties - -属性列表。定义的属性可以在 pom.xml 文件中任意处使用。使用方式为 `${propertie}` 。 - -```xml - - ... - - 1.7 - 1.7 - UTF-8 - UTF-8 - - ... - -``` diff --git a/docs/javatool/build/maven/maven-pom-02.md b/docs/javatool/build/maven/maven-pom-02.md deleted file mode 100644 index ccc7a192..00000000 --- a/docs/javatool/build/maven/maven-pom-02.md +++ /dev/null @@ -1,267 +0,0 @@ ---- -title: Maven 之 pom.xml 详解(二) -date: 2016/11/10 -categories: -- javatool -tags: -- java -- javatool -- build -- maven ---- - -# Maven 之 pom.xml 详解(二) - -## 构建配置 - -### build - -build 可以分为 "project build" 和 "profile build"。 - -```xml - - ... - - ... - - - - - ... - - - -``` - -基本构建配置: - -```xml - - install - ${basedir}/target - ${artifactId}-${version} - - filters/filter1.properties - - ... - -``` - -**defaultGoal** : 默认执行目标或阶段。如果给出了一个目标,它应该被定义为它在命令行中(如jar:jar)。如果定义了一个阶段(如安装),也是如此。 - -**directory** :构建时的输出路径。默认为:`${basedir}/target` 。 - -**finalName** :这是项目的最终构建名称(不包括文件扩展名,例如:my-project-1.0.jar) - -**filter** :定义 `* .properties` 文件,其中包含适用于接受其设置的资源的属性列表(如下所述)。换句话说,过滤器文件中定义的“name = value”对在代码中替换$ {name}字符串。 - -#### resources - -资源的配置。资源文件通常不是代码,不需要编译,而是在项目需要捆绑使用的内容。 - -```xml - - - ... - - - META-INF/plexus - false - ${basedir}/src/main/plexus - - configuration.xml - - - **/*.properties - - - - - ... - - ... - - -``` - -- **resources**: 资源元素的列表,每个资源元素描述与此项目关联的文件和何处包含文件。 -- **targetPath**: 指定从构建中放置资源集的目录结构。目标路径默认为基本目录。将要包装在 jar 中的资源的通常指定的目标路径是META-INF。 -- **filtering**: 值为 true 或 false。表示是否要为此资源启用过滤。请注意,该过滤器 `* .properties` 文件不必定义为进行过滤 - 资源还可以使用默认情况下在POM中定义的属性(例如$ {project.version}),并将其传递到命令行中“-D”标志(例如,“-Dname = value”)或由properties元素显式定义。过滤文件覆盖上面。 -- **directory**: 值定义了资源的路径。构建的默认目录是`${basedir}/src/main/resources`。 -- **includes**: 一组文件匹配模式,指定目录中要包括的文件,使用*作为通配符。 -- **excludes**: 与 `includes` 类似,指定目录中要排除的文件,使用*作为通配符。注意:如果 `include` 和 `exclude` 发生冲突,maven 会以 `exclude` 作为有效项。 -- **testResources**: `testResources` 与 `resources` 功能类似,区别仅在于:`testResources` 指定的资源仅用于 test 阶段,并且其默认资源目录为:`${basedir}/src/test/resources` 。 - -#### plugins - -```xml - - - ... - - - org.apache.maven.plugins - maven-jar-plugin - 2.6 - false - true - - test - - ... - ... - - - - -``` - -- **groupId**, **artifactId**, **version** :和基本配置中的 `groupId`、`artifactId`、`version` 意义相同。 - -- **extensions** :值为 true 或 false。是否加载此插件的扩展名。默认为false。 - -- **inherited** :值为 true 或 false。这个插件配置是否应该适用于继承自这个插件的 POM。默认值为 true。 - -- **configuration**:这是针对个人插件的配置,这里不扩散讲解。 - -- **dependencies** :这里的 `dependencies` 是插件本身所需要的依赖。 - -- **executions** :需要记住的是,插件可能有多个目标。每个目标可能有一个单独的配置,甚至可能将插件的目标完全绑定到不同的阶段。执行配置插件的目标的执行。 - - - **id**: 执行目标的标识。 - - **goals**: 像所有多元化的 POM 元素一样,它包含单个元素的列表。在这种情况下,这个执行块指定的插件目标列表。 - - **phase**: 这是执行目标列表的阶段。这是一个非常强大的选项,允许将任何目标绑定到构建生命周期中的任何阶段,从而改变 maven 的默认行为。 - - **inherited**: 像上面的继承元素一样,设置这个 false 会阻止 maven 将这个执行传递给它的子代。此元素仅对父 POM 有意义。 - - **configuration**: 与上述相同,但将配置限制在此特定目标列表中,而不是插件下的所有目标。 - -```xml - - ... - - - - maven-antrun-plugin - 1.1 - - - echodir - - run - - verify - false - - - Build Dir: ${project.build.directory} - - - - - - - - - -``` - -#### pluginManagement - -与 `dependencyManagement` 很相似,在当前 POM 中仅声明插件,而不是实际引入插件。子 POM 中只配置 `groupId` 和 `artifactId` 就可以完成插件的引用,且子 POM 有权重写pluginManagement定义。 - -它的目的在于统一所有子 POM 的插件版本。 - -#### directories - -```xml - - ... - - ${basedir}/src/main/java - ${basedir}/src/main/scripts - ${basedir}/src/test/java - ${basedir}/target/classes - ${basedir}/target/test-classes - ... - - -``` - -目录元素集合存在于 `build` 元素中,它为整个 POM 设置了各种目录结构。由于它们在配置文件构建中不存在,所以这些不能由配置文件更改。 - -如果上述目录元素的值设置为绝对路径(扩展属性时),则使用该目录。否则,它是相对于基础构建目录:`${basedir}`。 - -#### extensions - -扩展是在此构建中使用的 artifacts 的列表。它们将被包含在运行构建的 classpath 中。它们可以启用对构建过程的扩展(例如为Wagon传输机制添加一个ftp提供程序),并使活动的插件能够对构建生命周期进行更改。简而言之,扩展是在构建期间激活的 artifacts。扩展不需要实际执行任何操作,也不包含Mojo。因此,扩展对于指定普通插件接口的多个实现中的一个是非常好的。 - -```xml - - ... - - ... - - - org.apache.maven.wagon - wagon-ftp - 1.0-alpha-3 - - - ... - - -``` - -### reporting - -报告包含特定针对 `site` 生成阶段的元素。某些 maven 插件可以生成 `reporting` 元素下配置的报告,例如:生成 javadoc 报告。`reporting` 与 `build` 元素配置插件的能力相似。明显的区别在于:在执行块中插件目标的控制不是细粒度的,报表通过配置 `reportSet` 元素来精细控制。而微妙的区别在于 `reporting` 元素下的 `configuration` 元素可以用作 `build` 下的 `configuration` ,尽管相反的情况并非如此( `build` 下的 `configuration` 不影响 `reporting` 元素下的 `configuration` )。 - -另一个区别就是 `plugin` 下的 `outputDirectory` 元素。在报告的情况下,默认输出目录为 `${basedir}/target/site`。 - -```xml - - ... - - - - ... - - - sunlink - - javadoc - - true - - - http://java.sun.com/j2se/1.5.0/docs/api/ - - - - - - - - ... - -``` diff --git a/docs/javatool/build/maven/maven-pom-03.md b/docs/javatool/build/maven/maven-pom-03.md deleted file mode 100644 index d95a60c7..00000000 --- a/docs/javatool/build/maven/maven-pom-03.md +++ /dev/null @@ -1,334 +0,0 @@ ---- -title: Maven 之 pom.xml 详解(三) -date: 2016/11/10 -categories: -- javatool -tags: -- java -- javatool -- build -- maven ---- - -# Maven 之 pom.xml 详解(三) - -## 项目信息 - -项目信息相关的这部分标签**都不是必要的**,也就是说完全可以不填写。 - -它的作用仅限于描述项目的详细信息。 - -下面的示例是项目信息相关标签的清单: - -```xml - - ... - - - - - maven-notes - - - maven 学习笔记 - - - https://github.com/dunwu/maven-notes - - - 2017 - - - - - Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0.txt - repo - A business-friendly OSS license - - - - - - ... - ... - - - - - - victor - Zhang Peng - forbreak at 163.com - https://github.com/dunwu - ... - ... - - architect - developer - - +8 - ... - - - - - - - - - - - - - ... - -``` - -这部分标签都非常简单,基本都能做到顾名思义,且都属于可有可无的标签,所以这里仅简单介绍一下: - -- **name**:项目完整名称 - -- **description**:项目描述 - -- **url**:一般为项目仓库的 host - -- **inceptionYear**:开发年份 - -- **licenses**:开源协议 - -- **organization**:项目所属组织信息 - -- **developers**:项目开发者列表 - -- **contributors**:项目贡献者列表,`` 的子标签和 `` 的完全相同。 - - -## 环境配置 - -### issueManagement - -这定义了所使用的缺陷跟踪系统(Bugzilla,TestTrack,ClearQuest等)。虽然没有什么可以阻止插件使用这些信息的东西,但它主要用于生成项目文档。 - -```xml - - ... - - Bugzilla - http://127.0.0.1/bugzilla/ - - ... - -``` -### ciManagement - -CI 构建系统配置,主要是指定通知机制以及被通知的邮箱。 - -```xml - - ... - - continuum - http://127.0.0.1:8080/continuum - - - mail - true - true - false - false -
    continuum@127.0.0.1
    -
    -
    -
    - ... -
    -``` - -### mailingLists - -邮件列表 - -```xml - - ... - - - User List - user-subscribe@127.0.0.1 - user-unsubscribe@127.0.0.1 - user@127.0.0.1 - http://127.0.0.1/user/ - - http://base.google.com/base/1/127.0.0.1 - - - - ... - -``` - -### scm - -SCM(软件配置管理,也称为源代码/控制管理或简洁的版本控制)。常见的 scm 有 svn 和 git 。 - -```xml - - ... - - scm:svn:http://127.0.0.1/svn/my-project - scm:svn:https://127.0.0.1/svn/my-project - HEAD - http://127.0.0.1/websvn/my-project - - ... - -``` - -### prerequisites - -POM 执行的预设条件。 - -```xml - - ... - - 2.0.6 - - ... - -``` - -### repositories - -`repositories` 是遵循Maven存储库目录布局的 artifacts 集合。默认的Maven中央存储库位于https://repo.maven.apache.org/maven2/上。 - -```xml - - ... - - - - false - always - warn - - - true - never - fail - - codehausSnapshots - Codehaus Snapshots - http://snapshots.maven.codehaus.org/maven2 - default - - - - ... - - ... - -``` - -### pluginRepositories - -与 `repositories` 差不多。 - -```xml - - ... - - ... - http://mojo.codehaus.org/my-project - deployed - - ... - -``` - -### distributionManagement - -它管理在整个构建过程中生成的 artifact 和支持文件的分布。从最后的元素开始: - -```xml - - ... - - ... - http://mojo.codehaus.org/my-project - deployed - - ... - -``` - -- **repository**:与 `repositories` 相似 - -- **site**:站点信息 - -- **relocation**:项目迁移位置 - -### profiles - -`activation` 是一个 `profile` 的关键。配置文件的功能来自于在某些情况下仅修改基本 POM 的功能。这些情况通过 `activation` 元素指定。 - -```xml - - ... - - - test - - false - 1.5 - - Windows XP - Windows - x86 - 5.1.2600 - - - sparrow-type - African - - - ${basedir}/file2.properties - ${basedir}/file1.properties - - - ... - - - -``` diff --git a/docs/javatool/build/maven/maven-quickstart-01.md b/docs/javatool/build/maven/maven-quickstart-01.md deleted file mode 100644 index c9de36ee..00000000 --- a/docs/javatool/build/maven/maven-quickstart-01.md +++ /dev/null @@ -1,179 +0,0 @@ ---- -title: Maven 快速指南(一) -date: 2016/11/10 -categories: -- javatool -tags: -- java -- javatool -- build -- maven ---- - -# Maven 快速指南(一) - -## 概念 - -### Maven是什么 - -Maven 是一个项目管理工具。它负责管理项目开发过程中的几乎所有的东西。 - -- **版本** maven有自己的版本定义和规则。 - -- **构建** maven支持许多种的应用程序类型,对于每一种支持的应用程序类型都定义好了一组构建规则和工具集。 - -- **输出物管理** maven可以管理项目构建的产物,并将其加入到用户库中。这个功能可以用于项目组和其他部门之间的交付行为。 - -- **依赖关系** maven对依赖关系的特性进行细致的分析和划分,避免开发过程中的依赖混乱和相互污染行为 - -- **文档和构建结果** maven的site命令支持各种文档信息的发布,包括构建过程的各种输出,javadoc,产品文档等。 - -- **项目关系** 一个大型的项目通常有几个小项目或者模块组成,用maven可以很方便地管理。 - -- **移植性管理** maven可以针对不同的开发场景,输出不同种类的输出结果。 - - -### Maven的生命周期 - -maven把项目的构建划分为不同的生命周期(lifecycle)。粗略一点的话,它这个过程(phase)包括:编译、测试、打包、集成测试、验证、部署。maven中所有的执行动作(goal)都需要指明自己在这个过程中的执行位置,然后maven执行的时候,就依照过程的发展依次调用这些goal进行各种处理。 - -这个也是maven的一个基本调度机制。一般来说,位置稍后的过程都会依赖于之前的过程。当然,maven同样提供了配置文件,可以依照用户要求,跳过某些阶段。 - -### Maven的标准工程结构 - -Maven的标准工程结构如下: - -``` -|-- pom.xml(maven的核心配置文件) -|-- src -|-- main - |-- java(java源代码目录) - |-- resources(资源文件目录) -|-- test -    |-- java(单元测试代码目录) -|-- target(输出目录,所有的输出物都存放在这个目录下) -    |-- classes(编译后的class文件存放处) -``` - -### Maven的"约定优于配置" - -所谓的"约定优于配置",在maven中并不是完全不可以修改的,他们只是一些配置的默认值而已。但是除非必要,并不需要去修改那些约定内容。maven默认的文件存放结构如下: - -每一个阶段的任务都知道怎么正确完成自己的工作,比如compile任务就知道从src/main/java下编译所有的java文件,并把它的输出class文件存放到target/classes中。 - -对maven来说,采用"约定优于配置"的策略可以减少修改配置的工作量,也可以降低学习成本,更重要的是,给项目引入了统一的规范。 - -### Maven的版本规范 - -maven使用如下几个要素来唯一定位某一个输出物: - -- **groupId** 团体、组织的标识符。团体标识的约定是,它以创建这个项目的组织名称的逆向域名(reverse domain name)开头。一般对应着JAVA的包的结构。例如org.apache -- **artifactId** 单独项目的唯一标识符。比如我们的tomcat, commons等。不要在artifactId中包含点号(.)。 -- **version** 一个项目的特定版本。 -- **packaging** 项目的类型,默认是jar,描述了项目打包后的输出。类型为jar的项目产生一个JAR文件,类型为war的项目产生一个web应用。 - -maven有自己的版本规范,一般是如下定义 ``、``、`-` ,比如1.2.3-beta-01。要说明的是,maven自己判断版本的算法是major,minor,incremental部分用数字比 较,qualifier部分用字符串比较,所以要小心 alpha-2和alpha-15的比较关系,最好用 alpha-02的格式。 - -maven在版本管理时候可以使用几个特殊的字符串 SNAPSHOT,LATEST,RELEASE。比如"1.0-SNAPSHOT"。各个部分的含义和处理逻辑如下说明: - -- **SNAPSHOT** 这个版本一般用于开发过程中,表示不稳定的版本。 -- **LATEST** 指某个特定构件的最新发布,这个发布可能是一个发布版,也可能是一个snapshot版,具体看哪个时间最后。 -- **RELEASE** 指最后一个发布版。 - -## 安装 - -### 官网下载地址 - -[http://maven.apache.org/download.cgi](http://maven.apache.org/download.cgi) - -### 配置环境变量 - -> **注意:安装maven之前,必须先确保你的机器中已经安装了JDK。** - -1.解压压缩包(以apache-maven-3.3.9-bin.zip为例) - -2.添加环境变量MAVEN_HOME,值为apache-maven-3.3.9的安装路径 - -![img](http://oyz7npk35.bkt.clouddn.com//image/java/libs/maven/maven-quickstart-01.png) - -3.在Path环境变量的变量值末尾添加%MAVEN_HOME%\bin - -4.在cmd输入mvn –version,如果出现maven的版本信息,说明配置成功。 - -![img](http://oyz7npk35.bkt.clouddn.com//image/java/libs/maven/maven-quickstart-02.png) - -### 本地仓储配置 - -从中央仓库下载的jar包,都会统一存放到本地仓库中。我们需要配置本地仓库的位置。 - -打开maven安装目录,打开conf目录下的setting.xml文件。 - -可以参照下图配置本地仓储位置。 - -![img](http://oyz7npk35.bkt.clouddn.com//image/java/libs/maven/maven-quickstart-03.png) - -## 第一个Maven工程 - -### 在Eclipse中创建Maven工程 - -#### Maven插件 - -在Eclipse中创建Maven工程,需要安装Maven插件。 - -一般较新版本的Eclipse都会带有Maven插件,如果你的Eclipse中已经有Maven插件,可以跳过这一步骤。 - -点击Help -> Eclipse Marketplace,搜索maven关键字,选择安装红框对应的Maven插件。 - -![img](http://oyz7npk35.bkt.clouddn.com//image/java/libs/maven/maven-quickstart-04.png) - -#### Maven环境配置 - -点击Window -> Preferences - -如下图所示,配置settings.xml文件的位置 - -![img](http://oyz7npk35.bkt.clouddn.com//image/java/libs/maven/maven-quickstart-05.png) - -#### 创建Maven工程 - -File -> New -> Maven Project -> Next,在接下来的窗口中会看到一大堆的项目模板,选择合适的模板。 - -接下来设置项目的参数,如下: - -![img](http://oyz7npk35.bkt.clouddn.com//image/java/libs/maven/maven-quickstart-06.png) - -**groupId**是项目组织唯一的标识符,实际对应JAVA的包的结构,是main目录里java的目录结构。 - -**artifactId**就是项目的唯一的标识符,实际对应项目的名称,就是项目根目录的名称。 - -点击Finish,Eclipse会创建一个Maven工程。 - -### 使用Maven进行构建 - -**Eclipse中构建方式** - -在Elipse项目上右击 -> Run As 就能看到很多Maven操作。这些操作和maven命令是等效的。例如Maven clean,等同于mvn clean命令。 - -![img](http://oyz7npk35.bkt.clouddn.com//image/java/libs/maven/maven-quickstart-07.png) - -你也可以点击Maven build,输入组合命令,并保存下来。如下图: - -![img](http://oyz7npk35.bkt.clouddn.com//image/java/libs/maven/maven-quickstart-08.png) - -**Maven****命令构建方式** - -当然,你也可以直接使用maven命令进行构建。 - -进入工程所在目录,输入maven命令就可以了。 - -如下图 - -![img](http://oyz7npk35.bkt.clouddn.com//image/java/libs/maven/maven-quickstart-09.png) - -## 参考资料 - -- [官方文档](https://maven.apache.org/index.html) - -- [http://www.oschina.net/question/158170_29368](http://www.oschina.net/question/158170_29368) - -- [http://www.cnblogs.com/crazy-fox/archive/2012/02/09/2343722.html](http://www.cnblogs.com/crazy-fox/archive/2012/02/09/2343722.html) diff --git a/docs/javatool/build/maven/maven-quickstart-02.md b/docs/javatool/build/maven/maven-quickstart-02.md deleted file mode 100644 index ac62fca1..00000000 --- a/docs/javatool/build/maven/maven-quickstart-02.md +++ /dev/null @@ -1,253 +0,0 @@ ---- -title: Maven 快速指南(二) -date: 2016/11/10 -categories: -- javatool -tags: -- java -- javatool -- build -- maven ---- - -# Maven 快速指南(二) - -## 使用指导 - -### 如何添加外部依赖jar包 - -在Maven工程中添加依赖jar包,很简单,只要在POM文件中引入对应的``标签即可。 - -参考下例: - -```xml - - - 4.0.0 - com.zp.maven - MavenDemo - 0.0.1-SNAPSHOT - jar - MavenDemo - http://maven.apache.org - - - UTF-8 - 3.8.1 - - - - - junit - junit - ${junit.version} - test - - - - log4j - log4j - 1.2.12 - compile - - - -``` - -``标签最常用的四个属性标签: - -``:项目组织唯一的标识符,实际对应JAVA的包的结构。 - -``:项目唯一的标识符,实际对应项目的名称,就是项目根目录的名称。 - -``:jar包的版本号。可以直接填版本数字,也可以在properties标签中设置属性值。 - -``:jar包的作用范围。可以填写compile、runtime、test、system和provided。用来在编译、测试等场景下选择对应的classpath。 - -### 如何寻找jar包 - -可以在 [http://mvnrepository.com/](http://mvnrepository.com/) 站点搜寻你想要的jar包版本 - -例如,想要使用log4j,可以找到需要的版本号,然后拷贝对应的maven标签信息,将其添加到pom .xml文件中。 - -![img](http://oyz7npk35.bkt.clouddn.com//image/java/libs/maven/maven-quickstart-10.png) - -### 如何使用Maven插件(Plugin) - -要添加Maven插件,可以在pom.xml文件中添加 `` 标签。 - -```xml - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.3 - - 1.7 - 1.7 - - - - -``` - -`` 标签用来配置插件的一些使用参数。 - -### 如何一次编译多个工程 - -在Maven中,允许一个Maven Project中有多个Maven Module - -1.创建maven父工程步骤:new-->other-->选择maven project-->next-->勾选create a simple project-->next-->填写Group Id、Artifact Id、Version --> packaging选择pom-->finish。 - -![img](http://oyz7npk35.bkt.clouddn.com//image/java/libs/maven/maven-quickstart-11.png) - -2.创建maven子工程步骤:选中刚才创建的父工程右键-->new-->other-->选择maven module-->next-->勾选create a simple project-->填写module name(其实就是artifact id)-->next-->GAV继承父工程-->packaging选择你需要的-->finish。 - -![img](http://oyz7npk35.bkt.clouddn.com//image/java/libs/maven/maven-quickstart-12.png) - -3.完成,刷新父工程;如有多个子工程,继续按照第二步骤创建。 - -![img](http://oyz7npk35.bkt.clouddn.com//image/java/libs/maven/maven-quickstart-13.png) - -这时打开XXX中的pom.xml可以看到其中有以下标签 - -```xml -xxx1 - -``` - -选择编译XXX时,会依次对它的所有Module执行相同操作。 - -### 常用Maven插件 - -#### maven-antrun-plugin - -[http://maven.apache.org/plugins/maven-antrun-plugin/](http://maven.apache.org/plugins/maven-antrun-plugin/) - -maven-antrun-plugin能让用户在Maven项目中运行Ant任务。用户可以直接在该插件的配置以Ant的方式编写Target, 然后交给该插件的run目标去执行。在一些由Ant往Maven迁移的项目中,该插件尤其有用。此外当你发现需要编写一些自定义程度很高的任务,同时又觉 得Maven不够灵活时,也可以以Ant的方式实现之。maven-antrun-plugin的run目标通常与生命周期绑定运行。 - -#### maven-archetype-plugin - -[http://maven.apache.org/archetype/maven-archetype-plugin/](http://maven.apache.org/archetype/maven-archetype-plugin/) - -Archtype指项目的骨架,Maven初学者最开始执行的Maven命令可能就是**mvn archetype:generate**,这实际上就是让maven-archetype-plugin生成一个很简单的项目骨架,帮助开发者快速上手。可能也有人看到一些文档写了**mvn archetype:create**, 但实际上create目标已经被弃用了,取而代之的是generate目标,该目标使用交互式的方式提示用户输入必要的信息以创建项目,体验更好。 maven-archetype-plugin还有一些其他目标帮助用户自己定义项目原型,例如你由一个产品需要交付给很多客户进行二次开发,你就可以为 他们提供一个Archtype,帮助他们快速上手。 - -#### maven-assembly-plugin - -[http://maven.apache.org/plugins/maven-assembly-plugin/](http://maven.apache.org/plugins/maven-assembly-plugin/) - -maven-assembly-plugin的用途是制作项目分发包,该分发包可能包含了项目的可执行文件、源代码、readme、平台脚本等等。 maven-assembly-plugin支持各种主流的格式如zip、tar.gz、jar和war等,具体打包哪些文件是高度可控的,例如用户可以 按文件级别的粒度、文件集级别的粒度、模块级别的粒度、以及依赖级别的粒度控制打包,此外,包含和排除配置也是支持的。maven-assembly- plugin要求用户使用一个名为`assembly.xml`的元数据文件来表述打包,它的single目标可以直接在命令行调用,也可以被绑定至生命周期。 - -#### maven-dependency-plugin - -[http://maven.apache.org/plugins/maven-dependency-plugin/](http://maven.apache.org/plugins/maven-dependency-plugin/) - -maven-dependency-plugin最大的用途是帮助分析项目依赖,**dependency:list**能够列出项目最终解析到的依赖列表,**dependency:tree**能进一步的描绘项目依赖树,**dependency:analyze**可以告诉你项目依赖潜在的问题,如果你有直接使用到的却未声明的依赖,该目标就会发出警告。maven-dependency-plugin还有很多目标帮助你操作依赖文件,例如**dependency:copy-dependencies**能将项目依赖从本地Maven仓库复制到某个特定的文件夹下面。 - -#### maven-enforcer-plugin - -[http://maven.apache.org/plugins/maven-enforcer-plugin/](http://maven.apache.org/plugins/maven-enforcer-plugin/) - -在一个稍大一点的组织或团队中,你无法保证所有成员都熟悉Maven,那他们做一些比较愚蠢的事情就会变得很正常,例如给项目引入了外部的 SNAPSHOT依赖而导致构建不稳定,使用了一个与大家不一致的Maven版本而经常抱怨构建出现诡异问题。maven-enforcer- plugin能够帮助你避免之类问题,它允许你创建一系列规则强制大家遵守,包括设定Java版本、设定Maven版本、禁止某些依赖、禁止 SNAPSHOT依赖。只要在一个父POM配置规则,然后让大家继承,当规则遭到破坏的时候,Maven就会报错。除了标准的规则之外,你还可以扩展该插 件,编写自己的规则。maven-enforcer-plugin的enforce目标负责检查规则,它默认绑定到生命周期的validate阶段。 - -#### maven-help-plugin - -[http://maven.apache.org/plugins/maven-help-plugin/](http://maven.apache.org/plugins/maven-help-plugin/) -maven-help-plugin是一个小巧的辅助工具,最简单的**help:system**可以打印所有可用的环境变量和Java系统属性。**help:effective-pom**和**help:effective-settings**最 为有用,它们分别打印项目的有效POM和有效settings,有效POM是指合并了所有父POM(包括Super POM)后的XML,当你不确定POM的某些信息从何而来时,就可以查看有效POM。有效settings同理,特别是当你发现自己配置的 settings.xml没有生效时,就可以用**help:effective-settings**来验证。此外,maven-help-plugin的describe目标可以帮助你描述任何一个Maven插件的信息,还有all-profiles目标和active-profiles目标帮助查看项目的Profile。 - -#### maven-release-plugin - -[http://maven.apache.org/plugins/maven-release-plugin/](http://maven.apache.org/plugins/maven-release-plugin/) - -maven-release-plugin的用途是帮助自动化项目版本发布,它依赖于POM中的SCM信息。**release:prepare**用来准备版本发布,具体的工作包括检查是否有未提交代码、检查是否有SNAPSHOT依赖、升级项目的SNAPSHOT版本至RELEASE版本、为项目打标签等等。**release:perform**则 是签出标签中的RELEASE源码,构建并发布。版本发布是非常琐碎的工作,它涉及了各种检查,而且由于该工作仅仅是偶尔需要,因此手动操作很容易遗漏一 些细节,maven-release-plugin让该工作变得非常快速简便,不易出错。maven-release-plugin的各种目标通常直接在 命令行调用,因为版本发布显然不是日常构建生命周期的一部分。 - -#### maven-resources-plugin - -[http://maven.apache.org/plugins/maven-resources-plugin/](http://maven.apache.org/plugins/maven-resources-plugin/) - -为了使项目结构更为清晰,Maven区别对待Java代码文件和资源文件,maven-compiler-plugin用来编译Java代码,maven-resources-plugin则用来处理资源文件。默认的主资源文件目录是`src/main/resources`,很多用户会需要添加额外的资源文件目录,这个时候就可以通过配置maven-resources-plugin来实现。此外,资源文件过滤也是Maven的一大特性,你可以在资源文件中使用*${propertyName}*形式的Maven属性,然后配置maven-resources-plugin开启对资源文件的过滤,之后就可以针对不同环境通过命令行或者Profile传入属性的值,以实现更为灵活的构建。 - -#### maven-surefire-plugin - -[http://maven.apache.org/plugins/maven-surefire-plugin/](http://maven.apache.org/plugins/maven-surefire-plugin/) - -可能是由于历史的原因,Maven 2/3中用于执行测试的插件不是maven-test-plugin,而是maven-surefire-plugin。其实大部分时间内,只要你的测试 类遵循通用的命令约定(以Test结尾、以TestCase结尾、或者以Test开头),就几乎不用知晓该插件的存在。然而在当你想要跳过测试、排除某些 测试类、或者使用一些TestNG特性的时候,了解maven-surefire-plugin的一些配置选项就很有用了。例如 **mvn test -Dtest=FooTest** 这样一条命令的效果是仅运行FooTest测试类,这是通过控制maven-surefire-plugin的test参数实现的。 - -#### build-helper-maven-plugin - -[http://mojo.codehaus.org/build-helper-maven-plugin/](http://mojo.codehaus.org/build-helper-maven-plugin/) - -Maven默认只允许指定一个主Java代码目录和一个测试Java代码目录,虽然这其实是个应当尽量遵守的约定,但偶尔你还是会希望能够指定多个 源码目录(例如为了应对遗留项目),build-helper-maven-plugin的add-source目标就是服务于这个目的,通常它被绑定到 默认生命周期的generate-sources阶段以添加额外的源码目录。需要强调的是,这种做法还是不推荐的,因为它破坏了 Maven的约定,而且可能会遇到其他严格遵守约定的插件工具无法正确识别额外的源码目录。 - -build-helper-maven-plugin的另一个非常有用的目标是attach-artifact,使用该目标你可以以classifier的形式选取部分项目文件生成附属构件,并同时install到本地仓库,也可以deploy到远程仓库。 - -#### exec-maven-plugin - -[http://mojo.codehaus.org/exec-maven-plugin/](http://mojo.codehaus.org/exec-maven-plugin/) - -exec-maven-plugin很好理解,顾名思义,它能让你运行任何本地的系统程序,在某些特定情况下,运行一个Maven外部的程序可能就是最简单的问题解决方案,这就是**exec:exec**的 用途,当然,该插件还允许你配置相关的程序运行参数。除了exec目标之外,exec-maven-plugin还提供了一个java目标,该目标要求你 提供一个mainClass参数,然后它能够利用当前项目的依赖作为classpath,在同一个JVM中运行该mainClass。有时候,为了简单的 演示一个命令行Java程序,你可以在POM中配置好exec-maven-plugin的相关运行参数,然后直接在命令运行**mvn exec:java** 以查看运行效果。 - -#### jetty-maven-plugin - -[http://wiki.eclipse.org/Jetty/Feature/Jetty_Maven_Plugin](http://wiki.eclipse.org/Jetty/Feature/Jetty_Maven_Plugin) - -在进行Web开发的时候,打开浏览器对应用进行手动的测试几乎是无法避免的,这种测试方法通常就是将项目打包成war文件,然后部署到Web容器 中,再启动容器进行验证,这显然十分耗时。为了帮助开发者节省时间,jetty-maven-plugin应运而生,它完全兼容 Maven项目的目录结构,能够周期性地检查源文件,一旦发现变更后自动更新到内置的Jetty Web容器中。做一些基本配置后(例如Web应用的contextPath和自动扫描变更的时间间隔),你只要执行 **mvn jetty:run** ,然后在IDE中修改代码,代码经IDE自动编译后产生变更,再由jetty-maven-plugin侦测到后更新至Jetty容器,这时你就可以直接 测试Web页面了。需要注意的是,jetty-maven-plugin并不是宿主于Apache或Codehaus的官方插件,因此使用的时候需要额外 的配置`settings.xml`的pluginGroups元素,将org.mortbay.jetty这个pluginGroup加入。 - -#### versions-maven-plugin - -[http://mojo.codehaus.org/versions-maven-plugin/](http://mojo.codehaus.org/versions-maven-plugin/) - -很多Maven用户遇到过这样一个问题,当项目包含大量模块的时候,为他们集体更新版本就变成一件烦人的事情,到底有没有自动化工具能帮助完成这件 事情呢?(当然你可以使用sed之类的文本操作工具,不过不在本文讨论范围)答案是肯定的,versions-maven- plugin提供了很多目标帮助你管理Maven项目的各种版本信息。例如最常用的,命令 **mvn versions:set -DnewVersion=1.1-SNAPSHOT** 就能帮助你把所有模块的版本更新到1.1-SNAPSHOT。该插件还提供了其他一些很有用的目标,display-dependency- updates能告诉你项目依赖有哪些可用的更新;类似的display-plugin-updates能告诉你可用的插件更新;然后use- latest-versions能自动帮你将所有依赖升级到最新版本。最后,如果你对所做的更改满意,则可以使用 **mvn versions:commit** 提交,不满意的话也可以使用 **mvn versions:revert** 进行撤销。 - -更多详情请参考[https://maven.apache.org/plugins/](https://maven.apache.org/plugins/) - -### 常用Maven命令 - -| **生命周期** | **阶段描述** | -| --------------------------- | ---------------------------------------- | -| mvn validate | 验证项目是否正确,以及所有为了完整构建必要的信息是否可用 | -| mvn generate-sources | 生成所有需要包含在编译过程中的源代码 | -| mvn process-sources | 处理源代码,比如过滤一些值 | -| mvn generate-resources | 生成所有需要包含在打包过程中的资源文件 | -| mvn process-resources | 复制并处理资源文件至目标目录,准备打包 | -| mvn compile | 编译项目的源代码 | -| mvn process-classes | 后处理编译生成的文件,例如对Java类进行字节码增强(bytecode enhancement) | -| mvn generate-test-sources | 生成所有包含在测试编译过程中的测试源码 | -| mvn process-test-sources | 处理测试源码,比如过滤一些值 | -| mvn generate-test-resources | 生成测试需要的资源文件 | -| mvn process-test-resources | 复制并处理测试资源文件至测试目标目录 | -| mvn test-compile | 编译测试源码至测试目标目录 | -| mvn test | 使用合适的单元测试框架运行测试。这些测试应该不需要代码被打包或发布 | -| mvn prepare-package | 在真正的打包之前,执行一些准备打包必要的操作。这通常会产生一个包的展开的处理过的版本(将会在Maven 2.1+中实现) | -| mvn package | 将编译好的代码打包成可分发的格式,如JAR,WAR,或者EAR | -| mvn pre-integration-test | 执行一些在集成测试运行之前需要的动作。如建立集成测试需要的环境 | -| mvn integration-test | 如果有必要的话,处理包并发布至集成测试可以运行的环境 | -| mvn post-integration-test | 执行一些在集成测试运行之后需要的动作。如清理集成测试环境。 | -| mvn verify | 执行所有检查,验证包是有效的,符合质量规范 | -| mvn install | 安装包至本地仓库,以备本地的其它项目作为依赖使用 | -| mvn deploy | 复制最终的包至远程仓库,共享给其它开发人员和项目(通常和一次正式的发布相关) | - -**使用参数** - -`-Dmaven.test.skip=true`: 跳过单元测试(eg: mcn clean package -Dmaven.test.skip=true) - -## 常见问题 - -dependencies和dependencyManagement,plugins和pluginManagement有什么区别? - -dependencyManagement是表示依赖jar包的声明,即你在项目中的dependencyManagement下声明了依赖,maven不会加载该依赖,dependencyManagement声明可以被继承。 - -dependencyManagement的一个使用案例是当有父子项目的时候,父项目中可以利用dependencyManagement声明子项目中需要用到的依赖jar包,之后,当某个或者某几个子项目需要加载该插件的时候,就可以在子项目中dependencies节点只配置 groupId 和 artifactId就可以完成插件的引用。 - -dependencyManagement主要是为了统一管理插件,确保所有子项目使用的插件版本保持一致,类似的还有plugins和pluginManagement。 - -## 参考资料 - -- [官方文档](https://maven.apache.org/index.html) - -- [http://www.oschina.net/question/158170_29368](http://www.oschina.net/question/158170_29368) - -- [http://www.cnblogs.com/crazy-fox/archive/2012/02/09/2343722.html](http://www.cnblogs.com/crazy-fox/archive/2012/02/09/2343722.html) diff --git a/docs/javatool/build/maven/maven.xmind b/docs/javatool/build/maven/maven.xmind deleted file mode 100644 index 42876603..00000000 Binary files a/docs/javatool/build/maven/maven.xmind and /dev/null differ diff --git a/docs/javatool/elastic/README.md b/docs/javatool/elastic/README.md deleted file mode 100644 index 6bebd4ed..00000000 --- a/docs/javatool/elastic/README.md +++ /dev/null @@ -1,41 +0,0 @@ -# Elastic 技术栈 - -> **Elastic 技术栈通常被用来作为日志中心。** -> -> ELK 是 elastic 公司旗下三款产品 [ElasticSearch](https://www.elastic.co/products/elasticsearch) 、[Logstash](https://www.elastic.co/products/logstash) 、[Kibana](https://www.elastic.co/products/kibana) 的首字母组合。 -> -> [ElasticSearch](https://www.elastic.co/products/elasticsearch) 是一个基于 [Lucene](http://lucene.apache.org/core/documentation.html) 构建的开源,分布式,RESTful 搜索引擎。 -> -> [Logstash](https://www.elastic.co/products/logstash) 传输和处理你的日志、事务或其他数据。 -> -> [Kibana](https://www.elastic.co/products/kibana) 将 Elasticsearch 的数据分析并渲染为可视化的报表。 -> -> Elastic 技术栈,在 ELK 的基础上扩展了一些新的产品,如:[Beats](https://www.elastic.co/products/beats) 、[X-Pack](https://www.elastic.co/products/x-pack) 。 - -## 目录 - -[Elastic 技术栈之快速指南](elastic-quickstart.md) - -[Elastic 技术栈之 Logstash 基础](elastic-logstash.md) - -## 资源 - -**官方资源** - -[Elastic 官方文档](https://www.elastic.co/guide/index.html) - -**第三方工具** - -[logstash-logback-encoder](https://github.com/logstash/logstash-logback-encoder) - -**教程** - -[Elasticsearch 权威指南(中文版)](https://es.xiaoleilu.com/index.html) - -[ELK Stack权威指南](https://github.com/chenryn/logstash-best-practice-cn) - -**博文** - -[Elasticsearch+Logstash+Kibana教程](https://www.cnblogs.com/xing901022/p/4704319.html) - -[ELK(Elasticsearch、Logstash、Kibana)安装和配置](https://github.com/judasn/Linux-Tutorial/blob/master/ELK-Install-And-Settings.md) diff --git a/docs/javatool/elastic/elastic-beats.md b/docs/javatool/elastic/elastic-beats.md deleted file mode 100644 index 43380e96..00000000 --- a/docs/javatool/elastic/elastic-beats.md +++ /dev/null @@ -1,296 +0,0 @@ ---- -title: Elastic 技术栈之 Filebeat -date: 2017-01-03 -categories: -- javatool -tags: -- java -- javatool -- log -- elastic ---- - -# Elastic 技术栈之 Filebeat - -## 简介 - -Beats 是安装在服务器上的数据中转代理。 - -Beats 可以将数据直接传输到 Elasticsearch 或传输到 Logstash 。 - -![Beats Platform](https://www.elastic.co/guide/en/beats/libbeat/current/images/beats-platform.png) - -Beats 有多种类型,可以根据实际应用需要选择合适的类型。 - -常用的类型有: - -- **Packetbeat:**网络数据包分析器,提供有关您的应用程序服务器之间交换的事务的信息。 -- **Filebeat:**从您的服务器发送日志文件。 -- **Metricbeat:**是一个服务器监视代理程序,它定期从服务器上运行的操作系统和服务收集指标。 -- **Winlogbeat:**提供Windows事件日志。 - -> **参考** -> -> 更多 Beats 类型可以参考:[community-beats](https://www.elastic.co/guide/en/beats/libbeat/current/community-beats.html) -> -> **说明** -> -> 由于本人工作中只应用了 FileBeat,所以后面内容仅介绍 FileBeat 。 - -### FileBeat 的作用 - -相比 Logstash,FileBeat 更加轻量化。 - -在任何环境下,应用程序都有停机的可能性。 Filebeat 读取并转发日志行,如果中断,则会记住所有事件恢复联机状态时所在位置。 - -Filebeat带有内部模块(auditd,Apache,Nginx,System和MySQL),可通过一个指定命令来简化通用日志格式的收集,解析和可视化。 - -FileBeat 不会让你的管道超负荷。FileBeat 如果是向 Logstash 传输数据,当 Logstash 忙于处理数据,会通知 FileBeat 放慢读取速度。一旦拥塞得到解决,FileBeat 将恢复到原来的速度并继续传播。 - -![Beats design](https://www.elastic.co/guide/en/beats/filebeat/current/images/filebeat.png) - -## 安装 - -Unix / Linux 系统建议使用下面方式安装,因为比较通用。 - -``` -wget https://artifacts.elastic.co/downloads/beats/filebeat/filebeat-6.1.1-linux-x86_64.tar.gz -tar -zxf filebeat-6.1.1-linux-x86_64.tar.gz -``` - -> **参考** -> -> 更多内容可以参考:[filebeat-installation](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-installation.html) - -## 配置 - -### 配置文件 - -首先,需要知道的是:`filebeat.yml` 是 filebeat 的配置文件。配置文件的路径会因为你安装方式的不同而变化。 - -Beat 所有系列产品的配置文件都基于 [YAML](http://www.yaml.org/) 格式,FileBeat 当然也不例外。 - -filebeat.yml 部分配置示例: - -```yaml -filebeat: - prospectors: - - type: log - paths: - - /var/log/*.log - multiline: - pattern: '^[' - match: after -``` - -> **参考** -> -> 更多 filebeat 配置内容可以参考:[配置 filebeat](https://www.elastic.co/guide/en/beats/filebeat/current/configuring-howto-filebeat.html) -> -> 更多 filebeat.yml 文件格式内容可以参考:[filebeat.yml 文件格式](https://www.elastic.co/guide/en/beats/libbeat/6.1/config-file-format.html) - -### 重要配置项 - -#### filebeat.prospectors - -(文件监视器)用于指定需要关注的文件。 - -**示例** - -```yaml -filebeat.prospectors: -- type: log - enabled: true - paths: - - /var/log/*.log -``` - -#### output.elasticsearch - -如果你希望使用 filebeat 直接向 elasticsearch 输出数据,需要配置 output.elasticsearch 。 - -**示例** - -```yaml -output.elasticsearch: - hosts: ["192.168.1.42:9200"] -``` - -#### output.logstash - -如果你希望使用 filebeat 向 logstash输出数据,然后由 logstash 再向elasticsearch 输出数据,需要配置 output.logstash。 - -> **注意** -> -> 相比于向 elasticsearch 输出数据,个人更推荐向 logstash 输出数据。 -> -> 因为 logstash 和 filebeat 一起工作时,如果 logstash 忙于处理数据,会通知 FileBeat 放慢读取速度。一旦拥塞得到解决,FileBeat 将恢复到原来的速度并继续传播。这样,可以减少管道超负荷的情况。 - -**示例** - -```yaml -output.logstash: - hosts: ["127.0.0.1:5044"] -``` - -此外,还需要在 logstash 的配置文件(如 logstash.conf)中指定 beats input 插件: - -```yaml -input { - beats { - port => 5044 # 此端口需要与 filebeat.yml 中的端口相同 - } -} - -# The filter part of this file is commented out to indicate that it is -# optional. -# filter { -# -# } - -output { - elasticsearch { - hosts => "localhost:9200" - manage_template => false - index => "%{[@metadata][beat]}-%{[@metadata][version]}-%{+YYYY.MM.dd}" - document_type => "%{[@metadata][type]}" - } -} -``` - -#### setup.kibana - -如果打算使用 Filebeat 提供的 Kibana 仪表板,需要配置 setup.kibana 。 - -**示例** - -```yaml -setup.kibana: - host: "localhost:5601" -``` - -#### setup.template.settings - -在 Elasticsearch 中,[索引模板](https://www.elastic.co/guide/en/elasticsearch/reference/6.1/indices-templates.html)用于定义设置和映射,以确定如何分析字段。 - -在 Filebeat 中,setup.template.settings 用于配置索引模板。 - -Filebeat 推荐的索引模板文件由 Filebeat 软件包安装。如果您接受 filebeat.yml 配置文件中的默认配置,Filebeat在成功连接到 Elasticsearch 后自动加载模板。 - -您可以通过在 Filebeat 配置文件中配置模板加载选项来禁用自动模板加载,或加载自己的模板。您还可以设置选项来更改索引和索引模板的名称。 - -> **参考** -> -> 更多内容可以参考:[filebeat-template](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-template.html) -> -> **说明** -> -> 如无必要,使用 Filebeat 配置文件中的默认索引模板即可。 - -#### setup.dashboards - -Filebeat 附带了示例 Kibana 仪表板。在使用仪表板之前,您需要创建索引模式 `filebeat- *`,并将仪表板加载到Kibana 中。为此,您可以运行 `setup` 命令或在 `filebeat.yml` 配置文件中配置仪表板加载。 - -为了在 Kibana 中加载 Filebeat 的仪表盘,需要在 `filebeat.yml` 配置中启动开关: - -``` -setup.dashboards.enabled: true -``` - -> **参考** -> -> 更多内容可以参考:[configuration-dashboards](https://www.elastic.co/guide/en/beats/filebeat/current/configuration-dashboards.html) -> - -## 命令 - -filebeat 提供了一系列命令来完成各种功能。 - -执行命令方式: - -```sh -./filebeat COMMAND -``` - -> **参考** -> -> 更多内容可以参考:[command-line-options](https://www.elastic.co/guide/en/beats/filebeat/current/command-line-options.html) -> -> **说明** -> -> 个人认为命令行没有必要一一掌握,因为绝大部分功能都可以通过配置来完成。且通过命令行指定功能这种方式要求每次输入同样参数,不利于固化启动方式。 -> -> 最重要的当然是启动命令 run 了。 -> -> **示例** 指定配置文件启动 -> -> ```sh -> ./filebeat run -e -c filebeat.yml -d "publish" -> ./filebeat -e -c filebeat.yml -d "publish" # run 可以省略 -> ``` - -## 模块 - -Filebeat 提供了一套预构建的模块,让您可以快速实施和部署日志监视解决方案,并附带示例仪表板和数据可视化。这些模块支持常见的日志格式,例如Nginx,Apache2和MySQL 等。 - -### 运行模块的步骤 - -- 配置 elasticsearch 和 kibana - -``` -output.elasticsearch: - hosts: ["myEShost:9200"] - username: "elastic" - password: "elastic" -setup.kibana: - host: "mykibanahost:5601" - username: "elastic" - password: "elastic -``` - -> username 和 password 是可选的,如果不需要认证则不填。 - -- 初始化环境 - -执行下面命令,filebeat 会加载推荐索引模板。 - -``` -./filebeat setup -e -``` - -- 指定模块 - -执行下面命令,指定希望加载的模块。 - -``` -./filebeat -e --modules system,nginx,mysql -``` - -> **参考** -> -> 更多内容可以参考: [配置 filebeat 模块](https://www.elastic.co/guide/en/beats/filebeat/current/configuration-filebeat-modules.html) | [filebeat 支持模块](https://www.elastic.co/guide/en/beats/filebeat/current/filebeat-modules.html) - -## 原理 - -Filebeat 有两个主要组件: - -harvester:负责读取一个文件的内容。它会逐行读取文件内容,并将内容发送到输出目的地。 - -prospector:负责管理 harvester 并找到所有需要读取的文件源。比如类型是日志,prospector 就会遍历制定路径下的所有匹配要求的文件。 - -```yaml -filebeat.prospectors: -- type: log - paths: - - /var/log/*.log - - /var/path2/*.log -``` - -Filebeat保持每个文件的状态,并经常刷新注册表文件中的磁盘状态。状态用于记住 harvester 正在读取的最后偏移量,并确保发送所有日志行。 - -Filebeat 将每个事件的传递状态存储在注册表文件中。所以它能保证事件至少传递一次到配置的输出,没有数据丢失。 - -## 资料 - -[Beats 官方文档](https://www.elastic.co/guide/en/beats/libbeat/current/index.html) - diff --git a/docs/javatool/elastic/elastic-kibana.md b/docs/javatool/elastic/elastic-kibana.md deleted file mode 100644 index be50fb4b..00000000 --- a/docs/javatool/elastic/elastic-kibana.md +++ /dev/null @@ -1,307 +0,0 @@ -# Elastic 技术栈之 Kibana - -## Discover - -单击侧面导航栏中的 `Discover` ,可以显示 `Kibana` 的数据查询功能功能。 - -![https://www.elastic.co/guide/en/kibana/current/images/tutorial-discover.png](https://www.elastic.co/guide/en/kibana/current/images/tutorial-discover.png) - -在搜索栏中,您可以输入Elasticsearch查询条件来搜索您的数据。您可以在 `Discover` 页面中浏览结果并在 `Visualize` 页面中创建已保存搜索条件的可视化。 - -当前索引模式显示在查询栏下方。索引模式确定提交查询时搜索哪些索引。要搜索一组不同的索引,请从下拉菜单中选择不同的模式。要添加索引模式(index pattern),请转至 `Management/Kibana/Index Patterns` 并单击 `Add New`。 - -您可以使用字段名称和您感兴趣的值构建搜索。对于数字字段,可以使用比较运算符,如大于(>),小于(<)或等于(=)。您可以将元素与逻辑运算符 `AND`,`OR` 和 `NOT` 链接,全部使用大写。 - -默认情况下,每个匹配文档都显示所有字段。要选择要显示的文档字段,请将鼠标悬停在“可用字段”列表上,然后单击要包含的每个字段旁边的添加按钮。例如,如果只添加account_number,则显示将更改为包含五个帐号的简单列表: - -![https://www.elastic.co/guide/en/kibana/6.1/images/tutorial-discover-3.png](https://www.elastic.co/guide/en/kibana/6.1/images/tutorial-discover-3.png) - -### 查询语义 - -kibana 的搜索栏遵循 [query-string-syntax](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax) 文档中所说明的查询语义。 - -这里说明一些最基本的查询语义。 - -查询字符串会被解析为一系列的术语和运算符。一个术语可以是一个单词(如:quick、brown)或用双引号包围的短语(如"quick brown")。 - -查询操作允许您自定义搜索 - 下面介绍了可用的选项。 - -#### 字段名称 - -正如查询字符串查询中所述,将在搜索条件中搜索default_field,但可以在查询语法中指定其他字段: - -例如: - -* 查询 `status` 字段中包含 `active` 关键字 - -``` -status:active -``` - -* `title` 字段包含 `quick` 或 `brown` 关键字。如果您省略 `OR` 运算符,则将使用默认运算符 - -``` -title:(quick OR brown) -title:(quick brown) -``` - -* author 字段查找精确的短语 "john smith",即精确查找。 - -``` -author:"John Smith" -``` - -* 任意字段 `book.title`,`book.content` 或 `book.date` 都包含 `quick` 或 `brown`(注意我们需要如何使用 `\*` 表示通配符) - -``` -book.\*:(quick brown) -``` - -* title 字段包含任意非 null 值 - -``` -_exists_:title -``` - -#### 通配符 - -ELK 提供了 ? 和 * 两个通配符。 - -* `?` 表示任意单个字符; -* `*` 表示任意零个或多个字符。 - -``` -qu?ck bro* -``` - -> **注意:通配符查询会使用大量的内存并且执行性能较为糟糕,所以请慎用。** -> **提示**:纯通配符 \* 被写入 [exsits](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html) 查询,从而提高了查询效率。因此,通配符 `field:*` 将匹配包含空值的文档,如:```{“field”:“”}```,但是如果字段丢失或显示将值置为null则不匹配,如:```“field”:null}``` -> **提示**:在一个单词的开头(例如:`*ing`)使用通配符这种方式的查询量特别大,因为索引中的所有术语都需要检查,以防万一匹配。通过将 `allow_leading_wildcard` 设置为 `false`,可以禁用。 - -#### 正则表达式 - -可以通过 `/` 将正则表达式包裹在查询字符串中进行查询 - -例: - -``` -name:/joh?n(ath[oa]n)/ -``` - -支持的正则表达式语义可以参考:[Regular expression syntax](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#regexp-syntax) - -#### 模糊查询 - -我们可以使用 `~` 运算符来进行模糊查询。 - -例: - -假设我们实际想查询 - -``` -quick brown forks -``` - -但是,由于拼写错误,我们的查询关键字变成如下情况,依然可以查到想要的结果。 - -``` -quikc~ brwn~ foks~ -``` - -这种模糊查询使用 Damerau-Levenshtein 距离来查找所有匹配最多两个更改的项。所谓的更改是指单个字符的插入,删除或替换,或者两个相邻字符的换位。 - -默认编辑距离为 `2`,但编辑距离为 `1` 应足以捕捉所有人类拼写错误的80%。它可以被指定为: - -``` -quikc~1 -``` - -#### 近似检索 - -尽管短语查询(例如,`john smith`)期望所有的词条都是完全相同的顺序,但是近似查询允许指定的单词进一步分开或以不同的顺序排列。与模糊查询可以为单词中的字符指定最大编辑距离一样,近似搜索也允许我们指定短语中单词的最大编辑距离: - -例 - -``` -"fox quick"~5 -``` - -字段中的文本越接近查询字符串中指定的原始顺序,该文档就越被认为是相关的。当与上面的示例查询相比时,短语 `"quick fox"` 将被认为比 `"quick brown fox"` 更近似查询条件。 - -#### 范围 - -可以为日期,数字或字符串字段指定范围。闭区间范围用方括号 `[min TO max]` 和开区间范围用花括号 `{min TO max}` 来指定。 - -我们不妨来看一些示例。 - -* 2012 年的所有日子 - -``` -date:[2012-01-01 TO 2012-12-31] -``` - -* 数字 1 到 5 - -``` -count:[1 TO 5] -``` - -* 在 `alpha` 和 `omega` 之间的标签,不包括 `alpha` 和 `omega` - -``` -tag:{alpha TO omega} -``` - -* 10 以上的数字 - -``` -count:[10 TO *] -``` - -* 2012 年以前的所有日期 - -``` -date:{* TO 2012-01-01} -``` - -此外,开区间和闭区间也可以组合使用 - -* 数组 1 到 5,但不包括 5 - -``` -count:[1 TO 5} -``` - -一边无界的范围也可以使用以下语法: - -``` -age:>10 -age:>=10 -age:<10 -age:<=10 -``` - -当然,你也可以使用 AND 运算符来得到连个查询结果的交集 - -``` -age:(>=10 AND <20) -age:(+>=10 +<20) -``` - -#### Boosting - -使用操作符 `^` 使一个术语比另一个术语更相关。例如,如果我们想查找所有有关狐狸的文档,但我们对狐狸特别感兴趣: - -``` -quick^2 fox -``` - -默认提升值是1,但可以是任何正浮点数。 0到1之间的提升减少了相关性。 - -增强也可以应用于短语或组: - -``` -"john smith"^2 (foo bar)^4 -``` - -#### 布尔操作 - -默认情况下,只要一个词匹配,所有词都是可选的。搜索 `foo bar baz` 将查找包含 `foo` 或 `bar` 或 `baz` 中的一个或多个的任何文档。我们已经讨论了上面的`default_operator`,它允许你强制要求所有的项,但也有布尔运算符可以在查询字符串本身中使用,以提供更多的控制。 - -首选的操作符是 `+`(此术语必须存在)和 `-` (此术语不得存在)。所有其他条款是可选的。例如,这个查询: - -``` -quick brown +fox -news -``` - -这条查询意味着: - -* fox 必须存在 -* news 必须不存在 -* quick 和 brown 是可有可无的 - -熟悉的运算符 `AND`,`OR` 和 `NOT`(也写成 `&&`,`||` 和 `!`)也被支持。然而,这些操作符有一定的优先级:`NOT` 优先于 `AND`,`AND` 优先于 `OR`。虽然 `+` 和 `-` 仅影响运算符右侧的术语,但 `AND` 和 `OR` 会影响左侧和右侧的术语。 - -#### 分组 - -多个术语或子句可以用圆括号组合在一起,形成子查询 - -``` -(quick OR brown) AND fox -``` - -可以使用组来定位特定的字段,或者增强子查询的结果: - -``` -status:(active OR pending) title:(full text search)^2 -``` - -#### 保留字 - -如果你需要使用任何在你的查询本身中作为操作符的字符(而不是作为操作符),那么你应该用一个反斜杠来转义它们。例如,要搜索(1 + 1)= 2,您需要将查询写为 `\(1\+1\)\=2` - -保留字符是:`+ - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ /` - -无法正确地转义这些特殊字符可能会导致语法错误,从而阻止您的查询运行。 - -#### 空查询 - -如果查询字符串为空或仅包含空格,则查询将生成一个空的结果集。 - -## Visualize - -要想使用可视化的方式展示您的数据,请单击侧面导航栏中的 `Visualize`。 - -Visualize工具使您能够以多种方式(如饼图、柱状图、曲线图、分布图等)查看数据。要开始使用,请点击蓝色的 `Create a visualization` 或 `+` 按钮。 - -![https://www.elastic.co/guide/en/kibana/6.1/images/tutorial-visualize-landing.png](https://www.elastic.co/guide/en/kibana/6.1/images/tutorial-visualize-landing.png) - -有许多可视化类型可供选择。 - -![https://www.elastic.co/guide/en/kibana/6.1/images/tutorial-visualize-wizard-step-1.png](https://www.elastic.co/guide/en/kibana/6.1/images/tutorial-visualize-wizard-step-1.png) - -下面,我们来看创建几个图标示例: - -### Pie - -您可以从保存的搜索中构建可视化文件,也可以输入新的搜索条件。要输入新的搜索条件,首先需要选择一个索引模式来指定要搜索的索引。 - -默认搜索匹配所有文档。最初,一个“切片”包含整个饼图: - -![https://www.elastic.co/guide/en/kibana/6.1/images/tutorial-visualize-pie-1.png](https://www.elastic.co/guide/en/kibana/6.1/images/tutorial-visualize-pie-1.png) - -要指定在图表中展示哪些数据,请使用Elasticsearch存储桶聚合。分组汇总只是将与您的搜索条件相匹配的文档分类到不同的分类中,也称为分组。 - -为每个范围定义一个存储桶: - -1. 单击 `Split Slices`。 -2. 在 `Aggregation` 列表中选择 `Terms`。_注意:这里的 Terms 是 Elk 采集数据时定义好的字段或标签。_ -3. 在 `Field` 列表中选择 `level.keyword`。 -4. 点击 ![images/apply-changes-button.png](https://www.elastic.co/guide/en/kibana/6.1/images/apply-changes-button.png) 按钮来更新图表。 - -![image.png](https://upload-images.jianshu.io/upload_images/3101171-7fb2042dc6d59520.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -完成后,如果想要保存这个图表,可以点击页面最上方一栏中的 `Save` 按钮。 - -### Vertical Bar - -我们在展示一下如何创建柱状图。 - -1. 点击蓝色的 `Create a visualization` 或 `+` 按钮。选择 `Vertical Bar` -2. 选择索引模式。由于您尚未定义任何 bucket ,因此您会看到一个大栏,显示与默认通配符查询匹配的文档总数。 -3. 指定 Y 轴所代表的字段 -4. 指定 X 轴所代表的字段 -5. 点击 ![images/apply-changes-button.png](https://www.elastic.co/guide/en/kibana/6.1/images/apply-changes-button.png) 按钮来更新图表。 - -![image.png](https://upload-images.jianshu.io/upload_images/3101171-5aa7627284c19a56.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -完成后,如果想要保存这个图表,可以点击页面最上方一栏中的 `Save` 按钮。 - -## Dashboard - -`Dashboard` 可以整合和共享 `Visualize` 集合。 - -1. 点击侧面导航栏中的 Dashboard。 -2. 点击添加显示保存的可视化列表。 -3. 点击之前保存的 `Visualize`,然后点击列表底部的小向上箭头关闭可视化列表。 -4. 将鼠标悬停在可视化对象上会显示允许您编辑,移动,删除和调整可视化对象大小的容器控件。 diff --git a/docs/javatool/elastic/elastic-logstash.md b/docs/javatool/elastic/elastic-logstash.md deleted file mode 100644 index cc3a6220..00000000 --- a/docs/javatool/elastic/elastic-logstash.md +++ /dev/null @@ -1,519 +0,0 @@ ---- -title: Elastic 技术栈之 Logstash 基础 -date: 2017-12-26 -categories: -- javatool -tags: -- java -- javatool -- log -- elastic ---- - -# Elastic 技术栈之 Logstash 基础 - -> 本文是 Elastic 技术栈(ELK)的 Logstash 应用。 -> -> 如果不了解 Elastic 的安装、配置、部署,可以参考:[Elastic 技术栈之快速入门](https://github.com/dunwu/java-stack/blob/master/docs/javatool/elastic/elastic-quickstart.md) - -## 简介 - -Logstash 可以传输和处理你的日志、事务或其他数据。 - -### 功能 - -Logstash 是 Elasticsearch 的最佳数据管道。 - -Logstash 是插件式管理模式,在输入、过滤、输出以及编码过程中都可以使用插件进行定制。Logstash 社区有超过 200 种可用插件。 - -### 工作原理 - -Logstash 有两个必要元素:`input` 和 `output` ,一个可选元素:`filter`。 - -这三个元素,分别代表 Logstash 事件处理的三个阶段:输入 > 过滤器 > 输出。 - -![logstash 工作原理.png](https://www.elastic.co/guide/en/logstash/current/static/images/basic_logstash_pipeline.png) - -- input 负责从数据源采集数据。 -- filter 将数据修改为你指定的格式或内容。 -- output 将数据传输到目的地。 - -在实际应用场景中,通常输入、输出、过滤器不止一个。Logstash 的这三个元素都使用插件式管理方式,用户可以根据应用需要,灵活的选用各阶段需要的插件,并组合使用。 - -后面将对插件展开讲解,暂且不表。 - -## 设置 - -### 设置文件 - -- **`logstash.yml`**:logstash 的默认启动配置文件 -- **`jvm.options`**:logstash 的 JVM 配置文件。 -- **`startup.options`** (Linux):包含系统安装脚本在 `/usr/share/logstash/bin` 中使用的选项为您的系统构建适当的启动脚本。安装 Logstash 软件包时,系统安装脚本将在安装过程结束时执行,并使用 `startup.options` 中指定的设置来设置用户,组,服务名称和服务描述等选项。 - - -### logstash.yml 设置项 - -节选部分设置项,更多项请参考:https://www.elastic.co/guide/en/logstash/current/logstash-settings-file.html - -| 参数 | 描述 | 默认值 | -| -------------------------- | ---------------------------------------- | ---------------------------------------- | -| `node.name` | 节点名 | 机器的主机名 | -| `path.data` | Logstash及其插件用于任何持久性需求的目录。 | `LOGSTASH_HOME/data` | -| `pipeline.workers` | 同时执行管道的过滤器和输出阶段的工作任务数量。如果发现事件正在备份,或CPU未饱和,请考虑增加此数字以更好地利用机器处理能力。 | Number of the host’s CPU cores | -| `pipeline.batch.size` | 尝试执行过滤器和输出之前,单个工作线程从输入收集的最大事件数量。较大的批量处理大小一般来说效率更高,但是以增加的内存开销为代价。您可能必须通过设置 `LS_HEAP_SIZE` 变量来有效使用该选项来增加JVM堆大小。 | `125` | -| `pipeline.batch.delay` | 创建管道事件批处理时,在将一个尺寸过小的批次发送给管道工作任务之前,等待每个事件需要多长时间(毫秒)。 | `5` | -| `pipeline.unsafe_shutdown` | 如果设置为true,则即使在内存中仍存在inflight事件时,也会强制Logstash在关闭期间退出。默认情况下,Logstash将拒绝退出,直到所有接收到的事件都被推送到输出。启用此选项可能会导致关机期间数据丢失。 | `false` | -| `path.config` | 主管道的Logstash配置路径。如果您指定一个目录或通配符,配置文件将按字母顺序从目录中读取。 | Platform-specific. See [[dir-layout\]](https://github.com/elastic/logstash/blob/6.1/docs/static/settings-file.asciidoc#dir-layout). | -| `config.string` | 包含用于主管道的管道配置的字符串。使用与配置文件相同的语法。 | None | -| `config.test_and_exit` | 设置为true时,检查配置是否有效,然后退出。请注意,使用此设置不会检查grok模式的正确性。 Logstash可以从目录中读取多个配置文件。如果将此设置与log.level:debug结合使用,则Logstash将记录组合的配置文件,并注掉其源文件的配置块。 | `false` | -| `config.reload.automatic` | 设置为true时,定期检查配置是否已更改,并在配置更改时重新加载配置。这也可以通过SIGHUP信号手动触发。 | `false` | -| `config.reload.interval` | Logstash 检查配置文件更改的时间间隔。 | `3s` | -| `config.debug` | 设置为true时,将完全编译的配置显示为调试日志消息。您还必须设置`log.level:debug`。警告:日志消息将包括任何传递给插件配置作为明文的“密码”选项,并可能导致明文密码出现在您的日志! | `false` | -| `config.support_escapes` | 当设置为true时,带引号的字符串将处理转义字符。 | `false` | -| `modules` | 配置时,模块必须处于上表所述的嵌套YAML结构中。 | None | -| `http.host` | 绑定地址 | `"127.0.0.1"` | -| `http.port` | 绑定端口 | `9600` | -| `log.level` | 日志级别。有效选项:fatal > error > warn > info > debug > trace | `info` | -| `log.format` | 日志格式。json (JSON 格式)或 plain (原对象) | `plain` | -| `path.logs` | Logstash 自身日志的存储路径 | `LOGSTASH_HOME/logs` | -| `path.plugins` | 在哪里可以找到自定义的插件。您可以多次指定此设置以包含多个路径。 | | - -## 启动 - -### 命令行 - -通过命令行启动 logstash 的方式如下: - -``` -bin/logstash [options] -``` - -其中 [options] 是您可以指定用于控制 Logstash 执行的命令行标志。 - -在命令行上设置的任何标志都会覆盖 Logstash 设置文件(`logstash.yml`)中的相应设置,但设置文件本身不会更改。 - -> **注** -> -> 虽然可以通过指定命令行参数的方式,来控制 logstash 的运行方式,但显然这么做很麻烦。 -> -> 建议通过指定配置文件的方式,来控制 logstash 运行,启动命令如下: -> -> ``` -> bin/logstash -f logstash.conf -> ``` -> 若想了解更多的命令行参数细节,请参考:https://www.elastic.co/guide/en/logstash/current/running-logstash-command-line.html -> - -### 配置文件 - -上节,我们了解到,logstash 可以执行 `bin/logstash -f logstash.conf` ,按照配置文件中的参数去覆盖默认设置文件(`logstash.yml`)中的设置。 - -这节,我们就来学习一下这个配置文件如何配置参数。 - -#### 配置文件结构 - -在工作原理一节中,我们已经知道了 Logstash 主要有三个工作阶段 input 、filter、output。而 logstash 配置文件文件结构也与之相对应: - -``` -input {} - -filter {} - -output {} -``` - -> 每个部分都包含一个或多个插件的配置选项。如果指定了多个过滤器,则会按照它们在配置文件中的显示顺序应用它们。 - -#### 插件配置 - -插件的配置由插件名称和插件的一个设置块组成。 - -下面的例子中配置了两个输入文件配置: - -``` -input { - file { - path => "/var/log/messages" - type => "syslog" - } - - file { - path => "/var/log/apache/access.log" - type => "apache" - } -} -``` - -您可以配置的设置因插件类型而异。你可以参考: [Input Plugins](https://www.elastic.co/guide/en/logstash/current/input-plugins.html), [Output Plugins](https://www.elastic.co/guide/en/logstash/current/output-plugins.html), [Filter Plugins](https://www.elastic.co/guide/en/logstash/current/filter-plugins.html), 和 [Codec Plugins](https://www.elastic.co/guide/en/logstash/current/codec-plugins.html) 。 - -#### 值类型 - -一个插件可以要求设置的值是一个特定的类型,比如布尔值,列表或哈希值。以下值类型受支持。 - -- Array - -``` - users => [ {id => 1, name => bob}, {id => 2, name => jane} ] -``` - -- Lists - -``` - path => [ "/var/log/messages", "/var/log/*.log" ] - uris => [ "http://elastic.co", "http://example.net" ] -``` - -- Boolean - -``` - ssl_enable => true -``` - -- Bytes - -``` - my_bytes => "1113" # 1113 bytes - my_bytes => "10MiB" # 10485760 bytes - my_bytes => "100kib" # 102400 bytes - my_bytes => "180 mb" # 180000000 bytes -``` - -- Codec - -``` - codec => "json" -``` - -- Hash - -``` -match => { - "field1" => "value1" - "field2" => "value2" - ... -} -``` - -- Number - -``` - port => 33 -``` - -- Password - -``` - my_password => "password" -``` - -- URI - -``` - my_uri => "http://foo:bar@example.net" -``` - -- Path - -``` - my_path => "/tmp/logstash" -``` - -- String - - -- 转义字符 - -## 插件 - -### input - -> Logstash 支持各种输入选择 ,可以在同一时间从众多常用来源捕捉事件。能够以连续的流式传输方式,轻松地从您的日志、指标、Web 应用、数据存储以及各种 AWS 服务采集数据。 - -#### 常用 input 插件 - -- **file**:从文件系统上的文件读取,就像UNIX命令 `tail -0F` 一样 -- **syslog:**在众所周知的端口514上侦听系统日志消息,并根据RFC3164格式进行解析 -- **redis:**从redis服务器读取,使用redis通道和redis列表。 Redis经常用作集中式Logstash安装中的“代理”,它将来自远程Logstash“托运人”的Logstash事件排队。 -- **beats:**处理由Filebeat发送的事件。 - -更多详情请见:[Input Plugins](https://www.elastic.co/guide/en/logstash/current/input-plugins.html) - -### filter - -> 过滤器是Logstash管道中的中间处理设备。如果符合特定条件,您可以将条件过滤器组合在一起,对事件执行操作。 - -#### 常用 filter 插件 - -- **grok:**解析和结构任意文本。 Grok目前是Logstash中将非结构化日志数据解析为结构化和可查询的最佳方法。 -- **mutate:**对事件字段执行一般转换。您可以重命名,删除,替换和修改事件中的字段。 - -- **drop:**完全放弃一个事件,例如调试事件。 - -- **clone:**制作一个事件的副本,可能会添加或删除字段。 - -- **geoip:**添加有关IP地址的地理位置的信息(也可以在Kibana中显示惊人的图表!) - - -更多详情请见:[Filter Plugins](https://www.elastic.co/guide/en/logstash/current/filter-plugins.html) - -### output - -> 输出是Logstash管道的最后阶段。一个事件可以通过多个输出,但是一旦所有输出处理完成,事件就完成了执行。 - -#### 常用 output 插件 - -- **elasticsearch:**将事件数据发送给 Elasticsearch(推荐模式)。 -- **file:**将事件数据写入文件或磁盘。 -- **graphite:**将事件数据发送给 graphite(一个流行的开源工具,存储和绘制指标。 http://graphite.readthedocs.io/en/latest/)。 -- **statsd:**将事件数据发送到 statsd (这是一种侦听统计数据的服务,如计数器和定时器,通过UDP发送并将聚合发送到一个或多个可插入的后端服务)。 - -更多详情请见:[Output Plugins](https://www.elastic.co/guide/en/logstash/current/output-plugins.html) - -### codec - -用于格式化对应的内容。 - -#### 常用 codec 插件 - -- **json:**以JSON格式对数据进行编码或解码。 -- **multiline:**将多行文本事件(如java异常和堆栈跟踪消息)合并为单个事件。 - -更多插件请见:[Codec Plugins](https://www.elastic.co/guide/en/logstash/current/codec-plugins.html) - -## 实战 - -前面的内容都是对 Logstash 的介绍和原理说明。接下来,我们来实战一些常见的应用场景。 - -### 传输控制台数据 - -> stdin input 插件从标准输入读取事件。这是最简单的 input 插件,一般用于测试场景。 -> - -**应用** - -(1)创建 `logstash-input-stdin.conf` : - -``` -input { stdin { } } -output { - elasticsearch { hosts => ["localhost:9200"] } - stdout { codec => rubydebug } -} -``` - -更多配置项可以参考:https://www.elastic.co/guide/en/logstash/current/plugins-inputs-stdin.html - -(2)执行 logstash,使用 `-f` 来指定你的配置文件: - -``` -bin/logstash -f logstash-input-stdin.conf -``` - -### 传输 logback 日志 - -> elk 默认使用的 Java 日志工具是 log4j2 ,并不支持 logback 和 log4j。 -> -> 想使用 logback + logstash ,可以使用 [logstash-logback-encoder](https://github.com/logstash/logstash-logback-encoder) 。[logstash-logback-encoder](https://github.com/logstash/logstash-logback-encoder) 提供了 UDP / TCP / 异步方式来传输日志数据到 logstash。 -> -> 如果你使用的是 log4j ,也不是不可以用这种方式,只要引入桥接 jar 包即可。如果你对 log4j 、logback ,或是桥接 jar 包不太了解,可以参考我的这篇博文:[细说 Java 主流日志工具库](https://github.com/dunwu/java-stack/blob/master/docs/javalib/java-log.md) 。 - -#### TCP 应用 - -1. logstash 配置 - - (1)创建 `logstash-input-tcp.conf` : - -``` -input { -tcp { - port => 9251 - codec => json_lines - mode => server -} -} -output { - elasticsearch { hosts => ["localhost:9200"] } - stdout { codec => rubydebug } -} -``` - - 更多配置项可以参考:https://www.elastic.co/guide/en/logstash/current/plugins-inputs-tcp.html - - (2)执行 logstash,使用 `-f` 来指定你的配置文件:`bin/logstash -f logstash-input-udp.conf` - - -2. java 应用配置 - - (1)在 Java 应用的 pom.xml 中引入 jar 包: - -```xml - - net.logstash.logback - logstash-logback-encoder - 4.11 - - - - - ch.qos.logback - logback-core - 1.2.3 - - - ch.qos.logback - logback-classic - 1.2.3 - - - ch.qos.logback - logback-access - 1.2.3 - -``` - - (2)接着,在 logback.xml 中添加 appender - -```xml - - - 192.168.28.32:9251 - - - - - -``` - - (3)接下来,就是 logback 的具体使用 ,如果对此不了解,不妨参考一下我的这篇博文:[细说 Java 主流日志工具库](https://github.com/dunwu/java-stack/blob/master/docs/javalib/java-log.md) 。 - - **实例:**[我的logback.xml](https://github.com/dunwu/java-stack/blob/master/codes/javatool/src/main/resources/logback.xml) - -#### UDP 应用 - -UDP 和 TCP 的使用方式大同小异。 - -1. logstash 配置 - - (1)创建 `logstash-input-udp.conf` : - -``` -input { -udp { - port => 9250 - codec => json -} -} -output { - elasticsearch { hosts => ["localhost:9200"] } - stdout { codec => rubydebug } -} -``` - - 更多配置项可以参考:https://www.elastic.co/guide/en/logstash/current/plugins-inputs-udp.html - - (2)执行 logstash,使用 `-f` 来指定你的配置文件:`bin/logstash -f logstash-input-udp.conf` - - -2. java 应用配置 - - (1)在 Java 应用的 pom.xml 中引入 jar 包: - - 与 **TCP 应用** 一节中的引入依赖包完全相同。 - - (2)接着,在 logback.xml 中添加 appender - - ```xml - - 192.168.28.32 - 9250 - - - - - ``` - - (3)接下来,就是 logback 的具体使用 ,如果对此不了解,不妨参考一下我的这篇博文:[细说 Java 主流日志工具库](https://github.com/dunwu/java-stack/blob/master/docs/javalib/java-log.md) 。 - - **实例:**[我的logback.xml](https://github.com/dunwu/java-stack/blob/master/codes/javatool/src/main/resources/logback.xml) - -### 传输文件 - -> 在 Java Web 领域,需要用到一些重要的工具,例如 Tomcat 、Nginx 、Mysql 等。这些不属于业务应用,但是它们的日志数据对于定位问题、分析统计同样很重要。这时无法使用 logback 方式将它们的日志传输到 logstash。 -> -> 如何采集这些日志文件呢?别急,你可以使用 logstash 的 file input 插件。 -> -> 需要注意的是,传输文件这种方式,必须在日志所在的机器上部署 logstash 。 - -**应用** - -logstash 配置 - -(1)创建 `logstash-input-file.conf` : - -``` -input { - file { - path => ["/var/log/nginx/access.log"] - type => "nginx-access-log" - start_position => "beginning" - } -} - -output { - if [type] == "nginx-access-log" { - elasticsearch { - hosts => ["localhost:9200"] - index => "nginx-access-log" - } - } -} -``` - -(2)执行 logstash,使用 `-f` 来指定你的配置文件:`bin/logstash -f logstash-input-file.conf` - -更多配置项可以参考:https://www.elastic.co/guide/en/logstash/current/plugins-inputs-file.html - -## 小技巧 - -### 启动、终止应用 - -如果你的 logstash 每次都是通过指定配置文件方式启动。不妨建立一个启动脚本。 - -``` -# cd xxx 进入 logstash 安装目录下的 bin 目录 -logstash -f logstash.conf -``` - -如果你的 logstash 运行在 linux 系统下,不妨使用 nohup 来启动一个守护进程。这样做的好处在于,即使关闭终端,应用仍会运行。 - -**创建 startup.sh** - -``` -nohup ./logstash -f logstash.conf >> nohup.out 2>&1 & -``` - -终止应用没有什么好方法,你只能使用 ps -ef | grep logstash ,查出进程,将其kill 。不过,我们可以写一个脚本来干这件事: - -**创建 shutdown.sh** - -脚本不多解释,请自行领会作用。 - -``` -PID=`ps -ef | grep logstash | awk '{ print $2}' | head -n 1` -kill -9 ${PID} -``` - -## 资料 - -- [Logstash 官方文档](https://www.elastic.co/guide/en/logstash/current/index.html) -- [logstash-logback-encoder](https://github.com/logstash/logstash-logback-encoder) -- [ELK Stack权威指南](https://github.com/chenryn/logstash-best-practice-cn) -- [ELK(Elasticsearch、Logstash、Kibana)安装和配置](https://github.com/judasn/Linux-Tutorial/blob/master/ELK-Install-And-Settings.md) - -## 推荐阅读 - -- [Elastic 技术栈](https://github.com/dunwu/java-stack/blob/master/docs/javatool/elastic/README.md) -- [java-stack](https://github.com/dunwu/java-stack) diff --git a/docs/javatool/elastic/elastic-quickstart.md b/docs/javatool/elastic/elastic-quickstart.md deleted file mode 100644 index 0e3f3250..00000000 --- a/docs/javatool/elastic/elastic-quickstart.md +++ /dev/null @@ -1,289 +0,0 @@ ---- -title: Elastic 技术栈之快速入门 -date: 2017-12-06 -categories: -- javatool -tags: -- java -- javatool -- log -- elastic ---- - -# Elastic 技术栈之快速入门 - -## 概念 - -### ELK 是什么 - -ELK 是 elastic 公司旗下三款产品 [ElasticSearch](https://www.elastic.co/products/elasticsearch) 、[Logstash](https://www.elastic.co/products/logstash) 、[Kibana](https://www.elastic.co/products/kibana) 的首字母组合。 - -[ElasticSearch](https://www.elastic.co/products/elasticsearch) 是一个基于 [Lucene](http://lucene.apache.org/core/documentation.html) 构建的开源,分布式,RESTful 搜索引擎。 - -[Logstash](https://www.elastic.co/products/logstash) 传输和处理你的日志、事务或其他数据。 - -[Kibana](https://www.elastic.co/products/kibana) 将 Elasticsearch 的数据分析并渲染为可视化的报表。 - -### 为什么使用 ELK ? - -对于有一定规模的公司来说,通常会很多个应用,并部署在大量的服务器上。运维和开发人员常常需要通过查看日志来定位问题。如果应用是集群化部署,试想如果登录一台台服务器去查看日志,是多么费时费力。 - -而通过 ELK 这套解决方案,可以同时实现日志收集、日志搜索和日志分析的功能。 - -### Elastic 架构 - -![static/images/deploy3.png](https://www.elastic.co/guide/en/logstash/current/static/images/deploy3.png) - -> **说明** -> -> 以上是 ELK 技术栈的一个架构图。从图中可以清楚的看到数据流向。 -> -> [Beats](https://www.elastic.co/products/beats) 是单一用途的数据传输平台,它可以将多台机器的数据发送到 Logstash 或 ElasticSearch。但 Beats 并不是不可或缺的一环,所以本文中暂不介绍。 -> -> [Logstash](https://www.elastic.co/products/logstash) 是一个动态数据收集管道。支持以 TCP/UDP/HTTP 多种方式收集数据(也可以接受 Beats 传输来的数据),并对数据做进一步丰富或提取字段处理。 -> -> [ElasticSearch](https://www.elastic.co/products/elasticsearch) 是一个基于 JSON 的分布式的搜索和分析引擎。作为 ELK 的核心,它集中存储数据。 -> -> [Kibana](https://www.elastic.co/products/kibana) 是 ELK 的用户界面。它将收集的数据进行可视化展示(各种报表、图形化数据),并提供配置、管理 ELK 的界面。 - -## 安装 - -### 准备 - -ELK 要求本地环境中安装了 JDK 。如果不确定是否已安装,可使用下面的命令检查: - -```sh -java -version -``` - -> **注意** -> -> 本文使用的 ELK 是 6.0.0,要求 jdk 版本不低于 JDK8。 -> -> 友情提示:安装 ELK 时,三个应用请选择统一的版本,避免出现一些莫名其妙的问题。例如:由于版本不统一,导致三个应用间的通讯异常。 - -### Elasticsearch - -安装步骤如下: - -1. [elasticsearch 官方下载地址](https://www.elastic.co/downloads/elasticsearch)下载所需版本包并解压到本地。 -2. 运行 `bin/elasticsearch` (Windows 上运行 `bin\elasticsearch.bat`) -3. 验证运行成功:linux 上可以执行 `curl http://localhost:9200/` ;windows 上可以用访问 REST 接口的方式来访问 http://localhost:9200/ - -> **说明** -> -> Linux 上可以执行下面的命令来下载压缩包: -> -> ``` -> curl -L -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.0.0.tar.gz -> ``` -> -> Mac 上可以执行以下命令来进行安装: -> -> ``` -> brew install elasticsearch -> ``` -> -> Windows 上可以选择 MSI 可执行安装程序,将应用安装到本地。 - -### Logstash - -安装步骤如下: - -1. 在 [logstash 官方下载地址](https://www.elastic.co/downloads/logstash)下载所需版本包并解压到本地。 - -2. 添加一个 `logstash.conf` 文件,指定要使用的插件以及每个插件的设置。举个简单的例子: - - ``` - input { stdin { } } - output { - elasticsearch { hosts => ["localhost:9200"] } - stdout { codec => rubydebug } - } - ``` - -3. 运行 `bin/logstash -f logstash.conf` (Windows 上运行`bin/logstash.bat -f logstash.conf`) - -### Kibana - -安装步骤如下: - -1. 在 [kibana 官方下载地址](https://www.elastic.co/downloads/kibana)下载所需版本包并解压到本地。 -2. 修改 `config/kibana.yml` 配置文件,设置 `elasticsearch.url` 指向 Elasticsearch 实例。 -3. 运行 `bin/kibana` (Windows 上运行 `bin\kibana.bat`) -4. 在浏览器上访问 http://localhost:5601 - -### 安装 FAQ - -#### elasticsearch 不允许以 root 权限来运行 - -**问题:**在 Linux 环境中,elasticsearch 不允许以 root 权限来运行。 - -如果以 root 身份运行 elasticsearch,会提示这样的错误: - -``` -can not run elasticsearch as root -``` - -**解决方法:**使用非 root 权限账号运行 elasticsearch - -```sh -# 创建用户组 -groupadd elk -# 创建新用户,-g elk 设置其用户组为 elk,-p elk 设置其密码为 elk -useradd elk -g elk -p elk -# 更改 /opt 文件夹及内部文件的所属用户及组为 elk:elk -chown -R elk:elk /opt # 假设你的 elasticsearch 安装在 opt 目录下 -# 切换账号 -su elk -``` - -#### vm.max_map_count 不低于 262144 - -**问题:**`vm.max_map_count` 表示虚拟内存大小,它是一个内核参数。elasticsearch 默认要求 `vm.max_map_count` 不低于 262144。 - -``` -max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144] -``` - -**解决方法:** - -你可以执行以下命令,设置 `vm.max_map_count` ,但是重启后又会恢复为原值。 - -``` -sysctl -w vm.max_map_count=262144 -``` - -持久性的做法是在 `/etc/sysctl.conf` 文件中修改 `vm.max_map_count` 参数: - -``` -echo "vm.max_map_count=262144" > /etc/sysctl.conf -sysctl -p -``` - -> **注意** -> -> 如果运行环境为 docker 容器,可能会限制执行 sysctl 来修改内核参数。 -> -> 这种情况下,你只能选择直接修改宿主机上的参数了。 - -#### nofile 不低于 65536 - -**问题:** `nofile` 表示进程允许打开的最大文件数。elasticsearch 进程要求可以打开的最大文件数不低于 65536。 - -``` -max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536] -``` - -**解决方法:** - -在 `/etc/security/limits.conf` 文件中修改 `nofile` 参数: - -``` -echo "* soft nofile 65536" > /etc/security/limits.conf -echo "* hard nofile 131072" > /etc/security/limits.conf -``` - -#### nproc 不低于 2048 - -**问题:** `nproc` 表示最大线程数。elasticsearch 要求最大线程数不低于 2048。 - -``` -max number of threads [1024] for user [user] is too low, increase to at least [2048] -``` - -**解决方法:** - -在 `/etc/security/limits.conf` 文件中修改 `nproc` 参数: - -``` -echo "* soft nproc 2048" > /etc/security/limits.conf -echo "* hard nproc 4096" > /etc/security/limits.conf -``` - -#### Kibana No Default Index Pattern Warning - -**问题:**安装 ELK 后,访问 kibana 页面时,提示以下错误信息: - -``` -Warning No default index pattern. You must select or create one to continue. -... -Unable to fetch mapping. Do you have indices matching the pattern? -``` - -这就说明 logstash 没有把日志写入到 elasticsearch。 - -**解决方法:** - -检查 logstash 与 elasticsearch 之间的通讯是否有问题,一般问题就出在这。 - -## 使用 - -本人使用的 Java 日志方案为 slf4j + logback,所以这里以 logback 来讲解。 - -### Java 应用输出日志到 ELK - -**修改 logstash.conf 配置** - -首先,我们需要修改一下 logstash 服务端 logstash.conf 中的配置 - -``` -input { - # stdin { } - tcp { - # host:port就是上面appender中的 destination, - # 这里其实把logstash作为服务,开启9250端口接收logback发出的消息 - host => "127.0.0.1" port => 9250 mode => "server" tags => ["tags"] codec => json_lines - } -} -output { - elasticsearch { hosts => ["localhost:9200"] } - stdout { codec => rubydebug } -} -``` - -> **说明** -> -> 这个 input 中的配置其实是 logstash 服务端监听 9250 端口,接收传递来的日志数据。 - -然后,在 Java 应用的 pom.xml 中引入 jar 包: - -```xml - - net.logstash.logback - logstash-logback-encoder - 4.11 - -``` - -接着,在 logback.xml 中添加 appender - -```xml - - - 127.0.0.1:9250 - - - - - -``` - -大功告成,此后,`io.github.dunwu.spring` 包中的 TRACE 及以上级别的日志信息都会被定向输出到 logstash 服务。 - -![image.png](http://upload-images.jianshu.io/upload_images/3101171-cd876d79a14955b0.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 资料 - -- [elastic 官方文档](https://www.elastic.co/guide/index.html) - -- [elasticsearch github](https://github.com/elastic/elasticsearch) - -- [logstash github](https://github.com/elastic/logstash) - -- [kibana github](https://github.com/elastic/kibana) - - diff --git a/docs/javatool/elastic/elk.xmind b/docs/javatool/elastic/elk.xmind deleted file mode 100644 index 0ee8ce91..00000000 Binary files a/docs/javatool/elastic/elk.xmind and /dev/null differ diff --git a/docs/javatool/ide/README.md b/docs/javatool/ide/README.md deleted file mode 100644 index 75167cf3..00000000 --- a/docs/javatool/ide/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Java IDE - -## 目录 - -* [Intellij IDEA 使用小结](intellij.md) -* [Eclipse 使用小结](eclipse.md) diff --git a/docs/javatool/ide/eclipse.md b/docs/javatool/ide/eclipse.md deleted file mode 100644 index 3f5ad94c..00000000 --- a/docs/javatool/ide/eclipse.md +++ /dev/null @@ -1,248 +0,0 @@ ---- -title: Eclipse 使用小结 -date: 2016/03/20 -categories: -- javatool -tags: -- java -- javatool -- IDE ---- - -# Eclipse 使用小结 - -## 代码智能提示 - -### Java智能提示 - -Window -> Preferences -> Java -> Editor -> Content Assist -> Auto Activation - -![](http://upload-images.jianshu.io/upload_images/3101171-1a0d2206870c6542.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -delay是自动弹出提示框的延时时间,我们可以修改成100毫秒;triggers这里默认是".",只要加上"abcdefghijklmnopqrstuvwxyz"或者"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",嘿嘿!这下就能做到和VS一样的输入每个字母都能提示啦: - - ![](http://upload-images.jianshu.io/upload_images/3101171-5eb9ad6afbe05289.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -其它类型的文件比如HTML、JavaScript、JSP如果也能提供提示那不是更爽了?有了第二点设置的基础,其实这些设置都是一样的。 - -### JavaScript智能提示 - -Window -> Preferences -> JavaScript-> Editor -> Content Assist -> Auto-Activation - -![](http://upload-images.jianshu.io/upload_images/3101171-31bca3ed1b0d0050.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -### HTML智能提示 - -Window -> Preferences -> Web -> HTML Files -> Editor -> Content Assist -> Auto-Activation - -![](http://upload-images.jianshu.io/upload_images/3101171-7c1ce7a35793f234.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -保存后,我们再来输入看看,感觉真是不错呀: - -![](http://upload-images.jianshu.io/upload_images/3101171-c78cc7c7b02a6ca4.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - - -[![](http://upload-images.jianshu.io/upload_images/3101171-9d7284acfa56fa06.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)](http://images2015.cnblogs.com/blog/318837/201605/318837-20160519135330638-166066199.jpg) - -## 插件安装 - -很多教科书上说到Eclipse的插件安装都是通过 Help -> Install New SoftWare 这种自动检索的方式,操作起来固然是方便,不过当我们不需要某种插件时不太容易找到要删除哪些内容,而且以后Eclipse版本升级的时候,通过这种方式安装过的插件都得再重新装一次。另外一种通过Link链接方式,就可以解决这些问题。 - -我们以Eclipse的中文汉化包插件为例,先到官方提供的汉化包地址下载一个:[http://www.eclipse.org/babel/downloads.php ](http://www.eclipse.org/babel/downloads.php),注意选好自己的Eclipse版本: - -![](http://upload-images.jianshu.io/upload_images/3101171-d8a662eaba3550e3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -我的版本是Kepler,然后进入下载页面,单击红框框中的链接,即可下载汉化包了: - -![](http://upload-images.jianshu.io/upload_images/3101171-3544d5393f4e298f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -下载完解压缩后,会有个包含features和plugin目录的eclipse文件夹,把这个eclipse放在我们的Eclipse安装根目录,也就是和eclipse.exe同一级目录下。然后仍然在这一级目录下,新建一个links文件夹,并在该文件夹内,建一个language.link的文本文件。该文本文件的名字是可以任取的,后缀名是.link,而不是.txt哟。好了,最后一步,编辑该文件,在里面写入刚才放入的语言包的地址,并用“\\”表示路径,一定要有path= 这个前缀。 - -![](http://upload-images.jianshu.io/upload_images/3101171-18f74bf3080d2c1b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -保存文件后,重新打开Eclipse,熟悉的中文界面终于看到了。虽然汉化不完全,不过也够用了不是么。如果仍然出现的是英文,说明汉化失败,重新检查下language.link文件中配置的信息是否和汉化包的目录一致。  其它的插件安装方法也是如此,当不需要某个插件时,只需删除存放插件的目录和links目录下相应的link文件,或者改变下link文件里面的路径变成无效路径即可;对Eclipse做高版本升级时,也只需把老版存放插件的目录和links目录复制过去就行了。 - -## 基本设置 - -在Preference的搜索项中搜索 Text Editors。 -可以参考我的设置: -Show line numbers -Show print margin -Insert spaces for tabs - -![](http://upload-images.jianshu.io/upload_images/3101171-91aa025fbe7592f8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) -设置代码的字体类型和大小: - -Window -> Preferences -> General -> Appearance -> Content Assist -> Colors and Fornts,只需修改 Basic 里面的 Text Font 就可以了。 - -推荐Courier New。 - -![](http://upload-images.jianshu.io/upload_images/3101171-49b9126aa0dd58c0.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -![](http://upload-images.jianshu.io/upload_images/3101171-b7d6d71b2c211321.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 设置文本文件及JSP文件编码 - -Window -> Preferences -> General -> Workspace -> Text file encoding -> Other: - -![](http://upload-images.jianshu.io/upload_images/3101171-b7aa010673565c88.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - - Window -> Preferences -> Web -> JSP Files -> Text file encoding-> Other:![](http://upload-images.jianshu.io/upload_images/3101171-b83fa3476fddde46.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 设置JDK本地JavaDOC API路径及源码路径 - -在需要代码提示时,可能经常会遇到这样的情况: ![](http://upload-images.jianshu.io/upload_images/3101171-45b5dee3d3ce917a.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -![](http://upload-images.jianshu.io/upload_images/3101171-f960daf4839662e3.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -还都生成的是无意义的变量名,这样可能会对含有相同类型的变量参数的调用顺序造成干扰; - -这种问题,我们把JDK或者相应Jar包的源码导入进去就能避免了: - -Window -> Preferences -> Java -> Installed JREs -> Edit: - -![](http://upload-images.jianshu.io/upload_images/3101171-a08c9166dbbf4361.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -选中设置好的JRE目录,编辑,然后全选 JRE system libraries 下的所有Jar包,点击右边的 Source Attachment; - -![](http://upload-images.jianshu.io/upload_images/3101171-4e6c78afa8e3e50b.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -External location 下,选中JDK安装目录下的  src.zip 文件,一路OK 下来。 - -![](http://upload-images.jianshu.io/upload_images/3101171-e82d03ce88f64312.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -设置完,我们再来看看,幸福来的好突然有木有! - -[![](http://upload-images.jianshu.io/upload_images/3101171-400b3952aa60cb8e.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)](http://images2015.cnblogs.com/blog/318837/201605/318837-20160519135339451-1491707701.jpg) - -## 设置Servlet源码或其它Jar包源码 - -当我们使用非JDK,比如Servlet的包或者类时,这玩意又出来了,欲哭无泪的赶脚。。。![](http://upload-images.jianshu.io/upload_images/3101171-ec1980f58297e42c.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -上一步已经设置过了JDK的源码或JavaDoc路径,为啥现在又出来了呢?其实这个不难理解,因为我们使用到的类的源码并不在JDK的源码包中。 - -仔细看,我们会发现这些Jar包其实都在Tomcat根目录下的lib文件夹中,但是翻遍了Tomcat目录也没有相应的jar或zip文件呀。既然本地没有,那就去官网上找找: - -http://tomcat.apache.org/download-70.cgi这里有Tomcat的安装包和源码包; - - ![](http://upload-images.jianshu.io/upload_images/3101171-2962b9cd48422373.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -可以自定义一个专门用于存放JavaSource和JavaDoc的文件夹,把下载文件放到该目录下, - -然后再切换到Eclipse下,选中没有代码提示的类或者函数, 按下F3,点击 Change Attached Source: - -![](http://upload-images.jianshu.io/upload_images/3101171-e8c7cc17364206cf.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -选择我们刚才下载好的tomcat源码文件,一路OK。 - -![](http://upload-images.jianshu.io/upload_images/3101171-138cb66d553d9306.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -然后再回过头看看我们的代码提示,友好多了: - -![](http://upload-images.jianshu.io/upload_images/3101171-b27652e5ff1d6eab.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -其它Jar包源码的设置方式也一样。 - -## 反编译插件 JD-Eclipse - -无论是开发还是调试,反编译必不可少,每次都用jd-gui打开去看,多麻烦,干脆配置下JD插件,自动关联.class: - -先从 http://jd.benow.ca/ 上下载离线安装包 jdeclipse_update_site.zip,解压缩后把 features、plugins 这2个文件夹复制到 新建文件夹 jdeclipse,然后把 jdeclipse 文件夹整个复制到Eclipse根目录的dropins文件夹下,重启Eclipse即可。这种方式是不是比建link文件更方便了? - -![](http://upload-images.jianshu.io/upload_images/3101171-403597b1b46607ef.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -打开Eclipse,Window -> Preferences -> General - > Editors ,把 .class 文件设置关联成 jd插件的editor - -![](http://upload-images.jianshu.io/upload_images/3101171-d95625fcf362526c.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## Validate优化 - -我们在eclipse里经常看到这个进程,validating... 逐个的检查每一个文件。那么如何关闭一些validate操作呢? - -![](http://upload-images.jianshu.io/upload_images/3101171-8323d6f595f96fdd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -打开eclipse,点击【window】菜单,选择【preferences】选项。 - -![](http://upload-images.jianshu.io/upload_images/3101171-88bd81ece1b3f29f.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -在左侧点击【validation】选项,在右侧可以看到eclipse进行的自动检查都有哪些内容。 - -![](http://upload-images.jianshu.io/upload_images/3101171-0c3cf67c6e954dd6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -将Manual(手动)保持不动,将build里面只留下classpath dependency Validator,其他的全部去掉。 - -![](http://upload-images.jianshu.io/upload_images/3101171-e1a3051fde4828d8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -最后点击【OK】按钮,保存设置。 - -![](http://upload-images.jianshu.io/upload_images/3101171-7c6803eed8cf618a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -以后如果需要对文件进行校验检查的时候,在文件上点击右键,点击【Validate】进行检查。 - -## 常用快捷键  - -| 快捷键 | 描述 | -| -------------------- | ---------------------------------------- | -| Ctrl+1 | 快速修复(最经典的快捷键,就不用多说了,可以解决很多问题,比如import类、try catch包围等) | -| Ctrl+Shift+F | 格式化当前代码 | -| Ctrl+Shift+M | 添加类的import导入 | -| Ctrl+Shift+O | 组织类的import导入(既有Ctrl+Shift+M的作用,又可以帮你去除没用的导入,很有用) | -| Ctrl+Y | 重做(与撤销Ctrl+Z相反) | -| Alt+/ | 内容辅助(帮你省了多少次键盘敲打,太常用了) | -| Ctrl+D | 删除当前行或者多行 | -| Alt+↓ | 当前行和下面一行交互位置(特别实用,可以省去先剪切,再粘贴了) | -| Alt+↑ | 当前行和上面一行交互位置(同上) | -| Ctrl+Alt+↓ | 复制当前行到下一行(复制增加) | -| Ctrl+Alt+↑ | 复制当前行到上一行(复制增加) | -| Shift+Enter | 在当前行的下一行插入空行(这时鼠标可以在当前行的任一位置,不一定是最后) | -| Ctrl+/ | 注释当前行,再按则取消注释 | -| Alt+Shift+↑ | 选择封装元素 | -| Alt+Shift+← | 选择上一个元素 | -| Alt+Shift+→ | 选择下一个元素 | -| Shift+← | 从光标处开始往左选择字符 | -| Shift+→ | 从光标处开始往右选择字符 | -| Ctrl+Shift+← | 选中光标左边的单词 | -| Ctrl+Shift+→ | 选中光标又边的单词 | -| Ctrl+← | 光标移到左边单词的开头,相当于vim的b | -| Ctrl+→ | 光标移到右边单词的末尾,相当于vim的e | -| Ctrl+K | 参照选中的Word快速定位到下一个(如果没有选中word,则搜索上一次使用搜索的word) | -| Ctrl+Shift+K | 参照选中的Word快速定位到上一个 | -| Ctrl+J | 正向增量查找(按下Ctrl+J后,你所输入的每个字母编辑器都提供快速匹配定位到某个单词,如果没有,则在状态栏中显示没有找到了,查一个单词时,特别实用,要退出这个模式,按escape建) | -| Ctrl+Shift+J | 反向增量查找(和上条相同,只不过是从后往前查) | -| Ctrl+Shift+U | 列出所有包含字符串的行 | -| Ctrl+H | 打开搜索对话框 | -| Ctrl+G | 工作区中的声明 | -| Ctrl+Shift+G | 工作区中的引用 | -| Ctrl+Shift+T | 搜索类(包括工程和关联的第三jar包) | -| Ctrl+Shift+R | 搜索工程中的文件 | -| Ctrl+E | 快速显示当前Editer的下拉列表(如果当前页面没有显示的用黑体表示) | -| F4 | 打开类型层次结构 | -| F3 | 跳转到声明处 | -| Alt+← | 前一个编辑的页面 | -| Alt+→ | 下一个编辑的页面(当然是针对上面那条来说了) | -| Ctrl+PageUp/PageDown | 在编辑器中,切换已经打开的文件 | -| F5 | 单步跳入 | -| F6 | 单步跳过 | -| F7 | 单步返回 | -| F8 | 继续 | -| Ctrl+Shift+D | 显示变量的值 | -| Ctrl+Shift+B | 在当前行设置或者去掉断点 | -| Ctrl+R | 运行至行(超好用,可以节省好多的断点) | -| Alt+Shift+R | 重命名方法名、属性或者变量名 (是我自己最爱用的一个了,尤其是变量和类的Rename,比手工方法能节省很多劳动力) | -| Alt+Shift+M | 把一段函数内的代码抽取成方法 (这是重构里面最常用的方法之一了,尤其是对一大堆泥团代码有用) | -| Alt+Shift+C | 修改函数结构(比较实用,有N个函数调用了这个方法,修改一次搞定) | -| Alt+Shift+L | 抽取本地变量( 可以直接把一些魔法数字和字符串抽取成一个变量,尤其是多处调用的时候) | -| Alt+Shift+F | 把Class中的local变量变为field变量 (比较实用的功能) | -| Alt+Shift+I | 合并变量(可能这样说有点不妥Inline) | -| Alt+Shift+V | 移动函数和变量(不怎么常用) | -| Alt+Shift+Z | 重构的后悔药(Undo) | -| Alt+Enter | 显示当前选择资源的属性,windows下的查看文件的属性就是这个快捷键,通常用来查看文件在windows中的实际路径 | -| Ctrl+↑ | 文本编辑器 上滚行 | -| Ctrl+↓ | 文本编辑器 下滚行 | -| Ctrl+M | 最大化当前的Edit或View (再按则反之) | -| Ctrl+O | 快速显示 OutLine | -| Ctrl+T | 快速显示当前类的继承结构 | -| Ctrl+W | 关闭当前Editer | -| Ctrl+L | 文本编辑器 转至行 | -| F2 | 显示工具提示描述 | diff --git a/docs/javatool/ide/intellij.md b/docs/javatool/ide/intellij.md deleted file mode 100644 index e5ce73f6..00000000 --- a/docs/javatool/ide/intellij.md +++ /dev/null @@ -1,283 +0,0 @@ ---- -title: Intellij IDEA 使用小结 -date: 2016/03/20 -categories: -- javatool -tags: -- java -- javatool -- IDE ---- - -# Intellij IDEA 使用小结 - -## 快捷键 - -### 核心快捷键 - -IntelliJ IDEA 作为一个以快捷键为中心的 IDE,为大多数操作建议了键盘快捷键。在这个主题中,您可以找到最不可缺少的列表,使 IntelliJ IDEA 轻松实现第一步。 - -**核心快捷键表** - -| 操作 | 快捷键 | -| ---------------------------------------- | ---------------------------------- | -| [根据名称查找操作](https://www.jetbrains.com/help/idea/navigating-to-action.html) | `Ctrl+Shift+A` | -| 显示可用 [意图操作](https://www.jetbrains.com/help/idea/intention-actions.html) 列表 | `Alt+Enter` | -| 切换视图 ([Project](https://www.jetbrains.com/help/idea/project-tool-window.html),[Structure](https://www.jetbrains.com/help/idea/structure-tool-window-file-structure-popup.html), etc.). | `Alt+F1` | -| [切换](https://www.jetbrains.com/help/idea/navigating-between-files-and-tool-windows.html)工具窗口和在编辑器中打开的文件 | `Ctrl+Tab` | -| 显示 [导航栏](https://www.jetbrains.com/help/idea/navigation-bar.html). | `Alt+Home` | -| [插入代码模板](https://www.jetbrains.com/help/idea/generating-code.html). | `Ctrl+J` | -| [在周围插入代码模板](https://www.jetbrains.com/help/idea/creating-code-constructs-using-surround-templates.html). | `Ctrl+Alt+J` | -| [Edit an item from the Project or another tree view](https://www.jetbrains.com/help/idea/opening-and-reopening-files-in-the-editor.html). | `F4` | -| [注释](https://www.jetbrains.com/help/idea/commenting-and-uncommenting-blocks-of-code.html) | `Ctrl+/` `Ctrl+Shift+/` | -| [根据名称查找类或文件](https://www.jetbrains.com/help/idea/navigating-to-class-file-or-symbol-by-name.html). | `Ctrl+N` `Ctrl+Shift+N` | -| [拷贝当前行或指定的行](https://www.jetbrains.com/help/idea/adding-deleting-and-moving-code-elements.html#duplicate). | `Ctrl+D` | -| [增加或减少选中的表达式](https://www.jetbrains.com/help/idea/selecting-text-in-the-editor.html). | `Ctrl+W` and `Ctrl+Shift+W` | -| [在当前文件查找或替换](https://www.jetbrains.com/help/idea/finding-and-replacing-text-in-file.html). | `Ctrl+F` `Ctrl+R` | -| [在项目中或指定的目录中查找或替换](https://www.jetbrains.com/help/idea/finding-and-replacing-text-in-project.html) | `Ctrl+Shift+F` `Ctrl+Shift+R` | -| [全局搜索](https://www.jetbrains.com/help/idea/searching-everywhere.htmls) | 双击 `Shift` | -| [快速查看选中对象的引用](https://www.jetbrains.com/help/idea/highlighting-usages.html). | `Ctrl+Shift+F7` | -| [展开或折叠编辑器中的代码块](https://www.jetbrains.com/help/idea/code-folding.html). | `Ctrl+NumPad Plus` `Ctrl+NumPad -` | -| [调用代码完成](https://www.jetbrains.com/help/idea/auto-completing-code.html#basic_completion). | `Ctrl+Space` | -| [智能声明完成](https://www.jetbrains.com/help/idea/auto-completing-code.html#statements_completion). | `Ctrl+Shift+Enter` | -| [智能补全代码](https://www.jetbrains.com/help/idea/auto-completing-code.html#smart_completion) | `Ctrl+Shift+Space` | -| 显示可用的[重构](https://www.jetbrains.com/help/idea/refactoring-source-code.html)方法列表 | `Ctrl+Shift+Alt+T` - -### 快捷键分类 - -#### Tradition - -| 快捷键 | 介绍 | -| ----------------- | ---------------- | -| Ctrl + Z | 撤销 | -| Ctrl + Shift + Z | 取消撤销 | -| Ctrl + X | 剪切 | -| Ctrl + C | 复制 | -| Ctrl + S | 保存 | -| Tab | 缩进 | -| Shift + Tab | 取消缩进 | -| Shift + Home/End | 选中光标到当前行头位置/行尾位置 | -| Ctrl + Home/End | 跳到文件头/文件尾 | - -#### Editing - -| 快捷键 | 介绍 | -| ----------------------- | ---------------------------------------- | -| Ctrl + Space | 基础代码补全,默认在 Windows 系统上被输入法占用,需要进行修改,建议修改为 Ctrl + 逗号`(必备)` | -| Ctrl + Alt + Space | 类名自动完成 | -| Ctrl + Shift + Enter | 自动结束代码,行末自动添加分号`(必备)` | -| Ctrl + P | 方法参数提示显示 | -| Ctrl + Q | 光标所在的变量/类名/方法名等上面(也可以在提示补充的时候按),显示文档内容 | -| Shift + F1 | 如果有外部文档可以连接外部文档 | -| Ctrl + F1 | 在光标所在的错误代码处显示错误信息`(必备)` | -| Alt + Insert | 代码自动生成,如生成对象的 set/get 方法,构造函数,toString() 等`(必备)` | -| Ctrl + O | 选择可重写的方法 | -| Ctrl + I | 选择可继承的方法 | -| Ctrl + Alt + T | 对选中的代码弹出环绕选项弹出层`(必备)` | -| Ctrl + / | 注释光标所在行代码,会根据当前不同文件类型使用不同的注释符号`(必备)` | -| Ctrl + Shift + / | 代码块注释`(必备)` | -| Ctrl + W | 递进式选择代码块。可选中光标所在的单词或段落,连续按会在原有选中的基础上再扩展选中范围`(必备)` | -| Ctrl + Shift + W | 递进式取消选择代码块。可选中光标所在的单词或段落,连续按会在原有选中的基础上再扩展取消选中范围`(必备)` | -| Alt + Q | 弹出一个提示,显示当前类的声明/上下文信息 | -| Alt + Enter | IntelliJ IDEA 根据光标所在问题,提供快速修复选择,光标放在的位置不同提示的结果也不同`(必备)` | -| Ctrl + Alt + L | 格式化代码,可以对当前文件和整个包目录使用`(必备)` | -| Ctrl + Alt + O | 优化导入的类,可以对当前文件和整个包目录使用`(必备)` | -| Ctrl + Alt + I | 光标所在行 或 选中部分进行自动代码缩进,有点类似格式化 | -| Ctrl + Shift + C | 复制当前文件磁盘路径到剪贴板`(必备)` | -| Ctrl + Shift + V | 弹出缓存的最近拷贝的内容管理器弹出层 | -| Ctrl + Alt + Shift + C | 复制参考信息 | -| Ctrl + Alt + Shift + V | 无格式黏贴`(必备)` | -| Ctrl + D | 复制光标所在行 或 复制选择内容,并把复制内容插入光标位置下面`(必备)` | -| Ctrl + Y | 删除光标所在行 或 删除选中的行`(必备)` | -| Ctrl + Shift + J | 自动将下一行合并到当前行末尾`(必备)` | -| Shift + Enter | 开始新一行。光标所在行下空出一行,光标定位到新行位置`(必备)` | -| Ctrl + Shift + U | 对选中的代码进行大/小写轮流转换`(必备)` | -| Ctrl + Shift + ]/[ | 选中从光标所在位置到它的底部/顶部的中括号位置`(必备)` | -| Ctrl + Delete | 删除光标后面的单词或是中文句`(必备)` | -| Ctrl + BackSpace | 删除光标前面的单词或是中文句`(必备)` | -| Ctrl + +/- | 展开/折叠代码块 | -| Ctrl + Shift + +/- | 展开/折叠所有代码`(必备)` | -| Ctrl + F4 | 关闭当前编辑文件 | -| Ctrl + Shift + Up/Down | 光标放在方法名上,将方法移动到上一个/下一个方法前面,调整方法排序`(必备)` | -| Alt + Shift + Up/Down | 移动光标所在行向上移动/向下移动`(必备)` | -| Ctrl + Shift + 左键单击 | 把光标放在某个类变量上,按此快捷键可以直接定位到该类中`(必备)` | -| Alt + Shift + 左键双击 | 选择被双击的单词/中文句,按住不放,可以同时选择其他单词/中文句`(必备)` | -| Ctrl + Shift + T | 对当前类生成单元测试类,如果已经存在的单元测试类则可以进行选择`(必备)` | - -#### Search/Replace - -| 快捷键 | 介绍 | -| ----------------- | ------------------------------------- | -| Double Shift | 弹出 Search Everywhere 弹出层 | -| F3 | 在查找模式下,定位到下一个匹配处 | -| Shift + F3 | 在查找模式下,查找匹配上一个 | -| Ctrl + F | 在当前文件进行文本查找`(必备)` | -| Ctrl + R | 在当前文件进行文本替换`(必备)` | -| Ctrl + Shift + F | 根据输入内容查找整个项目 或 指定目录内文件`(必备)` | -| Ctrl + Shift + R | 根据输入内容替换对应内容,范围为整个项目 或 指定目录内文件`(必备)` | - -#### Usage Search - -| 快捷键 | 介绍 | -| ------------------ | ---------------------------------- | -| Alt + F7 | 查找光标所在的方法/变量/类被调用的地方 | -| Ctrl + Alt + F7 | 显示使用的地方。寻找被该类或是变量被调用的地方,用弹出框的方式找出来 | -| Ctrl + Shift + F7 | 高亮显示所有该选中文本,按Esc高亮消失`(必备)` | - -#### Compile and Run - -| 快捷键 | 介绍 | -| ------------------ | ------------------ | -| Ctrl + F9 | 执行 Make Project 操作 | -| Ctrl + Shift + F9 | 编译选中的文件/包/Module | -| Shift + F9 | Debug | -| Shift + F10 | Run | -| Alt + Shift + F9 | 弹出 Debug 的可选择菜单 | -| Alt + Shift + F10 | 弹出 Run 的可选择菜单 | - -#### Debugging - -| 快捷键 | 介绍 | -| ------------------ | ---------------------------------------- | -| F7 | 在 Debug 模式下,进入下一步,如果当前行断点是一个方法,则进入当前方法体内,如果该方法体还有方法,则不会进入该内嵌的方法中 | -| F8 | 在 Debug 模式下,进入下一步,如果当前行断点是一个方法,则不进入当前方法体内 | -| Shift + F7 | 在 Debug 模式下,智能步入。断点所在行上有多个方法调用,会弹出进入哪个方法 | -| Shift + F8 | 在 Debug 模式下,跳出,表现出来的效果跟 F9 一样 | -| Alt + F8 | 在 Debug 模式下,选中对象,弹出可输入计算表达式调试框,查看该输入内容的调试结果 | -| Alt + F9 | 在 Debug 模式下,执行到光标处 | -| F9 | 在 Debug 模式下,恢复程序运行,但是如果该断点下面代码还有断点则停在下一个断点上 | -| Ctrl + F8 | 在 Debug 模式下,设置光标当前行为断点,如果当前已经是断点则去掉断点 | -| Ctrl + Shift + F8 | 在 Debug 模式下,指定断点进入条件 | - -#### Navigation - -| 快捷键 | 介绍 | -| ------------------------- | ---------------------------------------- | -| Ctrl + N | 跳转到类`(必备)` | -| Ctrl + Shift + N | 跳转到文件`(必备)` | -| Ctrl + Alt + Shift + N | 跳转到符号`(必备)` | -| Alt + Left/Right | 切换当前已打开的窗口中的子视图,比如Debug窗口中有Output、Debugger等子视图,用此快捷键就可以在子视图中切换`(必备)` | -| F12 | 回到前一个工具窗口`(必备)` | -| ESC | 从工具窗口进入代码文件窗口`(必备)` | -| Shift + ESC | 隐藏当前 或 最后一个激活的工具窗口 | -| Ctrl + G | 跳转到当前文件的指定行处 | -| Ctrl + E | 显示最近打开的文件记录列表`(必备)` | -| Ctrl + Shift + E | 显示最近编辑的文件记录列表`(必备)` | -| Ctrl + Alt + Left/Right | 跳转到上一个/下一个操作的地方`(必备)` | -| Ctrl + Shift + Backspace | 退回到上次修改的地方`(必备)` | -| Alt + F1 | 显示当前文件选择目标弹出层,弹出层中有很多目标可以进行选择`(必备)` | -| Ctrl + B/Ctrl + 左键单击 | 跳转到声明处 | -| Ctrl + Alt + B | 在某个调用的方法名上使用会跳到具体的实现处,可以跳过接口 | -| Ctrl + Shift + B | 跳转到类型声明处`(必备)` | -| Ctrl + Shift + I | 快速查看光标所在的方法 或 类的定义 | -| Ctrl + U | 前往当前光标所在的方法的父类的方法/接口定义`(必备)` | -| Alt + Up/Down | 跳转到当前文件的前一个/后一个方法`(必备)` | -| Ctrl + ]/[ | 跳转到当前所在代码的花括号结束位置/开始位置 | -| Ctrl + F12 | 弹出当前文件结构层,可以在弹出的层上直接输入,进行筛选 | -| Ctrl + H | 显示当前类的层次结构 | -| Ctrl + Shift + H | 显示方法层次结构 | -| Ctrl + Alt + H | 调用层次 | -| F2/Shift + F2 | 跳转到下一个/上一个高亮错误 或 警告位置`(必备)` | -| F4 | 编辑源`(必备)` | -| Alt + Home | 定位/显示到当前文件的 Navigation Bar | -| F11 | 添加书签`(必备)` | -| Ctrl + F11 | 选中文件/文件夹,使用助记符设定/取消书签`(必备)` | -| Shift + F11 | 弹出书签显示层`(必备)` | -| Alt + 1,2,3...9 | 显示对应数值的选项卡,其中 1 是 Project 用得最多`(必备)` | -| Ctrl + 1,2,3...9 | 定位到对应数值的书签位置`(必备)` | - -#### Refactoring - -| 快捷键 | 介绍 | -| ----------------------- | ----------------- | -| Shift + F6 | 对文件/文件夹 重命名`(必备)` | -| Ctrl + Alt + Shift + T | 打开重构菜单`(必备)` | - -#### VCS/Local History - -| 快捷键 | 介绍 | -| ---------------------------------------- | ------------------------- | -| Ctrl + K | 版本控制提交项目,需要此项目有加入到版本控制才可用 | -| Ctrl + T | 版本控制更新项目,需要此项目有加入到版本控制才可用 | -| Alt + ` | 显示版本控制常用操作菜单弹出层`(必备)` | | -| Alt + Shift + C | 查看最近操作项目的变化情况列表 | -| Alt + Shift + N | 选择/添加 task`(必备)` | - -#### Live Templates - -| 快捷键 | 介绍 | -| -------------- | ---------------------- | -| Ctrl + J | 插入自定义动态代码模板`(必备)` | -| Ctrl + Alt + J | 弹出模板选择窗口,将选定的代码加入动态模板中 | - -#### General - -| 快捷键 | 介绍 | -| ----------------------- | ------------------------------------- | -| Ctrl + Tab | 编辑窗口切换,如果在切换的过程又加按上delete,则是关闭对应选中的窗口 | -| Ctrl + Alt + Y | 同步、刷新 | -| Ctrl + Alt + S | 打开 IntelliJ IDEA 系统设置`(必备)` | -| Ctrl + Alt + Shift + S | 打开当前项目设置`(必备)` | -| Ctrl + Shift + A | 查找动作/设置`(必备)` | -| Ctrl + Shift + F12 | 编辑器最大化`(必备)` | -| Alt + Shift + F | 显示添加到收藏夹弹出层/添加到收藏夹 | -| Alt + Shift + I | 查看项目当前文件 | - -### Intellij IDEA 官方快捷键表 - -![Paste_Image.png](http://upload-images.jianshu.io/upload_images/3101171-6a44121ae280a10e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) - -## 插件 - -推荐几个比较好用的插件 - -- [Key promoter](https://plugins.jetbrains.com/plugin/4455?pr=idea) 快捷键提示 -- [CamelCase](https://plugins.jetbrains.com/plugin/7160?pr=idea) 驼峰式命名和下划线命名交替变化 -- [CheckStyle-IDEA](https://plugins.jetbrains.com/plugin/1065?pr=idea) 代码规范检查 -- [FindBugs-IDEA](https://plugins.jetbrains.com/plugin/3847?pr=idea)潜在 Bug 检查 -- [MetricsReloaded](https://plugins.jetbrains.com/plugin/93?pr=idea) 代码复杂度检查 -- [Statistic](https://plugins.jetbrains.com/plugin/4509?pr=idea) 代码统计 -- [JRebel Plugin](https://plugins.jetbrains.com/plugin/?id=4441) 热部署 -- [GsonFormat](https://plugins.jetbrains.com/plugin/7654?pr=idea) 把 JSON 字符串直接实例化成类 -- [Eclipse Code Formatter](https://plugins.jetbrains.com/plugin/6546-eclipse-code-formatter) 如果你以前用的是IDE,并有自己的一套代码风格配置,可以通过此插件导入到 IDEA -- [Alibaba Java Coding Guidelines](https://plugins.jetbrains.com/plugin/10046-alibaba-java-coding-guidelines) 阿里 Java 开发规范的静态检查工具 -- [IDE Features Trainer](https://plugins.jetbrains.com/plugin/8554-ide-features-trainer) 官方的新手训练插件 -- [Markdown Navigator](https://plugins.jetbrains.com/plugin/7896-markdown-navigator) Markdown 插件,适用于喜欢用 markdown 写文档的人 - -## 个性化 - -### 颜色主题 - -[intellij-colors-solarized](https://github.com/jkaving/intellij-colors-solarized) 个人觉得这种色彩搭配十分优雅 - -下载地址:https://github.com/altercation/solarized - -![solarized show](https://github.com/altercation/solarized/raw/master/img/solarized-vim.png) - -## 破解 - -Intellij 是一个收费的 IDE,坦白说有点小贵,买不起。 - -所以,很惭愧,只好用下破解方法了。网上有很多使用注册码的网文,但是注册码不稳定,随时可能被封。还是自行搭建一个注册服务器比较稳定。我使用了 [ilanyu](http://blog.lanyus.com/) 博文 [IntelliJ IDEA License Server本地搭建教程](http://blog.lanyus.com/archives/174.html) 的方法,亲测十分有效。 - -我的备用地址:[百度云盘](https://yun.baidu.com/disk/home?#list/vmode=list&path=%2F%E8%BD%AF%E4%BB%B6%2F%E5%BC%80%E5%8F%91%E8%BD%AF%E4%BB%B6%2FIDE) - -下载并解压文中的压缩包到本地,选择适合操作系统的版本运行。 - -如果是在 Linux 上运行,推荐创建一个脚本,代码如下: - -```sh -# 使用 nohup 创建守护进程,运行 IntelliJIDEALicenseServer_linux_amd64 -# 如果运行在其他 Linux 发行版本,替换执行的脚本即可 -nohup sh IntelliJIDEALicenseServer_linux_amd64 2>&1 -``` - -这样做是因为:大部分人使用 linux 是使用仿真器连接虚拟机,如果断开连接,进程也会被 kill,每次启动这个注册服务器很麻烦不是吗?而启动了守护进程,则不会出现这种情况,只有你主动 kill 进程才能将其干掉。 - -Windows 版本是 exe 程序,将其设为开机自动启动即可,别告诉我你不知道怎么设置开机自动启动。 - -## 参考资料 - -[IntelliJ-IDEA-Tutorial](https://github.com/judasn/IntelliJ-IDEA-Tutorial) - -[极客学院 - Intellij IDEA 使用教程](http://wiki.jikexueyuan.com/project/intellij-idea-tutorial/) diff --git a/docs/javatool/server/README.md b/docs/javatool/server/README.md deleted file mode 100644 index c80cd6cb..00000000 --- a/docs/javatool/server/README.md +++ /dev/null @@ -1 +0,0 @@ -# Java 服务器 diff --git a/docs/javatool/server/jetty.md b/docs/javatool/server/jetty.md deleted file mode 100644 index 82e507ef..00000000 --- a/docs/javatool/server/jetty.md +++ /dev/null @@ -1,251 +0,0 @@ ---- -title: Jetty 使用小结 -date: 2017/11/08 -categories: -- javatool -tags: -- java -- javatool -- server ---- - -# Jetty 使用小结 - -## 概述 - -**jetty是什么?** - -jetty是轻量级的web服务器和servlet引擎。 - -它的最大特点是:可以很方便的作为**嵌入式服务器**。 - -它是eclipse的一个开源项目。不用怀疑,就是你常用的那个eclipse。 - -它是使用Java开发的,所以天然对Java支持良好。 - -[官方网址](http://www.eclipse.org/jetty/index.html) - -[github源码地址](https://github.com/eclipse/jetty.project) - -**什么是嵌入式服务器?** - -以jetty来说明,就是只要引入jetty的jar包,可以通过直接调用其API的方式来启动web服务。 - -用过Tomcat、Resin等服务器的朋友想必不会陌生那一套安装、配置、部署的流程吧,还是挺繁琐的。使用jetty,就不需要这些过程了。 - -jetty非常适用于项目的开发、测试,因为非常快捷。如果想用于生产环境,则需要谨慎考虑,它不一定能像成熟的Tomcat、Resin等服务器一样支持企业级Java EE的需要。 - -## jetty的嵌入式启动 - -我觉得嵌入式启动方式的一个好处在于:可以直接运行项目,无需每次部署都得再配置服务器。 - -jetty的嵌入式启动使用有两种方式: - -API方式 - -maven插件方式 - -### API方式 - -添加maven依赖 - -```xml -org.eclipse.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/atlantis1024/SpringNotes/tree/master/spring-helloworld) - -### 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/javatool/server/tomcat/README.md b/docs/javatool/server/tomcat/README.md deleted file mode 100644 index b16a8da8..00000000 --- a/docs/javatool/server/tomcat/README.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: Tomcat -date: 2018-01-08 -categories: -- javatool -tags: -- java -- javatool -- server ---- - -# Tomcat - -Tomcat 版本比对 - -| **Servlet Spec** | **JSP Spec** | **EL Spec** | **WebSocket Spec** | **JASPIC Spec** | **Apache Tomcat Version** | **Latest Released Version** | **Supported Java Versions** | -| ---------------- | ------------ | ----------- | ------------------ | --------------- | ------------------------- | --------------------------- | -------------------------------------- | -| 4.0 | 2.3 | 3.0 | 1.1 | 1.1 | 9.0.x | 9.0.2 (beta) | 8 and later | -| 3.1 | 2.3 | 3.0 | 1.1 | 1.1 | 8.5.x | 8.5.24 | 7 and later | -| 3.1 | 2.3 | 3.0 | 1.1 | N/A | 8.0.x (superseded) | 8.0.48 (superseded) | 7 and later | -| 3.0 | 2.2 | 2.2 | 1.1 | N/A | 7.0.x | 7.0.82 | 6 and later(7 and later for WebSocket) | -| 2.5 | 2.1 | 2.1 | N/A | N/A | 6.0.x (archived) | 6.0.53 (archived) | 5 and later | -| 2.4 | 2.0 | N/A | N/A | N/A | 5.5.x (archived) | 5.5.36 (archived) | 1.4 and later | -| 2.3 | 1.2 | N/A | N/A | N/A | 4.1.x (archived) | 4.1.40 (archived) | 1.3 and later | -| 2.2 | 1.1 | N/A | N/A | N/A | 3.3.x (archived) | 3.3.2 (archived) | 1.1 and later | - -## 参考 - -- [Tomcat 官网](https://tomcat.apache.org/index.html) \ No newline at end of file diff --git a/docs/javatool/server/tomcat/images/tomcat-intellij-run-config.png b/docs/javatool/server/tomcat/images/tomcat-intellij-run-config.png deleted file mode 100644 index 9861699d..00000000 Binary files a/docs/javatool/server/tomcat/images/tomcat-intellij-run-config.png and /dev/null differ diff --git a/docs/javatool/server/tomcat/images/tomcat.png b/docs/javatool/server/tomcat/images/tomcat.png deleted file mode 100644 index a07cb9f1..00000000 Binary files a/docs/javatool/server/tomcat/images/tomcat.png and /dev/null differ diff --git a/docs/javatool/server/tomcat/tomcat-how-to-work.md b/docs/javatool/server/tomcat/tomcat-how-to-work.md deleted file mode 100644 index e0b286d2..00000000 --- a/docs/javatool/server/tomcat/tomcat-how-to-work.md +++ /dev/null @@ -1,274 +0,0 @@ ---- -title: Tomcat 工作原理 -date: 2018-01-10 -categories: -- javatool -tags: -- java -- javatool -- server ---- - -# Tomcat 工作原理 - -## Tomcat 组成 - -![img](https://dn-mhke0kuv.qbox.me/9ccc3ed9de0df39faa1e.jpeg?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -**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 组成 - -![img](https://dn-mhke0kuv.qbox.me/1a2613edf5779c7bf184.jpeg?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -- **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 状态; - -![img](https://dn-mhke0kuv.qbox.me/75e7563785c89a252f3f.jpeg?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -Tomcat 的生命周期管理引入了事件机制,在组件或容器的生命周期状态发生变化时会通 知事件监听器,监听器通过判断事件的类型来进行相应的操作。 -事件监听器的添加可以在 server.xml 文件中进行配置; - -Tomcat 各类容器的配置过程就是通过添加 listener 的方式来进行的,从而达到配置逻辑与 容器的解耦。如 EngineConfig、HostConfig、ContextConfig。 - -- EngineConfig:主要打印启动和停止日志 -- HostConfig:主要处理部署应用,解析应用 META-INF/context.xml 并创建应用的 Context。 -- ContextConfig:主要解析并合并 web.xml,扫描应用的各类 web 资源 (filter、servlet、listener)。 - -![img](https://dn-mhke0kuv.qbox.me/1ea5e727c9ad4ca37e05.jpeg?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -### Tomcat 的启动过程 - -![img](https://dn-mhke0kuv.qbox.me/94989563f76b0c2b6b19.jpeg?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -启动从 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 方法来等待执行结果)。 - -``` -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 标志位来控制) - -### 请求处理过程 - -![img](https://dn-mhke0kuv.qbox.me/36a5730697cd0e18a7f5.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -1. 根据 server.xml 配置的指定的 connector 以及端口监听 http、或者 ajp 请求 -2. 请求到来时建立连接,解析请求参数,创建 Request 和 Response 对象,调用顶层容器 pipeline 的 invoke 方法 -3. 容器之间层层调用,最终调用业务 servlet 的 service 方法 -4. Connector 将 response 流中的数据写到 socket 中 - -### Pipeline 与 Valve - -![img](https://dn-mhke0kuv.qbox.me/76ce4dd2ecb33beabdbd.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -Pipeline 可以理解为现实中的管道,Valve 为管道中的阀门,Request 和 Response 对象在管道中 经过各个阀门的处理和控制。 - -每个容器的管道中都有一个必不可少的 basic valve,其他的都是可选的,basic valve 在管道中最 后调用,同时负责调用子容器的第一个 valve。 - -Valve 中主要的三个方法:setNext、getNext、invoke;valve 之间的关系是单向链式结构,本身 invoke 方法中会调用下一个 valve 的 invoke 方法。 - -各层容器对应的 basic valve 分别是 StandardEngineValve、StandardHostValve、 StandardContextValve、StandardWrapperValve。 - -## Connector - -![img](https://dn-mhke0kuv.qbox.me/edd423fe65e74312df50.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -阻塞 IO - -![img](https://dn-mhke0kuv.qbox.me/23ec3d5eb0c760ea277f.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -**非阻塞 IO** - -![img](https://dn-mhke0kuv.qbox.me/344773ecc6b8e38b8892.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -** IO多路复用** - -![img](https://dn-mhke0kuv.qbox.me/c5b59386908b65c3fcad.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -阻塞与非阻塞的区别在于进行读操作和写操作的系统调用时,如果此时内核态没有数据可读或者没有缓冲空间可写时,是否阻塞。 - -IO多路复用的好处在于可同时监听多个socket的可读和可写事件,这样就能使得应用可以同时监听多个socket,释放了应用线程资源。 - -**Tomcat各类Connector对比** - -![img](https://dn-mhke0kuv.qbox.me/9123bf19b3402b447bed.jpeg?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -- 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处理相关类** - -![img](https://dn-mhke0kuv.qbox.me/36ca2efea0d5318dc6d1.jpeg?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -Poller线程从EventQueue获取PollerEvent,并执行PollerEvent的run方法,调用Selector的select方法,如果有可读的Socket则创建Http11NioProcessor,放入到线程池中执行; - -CoyoteAdapter是Connector到Container的适配器,Http11NioProcessor调用其提供的service方法,内部创建Request和Response对象,并调用最顶层容器的Pipeline中的第一个Valve的invoke方法 - -Mapper主要处理http url 到servlet的映射规则的解析,对外提供map方法 - -**NIO Connector主要参数** - -![img](https://dn-mhke0kuv.qbox.me/6a25927b428e2e6db858.jpeg?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -## Comet - -Comet是一种用于web的推送技术,能使服务器实时地将更新的信息传送到客户端,而无须客户端发出请求 -在WebSocket出来之前,如果不适用comet,只能通过浏览器端轮询Server来模拟实现服务器端推送。 -Comet支持servlet异步处理IO,当连接上数据可读时触发事件,并异步写数据(阻塞) - -![img](https://dn-mhke0kuv.qbox.me/030676ad4f439effcd6f.jpeg?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -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 - -![img](https://dn-mhke0kuv.qbox.me/11e26c65a2cdc42e8f05.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -传统流程: - -- 首先,Servlet 接收到请求之后,request数据解析; -- 接着,调用业务接口的某些方法,以完成业务处理; -- 最后,根据处理的结果提交响应,Servlet 线程结束 - -![img](https://dn-mhke0kuv.qbox.me/7c352f6a0239331ec91f.png?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -异步处理流程: - -- 客户端发送一个请求 -- 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 组成与工作原理](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/javatool/server/tomcat/tomcat-quickstart.md b/docs/javatool/server/tomcat/tomcat-quickstart.md deleted file mode 100644 index 960af4ea..00000000 --- a/docs/javatool/server/tomcat/tomcat-quickstart.md +++ /dev/null @@ -1,363 +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 是由 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 和组成应用的其它组件,以及应用初始化参数、安全管理约束等。 - -## 安装 - -**前提条件** - -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](images/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/java-stack/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 中设置上下文名称)。 - -![](images/tomcat-intellij-run-config.png) - -> **说明** -> -> 个人认为这个插件不如 Eclipse 的 Tomcat 插件好用,Eclipse 的 Tomcat 插件支持对 Tomcat xml 配置文件进行配置。而这里,你只能自己去 Tomcat 安装路径下修改配置文件。 - -## 资料 - -- [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) - -## 推荐 - -文中的嵌入式启动示例可以参考[**我的示例项目**](https://github.com/dunwu/java-stack/tree/master/codes/javatool/server) \ No newline at end of file diff --git a/docs/others/how-to-learn.md b/docs/others/how-to-learn.md deleted file mode 100644 index 6a630efa..00000000 --- a/docs/others/how-to-learn.md +++ /dev/null @@ -1,13 +0,0 @@ -# 如何学习 - -## 学习步骤 - -1. 创建 `resources.md` 文件,收集学习资源。 -2. ​了解技术的边界: - -个人比较推崇 [5W2H分析法](http://wiki.mbalib.com/zh-tw/5W2H%E5%88%86%E6%9E%90%E6%B3%95) 来管理学习、工作。 - - - - - diff --git a/package.json b/package.json index cf21a677..93a7223a 100644 --- a/package.json +++ b/package.json @@ -1,24 +1,41 @@ { - "name": "java-stack", - "description": "Java 技术栈", + "name": "java-tutorial", + "version": "1.0.0", + "private": true, "scripts": { - "clean": "rimraf _book", - "prepare": "gitbook install", - "build": "gitbook build ./ --log=debug --debug", - "start": "gitbook serve" + "clean": "rimraf docs/.temp", + "start": "node --max_old_space_size=4096 ./node_modules/vuepress/cli.js dev docs", + "build": "node --max_old_space_size=4096 ./node_modules/vuepress/cli.js build docs", + "deploy": "bash scripts/deploy.sh", + "updateTheme": "yarn remove vuepress-theme-vdoing && rm -rf node_modules && yarn && yarn add vuepress-theme-vdoing -D", + "editFm": "node utils/editFrontmatter.js", + "lint": "markdownlint -r markdownlint-rule-emphasis-style -c docs/.markdownlint.json **/*.md -i node_modules", + "lint:fix": "markdownlint -f -r markdownlint-rule-emphasis-style -c docs/.markdownlint.json **/*.md -i node_modules", + "show-help": "vuepress --help", + "view-info": "vuepress view-info ./ --temp docs/.temp" }, - "repository": { - "type": "git", - "url": "https://github.com/dunwu/java-stack.git" - }, - "author": "Zhang Peng ", - "license": "MIT", - "bugs": { - "url": "https://github.com/dunwu/java-stack/issues" - }, - "homepage": "https://github.com/dunwu/java-stack", "devDependencies": { - "gitbook-cli": "^2.3.2", - "rimraf": "^2.6.1" + "dayjs": "^1.11.7", + "inquirer": "^9.1.4", + "json2yaml": "^1.1.0", + "markdownlint-cli": "^0.33.0", + "markdownlint-rule-emphasis-style": "^1.0.1", + "rimraf": "^4.1.2", + "vue-toasted": "^1.1.25", + "vuepress": "1.9.9", + "vuepress-plugin-baidu-tongji": "^1.0.1", + "vuepress-plugin-comment": "^0.7.3", + "vuepress-plugin-demo-block": "^0.7.2", + "vuepress-plugin-flowchart": "^1.4.2", + "vuepress-plugin-fulltext-search": "^2.2.1", + "vuepress-plugin-one-click-copy": "^1.0.2", + "vuepress-plugin-thirdparty-search": "^1.0.2", + "vuepress-plugin-zooming": "^1.1.7", + "vuepress-theme-vdoing": "^1.12.9", + "yamljs": "^0.3.0", + "markdownlint-cli": "^0.25.0", + "markdownlint-rule-emphasis-style": "^1.0.1", + "rimraf": "^3.0.1", + "vue-toasted": "^1.1.25" } } diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..2c6bc3d8 --- /dev/null +++ b/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + io.github.dunwu.java + java-tutorial + 1.0.0 + pom + JAVA-TUTORIAL + + + codes/java-distributed + codes/javaee + codes/javatech + codes/javatool + codes/trouble-shooting + codes/deadloop + + diff --git a/prettier.config.js b/prettier.config.js new file mode 100644 index 00000000..c9848e74 --- /dev/null +++ b/prettier.config.js @@ -0,0 +1,10 @@ +/** + * @see https://prettier.io/docs/en/options.html + * @see https://prettier.io/docs/en/configuration.html + */ +module.exports = { + tabWidth: 2, + semi: false, + singleQuote: true, + trailingComma: 'none' +} diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 00000000..b6ee6ee0 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env sh + +# ------------------------------------------------------------------------------ +# gh-pages 部署脚本 +# @author Zhang Peng +# @since 2020/2/10 +# ------------------------------------------------------------------------------ + +# 装载其它库 +ROOT_DIR=$( + cd $(dirname $0)/.. + pwd +) + +# 确保脚本抛出遇到的错误 +set -e + +# 生成静态文件 +npm run build + +# 进入生成的文件夹 +cd ${ROOT_DIR}/docs/.temp + +# 如果是发布到自定义域名 +# echo 'www.example.com' > CNAME + +#if [[ ${GITHUB_TOKEN} && ${GITEE_TOKEN} ]]; then +if [[ ${GITHUB_TOKEN} ]]; then + msg='自动部署' + GITHUB_URL=https://dunwu:${GITHUB_TOKEN}@github.com/dunwu/java-tutorial.git +# GITEE_URL=https://turnon:${GITEE_TOKEN}@gitee.com/turnon/java-tutorial.git + git config --global user.name "dunwu" + git config --global user.email "forbreak@163.com" +else + msg='手动部署' + GITHUB_URL=git@github.com:dunwu/java-tutorial.git +# GITEE_URL=git@gitee.com:turnon/java-tutorial.git +fi +git init +git add -A +git commit -m "${msg}" +# 推送到github gh-pages分支 +git push -f "${GITHUB_URL}" master:gh-pages +#git push -f "${GITEE_URL}" master:gh-pages + +cd - +rm -rf ${ROOT_DIR}/docs/.temp diff --git a/scripts/embed-tomcat-server-boot.sh b/scripts/embed-tomcat-server-boot.sh index 9548cd68..4214f068 100644 --- a/scripts/embed-tomcat-server-boot.sh +++ b/scripts/embed-tomcat-server-boot.sh @@ -5,8 +5,8 @@ # 删除旧日志文件 removeOldLog() { mkdir -p /home/zp/log/startup - if [ -f $LOG_FILE ];then - rm -rf $LOG_FILE + if [ -f $LOG_FILE ]; then + rm -rf $LOG_FILE fi } @@ -26,12 +26,12 @@ checkInput() { # 检查服务是否已经启动 PIDS="" checkStarted() { - PIDS=`ps -ef | grep java | grep ${app} | awk '{print $2}'` - if [ -n "$PIDS" ]; then - return 0 - else - return 1 - fi + PIDS=`ps -ef | grep java | grep ${app} | awk '{print $2}'` + if [ -n "$PIDS" ]; then + return 0 + else + return 1 + fi } execOper() { @@ -39,11 +39,11 @@ execOper() { start) echo -n "starting server: " #检查服务是否已经启动 -# if checkStarted ;then -# echo "ERROR: server already started!" -# echo "PID: $PIDS" -# exit 1 -# fi + # if checkStarted ;then + # echo "ERROR: server already started!" + # echo "PID: $PIDS" + # exit 1 + # fi args="${javaArgs} -classpath ${classpathArgs} ${bootstrapClass}" #echo -e "启动参数:\n${args}" @@ -52,22 +52,22 @@ execOper() { nohup java ${args} > ${LOG_FILE} 2>&1 & # echo -e "执行参数:\n${args}" echo -e "\nthe server is started..." - ;; + ;; stop) echo -n "stopping server: " #dubbo提供优雅停机, 不能使用kill -9 - if checkStarted ;then + if checkStarted; then kill $PIDS echo -e "\nthe server is stopped..." else echo -e "\nno server to be stopped..." fi - ;; + ;; restart) $0 ${app} stop "${javaArgs}" "${classpathArgs}" "${bootstrapClass}" sleep 5 $0 ${app} start "${javaArgs}" "${classpathArgs}" "${bootstrapClass}" - ;; + ;; *) echo "Invalid oper: ${oper}." exit 1 diff --git a/scripts/git-clone.sh b/scripts/git-clone.sh index 53ae0ec8..fc3abd77 100644 --- a/scripts/git-clone.sh +++ b/scripts/git-clone.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/usr/bin/env bash # # 检查脚本参数,如必要参数未传入,退出脚本。 diff --git a/scripts/init.sh b/scripts/init.sh index e0933a21..2556e38b 100644 --- a/scripts/init.sh +++ b/scripts/init.sh @@ -1,16 +1,17 @@ #!/usr/bin/env bash + ################################################################################# # javatool-server 项目初始化脚本 -# 执行本脚本后,会将 java-stack 下载到 /home/zp/source/java-stack 目录下。 +# 执行本脚本后,会将 JavaStack 下载到 /home/zp/source/JavaStack 目录下。 # 环境中必须安装了 git ################################################################################# rm -rf /home/temp -rm -rf /home/zp/source/java-stack +rm -rf /home/zp/source/JavaStack mkdir -p /home/temp cd /home/temp -wget https://raw.githubusercontent.com/dunwu/java-stack/master/scripts/git-clone.sh +wget https://raw.githubusercontent.com/dunwu/JavaStack/master/scripts/git-clone.sh chmod 777 git-clone.sh -./git-clone.sh java-stack master -chmod 777 -R /home/zp/source/java-stack +./git-clone.sh JavaStack master +chmod 777 -R /home/zp/source/JavaStack rm -rf /home/temp diff --git a/scripts/javatool-server-release.sh b/scripts/javatool-server-release.sh index 0fa07407..b2444df6 100644 --- a/scripts/javatool-server-release.sh +++ b/scripts/javatool-server-release.sh @@ -19,7 +19,7 @@ checkInput() { # 检查文件是否存在,不存在则退出脚本 # checkFileExist() { - if [ ! -f "$1" ];then + if [ ! -f "$1" ]; then echo "关键文件 $1 找不到,脚本执行结束" exit 0 fi @@ -29,7 +29,7 @@ checkFileExist() { # 检查目录是否存在,不存在则退出脚本 # checkFolderExist() { - if [ ! -d "$1" ];then + if [ ! -d "$1" ]; then echo "关键目录 $1 找不到,脚本执行结束" exit 0 fi @@ -57,11 +57,11 @@ saveVersionInfo() { export LANG="zh_CN.UTF-8" # 设置全局常量 -SOURCE_PATH=/home/zp/source/java-stack -SCRIPT_PATH=/home/zp/source/java-stack/scripts +SOURCE_PATH=/home/zp/source/JavaStack +SCRIPT_PATH=/home/zp/source/JavaStack/scripts # 分配 script 和 config 目录的权限 -chmod -R 755 /home/zp/source/java-stack +chmod -R 755 /home/zp/source/JavaStack # 0. 获取传入参数并检查 branch=`echo $1` @@ -81,7 +81,7 @@ checkFileExist ${SCRIPT_PATH}/embed-tomcat-server-boot.sh # 2. 更新代码 cd ${SOURCE_PATH} -${SCRIPT_PATH}/git-clone.sh java-stack ${branch} +${SCRIPT_PATH}/git-clone.sh JavaStack ${branch} chmod -R 777 ${SOURCE_PATH} # 3. 替换配置 diff --git a/scripts/javatool-server-run.sh b/scripts/javatool-server-run.sh index 88c34349..511dca94 100644 --- a/scripts/javatool-server-run.sh +++ b/scripts/javatool-server-run.sh @@ -23,7 +23,7 @@ checkFileExist() { } # 封装启动参数,调用启动脚本 -execBootScript(){ +execBootScript() { APP_NAME=javatool-server # JVM 参数 @@ -31,8 +31,8 @@ execBootScript(){ JAVA_OPTS=" -Djava.awt.headless=true -Dfile.encoding=UTF8 -Djava.net.preferIPv4Stack=true -Dspring.profiles.active=${profile} -Xms1024m -Xmx1024m -Xss2m " JAVA_DEBUG_OPTS="" if [ "$3" == "debug" ]; then - JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=2235,server=y,suspend=n " - shift + JAVA_DEBUG_OPTS=" -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=2235,server=y,suspend=n " + shift fi javaArgs=" ${JAVA_OPTS} ${JAVA_DEBUG_OPTS} " @@ -60,7 +60,7 @@ export LANG="zh_CN.UTF-8" #export PATH=$JAVA_HOME/bin:$JAVA_HOME/jre/bin:$MAVEN_HOME/bin:$PATH # 关键路径 -SCRIPT_PATH=/home/zp/source/java-stack/scripts -COMPILED_WEBAPP=/home/zp/source/java-stack/codes/javatool/server/target/javatool-server -RESOURCES_PATH=/home/zp/source/java-stack/codes/javatool/server/src/main/resources +SCRIPT_PATH=/home/zp/source/JavaStack/scripts +COMPILED_WEBAPP=/home/zp/source/JavaStack/codes/javatool/server/target/javatool-server +RESOURCES_PATH=/home/zp/source/JavaStack/codes/javatool/server/src/main/resources execBootScript diff --git a/settings/codestyle/google-codestyle.xml b/settings/codestyle/google-codestyle.xml new file mode 100644 index 00000000..f3a6743e --- /dev/null +++ b/settings/codestyle/google-codestyle.xml @@ -0,0 +1,598 @@ + + + + + + diff --git a/settings/codestyle/square-codestyle.xml b/settings/codestyle/square-codestyle.xml new file mode 100644 index 00000000..f7becd5d --- /dev/null +++ b/settings/codestyle/square-codestyle.xml @@ -0,0 +1,318 @@ + + + + + + diff --git a/settings/codestyle/zp-codestyle.xml b/settings/codestyle/zp-codestyle.xml new file mode 100644 index 00000000..270664c6 --- /dev/null +++ b/settings/codestyle/zp-codestyle.xml @@ -0,0 +1,1022 @@ + + + \ No newline at end of file diff --git "a/settings/jmeter/\350\257\273\345\217\226CSV\345\271\266\346\234\211\345\272\217\347\232\204\345\217\221\351\200\201\350\257\267\346\261\202.jmx" "b/settings/jmeter/\350\257\273\345\217\226CSV\345\271\266\346\234\211\345\272\217\347\232\204\345\217\221\351\200\201\350\257\267\346\261\202.jmx" new file mode 100644 index 00000000..8704dde0 --- /dev/null +++ "b/settings/jmeter/\350\257\273\345\217\226CSV\345\271\266\346\234\211\345\272\217\347\232\204\345\217\221\351\200\201\350\257\267\346\261\202.jmx" @@ -0,0 +1,181 @@ + + + + + + false + true + false + + + + + + + + continue + + false + 1 + + 100 + 1 + false + + + + + + + + + localhost + 8080 + http + + + 6 + + + + + + true + + + + false + [{"a":"${a}","b":"${b}"}] + = + + + + + + + + /getMessage + POST + true + false + true + false + + + + + + + + + Content-Type + application/json + + + + + + + 200 + + + Assertion.response_code + false + 8 + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + false + + saveConfig + + + true + true + true + + true + true + true + true + false + true + true + false + false + false + true + false + false + false + true + 0 + true + true + true + true + true + true + + + + + + + , + UTF-8 + D:/Temp/data.csv + true + true + true + shareMode.all + true + a,b + + + + false + false + + + + + + diff --git a/utils/config.yml b/utils/config.yml new file mode 100644 index 00000000..d387646b --- /dev/null +++ b/utils/config.yml @@ -0,0 +1,15 @@ +# 批量添加和修改、删除front matter配置文件 + +# 需要批量处理的路径,docs文件夹内的文件夹 (数组,映射路径:path[0]/path[1]/path[2] ... ) +path: + - docs # 第一个成员必须是docs + +# 要删除的字段 (数组) +delete: + # - tags + + # 要添加、修改front matter的数据 (front matter中没有的数据则添加,已有的数据则覆盖) +data: + # author: + # name: xugaoyi + # link: https://github.com/xugaoyi diff --git a/utils/editFrontmatter.js b/utils/editFrontmatter.js new file mode 100644 index 00000000..0998bf3d --- /dev/null +++ b/utils/editFrontmatter.js @@ -0,0 +1,98 @@ +/** + * 批量添加和修改front matter ,需要配置 ./config.yml 文件。 + */ +const fs = require('fs') // 文件模块 +const path = require('path') // 路径模块 +const matter = require('gray-matter') // front matter解析器 https://github.com/jonschlinkert/gray-matter +const jsonToYaml = require('json2yaml') +const yamlToJs = require('yamljs') +const inquirer = require('inquirer') // 命令行操作 +const chalk = require('chalk') // 命令行打印美化 +const readFileList = require('./modules/readFileList') +const { type, repairDate } = require('./modules/fn') +const log = console.log + +const configPath = path.join(__dirname, 'config.yml') // 配置文件的路径 + +main() + +/** + * 主体函数 + */ +async function main() { + const promptList = [ + { + type: 'confirm', + message: chalk.yellow('批量操作frontmatter有修改数据的风险,确定要继续吗?'), + name: 'edit' + } + ] + let edit = true + + await inquirer.prompt(promptList).then((answers) => { + edit = answers.edit + }) + + if (!edit) { + // 退出操作 + return + } + + const config = yamlToJs.load(configPath) // 解析配置文件的数据转为js对象 + + if (type(config.path) !== 'array') { + log(chalk.red('路径配置有误,path字段应该是一个数组')) + return + } + + if (config.path[0] !== 'docs') { + log(chalk.red("路径配置有误,path数组的第一个成员必须是'docs'")) + return + } + + const filePath = path.join(__dirname, '..', ...config.path) // 要批量修改的文件路径 + const files = readFileList(filePath) // 读取所有md文件数据 + + files.forEach((file) => { + let dataStr = fs.readFileSync(file.filePath, 'utf8') // 读取每个md文件的内容 + const fileMatterObj = matter(dataStr) // 解析md文件的front Matter。 fileMatterObj => {content:'剔除frontmatter后的文件内容字符串', data:{}, ...} + let matterData = fileMatterObj.data // 得到md文件的front Matter + + let mark = false + // 删除操作 + if (config.delete) { + if (type(config.delete) !== 'array') { + log(chalk.yellow('未能完成删除操作,delete字段的值应该是一个数组!')) + } else { + config.delete.forEach((item) => { + if (matterData[item]) { + delete matterData[item] + mark = true + } + }) + } + } + + // 添加、修改操作 + if (type(config.data) === 'object') { + Object.assign(matterData, config.data) // 将配置数据合并到front Matter对象 + mark = true + } + + // 有操作时才继续 + if (mark) { + if (matterData.date && type(matterData.date) === 'date') { + matterData.date = repairDate(matterData.date) // 修复时间格式 + } + const newData = + jsonToYaml + .stringify(matterData) + .replace(/\n\s{2}/g, '\n') + .replace(/"/g, '') + + '---\r\n' + + fileMatterObj.content + fs.writeFileSync(file.filePath, newData) // 写入 + log(chalk.green(`update frontmatter:${file.filePath} `)) + } + }) +} diff --git a/utils/modules/fn.js b/utils/modules/fn.js new file mode 100644 index 00000000..a4654f18 --- /dev/null +++ b/utils/modules/fn.js @@ -0,0 +1,25 @@ +// 类型判断 +exports.type = function (o) { + var s = Object.prototype.toString.call(o) + return s.match(/\[object (.*?)\]/)[1].toLowerCase() +} + +// 修复date时区格式的问题 +exports.repairDate = function (date) { + date = new Date(date) + return `${date.getUTCFullYear()}-${zero(date.getUTCMonth() + 1)}-${zero(date.getUTCDate())} ${zero( + date.getUTCHours() + )}:${zero(date.getUTCMinutes())}:${zero(date.getUTCSeconds())}` +} + +// 日期的格式 +exports.dateFormat = function (date) { + return `${date.getFullYear()}-${zero(date.getMonth() + 1)}-${zero(date.getDate())} ${zero(date.getHours())}:${zero( + date.getMinutes() + )}:${zero(date.getSeconds())}` +} + +// 小于10补0 +function zero(d) { + return d.toString().padStart(2, '0') +} \ No newline at end of file diff --git a/utils/modules/readFileList.js b/utils/modules/readFileList.js new file mode 100644 index 00000000..74a4eb6b --- /dev/null +++ b/utils/modules/readFileList.js @@ -0,0 +1,49 @@ +/** + * 读取所有md文件数据 + */ +const fs = require('fs') // 文件模块 +const path = require('path') // 路径模块 +const docsRoot = path.join(__dirname, '..', '..', 'docs') // docs文件路径 + +function readFileList(dir = docsRoot, filesList = []) { + const files = fs.readdirSync(dir) + files.forEach((item, index) => { + let filePath = path.join(dir, item) + const stat = fs.statSync(filePath) + if (stat.isDirectory() && item !== '.vuepress') { + readFileList(path.join(dir, item), filesList) //递归读取文件 + } else { + if (path.basename(dir) !== 'docs') { + // 过滤docs目录级下的文件 + + const filename = path.basename(filePath) + const fileNameArr = filename.split('.') + const firstDotIndex = filename.indexOf('.') + const lastDotIndex = filename.lastIndexOf('.') + + let name = null, + type = null + if (fileNameArr.length === 2) { + // 没有序号的文件 + name = fileNameArr[0] + type = fileNameArr[1] + } else if (fileNameArr.length >= 3) { + // 有序号的文件(或文件名中间有'.') + name = filename.substring(firstDotIndex + 1, lastDotIndex) + type = filename.substring(lastDotIndex + 1) + } + + if (type === 'md') { + // 过滤非md文件 + filesList.push({ + name, + filePath + }) + } + } + } + }) + return filesList +} + +module.exports = readFileList